1. 关于锁
- 锁的概念。
- 锁用来起保护作用,控制对被保护对象的访问。对于一个线程来说,锁有 2 种状态,一种状态 ‘可进入’或‘可通过’,这种状态下锁对该线程没有副作用,线程继续执行,而另一种状态 ‘不可进入’或‘不可通过’,这种状态下,锁对该线程产生了约束,线程可能会立即返回或者进入等待状态,直到某个条件成立,使得该线程可以通过锁,而等待过程中,该线程也可能被外部中断唤醒。
- 那么怎样理解:一个线程‘进入’或‘通过’锁呢?锁是边界,对于任一线程,它要么‘通过’锁,即通过这个边界,要么被这个边界阻拦,它无法越界!想想现实生活中的锁:一把锁通常会配有多把钥匙,线程拥有解锁的钥匙,便获得了通过边界的许可!
- 锁是什么?下面是一个简单的自定义锁的例子(即上一篇文章:Java中的多线程与锁(一)中利用 synchronized 关键字和 ‘等待/通知’方法实现的自定义锁组件)
/* 简单锁实现 */
class MyLock{
private int count = 0;
/* 获取锁 */
public void lock() {
synchronized(this) {
while(count != 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count++;
}
}
/* 释放锁 */
public void unLock() {
synchronized (this) {
if(count > 0) {
count--;
notify();
}
}
}
}
分析‘获取锁’和‘释放锁’的逻辑(暂时不用理会 wait() 和 notify() 方法),可以看到它们都是在 synchronized 代码块中通过更改(先检查变量的值,后更新变量的值)锁的内部状态来 表达 锁的获取与释放,synchronized 保证了更改操作的排它性,同一时刻只有一个线程能成功更改锁的状态,从而根据锁的状态来决定该线程的行为:它是通过锁还是等待锁,这是一个 ‘先检查后设置’ 的操作。‘更改操作’ 的排它性是实现锁功能的核心 (该操作过程被 synchronized 保证不会受到其它线程的干扰),而 wait() 和 notify() 方法提供的‘等待 / 通知’机制则起到辅助作用,wait() 方法使得未成功获取锁的线程能够让出 CPU,而不是无限的循环尝试获取锁(无疑,这种操作方式非常低效,‘等待/通知’机制便用于高效解决该问题),wait() 方法维护了一个线程等待队列,而 notify() 方法配合 wait() 方法,使得当一个线程释放锁时,等待该锁的线程能够及时被通知/唤醒。
综上述,一个锁的基本组成有:1 排它的检查/更新操作(原子操作,更改锁的状态来表达成功获取锁或释放锁)(必须), 2 一个队列,用于维护等待锁的线程(管理等待锁的线程)(可选,线程不一定要在锁上等待,它可以过一会儿再来尝试获取锁)。
锁的本质在于:通过一个原子操作检查及更改一个对象实例的状态,来表达执行操作的线程是否成功获取锁以及释放锁 (而对于如何处理未成功获取锁的线程,则取决于实际需求,线程可以在锁上等待,即 锁必须维护一个线程等待队列,线程也可以立即返回结果,即 获取锁失败)。其中 ‘原子操作’是保障 ‘是否成功获取锁以及释放锁’ 的前提,而任一能满足该 ‘原子操作’ 的 ‘对象实例’ 都能作为锁来使用。这里的锁更多的是一个形式化的概念,锁是边界,它执行入境检查,决定谁可以通过(进入),这里是单向检查,只管进不管出,当然,你必须先进去,才能够从里面出来。