xiaobenchi

导航

Java 并发包中锁原理剖析

Java 并发包中锁原理剖析

1. LockSupport工具类

JDK中的rt.jar包里面的LockSupport是个工具类,它的主要作用是挂起和唤醒线程,该工具类是创建锁和其他同步类的基础。

LockSupport类与每个使用它的线程都会管理一个许可证,在默认的情况下调用LockSupport类的方法的线程是不持有许可证的。LockSupport是使用Unsafe类实现的,下面介绍LockSupport中的几个主要函数。

  • void park( )方法

    如果调用park方法的线程已经拿到了与LockSupport关联的许可证,则调用LockSupport.park()时会马上返回,否则调用线程会被禁止参与线程的调度,也就是会被阻塞挂起。

    在其他线程调用unpark(Thread thread)方法并且将当前线程作为参数时,调用park方法而被阻塞的线程会返回。另外,如果其他线程调用了阻塞线程的interrupt()方法,设置了中断标志或者线程被虚假唤醒,则阻塞线程也会返回。所以在调用park方法时最好也使用循环条件判断方式。
    需要注意的是,因调用park()方法而被阻塞的线程被其他线程中断而返回时并不会抛出InterruptedException异常。

  • void unpark(Thread thread)方法

    当一个线程调用unpark时,如果参数thread线程没有持有thread与LockSupport类关联的许可证,则让thread线程持有。如果thread之前因调用park()而被挂起,则调用unpark后,该线程会被唤醒。如果thread之前没有调用park,则调用unpark方法后,再调用park方法,其会立刻返回。

  • void parkNanos(long nanos)方法

    和park方法类似,如果调用park方法的线程已经拿到了与LockSupport关联的许可证,则调用LockSupport.parkNanos(long nanos)方法后会马上返回。该方法的不同在于,如果没有拿到许可证,则调用线程会被挂起nanos时间后修改为自动返回。

  • park(Object blocker)方法

    public static void park(Object blocker){
        //获取调用线程
        Thread t = Thread.currentThread();
        
        //设置该线程的blocker变量
        setBlocker(t,blocker);
        
        //挂起线程
        UNSAFE.park(false,0L);
        
        //线程被激活后清楚blocker变量
        setBlocker(t,null);
    }
    

    Thread类里面有个变量volatile Object parkBlocker,用来存放park方法传递的blocker对象,也就是把blocker变量存放到了调用park方法的线程的成员变量里面。

  • void parkNanos(Object blocker,long nanos)方法

  • void partUtil(Object blocker,long deadline)方法

    public static void parkUtils(Object blocker,long deadline){
        Thread t = Thread.currentThread();
        setBlocker(t,blocker);
        //isAbsolute=true,time=deadline;表示到deadline事件后返回
        UNSATE.park(true,deadline);
        setBlock(t,null);
    }
    
  • 先进先出锁例子

    class FIFOMutex{
        private final AtomicBoolean locked = new AtomicBoolean(false);
        private final Queue<Thread> waiters = new ConcurrentLinkedQueue<Thread>();
        
        public void lock(){
             boolean wasInterputed = false;
            Thread current = Thread.currentThread();
            waiters.add(current);
            
            //只有队首的线程可以获取锁(1)
            while(waiters.peek() != current || !locked.compareAndSet(false,true)){
                lockSupport.park(this);
                if(Thread.interrupted()) //(2)
                    wasInterputed = true;
            }
            waiters.remove();
            if(wasInterputed) //(3)
                current.inerrupt();
        }
        
        public void unlock(){
            locked.set(false);
            LockSupport.unpark(waiters.peek());
        }
    }
    

    这是一个先进先出的锁,也就是只有队列的首元素可以获取锁。在代码(1)处,如果当前线程不是队首或者当前锁已经被其他线程获取,则调用park方法挂起自己。

    然后在代码(2)处判断,如果park方法是因为被中断而返回,则忽略中断,并且重置中断标志,做个标记,然后再次判断当前线程是不是队首元素或者当前锁是否已经被其他线程获取,如果是则继续调用park方法挂起自己。
    然后在代码(3)中,判断标记,如果标记为true则中断该线程,这个怎么理解呢?其实就是其他线程中断了该线程,虽然我对中断信号不感兴趣,忽略它,但是不代表其他线程对该标志不感兴趣,所以要恢复下。

