当某个线程请求一个由其他线程所持有的锁时,发出请求的线程会被阻塞。然而,由于内置锁是可重入的,所以当某个线程试图获取一个已经由它自己所持有的锁时,这个请求就会成功。
重入实现的一个方法是:为每个锁关联一个获取计数器和一个所有者线程。
当计数器值为0时,这个锁就被认为是没有被任何线程持有的。当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将计数器加1。如果同一个线程再次获取这个锁,计数器将加1,而当线程退出同步代码块时,计数器会相应地减1。当计数器为0时,这个锁将被释放。
下面这段代码,如果内置锁是不可重入的,那么这段代码将发生死锁。
public class Widget{ public synchronized void doSomething(){ .... } } public class LoggingWidget extends Widget{ public synchronized void doSomething(){ System.out.println(toString() + ": call doSomething"); super.doSomething(); } } 使用synchronized解决count+=1问题前面我们介绍原子性问题时提到count+=1存在原子性问题,那么现在我们使用synchronized来使count+=1成为一个原子操作。
代码如下所示。
class SafeCalc { long value = 0L; long get() { return value; } synchronized void addOne() { value += 1; } }SafeCalc 这个类有两个方法:一个是 get() 方法,用来获得 value 的值;另一个是 addOne() 方法,用来给 value 加 1,并且 addOne() 方法我们用 synchronized 修饰。下面我们分析看这个代码是否存在并发问题。
addOne() 方法,被 synchronized 修饰后,无论是单核 CPU 还是多核 CPU,只有一个线程能够执行 addOne() 方法,所以一定能保证原子操作。
那么可见性呢?是否可以保证一个线程调用addOne()使value加一的结果对另一个线程后面调用addOne()时可见?
答案是可以的。这就需要回顾到我们上篇博客提到的Happens-Before规则其中关于管程中的锁规则:对同一个锁的解锁 Happens-Before 后续对这个锁的加锁。即,一个线程在临界区修改的共享变量(该操作在解锁之前),对后续进入临界区(该操作在加锁之后)的线程是可见的。
此时还不能掉以轻心,我们分析get()方法。执行 addOne() 方法后,value 的值对 get() 方法是可见的吗?答案是这个可见性没有保证。管程中锁的规则,是只保证后续对这个锁的加锁的可见性,而 get() 方法并没有加锁操作,所以可见性没法保证。所以,最终的解决办法为也是用synchronized修饰get()方法。
class SafeCalc { long value = 0L; synchronized long get() { return value; } synchronized void addOne() { value += 1; } }代码转换成我们的锁模型为:(图来自【参考1】)
get() 方法和 addOne() 方法都需要访问 value 这个受保护的资源,这个资源用 this 这把锁来保护。线程要进入临界区 get() 和 addOne(),必须先获得 this 这把锁,这样 get() 和 addOne() 也是互斥的。
锁和受保护资源的关系受保护资源和锁之间的关联关系非常重要,一个合理的关系为:锁和受保护资源之间的关联关系是 1:N 。
拿球赛门票管理来类比,一个座位(资源)可以用一张门票(锁)来保护,但是不可以有两张门票预定了同一个座位,不然这两个人就会fight。
在现实中我们可以使用多把锁锁同一个资源,如果放在并发领域中,线程A获得锁1和线程B获得锁2都可以访问共享资源,那么达到互斥访问共享资源的目的。所以,在并发编程中使用多把锁锁同一个资源不可行。或许有人会想:要同时获得锁1和锁2才可以访问共享资源,这样应该是就可行的。我觉得是可以的,但是能用一个锁就可以保护资源,为什么还要加一个锁呢?
多把锁锁一个资源不可以,但是我们可以用同一把锁来保护多个资源,这个对应到现实球赛门票就是可以用一张门票预定所有座位,即“包场”。
下面举一个在并发编程中使用多把锁来保护同一个资源将会出现的并发问题:
class SafeCalc { static long value = 0L; synchronized long get() { return value; } synchronized static void addOne() { value += 1; } }把 value 改成静态变量,把 addOne() 方法改成静态方法。
仔细观察,就会发现改动后的代码是用两个锁保护一个资源。get()所使用的锁是this,而addOne()所使用的锁是SafeCalc.class。两把锁保护一个资源的示意图如下(图来自【参考1】)。
由于临界区 get() 和 addOne() 是用两个锁保护的,因此这两个临界区没有互斥关系,临界区 addOne() 对 value 的修改对临界区 get() 也没有可见性保证,这就导致并发问题。