黑马程序员 —— Java多线程2 (第十二天)

来源:互联网 发布:淘宝包邮拒签邮费谁出 编辑:程序博客网 时间:2024/06/05 16:17

------- android培训、java培训、期待与您交流! ----------

一   线程间通信1 - 示例代码


线程间的通信:其实就是多个线程操作同一个资源,但操作的动作不同。

之前写的程序,每个线程运行的代码都是一致的。

而现在需要线程之间通信,它们代码要不一致。

A线程往资源写数据,B线程往资源读数据,同时运行。

这样就有两个run方法同时运行,也就是有两个类。


Input类对象存入数据 → 对象(String name,String Sex) → Output类对象读取数据

首先,资源要描述一下,Input和Output要描述一下。


package Test3;class Res {public String name;public String sex;}class Input implements Runnable {Res res;boolean flag = true;public Input(Res res) {this.res = res;}public void run() {while (true) {if (flag == true) {res.name = "麦克";res.sex = "男";flag = false;} else {res.name = "May";res.sex = "girl";flag = true;}}}}class Output implements Runnable {Res res;public Output(Res res) {this.res = res;}public void run() {while (true) {System.out.println(res.name + " --- " + res.sex);}}}public class ThreadTongXin {public static void main(String[] args) {Res res = new Res();new Thread(new Input(res)).start();new Thread(new Output(res)).start();}}

程序输出:

麦克 --- girl麦克 --- 男May --- girl麦克 --- 男麦克 --- girlMay --- 男

为什么会出现输出的数据混乱,而且一下子输出一大片同一个人数据的情况呢?

原因:一下子出一大片 的原因是Outpu输出完,不一定是Input获取到CPU执行权,

可以继续是Output获取到,连续都获取到了,就会出现一大片一个人的数据了。

另外,Input往Res类的对象res里刚存完name,Output就获取到CPU执行权,

然后就输出了,性别的数据也就混乱了。


二   线程间通信2 - 解决安全问题


当然是“同步”。

两个类的while(true)中加上同步代码块,发现还是不行。

此时应该想到,同步需要满足两个前提:

1.这个程序只同步了一个线程,另外一个没有同步,而它们处理的是同一资源;

2.不是同一个锁,改为内存中唯一的对象,例如:Input.class,Res.class,res都行。

