IDC

查漏补缺synchronized和ReentrantLock的基本原理

作者:admin 2021-05-09 我要评论

一、java锁的类型 java的锁有这么几类。 乐观锁和悲观锁 乐观锁就是JVM认为不通过加锁也能保证并发的正确性。典型实现是诸如AtomicInteger的实现。 悲观锁就是需...

在说正事之前,我要推荐一个福利:你还在原价购买阿里云、腾讯云、华为云服务器吗?那太亏啦!来这里,新购、升级、续费都打折,能够为您省60%的钱呢!2核4G企业级云服务器低至69元/年,点击进去看看吧>>>)

 一、java锁的类型

java的锁有这么几类。

乐观锁和悲观锁

  • 乐观锁就是JVM认为不通过加锁也能保证并发的正确性。典型实现是诸如AtomicInteger的实现。
  • 悲观锁就是需要加锁互斥。典型实现是Synchronized(Synchronized属于乐观锁还是悲观锁其实跟具体实现有关,大部分场景下都是悲观锁)和ReentrantLock。

可重入和不可重入

  • 可重入是指当一个线程获取了锁,但是没有释放,这个线程又要获取这个锁,仍然能获取成功。Synchronized和ReentrantLock都是可重入锁。
  • 不可重入是可重入的否命题,这样自己会把自己死锁。应该没有这样的实现。

公平锁和非公平锁

  • 公平锁是先请求锁的线程肯定先获得锁,也就是FIFO。公平说是不是就是合理的?可能也不一定,因为这会造成上下文的切换。ReentrantLock默认是非公平锁,但是可以通过构造方法构造公平锁实例。
  • 非公平锁是新来的线程有优先获得锁的机会,也就是可以插队。合理吗?可能也不合理,因为这可能造成“饿死”现象:在排队的旧的线程总是获取不到锁。Sysnchronized其实就是非公平锁。

排他锁和共享锁

  • 排他锁是一个线程获得锁之后,其他线程不能再获得锁。大多数场景下都是排他锁。
  • 共享锁是指多个线程可以同时获得锁。常见的是多个线程可以同时获得读锁。

二、synchronized

synchronized基本原理是通过CPU指令实现的。在jdk1.6之前是很重的锁。因为java的多线程与操作系统的线程是一一对应的。当java线程阻塞的时候需要切换到内核态的线程进行阻塞,唤醒的时候又要从内核态切换到用户态,进行了很重的上下文切换。那么能不能当一个线程获取不到锁的时候不阻塞呢?自旋可以吗?这样就有了synchronized的四种实现:无锁、偏向锁、轻量锁、重量锁。

synchronized锁的是java的对象头,再详细点是mark word。


无锁

这个没有什么好说的。没有将这个对象通过synchronized包括。

偏向锁

当只有一个线程在访问锁的时候,会在mark word中通过CAS的方式设置当前线程的threadId。如果成功的话,加锁成功(由于只有一个线程,肯定成功)。这样当这个线程再次请求锁的时候,看mark word的thread id和自己是否相同,如果相同加锁成功。注意,它是没有解锁操作的。如果是另一个线程也来了,由于上一个线程没有解锁操作,这个新线程的CAS肯定失败。这时当JVM没有字节码要执行的时候(全局安全点),会检查上一个线程有没有结束,如果结束,则通过CAS将mark word中的thread id字段更新为新线程的threadId。如果上一个线程没有结束,这就存在并发了。偏向锁无法完成使命,需要升级为轻量锁。

轻量锁

接着上面偏向锁的上一个线程A和新的线程B的例子。JVM此时进行一下线程A对mark word的操作。将mark word拷贝到当前线程的栈空间中,CAS操作mark word的指针指向这个栈空间的地址,CAS操作当前线程的栈空间再加一个指向mark word的指针,这两个操作成功后,其实第一个CAS成功就是成功,这样线程A就获得了锁,升级成为了轻量锁。线程B会自旋等待线程A的释放。线程A怎么释放锁呢?只要将第一个CAS操作的指针(mark word指向线程栈的指针)释放了就可以了,线程B自旋检测mark word的指向,去抢占锁。如果此时又来一个线程C呢?是不是也自旋?可以同时有几个线程自旋?线程B能自旋多少次?这些都是有JVM参数可配置的。

重量锁

这个其实也没什么好说的。存在并发访问时,直接将线程切换到内核态阻塞。

