Java多线程——线程之间的协做

Java多线程——线程之间的协做

摘要:本文主要学习多线程之间是如何协做的,以及如何使用wait()方法与notify()/notifyAll()方法。html

部份内容来自如下博客:java

https://www.cnblogs.com/hapjin/p/5492645.html安全

https://www.cnblogs.com/sharpxiajun/p/2295677.html多线程

https://www.cnblogs.com/90zeng/p/java_multithread_2.html并发

为何线程之间须要进行协做

在多线程并发的状况下,若是都对共享资源进行操做,那么会致使线程安全问题,因此咱们使用线程的同步机制来保证多线程环境下程序的安全性,可是使用同步机制只能保证线程安全,并不能在两个线程或者多个线程之间自由切换,线程的切换彻底受CPU的影响。ide

若是使用同步机制让两个线程交替打印1到10的数字,代码以下:学习

 1 public class Demo {
 2     public static void main(String[] args) {
 3         DemoThread demoThread = new DemoThread();
 4         Thread a = new Thread(demoThread, "线程A");
 5         Thread b = new Thread(demoThread, "线程B");
 6         a.start();
 7         b.start();
 8     }
 9 }
10 
11 class DemoThread implements Runnable {
12     private int num = 1;
13 
14     @Override
15     public void run() {
16         while (num <= 10) {
17             synchronized (DemoThread.class) {
18                 if (num <= 10) {
19                     System.out.println(Thread.currentThread().getName() + " >>> " + num++);
20                 }
21             }
22         }
23     }
24 }

运行结果以下:this

 1 线程A >>> 1
 2 线程A >>> 2
 3 线程A >>> 3
 4 线程A >>> 4
 5 线程B >>> 5
 6 线程B >>> 6
 7 线程B >>> 7
 8 线程B >>> 8
 9 线程B >>> 9
10 线程B >>> 10

结果说明:spa

由于两个线程的调度彻底受CPU时间片的影响,只有当一个线程运行时间结束后,另外一个线程才能运行,并不能实如今线程运行的过程当中进行切换。线程

若是咱们想让两个线程交替打印数字,那么很显然同步机制是作不到的,这时候就须要两个线程的协做,让两个线程之间进行通讯。

线程的等待唤醒机制

要达到上面所说的两个线程交替打印,须要两个线程进行通讯,当第一个线程打印了以后,把本身锁起来,唤醒第二个线程打印,当第二个线程打印以后,也把本身锁起来,唤醒第一个线程,这样就能够实现两个线程的交替打印了。

线程的协做就是线程的通讯,好比有A和B两个线程,A和B均可以独立运行,A和B有时也会作信息的交换,这就是线程的协做了。在Java里线程的协做是经过Object类里的wait()和notify()/和notifyAll()来实现的。

涉及的方法

◆ wait()

该方法会致使当前线程等待,直到其余线程调用了此线程的notify()或者notifyAll()方法。注意到wait()方法会抛出异常,因此在面咱们的代码中要对异常进行捕获处理。

◆ wait(long timeout)

该方法与wait()方法相似,惟一的区别就是在指定时间内,若是没有notify或notifAll方法的唤醒,也会自动唤醒。wait(0)等效于wait()。

◆ nofity()

唤醒线程池中任意一个线程。

◆ notifyAll()

唤醒线程池中的全部线程。

方法的使用说明

这些方法都必须定义在同步中。由于这些方法是用于操做线程状态的方法,因此必需要明确到底操做的是哪一个锁上的线程。

注意到上述操做线程的方法都是放在Object类中,这是由于方法都是同步锁的方法。而锁能够是任意对象,任意的对象均可以调用的方法必定定义在Object类中。

这些方法属于Object类的一部分而不是Thread类的一部分,这个咋看一下真的很奇怪,不过细细思考下,这种作法是有道理的,咱们把锁机制授予对象会帮咱们扩展线程应用的思路,至少把这几个方法用在任何的具备同步控制功能的方法中,而不用去考虑方法所在的类是继承了Thread仍是实现了Runnable接口。
可是事实上使用这些方法仍是要注意只能在同步控制方法同步块里调用,由于这些操做都会使用到锁。

若是是在非同步的方法里调用这些方法,程序会编译经过,可是在运行时候程序会报出IllegalMonitorStateException异常,这个异常的含义是调用方法的线程在调用这些方法前必须拥有这个对象的锁,或者当前调用方法的对象锁不是以前同步时的那个锁

使用唤醒等待实现两个线程交替执行

代码以下:

 1 public class Demo {
 2     public static void main(String[] args) {
 3         DemoThread demoThread = new DemoThread();
 4         Thread a = new Thread(demoThread, "线程A");
 5         Thread b = new Thread(demoThread, "线程B");
 6         a.start();
 7         b.start();
 8     }
 9 }
10 
11 class DemoThread implements Runnable {
12     private Integer num = 1;
13 
14     @Override
15     public void run() {
16         while (true) {
17             synchronized (DemoThread.class) {
18                 DemoThread.class.notify();
19                 if (num <= 10) {
20                     System.out.println(Thread.currentThread().getName() + " >>> " + num++);
21                     try {
22                         DemoThread.class.wait();
23                     } catch (InterruptedException e) {
24                         e.printStackTrace();
25                     }
26                 }
27             }
28         }
29     }
30 }

