多线程之:Synchronized与ReentrantLock

什么是线程安全

  1. 保证多线程环境下共享的、可修改的状态的正确性。(这里的状态在程序中可以看作为数据)
  2. 反着来说则是如果状态非共享、不可修改,也就不存在线程安全的问题

    保证线程安全的两种方法

  3. 封装,通过封装将对象内部状态隐藏、保护起来
  4. 不可变,将状态改为不可变,例如将状态定义为final

线程安全要保证的基本特性

  1. 原子性
    相关操作不会在中途被其他线程所干扰,一般通过同步机制实现
  2. 可见性
    一个行程修改了某个共享变量,其新状态能够立即被其他线程知晓,通常被解释为将线程本地状态反映到主内存上,volatile就是负责保证可见性的
  3. 有序性
    保证线程内串行语义,避免指令重排

Synchronized与ReentrantLock

synchronized

synchronized可以很方便的解决多线程间资源共享同步的问题,也就是我们平常所说的线程安全问题。

它可以修饰方法和代码块,无法是用作何种修饰,synchronized获取的锁都是对象。

关于synchronized的使用这里就不说了。

ReentrantLock

ReentrantLock一般称为再入锁,是Lock的实现类,是一个互斥的同步器。

再入锁通过代码直接调用 lock() 方法获取,代码书写也更加灵活。同时ReentrantLock 提供了很多实用的方法,能够实现很多synchronized 无法做到的细节控制,比如可以控制 fairness,也就是公平性,或者利用条件定义等。

但是,编码中也需要注意,必须要明确调用 unlock() 方法释放,不然就会一直持有该锁。

条件变量(Condition)

ReentrantLock配合条件变量(java.util.concurrent.locks.Condition),可以将复杂而晦涩的同步操作转变为直观可控的对象行为。

条件变量最为典型的应用场景就是标准类库中的 ArrayBlockingQueue等,看下源码:

通过再入锁获取条件变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/** Condition for waiting takes */
private final Condition notEmpty;

/** Condition for waiting puts */
private final Condition notFull;

public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}

两个条件变量是从同一再入锁创建出来,然后使用在特定操作中,如下面的 take 方法,判断和等待条件满足:

1
2
3
4
5
6
7
8
9
10
11
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}

当队列为空时,试图take获取元素的线程会等待其他元素入队操作的发生,而不是直接返回,这是 BlockingQueue 的语义,使用条件 notEmpty 就可以优雅地实现这一逻辑。

那么,怎么保证入队触发后续 take 操作呢?请看 enqueue 实现:

1
2
3
4
5
6
7
private void enqueue(E e) {
final Object[] items = this.items;
items[putIndex] = e;
if (++putIndex == items.length) putIndex = 0;
count++;
notEmpty.signal(); // 通知等待的线程,非空条件已经满足
}

通过 signal/await 的组合,完成了条件判断和通知等待线程,非常顺畅就完成了状态流转。注意,signal 和 await 成对调用非常重要,不然假设只有 await 动作,线程会一直等待直到被打断(interrupt)

性能比较

synchronizedReentrantLock 的性能不能一概而论,早期版本 synchronized 在很多场景下性能相差较大,在后续版本进行了较多改进。

在低竞争场景中synchronized表现可能优于 ReentrantLock

而在多线程高竞争条件下,ReentrantLocksynchronized有更加优异的性能表现。

高竞争

如果大部分情况,每个线程都不需要真的获取锁,就是低竞争;反之,大部分都要获取锁才能正常工作,就是高竞争

用法比较
  1. Lock使用起来比较灵活,但是必须有释放锁的配合动作
  2. Lock必须手动获取与释放锁,而synchronized不需要手动释放和开启锁
  3. Lock只适用于代码块锁,而synchronized可用于修饰方法、代码块等
特性比较

ReentrantLock的优势体现在:

  1. 具备尝试非阻塞地获取锁的特性:当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁
  2. 能被中断地获取锁的特性:与synchronized不同,获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放
  3. 超时获取锁的特性:在指定的时间范围内获取锁;如果截止时间到了仍然无法获取锁,则返回
  4. 可以控制线程的竞争公平性
注意事项

在使用ReentrantLock类的时,一定要注意三点:

  1. 在finally中释放锁,目的是保证在获取锁之后,最终能够被释放
  2. 不要将获取锁的过程写在try块内,因为如果在获取锁时发生了异常,异常抛出的同时,也会导致锁无故被释放。
  3. ReentrantLock提供了一个newCondition的方法,以便用户在同一锁的情况下可以根据不同的情况执行等待或唤醒的动作。

原创文章,转载请出处注明。

下面是我的个人公众号,欢迎关注交流

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×