目录
共享带来的问题
分析
两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?
分析:
以上的结果可能是正数、负数、零
因为Java中对静态变量的自增、自减并不是原子操作,需要从字节码进行分析
例如对于i++而言(i为静态变量),会产生如下的JVM字节码指令:
i--:
而Java的内存模型如下,完成静态变量的自增、自减需要在主存和工作内存中进行数据交换:
如果是单线程,以上8行代码是顺序执行(不会交错),没有问题
但多线程下这8行代码可能交错运行
出现负数的情况:
出现正数的情况:
临界区Critical Section
一个程序运行多个线程本身是没有问题的,问题出在多个线程访问共享资源
- 多个线程读共享资源没有问题
- 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
例如:
static int counter = 0;
static void increment()
//临界区
{
counter++;
}
static void decrement()
//临界区
{
counter--;
}
竞态条件Race Condition
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
为了避免临界区的竞态条件发生,有多种解决方案:
- 阻塞式:synchronized,Lock
- 非阻塞式:原子变量
synchronized解决方案
synchronized,俗称对象锁,它采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其他线程再想获取这个对象锁时就会阻塞住,这样就能保证拥有锁的线程可以安全地执行临界区内的代码,不用担心线程上下文切换
注意:
虽然Java中互斥和同步都可以采用synchronized关键字来完成,但存在区别:
- 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
- 同步是由于线程执行的先后顺序不同,需要一个线程等待其他线程运行到某个点
语法1
synchronized(对象) // 线程1, 线程2(blocked)
{
临界区
}
解决:
理解:
- synchronized(对象) 中的对象,可以想象为一个房间(room),有唯一入口(门)房间只能一次进入一人进行计算,线程 t1,t2 想象成两个人
- 当线程 t1 执行到 synchronized(room) 时就好比 t1 进入了这个房间,并锁住了门拿走了钥匙,在门内执行 count++ 代码
- 这时候如果 t2 也运行到了 synchronized(room) 时,它发现门被锁住了,只能在门外等待,发生了上下文切换,阻塞住了
- 这中间即使 t1 的 cpu 时间片不幸用完,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去), 这时门还是锁住的,t1 仍拿着钥匙,t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片时才能开门进入
- 当 t1 执行完 synchronized{} 块内的代码,这时候才会从 obj 房间出来并解开门上的锁,唤醒 t2 线程把钥匙给它,t2 线程这时才可以进入 obj 房间,锁住了门拿上钥匙,执行它的 count-- 代码
图示:
思考:
synchronized实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断
- 如果把synchronized(obj)放在for循环的外面,如何理解?——原子性
- 如果t1 synchronized(obj1),而t2 synchronized(obj2)会怎么运作?——不同的锁对象
- 如果t1 synchronized(obj),而t2没有加会怎么样?——相当于都没有加锁
面向对象改进:
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Test17")
public class Test17 {
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
room.increment();
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
room.decrement();
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}", room.getCounter());
}
}
class Room {
private int counter = 0;
public void increment() {
synchronized (this){
counter++;
}
}
public void decrement() {
synchronized (this){
counter--;
}
}
public int getCounter() {
synchronized (this){
return counter;
}
}
}
语法2
成员方法:锁的是this对象
静态方法:锁的是类对象
线程八锁
考察synchronized锁住的是哪个对象
情况1:
12或21
情况2:
1s后12 或 2 1s后1
情况3:c方法没有synchronized,c和ab并行执行
3 1s后1 2 或 3 2 1s后1 或 2 3 1s后1
情况4: 两个对象n1和n2,不互斥
2 1s后1
情况5:a方法为静态方法,锁住的是类对象,与b方法锁住的对象不同,不互斥
2 1s后1
情况6:a、b都为静态方法
1s后12 或 2 1s后1
情况7:锁对象不同,不互斥
2 1s后1
情况8:锁对象相同,互斥
1s后12 或 2 1s后1
变量的线程安全
成员变量和静态变量是否线程安全?
如果它们没有共享,则线程安全
如果它们被共享,根据它们的状态是否能够改变,分为两种情况:
- 如果只有读操作,则线程安全
- 如果有读写操作,则这段代码是临界区,需要考虑线程安全
局部变量是否线程安全?
局部变量是线程安全的
但局部变量引用的对象则未必
- 如果该对象没有逃离方法的作用范围,它是线程安全的
- 如果该对象逃离方法的作用范围,需要考虑线程安全
每个线程调用test1()方法时,局部变量i会在每个线程的栈帧内存中被创建多份,因此不存在共享
局部变量的引用:
如果是成员变量
其中一种情况是,如果线程2还未add,线程1remove就会报错:
分析:
无论哪个线程的method2,引用的都是同一个对象中的list成员变量
将list修改为局部变量
分析:
list是局部变量,每个线程调用时会创建其不同实例,没有共享
如果把method2和method3的方法修改为public会不会有线程安全问题?
情况1:有其他线程调用method2和method3
- 没有问题,不同线程创建不同的list对象
情况2:在情况1的基础上,为ThreadSafe类添加子类,子类覆盖method2或method3方法
- 有问题,两个线程共享一个资源
常用线程安全类
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- java.util.concurrent包下的类
这里的线程安全指的是多个线程调用它们同一个实例的某个方法时,是线程安全的
它们的每个方法是原子的,但它们方法的组合不是原子的
线程安全类方法的组合:
下面代码不是线程安全的
不可变类线程安全性
String、Integer等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的
Monitor
Java对象头
以32位虚拟机为例
普通对象
数组对象
其中Mark Word结构为
64位虚拟机Mark Word
Monitor(监视器/管程)
每个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的对象不会关联监视器,不遵从以上规则
原理:synchronized
轻量级锁
轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化
轻量级锁对使用者是透明的,即语法仍然是synchronized
假设有两个方法同步块,利用同一个对象加锁:
- 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word
- 让锁记录中Object reference指向锁对象,并尝试用cas替换Object的Mark Word,将Mark Word的值存入锁记录
- 如果cas替换成功,对象头中存储了锁记录地址和状态00,表示由该线程给对象加锁
如果cas失败,有两种情况:
- 如果是其他线程已经持有了该Object的轻量级锁,这时表明有竞争,进入锁膨胀过程
- 如果是自己执行了synchronized锁重入,那么再添加一条Lock Record作为重入的计数
- 当退出synchronized代码块(解锁时),如果有取值为null的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
当退出synchronized代码块(解锁时),锁记录的值不为null,这时使用cas将Mark Word的值恢复给对象头
- 成功,则解锁成功
- 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
锁膨胀
如果在尝试加轻量级锁的过程中,cas操作无法成功,这是一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁
- 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
- 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程,即为 Object 对象申请 Monitor 锁,让Object 指向重量级锁地址,然后自己进入 Monitor 的 EntryList BLOCKED
- 当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒EntryList 中 BLOCKED 线程
自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当时线程自旋成功(即这时持锁线程已经退出来同步块,释放了锁),这时当前线程就可以避免阻塞
自旋会占用cpu时间,单核cpu自旋就是浪费,多核cpu自旋才能发挥优势
在Java 6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,比较智能
Java 7之后不能控制是否开启自旋功能
自旋重试成功的情况:
自旋重试失败的情况:
偏向锁
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行cas操作
Java 6引入了偏向锁来做进一步优化:只有第一次使用cas将线程ID设置到对象的Mark Word头,之后发现这个线程 ID是自己的就表示没有竞争,不用重新cas,以后只要不发生竞争,这个对象就归该线程所有
偏向状态
对象头格式:
一个对象创建时:
- 如果开启了偏向锁(默认开启),那么对象创建后,markword值为0x05,即最后3位为101,这时它的thread、epoch、age都为0
- 偏向锁默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加VM参数-XX:BiasedLockingStartupDelay=0来禁用延迟
- 如果没有开启偏向锁,那么对象创建后,markword值为0x01,即最后3位为001,这时它的hashCode、age都为0,第一次用到hashCode时才会赋值
处于偏向锁的对象解锁后,线程id仍存储于对象头中
正常状态对象一开始是没有hashCode的,第一次调用才生成
撤销-调用对象hashCode
调用了对象的hashCode,但偏向锁的对象markword中存储的是线程id,如果调用hashCode会导致偏向锁被撤销
- 轻量级锁会在锁记录中记录hashCode
- 重量级锁会在Minitor中记录hashCode
在调用hashCode后使用偏向锁,记得去掉-XX:-UseBiasedLocking
撤销-其他线程使用对象
当有其他线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
撤销-调用wait/notify
批量重偏向
如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程t1的对象仍有机会重新偏向t2,重偏向会重置对象的Thread ID
当撤销偏向锁阈值超过20次后,jvm会觉得是不是偏向错了,会在给这些对象加锁时重新偏向至加锁线程
批量撤销
当撤销偏向锁阈值超过40次后,jvm会觉得自己确实偏向错了,根本不应该偏向,于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的
参考资料:
https://www.cnblogs.com/LemonFive/p/11246086.html
https://www.cnblogs.com/LemonFive/p/11248248.html
死磕Synchronized底层实现--概论 · Issue #12 · farmerjohngit/myblog · GitHub
锁消除
JIT编译器在编译的时候,进行逃逸分析,分析synchronized锁对象是不是只可能被一个线程加锁,不存在其他线程来竞争加锁的情况,这时就可以消除该锁,提升执行效率
锁粗化
对相同对象多次加锁,导致线程发生多次重入,可以使用锁粗化方式来优化,将前后的锁合并为一个锁,避免频繁加锁释放锁
wait、notify
原理
- Owner线程发现条件不满足,调用wait方法,即可进入WaitSet,变为WAITING状态
- BLOCKED和WAITING的线程都处于阻塞状态,不占用cpu时间片
- BLOCKED线程会在Owner线程释放锁时唤醒
- WAITING线程会在Owner线程调用notify或notifyAll时唤醒,但唤醒后并不意味着立刻获得锁,仍需进入EntryList重新竞争
WAITING和BLOCKED区别:
- WAITING:获得锁但需要等待某个条件发生来唤醒
- BLOCKED:阻塞,没有获得锁
API介绍
- obj.wait() 让进入 object 监视器的线程到 WaitSet 等待
- obj.notify() 在 object 上正在 WaitSet 等待的线程中挑一个唤醒
- obj.notifyAll() 让 object 上正在 WaitSet 等待的线程全部唤醒
它们都是线程之间进行协作的手段,都属于 Object 对象的方法,必须获得此对象的锁,才能调用这几个方法
import lombok.extern.slf4j.Slf4j;
import static cn.itcast.n2.util.Sleeper.sleep;
@Slf4j(topic = "c.TestWaitNotify")
public class TestWaitNotify {
final static Object obj = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码....");
}
},"t1").start();
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码....");
}
},"t2").start();
// 主线程两秒后执行
sleep(0.5);
log.debug("唤醒 obj 上其它线程");
synchronized (obj) {
obj.notify(); // 唤醒obj上一个线程
// obj.notifyAll(); // 唤醒obj上所有等待线程
}
}
}
notify:
notifyAll:
wait()方法会释放对象的锁,进入WaitSet等待区,从而让其他线程有机会获取对象的锁,无限制等待,直到notify为止
wait(long n):有时限的等待,到n毫秒后结束等待,或是被notify
sleep(long n)与wait(long n)的区别:
- sleep是Thread方法,而wait是Object的方法
- sleep不需要强制和synchronized配合使用,但wait需要和synchronized一起用
- sleep在睡眠的同时不会释放对象锁,但wait在等待时会释放对象锁
它们的状态都是TIMED_WAITING
同步模式之保护性暂停
即 Guarded Suspension,用在一个线程等待另一个线程的执行结果
要点:
- 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个 GuardedObject
- 如果有结果不断从一个线程到另一个线程,那么可以使用消息队列(见生产者/消费者)
- JDK 中,join 的实现、Future 的实现,采用的就是此模式
- 因为要等待另一方的结果,因此归类到同步模式
实现:
设置超时时间:
异步模式之生产者/消费者
要点:
- 与前面的保护性暂停中的GuardObject不同,不需要产生结果和消费结果的线程一一对应
- 消费队列可以用来平衡生产和消费的线程资源
- 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据
- 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据
- JDK中各种阻塞队列采用的就是这种模式
实现:
import lombok.extern.slf4j.Slf4j;
import java.util.LinkedList;
import static cn.itcast.n2.util.Sleeper.sleep;
@Slf4j(topic = "c.Test21")
public class Test21 {
public static void main(String[] args) {
MessageQueue queue = new MessageQueue(2);
for (int i = 0; i < 3; i++) {
int id = i;
new Thread(() -> {
queue.put(new Message(id , "值"+id));
}, "生产者" + i).start();
}
new Thread(() -> {
while(true) {
sleep(1);
Message message = queue.take();
}
}, "消费者").start();
}
}
// 消息队列类 , java 线程之间通信
@Slf4j(topic = "c.MessageQueue")
class MessageQueue {
// 消息的队列集合
private LinkedList<Message> list = new LinkedList<>();
// 队列容量
private int capcity;
public MessageQueue(int capcity) {
this.capcity = capcity;
}
// 获取消息
public Message take() {
// 检查队列是否为空
synchronized (list) {
while(list.isEmpty()) {
try {
log.debug("队列为空, 消费者线程等待");
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 从队列头部获取消息并返回
Message message = list.removeFirst();
log.debug("已消费消息 {}", message);
list.notifyAll();
return message;
}
}
// 存入消息
public void put(Message message) {
synchronized (list) {
// 检查对象是否已满
while(list.size() == capcity) {
try {
log.debug("队列已满, 生产者线程等待");
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 将消息加入队列尾部
list.addLast(message);
log.debug("已生产消息 {}", message);
list.notifyAll();
}
}
}
final class Message {
private int id;
private Object value;
public Message(int id, Object value) {
this.id = id;
this.value = value;
}
public int getId() {
return id;
}
public Object getValue() {
return value;
}
@Override
public String toString() {
return "Message{" +
"id=" + id +
", value=" + value +
'}';
}
}
Park & Unpark
基本使用
它们是LockSupport类中的方法
// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)
先park再unpark:
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.LockSupport;
import static cn.itcast.n2.util.Sleeper.sleep;
@Slf4j(topic = "c.TestParkUnpark")
public class TestParkUnpark {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("start...");
sleep(1);
log.debug("park...");
LockSupport.park();
log.debug("resume...");
}, "t1");
t1.start();
sleep(2);
log.debug("unpark...");
LockSupport.unpark(t1);
}
}
先unpark再park:
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.LockSupport;
import static cn.itcast.n2.util.Sleeper.sleep;
@Slf4j(topic = "c.TestParkUnpark")
public class TestParkUnpark {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("start...");
sleep(2);
log.debug("park...");
LockSupport.park();
log.debug("resume...");
}, "t1");
t1.start();
sleep(1);
log.debug("unpark...");
LockSupport.unpark(t1);
}
}
特点
与Object的wait & notify相比:
- wait,notify和notifyAll必须配合Object Monitor一起使用,而park,unpark不必
- park & unpark是以线程为单位来阻塞和唤醒线程,而notify只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么精确
- park & unpark可以先unpark,而wait & notify不能先notify
原理
每个线程都有自己的一个 Parker 对象,由三部分组成 _counter , _cond 和 _mutex
1. 当前线程调用 Unsafe.park() 方法
2. 检查 _counter ,本情况为 0,这时,获得 _mutex 互斥锁
3. 线程进入 _cond 条件变量阻塞
4. 设置 _counter = 0
1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
2. 唤醒 _cond 条件变量中的 Thread_0
3. Thread_0 恢复运行
4. 设置 _counter 为 0
1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
2. 当前线程调用 Unsafe.park() 方法
3. 检查 _counter ,本情况为 1,这时线程无需阻塞,继续运行
4. 设置 _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
import lombok.extern.slf4j.Slf4j;
import static cn.itcast.n2.util.Sleeper.sleep;
@Slf4j(topic = "c.TestWaitNotify")
public class TestWaitNotify {
final static Object obj = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码....");
}
},"t1").start();
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码....");
}
},"t2").start();
// 主线程两秒后执行
sleep(0.5);
log.debug("唤醒 obj 上其它线程");
synchronized (obj) {
obj.notifyAll(); // 唤醒obj上所有等待线程
}
}
}
情况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
多把锁
一间大屋子有两个功能:睡觉、学习,互不相干, 现在小南要学习,小女要睡觉,但如果只用一间屋子(一个对象锁)的话,并发度很低
解决方法是准备多个房间(多个对象锁)
将锁的粒度细分:
- 好处:可以增强并发度
- 坏处:如果一个线程需要同时获得多把锁,就容易发生死锁
结果:
改进:
结果:
活跃性
死锁
有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁
相互等待对方释放临界资源
t1 线程 获得 A对象锁,接下来想获取 B对象 的锁
t2 线程 获得 B对象 锁,接下来想获取 A对象 的锁
import lombok.extern.slf4j.Slf4j;
import static cn.itcast.n2.util.Sleeper.sleep;
@Slf4j(topic = "c.TestDeadLock")
public class TestDeadLock {
public static void main(String[] args) {
test1();
}
private static void test1() {
Object A = new Object();
Object B = new Object();
Thread t1 = new Thread(() -> {
synchronized (A) {
log.debug("lock A");
sleep(1);
synchronized (B) {
log.debug("lock B");
log.debug("操作...");
}
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (B) {
log.debug("lock B");
sleep(0.5);
synchronized (A) {
log.debug("lock A");
log.debug("操作...");
}
}
}, "t2");
t1.start();
t2.start();
}
}
定位死锁
检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁:
避免死锁要注意加锁顺序
如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过 top 先定位到 CPU 占用高的 Java 进程,再利用 top -Hp 进程id来定位是哪个线程,最后再用 jstack 排查
哲学家就餐问题
有五位哲学家,围坐在圆桌旁
- 他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考
- 吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子
- 如果筷子被身边的人拿着,自己就得等待
import cn.itcast.n2.util.Sleeper;
import lombok.extern.slf4j.Slf4j;
import java.util.Random;
public class TestDeadLock {
public static void main(String[] args) {
Chopstick c1 = new Chopstick("1");
Chopstick c2 = new Chopstick("2");
Chopstick c3 = new Chopstick("3");
Chopstick c4 = new Chopstick("4");
Chopstick c5 = new Chopstick("5");
new Philosopher("苏格拉底", c1, c2).start();
new Philosopher("柏拉图", c2, c3).start();
new Philosopher("亚里士多德", c3, c4).start();
new Philosopher("赫拉克利特", c4, c5).start();
new Philosopher("阿基米德", c5, c1).start();
}
}
@Slf4j(topic = "c.Philosopher")
class Philosopher extends Thread {
Chopstick left;
Chopstick right;
public Philosopher(String name, Chopstick left, Chopstick right) {
super(name);
this.left = left;
this.right = right;
}
@Override
public void run() {
while (true) {
// 尝试获得左手筷子
synchronized (left) {
// 尝试获得右手筷子
synchronized (right) {
eat();
}
}
}
}
Random random = new Random();
private void eat() {
log.debug("eating...");
Sleeper.sleep(0.5);
}
}
class Chopstick {
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "筷子{" + name + '}';
}
}
活锁
活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束
import lombok.extern.slf4j.Slf4j;
import static cn.itcast.n2.util.Sleeper.sleep;
@Slf4j(topic = "c.TestLiveLock")
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();
}
}
饥饿
饥饿:一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束
顺序加锁解决:
ReentrantLock(可重入锁)
特点
相对于synchronized,它具有如下特点:
- 可中断
- 可以设置超时时间
- 可以设置为公平锁
- 支持多个条件变量
与synchronized一样,都支持可重入
基本语法
// 获取锁
reentrantLock.lock();
try {
// 临界区
} finally {
// 释放锁
reentrantLock.unlock();
}
可重入
可重入是指同一个线程如果首次获得了这把锁,因为它是这把锁的拥有者,因此有权利再次获取这把锁
如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
可打断
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import static cn.itcast.n2.util.Sleeper.sleep;
@Slf4j(topic = "c.Test22")
public class Test22_1 {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try {
//如果没有竞争,那么此方法就会获取lock对象锁
//如果有竞争,就会进入阻塞队列,可以被其他线程用interrupt方法打断
log.debug("尝试获得锁");
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("没有获得锁,返回");
return;
}
try {
log.debug("获取到锁");
}finally {
lock.unlock();
}
}, "t1");
lock.lock();
t1.start();
sleep(1);
log.debug("打断t1");
t1.interrupt();
}
}
锁超时
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import static cn.itcast.n2.util.Sleeper.sleep;
@Slf4j(topic = "c.Test22")
public class Test22_2 {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("尝试获得锁");
try {
if(!lock.tryLock(2,TimeUnit.SECONDS)){
log.debug("获取不到锁");
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("获取不到锁");
return;
}
try {
log.debug("获得到锁");
}finally {
lock.unlock();
}
},"t1");
lock.lock();
log.debug("获得到锁");
t1.start();
sleep(1);
log.debug("释放了锁");
lock.unlock();
}
}
使用 tryLock 解决哲学家就餐问题:
import cn.itcast.n2.util.Sleeper;
import lombok.extern.slf4j.Slf4j;
import java.util.Random;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j(topic = "c.Test23")
public class Test23 {public static void main(String[] args) {
Chopstick c1 = new Chopstick("1");
Chopstick c2 = new Chopstick("2");
Chopstick c3 = new Chopstick("3");
Chopstick c4 = new Chopstick("4");
Chopstick c5 = new Chopstick("5");
new Philosopher("苏格拉底", c1, c2).start();
new Philosopher("柏拉图", c2, c3).start();
new Philosopher("亚里士多德", c3, c4).start();
new Philosopher("赫拉克利特", c4, c5).start();
new Philosopher("阿基米德", c5, c1).start();
}
}
@Slf4j(topic = "c.Philosopher")
class Philosopher extends Thread {
Chopstick left;
Chopstick right;
public Philosopher(String name, Chopstick left, Chopstick right) {
super(name);
this.left = left;
this.right = right;
}
@Override
public void run() {
while (true) {
// 尝试获得左手筷子
if(left.tryLock()) {
try {
// 尝试获得右手筷子
if(right.tryLock()) {
try {
eat();
} finally {
right.unlock();
}
}
} finally {
left.unlock(); // 右手筷子获取失败,释放自己手里的左手筷子
}
}
}
}
Random random = new Random();
private void eat() {
log.debug("eating...");
Sleeper.sleep(0.5);
}
}
class Chopstick extends ReentrantLock {
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "筷子{" + name + '}';
}
}
公平锁
ReentrantLock 默认是不公平的
强行插入,有机会在中间输出
改为公平锁后:
强行插入,总是在最后输出
公平锁一般没有必要,会降低并发度
条件变量
synchronized 中也有条件变量,就是waitSet休息室,当条件不满足时进入 waitSet 等待
ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比
- synchronized 是那些不满足条件的线程都在一间休息室等消息
- 而 ReentrantLock 支持多间休息室,唤醒时也是按休息室来唤醒
使用流程:
- await前需要获得锁
- await执行后,会释放锁,进入conditionObject等待
- await的线程被唤醒(或打断、或超时),重新竞争lock锁
- 竞争lock锁成功后,从await后继续执行
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import static cn.itcast.n2.util.Sleeper.sleep;
@Slf4j(topic = "c.Test24")
public class Test24 {
static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
static ReentrantLock ROOM = new ReentrantLock();
// 等待烟的休息室
static Condition waitCigaretteSet = ROOM.newCondition();
// 等外卖的休息室
static Condition waitTakeoutSet = ROOM.newCondition();
public static void main(String[] args) {
new Thread(() -> {
ROOM.lock();
try {
log.debug("有烟没?[{}]", hasCigarette);
while (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
waitCigaretteSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("可以开始干活了");
} finally {
ROOM.unlock();
}
}, "小南").start();
new Thread(() -> {
ROOM.lock();
try {
log.debug("外卖送到没?[{}]", hasTakeout);
while (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
waitTakeoutSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("可以开始干活了");
} finally {
ROOM.unlock();
}
}, "小女").start();
sleep(1);
new Thread(() -> {
ROOM.lock();
try {
hasTakeout = true;
waitTakeoutSet.signal();
} finally {
ROOM.unlock();
}
}, "送外卖的").start();
sleep(1);
new Thread(() -> {
ROOM.lock();
try {
hasCigarette = true;
waitCigaretteSet.signal();
} finally {
ROOM.unlock();
}
}, "送烟的").start();
}
}
同步模式之顺序控制
固定运行顺序
必须先 2 后 1 打印
wait、notify版
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Test25")
public class Test25 {
static final Object lock = new Object();
// 表示 t2 是否运行过
static boolean t2runned = false;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lock) {
while (!t2runned) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("1");
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (lock) {
log.debug("2");
t2runned = true;
lock.notify();
}
}, "t2");
t1.start();
t2.start();
}
}
park、unpark版
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.LockSupport;
@Slf4j(topic = "c.Test26")
public class Test26 {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
LockSupport.park();
log.debug("1");
}, "t1");
t1.start();
new Thread(() -> {
log.debug("2");
LockSupport.unpark(t1);
},"t2").start();
}
}
交替输出
线程 1 输出 a 5 次,线程 2 输出 b 5 次,线程 3 输出 c 5 次,现在要求输出 abcabcabcabcabc 怎么实现
wait、notify版
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Test27")
public class Test27 {
public static void main(String[] args) {
WaitNotify wn = new WaitNotify(1, 5);
new Thread(() -> {
wn.print("a", 1, 2);
}).start();
new Thread(() -> {
wn.print("b", 2, 3);
}).start();
new Thread(() -> {
wn.print("c", 3, 1);
}).start();
}
}
/*
输出内容 等待标记 下一个标记
a 1 2
b 2 3
c 3 1
*/
class WaitNotify {
// 等待标记 1:a执行 2:b执行 3:c执行
private int flag;
// 循环次数
private int loopNumber;
public WaitNotify(int flag, int loopNumber) {
this.flag = flag;
this.loopNumber = loopNumber;
}
// 打印 a 1 2
public void print(String str, int waitFlag, int nextFlag) {
for (int i = 0; i < loopNumber; i++) {
synchronized (this) {
while(flag != waitFlag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.print(str);
flag = nextFlag;
this.notifyAll();
}
}
}
}
await、signal版
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class Test30 {
public static void main(String[] args) throws InterruptedException {
AwaitSignal awaitSignal = new AwaitSignal(5);
Condition a = awaitSignal.newCondition();
Condition b = awaitSignal.newCondition();
Condition c = awaitSignal.newCondition();
new Thread(() -> {
awaitSignal.print("a", a, b);
}).start();
new Thread(() -> {
awaitSignal.print("b", b, c);
}).start();
new Thread(() -> {
awaitSignal.print("c", c, a);
}).start();
Thread.sleep(1000);
awaitSignal.lock();
try {
System.out.println("开始...");
a.signal();
} finally {
awaitSignal.unlock();
}
}
}
class AwaitSignal extends ReentrantLock{
private int loopNumber;
public AwaitSignal(int loopNumber) {
this.loopNumber = loopNumber;
}
//参数1:打印内容,参数2:进入哪一间休息室,参数3:下一间休息室
public void print(String str, Condition current, Condition next) {
for (int i = 0; i < loopNumber; i++) {
lock();
try {
current.await();
System.out.print(str);
next.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
unlock();
}
}
}
}
注意:该实现没有考虑 a 、b、c 线程都就绪再开始
park、unpark版
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.LockSupport;
@Slf4j(topic = "c.Test31")
public class Test31 {
static Thread t1;
static Thread t2;
static Thread t3;
public static void main(String[] args) {
ParkUnpark pu = new ParkUnpark(5);
t1 = new Thread(() -> {
pu.print("a", t2);
});
t2 = new Thread(() -> {
pu.print("b", t3);
});
t3 = new Thread(() -> {
pu.print("c", t1);
});
t1.start();
t2.start();
t3.start();
LockSupport.unpark(t1);
}
}
class ParkUnpark {
private int loopNumber;
public ParkUnpark(int loopNumber) {
this.loopNumber = loopNumber;
}
public void print(String str, Thread next) {
for (int i = 0; i < loopNumber; i++) {
LockSupport.park();
System.out.print(str);
LockSupport.unpark(next);
}
}
}
标签:synchronized,Thread,管程,JUC3,线程,debug,new,共享,public
From: https://blog.csdn.net/m0_73902080/article/details/141228284