Java中三种重要的锁
1.Synchornized对象锁
一般我们要解决临界区中的静态条件发生,我们一般使用两种手段
- 阻塞式解决方案:synchronized lock
- 非阻塞式解决方案: 原子类
下面我们着重讲解阻塞式解决方案:synchornized
synchronized俗称对象锁,采用互斥的方案让同一时刻至多只有一个线程能持有对象锁,其他线程想要获取这个对象锁就会被阻塞住,这样就保证拥有锁的线程可以安全执行临界区内的代码,不用担心线程上下文切换导致的并发问题
synchronized(对象){//线程1执行,线程2阻塞//临界区}
synchronized(对象)中的对象,可以想象成一个房间(room),有唯一的入口(房门),房间只能一次进入一个人,而我们的线程t1和线程t2就是两个人
当线程1执行到synchronized(room)时就好比t1进入了房间,锁住了门拿走了钥匙,在门内执行临界区代码
如果t2也走到了synchronized时,会发现门被锁住了,就会在门外等待(阻塞),这中间即使t1的cpu时间片不幸被用完,被提出了门外,但是由于钥匙还在它身上,因此t2还是进不来,必须等到t1来执行完代码然后把锁打开放出钥匙才能执行。
语法:
class Test{public synchornized void test(){}
}
//等价于
class Test{public void test(){sychronized(this){} }
}
class Test{public synchornized statsic void test(){}
}
//等价于
class Test{public static void test(){sychronized(Test.class){}}}
sychornized底层
Monitor对象头
其中的状态会根据锁的升级而改变
Monitor工作原理
当一个线程进入到sychronized中时,Monitor内部会发生改变,如果发现Monitor中的Owner指向了别的线程,就会进入EntryList阻塞队列进行等待。当线程执行完了以后,WaitSet就会指向这个线程,然后Thread就会让出锁,Owner就会指向其他阻塞队列中的线程
如何判断是否获取锁:由Monitor中的Owner来判定,如果Owner指向的线程为进来的线程就说明拥有这把锁.
Sychornized优化
1.轻量级锁
如果一个对象虽然有多个线程访问,但是访问的时间是错开的,也就是没有竞争,那么可以使用轻量级锁来优化
轻量级锁对使用者是透明的,既语法仍然是synchronized
假设有两个方法同步块,利用同一个对象加锁
static final Object obj = new Object();
public static void method1(){synchronized(obj){//同步块Amethod2()}}public static void method2(){synchronized(obj){//同步块B}}
此时由于一个锁有两个线程获取锁,但是没有发生竞争,因为第二个方法里的锁以及在方法1中获取了,所以此时的锁会升级为轻量级锁
2.锁膨胀
如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁升级为重量级锁
static Object obj = new Object();
public static void method1(){synchronized(obj){//同步块}}
当Thread-1进行轻量级加锁时,Thread-0以及对该对象加了轻量级锁
这时Thread1加轻量级锁失败,进入锁膨胀流程
- 即为Object对象申请Monitor锁,让Object指向重量级锁地址
- 然后自己进入Monitor的EntryList BLOCKED
当Thread0退出同步块解锁时,使用CAS将Mark Word的值恢复给对象头,失败。这时就会进入重量级锁流程,即按照Monitor地址找到Monitor对象,设置Owner为null,唤醒EntryList中BLOCKED线程
自旋优化
偏向锁
只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS,以后只要不发生竞争,这个对象就归改线程所有
重量级锁
重量级锁是 synchronized
在处理激烈线程竞争时使用的同步机制。它依赖于操作系统的互斥量(mutex) 实现,线程的阻塞与唤醒需要通过操作系统内核态的介入,因此开销较大,故称为 “重量级”。
当多个线程频繁竞争同一把锁,且轻量级锁的自旋优化(见下文)无法解决竞争时,锁会膨胀为重量级锁,此时线程会进入阻塞状态,放弃 CPU 资源,等待被唤醒后重新竞争锁。
优点
- 能稳定处理激烈竞争场景,确保多线程同步的正确性(原子性、可见性、有序性)。
缺点
- 性能开销大:线程的阻塞与唤醒需要从用户态切换到内核态(上下文切换),而内核态操作的耗时远高于用户态(通常是微秒级 vs 纳秒级)。
- 效率低:阻塞的线程无法参与 CPU 调度,可能导致资源利用率下降。
2.ReentrantLock可重入锁
ReentrantLock相对于synchronized有如下特点
- 可中断
- 可以设置超时时间
- 可以设置为公平锁(先进先出)
- 支持多个条件变量(可以对变量细分,如notify可以细分唤醒变量,而synchronized就是直接全部唤醒)
与synchronized一样,都支持可重入
基本语法
和synchronized不一样,reentrantlock是对象而非方法,在使用前需要先new一个
//获取锁
reentrantlock.lock();
try{//临界区} finally{//释放锁reentrantlock.unlock(); }
可重入
如下图,我们两个方法都用Reentrantlock加锁,这样它们每次获取都是获取的同一把锁,相当于重入了同一把锁
public void outerMethod() {lock.lock();try {System.out.println("获取外层锁");innerMethod(); // 调用内层方法} finally {lock.unlock();System.out.println("释放外层锁");}}public void innerMethod() {lock.lock();try {System.out.println("获取内层锁");System.out.println("当前锁的持有次数: " + lock.getHoldCount());} finally {lock.unlock();System.out.println("释放内层锁");}}
可打断
当一个线程正在等待获取锁时,可以被其他线程通过interrupt()
方法中断,从而避免无限等待。这是ReentrantLock
相比synchronized
的一个重要优势,后者在等待锁时无法被中断。
ReentrantLock
提供了两种获取锁的方式:
- 不可打断模式:使用
lock()
方法获取锁。若锁被其他线程持有,当前线程会进入阻塞状态,且无法被中断,直到锁被释放。 - 可打断模式:使用
lockInterruptibly()
方法获取锁。若锁被其他线程持有,当前线程会进入阻塞状态,但可以被其他线程中断(通过调用Thread.interrupt()
)。
如下代码所示:
// 线程1:持有锁并休眠5秒Thread t1 = new Thread(() -> {lock.lock();try {System.out.println("线程1获取锁,开始执行...");Thread.sleep(5000); // 模拟长时间持有锁} catch (InterruptedException e) {System.out.println("线程1被中断");} finally {lock.unlock();System.out.println("线程1释放锁");}});// 线程2:使用lockInterruptibly()获取锁Thread t2 = new Thread(() -> {try {System.out.println("线程2尝试获取锁(可打断模式)");lock.lockInterruptibly();try {System.out.println("线程2获取锁,开始执行");} finally {lock.unlock();System.out.println("线程2释放锁");}} catch (InterruptedException e) {System.out.println("线程2被中断,放弃获取锁");Thread.currentThread().interrupt(); // 恢复中断状态}});
公平锁与非公平锁(Fairness)
- 公平锁:线程按照请求锁的顺序获取锁(FIFO),避免 “线程饥饿”。
- 非公平锁:允许插队,锁释放时任何线程都可能竞争获取锁,性能更高
创建方式
ReentrantLock fairLock = new ReentrantLock(true); // 公平锁
ReentrantLock unfairLock = new ReentrantLock(false); // 非公平锁(默认)
- 公平锁:适用于需要保证线程执行顺序的场景(如资源分配)。
- 非公平锁:适用于高并发场景,减少线程切换开销。
条件变量(Condition)
提供更灵活的线程等待 / 通知机制,替代synchronized
的wait()
/notify()
使用方式:
lock.newCondition()
:创建与锁绑定的条件变量。condition.await()
:线程等待,释放锁。condition.signal()
/signalAll()
:唤醒等待的线程
ReentrantLock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();// 生产者线程
lock.lock();
try {while (queue.isFull()) {notFull.await(); // 队列满,等待}queue.add(item);notEmpty.signal(); // 通知消费者
} finally {lock.unlock();
}// 消费者线程
lock.lock();
try {while (queue.isEmpty()) {notEmpty.await(); // 队列空,等待}queue.take();notFull.signal(); // 通知生产者
} finally {lock.unlock();
}
Volatile
Volatile原理
volatile的底层实现原理是内存屏障,Memory Barrier
- 对volatile变量的写指令后会加入写屏障
- 对volatile变量的读指令前会加入读屏障
Java 内存模型规定了线程之间的变量访问规则:
- 主内存:所有变量的原始值存储在此。
- 工作内存:每个线程有自己的工作内存,保存了从主内存拷贝的变量副本。
可见性问题:当线程 A 修改了变量 X 的值,可能只更新了自己的工作内存,而未及时同步到主内存;此时线程 B 读取变量 X 时,仍使用自己工作内存中的旧值。
写屏障保证在该屏障之前的,对共享变量的改动,都同步到主存当中
public class VolatileExample {private volatile boolean flag = false;public void writer() {flag = true; // 写屏障:确保flag=true立即刷新到主内存}public void reader() {while (!flag) { // 读屏障:确保每次读取flag时都从主内存获取// 循环等待}System.out.println("flag is now true");}
}
读写屏障如何保证有序性
编译器和处理器为了优化性能,可能会对指令进行重排序。但volatile
变量的读写操作不会被重排序:
- 写操作前的指令不会被重排序到写操作之后。
- 读操作后的指令不会被重排序到读操作之前。
int a = 0;
volatile boolean flag = false;public void init() {a = 1; // 1flag = true; // 2(写操作禁止重排序到1之前)
}public void use() {if (flag) { // 3(读操作禁止重排序到4之后)int b = a; // 4}
}