Java中常见的锁及其应用场景

Java中常见的锁及其应用场景

文章目录

一.概念二.常见锁及其应用场景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中。性能优化:通过偏向锁和轻量级锁减少锁的开销,提高多线程程序的性能。

相关推荐

轮回诀攻略大全 各玩法攻略汇总
beta365官网app下载

轮回诀攻略大全 各玩法攻略汇总

📅 07-31 👁️ 3912
在 iPhone、iPad 或 Mac 上使用 Game Center
约彩365彩票官方app下载安卓

在 iPhone、iPad 或 Mac 上使用 Game Center

📅 08-19 👁️ 7957
秒借借款怎么样?好下款吗?
beta365官网app下载

秒借借款怎么样?好下款吗?

📅 07-05 👁️ 7959
华迈云监控app官方版-华迈云监控手机版下载 v3.2.9.0316
约彩365彩票官方app下载安卓

华迈云监控app官方版-华迈云监控手机版下载 v3.2.9.0316

📅 07-10 👁️ 5036