为什么需要Lock,而不是直接用synchronized
构建Lock的理由
为了解决死锁问题所提出的方案是破坏不可抢占条件这一策略;然而该方法在使用synchronized关键字时无法有效解决问题。原因在于当使用synchronized关键字申请资源时会出现以下情况:在这种情况下会导致线程陷入阻塞状态;同时导致已占用的资源(锁)无法被释放从而引发死锁。
但是我们希望的是:
该机制要求线程在使用部分系统资源时,在无法获得剩余所需的系统资源情况下,则可主动放弃已占有的系统资源,并放弃该机制所依赖的核心特性从而避免了死锁的发生。
有以下三种解决方案:
- 能够响应中断 。synchronized 的问题是,持有锁 A 后,如果尝试获取锁 B 失败,那么线程就进入阻塞状态,一旦发生死锁,就没有任何机会来唤醒阻塞的线程,也就无法释放持有的锁A。但如果阻塞状态的线程能够响应中断信号,也就是说当我们给阻塞的线程发送中断信号的时候,能够唤醒它,那它就有机会释放曾经持有的锁 A。这样就破坏了不可抢占条件了。
- 支持超时 。如果线程在一段时间之内没有获取到锁,不是进入阻塞状态,而是返回一个错误,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。
- 非阻塞地获取锁 。如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。
这三种方案有效地弥补了synchronized的缺陷。它们则是构建Lock的核心依据,在Java API中,则是通过 Lock接口提供的三个核心方法来实现这一功能。
// 支持中断的 API
void lockInterruptibly() throws InterruptedException;
// 支持超时的 API
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 支持非阻塞获取锁的 API
boolean tryLock();
Lock如何保证可见性
class X {
/** 锁,应是私有的、不可变的、不可重用的。Integer 和 String 类型的对象不适合做锁。如果锁发生变化,就意味着失去了互斥功能。
Integer 和 String 类型的对象在 JVM 里面是可能被重用的,除此之外,JVM 里可能被重用的对象还有 Boolean,
重用意味着你的锁可能被其他代码使用,如果其他代码synchronized(你的锁),而且不释放,那你的程序就永远拿不到锁,这是隐藏的风险。*/
private final Lock rtl = new ReentrantLock();
int value;
public void addOne() {
// 获取锁
rtl.lock();
try {
value+=1;
} finally {
// 保证锁能释放
rtl.unlock();
}
}
}
实现可见性的原理在于借助volatile关键字所遵循的Happens-Before顺序规则。Java SDK中的ReentrantLock类通过维护一个volatile类型的状态变量来实现功能。在获取锁的过程中会读取和修改state变量;而释放锁时也会进行相应的读写操作。具体来说,在执行value+=1操作之前(即加粗部分),程序首先读取并修改了volatile状态变量;随后在完成该操作后又再次读取和修改了同一个变量以完成解锁过程。
顺序性规则规定,在线程 T₁ 中进行 value += 1 操作后会先执行 unlock 释放锁。
volatile 变量规则指出,在T₁执行 unlock 之前的状态变化(涉及 setState)发生于前于其在 T₂ 执行 lock。
基于传递性规则可知,在这种情况下值的变化会使得 lock 发生在 unlock 后面。
private volatile int state;
/** * Returns the current value of synchronization state.
* This operation has memory semantics of a {@code volatile} read.
* @return current state value
*/
protected final int getState() {
return state;
}
/** * Sets the value of synchronization state.
* This operation has memory semantics of a {@code volatile} write.
* @param newState the new state value
*/
protected final void setState(int newState) {
state = newState;
}
//获取锁
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();//读取state值
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);//更新state值
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;//读取state值
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);//更新state值
return free;
}
什么时候才使用锁
三个用锁的最佳时机:
建议仅在更新对象成员变量的过程中实施加锁机制
以避免在访问可变成员变量时进行不必要的加锁操作
绝不应该在调用其他对象的方法之前或之后进行加锁
Lock与synchronized的区别
Lock是一种类型而synchronized是Java提供的关键字;其中synchronized属于内置语言实现;
当synchronized关键字在发生异常时会自动释放被占用的锁因此能够避免死锁现象的发生;而如果在使用Lock类型时未主动调用unLock()方法则可能出现死锁现象因此建议在finally块中添加相应的释放操作以确保程序稳定运行;
此外L o c具有响应中断的功能即等待被锁住的线程能够在中断事件发生后暂时解除锁定状态;与此相比synchronized机制不具备这种特性导致等待被锁住的线程无法响应中断从而影响系统的响应速度;
另外L o c支持超时获取锁机制即使当前线程被阻塞也能通过超时时间设置后主动释放资源而避免长时间阻塞状态;而synchronized采用永久阻塞等待模式直到资源被释放才能继续执行后续操作;
L o c还支持非阻塞式的锁获取方式即允许多个线程在同一时间点上进行读或写操作而不影响其他线程的执行效率;这使得基于L o c实现的应用程序能够更好地适应多线程并发环境;
从性能角度来看当同步竞争较为宽松的时候synchronized由于是由编译器优化过的关键字通常表现得更为高效;但一旦同步需求变得非常强烈synchronized机制就会因为频繁抛出异常而导致性能急剧下降甚至出现性能瓶颈;相比之下即使是在最繁忙的情况下使用L o cthisReentrantLock也能够维持较好的性能水平从而保证系统的稳定运行;
