Java多线程——共享模型之管程
一、共享带来的问题
1.1 临界区
- 一个程序运行多个线程本身是没有问题的
- 问题会出现在多个线程访问共享资源
- 多个线程
读共享资源
不会发生问题 - 多个线程对
共享资源读写操作
时发生指令交错,会出现问题
- 多个线程
- 一段代码块内若存在对
共享资源
的多线程读写操作,则称这段代码为临界区
代码示例:
static int counter = 0;
static void increment() {
// 临界区
counter++;
}
// 这里的counter++虽然只有一行代码,但不是源自操作,这实际上是多步操作,而且是可被中断的;
// counter可被拆违三步操作:取出counter值,执行counter+1,将结果赋值给counter
1.2 竞态条件
多个线程在临界区内执行,由于代码的执行序列不同
而导致结果无法预测,称之为发生了竞态条件
二、synchronized解决方案
为了避免临界区的竞态条件发生,有多种手段可以达到目的:
- 阻塞式的解决方案:
synchronized
,Lock
- 非阻塞式的解决方案:
原子变量
本次使用阻塞式的解决方案:synchronized
,来解决上述问题,俗称【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其他线程在想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换问题。
ps:虽然Java中互斥和同步都可以采用synchronized关键字来完成,但他们还是有区别的:
- 互斥是保证临界区的竞态条件发生,同一时刻只能由一个线程执行临界区代码
- 同步是由于线程执行的先后、顺序不同,需要一个线程等待其他线程运行到某个点
2.1 synchronized
语法
synchronized(对象) {
临界区
}
代码示例
static int counter = 0;
// 创建一个公共对象来作为锁对象
static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
// 线程t1
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (lock) {
counter++;
}
}
}, "t1");
// 线程t2
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (lock) {
counter--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.info("counter: {}", counter);
}
2.2 synchronized加在方法上
- 加在成员方法上
public class Test {
//在方法上加上synchronized关键字
public synchronized void test() {
}
//等价于
public void test() {
synchronized(this) {
}
}
}
- 加在静态方法上
public class Test {
//在静态方法上加上synchronized关键字
public synchronized static void test() {
}
//等价于
public void test() {
synchronized(Test.class) {
}
}
}
三、变量的线程安全分析
成员变量和静态变量是否线程安全?
- 如果它们没有共享,则线程安全
- 如果它们被共享了,根据它们的状态是否能够改变,又分为两种情况
- 如果只有
读操作
,则线程安全 - 如果有
读写操作
,则这段代码是临界区,需要考虑线程安全
- 如果只有
局部变量是否线程安全?
- 局部变量是线程安全的
- 但局部变量引用的对象则未必
- 如果该对象没有逃离方法的作用范围,它是线程安全的
- 如果该对象逃离方法的作用范围,需要考虑线程安全问题
成员变量不是线程安全的,但有一些线程安全的类
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- java.util.concurrent包下的类
这里说它们是线程安全的是指,多个线程调用它们同一实例的某个方法时,是线程安全的。举个例子
Hashtable<String, String> table = new Hashtable<>();
new Thread(() -> {
table.put("key", "value1");
}).start();
new Thread(() -> {
table.put("key", "value2");
}).start();
四、Monitor概念
Monitor是操作系统来提供的,可翻译成监视器
或管程
首先来看一下Java的对象头
以32位虚拟机为例
普通对象
数组对象
其中Mark Word结构为
64位虚拟机的Mark Word
每个Java对象都可以关联一个Monitor对象,如果使用synchronized
给对象上锁(重量级)之后,该对象头的mark Word中就被设置只想Monitor对象的指针
Monitor结构如下
- 刚开始Monitor中的Owner为null
- 当Thread-2执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2, Monitor中只能有一个 Owner
- 在Thread-2 上锁的过程中,如果Thread-3, Thread-4, Thread-5 也来执行 synchronized(obj),就会进入 EntryList BLOCKED
- Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争时是非公平的
- 图中 WaitSet 中的 Thread-0, Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程
注意
- synchronized 必须是进入同一对象的 monitor才有上述的效果
- 不加 synchronized 的对象不会关联监视器,不遵循以上规则
五、Wait/Notify
原理
- Owner 线程发现条件不满足时,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
- BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
- BLOCKED 线程会在 Owner 线程释放锁时唤醒
- WAITING 线程会在 Owner线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味着立刻获得锁,仍需进入 EntryList 重新竞争
常用API介绍:
obj.wait()
:让进入 object 监视器的线程到 waitSet 等待obj.notify()
:让 object 上正在waitSet 等待的线程中挑一个唤醒obj.notifyAll()
:让 object 傻姑娘正在 waitSet 等待的线程全部唤醒
它们都是线程之间进行协作的手段,都属于 Object 对象的方法。所以必须获得此对象的锁,才能调用这几个方法。
static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (lock) {
log.info("执行...");
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info("其他代码...");
}
}).start();
new Thread(() -> {
synchronized (lock) {
log.info("执行...");
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info("其他代码...");
}
}).start();
Thread.sleep(2000);
log.info("唤醒 lock 上其他线程");
synchronized (lock) {
//lock.notify(); // 随机唤醒lock上一个线程
lock.notifyAll(); // 唤醒lock上所有线程
}
}
六、wait notify的正确使用
sleep(long n)
和wait(long n)
的区别
- sleep 是 Thread 的方法,而 wait 是 Object 的方法
- sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起使用
- sleep 在睡眠的同时,不会释放对象锁,但 wait 在等待的时候会释放对象锁
- 它们的状态都是 TIMED_WAITING
使用 wait 和 notify 时,一般会进行条件判断,不过不使用 if ,使用 while 进行判断,这是为了防止虚假唤醒,当条件真正满足时,才继续执行后续的代码
6.1 同步模式之保护性暂停
6.1.1 定义
保护性暂停,即 Guarded Suspension,用在一个线程等待另一个线程的执行结果
要点:
- 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个 GuardedObject
- 如果有结果不断从一个线程到另一个线程可以使用消息队列(生产者/消费者)
- 在JDK中,join 的实现、Future 的实现,采用的就是此模式
- 因为要等待另一方的结果,因此归类到同步模式
6.1.2 实现
public class GuardedObject {
private Object response;
private final Object lock = new Object();
public Object get() {
synchronized (lock) {
// 条件不满足则等待
while (response == null) {
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
return response;
}
}
public void complete(Object response) {
synchronized (lock) {
this.response = response;
// 条件满足,通知等待线程
lock.notifyAll();
}
}
}
应用
一个线程等待另一个线程的执行结果
publicstatic void main(String[] args) {
GuardedObject guardedObject = new GuardedObject();
new Thread(() -> {
List<String> responses = new ArrayList<>();
responses.add("test01");
responses.add("test02");
log.info("数据准备完毕");
guardedObject.complete(responses);
}).start();
log.info("waiting...");
Object response = guardedObject.get();
log.info("get response: [{}] lines", ((List<String>)response).size());
}
6.1.3 带超时版的GuardedObject
如果要控制超时时间
public class GuardedObjectV2 {
private Object response;
private final Object lock = new Object();
public Object get(long millis) {
synchronized (lock) {
// 1. 记录最初时间
long begin = System.currentTimeMillis();
// 2. 已经经历的时间
long timePassed = 0;
while (response == null) {
long waitTime = millis - timePassed;
log.info("waitTime: {}", waitTime);
if (waitTime <= 0) {
log.info("break...");
break;
}
try {
lock.wait(waitTime);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
timePassed = System.currentTimeMillis() - begin;
log.info("timePassed: {}, object is null {}", timePassed, response == null);
}
return response;
}
}
public void complete(Object response) {
synchronized (lock) {
// 条件满足,通知等待线程
this.response = response;
log.info("notify...");
lock.notifyAll();
}
}
}
join原理
join 内部实现就是这种保护性暂停方式,翻看 join 底层代码可知
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;
}
}
}
6.1.4 多任务版GuardedObject
图中 Futures 就好比居民楼一层的信箱(每个信箱有房间编号),左侧的 t0,t2,t4 就好比等待邮件的居民,右侧的t1,t3,t5 就好比邮递员
如果需要在多个类之间使用GuarderObject 对象,作为参数传递不是很方便,因此设计一个用来解耦的中间类,这样不仅能够解耦【结果等待者】和【结果生产者】,还能够同时支持多个任务的管理
6.2 异步模式之生产者消费者
6.2.1 定义
要点:
- 与前面的保护性暂停中的 GuardObject 不同,不需要产生结果和消费结果的线程一一对应
- 消费队列可以用来平衡生产和消费的线程资源
- 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据
- 消息队列是有容量限制的,满时不会再介入数据,空时不会再消耗数据
- JDK中各种阻塞队列,采用的就是这种模式
6.2.2 实现
@Data
public class Message {
private int id;
private Object message;
}
@Data
@Slf4j
public 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.info("没货了,wait");
try {
queue.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
Message message = queue.removeFirst();
queue.notifyAll();
return message;
}
}
public void put(Message message) {
synchronized (queue) {
while (queue.size() == capacity) {
log.info("库存已达上限,wait");
try {
queue.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
七、Park & Unpark
7.1 基本使用
park 和 unpark 都是 LockSupport 类中的方法
park()
:暂停当前线程unpark(暂停线程对象)
:恢复某个线程的运行
先 park 再 unpark
@Slf4j
public class Demo01 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.info("start...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info("park...");
LockSupport.park();
log.info("resume...");
}, "t1");
t1.start();
Thread.sleep(2000);
log.info("unpark...");
LockSupport.unpark(t1);
}
/**
* output
* 2022-12-11 16:23:11.618 INFO [t1] - start...
* 2022-12-11 16:23:12.625 INFO [t1] - park...
* 2022-12-11 16:23:13.624 INFO [main] - unpark...
* 2022-12-11 16:23:13.628 INFO [t1] - resume...
*/
}
先 unpark 再 park
@Slf4j
public class Demo01 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.info("start...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info("park...");
LockSupport.park();
log.info("resume...");
}, "t1");
t1.start();
Thread.sleep(1000);
log.info("unpark...");
LockSupport.unpark(t1);
}
/**
* output
* 2022-12-11 16:24:55.900 INFO [t1] - start...
* 2022-12-11 16:24:56.901 INFO [main] - unpark...
* 2022-12-11 16:24:57.905 INFO [t1] - park...
* 2022-12-11 16:24:57.907 INFO [t1] - resume...
*/
}
7.2 特点
与 Object 的 wait & notify 相比
- wait, notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必
- park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么【精确】
- park & unpark 可以先 unpark,而 wait & notify 不能先 notify
7.3 原理
每个线程都有自己的一个 Parker对象,由三部分组成_counter
,_cond
和_mutex
- 当前线程调用 Unsafe.park() 方法
- 检查 _counter,本情况为0,这时,获得 _mutex 互斥锁
- 线程进入 _cond 条件变量阻塞
- 设置 _counter = 0
- 调用 Unsafe.unpark(Thread_0)方法,设置 _counter 为1
- 唤醒 _cond 条件变量中的 Thread_0
- Thread_0 恢复运行
- 设置 _counter 为0
- 调用 Unsafe.unpark(Thread_0)方法,设置 _counter 为1
- 当前线程调用 Unsafe.park() 方法
- 检查 _counter,本情况为 1,这时线程无需阻塞,继续运行
- 设置 _counter 为 0
八、活跃性问题
常见的活跃性问题由死锁
、活锁
和饥饿
8.1 死锁
死锁、是指两个或两个以上线程(or进程)执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用干预,它们都死等下去,也都无法向下推进
产生死锁的必要条件
- 互斥条件:所谓互斥即线程再某一时间独占资源
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源持有不放
- 不剥夺条件:线程已获得资源,在未使用结束前,不能强行剥夺
- 循环等待条件:若干线程之间形成一种头尾相连的循环等待资源关系
经典案例:哲学家就餐问题
8.2 活锁
活锁的场景比较少见,也很难模拟,就是在线程在没有被阻塞情况下,但由于某些条件未能满足,导致一直重复尝试->失败,尝试->失败,如此下去
活锁和死锁的区别:处于活锁的实体是在不断的改变状态,处于死锁的实体变现为等待;活锁可能自行解开,死锁不能自行解开。
8.3 饥饿
一个或多个线程,因为种种原因无法获得所需要的资源,导致一直无法执行的状态
java中导致饥饿的原因
- 高优先级线程吞噬所有低优先级线程的CPU时间
- 线程被永久阻塞在等待进入同步块的状态,因其他线程总是能在它之前持续地对该同步块进行访问
- 线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的wait方法),因为其他线程总是被持续地唤醒
九、ReentrantLock
相对于 synchronized,它具备如下特点:
- 可中断
- 可以设置超时时间
- 可以设置为公平锁
- 支持多个条件变量
与 synchronized 一样,都支持可重入
基本语法:
// 获取锁
reentrantLock.lock();
try {
// 临界区
method();
} finally {
// 释放锁
reentrantLock.unlock();
}
9.1 可重入
可重入是指同一线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁;如果是不可重入锁,那么第二次获得锁时,自己也会被锁住
@Slf4j
public class Demo02 {
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
method1();
}
public static void method1() {
lock.lock();
try {
log.info("execute method1");
method2();
} finally {
lock.unlock();
}
}
public static void method2() {
lock.lock();
try {
log.info("execute method2");
method3();
} finally {
lock.unlock();
}
}
public static void method3() {
lock.lock();
try {
log.info("execute method3");
} finally {
lock.unlock();
}
}
/**
* output
* 2022-12-11 17:10:51.442 INFO [main] - execute method1
* 2022-12-11 17:10:51.443 INFO [main] - execute method2
* 2022-12-11 17:10:51.443 INFO [main] - execute method3
*/
}
9.2 可打断
@Slf4j
public class Demo02 {
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
log.info("启动...");
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
try {
Thread.sleep(500);
} catch (InterruptedException ex) {
throw new RuntimeException(ex);
}
log.info("等待锁的过程被打断");
return;
}
try {
log.info("获得了锁");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
log.info("获得了锁");
t1.start();
try {
Thread.sleep(1000);
t1.interrupt();
log.info("执行打断");
} finally {
lock.unlock();
}
}
/**
* output
* 2022-12-11 17:24:02.847 INFO [main] - 获得了锁
* 2022-12-11 17:24:02.849 INFO [t1] - 启动...
* 2022-12-11 17:24:03.853 INFO [main] - 执行打断
* java.lang.InterruptedException
* at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
* at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
* at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
* at com.liu.concurrency.chapter03.Demo02.lambda$main$0(Demo02.java:16)
* at java.lang.Thread.run(Thread.java:748)
* 2022-12-11 17:24:04.360 INFO [t1] - 等待锁的过程被打断
*/
}
9.3 锁超时
立刻失败
@Slf4j
public class Demo02 {
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
log.info("启动...");
if (!lock.tryLock()) {
log.info("获取立刻失败,返回");
return;
}
try {
log.info("获得了锁");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
log.info("获得了锁");
t1.start();
try {
Thread.sleep(2000);
} finally {
lock.unlock();
}
}
/**
* output
* 2022-12-11 17:34:50.784 INFO [main] - 获得了锁
* 2022-12-11 17:34:50.786 INFO [t1] - 启动...
* 2022-12-11 17:34:50.787 INFO [t1] - 获取立刻失败,返回
*/
}
超时失败
@Slf4j
public class Demo02 {
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
log.info("启动...");
try {
if (!lock.tryLock(1, TimeUnit.SECONDS)) {
log.info("获取等待 1s 后失败,返回");
return;
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
try {
log.info("获得了锁");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
log.info("获得了锁");
t1.start();
try {
Thread.sleep(2000);
} finally {
lock.unlock();
}
}
/**
* output
* 2022-12-11 17:48:36.919 INFO [main] - 获得了锁
* 2022-12-11 17:48:36.921 INFO [t1] - 启动...
* 2022-12-11 17:48:37.928 INFO [t1] - 获取等待 1s 后失败,返回
*/
}
9.4 公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
默认的ReentrantLock
是非公平锁,可以通过new TeentrantLock(true)
来获得公平锁
9.5 条件变量
synchronized 中也有条件变量,就是 waitSet,当条件不满足时进入 waitSet 等待
ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比
- synchronized 是那些不满足条件的线程都在一间休息室等消息
- 而 TeentrantLock支持多间休息室
使用要点
- await 前需要获得锁
- await 执行后,会释放锁,进入conditionObject 等待
- await 的线程被唤醒(或打断、超时),需重新竞争lock锁
- 竞争 lock 锁成功后,从 await 后继续执行
示例:一个可共享的缓冲区实现
public class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();//写线程条件
final Condition notEmpty = lock.newCondition();//读线程条件
final Object[] items = new Object[15];
int putptr/*写索引*/, takeptr/*读索引*/, count/*队列中存在的数据个数*/;
public void put(Object x) throws InterruptedException {
System .out.println("put wait lock");
lock.lock();
System.out.println("put get lock");
try {
while (count == items.length) {
System.out.println("buffer full, please wait");
notFull.await();
}
items[putptr] = x;
if (++putptr == items.length) {
putptr = 0;
}
++count;
System.out.println("--------------------"+x);
notEmpty.signal();
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
System.out.println("take wait lock");
lock.lock();
System.out.println("take get lock");
try {
while (count == 0) {
System.out.println("no elements, please wait");
notEmpty.await();
}
System.out.println("--------------------被唤醒");
Object x = items[takeptr];
if (++takeptr == items.length) {
takeptr = 0;
}
--count;
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}