2. 抽象同步队列AQS概述

  • AQS 锁的底层支持

    AbstractQueuedSynchronizer抽象同步队列简称AQS,它是实现同步器的基础组件,并发包中锁的底层就是使用AQS实现的。

    AQS是一个FIFO的双向队列,其内部通过节点head和tail记录队首和队尾元素,队列元素的类型为Node。其中Node中的thread变量用来存放进入AQS队列里面的线程;Node节点内部的SHARED用来标记该线程是获取共享资源时被阻塞挂起后放入AQS队列的,EXCLUSIVE用来标记线程是获取独占资源时被挂起后放入AQS队列的;waitStatus记录当前线程等待状态,可以为CANCELLED(线程被取消了)、SIGNAL(线程需要被唤醒)、CONDITION(线程在条件队列里面等待)、PROPAGATE(释放共享资源时需要通知其他节点);prev记录当前节点的前驱节点,next记录当前节点的后继节点。
    在AQS中维持了一个单一的状态信息state,可以通过getState、setState、compareAndSetState函数修改其值。

    AQS有个内部类ConditionObject,用来结合锁实现线程同步。

    对于AQS来说,线程同步的关键是对状态值state进行操作。根据state是否属于一个线程,操作state的方式分为独占方式和共享方式。

    在独占方式下,获取与释放资源的流程如下:

    (1) 当一个线程调用acquire(int arg)方法获取独占资源时,会首先使用tryAcquire方法尝试获取资源,具体是设置状态变量state的值,成功则直接返回,失败则将当前线程封装为类型为Node.EXCLUSIVE的Node节点后插入到AQS阻塞队列的尾部,并调用LockSupport.park(this)方法挂起自己。

    (2) 当一个线程调用release(int arg)方法时会尝试使用tryRelease操作释放资源,这里是设置状态变量state的值,然后调用LockSupport.unpark(thread)方法激活AQS队列里面被阻塞的一个线程(thread)。被激活的线程则使用tryAcquire尝试,看当前状态变量state的值是否能满足自己的需要,满足则该线程被激活,然后继续向下运行,否则还是会被放入AQS队列并被挂起。

    需要注意的是,AQS类并没有提供可用的tryAcquire和tryRelease方法,正如AQS是锁阻塞和同步器的基础框架一样,tryAcquire和tryRelease需要由具体的子类来实现。子类在实现tryAcquire和tryRelease时要根据具体场景使用CAS算法尝试修改state状态值,成功则返回true,否则返回false。子类还需要定义,在调用acquire和release方法时state状态值的增减代表什么含义。

    在共享方式下,获取与释放资源的流程如下

    (1) 当线程调用acquireShared(int arg)获取共享资源时,会首先使用tryAcquireShared尝试获取资源,具体是设置状态变量state的值,成功则直接返回,失败则将当前线程封装为类型为Node.SHARED的Node节点后插入到并使用LockSupport.park(this)方法挂起自己。

    (2) 当一个线程调用releaseShared(int arg)时会尝试使用tryReleaseShared操作释放资源,这里是设置状态变量state的值,然后使用LockSupport.unpark(thread)激活AQS队列里面被阻塞的一个线程(thread)。被激活的线程则使用tryReleaseShared查看当前状态变量state的值是否能满足自己的需要,满足则该线程被激活,然后继续向下运行,否则还是会被放入AQS队列并被挂起。

    同样需要注意的是,AQS类并没有提供可用的tryAcquireShared和tryReleaseShared方法,正如AQS是锁阻塞和同步器的基础框架一样,tryAcquireShared和tryReleaseShared需要由具体的子类来实现。

    维护AQS提供的队列,主要看入队操作

    入队操作:当一个线程获取锁失败后该线程会被转换为Node节点,然后就会使用enq(final Node node)方法将该节点插入到AQS的阻塞队列。

  • AQS——条件变量的支持

    正如在基础篇中讲解的,notify和wait,是配合synchronized内置锁实现线程间同步的基础设施一样,条件变量的signal和await方法也是用来配合锁(使用AQS实现的锁)实现线程间同步的基础设施。

    它们的不同在于,synchronized同时只能与一个共享变量的notify或wait方法实现同步,而AQS的一个锁可以对应多个条件变量。

    一个锁对应一个AQS阻塞队列,对应多个条件变量,每个条件变量有自己的一个条件队列。

    image-20220802123633129

  • 基于AQS实现自定义同步器

    • 代码实现基于AQS实现的不可重入的独占锁。

      //基于AQS实现的不可重入的独占锁
      class NonReentrantLock implenents Lock, java.io.Serializable{
          
          //内部帮助类
          private static class Sync extends AbstractQueuedSynchronizer{
              //是否锁已经被持有
              protected  boolean isHeldExclusively(){
                  return getState() == 1;
              }
              
              //如果state为0,则尝试获取锁
              public boolean tryAcquire(int acquires){
                  assert acquires == 1;
                  if(compareAndSetState(0,1)){
                      setExclusiveOwerThread(Thread.currentThread());
                      return true;
                  }
                  return false;
              }
              
              //尝试释放锁,设置state为0
              protected boolean tryRelease(int releases){
                  assert releases == 1;
                  if(getState() == 0) 
                      throw new IllegalMonitorStateException();
                  setExclusiveOwnerThread(null);
                  setState(0);
                  renturn true;
                  
                  //提供条件变量接口
                  Condition newCondition(){
                      return new ConditionObject();
                  }
              }
              
              //创建一个Sync来做具体工作
              private final Sync sync = new Sync();
                  
              public void lock(){
                  sync.acquire(1);
              }
              
              public boolean tryLock(){
                  return sync.tryAcquire(1);
              }
              
              public void unlock(){
                  syn.release(1);
              }
              
              public Condition newCondition(){
                  return sync.newCondition();
              }
              
              public boolean isLocked(){
                  return sync.isHeldExclusively();
              }
              
              public void lockInterruptibly() throws InterruptedException{
                  sync.acquireInterruptibly(1);
              }
              
              public boolean tryLock(long timeout,TimeUnit unit) throws InterruptedException{
                  return sync.tryAcquireNanos(1,unit.toNanos(timeout));
              }
          }
      }
      

      在如上代码中,NonReentrantLock定义了一个内部类Sync用来实现具体的锁的操作,Sync则继承了AQS。由于我们实现的是独占模式的锁,所以Sync重写了tryAcquire、tryRelease和isHeldExclusively 3个方法。另外,Sync提供了newCondition这个方法用来支持条件变量。

    • 使用自定义锁实现生产-消费模型

      final static NonReentrantLock lock = new NonReentrantLock();
      final static Condition notFull = lock.newCondition();
      final static Condition notEmpty = lock.newCondition();
      
      final static Queue<String> queue = new LinkedBlockingQueue<String>();
      final static int queueSize = 10;
      
      public static void main(String[] args){
          Thread producer = new Thread(new Runnable() {
              public void run(){
                  //获取独占锁
                  lock.lock();
                  try{
                      
                      //(1)如果队列满了,则等待
                      while(queue.size() == queueSize){
                          notEmpty.await();
                      }
                      
                      //(2) 添加元素到队列
                      queue.add("ele");
                      
                      //(3)唤醒消费线程
                      notFull.signalAll();
                  }catch(Exception e){
                      e.printStackTrace();
                  }finally{
                      //释放锁
                      lock.unlock();
                  }
              }
          });
          
          Thread consumer = new Thread(new Runnable(){
             public void run(){
                 //获取独占锁
                 lock.lock();
                 try{
                     //队列空,则等待
                     while(0 == queue.size()){
                         notFull.await();
                     }
                     
                     //消费一个元素
                     String ele = queue.poll();
                     //唤醒生产线程
                     notEmpty.signalAll();
                        
                 }catch(Exception e){
                     e.printStackTrace();
                 }finally{
                     //释放锁
                     lock.unlock();
                 }
             } 
          });
          
          //启动线程
          prducer.start();
          consumer.start();
      }
      

      如上代码首先创建了NonReentrantLock的一个对象lock,然后调用lock.newCondition创建了两个条件变量,用来进行生产者和消费者线程之间的同步。
      在main函数里面,首先创建了producer生产线程,在线程内部首先调用lock.lock()获取独占锁,然后判断当前队列是否已经满了,如果满了则调用notEmpty.await()阻塞挂起当前线程。需要注意的是,这里使用while而不是if是为了避免虚假唤醒。如果队列不满则直接向队列里面添加元素,然后调用notFull.signalAll()唤醒所有因为消费元素而被阻塞的消费线程,最后释放获取的锁。
      然后在main函数里面创建了consumer生产线程,在线程内部首先调用lock.lock()获取独占锁,然后判断当前队列里面是不是有元素,如果队列为空则调用notFull.await()阻塞挂起当前线程。需要注意的是,这里使用while而不是if是为了避免虚假唤醒。如果队列不为空则直接从队列里面获取并移除元素,然后唤醒因为队列满而被阻塞的生产线程,最后释放获取的锁。

