该栏目讲叙多线程基础、共享模型的内存、管程、无锁、不可变设计和多线程并发工具
文章目录
多线程共享同一资源问题
1、问题简述
- :多个线程同时共享一个成员变量,且都对该变量进行非原子性的写操作,由于分时调度系统,引发线程上下文切换,无法保证非原子性的写操作一定将结果写入内存,所以其他线程操作该变量时不一定正确的问题
2、问题举例
/**
* 线程安全问题:由于分时系统,引发线程上下文切换,导致线程安全问题
*/
public class SafeThreadQuestion {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
count++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
count--;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = " + count);
}
}
3、问题分析
- 从字节码层面理解 ++i(java 对静态变量的自增、自减不是原子操作)
getstatic i // 获取静态变量的值i
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i
- java 的内存模型理解:完成静态变量的自增、自减需要在主存和工作内存中进行数据交换
4、相关概念
临界区
:存在多线程共享问题的代码块,称这段代码块为临界区
static int count = 0;
static void increment()
// 临界区
{
count++;
}
static void decrement()
// 临界区
{
count--;
}
竞态条件
:多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
5、线程安全分析
成员变量
- 成员变量没有被共享,则线程安全
- 成员变量被共享,但只有读操作,则线程安全;如果读写操作都有,则考虑线程安全
静态变量
- 静态变量没有被共享,则线程安全
- 静态变量被共享,但只有读操作,则线程安全;如果读写操作都有,则考虑线程安全
局部变量
- 在调用方法中时,局部变量在每个线程的栈帧内存中被创建,所以是线程安全的。但局部变量的引用对象则未必
- 对象没有离开方法作用范围,则线程安全
- 对象离开方法作用范围,则考虑线程安全(子类重写方法,并启动新的线程;将引用对象返回)
- 在调用方法中时,局部变量在每个线程的栈帧内存中被创建,所以是线程安全的。但局部变量的引用对象则未必
6、解析方案
阻塞式
:synchronized、Lock非阻塞式
:原子变量
7、synchronized 解决方案
概述
:它采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其他线程想获取这个对象锁就会被阻塞,这样就保证了临界区代码的原子性,不用担心线程上下文切换注意
- 互斥是保证临界区的竞态条件不发生,同一时刻只有一个线程执行临界区代码
- 同步量由于线程执行的先后,顺序不同,需要一个线程等待其他线程运行到这个点
语法
对象头
普通对象
:Mark Word(32bit) + Klass Word(32bit)
数组对象
:Mark Word(32bit) + Klass Word(32bit)+ 数组长度(32bit)
Monitor
概述
:即管程,是由系统所创建的。每个 Java 对象都可以关联一个 Monitor 对象,当使用 synchronized 给对象上锁之后,该锁对象的 Mark Word 就会指向该 Monitor 对象工作原理
- 当 Thread-2 线程执行到 synchronized 就会将 Monitor 的 Owner 置为 Thread-2
- 在 Thread-2 上锁的过程中,如果 Thread-3、Thread-4、Thread-5 也来执行 synchronized,就会进入 EntryList,线程状态为 BLOCKED
- 在 Thread-2 执行完同步代码块的内容后,恢复对象锁的 Mark Word 并释放锁,唤醒 EntryList 中等待的线程来竞争锁
- Thread-0 和 Thread-1 是之前获得过锁,但条件不满足进入WaitSet,线程状态为 WAITING
synchronized 优化原理
1、轻量级锁
场景
:锁对象虽然被多个线程访问,但多线程之间访问的时间是错开的(锁之间没有竞争),那么可以使用轻量级锁来优化举例
static final Object obj = new Object();
public static void method1(){
synchronized(obj){
method2();
}
}
public static void method2(){
synchronized(obj){
}
}
原理
- 当执行到 synchronized 时,每个线程的栈帧都会创建一个锁记录,其内部可以存储锁对象的 Mark Word
- 让锁记录中的 Ojbect reference 指向锁对象,并尝试用CAS替换锁对象的 Mark Word,将 Mark Word 的值存入锁记录
- 如果 CAS 替换成功,对象头中存储了锁记录地址和状态 00,表示由该线程给对象加锁
- 如果 CAS 失败,有两种情况
- 如果是其他线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
- 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数
- 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
- 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 CAS 将 Mark Word 的值恢复给对象头
- 成功,则解锁成功
- 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级解锁流程
- 当执行到 synchronized 时,每个线程的栈帧都会创建一个锁记录,其内部可以存储锁对象的 Mark Word
2、锁膨胀
场景
:在尝试加轻量级锁过程中,如果 CAS 操作无法成功,这时有一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进入锁膨胀,将轻量级锁变为重量级锁过程
- 当 Thread-1 进入轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
- 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
- 即为 Ojbect 对象申请 Monitor 锁,让 Ojbect 指向重量级锁地址
- 然后自己进入 Monito r的 EntryList
- 当 Thread-0 退出同步块解锁时,使用 CAS 将 Mark Word 的值恢复给对象头,失败,这时会进入重量级锁解锁流程,即按 照Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中的 BLOCKED 线程
- 当 Thread-1 进入轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
3、自旋优化
概述
:重量级锁竞争的时候,还可以使用自旋来进行优化,如果当线程自旋成功(即这时候持锁对象已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞注意
- 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势
- 在 Java 6 之后自旋是自适应的,Java 7 之后不能控制是否开户自旋功能
4、偏向锁
概述
:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS,以后只要不发生竞争,这个对象就归该线程所有偏向状态
- 开启偏向锁,对象创建后,其 Mark Word 的最后 3 位为 101,这时它的 thread、epoch、age 为 0
- 如果没有开启偏向锁,那么创建对象后,其 Mark Word 的最后 3 位为 001,这时它的hashcode,age都为0,第一次用到 hashcode 时才会赋值
- 偏向锁默认是延迟的,如果想禁用延迟,可以加 VM 参数 -xx:BiasedLockingStartupDelay = 0
撤销
- 调用对象的 hashcode
- 其他线程使用偏向锁对象
- 调用 wait / notify
批量重偏向
:如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍有机会重新偏向T2,重偏向会重置对象的Thread ID,当撤销偏向锁阈值超过20次后,JVM会这样觉得,我是不是偏向错了呢,于是会给这些对象加锁时重新偏向到加锁线程批量撤销
:当撤销偏向锁阈值超过40次后,JVM会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都为变为不可偏向的,新建的对象也是不可偏向的
5、锁清除
- :JIT(即时编程器)会对Java的字节码文件进行进一步的优化。如果加锁的对象在方法内,且没有离开方法范围,则会将锁清除,提高效率
等待唤醒机制
1、相关 API
描述 | 方法 |
---|---|
持锁线程会释放对象的锁,并进入 waitSet 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到调用 notify() 为止 | wait() |
有时限的等待, 到 n 毫秒后结束等待,或是被 notify() 为止 | wait(long n) |
唤醒 waitSet 等待中的一个线程 | notify() |
唤醒 waitSet 等待中的所有线程 | notifyAll() |
2、wait & sleep 的区别
- sleep 是 Thread 方法,而 wait 是 Object 的方法
- sleep 不需要强制与 synchronized 配合使用,但 wait 需要
- sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁
- 它们状态都是 TIMED_WAITING
3、使用方式
synchronized(lock) {
while(条件不成立) {
lock.wait();
}
// 干活
}
// 另一个线程
synchronized(lock) {
lock.notifyAll();
}
4、wait & notify 原理
- Owner 线程发现条件不满足,调用 wait() 方法,即可进入 waitSet 变为 WAITING 状态
- BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
- BLOCKED 线程会在 Owner 线程释放锁时唤醒 WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入 EntryList 重新竞争
5、保护性暂停模式
描述
:一个线程等待另一个线程的结果实现
/**
* 资源实体
*/
@Getter
public class Resource<T> {
private final int resId;
private final T content;
public Resource(int resId, T content) {
this.resId = resId;
this.content = content;
}
}
///
/**
* 资源警卫
*/
public class ResourceGuard<T> {
private Resource<T> resource;
/**
* 获取资源
*
* @return 资源
*/
public Resource<T> getResource() {
synchronized (this) {
while (resource == null) {
try {
System.out.println("资源耗尽,等待资源生产...");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return resource;
}
}
/**
* 获取资源,带超时
*
* @param waitTime 超时时间
* @return 资源
*/
public Resource<T> getResource(long waitTime) {
synchronized (this) {
final long startTime = System.currentTimeMillis();
long spendTime = 0;
while (true) {
waitTime = waitTime - spendTime;
if (waitTime <= 0) {
System.out.println("等待超时...");
break;
}
try {
this.wait(waitTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
spendTime = System.currentTimeMillis() - startTime;
}
return resource;
}
}
/**
* 生产资源
*
* @param content 内容
*/
public void produceResource(T content) {
synchronized (this) {
System.out.println("生产资源...");
this.resource = new Resource<>((int) (Math.random() * 100), content);
this.notifyAll();
}
}
}
///
/**
* 测试类
*/
public class TestDemo {
public static void main(String[] args) {
ResourceGuard<String> resourceGuard = new ResourceGuard<>();
new Thread(() -> {
final Resource<String> resource = resourceGuard.getResource(3000);
if (resource != null) {
System.out.println(resource.getResId() + "==" + resource.getContent());
}
}, "T1").start();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
resourceGuard.produceResource("Hello Java");
}, "T2").start();
}
}
6、生产者消费者模式
描述
- 消费队列可以用来平衡生产和消费的线程资源
- 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据
- 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据
实现
/**
* 消息实体
*/
@Getter
public class Message {
private final int msgId;
private final String content;
public Message(int msgId, String content) {
this.msgId = msgId;
this.content = content;
}
}
///
/**
* 消息队列
*/
@Setter
public class MessageQueue {
private final LinkedList<Message> queue;
private int capacity;
public MessageQueue(int capacity) {
this.capacity = capacity;
this.queue = new LinkedList<>();
}
public Message take() {
synchronized (queue) {
if (queue.isEmpty()) {
try {
System.out.println("消息队列没有消息,等待生产...");
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
final Message message = queue.removeFirst();
queue.notifyAll();
return message;
}
}
public void putMessage(Message message) {
synchronized (queue) {
if (queue.size() == capacity) {
try {
System.out.println("消息队列已满了,等待消费");
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.add(message);
queue.notifyAll();
}
}
}
///
/**
* 测试类
*/
public class TestDemo {
public static void main(String[] args) {
MessageQueue queue = new MessageQueue(2);
new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
Message message = new Message(i, "Hello " + i);
queue.putMessage(message);
}
}, "producer").start();
new Thread(() -> {
while (true) {
final Message message = queue.take();
System.out.println(message.getMsgId() + "==" + message.getContent());
}
}, "consumer").start();
}
}
park & unpark
1、简介
- wait & notify 和 notifyAll 必须配合 Object Monitor 一起使用,而park&unpark不必
- park & unpark 是以线程为单位来阻塞和唤醒线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么精确
- park & unpark 可以先 unpark,而 wait & notify 不能先 notify
2、原理
-
先 park 后 unpark
-
先 unpark 后 park
3、实现
public class ParkAndUnParkDemo {
public static void main(String[] args) {
final Thread t1 = new Thread(() -> {
System.out.println("开始执行...");
LockSupport.park();
System.out.println("执行结束...");
}, "T1");
final Thread t2 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(5);
LockSupport.unpark(t1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "T2");
t1.start();
t2.start();
}
}
活跃性
1、死锁
概述
:两个线程互相等待对方锁释放定位死锁
:jstack 进程 ID 命令或 jconsole 工具解决死锁
:顺序加锁、使用超时锁举例
public class DeadLockDemo {
private static final Object objA = new Object();
private static final Object objB = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (objA) {
try {
System.out.println("lock A");
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (objB) {
System.out.println("lock B");
}
}
}, "T1");
Thread t2 = new Thread(() -> {
synchronized (objB) {
try {
System.out.println("lock B");
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (objA) {
System.out.println("lock B");
}
}
}, "T2");
t1.start();
t2.start();
}
}
2、活锁
概述
:两个线程互相改变对方的结束条件,最后谁也无法结束举例
public class LiveLock {
private static volatile int count = 10;
private static final Object obj = new Object();
public static void main(String[] args) {
new Thread(() -> {
while (count > 0) {
synchronized (obj) {
try {
TimeUnit.MILLISECONDS.sleep(200);
count--;
System.out.println("count=" + count);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(() -> {
while (count < 20) {
synchronized (obj) {
try {
TimeUnit.MILLISECONDS.sleep(200);
count++;
System.out.println("count=" + count);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
3、饥饿
- :一个线程的优先级太低,始终得不到 CPU 调度执行,也不能够结束
ReentrantLock
1、特性
可重入
:指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁可中断
:reentrantLock.lockInterruptibly()可以设置超时时间
:reentrantLock.tryLock(1, TimeUnit.SECONDS)可以设置为公平锁
支持多个条件变量
: Condition waitCigaretteQueue = lock.newCondition(); waitCigaretteQueue.await()
2、常用 API
public class ReentrantLockDemo {
private static final ReentrantLock lock = new ReentrantLock();
private static final Condition condition = lock.newCondition();
private static volatile boolean flag = false;
public static void main(String[] args) throws InterruptedException {
// reentrant1();
// interrupt();
// lockTimeout();
condition();
}
/**
* 可重入
*/
private static void reentrant1() {
lock.lock();
try {
System.out.println("reentrant 1");
reentrant2();
} finally {
lock.unlock();
}
}
private static void reentrant2() {
lock.lock();
try {
System.out.println("reentrant 2");
} finally {
lock.unlock();
}
}
/**
* 可中断
*/
private static void interrupt() throws InterruptedException {
Thread t1 = new Thread(() -> {
while (true) {
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
System.out.println("线程中断...");
e.printStackTrace();
}
try {
System.out.println("执行1...");
} finally {
lock.unlock();
}
}
});
t1.start();
TimeUnit.SECONDS.sleep(5);
t1.interrupt();
}
/**
* 锁超时
*/
private static void lockTimeout() {
Thread t1 = new Thread(() -> {
try {
final boolean flag = lock.tryLock(3, TimeUnit.SECONDS);
if (!flag) {
System.out.println("获取锁超时...");
return;
}
try {
System.out.println("获取锁成功...");
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
}
/**
* 条件变量
*/
private static void condition() throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
lock.lock();
while (!flag) {
try {
System.out.println("条件不满足,进入等待...");
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("执行...");
} finally {
lock.unlock();
}
});
t1.start();
TimeUnit.SECONDS.sleep(5);
lock.lock();
flag = true;
condition.signalAll();
lock.unlock();
}
}