• java并发包的基石:AbstractQueuedSychronier及synchornized


    java并发包的基石:AbstractQueuedSychronier

    简介

    AQS: AbstractQueuedSychronizer(抽象的队列同步器)是java的j.u.c包中Lock、Semaphore、ReentrantLock等这些锁都是基于AQS框架实现的。

    AQS有两种模式:
    1. 独占模式ReentrantLock 一次只有一个线程可以竞争到锁
    2. 共享模式 CountDownLatch 一次可以多个线程获取到资源

    核心

    AQS核心数据结构:

     // CAS: compare and set 共享资源通过cas操作来保证并发修改的正确性
     // state有两种共享方式:1. exclusive独占锁 ReentrantLock 2.share共享锁 CountDownLatch
     volatile int state;
     // 同步队列
     FIFO CHL;
     // 当前独占锁占有的线程
     Thread exclusiveOwnerThread;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    自定义同步器

    不同的自定义同步器竞争共享资源的方式也不同,自定义同步器在实现时只需要实现共享资源state的获取和释放即可,至于具体线程等待队列的维护(如获取资源失败入队,唤醒出队等), AQS已经在顶层实现好了,自定义同步器实现时主要实现以下几种方法:

    // 该线程是否正在独占资源
    isHeldExclusively();
    // 独占方式: 尝试获取资源,成功返回true,失败返回false
    tryAcquire(int);
    // 独占方式,释放资源,成功返回true,失败返回false
    tryRelease();
    // 共享方式,尝试获取资源,负数表示失败,0表示成功,但是没有剩余资源;正数表示成功,且有剩余资源
    tryAcquireShared(int);
    // 共享方式,尝试释放资源,如果释放后允许唤醒后续等待节点返回true,否则返回false
    tryReleaseShared(int)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    ReentrantLock

    ReentrantLock reentrantLock = new ReentrantLock();
    //加锁
    reentrantLock.lock();
    //释放锁
    reentrantLock.unlock();
    
    • 1
    • 2
    • 3
    • 4
    • 5

    加锁

    lock & unlock
    public void lock() {
     sync.lock();
    }
    
    final void lock() {
     acquire(1);
    }
    
    public final void acquire(int arg) {
     // tryAcquire获取到锁 直接返回
     // tryAcquire没有获取到锁,acquireQueued当前线程加入等待队列
     if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {}
           selfInterrupt();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    tryAcquire

    fairSync 公平锁

    protected final boolean tryAcquire(int acquires) {
     final Thread current = Thread.currentThread();
     int c = getState();
     if (c == 0) {
     	//  当前没有线程获取到锁
     	// cas compare and set 
       	if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)){
    		//  设置当前线程获取到锁
    		setExclusiveOwnerThread(current);
    		return true;
    	}
     }
     //  可重入锁,当前线程再次获取到锁
     else if (current == getExclusiveOwnerThread()) {
     	 if nextc = c  + acquires;
     	 if (nextc < 0) throw new Error("Maximum lock count exceeded");
     	 	// 设置 state值,为当前重入次数
     	 	setState(nextc);
     	 	return true;
     }
     // 无法获取到锁
     return false;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    acquireQueued
    final boolean acquireQueued(final Node node, int arg) {
    // 标记是否获取到锁资源
    boolean failed = true;
    try {
    	// 标记等待过程中是否被中断
    	boolean interrupted = false;
    	//自旋锁
    	for (;;) {
    		// 当前尝试获取锁的节点的前一个节点
    		final Node p = node.predecessor();
    		// 如果当前进程时队列中的老二,那么便可尝试获取锁(等待第一个线程释放锁就可以获取到锁了)
    		if (p == head && tryAcquire(arg)) {
    			// 可以获取到锁,将head指向该节点
    			setHead(node);
    			p.next = null;
    			// 表示成功获取到锁了
    			failed = false;
    			//返回等待过程中是否被中断过
    			return interrupted;
    		}
    		// 是否可以被挂起,挂起后等待唤醒
    		if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())			   
    		{
    		  // 有被中断过
    		  interrupted = true;
    		}
    	}
    
    } finally{
    	// 自旋锁没有获取到锁,可能是超时中断,那么可以取消节点在队列中的等待
    	if (failed) {
    		cancelAcquird(node);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    isHeldExclusively
    //该线程是否正在独占锁
    protected final boolean isHeldExclusively(){
    	return getExclusiveOwnerThread() == Thread.currentThrea();
    }
    
    class AbstractOwnableSynchronizer{
    	private transient Thread exclusiveOwnerThread;
    	
    	// 获取当前获取锁的线程
    	protected final Thread getExclusiveOwnerThread(){
    		return exclusiveOwnerThread;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    释放锁

    public void unlock() {
    	sync.release(1);
    }
    
    public final boolean release(int arg) {
    	if (tryRelease(arg)) {
    		// 表示锁已经完全释放
    		Node h = head;
    		if (h != null && h.waitStatus != 0) 
    			unparkSuccessor(h);
    		return true;
    	}
    	return false;
    }
    
    protected final boolean tryRelease(int release) {
    	int c = getState() - releases;
    	// 当前线程是不是获取到锁的线程 
    	if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitirStateException();
    	boolean free = false;
    	// state等于0比表示占领锁的线程已经全部(重入锁)被释放了
    	if (c == 0) {
    		free = true;
    		//exclusiveOwnerThread 设置为null
    		setExclusiveOwnerThread(null);		
    	}
    	setState(c);
    	return free;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    java中的一些锁

    synchornized

    synchornized关键字是同步锁,可以加在对象、类、方法、代码块,静态方法;Sychornized是可重入的锁。

    对象锁: 对象、方法、代码块
    类锁:类、静态方法

    类锁
    public Test{
    	public void demo() {
    		// 同一个jvm只有一个线程某刻可以获取到锁
    		sychornized(Test.class) {
    			// do something
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    静态方法
    public Test {
    	// 同一个jvm只有一个线程某刻可以获取到锁
    	public static sychornized void demo() {
    		// do somethig
    	}
    	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    对象
    public Test {
    	public void demo() {
    		// 同一个Test实例只有一个线程可以获取到锁
    		sychornized(this) {
    			// do something
    		}
    	}
    }
    
    pulic void demo1() {
    	Test test1 = new Test();
    	Test test2 = new Test();
    	// test1与test2可以同时获的锁,因为他们获得是两把锁
    	test1.demo();
    	test2.demo();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    方法
    	pubic Test{
    		//  同一个Test实例只有一个线程某刻可以获取到锁
    		public sychornized void demo(){
    			// de something
    		}
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    代码块
     public Test{
    	// 同一个Test实例只有一个线程某刻可以获取到锁
    	pulic void demo(Test test){
    		sychornized(test) {
    		 // do something
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    原理

    当代码块使用sychornized编译后会在代码中加入监视器Monitor。

    编译后的代码:

    monitorenter
    // 代码块编译后额命令
    monitorexit
    
    • 1
    • 2
    • 3

    当方法声明使用sychornized时,方法编译后会设置ACC_SYNCRONIZED来标识;
    当方法调用时,设置指针将会检查方法的ACC_SYNCRONIZED是否被设置了,如果被设置了,执行线程将先持有monitor,然后在执行方法,最后方法完成时释放 monitor。

    sychornized是可重入的锁,因此当持有的锁再一次获取到锁,monitor的计数器仍然会加1.每一次释放monitor计数器会减一。

    sychornized的锁升级过程: 无锁偏向锁-------> 轻量级锁(自旋锁) ---------> 重量级锁

    偏向锁:实际无竞争,只有一个线程在获取锁,在对象的mark word通过cas记录owner为当前线程,owner的值一定会从null到有值的。

    轻量级锁:自旋的获取锁,不需要进行线程切换,适合持有锁时间短,竞争不激烈,也是通过cas设置owner的值,多个线程交替短时间内持有锁

    重量级锁: 当自旋一段时间,无法获取锁,轻量级锁升级为重量级锁,重量级锁通过 monitor监视器实现。

    从这里看起来sychornized重入的实现和AQS的state思想是一致的。

    sychornized和ReentranLock的区别

    实现上: sychornized是java的关键字,有jvm实现,ReentranLock是API层面提供的。

    使用上:sychornized有编译器保证锁的加锁和释放;ReentranLock需要手工声明来加锁和释放锁,忘记释放锁会造成死锁。

    性能:sychornized优化后移入了偏向锁、轻量级锁(自旋锁)后两者性能差不多,官方建议如果使用场景一致,建议使用sychornized。

    公平非公平锁:sychornized是非公平的,ReentranLock默认是非公平锁,可以设置为公平锁,构造方法传true。

    功能区别上:

    ReentranLock支持:

    1. 等待可中断 持有锁的线程长时间不释放的时候,正在等待的锁可以选择放弃等待,lockInterruptibly方法

    2. 公平锁, ReentranLock可以实现公平锁

    3. 锁定多个条件 ReentranLock提供了一个条件类Condition,用来实现多个对象分组唤醒线程;sychornized只能要么随机唤醒一个线程要么全部唤醒

    两者的实现:
    sychornized实现: 重量级锁通过monitor entry 和 monitor exist来加锁代码,偏向锁和轻量级锁通过mark word对owner字段cas设置。

    ReentranLock通过AQS实现: state CHL 当前持有锁的线程。

    如果选择:
    当要使用ReentranLock的三个特性: 等待可中断、公平锁、多条件则选择ReentranLock,否则用sychronized

    死锁

    两辆车在单行道桥的两端,如果一方不让出路来,那么两俩车都过不了,这就是死锁。

    innodb处理死锁的方式: 将持有最少行派他锁的事务进行回滚。
    我们在实际开发中,避免死锁:大事务业务上允许插成小事务就插成是小事务。

    活锁

    两辆这在单行道桥的两端,两方都在给对方让路,那么两辆车都过不去,这就是活锁。

    公平与非公平锁

    公平锁与非公平锁在AQS中的实现:

    1. 公平锁:当线程想要获取锁,直接插入到CHL队列的队尾,当前线程释放了锁state等于0,则从队列队首获得锁,队列从队首到队尾依次获得。
    2. 当线程想要获取锁,参与竞争锁,如果竞争到则获的锁,没有竞争到,插到CHL队列队尾,后面的和公平锁一样。

    也就是公平锁与非公平锁的区别是: 当线程获取锁是直接参与竞争还是不参与直接插入队尾。

    重入与不可重入

    可重入锁的实现:当获取state不等于0,以及持有锁的线程等于当前线程则state加1,释放的时候state减1,直到为0。

    可重入锁:同一个线程可以多次获取同一把锁

    不可重入锁;同一个线程如果要再次获取锁,必须等待之前获取到的锁释放,不可重入锁很容易引入死锁。

    自旋锁

    一个线程尝试获取锁,获取不到,不会立即阻塞,而是采用循环的方式尝试获取,acquiedQueued方法中的for(;;)就是在自旋,自旋旋可以减少线程切换的上下文开销,但是如果自旋时间过长会非常耗费cpu的性能。

    乐观与悲观锁

    悲观锁

    在操作资源前先加锁,例如数据库中的行锁,一个线程读取到准备操作资源,其他线程只能阻塞等待了。

    悲观锁认为别人在操作资源的时候会对资源进行修改,所以在它持有资源的时候,会阻塞其他线程。

    select col from table where col1 > 1 for update
    
    • 1
    乐观锁

    乐观锁通过版本号来实现对资源的修改,当版本号不对则重新读取再次修改,不会阻塞其他线程。

    如线程A,B同时读取到资源id = 2, data = 1, 他们都做加一操作:
    则:
    A: update set data = 2 , version = version +1 where id = 2 and version =1 成功
    B: update set data =2, version =version +1 where id = 2 and verison =1 失败

    CAS: compare and set.也是乐观锁,因为CAS没有版本号所以容易发生ABA问题。

    参考

    从ReenttrantLock的实现看AQS的原理及应用

    java并发之AQS详解

    java常见锁类型

  • 相关阅读:
    【unity插件】Shader实现UGUI的特效——UIEffect为 Unity UI 提供视觉效果组件
    使用非递归的方式实现归并排序
    Markdown还能这么玩?这款开源神器绝了!
    测试资深人士推荐的GUI跨平台自动化测试工具
    Shopee买家通系统之如何注册虾皮账号
    工厂模式(Factory Pattern) 与抽象工厂模式(Abstract Factory Pattern)
    基础:JavaScript的怪癖之一:提升(Hoisting)
    【SLAM】lidar-camera外参标定(港大MarsLab)无需二维码标定板
    [Android开发学iOS系列] Auto Layout
    Overleaf论文排版踩坑记录
  • 原文地址:https://blog.csdn.net/u013565163/article/details/126766741