前言
本文需要具备一定的多线程基础才能更好的理解。
学习java多线程时,最头疼的知识点之一就是java中的锁了,什么互斥锁、排它锁、自旋锁、死锁、活锁等等,细分的话可以罗列出20种左右的锁,光是看着这些名字就足以让人望而却步了,更别说一个个去理解它们的含义了。其实我要在这里告诉大家,我们看到的其实只是假象,其实根本没有这么多锁,或者这样说,这里边有很多锁其实就是一个东西,当我们从不同的侧重点去看的时候,它们就会衍生出不同的名字。本文就是着重将这些锁进行分门别类的总结,另外,本文不着重阐述锁的实现原理,大家有兴趣可以自行去查找,资料有很多,本文着重让大家理解这些锁的概。好了,废话不多说,进入正题。
一、由ReentrantLock和synchronized实现的一系列锁
jdk1.5的java.util.concurrent并发包中的Lock接口和1.5之前的synchronized或许是我们最常用的同步方式,这两种同步方式特别是Lock的ReentrantLock实现,经常拿来进行比较,其实他们有很多相似之处,其实它们在实现同步的思想上大致相同,只不过在一些细节的策略上(诸如抛出异常是否自动释放锁)有所不同。前边说过了,本文着重讲锁的实现思想和不同锁的概念与分类,不对实现原理的细节深究,因此我在下面介绍第一类锁的时候经常讲他们放在一起来说。我们先来说一下Lock接口的实现之一ReentrantLock。当我们想要创建ReentrantLock实例的时候,jdk为我们提供两种重载的构造函数,如下:
1 /** 2 * Creates an instance of {@code ReentrantLock}. 3 * This is equivalent to using {@code ReentrantLock(false)}. 4 */ 5 public ReentrantLock() { 6 sync = new NonfairSync(); 7 } 8 9 /** 10 * Creates an instance of {@code ReentrantLock} with the 11 * given fairness policy. 12 * 13 * @param fair {@code true} if this lock should use a fair ordering policy 14 */ 15 public ReentrantLock(boolean fair) { 16 sync = fair ? new FairSync() : new NonfairSync(); 17 }
fair是什么意思?公平的意思,没错,这就是我们要说的第一种锁。
1.从其它等待中的线程是否按顺序获取锁的角度划分--公平锁与非公平锁
我先做个形象比喻,比如现在有一个餐厅,一次最多只允许一个持有钥匙的人进入用餐,那么其他没拿到钥匙的人就要在门口等着,等里面那个人吃完了,他出来他把钥匙扔地上,后边拿到钥匙的人才能进入餐厅用餐。
公平锁:是指多个线程在等待同一个锁时,必须按照申请锁的先后顺序来一次获得锁。所用公平锁就好像在餐厅的门口安装了一个排队的护栏,谁先来的谁就站的靠前,无法进行插队,当餐厅中的人用餐结束后会把钥匙交给排在最前边的那个人,以此类推。公平锁的好处是,可以保证每个排队的人都有饭吃,先到先吃后到后吃。但是弊端是,要额外安装排队装置。
非公平锁:理解了公平锁,非公平锁就很好理解了,它无非就是不用排队,当餐厅里的人出来后将钥匙往地上一扔,谁抢到算谁的。但是这样就造成了一个问题,那些身强体壮的人可能总是会先抢到钥匙,而那些身体瘦小的人可能一直抢不到,这就有可能将一直抢不到钥匙,最后导致需要很长时间才能拿到钥匙甚至一直拿不到直至饿死。
公平锁与非公平所的总结:
(1) 公平锁的好处是等待锁的线程不会饿死,但是整体效率相对低一些;非公平锁的好处是整体效率相对高一些,但是有些线程可能会饿死或者说很早就在等待锁,但要等很久才会获得锁。其中的原因是公平锁是严格按照请求所的顺序来排队获得锁的,而非公平锁时可以抢占的,即如果在某个时刻有线程需要获取锁,而这个时候刚好锁可用,那么这个线程会直接抢占,而这时阻塞在等待队列的线程则不会被唤醒。
(2) 在java中,公平锁可以通过new ReentrantLock(true)来实现;非公平锁可以通过new ReentrantLock(false)或者默认构造函数new ReentrantLock()实现。
(3)synchronized是非公平锁,并且它无法实现公平锁。
2.从能否有多个线程持有同一把锁的角度划分--互斥锁
互斥锁的概念非常简单,也就是我们常说的同步,即一次最多只能有一个线程持有的锁,当一个线程持有该锁的时候其它线程无法进入上锁的区域。在Java中synchronized就是互斥锁,从宏观概念来讲,互斥锁就是通过悲观锁的理念引出来的,而非互斥锁则是通过乐观锁的概念引申的。
3.从一个线程能否递归获取自己的锁的角度划分--重入锁(递归锁)
我们知道,一条线程若想进入一个被上锁的区域,首先要判断这个区域的锁是否已经被某条线程所持有。如果锁正在被持有那么线程将等待锁的释放,但是这就引发了一个问题,我们来看这样一段简单的代码:
1 public class ReentrantDemo { 2 private Lock mLock; 3 4 public ReentrantDemo(Lock mLock) { 5 this.mLock = mLock; 6 } 7 8 public void outer() { 9 mLock.lock(); 10 inner(); 11 mLock.unlock(); 12 } 13 14 public void inner() { 15 mLock.lock(); 16 // do something 17 mLock.unlock(); 18 } 19 }
ReentrantDemo
当线程A调用outer()方法的时候,会进入使用传进来mlock实例来进行mlock.lock()加锁,此时outer()方法中的这片区域的锁mlock就被线程A持有了,当线程B想要调用outer()方法时会先判断,发现这个mlock这把锁被其它线程持有了,因此进入等待状态。我们现在不考虑线程B,单说线程A,线程A进入outer()方法后,它还要调用inner()方法,并且inner()方法中使用的也是mlock()这把锁,于是接下来有趣的事情就来了。按正常步骤来说,线程A先判断mlock这把锁是否已经被持有了,判断后发现这把锁确实被持有了,但是可笑的是,是A自己持有的。那你说A能否在加了mlock锁的outer()方法中调用加了mlock锁的inner方法呢?答案是如果我们使用的是可重入锁,那么递归调用自己持有的那把锁的时候,是允许进入的。
可重入锁:可以再次进入方法A,就是说在释放锁前此线程可以再次进入方法A(方法A递归)。
不可重入锁(自旋锁):不可以再次进入方法A,也就是说获得锁进入方法A是此线程在释放锁钱唯一的一次进入方法A。
下面这段代码演示了不可重入锁:
1 public class Lock{ 2 private boolean isLocked = false; 3 public synchronized void lock() 4 throws InterruptedException{ 5 while(isLocked){ 6 wait(); 7 } 8 isLocked = true; 9 } 10 11 public synchronized void unlock(){ 12 isLocked = false; 13 notify(); 14 } 15 }
可以看到,当isLocked被设置为true后,在线程调用unlock()解锁之前不管线程是否已经获得锁,都只能wait()。
4.从编译器优化的角度划分--锁消除和锁粗化
锁消除和锁粗化,是编译器在编译代码阶段,对一些没有必要的、不会引起安全问题的同步代码取消同步(锁消除)或者对那些多次执行同步的代码且它们可以可并到一次同步的代码(锁粗化)进行的优化手段,从而提高程序的执行效率。
锁消除
对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判断依据是来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而能被其他线程访问到,那就可以把他们当做栈上数据对待,认为他们是线程私有的,同步加锁自然就无需进行。
来看这样一个方法
1 public String concatString(String s1, String s2, String s3) 2 { 3 StringBuffer sb = new StringBuffer(); 4 sb.append(s1); 5 sb.append(s2); 6 sb.append(s3); 7 return sb.toString(); 8 }
1 public synchronized StringBuffer append(StringBuffer sb) { 2 super.append(sb); 3 return this; 4 }
可见append的方法使用synchronized进行同步,我们知道对象的实例总是存在于堆中被多有线程共享,即使在局部方法中创建的实例依然存在于堆中,但是对该实例的引用是线程私有的,对其他线程不可见。即上边代码中虽然StringBuffer的实例是共享数据,但是对该实例的引用确实每条线程内部私有的。不同的线程引用的是堆中存在的不同的StringBuffer实例,它们互不影响互不可见。也就是说在concatString()方法中涉及了同步操作。但是可以观察到sb对象它的作用域被限制在方法的内部,也就是sb对象不会“逃逸”出去,其他线程无法访问。因此,虽然这里有锁,但是可以被安全的消除,在即时编译之后,这段代码就会忽略掉所有的同步而直接执行了