synchronized关键字
- 适用场景:
- 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁。例如在单例模式双重检查锁机制中,对创建实例的方法使用
synchronized
修饰,确保多线程下实例只被创建一次。
- 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁。适用于对整个类的静态资源进行同步访问,如操作类的静态计数器。
- 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁。可以更细粒度地控制锁的范围,比如只对部分共享资源操作时,可将这部分代码放在
synchronized
代码块中,并指定锁对象。
- 性能特点:
- 在早期JDK版本中,性能较差,因为是重量级锁,涉及操作系统内核态与用户态的切换。
- 从JDK 6开始,对
synchronized
进行了优化,引入偏向锁、轻量级锁,性能有了很大提升。在竞争不激烈时,偏向锁能减少锁获取的开销;竞争稍激烈时,轻量级锁通过自旋避免线程进入内核态,提高性能。
- 避免死锁:
- 按照固定顺序获取锁。比如多个线程都需要获取锁A和锁B,那么都先获取锁A再获取锁B,避免一个线程先获取A再获取B,另一个线程先获取B再获取A这种交叉获取锁的情况。
- 尽量减少同步代码块的嵌套,嵌套层次越多,死锁风险越高。
ReentrantLock
- 适用场景:
- 当需要更灵活的锁控制时,如可中断的锁获取、超时获取锁等。例如在一个线程池中任务执行时间较长,在获取锁时可以设置超时时间,如果超时未获取到锁,线程可以做其他处理,避免无限等待。
- 当需要公平锁时,
ReentrantLock
默认是非公平锁,但可以通过构造函数设置为公平锁。公平锁按照线程请求锁的顺序分配锁,适用于对线程公平性要求较高的场景,如一些资源分配场景,每个线程都希望能公平地获取资源。
- 性能特点:
- 在高竞争场景下,
ReentrantLock
性能比synchronized
更好,因为它的非公平锁在竞争时能减少线程切换的开销。
- 提供了更多的高级功能,如锁中断、锁超时等,这些功能在某些场景下能提高程序的健壮性,但也会带来一定的性能开销。
- 避免死锁:
- 与
synchronized
类似,按照固定顺序获取锁。
- 使用
tryLock
方法尝试获取锁,并设置超时时间,避免线程无限等待锁。例如:
ReentrantLock lock1 = new ReentrantLock();
ReentrantLock lock2 = new ReentrantLock();
try {
if (lock1.tryLock(100, TimeUnit.MILLISECONDS) && lock2.tryLock(100, TimeUnit.MILLISECONDS)) {
// 执行同步代码
}
} catch (InterruptedException e) {
// 处理中断异常
} finally {
if (lock1.isHeldByCurrentThread()) {
lock1.unlock();
}
if (lock2.isHeldByCurrentThread()) {
lock2.unlock();
}
}
总结
- 在竞争不激烈的情况下,优先使用
synchronized
关键字,因为从JDK 6起其性能有了较大提升,且使用简单,代码可读性好。
- 在竞争激烈,且需要灵活控制锁,如可中断锁获取、公平锁等场景下,使用
ReentrantLock
。
- 无论是使用
synchronized
还是ReentrantLock
,都要注意按照固定顺序获取锁,尽量减少同步代码块嵌套,合理使用锁的特性来避免死锁问题,从而优化高并发场景下共享资源访问的性能。