最后
例2:
public class MyServlet extends HttpServlet {
// 是否安全?no,共享的
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(…);
}
}
public class UserServiceImpl implements UserService {
// 记录调用次数
private int count = 0;//这里多个线程调用可能出问题
public void update() {
// …
count++;
}
}
例3:
@Aspect
@Component
public class MyAspect {//单例,成员变量会共享
// 是否安全?no
private long start = 0L;
@Before(“execution(* *(…))”)
public void before() {
start = System.nanoTime();
}
@After(“execution(* *(…))”)
public void after() {
long end = System.nanoTime();
System.out.println(“cost time:” + (end-start));
}
}
4-1 Java对象头
这里以32位虚拟机为例:
普通对象:
数组对象:
其中Mark Word 结构为:
64 位虚拟机 Mark Word:
4-2 原理之Monitor
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 状态的线程,后面讲 wait-notify 时会分析
注意:
- synchronized 必须是进入同一个对象的 monitor 才有上述的效果
- 不加 synchronized 的对象不会关联监视器,不遵从以上规则
5-1 轻量级锁
-
轻量级锁使用场景:当一个对象被多个线程所访问,但访问的时间是错开的(不存在竞争),此时就可以使用轻量级锁来优化。
-
轻量级锁对使用者是透明的,即语法仍然是
synchronized
这里举个栗子:假设有两个方法同步块,利用同一个对象加锁
static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}
- 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录对象,内部可以存储锁定对象的mark word(不再一开始就使用Monitor)
- 让锁记录中的Object reference指向锁对象(Object),并尝试用cas去替换Object中的mark word,将此mark word放入lock record中保存
- 如果cas替换成功,则将Object的对象头替换为锁记录的地址和状态 00(轻量级锁状态),并由该线程给对象加锁
-
如果 cas 失败,有两种情况:
-
如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
-
如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数
- 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
-
当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头
-
成功,则解锁成功
-
失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
5-2 锁膨胀
如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
- 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
-
这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
-
即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
-
然后自己进入 Monitor 的 EntryList BLOCKED
- 当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程
5-3 自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞
-
自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
-
在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
-
Java 7 之后不能控制是否开启自旋功能
5-4 偏向锁
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有
偏向状态
-
Normal:一般状态,没有加任何锁,前面62位保存的是对象的信息,最后2位为状态(01),倒数第三位表示是否使用偏向锁(未使用:0)
-
Biased:偏向状态,使用偏向锁,前面54位保存的当前线程的ID,最后2位为状态(01),倒数第三位表示是否使用偏向锁(使用:1)
-
Lightweight:使用轻量级锁,前62位保存的是锁记录的指针,最后两位为状态(00)
-
Heavyweight:使用重量级锁,前62位保存的是Monitor的地址指针,后两位为状态(10)
-
如果开启了偏向锁(默认开启),在创建对象时,对象的Mark Word后三位应该是101
-
但是偏向锁默认是有延迟的,不会再程序一启动就生效,而是会在程序运行一段时间(几秒之后),才会对创建的对象设置为偏向状态
-
如果没有开启偏向锁,对象的Mark Word后三位应该是001
撤销偏向
以下几种情况会使对象的偏向锁失效
-
调用对象的hashCode方法
-
多个线程使用该对象
-
调用了wait/notify方法(调用wait方法会导致锁膨胀而使用重量级锁)
批量重偏向
-
如果对象虽然被多个线程访问,但是线程间不存在竞争,这时偏向T1的对象仍有机会重新偏向T2。重偏向会重置Thread ID
-
当撤销超过20次后(超过阈值),JVM会觉得是不是偏向错了,这时会在给对象加锁时,重新偏向至加锁线程
批量撤销
当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的
6-1 原理
-
Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
-
BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
-
BLOCKED 线程会在 Owner 线程释放锁时唤醒
-
WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入 EntryList 重新竞争
这里附《Java并发编程的艺术》里的一张图:
6-2 API介绍
-
wait()
让进入 object 监视器的线程到 waitSet 等待 -
notify()
在 object 上正在 waitSet 等待的线程中挑一个唤醒 -
notifyAll()
让 object 上正在 waitSet 等待的线程全部唤醒 -
wait() 方法会释放对象的锁,进入 WaitSet 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到 notify
-
为止
wait(long n)
有时限的等待, 到 n 毫秒后结束等待,或是被 notify
注意:只有当对象被锁以后,才能调用wait和notify方法
例子:
@Slf4j
public class Test6 {
final static Object lock = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (lock) {
log.debug(“执行…”);
try {
lock.wait();//让线程在lock上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug(“其他代码…”);
}
}, “t1”).start();
new Thread(() -> {
synchronized (lock) {
log.debug(“执行…”);
try {
lock.wait();//让线程在lock上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug(“其他代码…”);
}
}, “t2”).start();
Sleeper.sleep(2);
log.debug(“唤醒 lock 上的线程”);
synchronized (lock) {
lock.notify(); //唤醒 lock 上的一个线程
//lock.notifyAll(); //唤醒 lock 上所有等待线程
}
}
}
//输出
20:36:08 DEBUG [t1] (Test6.java:13) - 执行…
20:36:08 DEBUG [t2] (Test6.java:25) - 执行…
20:36:10 DEBUG [main] (Test6.java:35) - 唤醒 lock 上的线程
20:36:10 DEBUG [t1] (Test6.java:19) - 其他代码…
6-3 wait/notify 的正确使用
开始之前先看看 sleep(long n)
和 wait(long n)
的区别 :
-
sleep 是 Thread 方法,而 wait 是 Object 的方法
-
sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用
-
sleep 在睡眠的同时,不会释放对象锁,但 wait 在等待的时候会释放对象锁
-
他们的状态都是
TIMED_WAITING
以下部分建议参考:《Java并发编程的艺术》读后笔记-part4
等待方遵循如下原则:
-
获取对象的锁
-
如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件
-
条件满足则执行对应的逻辑
对应伪代码:
synchronized(对象){
while(条件不满足){
对象.wait();
}
对应的处理逻辑
}
通知方遵循如下原则:
-
获得对象的锁
-
改变条件
-
通知所有等待在对象上的线程
对应伪代码:
synchronized(对象){
改变条件
对象.notifyAll();
}
7-1 基本使用
park/unpark
都是 LockSupport
类中的方法
//暂停线程运行
LockSupport.park;
//恢复线程运行
LockSupport.unpark(thread);
先 park 再 unpark:
@Slf4j
public class Test24 {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug(“start…”);
Sleeper.sleep(1);
log.debug(“park…”);
//暂停线程运行
LockSupport.park();
log.debug(“resume…”);
}, “t1”);
t1.start();
Sleeper.sleep(2);
log.debug(“unpark…”);
//恢复线程运行
LockSupport.unpark(t1);
}
}
13:11:08 DEBUG [t1] (Test24.java:12) - start…
13:11:09 DEBUG [t1] (Test24.java:14) - park…
13:11:10 DEBUG [main] (Test24.java:20) - unpark…
13:11:10 DEBUG [t1] (Test24.java:16) - resume…
7-2 特点
与 Object 的 wait/notify
相比:
-
wait,notify 和 notifyAll 必须配合Object Monitor一起使用,而park,unpark不必
-
park,unpark 是以线程为单位来阻塞和唤醒线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么精确
-
park & unpark 可以先 unpark,而 wait & notify 不能先 notify
-
park不会释放锁,而wait会释放锁
7-3 原理
每个线程都有一个自己的Park对象,并且该对象_counter
,_cond
,__mutex
组成
-
先调用park再调用unpark时
-
先调用park
-
线程运行时,会将Park对象中的_counter的值设为0;
-
调用park时,会先查看counter的值是否为0,如果为0,则将线程放入阻塞队列cond中
-
放入阻塞队列中后,会再次将counter设置为0
-
然后调用unpark
-
调用unpark方法后,会将counter的值设置为1
-
去唤醒阻塞队列cond中的线程
-
线程继续运行并将counter的值设为0
-
先调用unpark,再调用park
-
调用unpark
-
会将counter设置为1(运行时0)
-
调用park方法
-
查看counter是否为0
-
因为unpark已经把counter设置为1,所以此时将counter设置为0,但不放入阻塞队列cond中
假设有线程Thread t
情况一:NEW –> RUNNABLE
- 当调用了
t.start()
方法时,由 NEW –> RUNNABLE
情况二: RUNNABLE <–> WAITING
-
当调用了t 线程用 synchronized(obj) 获取了对象锁后
-
调用
obj.wait()
方法时,t 线程从 RUNNABLE –> WAITING -
调用
obj.notify()
,obj.notifyAll()
,t.interrupt()
时 -
竞争锁成功,t 线程从 WAITING –> RUNNABLE
-
竞争锁失败,t 线程从 WAITING –> BLOCKED
情况三:RUNNABLE <–> WAITING
-
当前线程调用
t.join()
方法时,当前线程从 RUNNABLE –> WAITING -
注意是当前线程在t 线程对象的监视器上等待
-
t 线程运行结束,或调用了当前线程的
interrupt()
时,当前线程从 WAITING –> RUNNABLE
情况四: RUNNABLE <–> WAITING
-
当前线程调用
LockSupport.park()
方法会让当前线程从 RUNNABLE –> WAITING -
调用
LockSupport.unpark(目标线程)
或调用了线程的interrupt()
,会让目标线程从 WAITING –> RUNNABLE
情况五: 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
情况六:RUNNABLE <–> TIMED_WAITING
-
当前线程调用
t.join(long n)
方法时,当前线程从 RUNNABLE –> TIMED_WAITING -
注意是当前线程在t 线程对象的监视器上等待
-
当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的
interrupt()
时,当前线程从 TIMED_WAITING –> RUNNABLE
情况七:RUNNABLE <–> TIMED_WAITING
-
当前线程调用
Thread.sleep(long n)
,当前线程从 RUNNABLE –> TIMED_WAITING -
当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING –> RUNNABLE
情况八:RUNNABLE <–> TIMED_WAITING
-
当前线程调用
LockSupport.parkNanos(long nanos)
或LockSupport.parkUntil(long millis)
时,当前线 程从 RUNNABLE –> TIMED_WAITING -
调用
LockSupport.unpark(目标线程)
或调用了线程的interrupt()
,或是等待超时,会让目标线程从 TIMED_WAITING–> RUNNABLE
情况九:RUNNABLE <–> BLOCKED
-
t 线程用
synchronized(obj)
获取了对象锁时如果竞争失败,从 RUNNABLE –> BLOCKED -
持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争成功,从 BLOCKED –> RUNNABLE ,其它失败的线程仍然 BLOCKED
情况十: RUNNABLE <–> TERMINATED
当前线程所有代码运行完毕,进入 TERMINATED
这里附《Java并发编程的艺术》的一张图做一个概括:
class BigRoom {
//额外创建对象来作为锁
private final Object studyRoom = new Object();
private final Object bedRoom = new Object();
}
将锁的粒度细分
-
好处:是可以增强并发度
-
坏处:如果一个线程需要同时获得多把锁,就容易发生死锁
10-1 死锁
有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁
如:
-
t1线程获得A对象锁,接下来想获取B对象的锁
-
t2线程获得B对象锁,接下来想获取A对象的锁
我们来看个死锁的栗子:
@Slf4j
public class Test26 {
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”);
Sleeper.sleep(1);
synchronized (B) {
log.debug(“lock B”);
log.debug(“操作…”);
}
}
}, “t1”);
Thread t2 = new Thread(() -> {
synchronized (B) {
log.debug(“lock B”);
Sleeper.sleep(1);
synchronized (A) {
log.debug(“lock A”);
log.debug(“操作…”);
}
}
}, “t2”);
t1.start();
t2.start();
}
}
死锁产生的必要条件:
-
互斥条件。只有对必须互斥使用的资源的争抢才会导致死锁(如哲学家的筷子、打印机设备)。像内存、扬声器这样可以同时让多个进程使用的资源是不会导致死锁的(因为进程不用阻塞等待这种资源)。
-
不剥夺条件。进程所获得的资源在未使用完之前,不能由其他进程强行夺走,只能主动释放。
-
请求和保持条件。进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源又被其他进程占有,此时请求进程被阻塞,但又对自己已有的资源保持不放。
-
循环等待条件。存在一种进程资源的循环等待链,链中的每一个进程已获得的资源同时被下一个进程所请求。
如果想要详细了解死锁,可以参考操作系统-死锁
10-2 定位死锁
检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁
jps+jstack ThreadID
jconsole
10-3 哲学家就餐问题
代码演示:
public class Test27 {
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();
}
}
/**
- 筷子类
*/
class Chopstick {
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return “筷子{” + name + ‘}’;
}
}
/**
- 哲学家类
*/
@Slf4j
class Philosopher extends Thread {
Chopstick left;
Chopstick right;
public Philosopher(String name, Chopstick left, Chopstick right) {
super(name);
this.left = left;
this.right = right;
}
public void eat() {
log.debug(“eating…”);
Sleeper.sleep(1);
}
@Override
public void run() {
while (true) {
while (true) {
// 获得左手筷子
synchronized (left) {
// 获得右手筷子
synchronized (right) {
// 吃饭
eat();
}
// 放下右手筷子
}
// 放下左手筷子
}
}
}
}
//该代码会发生死锁
10-4 活锁
活锁出现在两个线程互相改变对方的结束条件,最后后谁也无法结束
举个例子:
@Slf4j
public class Test28 {
static volatile int count = 10;
static final Object lock = new Object();
public static void main(String[] args) {
new Thread(() -> {
while (count > 0) {
Sleeper.sleep(0.2);
count–;
log.debug(“count:{}”, count);
}
}, “t1”).start();
new Thread(() -> {
while (count < 20) {
Sleeper.sleep(0.2);
count++;
log.debug(“count:{}”, count);
}
}, “t2”).start();
}
}
//可以改变两者睡眠时间,使其交错,避免活锁产生
死锁与活锁的区别:
-
死锁:是因为线程互相持有对象想要的锁,并且都不释放,最后到时线程阻塞,停止运行的现象。
-
活锁:是因为线程间修改了对方的结束条件,而导致代码一直在运行,却一直运行不完的现象
10-5 饥饿
一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束,这种现象就是饥饿
11-1 特点+语法
ReentrantLock
相对于 synchronized
它具备如下特点:
-
可中断
-
可以设置超时时间
-
可以设置为公平锁
-
支持多个条件变量(具有多个waitset)
与 synchronized
一样,都支持可重入
基本语法:
// 获取锁
reentrantLock.lock();
try {
// 临界区
} finally {
// 释放锁
reentrantLock.unlock();
}
11-2 可重入
-
可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
-
如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
锁重入例子:
@Slf4j
public class Test29 {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
lock.lock();
try {
log.debug(“enter main”);
m1();
} finally {
lock.unlock();
}
}
public static void m1() {
lock.lock();
try {
log.debug(“enter m1”);
m2();
} finally {
lock.unlock();
}
}
public static void m2() {
lock.lock();
最后
这份《“java高分面试指南”-25分类227页1000+题50w+字解析》同样可分享给有需要的朋友,感兴趣的伙伴们可挑战一下自我,在不看答案解析的情况,测试测试自己的解题水平,这样也能达到事半功倍的效果!(好东西要大家一起看才香)
的现象
10-5 饥饿
一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束,这种现象就是饥饿
11-1 特点+语法
ReentrantLock
相对于 synchronized
它具备如下特点:
-
可中断
-
可以设置超时时间
-
可以设置为公平锁
-
支持多个条件变量(具有多个waitset)
与 synchronized
一样,都支持可重入
基本语法:
// 获取锁
reentrantLock.lock();
try {
// 临界区
} finally {
// 释放锁
reentrantLock.unlock();
}
11-2 可重入
-
可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
-
如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
锁重入例子:
@Slf4j
public class Test29 {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
lock.lock();
try {
log.debug(“enter main”);
m1();
} finally {
lock.unlock();
}
}
public static void m1() {
lock.lock();
try {
log.debug(“enter m1”);
m2();
} finally {
lock.unlock();
}
}
public static void m2() {
lock.lock();
最后
这份《“java高分面试指南”-25分类227页1000+题50w+字解析》同样可分享给有需要的朋友,感兴趣的伙伴们可挑战一下自我,在不看答案解析的情况,测试测试自己的解题水平,这样也能达到事半功倍的效果!(好东西要大家一起看才香)
[外链图片转存中…(img-sh81Xm32-1715589620390)]
[外链图片转存中…(img-qw1s9uuV-1715589620391)]