本章内容:
共享问题、synchronized、线程安全分析Monitor、wait/notify、线程状态转换、活跃性、Lock
4.1共享带来的问题
Java的体现
![](https://i-blog.csdnimg.cn/blog_migrate/bc770eb3afeaad15c6305d40d2b60976.png)
问题分析:
以上的结果可能是正数、负数、零。为什么呢?因为Java中对静态变量的自增,自减并不是原子操作。
对于i++指令而言(i为静态变量),实际会产生如下的JVM字节码指令:
![](https://i-blog.csdnimg.cn/blog_migrate/48a431c198dada3440130ee86910dc4c.png)
![](https://i-blog.csdnimg.cn/blog_migrate/2c731e9a51f3856098841ab438761469.png)
![](https://i-blog.csdnimg.cn/blog_migrate/02d73d95b0a7a0d9780ed1b66d67a004.png)
![](https://i-blog.csdnimg.cn/blog_migrate/311103723304e4c81cf5b2372aedfed6.png)
![](https://i-blog.csdnimg.cn/blog_migrate/8380c99dd8a9e3262a93c1233b79d0b3.png)
临界区Critical Section
一个程序运行多个线程本身是没有问题的
问题出在多个线程访问共享资源
多个线程读共享资源其实也没有问题
在多个线程对共享资源读写操作时发生指令交错,就会出现问题
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
![](https://i-blog.csdnimg.cn/blog_migrate/7a1141a7553a6cc64fd79ab5c415cb3c.png)
竟态条件
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竟态条件
4.2 synchronized解决方案
应用之互斥
为了避免临界区的竟态条件发生,有多种手段可以达到目的
阻塞式的解决方案:synchronized
Lock非阻塞式的解决方案∶原子变量
synchronized,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。
synchronized
![](https://i-blog.csdnimg.cn/blog_migrate/aa827f76b0ba20278e29dca6c8de60a0.png)
![](https://i-blog.csdnimg.cn/blog_migrate/07e449d05dbe8477397c19349980ae7a.png)
面向对象改进
把需要保护的共享变量放入一个类
![](https://i-blog.csdnimg.cn/blog_migrate/fa8f2bc1a74f9cf2f7bd2d8b337297c4.png)
![](https://i-blog.csdnimg.cn/blog_migrate/eb7d7a286c31619b4ca336ae0d376c32.png)
4.3方法上的synchronized
![](https://i-blog.csdnimg.cn/blog_migrate/f94875c3c2d1b570269f27f66c3471dd.png)
4.4 synchronized原理进阶
轻量级锁
轻量级锁的使用场景∶如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是synchronized假设有两个方法同步块,利用同一个对象加锁
4.5变量的线程安全分析
成员变量和静态变量是否线程安全?
如果它们没有共享,则线程安全
如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
如果只有读操作,则线程安全
如果有读写操作,则这段代码是临界区,需要考虑线程安全
局部变量是否线程安全?
局部变量是线程安全的
但局部变量引用的对象则未必
如果该对象没有逃离方法的作用范围,它是线程安全的
如果该对象逃离方法的作用范围,需要考虑线程安全
局部变量线程安全分析
![](https://i-blog.csdnimg.cn/blog_migrate/8215ebc0f3ad72a92dac98d7c0f5ddc8.png)
![](https://i-blog.csdnimg.cn/blog_migrate/03d69750fe0eec47742a1c17ff789d0d.png)
案例分析见并发编程4.4
常见线程安全类
String
Lnteger
String
Buffer
Random
Vector
Hashtable
java.util.concurrent包下的类
这里说他们是线程安全的是指,多个线程调用他们同一个实例的某个方法时,是线程安全的,也可以理解为
![](https://i-blog.csdnimg.cn/blog_migrate/9df638193632adfc70d8ef7de742dda7.png)
他们的每个方法是原子的
但主要他们多个方法的组合不是原子的
线程安全类方法的组合
分析以下代码是否线程安全?
![](https://i-blog.csdnimg.cn/blog_migrate/25eacdeec46db6fa6c59d322f5115c74.png)
不可变类线程安全性
String、Integer等都是不可变类,因为其内部的状态不可以被改变,因此他们的方法都是线程安全的。
实例分析
![](https://i-blog.csdnimg.cn/blog_migrate/0306bc92fee2e3e3921462b208c5d4da.png)
例1:线程不安全 其中HashMap方法与Date方法均属于线程不安全
![](https://i-blog.csdnimg.cn/blog_migrate/b5e024103fb0e316698a567494978564.png)
例二:线程不安全,count为成员变量,有可能被其他线程访问更改
![](https://i-blog.csdnimg.cn/blog_migrate/2bcfb6d3c7b77f094ada83565972187f.png)
例3:线程不安全,start有可能被其他线程修改
例4
![](https://i-blog.csdnimg.cn/blog_migrate/925057e15a3466eb17999a5296d02c64.png)
例4线程安全
![](https://i-blog.csdnimg.cn/blog_migrate/4dd86b64d8c8d8ce5c72ded1cda880d5.png)
例5线程不安全,conn成员变量有可能被其他线程修改
![](https://i-blog.csdnimg.cn/blog_migrate/a7c243f3ffac585f40722ac5e1769537.png)
例6线程安全
![](https://i-blog.csdnimg.cn/blog_migrate/3b5722a59a3dfdcdd91e49a87899c12b.png)
例7:foo为抽象类,其子类行为不明确,有可能子类改变基类的成员变量
4.6 Monitor概念
Java对象头
32位虚拟机
![](https://i-blog.csdnimg.cn/blog_migrate/c18a1d689ad6311da4afd8d0d24122b4.png)
![](https://i-blog.csdnimg.cn/blog_migrate/f01260fdc8a8cb5d1c96c387cb683721.png)
4.7 wait notify
API介绍
obj.wait()让进入object 监视器的线程到waitSet等待
obj.notify()在object 上正在waitSet等待的线程中挑一个唤醒
obj.notifyAl1()让object 上正在waitSet 等待的线程全部唤醒
wait()方法会释放对象的锁,进入WaitSet 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到notify为止
wait(long n)有时限的等待,到n毫秒后结束等待,或是被notify。
4.8wait notify的正确姿势
sleep(long n)和wait(long n)的区别
1)sleep是Thread方法,而wait是object的方法
2)sleep不需要强制和synchronized配合使用,但wait需要和synchronized一起用
3)sleep 在睡眠的同时,不会释放对象锁的,但wait在等待的时候会释放对象锁
4)它们状态TIMED_WAITING
案例看《并发编程》p73
4.9 park&Unpark
基本使用:他们是LockSupport类中的方法
![](https://i-blog.csdnimg.cn/blog_migrate/3bb812bdc7c7890c5c9ac95b0ccc3e8f.png)
特点
与Object的 wait & notify相比
wait , notify和notifyAll必须配合Object Monitor一起使用,而 park , unpark 不必
park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而notify只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么【精确】
park & unpark可以先unpark,而wait & notify不能先notify
案例看《并发编程》p74
4.10重新理解线程状态转换
![](https://i-blog.csdnimg.cn/blog_migrate/618711b9d9d92194418c646ae7ea9cbf.png)
假如有线程Thread t
情况1 NEW -->RUNNABLE
当调用t.start()方法时,由NEW-->RUNNABLE
情况2 RUNNABLE <-->WAITING
t线程用synchronized(obj)获取了对象锁后
调用obj. wait()方法时,t线程从RUNNABLE 一> WAITING
调用obj . notify() , obj.notifyAl1(, t.interrupt()时。
竞争锁成功,t线程从WAITING -->RUNNABLE
竞争锁失败,t线程从WAITING --> BLOCKED
情况3 RUNNABLE<-->WAITING
当前线程调用t.join()方法时,当前线程从RUNNABLE --> WAITING
注意是当前线程在t线程对象的监视器上等待
t线程运行结束,或调用了当前线程的interrupt()时,当前线程从WAITING -->RUNNABLE
情况4 RUNNABLE<-->WAITING
当前线程调用LockSupport.park()方法会让当前线程从RUNNABLE --> WAITING
调用LockSupport .unpark(目标线程)或调用了线程的interrupt(),会让目标线程从WAITING -->RUNNABLE
情况5 RUNNABLE<-->TIMED_WAITING
t线程用synchronized(obj)获取了对象锁后
调用obj.wait(long n)方法时,t线程从RUNNABLE->TIMED_WAITING
t线程等待时间超过了n毫秒,或调用obj .notify(), obj .notifyAll(),t.interrupt()时
竞争锁成功,t线程从TIMED_WAITING -->RUNNABLE
竞争锁失败,t线程从TIMED_WAITING -->BLOCKED
情况6 RUNNABLE<-->TIMED_WAITING
当前线程调用t.join(long n)方法时,当前线程从RUNNABLE ->TIMED_WAITING
注意是当前线程在t线程对象的监视器上等待
当前线程等待时间超过了n毫秒,或t线程运行结束,或调用了当前线程的interrupt()时,当前线程从TIMED_WAITING -->RUNNABLE
情况7 RUNNABLE<-->TIMED_WAITING
当前线程调用Thread.sleep(long n),当前线程从RUNNABLE -->TIMED_WAITING
当前线程等待时间超过了n毫秒,当前线程从「TIMED_WAITING -->RUNNABLE
情况8 RUNNABLE<-->TIMED_WAITING
当前线程调用LockSupport. parkNanos(long nanos)或LockSupport . parkUntil(long millis)时,当前线程从RUNNABLE--> TIMED_WAITING
调用LockSupport.unpark(目标线程)或调用了线程的interrupt(),或是等待超时,会让目标线程从TIMED_WAITING-->RUNNABLE
情况9 RUNNABLE<-->BLOCKED
t线程用synchronized(obj)获取了对象锁时如果竞争失败,从RUNNABLE --> BLOCKED
持obj锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED的线程重新竞争,如果其中t线程竞争成功,从BLOCKED -->RUNNABLE,其它失败的线程仍然BLOCKED
情况10 RUNNABLE<-->TERMINATED
当前线程所有代码运行完毕,进入 TERMINATED
4.11多把锁
举例:
一间大屋子有两个功能︰睡觉、学习,互不相干。
现在小南要学习,小女要睡觉,但如果只用一间屋子(一个对象锁)的话,那么并发度很低解决方法是准备多个房间(多个对象锁)、
将锁的粒度细分
好处,是可以增强并发度
坏处,如果一个线程需要同时获得多把锁,就容易发生死锁
4.12活跃性
死锁
一个线程需要同时获取多把锁,这时就容易发生死锁
例:t1线程获得A对象锁,接下来想获取B对象的锁t2线程获得B对象锁,接下来想获取[A对象的锁。
定位死锁
检测死锁可以使用jconsole工具,或者使用jps定位进程id,再用jstack定位死锁
避免死锁要注意加锁顺序
另外如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况linux下可以通过top先定位到CPU占用高的Java进程,再利用top -Hp进程id 来定位是哪个线程,最后再用jstack排查
哲学家就餐问题见《并发编程》P82
活锁
活锁出现在两个线程互相改变对象的结束条件,最后谁也无法结束。
饥饿
下图是一个线程饥饿的例子,先来看看使用顺序加锁的方式解决之前的死锁问题
![](https://i-blog.csdnimg.cn/blog_migrate/52838d441b39e925a6e924ff20123ff3.png)
顺序加锁的解决方案
![](https://i-blog.csdnimg.cn/blog_migrate/fc0304db19760150ae6a0b8aaa77a20e.png)
4.13 ReentrantLock
相对于synchronized它具备如下特点
可中断
可以设置超时时间
可以设置为公平锁
支持多个条件变量
与synchronized一样,都支持可重入
基本语法
![](https://i-blog.csdnimg.cn/blog_migrate/ead4e4b850e9d89e120d487313f1d9bf.png)
可重入
可重入是指同一个线程如果首次获得了这把锁,那么因为他是这把锁的拥有者,因此有权利再次获取这把锁,如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
示例
![](https://i-blog.csdnimg.cn/blog_migrate/3edef9b37edfff9bbc013fc6b83ae232.png)
可打断、锁超时、公平锁,分别举例见并发编程P90
条件变量
synchronized 中也有条件变量,即waitSet休息室,当条件不满足时进入waitSet等待。ReentrantLock的条件变量比 synchronized强大之处在于,它是支持多个条件变量的,这就好比
synchronized是那些不满足条件的线程都在一间休息室等消息
而ReentrantLock支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒
使用要点:
await前需要获得锁
await 执行后,会释放锁,进入conditionObject等待
await的线程被唤醒(或打断、或超时)取重新竞争lock 锁
竞争lock 锁成功后,从await后继续执行
4.14本章总结
分析多线程访问共享资源时,哪些代码片段属于临界区
使用synchronized互斥解决临界区的线程安全问题。
掌握synchronized 锁对象语法
掌握synchronzied加载成员方法和静态方法语法
掌握wait/notify同步方法
使用lock 互斥解决临界区的线程安全问题
掌握lock 的使用细节:可打断、锁超时、公平锁、条件变量
学会分析变量的线程安全性、掌握常见线程安全类的使用
了解线程活跃性问题:死锁、活锁、饥饿
应用方面
互斥∶使用synchronized或Lock达到共享资源互斥效果
同步∶使用wait/notify 或 Lock的条件变量来达到线程间通信效果
原理方面
monitor、synchronized 、wait/notify 原理
synchronized进阶原理
park & unpark原理·
模式方面
同步模式之保护性暂停
异步模式之生产者消费者
同步模式之顺序控制