并发编程之AQS锁

一、多线程问题

并发编程之AQS锁_第1张图片

产生多线程问题主要有以下几点:

1.多线程环境

2.有临界资源

3.有多个线程在同一时刻操作临界资源

具体产生的问题:

1.可见性问题(volatile:被volatile修饰的共享数据会导致变量副本每次访问时强制清空!从而保证每次访问的都是主内存中的最新值!)

2.原子性问题(CAS、加锁) 

3.指令重排

二、ReentrantLock 

1. 简介

       ReentrantLock是一种基于AQS框架的应用实现,是JDK中的一种线程并发访问的同步
手段,它的功能类似于synchronized是一种互斥锁,可以保证线程安全。而且它具有比
synchronized更多的特性,比如它支持手动加锁与解锁,支持加锁的公平性。

2. 特点

2.1 可重入性

ReentrantLock允许线程多次获取同一把锁而不会造成死锁。内部通过计数器记录线程获取锁的次数,当计数器归零时,锁才会被释放。

2.2 显式锁定

synchronized隐式获取锁不同,使用ReentrantLock需要显式地调用lock()方法来获取锁,并在完成操作后调用unlock()方法来释放锁。

2.3 公平锁

ReentrantLock可以设置为公平锁,即锁倾向于将锁赋予等待时间最长的线程。默认情况下,ReentrantLock是非公平的,以提供更高的吞吐量。

ReentrantLock如何实现synchronized不具备的公平与非公平性呢?
在ReentrantLock内部定义了一个Sync的内部类,该类继承AbstractQueuedSynchronized,对
该抽象类的部分方法做了实现;并且还定义了两个子类:
1、FairSync 公平锁的实现
2、NonfairSync 非公平锁的实现
这两个类都继承自Sync,也就是间接继承了AbstractQueuedSynchronized,所以这一个
ReentrantLock同时具备公平与非公平特性。
上面主要涉及的设计模式:模板模式-子类根据需要做具体业务实现

2.4 可中断的锁获取

ReentrantLock提供了lockInterruptibly()方法,允许在等待锁的过程中响应中断。

2.5 尝试非阻塞地获取锁

ReentrantLock提供了tryLock()方法,尝试立即获取锁,如果锁可用则返回true,否则返回false。

2.6 条件变量

ReentrantLockCondition对象配合使用,提供类似Object.wait()Object.notify()的等待/通知机制,但提供了更灵活的用法。

3. 使用方法

3.1 创建ReentrantLock实例

	import java.util.concurrent.locks.ReentrantLock;
 
public class Counter {
  private final ReentrantLock lock = new ReentrantLock();
  // ...
}

3.2 获取和释放锁

	public void increment() {
  lock.lock(); // 获取锁
  try {
      // 执行临界区代码
      count++;
  } finally {
      lock.unlock(); // 释放锁
  }
}

3.3 使用公平锁

private final ReentrantLock lock = new ReentrantLock(true); // 创建公平锁

3.4 可中断的锁获取

	public void increment() throws InterruptedException {
  lock.lockInterruptibly(); // 可中断的锁获取
  try {
      count++;
  } finally {
      lock.unlock();
  }
}

3.5 尝试非阻塞地获取锁

	public boolean tryIncrement() {
  if (lock.tryLock()) { // 尝试获取锁
      try {
          count++;
          return true;
      } finally {
          lock.unlock(); // 释放锁
      }
  }
  return false;
}

3.6 使用条件变量

	public class BoundedBuffer {
  private final ReentrantLock lock = new ReentrantLock();
  private final Condition notFull = lock.newCondition();
  private final Condition notEmpty = lock.newCondition();
 
  // 入队操作
  public void put(Object item) throws InterruptedException {
      lock.lock();
      try {
          while (isFull()) {
              notFull.await(); // 等待队列不满
          }
          // 入队操作
          notEmpty.signal(); // 通知队列非空
      } finally {
          lock.unlock();
      }
  }
 
  // 出队操作
  public Object take() throws InterruptedException {
      lock.lock();
      try {
          while (isEmpty()) {
              notEmpty.await(); // 等待队列非空
          }
          // 出队操作
          notFull.signal(); // 通知队列不满
      } finally {
          lock.unlock();
      }
  }
}

 三、AQS

AQS具备特性
阻塞等待队列
共享/独占
公平/非公平
可重入
允许中断

ReentrantLock lock = new ReentrantLock(false);//false为非公平锁,true为公平锁
3个线程
T0 T1 T2
lock.lock() //加锁
    while(true){
        if(cas加锁成功){//cas->比较与交换compare and swap,
            break;跳出循环
        }
        //Thread.yeild()//让出CPU使用权
        //Thread.sleep(1);
        HashSet,LikedQueued(),
        HashSet.add(Thread)
            LikedQueued.put(Thread)
        阻塞。
        LockSupport.park();
        
        
    }
    
    T0获取锁
    xxxx业务逻辑
    xxxxx业务逻辑
    
lock.unlock() //解锁
Thread  t= HashSet.get()
Thread  t = LikedQueued.take();
LockSupport.unpark(t);

Lock,公平与公平两种特性

三大核心原理

自旋,LocksSuport, CAS,queue队列

CAS依赖汇编指令:cmpxchg()

Lock可重入性:可重入!

synchronized:可重入

公平

exclusiveOwnerThread 当前获取锁的线程是谁!
state 状态器

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

tryAcquire(arg) //锁竞争逻辑
//CLH 
addWaiter(Node.EXCLUSIVE) //线程入队,Node节点,Node对Thread引用
    Node:共享属性,独占属性 //响应式编程,异步非阻塞,FutureTask,Callbale
    创建节点Node = pre,next,waitestate,thread 重要属性
    waitestate节点的生命状态:信号量
        SIGNAL = -1 //可被唤醒
        CANCELLED = 1 //代表出现异常,中断引起的,需要废弃结束
        CONDITION = -2 // 条件等待
        PROPAGATE = -3 // 传播
        0 - 初始状态Init状态
为了保证所有阻塞线程对象能够被唤醒
compareAndSetTail(t, node) 入队也存在竞争
    
//当前节点,线程要开始阻塞
acquireQueued(Node(currentThread), arg)
    节点阻塞之前还得再尝试一次获取锁:
    1,能够获取到,节点出队,并且把head往后挪一个节点,新的头结点就是当前节点
    2、不能获取到,阻塞等待被唤醒
    	1.首先第1轮循环、修改head的状态,修改成sinal=-1标记处可以被唤醒.
    	2.第2轮循环,阻塞线程,并且需要判断线程是否是有中断信号唤醒的!
    	shouldParkAfterFailedAcquire(p, node)
    waitestate = 0 - > -1 head节点为什么改到-1,因为持有锁的线程T0在释放锁的时候,得判断head节点的waitestate是否!=0,如果!=0成立,会再把waitstate = -1->0,要想唤醒排队的第一个线程T1,T1被唤醒再接着走循环,去抢锁,可能会再失败(在非公平锁场景下),此时可能有线程T3持有了锁!T1可能再次被阻塞,head的节点状态需要再一次经历两轮循环:waitState = 0 -> -1
   Park阻塞线程唤醒有两种方式:
    1、中断
    2、release()

你可能感兴趣的:(java,开发语言,并发)