Java中的锁系列1
整篇文章,我想由浅到深开始写。刚开始可能会有一些比较基础的内容。
一、通过一个典型的并发问题,了解锁到底有什么用
开10个线程,对count变量进行++操作,每个线程执行50次++操作。
10个线程,每个线程加50次,理论上,应该是500;
但是,让我们看下实际的运行结果。
第一次运行结果:
第二次运行结果:
不是500也就算了,每次运行的结果还不一样。很显然,并发了。然后,我们加上synchronized关键字试试。
结果如下:
执行结果正确了。但是,我们也发现增加synchronized后,比增加之前执行时间长了很多。
二、锁消除和锁粗化
不难发现 , 是因为我们对Thread.currentThread().sleep(100);也加了锁,导致运行变慢。
然后我们将synchronized的位置改一下。
运行结果如下:
运行结果正确,但是运行时间快了很多。所以,写代码的时候,一定要注意,不该锁的代码,不要锁。我们通常也把这种原则叫做锁消除。
但是!
如果要是我们频繁的lock和unlock,同样会导致大量的开销。这个时候,我们需要将多个连续的锁扩展成一个范围更大的锁。这个叫做锁粗化。
有的同学可能会说了,我粗也不好,细也不好,你想让我一粗一细一粗一细么!
好吧,这个就看自己怎么去衡量了…
三、ReentrantLock类
Java里除了synchronized关键字外,还有很多其他的方式也可以避免并发问题。
比如典型的ReentrantLock,又叫做可重入锁。
ReentrantLock是继承自Lock接口。
除了ReentrantLock外,还有ReentrantReadWriteLock
我们可以发现,ReentrantReadWriteLock并没有直接实现Lock接口。
但是,ReentrantReadWriteLock有两个方法,可以获取ReadLock和WriteLock,而ReadLock和WriteLock实现了Lock接口。ReentrantReadWriteLock的具体使用和实现我们将在后面的文章中研究。
本文里,我们先了解下ReentrantLock。
首先, 我们来创建一个可重入锁ReentrantLock,先lock,然后在finally里unlock。
我们来看下执行效果:
什么鬼?明明锁起来了,怎么结果不是500!
检查下代码, 发现ReentrantLock reentrantLock = new ReentrantLock();这行代码居然写在run()方法里面。每次执行都会创建一把新的锁,当然没用......。那应该怎么做呢?
肯定是当成成员变量。
改好之后,再看下效果:
所以,一定记得要保证:需要同步的代码块拿到的是同一把锁。用吐槽大会池子的话说:知识点啊有木有。
四、什么是可重入锁?怎么证明synchronized和ReentrantLock都是可重入的?
前面一直在说ReentrantLock,字面翻译过来,也就是可重入锁,但是一直不明白可重入锁到底是个什么鬼?
其实,可重入锁是一个概念,并不就仅仅指ReentrantLock,虽然它翻译过来就是这个意思。
可重入锁,也叫做递归锁,指的是同一线程外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
通俗点说就是在同一个线程里调lock()之后,哪怕不释放,然后再调一次lock()方法,又可以拿到锁。可重入锁主要是为了解决递归调用产生的死锁问题。其实synchronized也是可重入锁的一种,因为写了synchronized关键字之后,递归调用可以正常执行。
为了加深理解,我们自己写一个不可重入锁,来对比一下可重入锁。
我们自己写的锁名字叫MyLock,一样也继承Lock接口。
然后我们用MyLock来锁一下试试:
执行结果如下:
结果正确!说明我们实现了锁的基本功能。
然后我们来实验一下需要可重入的场景试试。
我们在run方法里,执行两次count++操作,对每个count++执行一次加锁操作,但是不解锁。
代码如下:
执行结果如下:
可以看出,用我们自己写的MyLock锁,执行了一次,程序就死锁了。
然后我们把lock换成ReentrantLock再来一次。
private static Lock lock = new ReentrantLock();
再次执行,结果如下:
跟预期结果一样。
然后我们再改成Synchronized试试。
结果同上。
通过这个实验,我们可以证明,ReentrantLock和synchronized确实是可重入锁。
五、synchronized和ReentrantLock的比较
那么问题来了,既然synchronized和ReentrantLock都是可重入锁,synchronized那么方便,还需要ReentrantLock干啥?难道蛋疼?
我们可以看下ReentrantLock实现lock的方式。
然后Sync是继承自传说中的AbstractQueuedSynchronizer(AQS)。
另外在ReentrantLock内部还定义了另外两个类,分别是FairSync和NonFairSync,这两个类就是分别对应的锁公平分配和不公平分配的两个实现,它们都继承自Sync(类图已经清晰的描述出来了继承结构)。有关锁的分配和释放逻辑都是封装在了AQS里面。
而Synchronized实现的同步和上面提到的AQS的方式是不同的,AQS实现了一套自己的算法来实现共享资源的合理控制(具体算法实现,下文分析),而Synchronized实现的同步控制是基于java内部的对象锁的。
那什么是java内部的对象锁呢?
Java内部对象锁:JVM中每个对象和类实际上都与一把锁与之相关联,对于对象来说,监视的是这个对象变量,对于类来说,监视的是类变量。当虚拟机装载类时,会创建一个Class类的实例,锁住的实际上是这个类对应的Class累的实例。对象锁是可重入的,也就是说一个对象或者类上的锁是可以累加的。
然后网上也有一些ReentrantLock和synchronized的性能比较。
http://blog.csdn.net/lantian0802/article/details/8948696
各种数据都显示,ReentrantLock无论哪方面都比synchronized好。而且支持更多的特性,比如时间锁等候、可中断锁等候、无块结构锁、多个条件变量或者锁投票。
但是!是的,我们来但是了!
很多大神还是建议能用synchronized开发的时候,尽量别用ReentrantLock,除非能证明synchronized已经不适合所在场景。因为,大多数synchronized块几乎从来没有出现过争用,而ReentrantLock比synchronized优秀是体现在出现高争用的场景。但是一旦忘记写unlock了,那就是死锁!
下一篇,我们将具体讲解java内部的对象锁,包括偏向锁、轻量级锁、重量级锁和自旋锁。