1、共享带来的问题

问题

image.png

分析

由于分时系统的线程切换会导致线程安全问题。 从字节码层面分析 image.png Java内存模型中的流程如下 image.png image.png

2、synchronized解决方案

阻塞式解决方法由synchronized和Lock。
image.png

image.pngimage.png image.png

3、方法上的synchronized

1.synchronized加在成员方法上
image.png

演变过程 image.pngimage.png

1.synchronized加在静态方法上
image.png

主要区别在锁住的对象变成了Test.class

4、变量的线程安全问题

变量的安全问题

成员变量和静态变量的线程安全问题

1.如果没被共享,则线程安全
2.如果被共享了,分两种情况
只有读操作,则线程安全
如果有读写操作,则需要考虑线程安全问题。

局部变量是否线程安全

局部变量是线程安全的,但局部变量的引用对象不一定。

局部变量的安全性分析

线程安全的场景

1. image.pngimage.png

2. image.pngimage.png

线程不安全的场景

image.png 此时会出现线程安全问题,由于创建了子类,无法控制子类可以访问父类中的哪个对象,可能造成局部变量的引用暴露给其他线程。 解决办法: image.png private在一定程度上保护线程安全(子类无法覆盖)。 使用final关键字修饰类,防止子类重写。

常见的线程安全类

线程安全类方法组合

image.png

image.png 不同的方法组合并非原子,原子方法组合使用无法保证原子性 image.png

不可变类线程安全性

image.png
String的substring如何保证线程安全
(substring() 方法返回字符串的子字符串:public String substring(int beginIndex) 或public String substring(int beginIndex, int endIndex))

image.png 如果String中有内容则创建一个新的String image.png 然后将原有内容复制给新的String。

实例

1.不安全实例

image.png userService是Servlet的成员变量,多个线程可能同时使用。

image.png 无scope说明其实单例对象,需要被共享,所以其成员变量同样是被共享的。 解决:可将start和end其改成环绕通知中的局部变量。

2.线程安全实例

image.png dao层的方法无成员变量,为线程安全。service调用dao(其中没有可更改的成员变量)是安全的。servlet中使用service,service中的成员变量为private且无修改的方法,是线程安全的。 反例: image.png Connection没有写成方法内的局部变量,其被多个线程共享,不安全了。 改进1: image.png在service中每次创建新的userdao实现 但是!不推荐这种方法,很浪费内存。最好还是将Connection作为userdao的方法中的局部变量。

image.png 虽然此处的sdf为局部变量,但是其所在类为抽象类,sdf被传递给了一个抽象方法foo,有可能foo的子类会影响sdf。 再次强调:不想暴露的方法记得设置private和finnal修饰符。 (例如String类的修饰符为finnalimage.png),其目的就是为了防止被子类覆盖其中的方法。

5、Monitor与synchronized优化

重量级锁(jdk6前常用)

https://www.bilibili.com/video/BV16J411h7Rd?p=76&spm_id_from=pageDriver

Java对象头 image.png image.png 00表示可normal,00轻量级锁、10重量级锁、11被GC回收 image.png

image.png

image.pngowner为thread2,thread1,3在Entrylist中阻塞 image.pngthread2执行完毕,owner更换为thread1 image.png image.png

轻量级锁(jdk6开始的优化)

image.png

image.png 流程: oberject reference记录锁住对象的地址,lock record记录对象的markword image.png image.png markword的state为00时才能替换成功,00表示其为normal状态 image.png cas替换失败情况分两种 image.png 情况2的图示,但是情况2的失败并不影响(锁重入),所以lockrecord会被创建为null,将来加了几次锁就统计lockrecord的数量,解锁一次就去掉一个lockrecord。 image.png解锁时取值为null(表示锁重入),重入计数-1→image.png 最后解锁的时候,需要吧state的00(有轻量级锁的状态)重新设为01(normal) image.png

锁膨胀

锁膨胀过程

image.png

