线程同步
线程同步 是指多个线程访问同一个 共享数据 时,不会因为同时操作而导致数据的不一致或者程序出错。
就如同 5 个人同时上厕所,必须要等到前一个上完之后,后一个人才可以上。
synchronized
synchronized
关键字可以确保同一时刻只能有一个线程访问它修饰的代码块或方法。
同步代码块
synchronized (object) {
//需要同步的代码
}
注意
- object 也叫 同步监视器/锁,它必须是唯一的类;
- 继承自
Thread
的线程,可以使用类.class
作为 同步监视器(锁); - 实现了
Runnable
接口的线程,可以使用类.class
或this
作为 同步监视器(锁);
class MyThread extends Thread {
@Override
public void run() {
synchronized (MyThread.class) {
//同步代码
}
}
}
class MyThread implements Runnable {
@Override
public void run() {
synchronized (this) { //使用 MyThread.class/this 作为同步监视器
//同步代码
}
}
}
同步方法
public synchronized void method() {
//需要同步的代码
}
注意
同步方法默认的同步监视器是
this
,当线程继承自 Thread 时需要注意:- 由于继承自 Thread 的类,可能会有多个实例,因此同步方法可能会无法解决线程安全;
- 当然如果类内允许,把同步方法设置为
static
可以解决线程安全;
实现了
Runnable
接口的线程,可以直接使用 同步方法 解决线程安全,因为它的同步监视器this
就是唯一的;
class MyThread extends Thread {
@Override
public void run() {
show();
}
//不加static,默认的同步监视器是this,而这里的this指向的是创建的类的实例,它可能不唯一
public static synchronized void show() {
}
}
class MyThread implements Runnable {
@Override
public void run() {
show();
}
//synchronized修饰该方法,默认的同步监视器就是this,它是唯一的
public synchronized void show() {
}
}
synchronized的优缺点
优点:使用了同步监视器,确保了线程的安全性;
缺点:synchronized
在操作数据时,实际上是串行执行的,一个线程修改其他线程只能等待,效率比较低;
死锁
一个线程可以获取一个锁之后,再继续获取另一个锁。例如:
public void add(int m) {
synchronized(lockA) { //获得lockA的锁
this.value += m;
synchronized(lockB) { //获得lockB的锁
this.another += m;
}
}
}
public void dec(int m) {
synchronized(lockB) { //获得lockB的锁
this.another -= m;
synchronized(lockA) { //获得lockA的锁
this.value -= m;
}
}
}
此时,两个线程同时执行:
- add() 方法获取到 lockA 锁,准备获取 lockB 锁;
- dec() 方法获取到 lockB 锁,准备获取 lockA 锁;
两个线程各自持有不同的锁,然后各自试图获取对方手里的锁,造成了双方无限等待下去,这就是死锁。
注意:一旦出现了死锁,程序不会发生异常,也不会报错,只是线程处于阻塞状态无法继续执行,没有任何机制能解除死锁,除非强制结束 JVM 进程。
诱发死锁的原因及解决方案:
死锁原因 | 解决方案 |
---|---|
互斥条件 | 互斥条件基本无法解决,不互斥也就不存在死锁问题了 |
占用且等待 | 可以考虑让线程一次性占有所有资源,这样就不存在等待的问题 |
不可抢夺 | 占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放已经占用的资源 |
循环等待 | 可以将资源改为线性顺序,所有的线程按照一定顺序有序的使用资源 |
例如上面的案例,就可以按照一定的顺序使用锁来解决:
public void dec(int m) {
synchronized(lockA) { //先获得lockA的锁
this.another -= m;
synchronized(lockB) { //先获得lockB的锁
this.value -= m;
}
}
}
ReentrantLock锁
在前面我们使用了 synchronized
关键字用于加锁,这种锁一是很重,而是获取时必须一直等待,效率低并且没有额外的尝试机制。
java.util.concurrent.locks 包提供的 ReentrantLock
用于替代 synchronized
加锁。
我们来看一下传统的 synchronized 代码:
public class Counter {
private int count = 0;
public void add() {
synchronized(this) {
count += 1;
}
}
}
如果使用 ReentrantLock 替代,可以改造为:
public class Counter {
private static final Lock lock = new ReentrantLock(); //创建锁
private int count = 0;
public void add() {
lock.lock(); //开启锁
try {
count += 1;
} finally {
lock.unlock(); //finally中确保锁始终会被释放
}
}
}
另外,和 synchronized 不同的是,ReentrantLock 还可以尝试获取锁:
//尝试获取锁,最多等待 1 秒,1秒后没有获取到锁,tryLock()返回false
if (lock.tryLock(1, TimeUnit.SECONDS)) {
count += 1;
}
相比而言,ReentrantLock 比 synchronized 更加安全,tryLock()
尝试获取锁失败时,也不会导致死锁。