知识回顾开始学习之前先看一个经典的卖票例子:假设有3个售票员同时在卖50张票
执行结果:
B卖出了第5张票,剩余4张票
B卖出了第4张票,剩余3张票
B卖出了第9张票,剩余8张票
B卖出了第8张票,剩余7张票
A卖出了第11张票,剩余10张票
A卖出了第6张票,剩余5张票
省略.........
很明显不是我们想看到的结果,说明多个线程进行了资源的争夺,那么我们传统的解决方式就是用synchronized修饰:
再次执行:
省略........
B卖出了第6张票,剩余5张票
C卖出了第5张票,剩余4张票
C卖出了第4张票,剩余3张票
B卖出了第3张票,剩余2张票
A卖出了第2张票,剩余1张票
B卖出了第1张票,剩余0张票
这样就可以解决了,但是synchronized只是一个关键字,并没有用到juc包下的方法,那么我们就使用juc包下的方法尝试一下 >>
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使用:
执行结果:
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
.....
当只有两个线程时是没问题的,如果增加两个线程C、D再去执行:
执行结果:
A=>1
B=>0
C=>1
A=>2
C=>3
B=>2
C=>3
D=>2
D=>1
省略部分结果.......
由此可见,上面的代码是有问题的,那么问题处在哪儿呢?看一下官网文档:
这就是经典的虚假唤醒问题,解决方式也很简单,把上面的 if 修改为while 即可。 【原因分析】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,顺序执行:
执行结果:
A===>AAA
B===>BBB
C===>CCC
A===>AAA
B===>BBB
C===>CCC
省略.........
这样就可以通过Condition实现精准通知唤醒!!!
线程八锁
多线程里有个问题很基础但很多人搞不懂:锁是什么?锁到底锁的是谁?由此引申出线程八锁来彻底搞懂锁的问题,线程八锁其实就是8个问题,搞懂这8个问题,就能彻底搞懂锁。1. 标准情况下,两个线程先打印发短信还是打电话?
答案:发短信、打电话 【原因分析】
知识点:synchronized锁的对象是方法的调用者!因为两个方法用的是同一个锁,谁先抢到谁执行。
2. sendSms()延迟4秒,两个线程先打印发短信还是打电话?
答案:发短信、打电话
【原因分析】
与上述问题一样。
3. 线程B调用普通方法hello(),先打印发短信还是hello?
答案:发短信、hello
【原因分析】
hello()方法是一个普通方法,没有锁,不受锁的影响,因为sendSms()要睡4秒,所以先打印hello。
4. 两个对象,两个同步方法,先打印发短信还是打电话?
答案:打电话、发短信
【原因分析】
虽然两个方法是同步方法,但是为不同的对象调用,各自持有的锁不同,也不存在锁的竞争问题。
5. 增加两个静态方法,同一个对象调用,先打印发短信还是打电话?
答案:发短信、打电话
【原因分析】
a:两个方法调用使用的是同一个对象,存在锁的竞争
b:被static修饰的方法,在类加载的时候就有了,所以这里锁的是Phone3这个类
6. 两个对象,两个静态方法,先打印发短信还是打电话?
答案:发短信、打电话
【原因分析】
加了static修饰后,锁的是Class,虽然是两个对象调用,但两个对象的类模板只有一个,还是存在锁的竞争问题。7. 一个静态同步方法,一个普通同步方法,一个对象,先打印发短信还是打电话?
答案:打电话、发短信
【原因分析】
静态同步方法sendSms()锁的是Class,普通同步方法call()锁的是调用者,二者持有的锁不同,不存在锁的竞争。8. 一个静态同步方法,一个普通同步方法,两个对象,先打印发短信还是打电话?
答案:打电话、发短信
【原因分析】
理由同上。
下一篇:并发下的集合类
本文中涉及到的所有代码已上传到gitee和github,地址在公众号窗口 我的->git 查看相关内容