并发编程(三)AbstractQueuedSynchronizer(AQS)-ReentrantLock可重入、独占锁

并发之父 Doug Lea

AbstractQueuedSynchronizer

Java并发编程核心在于java.concurrent.util(juc)包

juc当中的大多数同步器实现都是围绕着共同的基础行为,比如等待队列、条件队列、独占获取、共享获取等,而这个行为的抽象就是基于AbstractQueuedSynchronizer简称AQS,AQS定义了一套多线程访问共享资源的同步器框架,是一个依赖状态(state)的同步器(state:记录当前是否加锁,加锁的次数等等,引入可重入的功能 )

  • 等待队列,条件队列:公平与非公平的特性
  • 独占获取:排他锁
  • 共享获取:共享锁

AQS中用到了大量的循环

AQS具备特性:

  • 阻塞等待队列
  • 共享/独占
  • 公平/非公平
  • 可重入
  • 允许中断

并发编程的实现

例如Java.concurrent.util当中同步器的实现如Lock,Latch,Barrier等,都是基 于AQS框架实现一般通过定义内部类Sync继承AQS

  • 一般通过定义内部类Sync继承AQS
  • 将同步器所有调用都映射到Sync对应的方法
  • 内部一般定义一个state 变量
    在这里插入图片描述

AQS框架-管理状态

  • AQS内部维护属性volatile int state (32位)
    • state表示资源的可用状态
  • State三种访问方式
    • getState()、setState()、compareAndSetState()
  • AQS定义两种资源共享方式
    • Exclusive-独占,只有一个线程能执行,如ReentrantLock(悲观锁:除非线程1全部运行完后才会释放锁,否则其他线程无法拿到锁。可重入,公平与非公平特征 )
    • Share-共享,多个线程可以同时执行,如Semaphore/CountDownLatch
  • AQS定义两种队列
    • 同步等待队列:获取锁失败会进入此队列
    • 条件等待队列

在这里插入图片描述

Node

static final class Node {
   /**
    * 标记节点未共享模式
    * */
   static final Node SHARED = new Node();
   /**
    *  标记节点为独占模式
    */
   static final Node EXCLUSIVE = null;

   /**
    * 在同步队列中等待的线程等待超时或者被中断,需要从同步队列中取消等待
    * */
   static final int CANCELLED =  1;
   /**
    *  后继节点的线程处于等待状态,而当前的节点如果释放了同步状态或者被取消,
    *  将会通知后继节点,使后继节点的线程得以运行。
    */
   static final int SIGNAL    = -1;
   /**
    *  节点在等待队列中,节点的线程等待在Condition上,当其他线程对Condition调用了signal()方法后,
    *  该节点会从等待队列中转移到同步队列中,加入到同步状态的获取中
    */
   static final int CONDITION = -2;
   /**
    * 表示下一次共享式同步状态获取将会被无条件地传播下去
    */
   static final int PROPAGATE = -3;

   /**
    * 标记当前节点的信号量状态 (1,0,-1,-2,-3)5种状态
    * 使用CAS更改状态,保证数据原子操作:内存值对比修改方式,内存值与原值是否一致,一致则修改为新值
    * 即被一个线程修改后,状态会立马让其他线程可见。
    */
   volatile int waitStatus;

   /**
    * 前驱节点,当前节点加入到同步队列中被设置
    */
   volatile Node prev;

   /**
    * 后继节点
    */
   volatile Node next;

   /**
    * 节点同步状态的线程
    */
   volatile Thread thread;

   /**
    * 等待队列中的后继节点,如果当前节点是共享的,那么这个字段是一个SHARED常量,
    * 也就是说节点类型(独占和共享)和等待队列中的后继节点共用同一个字段。
    */
   Node nextWaiter;

   /**
    * Returns true if node is waiting in shared mode.
    */
   final boolean isShared() {
       return nextWaiter == SHARED;
   }

   /**
    * 返回前驱节点
    */
   final Node predecessor() throws NullPointerException {
       Node p = prev;
       if (p == null)
           throw new NullPointerException();
       else
           return p;
   }

   Node() {    // Used to establish initial head or SHARED marker
   }

   Node(Thread thread, Node mode) {     // Used by addWaiter
       this.nextWaiter = mode;
       this.thread = thread;
   }

   Node(Thread thread, int waitStatus) { // Used by Condition
       this.waitStatus = waitStatus;
       this.thread = thread;
   }
}

同步队列

CLH队列是Craig、Landin、Hagersten三人发明的一种基于双向链表数据结构的队列,是FIFO先入先出线程等待队列,Java中的CLH队列是原CLH队列的一个变种,线程由原自旋机制改为阻塞机制。

内部类中:Node中定义了同步队列的属性,独占模式,还是共享模式

如果Node在条件队列当中,Node必须是独占模式 (例如:阻塞队列BlockingQueue )

不管条件队列还是同步队列都是基于Node来构造的,根据指针记录前后节点是谁
信号量指的是:waitStatus
在这里插入图片描述
根据双向链表前后指针找到前后节点

条件队列

