共享带来的问题

- 在这些时候,算盘没利用起来(不能收钱了),老王觉得有点不划算
- 另外,小女也想用用算盘,如果总是小南占着算盘,让小女觉得不公平
- 于是,老王灵机一动,想了个办法 [ 让他们每人用一会,轮流使用算盘 ]
- 这样,当小南阻塞的时候,算盘可以分给小女使用,不会浪费,反之亦然
- 最近执行的计算比较复杂,需要存储一些中间结果,而学生们的脑容量(工作内存)不够,所以老王申请了
- 一个笔记本(主存),把一些中间结果先记在本上
- 计算流程是这样的

- 但是由于分时系统,有一天还是发生了事故
- 小南刚读取了初始值 0 做了个 +1 运算,还没来得及写回结果
- 老王说 [ 小南,你的时间到了,该别人了,记住结果走吧 ],于是小南念叨着 [ 结果是1,结果是1…] 不甘心地
到一边待着去了(上下文切换)
- 老王说 [ 小女,该你了 ],小女看到了笔记本上还写着 0 做了一个 -1 运算,将结果 -1 写入笔记本
- 这时小女的时间也用完了,老王又叫醒了小南:[小南,把你上次的题目算完吧],小南将他脑海中的结果 1 写
入了笔记本
小南和小女都觉得自己没做错,但笔记本里的结果是 1 而不是 0
临界区 Critical Section
- 一个程序运行多个线程本身是没有问题的
- 问题出在多个线程访问共享资源
- 多个线程读共享资源其实也没有问题
- 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
- 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为 临界区
static int counter = 0;static void increment()// 临界区{counter++;}static void decrement()// 临界区{counter--;}
竞态条件 Race Condition
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件synchronized 解决方案
应用之互斥
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
阻塞式的解决方案:synchronized,Lock
非阻塞式的解决方案:原子变量
本次课使用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一
时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换注意 虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的: 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
synchronized
语法synchronized(对象) // 线程1, 线程2(blocked){临界区}

思考
synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切
换所打断。
为了加深理解,请思考下面的问题
- 如果把 synchronized(obj) 放在 for 循环的外面,如何理解?— 原子性
- 如果 t1 synchronized(obj1) 而 t2 synchronized(obj2) 会怎样运作?— 锁对象
如果 t1 synchronized(obj) 而 t2 没有加会怎么样?如何理解?— 锁对象
方法上的 synchronized
class Test{public synchronized void test() {}}等价于class Test{public void test() {synchronized(this) {}}}
class Test{public synchronized static void test() {}}等价于class Test{public static void test() {synchronized(Test.class) {}}}
所谓的“线程八锁”
其实就是考察 synchronized 锁住的是哪个对象
@Slf4j(topic = "c.Number")class Number{public synchronized void a() {log.debug("1");}public synchronized void b() {log.debug("2");}}public static void main(String[] args) {Number n1 = new Number();new Thread(()->{ n1.a(); }).start();new Thread(()->{ n1.b(); }).start();}12 或 21
```java @Slf4j(topic = “c.Number”) class Number{ public synchronized void a() { sleep(1); log.debug(“1”); } public synchronized void b() { log.debug(“2”); } } public static void main(String[] args) { Number n1 = new Number(); new Thread(()->{ n1.a(); }).start(); new Thread(()->{ n1.b(); }).start(); }
```java@Slf4j(topic = "c.Number")class Number{public synchronized void a() {sleep(1);log.debug("1");}public synchronized void b() {log.debug("2");}public void c() {log.debug("3");}}public static void main(String[] args) {Number n1 = new Number();new Thread(()->{ n1.a(); }).start();new Thread(()->{ n1.b(); }).start();new Thread(()->{ n1.c(); }).start();}3 1s 1223 1s 132 1s 1
@Slf4j(topic = "c.Number")class Number{public synchronized void a() {sleep(1);log.debug("1");}public synchronized void b() {log.debug("2");}}public static void main(String[] args) {Number n1 = new Number();Number n2 = new Number();new Thread(()->{ n1.a(); }).start();new Thread(()->{ n2.b(); }).start();}
@Slf4j(topic = "c.Number")class Number{public static synchronized void a() {sleep(1);log.debug("1");}public synchronized void b() {log.debug("2");}}public static void main(String[] args) {Number n1 = new Number();new Thread(()->{ n1.a(); }).start();new Thread(()->{ n1.b(); }).start();}
@Slf4j(topic = "c.Number")class Number{public static synchronized void a() {sleep(1);log.debug("1");}public static synchronized void b() {log.debug("2");}}public static void main(String[] args) {Number n1 = new Number();new Thread(()->{ n1.a(); }).start();new Thread(()->{ n1.b(); }).start();}
@Slf4j(topic = "c.Number")class Number{public static synchronized void a() {sleep(1);log.debug("1");}public synchronized void b() {log.debug("2");}}public static void main(String[] args) {Number n1 = new Number();Number n2 = new Number();new Thread(()->{ n1.a(); }).start();new Thread(()->{ n2.b(); }).start();}
class Number{public static synchronized void a() {sleep(1);log.debug("1");}public static synchronized void b() {log.debug("2");}}public static void main(String[] args) {Number n1 = new Number();Number n2 = new Number();new Thread(()->{ n1.a(); }).start();new Thread(()->{ n2.b(); }).start();}
变量的线程安全分析
成员变量和静态变量是否线程安全?
- 如果它们没有共享,则线程安全
- 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
- 如果只有读操作,则线程安全
-
局部变量是否线程安全?
局部变量是线程安全的
- 但局部变量引用的对象则未必
- 如果该对象没有逃离方法的作用访问,它是线程安全的
- 如果该对象逃离方法的作用范围,需要考虑线程安全