class Input implements Runnable {Res res;boolean flag = true;public Input(Res res) {this.res = res;}public void run() {while (true) {synchronized(Res.class){   //注意这里if (flag == true) {res.name = "麦克";res.sex = "男";flag = false;} else {res.name = "May";res.sex = "girl";flag = true;}}}}}class Output implements Runnable {Res res;public Output(Res res) {this.res = res;}public void run() {while (true) {synchronized(Res.class){  //注意这里System.out.println(res.name + " --- " + res.sex);}}}}

为节省篇幅,仅截取Input和Ouput两个被修改类的代码。


三   线程间通信3 - 等待唤醒机制


现在需要将程序修改为:一个写入后,等另一个输出才继续写入。

为了满足需求,可以再Res类中加入标记,用于标记Input类的对象是否已写入新值。


模拟执行过程:

Input对象判断标记为false,无新值,写入并将标记改为true,Input对象再输入判断到为true,就等着。

Output对象判断,标记为true有内容,输出然后修改标记为false,唤醒Input的对象,它自己等着。


等待的线程在哪里?

线程运行时,内存中会建立线程池,等待的线程就存放在线程池中,

notify唤醒线程池中的线程,通常唤醒第一个wait的。


notify的原理,可以用“抓人游戏”做类比。

如果想唤醒线程池中全部正在wait的线程,可以用notifyAll()方法。


注意:wait()、notify()、notifyAll()是定义在Object类中的方法,

观察一下这三个方法在API文档中的描述。







注意文档中“必须拥有对象监视器”和“在此对象监视器上”的描述,

这表明,这三个方法,必须用在 synchronized“同步”当中,不然会抛异常,

这里十分重要,因为这个原因,上机浪费了一个多半小时!!!

参考: IllegalMonitorStateException 异常例子的解决方法

而且,必须标示出wait()方法所操作的线程所属的锁。

为什么?因为同步有可能会出现嵌套。


Q:为什么定义在Object类?

A:因为“锁”可以是任意对象,任意对象能调用的方法,当然是要在Object类中。


示例代码:

/*线程间通讯:其实就是多个线程在操作同一个资源,但是操作的动作不同。 */import java.lang.*;class Res {String name;String sex;boolean flag = false;}class Input implements Runnable {private Res r;Input(Res r) {this.r = r;}public void run() {int x = 0;while (true) {synchronized (r) {if (r.flag) {try {r.wait();} catch (Exception e) {}}if (x == 0) {r.name = "mike";r.sex = "man";} else {r.name = "丽丽";r.sex = "女女女女女";}x = (x + 1) % 2;r.flag = true;r.notify();}}}}class Output implements Runnable {private Res r;Output(Res r) {this.r = r;}public void run() {while (true) {synchronized (r) {if (!r.flag) {try {r.wait();} catch (Exception e) {}}System.out.println(r.name + "    " + r.sex);r.flag = false;r.notify();}}}}public class Demo {public static void main(String args[]) {Res r = new Res();Input in = new Input(r);Output out = new Output(r);Thread t1 = new Thread(in);Thread t2 = new Thread(out);t1.start();t2.start();}}


四   线程间通信4 - 代码优化

class Res {private String name;private String sex;private boolean flag = false;public synchronized void set(String name, String sex) {if (this.flag) {try {this.wait();} catch (Exception e) {}}this.name = name;this.sex = sex;this.flag = true;this.notify();}public synchronized void out() {if (!this.flag) {try {this.wait();} catch (Exception e) {}}System.out.println(this.name + "    " + this.sex);this.flag = false;this.notify();}}class Input implements Runnable {private Res r;Input(Res r) {this.r = r;}public void run() {int x = 0;while (true) {if (x == 0) {r.set("mike", "man");} else {r.set("丽丽", "女女女女女");}x = (x + 1) % 2;}}}class Output implements Runnable {private Res r;Output(Res r) {this.r = r;}public void run() {while (true) {r.out();}}}public class Demo {public static void main(String args[]) {Res r = new Res();new Thread(new Input(r)).start();new Thread(new Output(r)).start();}}

加上private和getter、setter方法赋值时需要同步,因为可能出现安全问题。


五   线程间通信5 - 生产者消费者问题



上面的示例代码,输出的是姓名,不好分辨。

现在通过生产者消费者问题(生产一个、消费一个……”)来进行说明。

通过带编号产品(新产品编号在setter方法中+1)来进行区分。


Q:单个生产单个消费没问题,那如果换为多个生产多个消费呢?

  可能会出现生产一个,消费两个的情况,或者生产两个消费一个,为什么呢?

A:因为没判断标记flag,只要将标记判断的if改为while就可以了,

  但会全部等待了。还要notifyAll()。


package Test3;class Resource {private String name;private int conut = 1;private boolean ResControlFlag = false;public synchronized void Set(String name) {while (true) {while (ResControlFlag) {try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}}this.name = name + this.conut++;System.out.println(Thread.currentThread().getName() + " --------> "+ this.name);ResControlFlag = true;this.notifyAll();}}public synchronized void Out() {while (true) {while (!ResControlFlag) {try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(Thread.currentThread().getName() + " <--- "+ this.name);ResControlFlag = false;this.notifyAll();}}}class Producer implements Runnable {private Resource res;public Producer(Resource res) {this.res = res;}public void run() {res.Set("电脑");}}class Consumer implements Runnable {private Resource res;public Consumer(Resource res) {this.res = res;}public void run() {res.Out();}}public class ProducerConsumerDemo {public static void main(String[] args) {Resource res = new Resource();new Thread(new Producer(res), "生产者").start();new Thread(new Consumer(res), "消费者").start();new Thread(new Producer(res), "生产者").start();new Thread(new Consumer(res), "消费者").start();new Thread(new Producer(res), "生产者").start();new Thread(new Consumer(res), "消费者").start();}}


Q:对于多个生产者和消费者,为什么要定义while判断标记?

A:为了让被唤醒的线程再一次判断标记。


Q:为什么要使用notifyAll?

A:因为需要唤醒对方线程,因为只用notify,