3. 独占锁ReentrantLock的原理

  • 类图结构

    image-20220807114729869

ReentrantLock最终还是使用AQS来实现的,并且根据参数来决定其内部是公平锁还是非公平锁。

public ReentrantLock(){
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair){
    sync = fair ? new FairSync():new NonfairSync();
}

在这里,AQS的state状态值表示线程获取该锁的可重入次数。

  • 获取锁

    • void lock()方法

      若锁当前未被其他线程占用且未获取过该锁,则当前线程获取到该锁,并设置AQS的状态值为1,然后返回,若获取过该锁,则简单地把AQS的状态值加1后返回。若锁被占用,则该线程会被放入AQS对列后阻塞挂起。

      //ReentrantLock的lock()委托给了sync类
      public void lock(){
          sync.lock();
      }
      
      final void lock(){
          //(1)CAS设置状态值
          if(compareAndSetAtate(0,1))
              setExclusiveOwnerThread(Thread.currentThread());
          else
              //调用AQS的acquire方法
              acquire(1);
      }
      
      public final void acquire(int arg){
          if(!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE),arg))
              selfInterrupt();
      }
      
    • void lockInerruptibly()方法

      该方法于lock方法类似,它的不同在于,它对中断进行响应,就是当前线程在调用该方法时,如果其他线程调用了当前线程的interrupt()方法,则当前线程会抛出InterruptedException异常,然后返回。

    • boolean tryLock()方法

      尝试获取锁,如果当前该锁没有被其他线程持有,则当前线程获取锁并返回true,否则返回false。注意,该方法不会引起当前线程阻塞。

    • boolean tryLock(long timeout,TimeUnit unit)方法

      尝试获取锁,与tryLock()的不同之处在于,它设置了超时时间,如果超时时间到没有获取到该锁则返回false。

  • 释放锁

    • void unlock()方法

      尝试释放锁,如果当前线程持有该锁,则调用该方法会让该线程对该线程持有的AQS状态值减1,如果减去1后当前状态值为0,则当前线程会释放该锁,否则仅仅减1而已。如果当前线程没有持有该锁而调用了该方法则会抛出IllegalMonitorStateException异常。

  • 案例介绍

    使用ReentrantLock来实现一个简单的线程安全的list。

    public static class RentrantLockList{
        
        //线程不安全的list
        private ArrayList<String> array = new ArrayList<String>();
        //独占锁
        private volatile ReentrantLock lock = new ReentrantLock();
        
        //添加元素
        public void add(String e){
            lock.lock();
            try{
                array.add(e);
            }finally{
                lock.unlock();
            }
        }
        
        //删除元素
        public void remove(String e){
            lock.lock();
            try{
                array.remove(e);
            }finally{
                lock.unlock();
            }
        }
        
        //获取数据
        public String get(int index){
            lock.lock();
            try{
                return array.get(index);
            }finally{
                lock.unlock();
            }
        }
    }
    

