天天看点

搞明白synchronized和ReetrantLock

上一篇文章,我们熟悉了Java锁的分类。今天,来学习下Java中常用的悲观锁synchronized和ReetrantLock吧。学习使我快乐,哦耶!

synchronized关键字可以保证,一段时间内共享资源只能被一个线程所使用,或者说一段代码一段时间内只能被一个线程执行,并且共享资源对其他线程是可见的。

实际上,synchronized就是,某个线程拿到一个锁,锁住共享资源,当使用完,放开锁,让其他线程申请锁并使用共享资源。

synchronized作用在普通方法或者代码片段上时,锁为对象本身。作用在static方法或者代码片段上时,锁为类本身。

我们设想一个卖票场景,有A、B两个售票窗口卖票,票池(共享资源)只有一个。

实验1

首先创建售票类<code>SellTicketRunnable</code>,定义公告资源<code>ticket</code>为1000张票,我们每个窗口模拟卖票550张,如果发现票卖完了,就系统提示,否则票数减1。<code>main</code>方法开启两个线程,发现完美运行。发现票数最终为0,并且每个线程访问共享资源的时间内都是独享的。

刚才只生成了一个<code>SellTicketRunnable</code>,只有一把锁。那我们生成两个<code>SellTicketRunnable</code>对象,会不会有两把锁呢?

实验2

将<code>main</code>方法改成上边所示。首先,访问共享资源的时间不再独享。A窗口还没访问完数据库呢,B窗口就去访问了。这最终导致票可能超卖。(就剩1张票了,A、B窗口同时卖出,同时更新共享资源)。当然这段代码你多运行几次才会出现剩余<code>-1</code>的情况,有时候可能为<code>0</code>,毕竟那么巧的事,不是每次都遇到哈。说明,此时锁是对象级别的。

实际上,如果<code>synchronized</code>作用在对象级别上。内存中,对象的对象头会记录当前获取锁的线程,利用的是<code>Monitor</code>机制。

实验3

将实验2主函数中的<code>SellTicketRunnable</code>类换成<code>SellTicketRunnablePlus</code>。<code>SellTicketRunnablePlus</code>只是给<code>sell</code>方法内部,<code>synchronized</code>锁的是<code>SellTicketRunnablePlus</code>类。此时又是一把锁了,所以两个窗口又可以某段时间内独享共享资源了。

synchronized可以保证不同线程同一时间只能有有一个独享共享资源,比如说线程1持有了锁,线程2去申请锁的时候,发现线程1持有锁呢,所以线程2需要等会(线程阻塞)。那么线程1在持有锁的情况下,可以再申请一把同样的锁吗?

实验4

当线程1执行<code>outMethod</code>方法时,获得了锁。<code>outMethod</code>调用<code>innerMethod</code>方法时,线程1又去申请了同一把锁,发现申请成功了。<code>可重入锁</code>是指同一个线程可以多次加同一把锁。

自JDK1.6开始,当只有两个线程竞争锁时,<code>synchronized</code>是轻量级锁,超过两个线程竞争的时候是重量级锁。关于锁的分类,请戳链接: Java锁分类原来是这个样子。

搞明白synchronized和ReetrantLock

<code>synchronized</code>是关键字,很多操作都是隐式的,比如说<code>释放锁</code>、<code>自旋次数</code>等,都是虚拟机帮你搞定的。为了显示操作,并且拥有更强大的功能,<code>ReetrantLock</code>来了。

实验5

ReetrantLock需要手动申请锁和释放锁,分别为方法<code>lock</code>和<code>unlock</code>。

和<code>synchronized</code>一样,<code>ReetrantLock</code>也具备重入性。

实验6

<code>ReetrantLock</code>可以申请公平锁或者非公平锁(了解锁的分类:Java锁分类原来是这个样子)。

首先我们补充一个知识点,<code>ReetrantLock</code>是实现<code>AQS</code>机制的,就是说所有申请锁的线程,会被按需放到一个队列中,然后依次获取锁。<code>公平锁</code>保证了,获取锁的顺序性。

实验7

<code>ReentrantLock</code>new的时候传入<code>true</code>,就是申请了一把公平锁。<code>FairLockThread</code>方法里面让一个线程执行两次申请锁、释放锁操作,并且模拟使用锁0.5秒。<code>getQueueLength</code>方法就是查看,当前队列中阻塞的线程数。可以看出,锁的两遍申请是按照顺序的,从1~5。从线程是也可以看出,没有哪个线程可以偷偷的自己两边都执行完。

还是实验7

我们只需要将主函数,new<code>ReentrantLock</code>的时候设置成false,此时申请的就是非公平锁了。再看运行结果,某个线程执行完第一遍,很大概率上就会执行第二遍。没有按照顺序执行,这是不公平的。

执行完一遍,然后紧接着执行第二遍,不用切换上下文,某线程一致使用CPU,这样效率更快的,所以非公平锁效率更高。

试想一下,如果线程1已经持有锁1,现在想拿锁2,然后就可以开心的结束了。线程2已经持有锁2,现在想拿锁1,然后就可以开心的结束了。这俩线程还愉快的碰面了,结果谁都不放手,谁都不能愉快的结束,于是乎,死锁就产生了。

实验8

以上,运行到电脑死机也不会结束了。如果我们在主函数最后一行后面加上一行

运行结果,放个图吧。

搞明白synchronized和ReetrantLock

可以看出,虽然A牺牲掉了,但是由于A的中断(放弃持有锁1)。B顺利完成了!为小A默哀一分钟。。。

<code>synchronized</code>和<code>ReetrantLock</code>都是悲观锁、可重入锁。

<code>synchronized</code>是隐士申请、释放锁,虚拟机层面维护。<code>ReetrantLock</code>是显示操作,代码维护。

在JDK1.6之前, <code>synchronized</code>性能极差,1.6之后,它俩性能差不多。

<code>ReetrantLock</code>可中断,避免死锁产生。

<code>ReetrantLock</code>可以申请<code>公平锁</code>或者<code>非公平锁</code>,可根据需求定制。

呜呼,从探索到验证,辣条君用了一天,小伙伴们点个赞再走吧。

搞明白synchronized和ReetrantLock

继续阅读