  容易出现只唤醒本方线程的情况,导致程序中的所有线程都等待。


六   线程间通信6 - 生产者消费者问题(升级版)



Q:这时候,问题就来了,notifyAll会唤醒在此对象监视器上等待的所有线程,

  怎样才能“不唤醒自己,而只唤醒其他线程呢?”

A:JDK1.5中提供了现成的升级解决方案:将“同步synchronized”替换成“显式锁Lock”操作,

   将 object 中的 wait ,notify,notifyAll,替换成了condition 对象。


public interface Lock

Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。

Lock这个接口,把隐式的加锁,变成显式的加锁了。


public interface Condition :

Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,

以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set(wait-set)。

其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。


synchronized  →  Lock

Condition   →   wait、notify 和 notifyAll


示例代码:

import java.util.concurrent.locks.*;class Demo {public static void main(String args[]) {Resource r = new Resource();Producer pro = new Producer(r);Consumer con = new Consumer(r);Thread t1 = new Thread(pro);Thread t3 = new Thread(pro);Thread t2 = new Thread(con);Thread t4 = new Thread(con);t1.start();t2.start();t3.start();t4.start();}}class Resource {private String name;private int count = 1;private boolean flag = false;private Lock lock = new ReentrantLock();private Condition condition_pro = lock.newCondition();private Condition condition_con = lock.newCondition();public void set(String name) throws InterruptedException {lock.lock();try {while (flag) {condition_pro.await();}this.name = name + "--" + count++;System.out.println(Thread.currentThread().getName() + "...生产者..."+ this.name);flag = true;condition_con.signal();} finally {lock.unlock();}}public void out() throws InterruptedException {lock.lock();try {while (!flag) {condition_con.await();}System.out.println(Thread.currentThread().getName()+ "...消费者..........." + this.name);flag = false;condition_pro.signal();} finally {lock.unlock();}}}class Producer implements Runnable {private Resource res;Producer(Resource res) {this.res = res;}public void run() {while (true) {try {res.set("+商品+");} catch (InterruptedException e) {}}}}class Consumer implements Runnable {private Resource res;Consumer(Resource res) {this.res = res;}public void run() {while (true) {try {res.out();} catch (InterruptedException e) {}}}}

更多用法和说明直接看API文档就行了,说得很清楚。

七   线程停止


查阅API文档,Thread类的stop()方法已经过时。

那我们应该如何让线程停止呢?    

停止线程只有一种方法,就是让run()方法结束。


让run()方法结束的两种情况:

1.通常情况:多线程运行,运行代码通常是循环结构。只要控制循环,就可以让run方法结束,也就是线程结束。

2.特殊情况:当线程处于冻结状态,就不会读取到标记,那么线程就不会结束。  

            当没有指定的方式让冻结的线程恢复到运行状态时,这时需要对冻结状态进行清除。

            强制让线程恢复到运行状态中来,这样就可以操作标记让线程结束。

            使用interrupt()方法,将处于冻结状态的线程强制的恢复到运行状态中来

class Demo {public static void main(String args[]) {StopThread st = new StopThread();Thread t1 = new Thread(st);Thread t2 = new Thread(st);t1.start();t2.start();int num = 0;while (true) {if (num++ == 60) {st.changeFlag();t1.interrupt();t2.interrupt();break;}System.out.println(Thread.currentThread().getName() + num);}}}class StopThread implements Runnable {private boolean flag = true;public void run() {while (flag) {System.out.println(Thread.currentThread().getName() + "....run");}}public void changeFlag() {flag = false;}}



八   守护线程


守护线程:可以理解为“后台线程”,把线程标记为“后台线程”后,

