转载

【Java基础】synchronized与wait,notify& ReentrantLock与await,signal

背景

最近在重构证书管理的需求。之前的代码中,更新证书与吊销证书异步进行,没有进行同步控制。但是会导致主要的两个问题:

1. 更新证书线程和吊销证书线程会对相同的文件进行操作。

2. 不对这两个线程进行同步,日志内容交叉,不好查看。

所以我使用ReentrantLock结合Condition来保证更新证书线程与吊销证书线程交替执行。

synchronized & Object.wait() Object.notify()

ReentrantLock是synchronized的高级版本,而Object.wait()和Object.signal()又与condition.wait()和condition.signal()对应,所以我们先看一看synchronized,再进一步对比ReentrantLock。

synchronized

synchronized锁定的是一块内存区域,而这把锁是一个对象。

synchronized有四种用法:

  1. 修饰代码块(参数为this)
  2. 修饰方法
  3. 修饰代码块(参数为类对象)
  4. 修饰静态方法

前面两种用法使用的锁是对象锁,而后两种锁使用的类锁。

即在需要执行被synchronize修饰的代码块(参数为this)和成员方法时,需要先获取当前的对象锁。如果两个线程同时都调用了同一个对象的一个被synchronize修饰的方法或方法里有synchronize修饰的代码时,这两个线程无法同时执行。

而在需要执行被synchronize修饰的代码块(参数为类对象)或者静态方法时,需要先获取当前的类对象。如果两个线程同时调用了同一个synchronize修饰的代码块(参数为类对象)或者静态方法时,这两个线程将无法同时执行。

Object.wait() Object.signal()

执行Object.wait()会使当前线程进入阻塞状态,并且释放占用的锁。

执行Object.signal()会唤醒一个阻塞线程,使其进入就绪状态。此时被唤醒的线程会试图去获取锁。

实现线程交替执行的Java代码

public class Main {
   public static void main(String[] args) {
       Main main = new Main();
       new Thread(() -> {
           for (int i = 0; i < 3; i++) {
               main.method();
           }
       }, "A").start();

       new Thread(() -> {
           for (int i = 0; i < 3; i++) {
               main.method();
           }
       }, "B").start();
   }

   private synchronized void method() {
       notify();
       System.out.println(Thread.currentThread().getName());
       try {
           wait();
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
   }
}

输出如下

A
B
A
B
A
B

ReentrantLock & condition.wait() condition.signal()

ReentrantLock

ReentrantLock提供了比synchronized更高级的功能,下面我们对比一下 使用上 这两种锁的异同点。

相同的部分:

  1. 无论是ReentrantLock还是synchronized都是可以重入的。可重入的是指可以申请同一个锁,可以处理这样一种情况:当线程需要递归执行时,重新进入方法还可以申请锁。

    你可能会觉得递归进入同一个方法时,已经拥有了锁,为什么还要申请锁?
     但是方法执行完之后会释放锁,那么如果我们不再重新申请锁,在内层递归执行完之后,
    锁就释放了。回到外层方法时,此时没有锁保护该方法被排他性的访问。
  2. 都保证了可见性和互斥性。

    其中可见性是指线程可以看到另一个线程的修改。那么我们会有一个疑问,保证了哪一部分变量的可见性呢?保证的是被两个线程使用到的共享变量,比如说两个线程都要使用到同一个对象的成员变量,那么一个线程的成员变量就会被两一个对象察觉。

    为什么会存在可见性的问题?
    原因是因为现在的CPU通常是多核,我们可以理解一个核会有一个线程跑在上面,而每一个核又有自己的缓存。这些缓存是 
    主内存的拷贝,线程在执行时,如果没有同步会直接修改自己缓存中的东西而不会修改主内存。

    而互斥性则是指同一个内存区需被使用同一把锁的线程互斥地访问。

不同点:

  1. 首先synchronized是JVM级别的实现,而ReentrantLock是API级别的;
  2. ReentrantLock可以用于实现公平锁。
    只需在创建ReentrantLock时传入参数true即可,如ReentrantLock lock = new ReentrantLock(true);
  3. ReentrantLock可以与多个Condition绑定。
    使用多个Condition可以唤醒特定的线程,比如有两个线程,我们分别定义两个Condition——firstCondition,secondCondition,此时其中一个线程我们调用firstCondition.await()。那么在我们要唤醒该线程时,我们可以调用firstCondition.signal()。

condition.await() condition.signal()

Condition提供了Object的await()和notify()的功能。如下是Java doc中关于Condition的说明。我们可以得到这样的信息:Condition是为了使同一个对象产生多阻塞集的效果。使用synchronized方式同步,我们无法再锁定同一个区域的情况下,调用Object.wait来唤醒指定的线程。

Condition factors out the Object monitor methods (wait, notify and notifyAll) into distinct objects to give the effect of having multiple wait-sets per object, by combining them with the use of arbitrary Lock implementations. Where a Lock replaces the use of synchronized methods and statements, a Condition replaces the use of the Object monitor methods.
原文  https://segmentfault.com/a/1190000021394353
正文到此结束
Loading...