互联网技术交流Java并发编程的艺术

Java中的锁系列1

2017-03-31  本文已影响250人  窝牛狂奔

整篇文章,我想由浅到深开始写。刚开始可能会有一些比较基础的内容。

一、通过一个典型的并发问题,了解锁到底有什么用

开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内部的对象锁,包括偏向锁、轻量级锁、重量级锁和自旋锁。

Java中的锁系列2

上一篇下一篇

猜你喜欢

热点阅读