         它开启、运行等等状态跟“前台线程”都没有区别,就结束有区别,

          当所有”前台线程”都结束后,”后台线程”会自动结束。


void  setDaemon(boolean on) :将该线程标记为守护线程或用户线程。


知识点:

1.setDeamon(true)设为守护线程

2.全剩下守护线程,程序结束


九   Join方法


join()方法:

把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。

比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。

class Demo {public static void main(String args[]) throws Exception {StopThread st = new StopThread();Thread t1 = new Thread(st);Thread t2 = new Thread(st);t1.start();t1.join();t2.start();t2.join();for (int x = 0; x < 70; x++) {System.out.println(Thread.currentThread().getName() + "    " + x);}System.out.println("over");}}class StopThread implements Runnable {public void run() {for (int x = 0; x < 70; x++) {System.out.println(Thread.currentThread().getName() + "    " + x);}}}

join()方法,抢先获得CPU的执行权,先于主线程执行,

例如,t1.join();。此时,主线程处于冻结状态,只有t1可以运行;

只有t1运行结束,主线程才可以运行,等待让主线程让出执行权的线程死亡。一般用于临时加入线程。

join方法的位置会有影响,如果放在多个线程开启的下面,例如,t1.start();t2.start();t1.join();,

t1和t2会交替执行,因为t1抢的是主线程的运行权,和t2无关。主线程依然在t1结束后运行,和t2的执行无关。

当A线程执行到了B线程的join方法时,A线程就会等待,等B线程都执行完,A线程才会执行。可以用来临时加入线程执行。可以嵌套使用。

如果t1等待,会造成主线程无法运行,所以使用interrupt方法来结束冻结状态,继续运行。

因为是强制恢复运行,所以可能会出现异常,所以join方法会抛出InterruptedException异常。

开启线程的线程组,称为主线程组,里面的成员构成一个线程组。也可以通过ThreadGroup创建新的对象,封装进其他的组。很少用。



十   优先级和yield方法

1、关于线程的优先级

线程的执行有优先级,可通过setPriority(int newPriority)方法更改线程的优先级。

优先级:代表抢资源的频率,设置越高,执行越频繁,优先执行。

所有线程的优先级,包括主线程,优先级默认设置为5,共有10级,数字越大,越优先执行。

如果数字相差不大,优先程度几乎没有差别,只有1、5、10之间最明显,

分别对应Thread类的三个字段:MIN_PRIORITY、NORM_PRIORITY、MAX_PRIORITY

虽然可以设为最高级,也只是相对高而已,不可能只执行一个线程。


2、yield方法

Thread.yield();暂停当前正在执行的线程对象,并执行其他线程。

多个线程运行时,通过临时强制释放线程的执行权,使得其他线程可以执行。

这样可以减缓线程的运行,使得线程都有机会运行。


3、开发时如何编写线程

Q:什么时候用多线程?

A:当某些代码需要同时运行时,可以用单独的线程进行封装。

      也可以封装成类,或者对象。


示例代码:

 new Thread()//通过Thread类建立匿名类{       public void run()       {              for (int x=0;x<30;x++ )              {                     System.out.println(Thread.currentThread().getName()+"...Jobs..."+x);              }       }}.start();
Runnable r = new Runnable()//通过Runnable接口建立匿名类对象{       public void run()       {              for (int x=0;x<30;x++ )              {                     System.out.println(Thread.currentThread().getName()+"...Intel..."+x);              }       }};new Thread(r).start();


------- android培训、java培训、期待与您交流! ----------

0 0
原创粉丝点击