4. 读写锁ReentrantReadWriteLock的原理

ReentrantReadWriteLock采用读写分离的策略,允许多个线程可以同时获取读锁。

读写锁的内部维护了一个ReadLock和一个WriteLock,它们依赖Sync实现具体功能。而Sync继承自AQS,并且也提供了公平和非公平的实现。

ReentrantReadWriteLock巧妙地使用state的高16位表示读状态,也就是获取到读锁的次数;使用低16位表示获取到写锁的线程的可重入次数。

  • 写锁的获取与释放

    • void lock()

      写锁独占,且可重入。

      在lock()内部调用了AQS的acquire方法,其中tryAcquire是ReentrantReadWriteLock内部的sync类重写的。

    • void lockInterruptibly()

      会对中断进行响应

    • boolean tryLock()

      尝试获取写锁

    • boolean tryLock(long timeout,TimeUnit unit)

    • void unlock()

  • 读锁的获取与释放

    ReentrantReadWriteLock中的读锁是使用ReadLock来实现的。

    • void lock

      获取读锁,如果当前没有其他线程持有写锁,则当前线程可以获取读锁,AQS的状态值state的高16位的值会增加1,然后方法返回。否则如果其他一个线程持有写锁,则当前线程会被阻塞。

    • void lockInterruptibly()

    • boolean tryLock

    • boolean tryLock(long timeout,TimeUnit unit)

    • void unlock()

  • 案例介绍

    上节介绍了如何使用ReentrantLock实现线程安全的list,但是由于ReentrantLock是独占锁,所以在读多写少的情况下性能很差。下面使用ReentrantReadWriteLock来改造它,代码如下。

    public static class RentrantLockList{
        
        //线程不安全的list
        private ArrayList<String> array = new ArrayList<String>();
        //独占锁
        private final ReentrantReadWirteLock lock = new ReentrantReadWriteLock();
        private final Lock readLock = lock.readLock();
        private final Lock writeLock = lock.writeLock();
        
        //添加元素
        public void add(String e){
            writeLock.lock();
            try{
                array.add(e);
            }finally{
                writeLock.unlock();
            }
        }
        
        //删除元素
        public void remove(String e){
            writeLock.lock();
            try{
                array.remove(e);
            }finally{
                writeLock.unlock();
            }
        }
        
        //获取数据
        public String get(int index){
            readLock.lock();
            try{
                return array.get(index);
            }finally{
                readLock.unlock();
            }
        }
    }
    

5. JDK 8中新增的StampedLock锁

  • 概述

    StampedLock是并发包里面JDK8版本新增的一个锁,该锁提供了三种模式的读写控制,当调用获取锁的系列函数时,会返回一个long型的变量,我们称之为戳记(stamp),这个戳记代表了锁的状态。其中try系列获取锁的函数,当获取锁失败后会返回为0的stamp值。当调用释放锁和转换锁的方法时需要传入获取锁时返回的stamp值。

    • 写锁writeLock

      独占锁,不可重入。

    • 悲观读锁readLock

      共享锁

    • 乐观读锁tryOptimisticRead

      它是相对于悲观锁来说的,在操作数据前并没有通过CAS设置锁的状态,仅仅通过位运算测试。如果当前没有线程持有写锁,则简单地返回一个非0的stamp版本信息。

  • 小结

    StampedLock提供的读写锁与ReentrantReadWriteLock类似,只是前者提供的是不可重入锁。但是前者通过提供乐观读锁在多线程多读的情况下提供了更好的性能,这是因为获取乐观读锁时不需要进行CAS操作设置锁的状态,而只是简单地测试状态。

posted on 2022-08-07 14:50  小迟在努力  阅读(77)  评论(0)    收藏  举报