文章目录
一.概念二.常见锁及其应用场景1. synchronized关键字1.1 应用场景1.2 特点1.3 实现原理1. 监视器2. 锁的获取和释放3. 示例代码
2. ReentrantLock(可重入锁)2.1 应用场景2.2 特点2.3 实现原理2.4 获取锁(lock):2.5 释放锁(unlock):
3. ReadWriteLock(读写锁)3.1 应用场景3.2 特点
4. OptimisticLock(乐观锁)4.1 应用场景4.2 特点
5. PessimisticLock(悲观锁)5.1 应用场景5.2 特点
6. CountDownLatch(倒计时锁存器)6.1 应用场景6.2 特点
三. ReentrantLock 和 synchronized 的区别1. 可重入性:2. 可中断性:3. 尝试非阻塞获取锁:4. 公平锁:5. 锁状态查询:6. 条件变量:7. 适用场景
四. 锁的升级过程一、锁的类型二、锁的升级过程(一)无锁状态(二)偏向锁(三)轻量级锁(四)重量级锁
三、锁的升级过程示例分析:
四、锁的升级过程的性能优化五、总结
一.概念
在Java中,锁是一种同步机制,用于控制多线程对共享资源的访问,以确保在任一时刻,只有一个线程能够执行业务代码。锁的主要目的是防止多个线程同时修改共享资源,从而保证数据的一致性和线程安全。
二.常见锁及其应用场景
1. synchronized关键字
1.1 应用场景
用于方法或代码块上,保证同一时间只有一个线程能够执行当前代码。每个对象都有一个监视器(monitor),当一个线程访问被synchronized修饰的方法或者代码块时,它必须先获得该对象的监视器.
1.2 特点
锁的粒度比较大,只能锁定整个方法或代码块.属于重量级锁.可重入锁:一个线程获取了锁之后,可以再次获取这个锁而不会被阻塞,通常用于复杂的业务逻辑.不可中断:当一个线程持有synchronized锁时,其他线程无法中断它,除非抛出异常或者正常执行结束。
1.3 实现原理
1. 监视器
定义:每个Java对象都有一个与之关联的监视器。当一个线程进入一个对象的 synchronized 方法或 synchronized 块时,它会尝试获取该对象的监视器。
monitor 的结构 • 锁状态:表示锁是否被占用。 • 持有锁的线程:记录当前持有锁的线程。 • 等待队列:存储等待锁的线程。 • 条件队列:存储等待条件变量的线程。
作用:监视器用于确保在同一时刻只有一个线程可以执行该对象的 synchronized 代码块。如果一个线程已经持有对象的监视器,其他线程必须等待该锁被释放
2. 锁的获取和释放
获取锁:当线程尝试进入一个 synchronized 方法或 synchronized 块时,它会请求获取对象的监视器。如果锁已经被其他线程持有,当前线程会被阻塞,直到锁被释放。释放锁:当线程完成 synchronized 方法或 synchronized 块的执行后,它会自动释放监视器锁。如果其他线程正在等待该锁,其中一个线程将被唤醒并尝试获取锁。
3. 示例代码
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
public class Main {
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + counter.getCount());
}
}
2. ReentrantLock(可重入锁)
2.1 应用场景
需要更灵活的锁操作时使用,比如尝试非阻塞获取锁、可中断获取锁、公平性选择、超时获取锁。
2.2 特点
显示地获取和释放
可中断:通过lockInterruptibly()方法支持中断,使得线程在等待锁时可以被中断,从而避免长时间的阻塞。
可重入性:ReentrantLock支持可重入性,即同一个线程可以多次获取同一个锁而不会导致死锁。这是通过维护一个锁计数器来实现的,每次获取锁时计数器递增,每次释放锁时计数器递减
非公平锁:默认情况下,ReentrantLock是非公平的。这意味着即使有线程在等待队列中等待,新来的线程仍有机会立即获取锁。
公平锁:可以通过构造函数指定为公平锁。在公平锁模式下,AQS会严格按照FIFO顺序来分配锁
非阻塞性:tryLock()方法,线程在尝试获取锁时,如果锁不可用,线程不会进入阻塞状态,而是立即返回一个失败的结果。从而避免线程在等待锁的过程中被长时间阻塞。比如热点key过期,未获取锁的线程可以返回给用户历史数据,而不是阻塞在那里等待获取锁,为的就是提升用户体验.
超时获取:允许在指定时间内尝试获取锁—tryLock(long timeout, TimeUnit unit)方法–如果获取成功,则执行业务逻辑;如果超时未能获取锁,则执行其他操作或重试.
2.3 实现原理
基于AQS来实现,它是一个框架,有两大核心组件状态变量(state):AQS维护一个volatile int类型的变量state,用于表示同步状态。对于ReentrantLock,state表示锁的持有计数。等待队列:AQS使用一个FIFO队列来管理等待获取锁的线程。当线程尝试获取锁但失败时,它会被封装成一个节点并加入到等待队列中。
2.4 获取锁(lock):
当线程尝试获取锁时,首先检查state是否为0(表示锁未被持有).如果state为0,尝试通过CAS操作将state设置为1(表示锁被当前线程持有).如果state不为0,且当前线程已经持有锁(可重入性),则state递增.如果锁被其他线程持有,当前线程会被封装成节点并加入到等待队列中,然后进入阻塞状态
2.5 释放锁(unlock):
当线程释放锁时,state递减.如果state递减为0,表示锁完全释放,AQS会从等待队列中唤醒下一个节点对应的线程
3. ReadWriteLock(读写锁)
3.1 应用场景
当读操作远多于写操作时,用于提高并发性能,允许多个读操作同时进行,写操作则独自占用锁.
3.2 特点
由两个锁组成,一个读锁.一个写锁,读锁可以被多个读操作共享,写锁是独占的.
4. OptimisticLock(乐观锁)
4.1 应用场景
一般用于读多写少的场景
4.2 特点
乐观地认为共享数据被修改的概率很低,所以在读取的时候不会加锁.一般通过版本号或者时间戳来实现.线程在提交更新的时候会检查数据是否被其他线程修改过.
5. PessimisticLock(悲观锁)
5.1 应用场景
一般用于写多读少的场景
5.2 特点
悲观地认为共享数据被修改的概率很高,因此在数据被访问的时候立即加锁,以防止其他线程的修改.
6. CountDownLatch(倒计时锁存器)
6.1 应用场景
用于一个或多个线程等待其他线程完成操作后再继续执行的场景,比如主线程阻塞,等所有子线程操作完之后,再继续执行。
6.2 特点
通过一个计数器来控制,当计数器减到0时,所有等待的线程才会继续执行.
三. ReentrantLock 和 synchronized 的区别
1. 可重入性:
synchronized:天然支持可重入性。当一个线程进入一个同步方法或代码块时,它会自动获得锁,并且在同一个线程中可以多次进入同步方法或代码块.ReentrantLock:也支持可重入性,但需要显式地调用lock()和unlock()方法来获取和释放锁.
2. 可中断性:
synchronized:不支持中断。当线程在等待锁时被中断,它仍然会继续等待.ReentrantLock:支持可中断性。通过lockInterruptibly()方法,线程在等待锁时可以被中断,如果被中断,则会抛出InterruptedException.
3. 尝试非阻塞获取锁:
synchronized:没有提供尝试非阻塞获取锁的机制.ReentrantLock:通过tryLock()方法可以尝试获取锁,如果获取失败,可以立即返回或设置超时时间.
4. 公平锁:
synchronized:不支持公平锁的概念.ReentrantLock:可以创建公平锁,通过构造函数指定是否为公平锁,从而确保线程按照请求锁的顺序获取锁.
5. 锁状态查询:
synchronized:无法查询锁的状态,例如是否有线程正在等待锁.ReentrantLock:提供了查询锁状态的方法,如isLocked()、isFair()、hasQueuedThreads()等.
6. 条件变量:
synchronized:不支持条件变量.ReentrantLock:通过newCondition()方法可以创建条件变量,用于复杂的线程同步逻辑,如生产者-消费者模型(条件变量用于判断是否使得消费者线程处于阻塞状态)
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadOrderExample {
private final ReentrantLock lock = new ReentrantLock();
private final Condition producerCondition= lock.newCondition();
private final Condition consumerCondition= lock.newCondition();
private int turn = 1; // 初始为1,表示生产者线程先执行
public void producerThread() {
lock.lock();
try {
while (turn != 1) {
producerCondition.await();
}
System.out.println("producerThread is running");
turn = 2;
// 通过signal()方法唤醒下一个线程的条件
consumerCondition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void ConsumerThread() {
lock.lock();
try {
while (turn != 2) {
consumerCondition.await();
}
System.out.println("ConsumerThread is running");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ThreadOrderExample example = new ThreadOrderExample();
Thread t1 = new Thread(() -> example.consumerThread());
Thread t2 = new Thread(() -> example.producerThread());
t1.start();
t2.start();
}
}
7. 适用场景
synchronized:适用于简单的同步需求,代码简洁,易于理解和使用.ReentrantLock:适用于复杂的同步需求,如需要中断、尝试获取锁、公平锁等特性时.
四. 锁的升级过程
在Java中,锁的升级过程主要与Java内存模型(JMM)和Java的同步机制相关。锁的升级是Java虚拟机(JVM)为了优化锁的性能而采取的一种策略。以下是锁的升级过程的详细解释,包括相关的概念和代码示例。
一、锁的类型
在Java中,锁主要分为以下几种类型:
无锁(无同步状态):没有任何锁的状态。偏向锁(Biased Locking):偏向于第一个获取锁的线程,减少锁的开销。轻量级锁(Lightweight Locking):通过CAS操作实现锁的获取和释放。重量级锁(Heavyweight Locking):使用操作系统级别的锁,性能开销较大。
二、锁的升级过程
锁的升级过程是单向的,从偏向锁到轻量级锁,再到重量级锁。锁的状态存储在对象头的Mark Word中。
(一)无锁状态
状态:对象头的Mark Word中没有锁信息。适用场景:当对象没有被任何线程锁定时,处于无锁状态。
(二)偏向锁
状态:对象头的Mark Word中存储了偏向线程的ID。适用场景:当一个线程多次访问同一个对象时,偏向锁可以减少锁的开销。工作原理:
当线程第一次获取锁时,JVM会尝试将对象头的Mark Word设置为偏向该线程的ID。如果Mark Word已经是该线程的ID,则直接进入临界区。如果Mark Word是其他线程的ID,或者是无锁状态,JVM会尝试撤销偏向锁并升级为轻量级锁。
(三)轻量级锁
状态:对象头的Mark Word中存储了轻量级锁的标记。适用场景:当锁的竞争不激烈时,轻量级锁可以减少锁的开销。工作原理:
当线程尝试获取锁时,JVM会使用CAS操作将对象头的Mark Word设置为轻量级锁的状态。如果CAS操作成功,线程进入临界区。如果CAS操作失败(说明其他线程已经获取了锁),JVM会尝试自旋等待锁的释放。如果自旋等待失败(例如锁被其他线程长时间持有),JVM会将轻量级锁升级为重量级锁。
(四)重量级锁
状态:对象头的Mark Word中存储了重量级锁的标记。适用场景:当锁的竞争非常激烈时,重量级锁可以保证线程的安全性。工作原理:
当线程尝试获取锁时,JVM会将对象头的Mark Word设置为重量级锁的状态。如果锁已经被其他线程持有,当前线程会被阻塞,直到锁被释放。
三、锁的升级过程示例
以下是一个简单的代码示例,展示了锁的升级过程:
public class LockUpgradeExample {
private final Object lock = new Object();
public void method() {
synchronized (lock) {
// 临界区代码
try {
Thread.sleep(1000); // 模拟长时间的锁持有
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
LockUpgradeExample example = new LockUpgradeExample();
// 启动多个线程,模拟锁的竞争
Thread t1 = new Thread(() -> example.method());
Thread t2 = new Thread(() -> example.method());
t1.start();
Thread.sleep(100); // 确保t1先获取锁
t2.start();
t1.join();
t2.join();
}
}
分析:
偏向锁:
当Thread t1第一次尝试获取锁时,JVM会尝试将锁偏向Thread t1。如果Thread t1多次访问该锁,偏向锁可以减少锁的开销。 轻量级锁:
当Thread t2尝试获取锁时,发现锁已经被Thread t1持有,JVM会将偏向锁升级为轻量级锁。Thread t2会尝试自旋等待锁的释放。 重量级锁:
如果Thread t1长时间持有锁,Thread t2的自旋等待失败,JVM会将轻量级锁升级为重量级锁。Thread t2会被阻塞,直到Thread t1释放锁。
四、锁的升级过程的性能优化
锁的升级过程是JVM为了优化锁的性能而采取的一种策略。通过偏向锁和轻量级锁,JVM可以减少锁的开销,提高多线程程序的性能。然而,锁的升级过程也会带来一些开销,因此在设计多线程程序时,需要注意以下几点:
减少锁的竞争:尽量减少多个线程对同一个锁的竞争。合理设计锁的粒度:根据实际需求,选择合适的锁粒度,避免锁的过度竞争。使用其他同步机制:在某些情况下,可以使用java.util.concurrent包中的其他同步机制,如ReentrantLock、Semaphore等,以提高性能。
五、总结
锁的升级过程:从偏向锁到轻量级锁,再到重量级锁。锁的状态存储:锁的状态存储在对象头的Mark Word中。性能优化:通过偏向锁和轻量级锁减少锁的开销,提高多线程程序的性能。