private 或 final 提供【安全】的意义所在,请体会开闭原则中的【闭】
常见线程安全类
String
Integer
StringBuffer
Random
Vector
Hashtable
java.util.concurrent 包下的类
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为 它们的每个方法是原子的 但注意它们多个方法的组合不是原子的,见后面分析
Hashtable table = new Hashtable();new Thread(()->{table.put("key", "value1");}).start();new Thread(()->{table.put("key", "value2");}).start();
线程安全类方法的组合
Hashtable table = new Hashtable();// 线程1,线程2if( table.get("key") == null) {table.put("key", value);}
不可变类线程安全性
String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的
有同学或许有疑问,String 有 replace,substring 等方法【可以】改变值啊,那么这些方法又是如何保证线程安
全的呢?
Monitor 概念
Java 对象锁
概念
轻量级锁
轻量级锁的使用场景:如果一个对象然有多个线程访问,单多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是 synchronized
假设有两个方法同步块,利用同一个对象加锁




锁膨胀
自旋优化
偏向锁
偏向状态
禁用偏向锁
调用hashcode时
撤销 wait notify 时也会放弃偏向锁。
批量重定向
批量撤销
锁消除
JIT 对字节码优化 不会枷锁的地方不加锁
wait / notify

API 介绍
- obj.wait() 让进入 object 监视器的线程到 waitSet 等待
- obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒
- obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒
它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法。
- wait() 方法会释放对象的锁,进入 WaitSet 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到
notify 为止
- wait(long n) 有时限的等待, 到 n 毫秒后结束等待,或是被 notify
wait notify 的正确姿势
开始之前先看看sleep(long n) 和 wait(long n) 的区别
1) sleep 是 Thread 方法,而 wait 是 Object 的方法
2) sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用
3) sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁
4) 它们状态 TIMED_WAITING
synchronized(lock) {while(条件不成立) {lock.wait();}// 干活}//另一个线程synchronized(lock) {lock.notifyAll();}
Park & Unpark
原理



重新理解线程状态转换
情况 1 NEW —> RUNNABLE
- 当调用 t.start() 方法时,由 NEW —> RUNNABLE
情况 2 RUNNABLE <—> WAITING
t 线程 用 synchronized(obj) 获取了对象锁后
- 调用 obj.wait() 方法时,t 线程从 RUNNABLE —> WAITING
调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
- 竞争锁成功,t 线程从 WAITING —> RUNNABLE
竞争锁失败,t 线程从 WAITING —> BLOCKED ```java public class TestWaitNotify { final static Object obj = new Object(); public static void main(String[] args) { new Thread(() -> { synchronized (obj) {
log.debug("执行....");try {obj.wait();} catch (InterruptedException e) {e.printStackTrace();}log.debug("其它代码...."); // 断点}
},”t1”).start(); new Thread(() -> {
synchronized (obj) {log.debug("执行....");try {obj.wait();} catch (InterruptedException e) {e.printStackTrace();}log.debug("其它代码...."); // 断点}
},”t2”).start();
sleep(0.5); log.debug(“唤醒 obj 上其它线程”); synchronized (obj) {
obj.notifyAll(); // 唤醒obj上所有等待线程 断点
} } }
<a name="qjYOe"></a>### 情况 3 RUNNABLE <--> WAITING- **当前线程**调用 **t.join()** 方法时,**当前线程**从 **RUNNABLE **--> **WAITING**- 注意是当前线程在**t 线程对象**的监视器上等待- **t 线程**运行结束,或调用了**当前线程**的 interrupt() 时,当前线程从 **WAITING **--> **RUNNABLE**<a name="Kc1RB"></a>### 情况 4 RUNNABLE <--> WAITING- **当前线程**调用 **LockSupport.park()** 方法会让当前线程从 **RUNNABLE **--> **WAITING**- 调用 **LockSupport.unpark(目标线程)** 或调用了线程 的 **interrupt() **,会让**目标线程**从 **WAITING **-->**RUNNABLE**<a name="zXMcG"></a>### 情况 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 **<a name="adIiw"></a>### 情况 6 RUNNABLE <--> TIMED_WAITING- **当前线程**调用** t.join(long n)** 方法时,当前线程从 **RUNNABLE **--> **TIMED_WAITING**- 注意是**当前线程**在**t 线程对象**的**监视器**上等待- **当前线程**等待时间超过了 n 毫秒,或**t 线程**运行结束,或调用了当前线程的** interrupt()** 时,当前线程从**TIMED_WAITING **--> **RUNNABLE**<a name="dclIw"></a>###情况 7 RUNNABLE <--> TIMED_WAITING- **当前线程**调用 **Thread.sleep(long n)** ,**当前线程**从 **RUNNABLE **--> **TIMED_WAITING**- **当前线程**等待时间超过了 n 毫秒,**当前线程**从 **TIMED_WAITING **--> **RUNNABLE**<a name="Pm9dt"></a>### 情况 8 RUNNABLE <--> TIMED_WAITING- 当前线程调用 **LockSupport.parkNanos(long nanos)** 或 **LockSupport.parkUntil(long millis) **时,**当前线****程**从 **RUNNABLE** --> **TIMED_WAITING**- 调用 **LockSupport.unpark(目标线程) **或调用了线程 的** interrupt()** ,或是等待超时,会让**目标线程**从**TIMED_WAITING**--> **RUNNABLE**<a name="P8fIa"></a>### 情况 9 RUNNABLE <--> BLOCKED- **t 线程**用 **synchronized(obj)** 获取了对象锁时如果竞争失败,从 **RUNNABLE **--> **BLOCKED**- 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 **BLOCKED **的线程重新竞争,如果其中 t 线程竞争成功,从 **BLOCKED **--> **RUNNABLE **,其它失败的线程仍然 **BLOCKED**<a name="r1BSB"></a>### 情况 10 RUNNABLE <--> TERMINATED当前线程所有代码运行完毕,进入 **TERMINATED**<a name="JVHix"></a>## 多把锁<a name="VBK6J"></a>### 多把不相干的锁一间大屋子有两个功能:睡觉、学习,互不相干。<br />现在小南要学习,小女要睡觉,但如果只用一间屋子(一个对象锁)的话,那么并发度很低<br />解决方法是准备多个房间(多个对象锁)```javaclass BigRoom {public void sleep() {synchronized (this) {log.debug("sleeping 2 小时");Sleeper.sleep(2);}}public void study() {synchronized (this) {log.debug("study 1 小时");Sleeper.sleep(1);}}}BigRoom bigRoom = new BigRoom();new Thread(() -> {bigRoom.compute();},"小南").start();new Thread(() -> {bigRoom.sleep();},"小女").start();12:13:54.471 [小南] c.BigRoom - study 1 小时12:13:55.476 [小女] c.BigRoom - sleeping 2 小时
改进
class BigRoom {private final Object studyRoom = new Object();private final Object bedRoom = new Object();public void sleep() {synchronized (bedRoom) {log.debug("sleeping 2 小时");Sleeper.sleep(2);}}public void study() {synchronized (studyRoom) {log.debug("study 1 小时");Sleeper.sleep(1);}}}
将锁的粒度细分
- 好处,是可以增强并发度
- 坏处,如果一个线程需要同时获得多把锁,就容易发生死锁
活跃性
死锁
有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁
t1 线程 获得 A对象 锁,接下来想获取 B对象 的锁
t2 线程 获得 B对象 锁,接下来想获取 A对象 的锁定位死锁
检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁:
```java cmd > jstack 33200 Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8 2018-12-29 05:51:40 Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.91-b14 mixed mode): “DestroyJavaVM” #13 prio=5 os_prio=0 tid=0x0000000003525000 nid=0x2f60 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE “Thread-1” #12 prio=5 os_prio=0 tid=0x000000001eb69000 nid=0xd40 waiting for monitor entry [0x000000001f54f000] java.lang.Thread.State: BLOCKED (on object monitor) at thread.TestDeadLock.lambda$main$1(TestDeadLock.java:28)cmd > jpsPicked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-812320 Jps22816 KotlinCompileDaemon33200 TestDeadLock // JVM 进程11508 Main28468 Launcher
- waiting to lock <0x000000076b5bf1c0> (a java.lang.Object)
- locked <0x000000076b5bf1d0> (a java.lang.Object) at thread.TestDeadLock$$Lambda$2/883049899.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) “Thread-0” #11 prio=5 os_prio=0 tid=0x000000001eb68800 nid=0x1b28 waiting for monitor entry [0x000000001f44f000] java.lang.Thread.State: BLOCKED (on object monitor) at thread.TestDeadLock.lambda$main$0(TestDeadLock.java:15)
- waiting to lock <0x000000076b5bf1d0> (a java.lang.Object)
- locked <0x000000076b5bf1c0> (a java.lang.Object) at thread.TestDeadLock$$Lambda$1/495053715.run(Unknown Source) at java.lang.Thread.run(Thread.java:745)
// 略去部分输出
Found one Java-level deadlock:
“Thread-1”: waiting to lock monitor 0x000000000361d378 (object 0x000000076b5bf1c0, a java.lang.Object), which is held by “Thread-0” “Thread-0”: waiting to lock monitor 0x000000000361e768 (object 0x000000076b5bf1d0, a java.lang.Object), which is held by “Thread-1”
Java stack information for the threads listed above:
“Thread-1”: at thread.TestDeadLock.lambda$main$1(TestDeadLock.java:28)
- waiting to lock <0x000000076b5bf1c0> (a java.lang.Object)
- locked <0x000000076b5bf1d0> (a java.lang.Object) at thread.TestDeadLock$$Lambda$2/883049899.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) “Thread-0”: at thread.TestDeadLock.lambda$main$0(TestDeadLock.java:15)
- waiting to lock <0x000000076b5bf1d0> (a java.lang.Object)
- locked <0x000000076b5bf1c0> (a java.lang.Object) at thread.TestDeadLock$$Lambda$1/495053715.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) Found 1 deadlock.
- 避免死锁要注意加锁顺序- 另外如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过 top 先定位到CPU 占用高的 Java 进程,再利用 top -Hp 进程id 来定位是哪个线程,最后再用 jstack 排查<a name="adygu"></a>### 哲学家就餐问题<a name="L7H1c"></a>### 活锁活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束<a name="NmgDk"></a>### 饥饿很多教程中把饥饿定义为,**一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束,**饥饿的情况不<br />易演示,讲读写锁时会涉及饥饿问题<br />下面我讲一下我遇到的一个线程饥饿的例子,先来看看使用顺序加锁的方式解决之前的死锁问题<br /><br /><a name="SPdQ3"></a>## ReentrantLock相对于 synchronized 它具备如下特点- 可中断- 可以设置超时时间- 可以设置为公平锁- 支持多个条件变量与 synchronized 一样,都支持可重入<br />基本语法```java// 获取锁reentrantLock.lock();try {// 临界区} finally {// 释放锁reentrantLock.unlock();}
可重入
可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁 如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
static ReentrantLock lock = new ReentrantLock();public static void main(String[] args) {method1();}public static void method1() {lock.lock();try {log.debug("execute method1");method2();} finally {lock.unlock();}}public static void method2() {lock.lock();try {log.debug("execute method2");method3();} finally {lock.unlock();}}public static void method3() {lock.lock();try {log.debug("execute method3");} finally {lock.unlock();}}
可打断
ReentrantLock lock = new ReentrantLock();Thread t1 = new Thread(() -> {log.debug("启动...");try {lock.lockInterruptibly();} catch (InterruptedException e) {e.printStackTrace();log.debug("等锁的过程中被打断");return;}try {log.debug("获得了锁");} finally {lock.unlock();}}, "t1");lock.lock();log.debug("获得了锁");t1.start();try {sleep(1);t1.interrupt();log.debug("执行打断");} finally {lock.unlock();}

注意如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断
ReentrantLock lock = new ReentrantLock();Thread t1 = new Thread(() -> {log.debug("启动...");lock.lock();try {log.debug("获得了锁");} finally {lock.unlock();}}, "t1");lock.lock();log.debug("获得了锁");t1.start();try {sleep(1);t1.interrupt();log.debug("执行打断");sleep(1);} finally {log.debug("释放了锁");lock.unlock();}
锁超时
立刻失败
ReentrantLock lock = new ReentrantLock();Thread t1 = new Thread(() -> {log.debug("启动...");if (!lock.tryLock()) {log.debug("获取立刻失败,返回");return;}try {log.debug("获得了锁");} finally {lock.unlock();}}, "t1");lock.lock();log.debug("获得了锁");t1.start();try {sleep(2);} finally {lock.unlock();}
公平锁
条件变量
synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待
ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比
- synchronized 是那些不满足条件的线程都在一间休息室等消息
- 而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤 醒
使用要点:













