16.Java_Lock体系

一、Lock简介(JDK1.5提供的)

锁:控制多个线程访问共享资源。

  • 在Lock接口出现之前,java程序靠synchronized关键字实现锁功能。
  • Lock接口失去了像synchronized关键字隐式加锁解锁的便捷性,但拥有锁获取和释放的可操作性、可中断的获取锁、超时获取锁等同步特性。
  • synchronized同步块执行完或遇到异常时锁会自动释放,Lock必须调用unlock()释放锁。
Lock lock = new ReetrantLock();
try{
	lock.lock();
	//以下代码只有一个线程可以运行
	...
}finally{
	lock.unlock();//显式解锁
}

二、lock常见API

lock体系拥有可中断获取锁、超时获取锁以及共享锁等特性。

  • void lock();//获取锁
  • void lockInterruptibly() throws InterruptedException();//响应中断锁
  • boolean trylock();//获取锁返回true,反之返回false
  • boolean trylock(long time,TimeUnit unit);//超时获取锁,在规定时间内未获取到锁返回false
  • Condition newCondition();//获取与lock绑定的等待通知组件
  • void unlock();释放锁

Lock接口的实现子类ReetrantLock中所有的方法实际上都是调用了其静态内部类Sync中的方法,而Sync继承了AbstractQueuedSynchronizer(AQS-简称同步器)

自定义类 implements Lock{
	lock();
	unlock();

	static class Sync extends AbstractQueuedSynchronzer{

	}
}

三、AQS—同步器

定义:用来构建锁以及其他同步组件的基础框架,他的实现主要是依赖一个int状态变量以及通过一个FIFO队列构成同步队列。

  • 子类必须重写AQS的protected修饰的用来改变同步状态的方法。其他方法主要是实现了排队与阻塞机制。int状态的更新使用getState()、setState()以及compareAndSetState()。
  • 子类推荐使用静态内部类来继续AQS实现自己的同步语义。同步器既支持独占锁,也支持共享锁。

锁与AQS的关系:

  • 锁面对使用者,定义了使用者与锁交互的接口。
  • 同步器面向锁的实现,简化了锁的实现方式,屏蔽同步状态管理,线程排队、等待、唤醒等操作。

四、AQS的模板模式

定义:AQS使用模板方法模式,将一些与状态相关的核心方法开放给子类重写,而后AQS会使用子类重写的关于状态的方法进行线程的排队、阻塞以及唤醒等操作。

  • AQS采用模板方法进行设计,AQS的protected修饰的方法需要由继承AQS的子类进行重写实现,当调用AQS的子类的方法时就会调用被重写的方法。

五、AQS详解

作用:在同步组件中,AQS是最核心的部分,同步组件的实现依赖AQS提供的模板方法来实现同步组件语义。

  • AQS实现了对同步状态的管理,以及对阻塞线程进行排队、等待通知等底层实现。
  • AQS核心组成:同步队列、独占锁的获取与释放、共享锁的获取与释放、可中断锁、超时锁。这一系列功能的实现依赖于AQS提供的模板方法。
1、独占锁

(1)void acquire(int arg);
独占式获取同步状态,如果获取失败插入同步队列进行等待。
(2)void acquireInterruptibly(int arg);
在(1)的基础上,此方法可以在同步队列中响应中断。
(3)boolean tryAcquireNanos(int arg,long nanosTimeOut);
在(2)的基础上增加了超时等待功能,到了预计时间还未获得锁,直接返回。
(4)boolean tryAcquire(int arg);
互殴锁成功返回true,否则返回false。
(5)boolean release(int arg);
释放同步状态,此方法会唤醒在同步队列的下一个节点。

2、共享式锁

(1)void acquireShared(int arg);
共享获得同步状态,同一时刻多个线程获取同步状态。
(2)void acquireSharedInterruptibly(int arg);
在(1)的基础上增加响应中断。
(3)boolean tryAcquireSharedNanos(int arg,long nanosTimeOut);
在(2)的基础上增加超时等待。
(4)boolean releaseShared(int arg);
共享式释放同步状态。

3、同步队列

在AQS内部有一个静态内部类Node,这是同步队列中每个具体的结点。

节点中有如下属性:

  • int waitStatus:节点状态
  • Node prev:同步队列中前驱节点
  • Node next:同步队列中后继节点
  • Thread thread:当前结点包装的线程对象
  • Node nextWaiter:等待队列中下一个节点

节点状态值如下:

  • int INITIAL = 0;//初始状态
  • int CANCELLED = 1;//当前结点从同步队列中取消
  • int SIGNAL = -1;//后继节点处于等待状态。如果当前结点释放同步状态会通知后继节点,使后继节点继续运行
  • int CONDITION = -2;//结点处于等待队列中。当其他线程对Condition调用signal()方法后,该结点会从等待队列移到同步队列中。
  • int PROPAGATE = -3;//共享式同步状态会无条件的传播

AQS同步队列采用带有头尾结点的双向链表。

六、独占锁的获取:acquire(int arg)

获取锁失败后调用AQS提供的acquire(int arg)模板方法

  • tryAcquire(arg):再次尝试获取同步状态,成功直接方法退出,失败调用addWaiter();
  • addWaiter(Node.EXCLUSIVE),arg):将当前线程以指定模式(独占式、共享式)封装为Node节点后尾插置入同步队列。
private Node addWaiter(Node node){
	Node node = new Node(Thread.currentThread(),node);
	Node pred = tail;
	if(perd != null){
		node.prev = pred;
		if(compareAndSetTail(pred,node)){
			pred.next = node;
			return node;
		}
	}
	enq(node);
	return node;
}
  • enq(Node node):当前队列为空或者CAS尾插失败,调用此方法来初始化队列或不断尾插。
private Node enq(final Node node) { 
   for (;;) {
   		Node t = tail;     
        if (t == null) { // Must initialize            
        // 头结点初始化 
        if (compareAndSetHead(new Node()))
            tail = head;        
        } else {            
          	node.prev = t;            // CAS尾插,失败进行自旋重试直到成功为止。            
          	if (compareAndSetTail(t, node)) {                
          		t.next = node;                
          		return t;            
          	}        
         }    
     } 
 }
  • acquireQueued()获取锁成功条件:结点入队后排队获取同步状态。
    当前节点前驱为头结点,并且再次获取同步状态成功。
  • 结点在同步队列中获取锁,失败后调用shouldParkAfterFailedAcquire(Node prev,Node node)
    此方法主要逻辑是
    使用CAS将前驱节点状态置为SIGNAL,表示需要将当前结点阻塞。
    如果CAS失败,不断自旋直到前驱节点状态置为SIGNAL为止。
  • acquireQueued();
    1.如果当前节点的前驱节点为头结点,并且能够成功获取同步状态,当前线程获取锁成功,方法退出。
    2.如果获取锁失败,先不断自旋将前驱节点状态置为SIGINAL,而后调用LockSupport.park()方法将当前线程阻塞。
  • 结点获取到同步状态的前置条件:
    当前节点的前驱节点为头结点,并且调用tryAcquire获取到了同步状态->前驱头节点出队

独占锁的释放:release()

unlock()方法实际调用AQS提供的release()模板方法

  • release()方法是unlock()方法的具体实现。首先获取头结点的后继节点,当后继节点不为null,会调用LockSupport.unpark()方法唤醒后继节点包装的过程。因此,每一次锁释放后就会唤醒队列中该节点的后继节点所包装的线程。

独占式锁获取与释放总结:

1.线程获取锁失败,将线程调用addWiter()封装成Node进行入队操作。addWriter()中方法enq()完成对同步队列的头结点初始化以及CAS尾插失败后的重试处理。
2.入队后排队获取锁的核心方法acquireQueued(),结点排队获取锁是一个自旋过程。当且仅当,当前节点的前驱节点为头结点并且成功获取同步状态时,节点出队并且该节点引用的线程获取到锁。否则,不满足条件时会不断自旋将前驱节点的状态置为SIGNAL而后调用LockSupport.park()将当前线程阻塞。
3.释放锁时会唤醒后继节点(后继节点不为null)。

七、独占锁的特性

1.获取锁时响应中断:

原理与acquire()几乎一样,唯一区别在于当parkAndCheckInterrupt()返回true时表示线程阻塞时被中断,抛出中断异常后线程退出。

2.超时等待获取锁:

tryAcquireNanos(),该方法在三种情况下会返回:
在超时时间内,当前线程成功获取到锁。
当前线程在超时时间内被中断
超时时间结束,仍未获取到锁,线程退出返回false。

总结超时获取锁逻辑与可中断获取锁基本一致。唯一区别在于获取锁失败后,增加了一个时间处理。如果当前时间超过截止时间,线程不再等待,直接退出,返回false。否则将线程阻塞置为等待状态排队获取锁。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值