共享带来的问题
两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?
static int counter = 0; public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> { for (int i = 0; i < 5000; i++) { counter++; } }, "t1");
Thread t2 = new Thread(() -> { for (int i = 0; i < 5000; i++) { counter--; } }, "t2");
t1.start(); t2.start(); t1.join(); t2.join(); log.debug("{}",counter); }
以上的结果可能是正数、负数、零。为什么呢?因为 Java 中 对静态变量的 自增,自减并不是原子操作。
例如对于 i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:
getstatic i // 获取静态变量i的值 iconst_1 // 准备常量1 iadd // 自增 putstatic i // 将修改后的值存入静态变量i
而对应 i-- 也是类似:
getstatic i // 获取静态变量i的值 iconst_1 // 准备常量1 isub // 自减 putstatic i // 将修改后的值存入静态变量i
出现负数的情况:
出现正数的情况:
临界区 Critical Section
- 一个程序运行多个线程本身是没有问题的
- 问题出在多个线程访问共享资源
- 多个线程读共享资源其实也没有问题
- 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
- 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
竞态条件 Race Condition
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
synchronized
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
- 阻塞式的解决方案:synchronized,Lock
- 非阻塞式的解决方案:原子变量
注意
虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:
- 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
- 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
解决
注意为了保护同一个资源,应该对同一个对象加锁
static int counter = 0; static final Object room = new Object(); public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> { for (int i = 0; i < 5000; i++) { synchronized (room) { counter++; } } }, "t1");
Thread t2 = new Thread(() -> { for (int i = 0; i < 5000; i++) { synchronized (room) { counter--; } } }, "t2");
t1.start(); t2.start(); t1.join(); t2.join(); log.debug("{}",counter); }
优化面向对象改进
把需要保护的共享变量放入一个类
class Room { int value = 0; public void increment() { synchronized (this) { value++; } } public void decrement() { synchronized (this) { value--; } } public int get() { synchronized (this) { return value; } } }
@Slf4j public class Test1 { public static void main(String[] args) throws InterruptedException { Room room = new Room();
Thread t1 = new Thread(() -> { for (int j = 0; j < 5000; j++) { room.increment(); } }, "t1");
Thread t2 = new Thread(() -> { for (int j = 0; j < 5000; j++) { room.decrement(); } }, "t2"); t1.start(); t2.start(); t1.join(); t2.join(); log.debug("count: {}" , room.get()); } }
一、方法上的 synchronized
sychronized 只能锁对象。加再方法上,等于锁住了 this 对象
class Test{ public synchronized void test() { } } 等价于 class Test{ public void test() { synchronized(this) { } } }
二、加在静态方法上的 synchronized
sychronized 只能锁对象。加在静态方法上,等于锁住了类对象(类对象只有一个)
- 静态方法:是使用static关键字修饰的方法,又叫类方法.属于类的,不属于对象, 在实例化对象之前就可以通过类名.方法名调用静态方法。 (静态属性,静态方法都是属于类的,可以直接通过类名调用)。
- 非静态方法:是不含有static关键字修饰的普通方法,又称为实例方法,成员方法。属于对象的,不属于类的。(成员属性,成员方法是属于对象的,必须通过new关键字创建对象后,再通过对象调用)。
class Test{ public synchronized static void test() { } } 等价于 class Test{ public static void test() { synchronized(Test.class) { } } }
例子: n1 和 n2 虽然是两个不同的对象,但是 a 和 b 都是静态方法被修饰。所以 n1.a() 和 n2.b() 锁住的都是同一个类对象 Number.class , 有互斥
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(); }
变量的线程安全分析
一、成员变量和静态变量是否线程安全?
- 如果它们没有共享,则线程安全
- 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
- 如果只有读操作,则线程安全
- 如果有读写操作,则这段代码是临界区,需要考虑线程安全
二、局部变量是否线程安全?
- 局部变量是线程安全的
- 但局部变量引用的对象则未必
- 如果该对象没有逃离方法的作用访问,它是线程安全的
- 如果该对象逃离方法的作用范围,需要考虑线程安全
三、局部变量线程安全分析
相比静态变量一般都是安全的,因为是每个线程创建一个新对象
图一:List 作为 static 全局变量,对其进行读写操作(add remove)
图一:List 作为方法内的局部变量,对其进行读写操作(add remove)
局部变量不安全的情况
新起了线程对局部变量进行操作,这样这个局部变量对于 原线程 和 新线程 来说是共享资源,就带来了线程安全的问题
(将局部变量暴露了出去?)
从这个例子可以看出 private 或 final 提供【安全】的意义所在,请体会开闭原则中的【闭】
class ThreadSafe { public final void method1(int loopNumber) { ArrayList<String> list = new ArrayList<>(); for (int i = 0; i < loopNumber; i++) { method2(list); method3(list); } } private void method2(ArrayList<String> list) { list.add("1"); } public void method3(ArrayList<String> list) { list.remove(0); } } class ThreadSafeSubClass extends ThreadSafe{ @Override public void method3(ArrayList<String> list) { new Thread(() -> { list.remove(0); }).start(); } }
三 常见线程安全类
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- java.util.concurrent 包下的类
- (Date SimpleDateFormat 不是线程安全的)
这里说它们是线程安全的是指,多个线程调用它们同一个实例的同个方法时,是线程安全的。也可以理解为
它们的每个方法是原子的
Hashtable table = new Hashtable(); new Thread(()->{ table.put("key", "value1"); }).start();
new Thread(()->{ table.put("key", "value2"); }).start();
线程安全类方法的组合
它们多个方法的组合不是原子的
Hashtable table = new Hashtable(); // 线程1,线程2 if( table.get("key") == null) { table.put("key", value); }
不可变类线程安全性
String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的
如 String 的 substring replace 方法都是重新返回一个新创建的 String 对象
public String substring(int beginIndex) { if (beginIndex < 0) { throw new StringIndexOutOfBoundsException(beginIndex); } int subLen = value.length - beginIndex; if (subLen < 0) { throw new StringIndexOutOfBoundsException(subLen); } return (beginIndex == 0) ? this : new String(value, beginIndex, subLen); } public String(char value[], int offset, int count) { if (offset < 0) { throw new StringIndexOutOfBoundsException(offset); } if (count <= 0) { if (count < 0) { throw new StringIndexOutOfBoundsException(count); } if (offset <= value.length) { this.value = "".value; return; } } // Note: offset or count might be near -1>>>1. if (offset > value.length - count) { throw new StringIndexOutOfBoundsException(offset + count); } this.value = Arrays.copyOfRange(value, offset, offset+count); }
无状态(没有成员变量)的类一般是线程安全的
sychronized 原理、锁升级
wait/notify
一、wait / notify 原理
- Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
- BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
- BLOCKED 是线程在等待获取锁,而 WAITING 是获取锁后又调用 wait() 方法放弃了锁(只有获取了 sychronized 锁的对象,也就是说在同步代码块内,才可以调用 wait(),否则会报错)
- BLOCKED 线程会在 Owner 线程释放锁时唤醒
- WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入 EntryList 重新竞争
二、API 介绍
- obj.wait() 让进入 object Monitor 的线程(Monitor 的 owner)释放持有的锁(不再是 owner),到 waitSet 进行无时限的等待
- obj.wait() 有时限的等待
- obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒,唤醒后并不意味者立刻获得锁,仍需进入 EntryList 重新竞争
- obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒,唤醒后并不意味者立刻获得锁,仍需进入 EntryList 重新竞争
它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须先获得此对象的锁(在 sychronized 同步代码块内),才能调用这几个方法,否则运行时会报错
new Thread(() -> { synchronized (room) { log.debug("有烟没?[{}]", hasCigarette); if (!hasCigarette) { log.debug("没烟,先歇会!"); try { room.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("有烟没?[{}]", hasCigarette); if (hasCigarette) { log.debug("可以开始干活了"); } else { log.debug("没干成活..."); } } }, "小南").start();
new Thread(() -> { synchronized (room) { Thread thread = Thread.currentThread(); log.debug("外卖送到没?[{}]", hasTakeout); if (!hasTakeout) { log.debug("没外卖,先歇会!"); try { room.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("外卖送到没?[{}]", hasTakeout); if (hasTakeout) { log.debug("可以开始干活了"); } else { log.debug("没干成活..."); } } }, "小女").start(); sleep(1); new Thread(() -> { synchronized (room) { hasTakeout = true; log.debug("外卖到了噢!"); room.notifyAll(); } }, "送外卖的").start();
三、sleep(long n) 和 wait(long n) 的区别
- sleep 是 Thread 方法,而 wait 是 Object 的方法
- sleep 不需要强制和 synchronized 配合使用,但 wait 需要 和 synchronized 一起用
- sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁
共同点:
进入 sleep 或 wait 后状态都是 TIMED_WAITING
wait / notify 的正确姿势
step1:用 sleep 等待烟(有烟才能干活)
- 其它干活的线程,都要一直阻塞,效率太低
- 小南线程必须睡足 2s 后才能醒来,就算烟提前送到,也无法立刻醒来
- 加了 synchronized (room) 后,就好比小南在里面反锁了门睡觉,烟根本没法送进门,main 没加 synchronized 就好像 main 线程是翻窗户进来的
- 解决方法,使用 wait - notify 机制
step2:用 wait - notify 机制(烟到了去notify)
- 解决了其它干活的线程阻塞的问题
- 但如果有其它线程也在等待条件呢?
step3: 有多个等待的线程(有个在等烟有个在等外卖),用 wait - notify (外卖到了去notify)
- notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线 程,称之为【虚假唤醒】
- 解决方法,改为 notifyAll
step4: 有多个等待的线程(有个在等烟有个在等外卖),用 wait - notifyAll (外卖到了去notify)
- 用 notifyAll 仅解决多个线程的唤醒问题,但使用 if + wait 判断仅有一次机会,一旦条件不成立,就没有重新【继续等待 & 直到被正确条件唤醒】的机会了
- 解决方法,用 while + wait,当条件不成立,再次 wait
step5:将 if 改为 while
if (!hasCigarette) { log.debug("没烟,先歇会!"); try { room.wait(); } catch (InterruptedException e) { e.printStackTrace(); } }
改为
while (!hasCigarette) { log.debug("没烟,先歇会!"); try { room.wait(); } catch (InterruptedException e) { e.printStackTrace(); } }
模式
synchronized(lock) { while(条件不成立) { lock.wait(); } // 干活 }
//另一个线程 synchronized(lock) { lock.notifyAll(); }
四、join() 原理
回忆用法:可以在 t2 里面通过调用 t1.join() 来等待 t1 运行完毕
就是用了 wait()/notify() 的保护性暂停原理
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"); } if (millis == 0) { while (isAlive()) { wait(0); } } else { while (isAlive()) { long delay = millis - now; if (delay <= 0) { break; } wait(delay); now = System.currentTimeMillis() - base; } } }
五、阻塞队列原理
可以基于 wait/notify 来实现
class Message { private int id; private Object message; public Message(int id, Object message) { this.id = id; this.message = message; } public int getId() { return id; } public Object getMessage() { return message; } } class MessageQueue { private LinkedList<Message> queue; private int capacity; public MessageQueue(int capacity) { this.capacity = capacity; queue = new LinkedList<>(); } public Message take() { synchronized (queue) { //队列空,取出操作阻塞 while (queue.isEmpty()) { log.debug("没货了, wait"); try { queue.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //队列不空了,可以取出了 Message message = queue.removeFirst(); //通知等待的所有线程(因为刚取出,所以因为队列满而put等待的线程会被真正唤醒) queue.notifyAll(); return message; } } public void put(Message message) { synchronized (queue) { //队列满,放入操作阻塞 while (queue.size() == capacity) { log.debug("库存已达上限, wait"); try { queue.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //队列不空了,可以取出了 queue.addLast(message); //通知等待的所有线程(因为刚取出,所以因为队列空而take等待的线程会被真正唤醒) queue.notifyAll(); } } }
park/unpark
一、基本使用
它们是 LockSupport 类中的方法
// 暂停当前线程 LockSupport.park(); // 恢复某个线程的运行 LockSupport.unpark(暂停线程对象)
- 先 park 再被 unpark:park() 时阻塞,当别的线程调用了 unpark(t1) 后会将 t1 唤醒
- 先被 unpark 再 park:park() 时不会阻塞
Thread t1 = new Thread(() -> { log.debug("start..."); sleep(1); log.debug("park..."); LockSupport.park(); log.debug("resume..."); },"t1"); t1.start(); sleep(3); log.debug("unpark..."); LockSupport.unpark(t1);
输出
18:42:52.585 c.TestParkUnpark [t1] - start... 18:42:53.589 c.TestParkUnpark [t1] - park... 18:42:54.583 c.TestParkUnpark [main] - unpark... 18:42:54.583 c.TestParkUnpark [t1] - resume...
特点:
与 Object 的 wait & notify 相比
- wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必
- unpark 是以线程为单位来【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么【精确】
- park & unpark 可以先 unpark(后面park不会阻塞),而 wait & notify 不能先 notify
二、原理
1.先 park 再被 unpark(t1)
- 当前线程调用 Unsafe.park() 方法
- 检查 _counter ,本情况为 0,这时,获得 _mutex 互斥锁
- 线程进入 _cond 条件变量阻塞
- 设置 _counter = 0
- 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1(counter 最多到1)
- 唤醒 _cond 条件变量中的 Thread_0
- Thread_0 恢复运行
- 设置 _counter 为 0
2.先被 unpark(t1) 再 park
- 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
- 当前线程调用 Unsafe.park() 方法
- 检查 _counter ,本情况为 1,这时线程无需阻塞,继续运行
- 设置 _counter 为 0
线程状态转换
假设有线程 Thread t
情况 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
情况 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
obj.wait(long n) ,情况2的有时限版,只是退出的条件多了一个等待时间超过了 n 毫秒
情况 6 RUNNABLE <--> TIMED_WAITING
t.join(long n) ,情况3的有时限版,只是退出的条件多了一个等待时间超过了 n 毫秒
情况 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) ,情况4的有时限版,只是退出的条件多了一个等待时间超过了 n 毫秒
情况 9 RUNNABLE <--> BLOCKED
- t 线程用 synchronized(obj) 获取对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED
- 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争成功,从 BLOCKED --> RUNNABLE ,其它失败的线程仍然 BLOCKED
情况 10 RUNNABLE <--> TERMINATED
当前线程所有代码运行完毕,进入 TERMINATE
多把锁与活跃性
1.死锁问题
为了并发度更高,将锁的粒度设置得更小后,如果一个线程需要同时获取多把锁时,就会容易产生死锁
互相持有并等待
避免死锁要注意加锁顺序
另外如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下
可以通过 top 先定位到 CPU 占用高的 Java 进程,再利用 top -Hp 进程id 来定位是哪个线程,最后再用 jstack 排查
定位死锁
检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁:
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) - 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.
哲学家就餐问题
如果每个哲学家都同时拿起左手的筷子,那么就形成了环形依赖,在这种特殊的情况下,每个人都拿着左手的筷子,都缺少右手的筷子,那么就没有人可以开始吃饭了,自然也就没有人会放下手中的筷子。这就陷入了死锁,形成了一个相互等待的情况。
多种解决方案
(1)服务员检查
第一个解决方案就是引入服务员检查机制。比如我们引入一个服务员,当每次哲学家要吃饭时,他需要先询问服务员:我现在能否去拿筷子吃饭?此时,服务员预先判断他拿筷子有没有发生死锁的可能,假如有的话,服务员会说:现在不允许你吃饭。这是一种解决方案
(2)领导调节
死锁检测和恢复策略,可以引入一个领导,这个领导进行定期巡视。如果他发现已经发生死锁了,就会剥夺某一个哲学家的筷子,让他放下。这样一来,由于这个人的牺牲,其他的哲学家就都可以吃饭了。这也是一种解决方案
(3)改变一个哲学家拿筷子的顺序
我们还可以利用死锁避免策略,那就是从逻辑上去避免死锁的发生,比如改变其中一个哲学家拿筷子的顺序。我们可以让 4 个哲学家都先拿左边的筷子再拿右边的筷子,但是有一名哲学家与他们相反,他是先拿右边的再拿左边的,这样一来就不会出现循环等待同一边筷子的情况,也就不会发生死锁了
ABCDE 五个哲学家,他们中间12345无双筷子,都是先拿左手边再拿右手边,那么顺序是:
A 1 2 --- B 2 3 --- C 3 4 --- D 4 5 --- E 5 1
改变 E 的拿筷子顺序,由 5 1 变为 1 5
A 1 2 --- B 2 3 --- C 3 4 --- D 4 5 --- E 1 5
(4)锁超时
获取不到锁就直接放弃,或者等待一段时间后没获得再放弃。如用 ReentranedLock 的 tryLock() 方法
@Override public void run() { while (true) { // 尝试获得左手筷子 if (left.tryLock()) { try { // 尝试获得右手筷子 if (right.tryLock()) { try { eat(); } finally { right.unlock(); } } } finally { left.unlock(); } } } }
2.活锁问题
活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束,例如
解决:
引入随机性
public class TestLiveLock {
static volatile int count = 10;
static final Object lock = new Object();
public static void main(String[] args) { new Thread(() -> { // 期望减到 0 退出循环 while (count > 0) { sleep(0.2); count--; log.debug("count: {}", count); } }, "t1").start();
new Thread(() -> { // 期望超过 20 退出循环 while (count < 20) { sleep(0.2); count++; log.debug("count: {}", count); } }, "t2").start(); } }
3.饥饿问题
很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束,饥饿的情况不 易演示,讲读写锁时会涉及饥饿问题。
在实际情况中,如果不是顺序加锁,容易死锁,如线程1先对A加锁再对B加锁,线程2先对B加锁再对A加锁,那么这种情况就容易发生死锁
而如果改为顺序加锁,都是先对A加锁再对B加锁,则容易出现饥饿问题:看看他举的哲学家就餐例子
ABCDE 五个哲学家,他们中间12345无双筷子,都是先拿左手边再拿右手边,那么顺序是:
A 1 2 --- B 2 3 --- C 3 4 --- D 4 5 --- E 5 1
改变 E 的拿筷子顺序,由 5 1 变为 1 5
A 1 2 --- B 2 3 --- C 3 4 --- D 4 5 --- E 1 5
ReentrantLock
相对于 synchronized 它具备如下特点
- 可打断
- 可以设置超时时间
- 可以设置为公平锁
- 支持多个条件变量
与 synchronized 一样,都支持可重入
基本用法
// 获取锁 reentrantLock.lock(); try { // 临界区 } finally { // 释放锁 reentrantLock.unlock(); }
可重入
可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
可打断
我在进入 Blocked list 等待锁的过程中,别的线程可以用 interupt() 方法打断我的等待(sychronized不可打断,reentrantLock.lock() 不可打断,reentrantLock.lockInterruptibly() 才可以打断)
被打断后会抛出 InteruptException 异常
可以说是一种 被动地避免死等的手段
锁超时
lock.tryLock() 没有参数的话,如果没获得锁,立刻返回false
有超时参数的话,等待一段时间后还没获得锁,再返回false
ReentrantLock lock = new ReentrantLock(); Thread t1 = new Thread(() -> { log.debug("启动..."); try { if (!lock.tryLock(1, TimeUnit.SECONDS)) { log.debug("获取等待 1s 后失败,返回"); return; } } catch (InterruptedException e) { e.printStackTrace(); } try { log.debug("获得了锁"); } finally { lock.unlock(); } }, "t1");
lock.lock(); log.debug("获得了锁"); t1.start(); try { sleep(2); } finally { lock.unlock(); }
用 tryLock 解决哲学家就餐问题
@Override public void run() { while (true) { // 尝试获得左手筷子 if (left.tryLock()) { try { // 尝试获得右手筷子 if (right.tryLock()) { try { eat(); } finally { right.unlock(); } } } finally { left.unlock(); } } } }
公平锁
非公平锁:之前持有锁的线程释放了锁,那么所有在 entryList 的线程一拥而上,谁先抢到了谁就是主人,而不是谁先到谁就先得
公平锁:阻塞队列里的线程抢锁时,是按进入阻塞的时间先入先得的顺序获得锁
本意是为了减少饥饿问题,但是 tryLock 可能更好。一般不会使用公平锁,因为会降低并发度(后面原理会讲?)
ReentrantLock 默认是非公平锁,如果要是公平锁要显式设置
ReentrantLock lock = new ReentrantLock(true);
条件变量
synchronized 中也有条件变量,就是获取了 sychronized 获取锁后可以调用 .wait() ,当条件不满足时进入 waitSet 等待
ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比 synchronized 是那些不满足条件的线程都在一间休息室等消息
而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒
与 sychronized 配合的 wait() 类似,使用要点:
- await 前需要获得 ReentrantLock 锁
- await 执行后,会释放锁,进入 conditionObject 等待
- await 的线程被 .signal() .signalAll() 唤醒(或打断、或超时)时重新竞争 lock 锁
- 竞争 lock 锁成功后,从 await 后继续执行
用法:
lock 有一点就是一定要在 finally 里释放,lock 和 unlock 成对出现
static ReentrantLock roomlock = new ReentrantLock(); static Condition waitCigaretteQueue = lock.newCondition(); static Condition waitbreakfastQueue = lock.newCondition(); static volatile boolean hasCigrette = false; static volatile boolean hasBreakfast = false;
public static void main(String[] args) { new Thread(() -> { try { roomlock.lock(); while (!hasCigrette) { try { waitCigaretteQueue.await(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("等到了它的烟"); } finally { lock.unlock(); } }).start();
new Thread(() -> { try { roomlock.lock(); while (!hasBreakfast) { try { waitbreakfastQueue.await(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("等到了它的早餐"); } finally { lock.unlock(); } }).start();
sleep(1); sendBreakfast(); sleep(1); sendCigarette(); }
private static void sendCigarette() { roomlock.lock(); try { log.debug("送烟来了"); hasCigrette = true; waitCigaretteQueue.signal(); } finally { lock.unlock(); } }
private static void sendBreakfast() { roomlock.lock(); try { log.debug("送早餐来了"); hasBreakfast = true; waitbreakfastQueue.signal(); } finally { lock.unlock(); } }
本章小结
Monitor 有的也被翻译为管程。sychronized 是 jvm 用 c++ 使用的 monitor ,ReentrandLock 是用 java 语言使用的 Monitor
本章我们需要重点掌握的是
- 分析多线程访问共享资源时,哪些代码片段属于临界区
- 使用 synchronized 互斥解决临界区的线程安全问题
- 掌握 synchronized 锁对象语法
- 掌握 synchronzied 加在成员方法和静态方法语法(成员方法锁 this 对象,静态方法锁 class 对象)
- 掌握 wait/notify 同步方法
- 使用 lock 互斥解决临界区的线程安全问题
- 掌握 lock 的使用细节:可打断(.lockInterruptibly())、锁超时(.tryLock())、公平锁、条件变量(reentrandLock 比 sychronized 的强处)
- 学会分析变量的线程安全性、掌握常见线程安全类的使用
- 了解线程活跃性问题:死锁、活锁、饥饿
- 应用方面
- 互斥:使用 synchronized 或 Lock 达到共享资源互斥效果
- 同步:使用 wait/notify (sychronized住的对象.wait() sychronized住的对象.notify())或 Lock 的条件变量(condition = reentrandLock.newCondition() condition.await() condition.signal())来达到线程间通信效果
- 原理方面
- monitor、synchronized 、wait/notify 原理
- synchronized 进阶原理
- park & unpark 原理
- 模式方面
- 同步模式之保护性暂停
- 异步模式之生产者消费者
- 同步模式之顺序控制