知识回顾开始学习之前先看一个经典的卖票例子:假设有3个售票员同时在卖50张票

线程通信,线程八锁,看完必会_发短信

执行结果:


B卖出了第5张票,剩余4张票
B卖出了第4张票,剩余3张票
B卖出了第9张票,剩余8张票
B卖出了第8张票,剩余7张票
A卖出了第11张票,剩余10张票
A卖出了第6张票,剩余5张票
省略.........

很明显不是我们想看到的结果,说明多个线程进行了资源的争夺,那么我们传统的解决方式就是用synchronized修饰:

线程通信,线程八锁,看完必会_发短信_02

再次执行:



省略........
B卖出了第6张票,剩余5张票
C卖出了第5张票,剩余4张票
C卖出了第4张票,剩余3张票
B卖出了第3张票,剩余2张票
A卖出了第2张票,剩余1张票
B卖出了第1张票,剩余0张票

这样就可以解决了,但是synchronized只是一个关键字,并没有用到juc包下的方法,那么我们就使用juc包下的方法尝试一下 >>

线程通信,线程八锁,看完必会_线程通信_03

synchronized和Lock的区别:

1. synchronized是一个关键字,lock是一个java类;

2. synchronized无法获取锁的状态,lock可以判断是否获取到了锁;

3. synchronized自动释放锁,lock必须手动释放锁;

4. synchronized假设有线程1(获得锁,阻塞),线程2(等待,傻傻的等),lock锁就不一定,它自带一个方法lock.tryLock()可以尝试获取锁;

5. synchronized是可重入锁,不可以中断的,非公平,lock可重入锁,可自定义设置是否为公平、非公平锁;

6. synchronized适合锁少量的代码同步问题,lock适合锁大量的同步代码。




线程间的通信问题

    线程间的通信问题,即生产者和消费者的问题,是一个重要的知识点。我们这里做一个测试:假设有A、B两个线程操作同一个变量num,A线程加1,B线程减1,且必须是A加完后通知B去减,线程交替执行

一、synchronized版

老版本的线程通信使用synchronized配合wait和notifyAll使用:

线程通信,线程八锁,看完必会_同步方法_04

执行结果:


A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
.....

当只有两个线程时是没问题的,如果增加两个线程C、D再去执行:

线程通信,线程八锁,看完必会_同步方法_05

执行结果:




A=>1
B=>0
C=>1
A=>2
C=>3
B=>2
C=>3
D=>2
D=>1
省略部分结果.......

由此可见,上面的代码是有问题的,那么问题处在哪儿呢?看一下官网文档:

线程通信,线程八锁,看完必会_同步方法_06

这就是经典的虚假唤醒问题,解决方式也很简单,把上面的 if 修改为while 即可。 线程通信,线程八锁,看完必会_发短信_07 【原因分析】1. 当num=0时,A线程进入increment()方法,跳过if判断,执行加1操作;2. 此时正好C线程也进入increment()方法,这时候num=1,进入if语句块,执行wait()方法等待;3. A线程加1之后,执行了notifyAll()方法,唤醒了其他线程,那么这时候C线程直接跳出if语句块向下执行加1操作,num=2;4. 如果是while循环,则C线程被唤醒后,继续进行判断,num = 1,while(num !=0) 条件成立,重新进入wait()方法等待。    当一个条件满足时,很多线程都被唤醒了,但是只有其中部分是有用的唤醒,其它的唤醒都是无用功。比如说买货,如果商品本来没有货物,突然进了一件商品,这是所有的线程都被唤醒了,但是只能一个人买,所以其他人都是假唤醒,获取不到对象的锁。    在if块中使用wait方法,是非常危险的,因为一旦线程被唤醒,并得到锁,就不会再判断if条件,而执行if语句块外的代码,所以建议,凡是先要做条件判断,再wait的地方,都使用while循环来做。二、juc版线程通信在java.util.concurrent.locks包下有三个接口:


  • Lock

  • Condition

  • ReadWriteLock


    其中Conditon就是juc为我们提供的线程通信的接口,它也有对应的等待和唤醒的方法:await()与signal()、signalAll()

    上述synchronized版虽然实现了线程间的通信,但可以发现各线程执行是随机的,不能指定线程唤醒,这就是juc版与它的不同。    代码测试:创建A、B、C三个线程,交替打印,即A执行完调用B,B执行完调用C,C执行完调用A,顺序执行:

线程通信,线程八锁,看完必会_线程通信_08

执行结果:




A===>AAA
B===>BBB
C===>CCC
A===>AAA
B===>BBB
C===>CCC
省略.........

这样就可以通过Condition实现精准通知唤醒!!!



线程八锁

    多线程里有个问题很基础但很多人搞不懂:锁是什么?锁到底锁的是谁?由此引申出线程八锁来彻底搞懂锁的问题,线程八锁其实就是8个问题,搞懂这8个问题,就能彻底搞懂锁。1. 标准情况下,两个线程先打印发短信还是打电话?

线程通信,线程八锁,看完必会_线程通信_09

答案:发短信、打电话线程通信,线程八锁,看完必会_发短信_07​ 【原因分析】

知识点:synchronized锁的对象是方法的调用者!因为两个方法用的是同一个锁,谁先抢到谁执行。

2. sendSms()延迟4秒,两个线程先打印发短信还是打电话?

线程通信,线程八锁,看完必会_线程通信_11

答案:发短信、打电话

线程通信,线程八锁,看完必会_发短信_07 【原因分析】

与上述问题一样。

3. 线程B调用普通方法hello(),先打印发短信还是hello?

线程通信,线程八锁,看完必会_线程通信_13

答案:发短信、hello

线程通信,线程八锁,看完必会_发短信_07​ 【原因分析】

hello()方法是一个普通方法,没有锁,不受锁的影响,因为sendSms()要睡4秒,所以先打印hello。

4. 两个对象,两个同步方法,先打印发短信还是打电话?

线程通信,线程八锁,看完必会_发短信_15

答案:打电话、发短信

线程通信,线程八锁,看完必会_发短信_07​ 【原因分析】

虽然两个方法是同步方法,但是为不同的对象调用,各自持有的锁不同,也不存在锁的竞争问题。

5. 增加两个静态方法,同一个对象调用,先打印发短信还是打电话?

线程通信,线程八锁,看完必会_发短信_17

答案:发短信、打电话

线程通信,线程八锁,看完必会_发短信_07​ 【原因分析】

a:两个方法调用使用的是同一个对象,存在锁的竞争

b:被static修饰的方法,在类加载的时候就有了,所以这里锁的是Phone3这个类

6. 两个对象,两个静态方法,先打印发短信还是打电话?

线程通信,线程八锁,看完必会_同步方法_19

答案:发短信、打电话

线程通信,线程八锁,看完必会_发短信_07​ 【原因分析】

加了static修饰后,锁的是Class,虽然是两个对象调用,但两个对象的类模板只有一个,还是存在锁的竞争问题。7. 一个静态同步方法,一个普通同步方法,一个对象,先打印发短信还是打电话?

线程通信,线程八锁,看完必会_线程通信_21

答案:打电话、发短信

线程通信,线程八锁,看完必会_发短信_07​ 【原因分析】

静态同步方法sendSms()锁的是Class,普通同步方法call()锁的是调用者,二者持有的锁不同,不存在锁的竞争。8. 一个静态同步方法,一个普通同步方法,两个对象,先打印发短信还是打电话?

线程通信,线程八锁,看完必会_同步方法_23

答案:打电话、发短信

线程通信,线程八锁,看完必会_发短信_07​ 【原因分析】

理由同上。


下一篇:并发下的集合类


本文中涉及到的所有代码已上传到gitee和github,地址在公众号窗口 我的->git 查看相关内容