聊一聊Java中到底有那些锁??
咦咦咦,各位小可爱,我是你们的好伙伴——bug菌,今天又来给大家普及Java SE相关知识点了,别躲起来啊,听我讲干货还不快点赞,赞多了我就有动力讲得更嗨啦!所以呀,养成先点赞后阅读的好习惯,别被干货淹没了哦~
🏆本文收录于「滚雪球学Java」专栏中,这个专栏专为有志于提升Java技能的你打造,覆盖Java编程的方方面面,助你从零基础到掌握Java开发的精髓。赶紧关注,收藏,学习吧!
环境说明:Windows 10 + IntelliJ IDEA 2021.3.2 + Jdk 1.8
前言
在现代软件开发中,多线程编程是提高应用程序性能和响应能力的关键。Java作为一种广泛使用的编程语言,提供了多种锁机制来确保线程安全。理解这些锁的类型及其应用场景,对于开发高效、稳定的多线程应用至关重要。本文将全面探讨Java中的锁机制,包括内置锁、重入锁、读写锁、悲观锁与乐观锁、偏向锁等,通过具体的代码示例深入解析每种锁的特点和使用方法,以帮助读者在实际开发中选择合适的锁。
Java中的锁概述
在Java中,锁用于控制多个线程对共享资源的访问,以避免竞争条件和数据不一致的问题。Java的锁可以分为以下几类:
- 内置锁(监视器锁)
- 重入锁
- 读写锁
- 悲观锁与乐观锁
- 偏向锁、轻量级锁和重量级锁
- 其他并发工具和特性
- 锁的选择策略
1. 内置锁(监视器锁)
Java中的每个对象都可以作为锁,使用 synchronized
关键字,开发者可以通过对象的监视器锁来保护代码块或方法。内置锁的使用相对简单,但在高竞争场景下容易导致线程阻塞。
示例代码:
class SynchronizedExample {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
在这个示例中,increment
方法被声明为同步方法,意味着同一时间只有一个线程可以执行它,从而确保对 count
变量的安全访问。
代码解析:
在本次的代码演示中,我将会深入剖析每句代码,详细阐述其背后的设计思想和实现逻辑。通过这样的讲解方式,我希望能够引导同学们逐步构建起对代码的深刻理解。我会先从代码的结构开始,逐步拆解每个模块的功能和作用,并指出关键的代码段,并解释它们是如何协同运行的。通过这样的讲解和实践相结合的方式,我相信每位同学都能够对代码有更深入的理解,并能够早日将其掌握,应用到自己的学习和工作中。
这段Java代码定义了一个名为SynchronizedExample
的类,演示了如何使用synchronized
关键字来确保线程安全。以下是代码的逐行解读:
SynchronizedExample 类
class SynchronizedExample
定义了一个名为SynchronizedExample
的类。
属性
private int count = 0;
定义了一个私有成员变量count
,初始值为0。
increment 方法
-
public synchronized void increment()
定义了一个公共的synchronized
方法increment
。synchronized
关键字用于确保当一个线程访问此方法时,其他线程不能访问类的其他synchronized
方法,从而避免并发问题。
-
count++;
在increment
方法内部,将count
变量的值增加1。
getCount 方法
-
public int getCount()
定义了一个公共方法getCount
,用于获取count
变量的当前值。- 返回
count
的值。
- 返回
小结
这个SynchronizedExample
类演示了如何使用synchronized
关键字来确保线程安全。通过将increment
方法声明为synchronized
,可以确保在多线程环境下,每次只有一个线程能够执行这个方法,从而避免了并发修改count
变量时可能出现的竞态条件。
使用示例
以下是如何使用SynchronizedExample
类的示例:
public class SynchronizedExampleTest {
public static void main(String[] args) {
SynchronizedExample example = new SynchronizedExample();
// 创建线程
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
// 启动线程
t1.start();
t2.start();
try {
// 等待线程执行完成
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 输出最终的计数值
System.out.println("最终计数值: " + example.getCount());
}
}
解释
-
创建实例:创建一个
SynchronizedExample
的实例。 -
创建线程:创建两个线程
t1
和t2
,它们都执行相同的任务——调用increment
方法1000次。 -
启动线程:通过调用
start
方法启动这两个线程。 -
等待线程执行完成:通过调用
join
方法等待这两个线程执行完成。 -
输出结果:输出最终的计数值。由于
increment
方法是synchronized
的,所以最终的计数值应该是2000。
注意事项
-
锁定对象:
synchronized
方法锁定的是当前实例对象(this
),这意味着不同实例的synchronized
方法之间不会相互阻塞。 -
性能问题:过度使用
synchronized
可能会导致性能问题,因为它限制了代码的并行执行。在某些情况下,可以考虑使用其他并发工具,如ReentrantLock
。 -
锁定范围:如果需要更细粒度的锁定控制,可以使用
synchronized
块,这样可以只锁定需要同步的代码段,而不是整个方法。
2. 重入锁(ReentrantLock)
ReentrantLock
是 java.util.concurrent.locks
包中提供的锁,功能更为强大,支持公平锁和非公平锁。重入锁允许同一个线程多次获得同一把锁,避免了死锁的风险。
示例代码:
import java.util.concurrent.locks.ReentrantLock;
class ReentrantLockExample {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock(); // 获取锁
try {
count++;
} finally {
lock.unlock(); // 确保释放锁
}
}
public int getCount() {
return count;
}
}
在这个示例中,ReentrantLock
用于控制对 count
的访问,确保在多线程环境中操作的安全性。通过 try-finally
结构,确保锁在任何情况下都能释放,从而避免潜在的死锁问题。
代码解析:
在本次的代码演示中,我将会深入剖析每句代码,详细阐述其背后的设计思想和实现逻辑。通过这样的讲解方式,我希望能够引导同学们逐步构建起对代码的深刻理解。我会先从代码的结构开始,逐步拆解每个模块的功能和作用,并指出关键的代码段,并解释它们是如何协同运行的。通过这样的讲解和实践相结合的方式,我相信每位同学都能够对代码有更深入的理解,并能够早日将其掌握,应用到自己的学习和工作中。
这段Java代码定义了一个名为ReentrantLockExample
的类,演示了如何使用ReentrantLock
来确保线程安全。以下是代码的逐行解读:
导入必要的类
import java.util.concurrent.locks.ReentrantLock;
这行导入语句引入了ReentrantLock
类,它是Java并发包中提供的一个锁机制。
ReentrantLockExample 类
class ReentrantLockExample
定义了一个名为ReentrantLockExample
的类。
属性
-
private int count = 0;
定义了一个私有成员变量count
,初始值为0。 -
private final ReentrantLock lock = new ReentrantLock();
定义了一个ReentrantLock
实例lock
,并初始化它。final
关键字表示这个锁实例在初始化后不能被重新赋值。
increment 方法
-
public void increment()
定义了一个公共方法increment
。 -
lock.lock();
调用lock
对象的lock
方法获取锁。如果锁当前被其他线程持有,则当前线程将被阻塞,直到该锁被释放。 -
try { count++; }
在try
块中,执行增加count
的操作。将count
变量的值增加1。 -
finally { lock.unlock(); }
在finally
块中,调用unlock
方法释放锁。这样做确保了无论try
块中的代码是否成功执行,锁都会被释放,避免了死锁的发生。
getCount 方法
-
public int getCount()
定义了一个公共方法getCount
,用于获取count
变量的当前值。- 返回
count
的值。
- 返回
总结
这个ReentrantLockExample
类演示了如何使用ReentrantLock
来确保线程安全。通过在修改共享资源之前获取锁,在修改之后释放锁,可以确保在多线程环境下,每次只有一个线程能够执行修改操作,从而避免了并发修改共享资源时可能出现的竞态条件。
使用示例
以下是如何使用ReentrantLockExample
类的示例:
public class ReentrantLockExampleTest {
public static void main(String[] args) {
ReentrantLockExample example = new ReentrantLockExample();
// 创建线程
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
// 启动线程
t1.start();
t2.start();
try {
// 等待线程执行完成
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 输出最终的计数值
System.out.println("最终计数值: " + example.getCount());
}
}
解释
-
创建实例:创建一个
ReentrantLockExample
的实例。 -
创建线程:创建两个线程
t1
和t2
,它们都执行相同的任务——调用increment
方法1000次。 -
启动线程:通过调用
start
方法启动这两个线程。 -
等待线程执行完成:通过调用
join
方法等待这两个线程执行完成。 -
输出结果:输出最终的计数值。由于
increment
方法是通过ReentrantLock
来同步的,所以最终的计数值应该是2000。
注意事项
-
锁的公平性:
ReentrantLock
可以是公平锁或非公平锁。默认情况下,它是非公平锁,因为非公平锁的性能通常更好。如果需要公平性,可以在创建ReentrantLock
实例时传递true
作为参数。 -
锁的重入性:
ReentrantLock
支持重入,即同一个线程可以多次获得同一把锁。 -
条件变量:
ReentrantLock
还支持条件变量,可以通过lock.newCondition()
来创建。 -
性能:与
synchronized
相比,ReentrantLock
提供了更高的灵活性,例如尝试非阻塞地获取锁、可中断的锁获取等。
3. 读写锁(ReadWriteLock)
ReadWriteLock
允许多个线程同时读取,但在写入时,所有读取和写入都会被阻塞。读写锁适合于读多写少的场景,可以提高读取效率。
示例代码:
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
class ReadWriteLockExample {
private int count = 0;
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
public void increment() {
rwLock.writeLock().lock(); // 获取写锁
try {
count++;
} finally {
rwLock.writeLock().unlock(); // 释放写锁
}
}
public int getCount() {
rwLock.readLock().lock(); // 获取读锁
try {
return count;
} finally {
rwLock.readLock().unlock(); // 释放读锁
}
}
}
在此示例中,increment
方法使用写锁,而 getCount
方法使用读锁。这样允许多个线程同时读取 count
,提高了程序的并发性能。
代码解析:
在本次的代码演示中,我将会深入剖析每句代码,详细阐述其背后的设计思想和实现逻辑。通过这样的讲解方式,我希望能够引导同学们逐步构建起对代码的深刻理解。我会先从代码的结构开始,逐步拆解每个模块的功能和作用,并指出关键的代码段,并解释它们是如何协同运行的。通过这样的讲解和实践相结合的方式,我相信每位同学都能够对代码有更深入的理解,并能够早日将其掌握,应用到自己的学习和工作中。
这段Java代码定义了一个名为ReadWriteLockExample
的类,演示了如何使用ReentrantReadWriteLock
实现读写锁,以优化多线程环境中读操作的并发性能。以下是代码的逐行解读:
导入必要的类
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
这两行导入了ReadWriteLock
和ReentrantReadWriteLock
类,它们都是Java并发包中提供的一部分,用于实现读写锁。
ReadWriteLockExample 类
class ReadWriteLockExample
定义了一个名为ReadWriteLockExample
的类。
属性
-
private int count = 0;
定义了一个私有成员变量count
,初始值为0。 -
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
定义了一个ReadWriteLock
实例rwLock
,并初始化为ReentrantReadWriteLock
。final
关键字表示这个锁实例在初始化后不能被重新赋值。
increment 方法
-
public void increment()
定义了一个公共方法increment
。 -
rwLock.writeLock().lock();
调用rwLock
的writeLock
方法获取写锁。写锁确保在修改共享资源时不会有其他线程进行读或写操作。 -
try { count++; }
在try
块中,执行增加count
的操作。将count
变量的值增加1。 -
finally { rwLock.writeLock().unlock(); }
在finally
块中,调用unlock
方法释放写锁。这样做确保了无论try
块中的代码是否成功执行,锁都会被释放,避免了死锁的发生。
getCount 方法
-
public int getCount()
定义了一个公共方法getCount
,用于获取count
变量的当前值。 -
rwLock.readLock().lock();
调用rwLock
的readLock
方法获取读锁。读锁允许多个线程同时读取共享资源,但在写操作执行时,所有读操作都必须等待。 -
try { return count; }
在try
块中,返回count
的值。 -
finally { rwLock.readLock().unlock(); }
在finally
块中,调用unlock
方法释放读锁。
总结
这个ReadWriteLockExample
类演示了如何使用ReentrantReadWriteLock
来实现读写锁,以优化多线程环境下读操作的并发性能。通过在修改共享资源时获取写锁,在读取共享资源时获取读锁,可以确保数据的一致性和线程安全。同时,允许多个线程同时读取,提高了程序的并发性能。
使用示例
以下是如何使用ReadWriteLockExample
类的示例:
public class ReadWriteLockExampleTest {
public static void main(String[] args) {
final ReadWriteLockExample example = new ReadWriteLockExample();
// 创建读线程
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
System.out.println("读操作结果: " + example.getCount());
}
});
// 创建写线程
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
// 启动线程
t1.start();
t2.start();
try {
// 等待线程执行完成
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 输出最终的计数值
System.out.println("最终计数值: " + example.getCount());
}
}
解释
-
创建实例:创建一个
ReadWriteLockExample
的实例。 -
创建线程:创建一个读线程
t1
和一个写线程t2
。读线程执行1000次读操作,写线程执行1000次写操作。 -
启动线程:通过调用
start
方法启动这两个线程。 -
等待线程执行完成:通过调用
join
方法等待这两个线程执行完成。 -
输出结果:输出最终的计数值。由于
increment
方法和getCount
方法是通过ReentrantReadWriteLock
来同步的,所以最终的计数值应该是2000。
注意事项
-
锁的公平性:与
ReentrantLock
一样,ReentrantReadWriteLock
也可以是公平锁或非公平锁。默认情况下,它是非公平锁。 -
锁的重入性:
ReentrantReadWriteLock
支持重入,即同一个线程可以多次获得同一把锁。 -
性能:读写锁提供了比
synchronized
更细粒度的锁控制,允许多个线程同时进行读操作,从而提高了并发性能。 -
死锁:尽管读写锁可以提高并发性能,但不当使用仍然可能导致死锁。例如,如果一个线程同时持有读锁和写锁,就可能导致其他线程永远等待锁的释放。
4. 悲观锁与乐观锁
- 悲观锁:假设会发生冲突,通常通过
synchronized
或ReentrantLock
来实现。它会阻止其他线程访问被锁定的资源。
悲观锁示例代码:
class PessimisticLockExample {
private int count = 0;
public synchronized void increment() {
count++;
}
}
- 乐观锁:假设不会发生冲突,使用版本号或时间戳进行数据更新。Java中的
Atomic
类和compareAndSet
方法是乐观锁的实现。
乐观锁示例:
import java.util.concurrent.atomic.AtomicInteger;
class OptimisticLockExample {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
乐观锁适用于那些并发冲突较少的场景,因为它通过乐观假设避免了锁的开销。
5. 偏向锁、轻量级锁和重量级锁
-
偏向锁:优化锁的性能,当一个线程获得锁后,其他线程尝试获取时,不会立即阻塞,而是稍等。这种锁的设计用于减少无竞争情况下的锁获取延迟。
-
轻量级锁:当锁没有被占用时,轻量级锁不会引起线程的阻塞,允许多个线程尝试获取锁。
-
重量级锁:当锁被占用时,其他线程必须进入等待状态,这会造成性能的下降。使用
synchronized
关键字时,通常会使用重量级锁。
6. 其他并发工具和特性
Java还提供了其他并发工具和特性,如 Semaphore
、CountDownLatch
和 CyclicBarrier
等,帮助开发者在不同场景下进行更灵活的线程控制。
示例代码:
- Semaphore:可以限制同时访问特定资源的线程数量。
import java.util.concurrent.Semaphore;
class SemaphoreExample {
private final Semaphore semaphore = new Semaphore(3); // 允许3个线程访问
public void accessResource() {
try {
semaphore.acquire(); // 获取许可
// 访问共享资源
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
semaphore.release(); // 释放许可
}
}
}
- CountDownLatch:用于等待一组线程完成。
import java.util.concurrent.CountDownLatch;
class CountDownLatchExample {
private final CountDownLatch latch = new CountDownLatch(3);
public void performTask() {
// 模拟任务执行
latch.countDown(); // 完成一个任务
}
public void waitForTasks() throws InterruptedException {
latch.await(); // 等待所有任务完成
}
}
代码解析:
在本次的代码演示中,我将会深入剖析每句代码,详细阐述其背后的设计思想和实现逻辑。通过这样的讲解方式,我希望能够引导同学们逐步构建起对代码的深刻理解。我会先从代码的结构开始,逐步拆解每个模块的功能和作用,并指出关键的代码段,并解释它们是如何协同运行的。通过这样的讲解和实践相结合的方式,我相信每位同学都能够对代码有更深入的理解,并能够早日将其掌握,应用到自己的学习和工作中。
这两段Java代码演示了如何使用Semaphore
和CountDownLatch
这两种并发工具来控制线程的访问和同步。以下是对每个示例的详细解读:
SemaphoreExample 类
-
Semaphore 概念:
Semaphore
是一个计数信号量,用来控制同时访问某个特定资源或资源池的线程数量。
-
代码解读:
import java.util.concurrent.Semaphore; class SemaphoreExample { private final Semaphore semaphore = new Semaphore(3); // 允许3个线程访问 public void accessResource() { try { semaphore.acquire(); // 获取许可 // 访问共享资源 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { semaphore.release(); // 释放许可 } } }
private final Semaphore semaphore = new Semaphore(3);
初始化一个Semaphore
对象,允许最多3个线程同时访问受保护的资源。semaphore.acquire();
当线程想要访问资源时,它首先需要获取一个许可。如果没有可用的许可,线程将被阻塞,直到其他线程释放许可。semaphore.release();
线程访问完资源后释放许可,使得其他正在等待的线程可以获得许可并继续执行。
CountDownLatchExample 类
-
CountDownLatch 概念:
CountDownLatch
是一个同步助手,用来让一个或多个线程等待直到在其他线程中执行的一组操作完成。
-
代码解读:
import java.util.concurrent.CountDownLatch; class CountDownLatchExample { private final CountDownLatch latch = new CountDownLatch(3); public void performTask() { // 模拟任务执行 latch.countDown(); // 完成一个任务 } public void waitForTasks() throws InterruptedException { latch.await(); // 等待所有任务完成 } }
private final CountDownLatch latch = new CountDownLatch(3);
初始化一个CountDownLatch
对象,初始化计数值为3,表示主线程将等待3个任务完成。latch.countDown();
每次任务执行完毕后调用此方法,将内部计数减1。latch.await();
调用此方法的线程将一直等待,直到CountDownLatch
的计数器达到零。这确保了只有当所有指定的任务都完成后,才会继续执行后续的代码。
使用场景
-
Semaphore:
- 适用于限制资源的并发访问数量,例如数据库连接池、限流等。
-
CountDownLatch:
- 适用于需要等待一组操作完成后再继续执行的场景,例如初始化操作、等待多个异步任务完成等。
注意事项
-
Semaphore:
- 必须确保
acquire
和release
成对出现,包括异常情况下也应释放许可。
- 必须确保
-
CountDownLatch:
- 计数器一旦被创建,不能被重置。如果需要重用类似的同步模式,必须重新创建
CountDownLatch
实例。 await
方法可以带超时参数,以防止可能的死锁问题。
- 计数器一旦被创建,不能被重置。如果需要重用类似的同步模式,必须重新创建
7. 锁的选择策略
在选择锁的过程中,开发者应考虑以下几点:
- 应用场景:确定是读多写少还是写多读少。读写锁在读多写少的场景中非常高效。
- 性能需求:在高并发场景下,轻量级锁和乐观锁能够减少竞争和等待,从而提高性能。
- 复杂性:使用简单易懂的锁机制有助于提高代码的可维护性,避免复杂的锁管理带来的问题。
- 开发环境:考虑团队对锁的理解和使用能力,选择合适的锁以便于团队的协作和维护。
锁的性能考量
在选择锁时,开发者应根据具体的应用场景权衡性能和复杂性。以下是一些常见的性能考量:
-
竞争程度:在多个线程频繁竞争同一资源的情况下,使用读写锁或更高效的锁可以减少性能损失。
-
锁的粒度:过于粗粒度的锁可能导致性能瓶颈,而过于细粒度的锁则可能增加管理复杂性。
-
读多写少的场景:在读多写少的场景中,使用读写锁通常可以提高性能。
-
系统负载:在高负载的系统中,轻量级锁或乐观锁可以减少上下文切换,提高性能。
-
代码可维护性:选择简单易用的锁可以降低代码复杂性,提高可维护性。
-
死锁风险:在设计锁时,需避免死锁的发生。使用重入锁可以帮助降低死锁的概率。
总结
Java中的锁机制为多线程编程提供了重要支持,了解不同类型的锁及其适用场景对开发高效和安全的并发程序至关重要。通过掌握内置锁、重入锁、读写锁等各种锁的特点,开发者可以根据实际需求选择合适的锁,提高程序的性能和稳定性。同时,借助Java的并发工具,开发
者可以更灵活地管理线程,从而编写出更简洁和高效的并发代码。
希望本文的内容能够帮助读者深入理解Java中的锁机制,并在实际编程中灵活应用。
参考文献
- Oracle. (2024). Java Concurrency in Practice.
- Java Documentation. (2024). java.util.concurrent.lock Package. Retrieved from Oracle Documentation.
- Bloch, J. (2018). Effective Java. Addison-Wesley.
- Lea, D. (2000). Concurrent Programming in Java: Design Principles and Patterns. Addison-Wesley.
- Goetz, B. et al. (2014). Java Concurrency. Addison-Wesley.
通过以上内容的深入探讨,读者不仅能掌握Java中锁的多种类型及其应用场景,还能在多线程开发中有效地提高代码的安全性和性能。希望这篇文章能为你的学习和工作带来帮助和启发。
☀️建议/推荐你
无论你是计算机专业的学生,还是对编程有兴趣的小伙伴,都建议直接毫无顾忌的学习此专栏「滚雪球学Java」,bug菌郑重承诺,凡是学习此专栏的同学,均能获取到所需的知识和技能,全网最快速入门Java编程,就像滚雪球一样,越滚越大,指数级提升。
码字不易,如果这篇文章对你有所帮助,帮忙给bug菌来个一键三连(关注、点赞、收藏) ,您的支持就是我坚持写作分享知识点传播技术的最大动力。
同时也推荐大家关注我的硬核公众号:「猿圈奇妙屋」 ;以第一手学习bug菌的首发干货,不仅能学习更多技术硬货,还可白嫖最新BAT大厂面试真题、4000G Pdf技术书籍、万份简历/PPT模板、技术文章Markdown文档等海量资料,你想要的我都有!
📣关于我
我是bug菌,CSDN | 掘金 | infoQ | 51CTO 等社区博客专家,历届博客之星Top30,掘金年度人气作者Top40,51CTO年度博主Top12,掘金等平台签约作者,华为云 | 阿里云| 腾讯云等社区优质创作者,全网粉丝合计30w+ ;硬核微信公众号「猿圈奇妙屋」,欢迎你的加入!免费白嫖最新BAT互联网公司面试题、4000G pdf电子书籍、简历模板等海量资料。
–End
- 点赞
- 收藏
- 关注作者
评论(0)