juc并发工具包中各种锁 2020最新

​一.java中的锁

根据加入Java中的时间,Java中的锁,可以分为"同步锁"和"JUC包中的锁"。

同步锁

  即通过synchronized关键字来进行同步,实现对竞争资源的互斥访问的锁。Java 1.0版本中就已经支持同步锁了。

  同步锁的原理是,对于每一个对象,有且仅有一个同步锁;不同的线程能共同访问该同步锁。但是,在同一个时间点,该同步锁能且只能被一个线程获取到。这样,获取到同步锁的线程就能进行CPU调度,从而在CPU上执行;而没有获取到同步锁的线程,必须进行等待,直到获取到同步锁之后才能继续运行。这就是,多线程通过同步锁进行同步的原理! 

  关于"同步锁"的更多内容,请参考"Java锁的基础部分"的内容。

 

JUC包中的锁 

  相比同步锁,JUC包中的锁的功能更加强大,它为锁提供了一个框架,该框架允许更灵活地使用锁,只是它的用法更难罢了。

  JUC包中的锁,包括:Lock接口,ReadWriteLock接口,LockSupport阻塞原语,Condition条件,AbstractOwnableSynchronizer/AbstractQueuedSynchronizer/AbstractQueuedLongSynchronizer三个抽象类,ReentrantLock独占锁,ReentrantReadWriteLock读写锁。由于CountDownLatch,CyclicBarrier和Semaphore也是通过AQS来实现的;因此,我也将它们归纳到锁的框架中进行介绍。

  先看看锁的框架图,如下所示。

01. Lock接口

  JUC包中的 Lock 接口支持那些语义不同(重入、公平等)的锁规则。所谓语义不同,是指锁可是有"公平机制的锁"、"非公平机制的锁"、"可重入的锁"等等。"公平机制"是指"不同线程获取锁的机制是公平的",而"非公平机制"则是指"不同线程获取锁的机制是非公平的","可重入的锁"是指同一个锁能够被一个线程多次获取。

 

02. ReadWriteLock

  ReadWriteLock 接口以和Lock类似的方式定义了一些读取者可以共享而写入者独占的锁。JUC包只有一个类实现了该接口,即 ReentrantReadWriteLock,因为它适用于大部分的标准用法上下文。但程序员可以创建自己的、适用于非标准要求的实现。

 

03. AbstractOwnableSynchronizer/AbstractQueuedSynchronizer/AbstractQueuedLongSynchronizer
  AbstractQueuedSynchronizer就是被称之为AQS的类,它是一个非常有用的超类,可用来定义锁以及依赖于排队阻塞线程的其他同步器;ReentrantLock,ReentrantReadWriteLock,CountDownLatch,CyclicBarrier和Semaphore等这些类都是基于AQS类实现的。AbstractQueuedLongSynchronizer 类提供相同的功能但扩展了对同步状态的 64 位的支持。两者都扩展了类 AbstractOwnableSynchronizer(一个帮助记录当前保持独占同步的线程的简单类)。


04. LockSupport
  LockSupport提供“创建锁”和“其他同步类的基本线程阻塞原语”。 
  LockSupport的功能和"Thread中的Thread.suspend()和Thread.resume()有点类似",LockSupport中的park() 和 unpark() 的作用分别是阻塞线程和解除阻塞线程。但是park()和unpark()不会遇到“Thread.suspend 和 Thread.resume所可能引发的死锁”问题。

 

05. Condition
  Condition需要和Lock联合使用,它的作用是代替Object监视器方法,可以通过await(),signal()来休眠/唤醒线程。
Condition 接口描述了可能会与锁有关联的条件变量。这些变量在用法上与使用 Object.wait 访问的隐式监视器类似,但提供了更强大的功能。需要特别指出的是,单个 Lock 可能与多个 Condition 对象关联。为了避免兼容性问题,Condition 方法的名称与对应的 Object 版本中的不同。

 

  06. ReentrantLock
  ReentrantLock是独占锁。所谓独占锁,是指只能被独自占领,即同一个时间点只能被一个线程锁获取到的锁。ReentrantLock锁包括"公平的ReentrantLock"和"非公平的ReentrantLock"。"公平的ReentrantLock"是指"不同线程获取锁的机制是公平的",而"非公平的  ReentrantLock"则是指"不同线程获取锁的机制是非公平的",ReentrantLock是"可重入的锁"。
  ReentrantLock的UML类图如下:

  (01) ReentrantLock实现了Lock接口。
  (02) ReentrantLock中有一个成员变量sync,sync是Sync类型;Sync是一个抽象类,而且它继承于AQS。
  (03) ReentrantLock中有"公平锁类"FairSync和"非公平锁类"NonfairSync,它们都是Sync的子类。ReentrantReadWriteLock中sync对象,是FairSync与NonfairSync中的一种,这也意味着ReentrantLock是"公平锁"或"非公平锁"中的一种,ReentrantLock默认是非公平锁。

 