流程: image.png 想去加锁,state为00,说明该对象已被加轻量级锁。 image.png 进入锁膨胀流程,thread1加轻量级锁失败,为Object对象申请Monitor锁,同时更改Object的markword为Monitor地址,state此时更改为10,Monitor的Owner只想t0的Object reference(锁住对象的地址)。 t1进入Monitor的Entrylist阻塞。 image.png t0退出时,state由00变成10,解锁失败,进入重量级锁解锁流程 image.png

自旋优化(多核cpu下)

image.png

即竞争锁失败时先不进入Entrylist区blocked,而是自身循环几次,等到锁释放。(避免阻塞的上下文切换)。

自旋成功

image.png

自旋失败

image.png

image.png

偏向锁

偏向锁的使用场景一般为冲突很少的情况,如果是在多线程经常竞争释放对象的情况下可以通过image.png来禁用。
image.png

流程: image.png markword中,basied_lock位为0表示没有启用偏向锁,为1表示启用了。 (程序刚启动时,其后三位不是101,需要等几秒或者禁用延迟才会是101) image.png

偏向锁的撤销场景

1.一个可用偏向锁的对象调用hashcode方法也会禁用掉偏向锁

image.png

2.有其他线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
3.调用wait/notify时也会撤销偏向锁(wait和notify只有重量级锁才有)

批量重偏向

image.png

markword的后三位会从101变为001再变为101。

批量撤销

image.png

锁消除

image.png

代码主要是为了比较加锁与不加锁的情况下的性能差异 image.png结果差异并不大。 JIT(即时编译器)会对热点代码(反复执行的代码)进行优化,在真正执行时可能JIT会将synchronized忽略。

6、wait(会释放锁)和notify

image.png

注意wait是已经获得过锁但又放弃了的线程,blocked是没有获得过锁的线程。

api

image.png

没有获得锁的直接调用wait方法会报错 image.png—->image.png image.png获得锁(成为owner之后才能wait)

wait(long n)在等待n毫秒的时间或被notify方法唤醒

notify(挑一个线程唤醒)和notifyall(唤醒所有在waitset中的线程)

wait和notify的使用

sleep和wait方法的区别

image.png 但sleep和wait的线程状态都是TIMED_WAITING(有时限等待)

虚假唤醒问题

线程1(小南) 线程2(小女)
image.pngimage.png
唤醒线程(外卖)
image.png—->image.png

分析:唤醒线程(外卖)错误的唤醒了线程1而非目标线程1。 方法1:(不太好) image.png—->image.png 可以使用notifyall()来唤醒全部线程,但是还是把线程1给唤醒了。 方法二: image.png—->image.png 将if判断改为while,线程1不满足条件时

总结:wait与notify的正确使用格式
image.png

设计模式

同步模式之保护性暂停

演示

(相比于jion方法的优点:①jion方法需要t2线程执行结束,,而通过这种设计模式t2线程不需要结束就传递结果后仍可以执行其他代码。②jion方法两个线程间传递的必须为全局变量,而此处可以为局部变量。)
image.png


线程1:
image.png
线程2:
image.png

线程1等待线程2的结果并打印其大小(通过complete方法传递结果并唤醒线程1) image.png—->image.png image.png

优化:不让t1线程无时间限制的等待t2线程,设置一个等待时间。
image.png

this.wait(n)参数不为timeout的原因: 不能设置为timeout,如果t1在15.00开始运行,等待时间timeout为2s就不等待。 如果有其他线程在15.00.00至15.00.02之间虚假唤醒了t1,t1则又要重新等待timeout时间。 所以应该设置为timeout-passedTime时间。

