Java线程同步机制
线程同步机制是一套用于协调线程间的数据访问及活动的机制,该机制用于保障线程安全以及实现这些线程的共同目标。Java平台提供的线程同步机制包括锁、volatile关键字、final关键字、static关键字以及一些相关的API,如Object.wait()/Object.notify()等。
1. 锁
锁可以理解为对共享数据进行保护的许可证。对于同一个许可证所保护的共享数据而言,任何线程访问这些共享数据之前必须持先持有该许可证,一个许可证一次只能够被一个线程所持有(锁的排他性,这种锁也被称为排他锁或互斥锁);许可证的持有线程必须在其接束对这些共享数据的访问后让出即释放这个许可证,以便其他线程能够对这些共享数据进行访问。
作用:锁能够保护共享数据以实现线程安全,其作用包括保障原子性、保障可见性和保障有序性。
1.1 内部锁
Java平台中的任何一个对象都有唯一一个与之相关联的锁。这种锁被称为监视器或者内部锁。内部锁是一种排他锁,能够保障原子性、可见性和有序性。
内部锁是通过synchronized关键字实现的。synchronized关键字可以用来修饰方法以及代码块(包括{}包裹的代码)。
1.2 显式锁
显式锁是java.util.concurrent.locks.Lock 接口的实例。该接口对显式锁进行了抽象,java.util.concurrent.locks.ReentrantLock是Lock接口的默认实现类。
一个Lock接口实例就是一个显式锁对象,Lock接口定义的lock方法和unlock方法分别用于申请和释放相应lock实例表示的锁。使用方法如下:
- 创建Lock接口的实例。如果没有特别要求可以创建Lock的默认实现类的实例。
- 在访问共享数据currentValue 前申请相应的显式锁,直接调用Lock.lock()。
- 在临界区中访问共享数据。
- 共享数据访问结束后释放锁。
import java.util.concurrent.locks.*;
public class SequenceGenerator {
private final Lock lock = new ReentrantLock();
private int currentValue = 0;
private final int maxValue;
public SequenceGenerator(int maxValue) {
this.maxValue = maxValue;
}
public int getNext() {
lock.lock(); // 上锁
try {
if (currentValue == maxValue) {
currentValue = 0;
}
return currentValue++;
} finally {
lock.unlock(); // 释放锁
}
}
}
1.3 比较
内部锁是基于代码块的锁,因此其使用基本无灵活性可言:要么使用它,要么不使用。不会导致锁泄露。
显式锁是基于对象的锁,其使用可以充分发挥面向对象编程的灵活性。
比如:内部锁的申请与释放只能是在一个方法内进行的,而显式锁支持在一个方法内申请锁,另一个方法内释放锁。容易被错用导致锁泄露。
2. 轻量级同步机制: volatile关键字
volatile关键字表示被修饰的变量的值容易变化(即被其他线程更改),因而不稳定。volatile变量的不稳定性意味着对这种变量的读和写操作必须从高缓存或者主内存(也是通过高速缓存读取)中读取,以读取变量的相对新值。
对于 volatile 变量,在保证可见性的同时,也可以提供一定的有序性保障。volatile 变量的写操作会在写操作之前插入一个“写屏障”,而读操作会在读操作之后插入一个“读屏障”。这个“写屏障”会使得所有之前的写操作都完成后再执行当前写操作,而“读屏障”则会让当前读操作在执行之前读取之前的所有写操作的结果,从而保证了有序性。
但是需要注意的是,volatile 变量只能保证单个变量的有序性,而无法保证多个变量之间的有序性。如果需要保证多个变量之间的操作顺序,需要考虑使用锁或者其他同步工具。
下面是一个示例程序,演示了 volatile 变量的有序性保障:
public class VolatileOrderDemo {
private static volatile int value1 = 0;
private static volatile int value2 = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
value1 = 1;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
value2 = 2;
});
Thread t2 = new Thread(() -> {
while (value2 == 0) { }
System.out.println(value1);
});
t1.start();
t2.start();
t1.join();
t2.join();
}
}
在上面的示例中,定义了两个 volatile 变量 value1 和 value2,并启动了两个线程 t1 和 t2。其中,t1 先将 value1 的值修改为 1,然后睡眠 1s 后将 value2 的值修改为 2;而 t2 则持续等待 value2 的值变为非 0,并输出 value1 的值。由于 value1 和 value2 都是 volatile 变量,因此它们的写入操作会受到一定的有序性保障。因此,在 value1 的写操作完成后,value2 的写操作才会开始执行,从而保证了 t2 输出的是 1 而不是 0。