synchronized和Lock
synchronized
Java提供的关键字,属于Java语法层面的互斥,隐式锁,由JVM来实现加锁和释放锁。
优点
- 代码编写简单
- 可读性好
缺点
- 加锁和释放锁由JVM来完成,不够灵活。
- 获取锁的过程不可中断。
- 不支持超时,获取不到锁会一直阻塞。
- 非公平锁,不允许修改。
- 不支持尝试获取锁的判断。
- 不支持读写锁,比较僵硬。
Lock
Lock属于Java代码级的显式锁,加锁和释放锁由Java代码来完成,和JVM无关。
优点
- 非常灵活,代码级别的加锁和释放。
- 获取锁的过程可以被中断。
- 支持超时,解决无限阻塞。
- 支持公平锁和非公平锁。
- 支持尝试获取锁的判断。
- 支持读写锁。
- 线程的唤醒更灵活。
缺点
- 代码编写较复杂。
使用示例
public class SyncLockDemo {
private int index = 0;
private Lock lock = new ReentrantLock();
synchronized void syncAdd(){
System.out.println(++index);
}
void lockAdd(){
lock.lock();
System.out.println(++index);
lock.unlock();
}
}
公平锁和非公平锁
公平锁
等待时间最长的线程,优先获取到锁。
优点:锁竞争激烈时,不存在“饥饿”问题。
缺点:性能较差。
锁竞争时,优先分配给等待时间最长的线程,如果该线程处于休眠状态,需要将其唤醒,频繁的挂起和唤醒线程很消耗资源。
非公平锁
优点:效率高。
缺点:锁竞争激烈时,存在“饥饿”问题,可能导致有的线程一直得不到执行。
锁竞争时,优先分配给当前活跃的线程,减少了“公平锁”中对线程频繁的挂起和唤醒,性能更高,但是会导致有些线程一直得不到执行。
两者各有优缺点,需要根据业务场景来选择使用哪种锁。
ReentrantLock默认使用的就是非公平锁!
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
可重入锁
可重入:线程获得锁之后,能否再次获得锁?
典型应用场景:同步方法的递归、同步方法中调用另一个同步方法。
synchronized和ReentrantLock都支持锁的可重入。
ReentrantLock在内部维护了一个计数器,lock时计数器会加一,unlock时计数器减一,当unlock到0时,才会真正释放锁,其他线程开始竞争。
读写锁
可以查看笔者的博客:《读写锁以及手写实现》。
Condition接口
LockSupport
在Java中,挂起和唤醒线程可以使用wait()和notify(),这两个方法来自Object类,调用之前必须前获取到锁的监视器对象,否则会报错。
LockSupport也可以支持将线程挂起和唤醒,直接针对线程,使用起来更加的直观。
API
- void park()
阻塞当前线程。 - void parkNanos(long nanos)
阻塞当前线程,支持纳秒级的超时。 - void parkUntil(long deadline)
阻塞当前线程,直到deadline时间点。 - void unpark(Thread)
唤醒指定线程。
使用示例
public class LockSupportDemo {
public static void main(String[] args) {
Thread mainThread = Thread.currentThread();
new Thread(()->{
//2s后唤醒main
SleepUtil.SEC.sleep(2);
LockSupport.unpark(mainThread);
}).start();
//main阻塞
LockSupport.park();
//唤醒后做的事儿
System.out.println("main unpack");
}
}