synchronized
- synchronized 是 Java 内建的同步机制,提供了互斥的语义和可见性。
- 当一个线程已经获得某个锁时,试图获取同一个锁的其它线程将只能等待(或阻塞)。
- 用关键字 synchronized 修饰方法等价于将方法体中的内容用 synchronized 语句块包起来。
public class C {
private static int s;
private int a;
public synchronized void setA(int v) {
this.a = v;
}
public void setA2(int v) {
synchronized (this) {
this.a = v;
}
}
public static synchronized void setS(int v) {
s = v;
}
public static void setS(int v) {
synchronized (C.class) {
s = v;
}
}
}
ReentrantLock
- ReentrantLock 直译为“再入锁”,语义和 synchronized 类似。
- “再入”是指对 ReentrantLock 的持有是以线程为单位的,而不是基于调用次数。
- 例:StampedLock 不支持“再入”
- 当一个线程试图再次获取它已经获得的某个 ReentrantLock 时,获取操作自动成功
- 这是它内部的同步器类NonfairSync和FairSync实现的,它们都是AbstractQueuedSynchronizer(传说中的AQS)的子类;
- AQS的父类AbstractOwnableSynchronizer(传说中的AQS)则提供了对互斥的支持。它有一个字段保存了当前拥有它的线程对象的引用。
- 当一个线程试图再次获取它已经获得的某个 ReentrantLock 时,获取操作自动成功
- 与 synchronized 相比,ReentrantLock 更用法更灵活,支持更多自定义操作。
- 可调用 ReentrantLock.lock() 方法获取锁,调用 ReentrantLock.unlock() 方法释放锁。
- 注:如果编码时未处理好释放锁的操作,可能导致程序一直持有某个ReentrantLock,继而引发异常状况。
- 如,未规划好异常处理,导致通过 lock() 方法拿到锁后出现异常并跳过了 unlock() 方法
- 注:如果编码时未处理好释放锁的操作,可能导致程序一直持有某个ReentrantLock,继而引发异常状况。
- 可调用 ReentrantLock.lock() 方法获取锁,调用 ReentrantLock.unlock() 方法释放锁。
ReentrantLock lock;
...
public void func1() {
lock.lock();
doSomethingMayThrowException();
lock.unlock();
}
public void func2() {
lock.lock();
try {
doSomethingMayThrowException();
} finally {
lock.unlock();
}
}
- 可通过 ReentrantLock 的构造方法指定该锁是公平锁还是非公平锁
- 公平锁可以将锁给等待时间最久的线程,减少个别线程长期等待锁的现象
- 只有当确实需要公平时才使用公平锁;因为它会导致吞吐量下降
- 其构造方法内部会根据参数使用不同的同步器类(Sync)
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
- tryLock(long timeout, TimeUnit unit) 方法可设定获取锁的最长等待时间
- lockInterruptibly() 方法可以在当前线程未处于中断状态时才获取锁
- 如果当前线程处于中断状态,此方法会抛出InterruptedException
- 可通过 ReentrantLock.newCondition() 方法创建多个 Condition 对象(java.util.concurrent.locks),并利用这些 Condition 对象组合适应不同的使用场景
- 通过 Condition 的 await() 和 signal() 等方法就可以方便地实现一些同步需求
- ArrayBlockingQueue就是通过Condition实现了Blocking语义
final ReentrantLock lock;
private final Condition notEmpty;
// 构造方法
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
// 创建Condition
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
// 获取元素
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
// 如果队列中没有元素,将一直等待
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
// 添加元素
private void enqueue(E x) {
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
// 发布队列非空通知。该操作可让take()方法结束等待
notEmpty.signal();
}
性能
- 在锁竞争较低的场景中 synchronized 的性能可能更好
- 多线程高竞争场景下,ReentrantLock 的性能更好
实际使用时可先侧重设计简单性与代码整洁性,使用 synchronized。
当确定存在性能问题,且经过测试 ReentrantLock 性能更好时再改。
(《解剖一个有缺陷的微基准测试》)
《性能对比》
Peter Lawrey 写道
Conclusion
In general, unless you have measured you system and you know you have a performance issue, you should do what you believe is simplest and clearest and it is likely to performance well.
These results indicate that synchronized is best for a small number of threads accessing a lock (<4) and Lock may be best for a high number of threads accessing the same locks.
In general, unless you have measured you system and you know you have a performance issue, you should do what you believe is simplest and clearest and it is likely to performance well.
These results indicate that synchronized is best for a small number of threads accessing a lock (<4) and Lock may be best for a high number of threads accessing the same locks.
其它TODO
线程安全性
- 线程安全是指,在多线程环境下,共享的、可修改的数据的正确性。
- 为了达到线程安全,需要保证操作的原子性、可见性、有序性。
- 原子性:执行某个操作时,其内部子操作的执行不会被其它线程干扰
- 可以通过同步机制实现。比如,加锁
- 可见性:某个共享数据被一个线程修改后,其它线程线程能立即知道该数据已被修改
- Java的 volatile 关键字可以保证其修饰的字段的可见性。《volatile》
- 有序性:对共享数据的读写指令是按照代码中的写明的顺序执行的,不会被编译器优化等其它操作重排
- 原子性:执行某个操作时,其内部子操作的执行不会被其它线程干扰