07. ReentrantReadWriteLock
  ReentrantReadWriteLock是读写锁接口ReadWriteLock的实现类,它包括子类ReadLock和WriteLock。ReentrantLock是共享锁,而WriteLock是独占锁。
  ReentrantReadWriteLock的UML类图如下:


  (01) ReentrantReadWriteLock实现了ReadWriteLock接口。
  (02) ReentrantReadWriteLock中包含sync对象,读锁readerLock和写锁writerLock。读锁ReadLock和写锁WriteLock都实现了Lock接口。
  (03) 和"ReentrantLock"一样,sync是Sync类型;而且,Sync也是一个继承于AQS的抽象类。Sync也包括"公平锁"FairSync和"非公平锁"NonfairSync。


08. CountDownLatch
  CountDownLatch是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。 
  CountDownLatch的UML类图如下:


  CountDownLatch包含了sync对象,sync是Sync类型。CountDownLatch的Sync是实例类,它继承于AQS。

 

09. CyclicBarrier
  CyclicBarrier是一个同步辅助类,允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。因为该 barrier 在释放等待线程后可以重用,所以称它为循环 的 barrier。
  CyclicBarrier的UML类图如下:


  CyclicBarrier是包含了"ReentrantLock对象lock"和"Condition对象trip",它是通过独占锁实现的。
  CyclicBarrier和CountDownLatch的区别是:
  (01) CountDownLatch的作用是允许1或N个线程等待其他线程完成执行;而CyclicBarrier则是允许N个线程相互等待。
  (02) CountDownLatch的计数器无法被重置;CyclicBarrier的计数器可以被重置后使用,因此它被称为是循环的barrier。

 

10. Semaphore
  Semaphore是一个计数信号量,它的本质是一个"共享锁"。
  信号量维护了一个信号量许可集。线程可以通过调用acquire()来获取信号量的许可;当信号量中有可用的许可时,线程能获取该许可;否则线程必须等待,直到有可用的许可为止。线程可以通过release()来释放它所持有的信号量许可。
  Semaphore的UML类图如下:


  和"ReentrantLock"一样,Semaphore包含了sync对象,sync是Sync类型;而且,Sync也是一个继承于AQS的抽象类。Sync也包括"公平信号量"FairSync和"非公平信号量"NonfairSync。

Exchanger

二.数据库中的锁

【共享锁】(读锁):同一时间段内,多个用户可以读取同一个资源,读取的过程中数据不会发生任何变化。读锁之间是相互不阻塞的,多个用户可以同时读,但是

不允许有人修改,任何事务都不允许获得数据上的排他锁,直到数据上释放掉所有的共享锁。

在执行语句后加lock in share mode

【排他锁】(写锁):在任何时候只能有一个用户写入资源,当进行写锁时会阻塞其他的读锁或者写锁操作,只能由这一个用户来写,其他用户既不能读也不能写

指的是对于多个不同的事务,对同一个资源只能有一把锁。在执行语句后加上for update

··加锁会有粒度问题,从粒度上从大到小可以划分为 

【表锁】获取锁定资源开销较小,一旦有用户访问这个表就会加锁,其他用户就不能对这个表操作了,应用程序的访问请求遇到锁等待的可能性比较高,并发处理能力弱

【行锁】获取锁定资源开销较大,能具体锁定到表中的某一行数据,但是能更好的支持并发处理,会发生死锁

【页锁】是mysql中比较独特的一种锁定级别,锁定粒度介于行锁和表锁之间,所以获取锁定的资源开销也介于二者之间,另外,页级锁定和行级锁定一样,会发生死锁

 

三.按照java类中的锁定范围分为

类锁

类锁,是用来锁类的,我们知道一个类的所有对象共享一个class对象,共享一组静态方法,类锁的作用就是使持有者可以同步地调用静态方法。当synchronized指定修饰静态方法或者class对象的时候,拿到的就是类锁,类锁是所有对象共同争抢一把。

B中有两个方法mB和mC//mB是synchronized修饰静态方法,拿到类锁//mC是synchronized修饰非静态方法,拿到的也是类锁

