多线程-锁
# 多线程
# 锁
# 可重入锁
- 可重入锁又名递归锁
- 是指同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁,前提锁对象是同一个对象
- Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可以一定程度避免死锁
- 分开解释
- 可:可以
- 重:再次
- 入:进入
- 锁:同步锁
- 进入什么
- 进入同步域(同步代码块/方法或显式锁锁定的代码)
- 一句话
- 一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入
- 自己可以获取自己的内部锁
- 种类
- 隐式锁(即synchronized关键字使用的锁)默认是可重入锁
- 同步块
- 同步方法
- synchronized的重入实现原理
- 每一个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针
- 当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且计数器加1。
- 在目标锁对象的计数器不为零的情况下,如果锁对象持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。
- 当执行monitorexit时,Java虚拟机则需要将锁对象的计数器减1。计数器为零代表锁已经释放。
- 显式锁(即Lock)也有ReentrantLock这样的可重入锁
- 隐式锁(即synchronized关键字使用的锁)默认是可重入锁
- Demo
public class ReentrantLockDemo {
//同步对象
static Object objectLockA = new Object();
public static void m1(){
new Thread(() -> {
synchronized (objectLockA){
System.out.println(Thread.currentThread().getName() + "\t外层代码块!");
synchronized (objectLockA){
System.out.println(Thread.currentThread().getName() + "\t内层代码块!");
}
}
},"AAA").start();
}
//同步方法
public synchronized void method1(){
System.out.println(Thread.currentThread().getName() + "\t外层方法调用");
method2();
}
public synchronized void method2(){
System.out.println(Thread.currentThread().getName() + "\t内层方法调用");
}
//Lock
static Lock lock = new ReentrantLock();
public static void main(String[] args) {
// m1();
// new ReentrantLockDemo().method1();
new Thread(() -> {
lock.lock();
try{
System.out.println(Thread.currentThread().getName() + "\t外层lock");
lock.lock();
try{
System.out.println(Thread.currentThread().getName() + "\t内层lock");
}finally{
lock.unlock();
}
}finally{
lock.unlock();
}
},"BBB").start();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# LockSupport
- LockSupport是什么?
- LockSupport是用来创建锁和其他同步线程的基本线程阻塞原语
- LockSupport中的park()和unpark()的作用分别是阻塞线程和解除阻塞线程
- 线程等待唤醒机制(wait/notify)改良加强
3种让线程等待和唤醒的方法
方式1:使用Object类的wait()方法让线程等待,使用Object类中的notify()方法唤醒线程
- Demo
public class WaitNotifyDemo { static Object objectLock = new Object(); public static void main(String[] args) { new Thread(() -> { try{ TimeUnit.SECONDS.sleep(5); }catch(Exception e){e.printStackTrace();} synchronized (objectLock){ System.out.println(Thread.currentThread().getName() + "\t come in"); try { objectLock.wait(); } catch (Exception e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "\t 被唤醒"); } },"AA").start(); new Thread(() -> { synchronized (objectLock){ objectLock.notify(); System.out.println(Thread.currentThread().getName() + "\t 唤醒某一个线程"); } },"BB").start(); } }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23- 缺点
- 只能在同步方法或同步代码块中使用,且成对出现使用,否则报java.lang.IllegalMonitorStateException。
- 必须先wait后notify,否则无法唤醒,死锁。
方式2:使用Condition类的await()方法让线程等待,使用Condition类的signal()方法唤醒线程
- Demo
public class AwaitSignalDemo { static Lock lock = new ReentrantLock(); static Condition condition = lock.newCondition(); public static void main(String[] args) { new Thread(() -> { // try{ TimeUnit.SECONDS.sleep(5); }catch(Exception e){e.printStackTrace();} lock.lock(); try{ System.out.println(Thread.currentThread().getName() + "\t come in"); try { condition.await(); } catch (Exception e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "\t 被唤醒"); }finally{ lock.unlock(); } },"AA").start(); new Thread(() -> { lock.lock(); try{ condition.signal(); System.out.println(Thread.currentThread().getName() + "\t 通知"); }finally{ lock.unlock(); } },"BB").start(); } }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30- 缺点
- 只能在lock.lock(),lock.unlock()中使用,且成对出现使用,否则报java.lang.IllegalMonitorStateException。
- 必须先await后signal,否则无法唤醒,死锁。
方式3:LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程
Object类中的wait和notify方法实现线程等待和唤醒
Condition接口中的await后signal方法实现线程等待和唤醒
传统的synchronized和Lock实现等待唤醒通知的约束
- 线程先要获得并持有锁,必须在锁块(synchronized或lock)中
- 必须要先等待后唤醒,线程才能够被唤醒
LockSupport类中的park等待和unpark唤醒
- 是什么
- 通过park()和unpark(thread)方法来实现阻塞和唤醒线程
- 官网解释--permit上限是1
- LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
- LockSupport类使用了一种名为Permit(许可)的概念来阻塞和唤醒线程,每个线程都有一个许可,permit只有两个值,0和1,默认是0;可以把许可看做是一种(0,1)信号量(Semaphore),但与Semaphore不同,许可的累加上限是1
- park() -- 除非许可证可用,否则禁用当前线程以进行线程调度。
- unpark(thread) -- 如果给定线程尚不可用,则为其提供许可。
- 主要方法
- 阻塞
- park()/park(Object)
- 调用park()时
permit默认是0,所以一开始调用park()方法时,当前线程就会被阻塞,直到其他线程将当前线程的permit设置为1时,park()方法会被唤醒,然后将permit再次设置为0并返回。
- 调用park()时
- 阻塞当前线程/阻塞传入的具体线程
- park()/park(Object)
- 唤醒
- unpark(Thread)
- 调用unpark(Thread)时
调用unpark(thread)后,就会将thread线程的许可permit设置成1(注意,多次调用此方法,不会累加,permit值还是1),会自动唤醒thread线程,即之前阻塞中的LockSupport.park()会立即返回。
- 调用unpark(Thread)时
- 唤醒处于阻塞状态的指定线程
- unpark(Thread)
- 阻塞
- Demo
public class LockSupportDemo { public static void main(String[] args) { Thread a = new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\t come in"); LockSupport.park(); System.out.println(Thread.currentThread().getName() + "\t 被唤醒"); },"A"); a.start(); Thread b = new Thread(() -> { LockSupport.unpark(a); System.out.println(Thread.currentThread().getName() + "\t 通知"); },"B"); b.start(); } }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15- 优点
- 正常+无锁块要求
- 无需先等待后唤醒,可以任意位置
- 重要说明
- LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
- LockSupport是一个线程阻塞工具类,它的所有方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法,LockSupport调用UNSAFE中的native代码。
- LockSupport提供park()和unpark(thread)方法实现阻塞线程和唤醒线程的过程。
- LockSupport和每个使用它的线程都有一个许可(permit)关联。permit相当于1,0的开关,默认是0。
- 调用一次unpark就加1变成1,也只能到1。
- 调用一次park会消费permit,将1变成0,同时park立即返回。
- 如果再次调用park会变成阻塞(因为permit为0了,一直到permit为1),这时调用unpark会把permit置为1。
- 每个线程都有一个相关的permit,permit最多只有1个,重复调用unpark也不会累计凭证。
- 是什么
- 面试题
- 1.为什么可以先唤醒后阻塞线程?
- 因为unpark获得了一个permit(凭证),之后再调用方法,就可以消费线程。
- 2.为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?
- 因为permit(凭证)的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证,而调用两次park却需要两个凭证,故阻塞。
- 1.为什么可以先唤醒后阻塞线程?
编辑 (opens new window)
上次更新: 2024/05/24, 11:36:46