三、ReentrantLock

ReentrantLock是通过AQS(AbstractQueuedSynchronizer)实现的。需要解决的问题:

需要有个状态表示这个lock对象是不是被抢占了,如果可重入的话,被这个线程抢占了多少次。这个状态标识其实就是AQS的state成员变量。对state的操作肯定要线程安全。可以通过CAS解决。

  1. protected final boolean tryAcquire(int acquires) { 
  2.     final Thread current = Thread.currentThread(); 
  3.     int c = getState(); 
  4.     if (c == 0) { 
  5.         // 这个是公平锁的实现。需要判断队列中有没有等待的线程, 
  6.         // 如果没有才进行CAS抢占     
  7.         if (!hasQueuedPredecessors() && 
  8.             compareAndSetState(0, acquires)) { 
  9.             setExclusiveOwnerThread(current); 
  10.             return true
  11.         } 
  12.     } 
  13.     // 这里就是可重入逻辑 
  14.     else if (current == getExclusiveOwnerThread()) { 
  15.         int nextc = c + acquires; 
  16.         if (nextc < 0) 
  17.             throw new Error("Maximum lock count exceeded"); 
  18.         setState(nextc); 
  19.         return true
  20.     } 
  21.     return false

多个线程同时抢占lock,只有一个线程能成功,其他线程怎么排队呢?排队的线程怎么抢占锁呢?这就用到了一个队列。这个队列的插入是通过自旋和CAS实现的。

  1. private Node addWaiter(Node mode) { 
  2.     Node node = new Node(mode); 
  3.     // 循环尝试 
  4.     for (;;) { 
  5.         Node oldTail = tail; 
  6.         if (oldTail != null) { 
  7.             // 无锁修改前驱指针 
  8.             node.setPrevRelaxed(oldTail); 
  9.             // CAS修改tail 
  10.             if (compareAndSetTail(oldTail, node)) { 
  11.                 // 修改后续指针 
  12.                 oldTail.next = node; 
  13.                 return node; 
  14.             } 
  15.         } else { 
  16.             initializeSyncQueue(); 
  17.         } 
  18.     } 

排队的线程抢占lock呢?

  1. final boolean acquireQueued(final Node node, int arg) { 
  2.     boolean interrupted = false
  3.     try { 
  4.         for (;;) { 
  5.             final Node p = node.predecessor(); 
  6.             // 如果前驱节点是头节点,并且获取锁成功,直接返回。 
  7.             // 但是大多数情况,可能运气没这么好 
  8.             if (p == head && tryAcquire(arg)) { 
  9.                 setHead(node); 
  10.                 p.next = null; // help GC 
  11.                 return interrupted; 
  12.             } 
  13.             // 是否需要阻塞 
  14.             if (shouldParkAfterFailedAcquire(p, node)) 
  15.                 // 这里阻塞 
  16.                 interrupted |= parkAndCheckInterrupt(); 
  17.         } 
  18.     } catch (Throwable t) { 
  19.         cancelAcquire(node); 
  20.         if (interrupted) 
  21.             selfInterrupt(); 
  22.         throw t; 
  23.     } 

怎么唤醒上面阻塞的线程呢?这就要看下释放逻辑。

  1. public final boolean release(int arg) { 
  2.     // 释放lock 
  3.     if (tryRelease(arg)) { 
  4.         Node h = head; 
  5.         if (h != null && h.waitStatus != 0) 
  6.             unparkSuccessor(h);  // 唤醒头结点的后续节点。注意头结点是虚节点,没有实在意义 
  7.         return true
  8.     } 
  9.     return false

本文转载自网络,原文链接:https://www.toutiao.com/i6933840985927598600/

版权声明:本文转载自网络,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。本站转载出于传播更多优秀技术知识之目的,如有侵权请联系QQ/微信:153890879删除

相关文章
  • Namejet 4月交易125万美元:746.com以7

    Namejet 4月交易125万美元:746.com以7

  • .CLOUD参加第二届CIIE进博会精彩回顾

    .CLOUD参加第二届CIIE进博会精彩回顾

  • 判决出炉:威瑞信再次败诉!.com起诉.x

    判决出炉:威瑞信再次败诉!.com起诉.x

  • 爆料:腾讯TGP游戏平台将更名Wegame!

    爆料:腾讯TGP游戏平台将更名Wegame!

腾讯云代理商
海外云服务器