Join原理(Thread类的方法)

  1. public final synchronized void join(long millis)
  2. throws InterruptedException {
  3. long base = System.currentTimeMillis();
  4. long now = 0;
  5. if (millis < 0) {
  6. throw new IllegalArgumentException("timeout value is negative");
  7. }
  8. //此处调用wait方法,wait(0)实际上为一直无限等待
  9. if (millis == 0) {
  10. while (isAlive()) {
  11. wait(0);
  12. }
  13. //else部分的代码为典型的保护性暂停模式逻辑
  14. } else {
  15. while (isAlive()) {
  16. long delay = millis - now;
  17. if (delay <= 0) {
  18. break;
  19. }
  20. wait(delay);
  21. now = System.currentTimeMillis() - base;
  22. }
  23. }
  24. }

扩展

https://www.bilibili.com/video/BV16J411h7Rd?p=103&spm_id_from=pageDriver

异步模式之生产者/消费者

image.png


image.png定义Message消息对象
image.png定义一个容量为capacity的LinkedList消息队列
image.png定义消息队列的take方法获取消息(从尾部取)
image.png定义消息队列的put方法存入消息(从头部加)

image.png (此处用id存i的是因为lambda表达式中引用外部的局部变量必须为final,不能直接引用i(不断变化)。) image.png

7、Park和Unpark

基本使用

image.png

image.png

(park使线程进入WAIT状态)unpark在park之前和之后调用,都可以恢复park住的线程。

特点

image.png

原理

image.png

1、先调用park在unpark image.png—-> 调用Unsafe.park时,检查counter为0时,thread0获得mutex互斥锁后进入cond条件变量阻塞,再次将counter设置为0(即使先前为0仍需设置)。 image.png 调用Unsafe.unpark时,不检查直接将counter设为1,唤醒cond条件变量中的thread0,thread0恢复运行且再次将counter设为0。

2、先调用unpark在park image.png

多把锁

image.png
改进:
image.png

8、活跃性

死锁

演示

image.png
image.png—->image.png
两个线程都无法继续执行下去,陷入了死锁。

定位死锁

image.png

哲学家就餐问题

image.pngimage.png
image.png定义筷子类
image.png定义哲学家类
image.png定义哲学家就餐问题类

image.png image.png最后出现了死锁

解决方法

主要在ReentrantLock篇

活锁

image.png
image.png—->image.png

可通过将线程的睡眠时间改为随机时间,解决活锁问题。

饥饿

image.png
image.png
按照获得锁顺序为AB的方法解决:
image.png

解决哲学家就餐(会造成线程饥饿):改变加锁顺序 image.png—->image.png 使得筷子资源的获取顺序都为1.2.3.4.5的顺序,但是其他线程会出现线程饥饿问题。 image.pngimage.png,但是出现了线程饥饿问题。

9、ReentrantLock

image.pngimage.png

需要先创建reentrantlock对象,lock和unlock成对出现(unlock放在finally块中保证即使出现异常也能正确释放锁)

可重入

image.png

image.png—->image.png—->image.pngimage.png main中调用m1,m1中调用m2,三次操作都给重入了同一个锁。

可打断

image.png只有Reentrantlock中的lockInterruptibly才是可打断锁。

锁超时

立即失败

image.png—->image.png

image.png无参的tryLock方法获取不到锁立即返回fasle。

image.png—->image.png

(catch块中加return,意味着获取不到锁应该退出代码块而不是接着执行下面的tay、catch代码) image.png加了参数代表尝试获取锁的等待时间(单位second)

image.png—->image.png
image.png如果主线程在1s内释放了锁则t1线程可以获得锁

利用锁超时解决哲学家就餐问题

主要改变点:
image.png筷子类继承ReentantLock类
image.pngrun方法中使用trylock方法获得锁(筷子类)
image.png死锁解决。

公平锁

image.png不会按照阻塞队列的先后顺序得到锁
image.pngfail默认为false,不公平。

公平锁一般没有必要,会降低并发度,后面会有详解。

条件变量

image.png
使用流程
image.png

image.png

设计模式

固定顺序运行

image.png

交替输出

https://www.bilibili.com/video/BV16J411h7Rd?p=130&spm_id_from=pageDriver

小结

image.png
image.png

补充:
image.png
作为锁的对象,最好定义为final,让其引用不可变。