在多线程编程的世界里,多个线程同时访问和修改共享资源是很常见的场景。然而,这种并发访问可能会导致数据不一致的问题,就好像好几个人同时修改一份文档,最后文档内容可能变得混乱不堪。为了避免这种情况,我们需要进行线程同步,保证数据的一致性。这一节,我们就来详细了解如何使用 关键字和
synchronized 接口来实现线程同步,并且学习如何处理线程同步中可能出现的死锁问题。
Lock
目录
线程同步的必要性synchronized 关键字的使用同步方法同步代码块
Lock 接口的使用ReentrantLock 类公平锁和非公平锁
处理线程同步中的死锁问题死锁的示例避免死锁的方法
总结
线程同步的必要性
在多线程环境下,多个线程可能会同时对同一个共享资源进行读写操作。如果没有适当的同步机制,就会出现数据不一致的问题,比如数据丢失、数据错误等。举个简单的例子,假设有两个线程同时对一个计数器进行加 1 操作。
public class Counter {
private int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("Count: " + counter.getCount());
}
}
在这个例子中,我们期望计数器的最终值是 2000,但实际上,由于两个线程可能同时读取和修改 变量,最终结果可能小于 2000。这就是因为没有进行线程同步,导致数据不一致。
count
synchronized 关键字的使用
同步方法
关键字可以用来修饰方法,使得该方法在同一时间只能被一个线程访问。当一个线程进入一个被
synchronized 修饰的方法时,它会自动获取该对象的锁,其他线程必须等待该线程释放锁才能进入该方法。
synchronized
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
在这个例子中, 方法被
increment 修饰,当一个线程调用
synchronized 方法时,其他线程必须等待该线程执行完该方法才能再次调用。这样就保证了
increment 变量的一致性。
count
同步代码块
除了同步方法, 关键字还可以用来创建同步代码块。同步代码块可以更细粒度地控制锁的范围,只对需要同步的代码进行加锁。
synchronized
public class Counter {
private int count = 0;
private final Object lock = new Object();
public void increment() {
synchronized (lock) {
count++;
}
}
public int getCount() {
return count;
}
}
在这个例子中,我们创建了一个 类型的锁对象
Object,并在
lock 方法中使用
increment 关键字对
synchronized 对象进行加锁。只有获取到
lock 对象锁的线程才能执行
lock 操作,从而保证了数据的一致性。
count++
Lock 接口的使用
接口是 Java 提供的另一种实现线程同步的方式。与
Lock 关键字相比,
synchronized 接口提供了更灵活的锁机制,比如可以实现公平锁、可重入锁等。
Lock
ReentrantLock 类
是
ReentrantLock 接口的一个实现类,它是一个可重入的互斥锁。可重入意味着同一个线程可以多次获取该锁,而不会被阻塞。
Lock
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private int count = 0;
private final Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
return count;
}
}
在这个例子中,我们使用 类来实现线程同步。在
ReentrantLock 方法中,我们首先调用
increment 方法获取锁,然后执行
lock.lock() 操作,最后在
count++ 块中调用
finally 方法释放锁。这样可以确保无论是否发生异常,锁都会被释放。
lock.unlock()
公平锁和非公平锁
可以创建公平锁和非公平锁。公平锁会按照线程请求锁的顺序来分配锁,而非公平锁则不保证这一点。默认情况下,
ReentrantLock 创建的是非公平锁。
ReentrantLock
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private int count = 0;
private final Lock fairLock = new ReentrantLock(true); // 公平锁
public void increment() {
fairLock.lock();
try {
count++;
} finally {
fairLock.unlock();
}
}
public int getCount() {
return count;
}
}
处理线程同步中的死锁问题
死锁是线程同步中一个比较棘手的问题。当两个或多个线程相互等待对方释放锁时,就会发生死锁。例如,线程 A 持有锁 1 并等待锁 2,而线程 B 持有锁 2 并等待锁 1,这样两个线程就会陷入无限等待的状态。
死锁的示例
public class DeadlockExample {
private final Object lock1 = new Object();
private final Object lock2 = new Object();
public void thread1Method() {
synchronized (lock1) {
System.out.println("Thread 1: Holding lock 1...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 1: Waiting for lock 2...");
synchronized (lock2) {
System.out.println("Thread 1: Holding lock 1 and lock 2...");
}
}
}
public void thread2Method() {
synchronized (lock2) {
System.out.println("Thread 2: Holding lock 2...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 2: Waiting for lock 1...");
synchronized (lock1) {
System.out.println("Thread 2: Holding lock 2 and lock 1...");
}
}
}
public static void main(String[] args) {
DeadlockExample example = new DeadlockExample();
Thread thread1 = new Thread(example::thread1Method);
Thread thread2 = new Thread(example::thread2Method);
thread1.start();
thread2.start();
}
}
在这个例子中, 方法先获取
thread1Method 锁,然后尝试获取
lock1 锁;而
lock2 方法先获取
thread2Method 锁,然后尝试获取
lock2 锁。如果两个线程同时执行,就可能会发生死锁。
lock1
避免死锁的方法
避免嵌套锁:尽量避免在一个同步代码块中嵌套另一个同步代码块,减少死锁的可能性。按顺序获取锁:如果需要获取多个锁,确保所有线程都按照相同的顺序获取锁。使用定时锁:使用 接口的
Lock 方法,设置一个超时时间,如果在规定时间内无法获取锁,就放弃获取,避免无限等待。
tryLock
总结
通过这一节的学习,我们了解了线程同步的必要性,并且学会了使用 关键字和
synchronized 接口来实现线程同步。
Lock 关键字简单易用,适合大多数场景;而
synchronized 接口提供了更灵活的锁机制,比如公平锁和可重入锁。同时,我们还学习了如何处理线程同步中可能出现的死锁问题。
Lock
掌握了线程同步的实现方法后,下一节我们将深入学习线程间的通信机制,进一步完善对本章并发编程基础主题的认知。
🍃 程序员JDK5修炼手册系列专栏导航
建议按系列顺序阅读,从基础到进阶逐步掌握JDK核心能力,避免遗漏关键知识点~
系列文章衔接
🔖 专栏目录:《JDK5新特性》🔖 专栏目录:《JDK8新特性》🍃 博客概览:《程序员技术成长导航,专栏汇总》