• Java并发:重入锁 ReentrantLock(二)


    一、理解锁的实现原理

    1. 用wait()去实现一个lock方法,wait()要和synchronized同步关键字一起去使用的,直接使用wait方法会直接报IllegalMonitorStateException错误,使用wait方法实现一个lock,还要使用synchronized是多此一举的。

    1     public void lock() {
    2         try {
    3             wait();     // 需要搭配synchronized关键字在去使用的
    4         } catch (InterruptedException e) {
    5             e.printStackTrace();
    6         }
    7     }

    不搭配synchronized关键字会报错。

     wait源码注释说明了需要搭配synchronized关键字。

    2. 使用sleep来模拟线程阻塞,可以通过很大的数字进行无限阻塞来达到类似的效果,但是这里是有问题,需要传入一个很大的数字,还有sleep之后如何唤醒线程,所以使用sleep也不合适。

    3. 使用while循环,不断轮询加锁的状态

     1     public void lock() {
     2         // 判断当前线程是否要阻塞
     3         /*
     4         while (state != 0) {}
     5         state = 1;
     6         */
     7         while (!unsafe.compareAndSwapInt(this, stateOffset, 0, 1)) {
     8             System.out.println(Thread.currentThread().getName() + "正在加锁");
     9         }
    10         System.out.println(Thread.currentThread().getName() + "加到锁了");
    11     }

    当有多个线程同时使用这个lock锁,,其中一个线程抢到了锁,另外的线程就会不断的while循环获取锁,这样会耗费CPU的资源。

    1     new Thread(new Runnable() {
    2             @Override
    3             public void run() {
    4                 lock.lock();
    5                 drawMoney();
    6                 lock.unlock();
    7             }
    8         }, "线程1/2/3").start();

    4. 使用LockSupport.park/unpark去阻塞/解阻塞线程。

     二、深入ReentrantLock源码

     1     private static final ReentrantLock lock = new ReentrantLock();
     2 
     3     public static void main(String[] args) {
     4         new Thread(new Runnable() {
     5             @Override
     6             public void run() {
     7                 lock.lock();
     8                 drawMoney();
     9                 lock.unlock();
    10             }
    11         }, "线程一").start();
    12     }

    查看ReentrantLock.lock方法

    1     /**
    2      * 获得锁。
    3      * 如果其他线程没有持有锁,则获取该锁并立即返回,将锁持有计数设置为 1。
    4      * 如果当前线程已经持有锁,那么持有计数加一并且该方法立即返回。
    5      * 如果该锁被另一个线程持有,那么当前线程将因线程调度目的而被禁用并处于休眠状态,直到获得该锁为止,此时锁持有计数设置为 1。
    6      */
    7     public void lock() {
    8         sync.lock();
    9     }

    这个lock方法有两种实现,一个是公平锁,一个是非公平锁

     先看公平锁FairSync,这个acquire方法是加锁。

     acquire方法会去尝试加把锁,若果尝试加锁成功了,这个方法就会直接返回了,线程就会继续执行。若是没有成功,就会添加到等待队列中(Node),排队以独占不间断模式获取锁的状态。

     进入尝试加锁的tryAcquire方法,这个方法也有公平锁、非公平锁的实现,先看公平锁的。

     c是锁的状态,c=0就是这把锁没人占用,就会通过cas修改锁的状态,并设置这个锁属于当前线程,在这之前要hasQueuedPredecessors判断有没有人在排队。这个c可能是1,也可能是2,也可以是3,取决于你线程中加锁的次数(可重入)。后面还会检查当前线程是不是这把锁的线程持有者,是线程的持有者则继续设置state状态,超出最大锁定计数则会报错。前面的两个if条件没有满足,就会返回false,加锁失败,加入等待队列排队。

    hasQueuedPredecessors方法,需要注意的是在高并发状态下,头结点不等于尾结点,但是头结点的下一个节点s为空,这是因为可能在头连接在链接一个等待线程的结点的过程还没完成,所以h.next为null,或者是s节点的线程不是当前线程,则判断为队列中有线程在排队,返回true。

    没有加锁成功,就会添加到等待队列,并由acquireQueued方法进行park,阻塞线程。

     addWaiter方法先构造的同步节点,判断尾节点是否为空,不为空就通过CAS把新的结点加入到队列尾部。加入成功返回结点node给acquireQueued方法。

     

     如果尾节点为空,则看enq方法,for循环不断地去判断队列是否需要重新初始化并把node结点加入到队尾。

     

     有多个线程竞争同一把锁,因为队列里首节点拿到锁以后,就会出队列,这时候队列为空,就需要new一个空结点作为队列的head结点,再把等待的新节点,添加到队尾,

     

  • 相关阅读:
    DOCTYPE
    js——类型转换
    对象Object
    Array数组
    es6学习笔记
    springboot第一个项目【mybatis】
    springboot第一个项目【创建】
    项目管理和流程的拙见
    树莓派 Zero作为飞控图传
    一根数据线玩转树莓派Zero
  • 原文地址:https://www.cnblogs.com/magic-sea/p/15341007.html
Copyright © 2020-2023  润新知