Java线程同步方式:synchronized、ReentrantLock、wait/notify与并发工具

线程的同步是为了保证多个线程按照特定的顺序、协调地访问共享资源,避免数据不一致和竞争条件等问题。
在Java中,常见的线程同步方式有以下几种:
一、使用synchronized关键字
synchronized关键字是Java中最常用的线程同步方式之一。
通过在方法或代码块前加上synchronized关键字。,实现对共享资源的互斥访问。当一个线程进入synchronized代码块或方法时,它会自动获取到该对象的锁,其他线程需要等待锁释放后才能进入。确保同一时间只有一个线程可以执行标记为同步的代码。这样可以避免多个线程同时访问共享资源造成的数据不一致问题。
原理
对象锁:
每个 Java 对象都有一个内置的锁(也称为监视器锁或互斥锁),这个锁是线程独占的。
当一个线程进入 synchronized 块时,它会尝试获取对象的锁。如果锁已经被其他线程持有,则该线程会被阻塞直到获取到这个锁为止。
对象头信息:
在对象的内存布局中,有一个对象头信息存储着指向相关监视器(锁)的指针。
当线程尝试进入 synchronized 块时,它会尝试获取对象头信息中的锁。
锁升级:
synchronized 锁有多种状态,包括偏向锁、轻量级锁和重量级锁,根据并发情况会进行相应的升级。
通常情况下,当多个线程竞争同一把锁时,锁会从偏向锁升级为轻量级锁,再升级为重量级锁,以保证线程安全。
原子性操作:
synchronized 关键字可以确保其中的代码块在同一时刻只能有一个线程执行,保证这些操作是原子性的,不会被其他线程干扰。
释放锁:
当线程退出 synchronized 块时,它会释放对象的锁,让其他线程可以获取这个锁并执行相应的代码。
注意点
使用synchronized关键字时,需要注意以下几点:
确定共享资源:需要明确共享资源是什么,即在哪些地方需要进行同步。
锁的粒度:锁的粒度应该尽可能小,以减少同步带来的性能损失。只在必要的地方进行同步。
避免死锁:确保同步过程中不会出现死锁的情况。避免循环锁定和资源竞争。
使用示例
public class SynchronizationExample {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
public static void main(String[] args) {
SynchronizationExample example = new SynchronizationExample();
// 创建多个线程并启动
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
thread1.start();
thread2.start();
// 等待线程执行完毕
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + example.getCount());
}
}
二 、使用ReentrantLock类
它是一个可重入锁,通过调用lock()和unlock()方法获取和释放锁。与synchronized不同,ReentrantLock提供了更灵活的同步控制,例如可实现公平性和试锁等待时间。
原理
独占锁:
ReentrantLock 是一种独占锁,每次只允许一个线程获取锁并执行锁保护的临界区代码。
可重入性:
ReentrantLock 支持线程的可重入性,即同一个线程可以多次获得同一把锁而不会发生死锁。
这意味着线程可以在持有锁的情况下多次进入由这把锁保护的临界区代码,而不会被阻塞。
Condition 条件变量:
ReentrantLock 提供了 Condition 对象,可以实现更灵活的线程通信和控制。
Condition 对象可以让线程在特定条件下等待或唤醒其他线程,这种功能比内置的 wait() 和 notify() 更加灵活。
公平锁和非公平锁:
ReentrantLock 可以根据需要创建公平锁或非公平锁。
公平锁会按照线程请求锁的顺序进行获取,而非公平锁则不保证线程获取锁的顺序。
Lock 接口的方法:
ReentrantLock 实现了 Lock 接口,提供了 lock()、unlock()、tryLock() 等方法来控制锁的获取和释放。
通过这些方法,可以精确地控制线程对共享资源的访问,实现更细粒度的线程同步。
使用示例
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private int count = 0;
private Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
return count;
}
public static void main(String[] args) {
ReentrantLockExample example = new ReentrantLockExample();
// 创建多个线程并启动
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
thread1.start();
thread2.start();
// 等待线程执行完毕
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + example.getCount());
}
}
三、使用wait()、notify()和notifyAll()方法
这些方法是Object类的方法,允许线程间进行协作和通信。通过调用wait()方法使线程进入等待状态,然后其他线程可以通过notify()或notifyAll()方法唤醒等待的线程。
使用示例
public class WaitNotifyExample {
public static void main(String[] args) {
Object lock = new Object();
boolean flag = false;
Thread thread1 = new Thread(() -> {
synchronized (lock) {
while (!flag) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Thread 1 executed");
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock) {
flag = true;
lock.notify();
System.out.println("Thread 2 executed");
}
});
thread1.start();
thread2.start();
}
}
四、使用CountDownLatch和CyclicBarrier
它们是并发工具类,用于线程之间的同步和等待。CountDownLatch可用于等待一组线程完成操作,而CyclicBarrier用于等待一组线程互相达到屏障位置。
使用示例
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
public class ConcurrentUtilsExample {
public static void main(String[] args) {
CountDownLatch latch = new CountDownLatch(2);
CyclicBarrier barrier = new CyclicBarrier(2);
Thread thread1 = new Thread(() -> {
try {
// 模拟线程执行操作
Thread.sleep(2000);
System.out.println("Thread 1 completed");
latch.countDown();
barrier.await();
} catch (Exception e) {
e.printStackTrace();
}
});
Thread thread2 = new Thread(() -> {
try {
// 模拟线程执行操作
Thread.sleep(3000);
System.out.println("Thread 2 completed");
latch.countDown();
barrier.await();
} catch (Exception e) {
e.printStackTrace();
}
});
thread1.start();
thread2.start();
try {
latch.await();
System.out.println("All threads completed their tasks");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
对比
四种方式实现线程同步的优缺点和适用场景
方法优点缺点适用场景使用synchronized关键字- 简单易用- 内置的锁机制- 自动释放锁- 不可中断- 无法设置超时时间- 只能使用一把锁- 单一资源的同步- 简单的同步需求使用ReentrantLock类- 可重入锁- 可以实现公平性- 可以设置超时时间- 需要手动释放锁- 代码复杂一些- 复杂的同步需求- 需要更灵活的同步控制使用wait()、notify()和notifyAll()方法- 线程间协作和通信- 可以唤醒等待的线程- 需要手动管理等待和唤醒的顺序- 只能在同步代码块中使用- 多个线程间的合作和协调- 生产者消费者模式使用CountDownLatch和CyclicBarrier- 线程间同步和等待- 灵活的等待方式- CountDownLatch只能使用一次- CyclicBarrier需要重置后才能再次使用- 一组线程间的同步和等待- 多个线程间达到某个状态后再继续执行