Skip to content

线程同步

线程同步 是指多个线程访问同一个 共享数据 时,不会因为同时操作而导致数据的不一致或者程序出错。

就如同 5 个人同时上厕所,必须要等到前一个上完之后,后一个人才可以上。

synchronized

synchronized 关键字可以确保同一时刻只能有一个线程访问它修饰的代码块方法

同步代码块

java
synchronized (object) {
  //需要同步的代码
}

注意

  1. object 也叫 同步监视器/锁,它必须是唯一的类;
  2. 继承自 Thread 的线程,可以使用 类.class 作为 同步监视器(锁);
  3. 实现了 Runnable 接口的线程,可以使用 类.classthis 作为 同步监视器(锁);
java
class MyThread extends Thread {
  @Override
  public void run() {
    synchronized (MyThread.class) {
      //同步代码
    }
  }
}
java
class MyThread implements Runnable {
  @Override
  public void run() {
    synchronized (this) { //使用 MyThread.class/this 作为同步监视器
      //同步代码
    }
  }
}

同步方法

java
public synchronized void method() {
  //需要同步的代码
}

注意

  1. 同步方法默认的同步监视器是 this,当线程继承自 Thread 时需要注意:

    • 由于继承自 Thread 的类,可能会有多个实例,因此同步方法可能会无法解决线程安全;
    • 当然如果类内允许,把同步方法设置为 static 可以解决线程安全;
  2. 实现了 Runnable 接口的线程,可以直接使用 同步方法 解决线程安全,因为它的同步监视器 this 就是唯一的;

java
class MyThread extends Thread {
  @Override
  public void run() {
    show();
  }

 	//不加static,默认的同步监视器是this,而这里的this指向的是创建的类的实例,它可能不唯一
  public static synchronized void show() {
  }
}
java
class MyThread implements Runnable {
  @Override
  public void run() {
    show();
  }

  //synchronized修饰该方法,默认的同步监视器就是this,它是唯一的
  public synchronized void show() {
  }
}

synchronized的优缺点

优点:使用了同步监视器,确保了线程的安全性;

缺点:synchronized 在操作数据时,实际上是串行执行的,一个线程修改其他线程只能等待,效率比较低;

死锁

一个线程可以获取一个锁之后,再继续获取另一个锁。例如:

java
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 进程。

诱发死锁的原因及解决方案:

死锁原因解决方案
互斥条件互斥条件基本无法解决,不互斥也就不存在死锁问题了
占用且等待可以考虑让线程一次性占有所有资源,这样就不存在等待的问题
不可抢夺占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放已经占用的资源
循环等待可以将资源改为线性顺序,所有的线程按照一定顺序有序的使用资源

例如上面的案例,就可以按照一定的顺序使用锁来解决:

java
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 代码:

java
public class Counter {
  private int count = 0;

  public void add() {
    synchronized(this) {
      count += 1;
    }
  }
}

如果使用 ReentrantLock 替代,可以改造为:

java
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 还可以尝试获取锁:

java
//尝试获取锁,最多等待 1 秒,1秒后没有获取到锁,tryLock()返回false
if (lock.tryLock(1, TimeUnit.SECONDS)) {
  count += 1;
}

相比而言,ReentrantLock 比 synchronized 更加安全,tryLock() 尝试获取锁失败时,也不会导致死锁。

Released under the MIT License.