class B {

synchronized public static void mB(String value) throws InterruptedException {for (int i = 0; i < 1000; i++) {System.out.print(value);}} 

public void mC(String value) {synchronized (B.class) {for (int i = 0; i < 1000; i++) {System.out.print(value);}}}

}

 

对象锁

对象锁,是用来对象的,虚拟机为每个的非静态方法和非静态域都分配了自己的空间,不像静态方法和静态域,是所有对象共用一组。所以synchronized修饰非静态方法或者this的时候拿到的就是对象锁,对象锁是每个对象各有一把的,即同一个类如果有两个对象。

//类C中有两个方法mB和mC//mB是synchronized非静态方法,拿到对象锁//mC是synchronized修饰this,拿到的也是对象锁class C {

synchronized publi void mB(String value) throws InterruptedException {for (int i = 0; i < 1000; i++) {System.out.print(value);}} 

public void mC(String value) {synchronized (this) {for (int i = 0; i < 1000; i++) {System.out.print(value);}}}

}

三.悲观锁与乐观锁

悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作

乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性,乐观锁不能解决脏读的问题

【操作系统的悲观锁,乐观锁】

··悲观锁

cpu是时分复用的,也就是把cpu的时间片,分配给不同的thread/process轮流执行,时间片与时间片之间,需要进行cpu切换,也就是会发生

进程的切换。切换涉及到清空寄存器,缓存数据,然后重新加载新的thread所需数据。当一个线程被挂起时,加入到阻塞队列,在一定的时间或条件下,

在通过notify(),notifyAll()唤醒回来。在某个资源不可用的时候,就将cpu让出,把当前等待线程切换为阻塞状态,等到资源(比如一个

共享数据)可用了,那么就将线程唤醒,让他进入runnable状态等待cpu调度.这是典型的悲观锁的实现。独占锁是一种悲观锁,synchronized就是

一种独占锁,它假设最坏的情况,只有在确保其它线程不会造成干扰的情况下执行,会导致其它需要锁的线程挂起,等待持有锁的线程释放锁.

但是,由于进程挂起和恢复执行过程中存在很大的开销,当一个线程在等待锁时,它不能做任何事,所以悲观锁有很大的缺点。举例:

如果一个线程需要某个资源,但是这个资源的占用时间很短,当线程第一次抢占这个资源时,可能这个资源被占用,如果此时挂起这个线程,可能立刻就

发现资源可用,然后又需要花费很长的时间重新抢占锁,时间代价就会非常高

 

··乐观锁

每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。在上面的例子中,某个线程可以不让出cpu,而是一直

while循环,如果失败就重试,直到成功为止。所以当数据争用不严重时,乐观锁效果更好

JDK1.5中引入了底层的支持,在int,long和对象的引用等类型上都公开了CAS的操作,并且JVM把它们编译为底层硬件提供的最有效方法,在运行CAS

的平台上,运行时把它们编译为相应的机器指令。

在CAS操作中,会出现ABA问题。就是如果V的值由A变成B,再由B变成A,那么仍然认为是发生了变化,并需要重新执行算法中的步骤。有简单的解决

方案:不是更新某个引用的值,而是更新两个值,包括一个引用和一个版本号,即使这个值由A变成B,然后变成A,版本号也是不同的

 

 

··优缺点

两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样就可以省去锁的开销,加大

系统的整个吞吐量;但如果经常·产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,这种情况下用悲观锁比较合适

 

【数据库层面的悲观锁,乐观锁】

【乐观锁】不是数据库自带的,需要我们自己去实现。乐观锁是指操作数据库时(更新操作),认为这次操作不会导致冲突,在操作数据时,不进行任何

其他的特殊处理,在进行更新后,再去判断是否有冲突。

通常实现是这样的:在表中数据进行操作时,先给数据表加一个版本字段,每操作一次,将那条记录的版本号加一。也就是先查询出那条记录,获取

version字段,如果要对那条记录进行操作(更新),则先判断此刻version的值是否与刚查出的version的值相等,如果相等,说明这段时间没有其他程序对其进行操作,

则可以执行更新,version的值加1;如果version的值不相等,说明这段时间已经有其他程序对其进行操作了,则不进行更新操作。

除了基于版本控制实现外,还可以通过时间戳的方式,通过提前读取,事后对比的方式实现

【悲观锁】悲观锁是由数据库自己实现了的,要用的时候,直接调用数据库相关语句就可以了


 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值