Condition是一个多线程间协调通信的工具类,使得某个,或者某些线程一起等待某个条件(Condition),只有当该条件具备时 ,这些等待线程才会被唤醒,从而重新争夺锁

前后指针都是空值(单项链表)只有nextWaiter中存在值
在这里插入图片描述
单项链表

重入锁

在这里插入图片描述
假设村民排队打水,水桶代表不同的业务。a,b关联度比较高,需要保证a,b都运行完成后才能释放锁

不可重入锁

在这里插入图片描述

公平锁

依次等候
在这里插入图片描述

static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;

    final void lock() {
        acquire(1);
    }
	
    protected final boolean tryAcquire(int acquires) {
    	// 当前线程
        final Thread current = Thread.currentThread();
        // 加锁次数
        int c = getState();
        if (c == 0) {
        	// 判断队列中是否有等待锁的节点 
        	// 通过cas比较更改锁的状态:保证数据原子操作:内存值对比修改方式,内存值与原值是否一致,一致则修改为新值
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                // 修改独占锁的线程为当前线程
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}

非公平锁

在这里插入图片描述

读写锁

  • 写锁(独享锁、排他锁),是指该锁一次只能被一个线程所持有。如果线程T对数据A加上排它锁后,则其他线程不能再对A加任何类型的锁。获得写锁的线程即能读数据又能修改数据。
  • 读锁(共享锁)是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得读锁的线程只能读数据,不能修改数据。

AQS中state字段(int类型,32位),此处state上分别描述读锁和写锁的数量于是将state变量“按位切割”切分成了两个部分

  • 高16位表示读锁状态(读锁个数)
  • 低16位表示写锁状态(写锁个数)
    在这里插入图片描述

ReentrantLock

public class LockTemplete {
    private Integer counter = 0;
    /**
     * 可重入锁,公平锁
     * 公平锁:true
     * 非公平锁:false 如果不指定值则是非公平锁
     * 需要保证多个线程使用的是同一个锁
     *
     * synchronized可重入
     * 虚拟机,在ObjectMonitor.hpp定义了synchronized他怎么取重入加锁 ..。hotspot源码
     * counter +1
     * 基于AQS 去实现加锁与解锁
     */
    private ReentrantLock lock = new ReentrantLock(true);

    /**
     * 需要保证多个线程使用的是同一个ReentrantLock对象
     * @return
     */
    public void modifyResources(String threadName){
        System.out.println("通知《管理员》线程:--->"+threadName+"准备打水");
        //默认创建的是独占锁,排它锁;同一时刻读或者写只允许一个线程获取锁
        lock.lock();
        System.out.println("线程:--->"+threadName+"第一次加锁");
        counter++;
        System.out.println("线程:"+threadName+"打第"+counter+"桶水");
        //重入该锁,我还有一件事情要做,没做完之前不能把锁资源让出去
        lock.lock();
        System.out.println("线程:--->"+threadName+"第二次加锁");
        counter++;
        System.out.println("线程:"+threadName+"打第"+counter+"桶水");
        lock.unlock();
        System.out.println("线程:"+threadName+"释放一个锁");
        lock.unlock();
        System.out.println("线程:"+threadName+"释放一个锁");
    }


    public static void main(String[] args){
        LockTemplete tp = new LockTemplete();

        new Thread(()->{
            String threadName = Thread.currentThread().getName();
            tp.modifyResources(threadName);
        },"Thread:A").start();

        new Thread(()->{
            String threadName = Thread.currentThread().getName();
            tp.modifyResources(threadName);
        },"Thread:B").start();

    }

}

当只有一个线程时head、tail都是空的,多个线程时则如图所示

当运行代码时断点可知
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

ReentrantLock对比Synchronized

  1. 资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态。
  2. synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定,但是使用Lock则不行,lock是通过代码实现的,要确保保证锁定一定会被释放,必须将unLock放到finally{}中
  3. ReentrantLock 拥有Synchronized相同的并发性和内存语义,此外还多了 轮询锁,定时锁等候和中断锁等候
    1. 线程A和B都要获取对象O的锁定,假设A获取了对象O锁,B将等待A释放对O的锁定;
    2. 如果使用 synchronized ,如果A不释放,B将一直等下去,不能被中断;
    3. 如果使用ReentrantLock,如果A不释放,可以使B在等待了足够长的时间以后,中断等待,而干别的事情。

什么时候选择用 ReentrantLock 代替 synchronized

  • 在确实需要一些 synchronized 所没有的特性的时候,比如时间锁等候、可中断锁等候、无块结构锁、多个条件变量或者轮询锁。
  • ReentrantLock 还具有可伸缩性的好处,应当在高度争用的情况下使用它,但是请记住,大多数 synchronized 块几乎从来没有出现过争用,所以可以把高度争用放在一边。
  • 建议用 synchronized 开发,直到确实证明 synchronized 不合适,而不要仅仅是假设如果使用 ReentrantLock 性能会更好。请记住,这些是供高级用户使用的高级工具。(而且,真正的高级用户喜欢选择能够找到的最简单工具,直到他们认为简单的工具不适用为止。)。一如既往,首先要把事情做好,然后再考虑是不是有必要做得更快。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值