1、共享带来的问题
问题
分析
由于分时系统的线程切换会导致线程安全问题。 从字节码层面分析 Java内存模型中的流程如下
2、synchronized解决方案
阻塞式解决方法由synchronized和Lock。
3、方法上的synchronized
1.synchronized加在成员方法上
演变过程 →
1.synchronized加在静态方法上
主要区别在锁住的对象变成了Test.class
4、变量的线程安全问题
变量的安全问题
成员变量和静态变量的线程安全问题
1.如果没被共享,则线程安全
2.如果被共享了,分两种情况
只有读操作,则线程安全
如果有读写操作,则需要考虑线程安全问题。
局部变量是否线程安全
局部变量的安全性分析
线程安全的场景
1.
2.
线程不安全的场景
此时会出现线程安全问题,由于创建了子类,无法控制子类可以访问父类中的哪个对象,可能造成局部变量的引用暴露给其他线程。 解决办法: private在一定程度上保护线程安全(子类无法覆盖)。 使用final关键字修饰类,防止子类重写。
常见的线程安全类
线程安全类方法组合
不同的方法组合并非原子,原子方法组合使用无法保证原子性
不可变类线程安全性
String的substring如何保证线程安全
(substring() 方法返回字符串的子字符串:public String substring(int beginIndex) 或public String substring(int beginIndex, int endIndex))
如果String中有内容则创建一个新的String 然后将原有内容复制给新的String。
实例
1.不安全实例
userService是Servlet的成员变量,多个线程可能同时使用。
无scope说明其实单例对象,需要被共享,所以其成员变量同样是被共享的。 解决:可将start和end其改成环绕通知中的局部变量。
2.线程安全实例
dao层的方法无成员变量,为线程安全。service调用dao(其中没有可更改的成员变量)是安全的。servlet中使用service,service中的成员变量为private且无修改的方法,是线程安全的。 反例: Connection没有写成方法内的局部变量,其被多个线程共享,不安全了。 改进1: 在service中每次创建新的userdao实现 但是!不推荐这种方法,很浪费内存。最好还是将Connection作为userdao的方法中的局部变量。
虽然此处的sdf为局部变量,但是其所在类为抽象类,sdf被传递给了一个抽象方法foo,有可能foo的子类会影响sdf。 再次强调:不想暴露的方法记得设置private和finnal修饰符。 (例如String类的修饰符为finnal),其目的就是为了防止被子类覆盖其中的方法。
5、Monitor与synchronized优化
重量级锁(jdk6前常用)
https://www.bilibili.com/video/BV16J411h7Rd?p=76&spm_id_from=pageDriver
Java对象头 00表示可normal,00轻量级锁、10重量级锁、11被GC回收
owner为thread2,thread1,3在Entrylist中阻塞 thread2执行完毕,owner更换为thread1
轻量级锁(jdk6开始的优化)
流程: oberject reference记录锁住对象的地址,lock record记录对象的markword markword的state为00时才能替换成功,00表示其为normal状态 cas替换失败情况分两种 情况2的图示,但是情况2的失败并不影响(锁重入),所以lockrecord会被创建为null,将来加了几次锁就统计lockrecord的数量,解锁一次就去掉一个lockrecord。 解锁时取值为null(表示锁重入),重入计数-1→ 最后解锁的时候,需要吧state的00(有轻量级锁的状态)重新设为01(normal)
锁膨胀
锁膨胀过程
流程: 想去加锁,state为00,说明该对象已被加轻量级锁。 进入锁膨胀流程,thread1加轻量级锁失败,为Object对象申请Monitor锁,同时更改Object的markword为Monitor地址,state此时更改为10,Monitor的Owner只想t0的Object reference(锁住对象的地址)。 t1进入Monitor的Entrylist阻塞。 t0退出时,state由00变成10,解锁失败,进入重量级锁解锁流程
自旋优化(多核cpu下)
即竞争锁失败时先不进入Entrylist区blocked,而是自身循环几次,等到锁释放。(避免阻塞的上下文切换)。
自旋成功
自旋失败
偏向锁
偏向锁的使用场景一般为冲突很少的情况,如果是在多线程经常竞争释放对象的情况下可以通过来禁用。
流程: markword中,basied_lock位为0表示没有启用偏向锁,为1表示启用了。 (程序刚启动时,其后三位不是101,需要等几秒或者禁用延迟才会是101)
偏向锁的撤销场景
1.一个可用偏向锁的对象调用hashcode方法也会禁用掉偏向锁
2.有其他线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
3.调用wait/notify时也会撤销偏向锁(wait和notify只有重量级锁才有)
批量重偏向
markword的后三位会从101变为001再变为101。
批量撤销
锁消除
代码主要是为了比较加锁与不加锁的情况下的性能差异 结果差异并不大。 JIT(即时编译器)会对热点代码(反复执行的代码)进行优化,在真正执行时可能JIT会将synchronized忽略。
6、wait(会释放锁)和notify
注意wait是已经获得过锁但又放弃了的线程,blocked是没有获得过锁的线程。
api
没有获得锁的直接调用wait方法会报错 —-> 获得锁(成为owner之后才能wait)
wait(long n)在等待n毫秒的时间或被notify方法唤醒
notify(挑一个线程唤醒)和notifyall(唤醒所有在waitset中的线程)
wait和notify的使用
sleep和wait方法的区别
但sleep和wait的线程状态都是TIMED_WAITING(有时限等待)
虚假唤醒问题
线程1(小南) 线程2(小女)
与
唤醒线程(外卖)
—->
分析:唤醒线程(外卖)错误的唤醒了线程1而非目标线程1。 方法1:(不太好) —-> 可以使用notifyall()来唤醒全部线程,但是还是把线程1给唤醒了。 方法二: —-> 将if判断改为while,线程1不满足条件时
设计模式
同步模式之保护性暂停
演示
(相比于jion方法的优点:①jion方法需要t2线程执行结束,,而通过这种设计模式t2线程不需要结束就传递结果后仍可以执行其他代码。②jion方法两个线程间传递的必须为全局变量,而此处可以为局部变量。)
线程1:
线程2:
线程1等待线程2的结果并打印其大小(通过complete方法传递结果并唤醒线程1) —->
优化:不让t1线程无时间限制的等待t2线程,设置一个等待时间。
this.wait(n)参数不为timeout的原因: 不能设置为timeout,如果t1在15.00开始运行,等待时间timeout为2s就不等待。 如果有其他线程在15.00.00至15.00.02之间虚假唤醒了t1,t1则又要重新等待timeout时间。 所以应该设置为timeout-passedTime时间。
Join原理(Thread类的方法)
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
//此处调用wait方法,wait(0)实际上为一直无限等待
if (millis == 0) {
while (isAlive()) {
wait(0);
}
//else部分的代码为典型的保护性暂停模式逻辑
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
扩展
https://www.bilibili.com/video/BV16J411h7Rd?p=103&spm_id_from=pageDriver
异步模式之生产者/消费者
定义Message消息对象
定义一个容量为capacity的LinkedList消息队列
定义消息队列的take方法获取消息(从尾部取)
定义消息队列的put方法存入消息(从头部加)
(此处用id存i的是因为lambda表达式中引用外部的局部变量必须为final,不能直接引用i(不断变化)。)
7、Park和Unpark
基本使用
(park使线程进入WAIT状态)unpark在park之前和之后调用,都可以恢复park住的线程。
特点
原理
1、先调用park在unpark —-> 调用Unsafe.park时,检查counter为0时,thread0获得mutex互斥锁后进入cond条件变量阻塞,再次将counter设置为0(即使先前为0仍需设置)。 调用Unsafe.unpark时,不检查直接将counter设为1,唤醒cond条件变量中的thread0,thread0恢复运行且再次将counter设为0。
2、先调用unpark在park
多把锁
8、活跃性
死锁
演示
定位死锁
哲学家就餐问题
定义筷子类
定义哲学家类
定义哲学家就餐问题类
最后出现了死锁
解决方法
活锁
—->
可通过将线程的睡眠时间改为随机时间,解决活锁问题。
饥饿
按照获得锁顺序为AB的方法解决:
解决哲学家就餐(会造成线程饥饿):改变加锁顺序 —-> 使得筷子资源的获取顺序都为1.2.3.4.5的顺序,但是其他线程会出现线程饥饿问题。 ,但是出现了线程饥饿问题。
9、ReentrantLock
需要先创建reentrantlock对象,lock和unlock成对出现(unlock放在finally块中保证即使出现异常也能正确释放锁)
可重入
—->—-> main中调用m1,m1中调用m2,三次操作都给重入了同一个锁。
可打断
只有Reentrantlock中的lockInterruptibly才是可打断锁。
锁超时
立即失败
—->
无参的tryLock方法获取不到锁立即返回fasle。
—->
(catch块中加return,意味着获取不到锁应该退出代码块而不是接着执行下面的tay、catch代码) 加了参数代表尝试获取锁的等待时间(单位second)
利用锁超时解决哲学家就餐问题
主要改变点:
筷子类继承ReentantLock类
run方法中使用trylock方法获得锁(筷子类)
死锁解决。
公平锁
不会按照阻塞队列的先后顺序得到锁
fail默认为false,不公平。
公平锁一般没有必要,会降低并发度,后面会有详解。
条件变量
使用流程
设计模式
固定顺序运行
交替输出
https://www.bilibili.com/video/BV16J411h7Rd?p=130&spm_id_from=pageDriver
小结
补充:
作为锁的对象,最好定义为final,让其引用不可变。