运行结果以下:

 1 线程A >>> 1
 2 线程B >>> 2
 3 线程A >>> 3
 4 线程B >>> 4
 5 线程A >>> 5
 6 线程B >>> 6
 7 线程A >>> 7
 8 线程B >>> 8
 9 线程A >>> 9
10 线程B >>> 10

线程的虚假唤醒

在使用wait()时,当被唤醒时有可能会被虚假唤醒,建议使用while而不是if来进行判断,即在循环中使用wait()方法。

在下面的代码中,没有在循环中使用wait()方法:

 1 public class Demo {
 2     public static void main(String[] args) {
 3         DemoThread demoThread = new DemoThread();
 4         
 5         Thread a = new Thread(new Runnable() {
 6             @Override
 7             public void run() {
 8                 for (int i = 0; i < 4; i++) {
 9                     demoThread.increase();
10                 }
11             }
12         }, "线程A");
13         Thread b = new Thread(new Runnable() {
14             @Override
15             public void run() {
16                 for (int i = 0; i < 4; i++) {
17                     demoThread.decrease();
18                 }
19             }
20         }, "线程B");
21         Thread c = new Thread(new Runnable() {
22             @Override
23             public void run() {
24                 for (int i = 0; i < 4; i++) {
25                     demoThread.increase();
26                 }
27             }
28         }, "线程C");
29         Thread d = new Thread(new Runnable() {
30             @Override
31             public void run() {
32                 for (int i = 0; i < 4; i++) {
33                     demoThread.decrease();
34                 }
35             }
36         }, "线程D");
37         a.start();
38         b.start();
39         c.start();
40         d.start();
41     }
42 }
43 
44 class DemoThread {
45     private static Integer num = 1;
46     
47     public synchronized void increase() {
48         if (num > 0) {
49             try {
50                 this.wait();
51             } catch (InterruptedException e) {
52                 e.printStackTrace();
53             }
54         }
55         num++;
56         System.out.print(num + " ");
57         this.notifyAll();
58     }
59     
60     public synchronized void decrease() {
61         if (num == 0) {
62             try {
63                 this.wait();
64             } catch (InterruptedException e) {
65                 e.printStackTrace();
66             }
67         }
68         num--;
69         System.out.print(num + " ");
70         this.notifyAll();
71     }
72 }

运行结果以下:

1 0 1 0 1 2 1 0 1 2 1 0 1 2 1 2 1

能够看到即使使用了synchronized关键字,仍然出现了线程安全问题,缘由以下:

在某一刻,一个负责增长的线程得到了资源,此时num为1,因此执行 this.wait(); 并等待。

下一刻,另外一个负责增长的线程得到了资源,此时num仍然为1,因此再次执行 this.wait(); 并等待。

此后负责减小的线程将num减小到0并唤醒全部等待进程,两个负责增长的线程被唤醒,执行两次增长运算,致使num为2的状况产生。

解决办法就是将 if (num > 0) { 和 if (num == 0) { 中的if换成while。

wait()和sleep()方法的区别

两个方法声明的位置不一样:Thread类中声明sleep() ,Object类中声明wait()。

使用方法不一样:wait()能够指定时间,也能够不指定时间,sleep()必须指定时间。

调用的要求不一样:sleep()能够在任何须要的场景下调用, wait()必须使用在同步代码块或同步方法中。

关因而否释放同步监视器:若是两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。

生产者消费者问题

 1 public class Demo {
 2     public static void main(String[] args) {
 3         Clerk clerk = new Clerk();
 4         Productor productor = new Productor(clerk);
 5         Consumer consumer = new Consumer(clerk);
 6         Thread productor1 = new Thread(productor, "生产者1");
 7         Thread productor2 = new Thread(productor, "生产者2");
 8         Thread consumer1 = new Thread(consumer, "消费者1");
 9         Thread consumer2 = new Thread(consumer, "消费者2");
10         productor1.start();
11         productor2.start();
12         consumer1.start();
13         consumer2.start();
14     }
15 }
16 
17 class Clerk {// 店员
18     private int num = 0;// 产品数量
19 
20     public synchronized void product() {// 生产产品
21         if (num < 2000) {
22             System.out.println(Thread.currentThread().getName() + ":生产了第" + ++num + "个产品");
23             notifyAll();
24         } else {
25             try {
26                 wait();
27             } catch (InterruptedException e) {
28                 e.printStackTrace();
29             }
30         }
31     }
32 
33     public synchronized void consume() {// 消费产品
34         if (num > 0) {
35             System.out.println(Thread.currentThread().getName() + ":消费了第" + num-- + "个产品");
36             notifyAll();
37         } else {
38             try {
39                 wait();
40             } catch (InterruptedException e) {
41                 e.printStackTrace();
42             }
43         }
44     }
45 }
46 
47 class Productor implements Runnable {// 生产者线程
48     Clerk clerk;
49 
50     public Productor(Clerk clerk) {
51         this.clerk = clerk;
52     }
53 
54     @Override
55     public void run() {
56         System.out.println(Thread.currentThread().getName() + "开始生产产品");
57         while (true) {
58             clerk.product();
59         }
60     }
61 }
62 
63 class Consumer implements Runnable {// 消费者线程
64     Clerk clerk;
65 
66     public Consumer(Clerk clerk) {
67         this.clerk = clerk;
68     }
69 
70     @Override
71     public void run() {
72         System.out.println(Thread.currentThread().getName() + "开始消费产品");
73         while (true) {
74             clerk.consume();
75         }
76     }
77 }
相关文章
相关标签/搜索