章
目
录
当多个线程同时访问和修改共享资源时,就可能出现数据不一致等问题。为了解决这些问题,我们需要用到Java线程同步机制。接下来,就给大家详细介绍Java中几种常见的线程同步方式。
一、synchronized关键字
synchronized
是Java自带的一种锁机制,主要用来实现线程同步。它的使用方式比较灵活,可以修饰方法,也可以修饰代码块。
(一)修饰实例方法
当synchronized
修饰实例方法时,它会同步当前对象(也就是this
)的锁。只有获取到这个对象锁的线程,才能执行该方法。例如:
public synchronized void method() {
// 线程安全代码
}
这就好比一个房间(对象)只有一把钥匙(对象锁),拿到钥匙的线程才能进入房间执行任务。
(二)修饰静态方法
如果synchronized
修饰的是静态方法,那么它同步的是类的Class对象锁。因为静态方法属于类,不属于某个具体的实例,所以用类的Class对象作为锁。示例如下:
public static synchronized void staticMethod() {
// 线程安全代码
}
这时候,就好像整个类是一个大房间,所有静态方法共用一把钥匙(类的Class对象锁) 。
(三)修饰代码块
synchronized
修饰代码块时,可以指定要锁定的对象。这种方式更灵活,能精确控制同步的范围。代码如下:
public void method() {
synchronized (this) {
// 线程安全代码
}
}
这里,我们以this
作为锁定对象,只有拿到this
对象锁的线程,才能执行花括号里的代码。
二、ReentrantLock(可重入锁)
ReentrantLock
位于java.util.concurrent.locks
包中,它比synchronized
提供了更丰富的功能。使用示例如下:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Example {
// 创建一个可重入锁实例
private final Lock lock = new ReentrantLock();
public void method() {
// 获取锁,后续代码进入同步区域
lock.lock();
try {
// 线程安全代码
} finally {
// 确保无论是否发生异常,都能释放锁
lock.unlock();
}
}
}
它有几个比较突出的特点:
- 支持公平锁和非公平锁:公平锁会按照线程请求的顺序来分配锁,非公平锁则允许线程在锁可用时直接竞争,不按照顺序。
- 支持尝试获取锁(
tryLock()
):调用这个方法后,线程会尝试获取锁,如果获取成功就返回true
,否则返回false
,不会一直等待。 - 支持中断获取锁(
lockInterruptibly()
):在获取锁的过程中,如果线程被中断,它会抛出异常并停止等待。 - 支持条件变量(
Condition
):通过条件变量,可以实现更精细的线程间协作。
三、volatile关键字
volatile
关键字主要用于修饰变量,它能保证变量的可见性。也就是说,当一个线程修改了被volatile
修饰的变量的值,其他线程能马上看到这个变化。不过,它不能保证对变量的操作是原子性的。比如下面这个例子:
private volatile boolean flag = true;
在实际应用中,volatile
比较适合单个变量的读写操作场景。但如果涉及到复合操作,比如先读取变量的值,再根据这个值进行其他操作,volatile
就不太适用了。
四、Atomic类
java.util.concurrent.atomic
包下有一系列原子操作类,像AtomicInteger
、AtomicLong
、AtomicBoolean
等。这些类利用CAS(Compare-And-Swap,比较并交换)算法,实现了高效的线程安全操作。例如:
import java.util.concurrent.atomic.AtomicInteger;
public class Example {
// 创建一个初始值为0的AtomicInteger对象
private AtomicInteger counter = new AtomicInteger(0);
public void increment() {
// 原子操作,对counter的值进行自增
counter.incrementAndGet();
}
}
这类原子操作类的优点是高效且无锁,很适合处理简单的数值操作场景。
五、ThreadLocal
ThreadLocal
的作用是为每个线程提供独立的变量副本,这样不同线程之间对这个变量的操作就不会相互干扰,从而避免了线程间的竞争。示例代码如下:
public class Example {
// 创建一个ThreadLocal对象,初始值为0
private ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
public void set(int value) {
// 设置当前线程的变量副本的值
threadLocal.set(value);
}
public int get() {
// 获取当前线程的变量副本的值
return threadLocal.get();
}
}
在实际开发中,如果某个变量只需要在线程内部使用,不涉及线程间共享,使用ThreadLocal
就非常合适。
六、Semaphore(信号量)
Semaphore
用于控制同时访问某个资源的线程数量。假设我们有一个资源,只允许一定数量的线程同时访问,就可以使用Semaphore
来实现。代码示例如下:
import java.util.concurrent.Semaphore;
public class Example {
// 创建一个信号量,允许最多3个线程同时访问资源
private Semaphore semaphore = new Semaphore(3);
public void method() throws InterruptedException {
// 获取许可,如果当前没有可用许可,线程会阻塞等待
semaphore.acquire();
try {
// 线程安全代码
} finally {
// 释放许可,让其他等待的线程有机会获取
semaphore.release();
}
}
}
这里创建的Semaphore
对象允许最多3个线程同时访问资源,当有更多线程尝试获取许可时,它们就需要等待,直到有线程释放许可。
七、CountDownLatch(倒计时锁存器)
CountDownLatch
主要用于让一个线程等待一组线程完成任务后,再继续执行。比如,有一个主线程需要等待多个子线程都完成各自的任务后,才能进行下一步操作,就可以用CountDownLatch
。示例代码如下:
import java.util.concurrent.CountDownLatch;
public class Example {
// 创建一个倒计时锁存器,初始计数值为3
private CountDownLatch latch = new CountDownLatch(3);
public void task() {
new Thread(() -> {
System.out.println("Task finished");
// 每次调用countDown(),计数值减一
latch.countDown();
}).start();
}
public void await() throws InterruptedException {
// 主线程在这里等待,直到计数值变为0
latch.await();
System.out.println("All tasks completed");
}
}
在这个例子中,latch
的初始值为3,每一个子线程完成任务后调用countDown()
,当计数值变为0时,等待的线程(比如主线程)就会继续执行。
八、CyclicBarrier(循环屏障)
CyclicBarrier
的作用是让一组线程互相等待,直到所有线程都到达某个屏障点后,再一起继续执行。示例如下:
import java.util.concurrent.CyclicBarrier;
public class Example {
// 创建一个循环屏障,需要3个线程到达屏障点,并设置一个屏障动作
private CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("All threads have reached the barrier");
});
public void task() {
new Thread(() -> {
try {
System.out.println("Thread waiting at barrier");
// 线程在这里等待,直到所有线程都调用了await()
barrier.await();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
这里创建的CyclicBarrier
要求3个线程都调用await()
方法后,所有线程才会继续执行后续代码,并且在所有线程都到达屏障点时,会执行我们设置的屏障动作。
九、BlockingQueue(阻塞队列)
BlockingQueue
是一种线程安全的队列,它支持阻塞操作。在多线程环境下,生产者-消费者模型经常会用到它。例如:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class Example {
// 创建一个容量为10的阻塞队列
private BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
public void producer() {
new Thread(() -> {
try {
// 向队列中添加元素,如果队列已满,线程会阻塞
queue.put(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
public void consumer() {
new Thread(() -> {
try {
// 从队列中取出元素,如果队列为空,线程会阻塞
Integer value = queue.take();
System.out.println("Consumed: " + value);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
在这个例子中,生产者线程往队列里添加元素,消费者线程从队列里取出元素。如果队列满了,生产者线程会被阻塞;如果队列空了,消费者线程会被阻塞。
十、总结
不同的线程同步方式适用于不同的场景:
- 简单同步场景:如果只是需要实现基本的同步功能,使用
synchronized
关键字就足够了。 - 需要高级功能时:当需要更灵活的锁机制,比如支持公平锁、尝试获取锁、中断获取锁等功能,
ReentrantLock
和Condition
会是更好的选择。 - 高效原子操作场景:对于简单的数值操作,
Atomic
类能提供高效且线程安全的解决方案。 - 资源限制场景:如果要控制同时访问资源的线程数量,
Semaphore
是不错的选择。 - 线程协作场景:涉及到线程之间的协作,比如等待一组线程完成任务,或者让一组线程互相等待,
CountDownLatch
和CyclicBarrier
就能派上用场。 - 生产者-消费者场景:在生产者-消费者模型中,
BlockingQueue
可以很好地协调生产者和消费者之间的操作。
希望通过这篇文章,大家对Java中的线程同步方式有更深入的理解,在实际开发中能够根据不同的场景选择合适的同步方式,你学废了吗