并发—多线程进阶

ReentrantLock

1、简介

在 JUC.lock 包下,加锁或解锁的时候,会用到 Sync 这个对象,而这个类继承了AbstractQueuedSynchronizer(AQS)

private final Sync sync;
​

1.2、lock()方法

这个lock()可以是由FairSync去实现,也可以是由NonfairSync。

所以ReentrantLock既可以实现公平锁,也可以实现非公平锁,同时也是互斥锁、可重入锁

public void lock() {
  sync.lock();
}

1.2.1、非公平锁的lock():ReentrantLock下

非公平锁一上来就尝试获取锁资源,获取不到才会去排队。

而公平锁就是一上来就调用 AQS 的 acquire(1)。

通过CAS的方式,尝试将state从0修改成1,若返回true则代表修改成功,

然后将exclusiveOwnerThread设置为当前线程,setExclusiveOwnerThread这个方法的类是AbstractOwnableSynchronizer,它是AQS的父类。

final void lock() {
  if (compareAndSetState(0, 1))
    setExclusiveOwnerThread(Thread.currentThread());
  else
    acquire(1);
}
1.2.1.1、acquire(1)

如果是false,就进入AQS的acquire(1),

第一个判断:tryAcquire(arg)去尝试再次做CAS,如果成功就返回true,然后 ! 一下变false,然后短路,

如果尝试失败,就将当前线程封装成一个Node,追加到AQS的队列中,

addWaiter这个方法无论如何都会让新建一个节点然后将当前线程放进去,最后让其变成AQS中的tail,

如果加列队也失败了,就做线程中断

/**
 * 以独占模式获取,忽略中断。通过至少调用一次tryAcquire来实现,成功时返回。
 * 否则,线程将进入队列,可能会重复阻塞和解除阻塞,调用tryAcquire直到成功。
 * 此方法可用于实现Lock.lock方法。
 */
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
1.2.1.2、tryAcquire(arg)

tryAcquire(arg)是AQS的一个可继承的方法,AQS里面就直接抛出了一个异常,然后具体的实现玩法,需要自己去实现,就比如:ReentrantLock下的:FairSync、NonfairSync,ReentrantReadWriteLock下的:Sync,ThreadPoolExecutor下的:Worker

  • NonfairSync的tryAcquire(arg):ReentrantLock下

 final boolean nonfairTryAcquire(int acquires) {
  // 获取当前线程
  final Thread current = Thread.currentThread();
  // 获取AQS的state的值,即获取锁资源的状态
  int c = getState();
  // 如果state是0,即刚刚被占有的锁现在已经变成空闲状态
  if (c == 0) {
    // 再次CAS尝试获取锁资源
    if (compareAndSetState(0, acquires)) {
        setExclusiveOwnerThread(current);
        return true;
    }
  }
  // 如果锁资源已经被抢占,判断占有锁资源的线程是否是当前线程
  // 即做了一个可重入的操作
  else if (current == getExclusiveOwnerThread()) {
    // 将state+1
    int nextc = c + acquires;
    // 如果+1后小于0,就抛出Error,超出了锁可重入的最大值
    // 可能这个数太大,+1后所有比特位都变成了1,即连符号位都变成了1,就成负数了
    if (nextc < 0)
        throw new Error("Maximum lock count exceeded");
    // 重新对state做赋值
    setState(nextc);
    // 标志锁重入成功
    return true;
  }
  return false;
}
1.2.1.3、addWaiter(Node.EXCLUSIVE)

如果再次尝试失败,就进入AQS下的addWaiter(Node.EXCLUSIVE)。

该方法就是将这个需要去排队的线程封装进 Node,然后保证这个 Node 一定会加入队列

// 前面获取锁资源失败,所以要放到队列中等待
private Node addWaiter(Node mode) {
  // 创建Node类,设置thread为当前线程,mode就是传过来的锁类型,
  // 因为是非公平锁过来的,所以设置为排它锁类型
  Node node = new Node(Thread.currentThread(), mode);
  // 获取AQS的双向队列中的尾部节点
  Node pred = tail;
  // 如果队列中有人
  if (pred != null) {
    // 让自己的前驱节点指向刚刚的尾节点,即想让当前节点变成尾节点
    node.prev = pred;
    // 基于CAS操作,将tail从刚刚的尾节点变成当前节点
    if (compareAndSetTail(pred, node)) {
      pred.next = node;
      return node;
    }
  }
  enq(node);
  return node;
}
1.2.1.4、enq(node)

如果队列中没人或让当前节点变成尾节点失败,则进入AQS下的enq(node)

这个方法就是用死循环保证一定会把 Node 放入队列

private Node enq(final Node node) {
  // 死循环,不断尝试,类似自旋
  for (;;) {
    // 重新获取AQS当前的tail
    Node t = tail;
    // 没人排队
    if (t == null) {
      // 初始化一个Node作为head,而这个head没有意义
      if (compareAndSetHead(new Node()))
        // 使这个head,既是头节点head,又是尾节点tail
        tail = head;
    } 
    // 有人在排队,那么替换尾节点
    else {
      // 让自己的前驱节点指向刚刚的尾节点,即想让当前节点变成尾节点
      node.prev = t;
      // 基于CAS操作,替换tail
      if (compareAndSetTail(t, node)) {
        t.next = node;
        return t;
      }
    }
  }
}
1.2.1.5、acquireQueued

两次CAS修改state失败并且把当前线程放入节点,并且这个node变成tail后,进入AQS下的acquireQueued

就是确保前一个节点的状态是-1,然后将线程阻塞,使其等待唤醒然后获取锁资源

final boolean acquireQueued(final Node node, int arg) {
  // 这个是获取锁资源失败的标识
  boolean failed = true;
  try {
    // 这个是是否打断线程的标识,即使线程阻塞,等待唤醒然后获取锁资源
    boolean interrupted = false;
    // 死循环
    for (;;) {
      // 获取前驱节点,如果没有前驱节点就抛出空指针异常
      final Node p = node.predecessor();
      // 如果上一个节点是head,那说明此节点紧跟着head
      // 那么就再次尝试获取锁资源或做可重入操作
      if (p == head && tryAcquire(arg)) {
        // 拿到锁资源后,设置head为当前节点
        setHead(node);
        // 上一个head的next变成null,帮助GC回收
        p.next = null;
        // 将标识修改为false,即没有失败,成功获取了锁资源
        failed = false;
        // 返回这个标识,即不需要被打断
        return interrupted;
      }
      // 没拿到锁资源后,在保证此节点的上一个节点可用的情况下做循环
      // 确保上一个系欸但的状态是-1,才会返回true
      // parkAndCheckInterrupt()就是将线程挂起阻塞,等待被唤醒。
      // 基于Unsafe的park()方法,挂起线程
      if (shouldParkAfterFailedAcquire(p, node) &&
        parkAndCheckInterrupt())
        interrupted = true;
    }
  } finally {
    if (failed)
      // 取消此节点的排队
      cancelAcquire(node);
  }
}
1.2.1.6、setHead

获取到锁资源后进入AQS下的setHead

因为这个线程已经拿到锁资源了,所以它不用排队了,把这个排队的位置(node)变成head,然后再变成一个无意义的节点。

而真正需要去执行业务的Thread其实已经赋值给了AQS里的exclusiveOwnerThread了,这个线程代表拿到锁资源要执行业务的线程

而之前的head没有引用了,只需要再把它的next指向变为null,就可以被GC回收掉了。

private void setHead(Node node) {
  head = node;
  node.thread = null;
  node.prev = null;
}
1.2.1.7、shouldParkAfterFailedAcquire

如果没获取到锁资源,进入AQS下的shouldParkAfterFailedAcquire

这个方法就是保证前驱节点是正常可用的,即保证该线程被挂起后可正常被唤醒

pred是当前节点的前驱节点,node是当前节点

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
  // 获取上一个节点的状态
  int ws = pred.waitStatus;
  // 如果上一个节点的状态是这个,就代表一切正常
  if (ws == Node.SIGNAL)
    return true;
  // 如果上一个节点的状态的值大于0,就代表上一个节点已经失效了,
  // 即上一个节点已经无法唤醒当前节点了,那么当前节点应该往前找一个有效的节点
  if (ws > 0) {
    do {
      // 例如:A、B、C
      // 上个节点的指向变成上个节点的上一个节点,即让A和B变成同一个node,A、B重合了
      // 然后当前节点C指向的上一个节点就是A了
      // 记住,上一个节点的上一个节点的对象永远只有一个,改变的只是指向
      node.prev = pred = pred.prev;
    } while (pred.waitStatus > 0);
    // 找到的有效的上一个节点的next变成当前节点
    pred.next = node;
  } 
  // 状态值是:小等于0且不等于-1的情况
  else {
    // 基于CAS,将上一个有效节点的状态值变成-1
    compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
  }
  return false;
}
1.2.1.8、cancelAcquire(node)

这个方法的前置判断是 if(failed),即只有当这个线程在走 acquireQueued 方法的时候抛出了异常,才会取消该线程的排队,而死循环中可能抛出异常的语句只有 final Node p = node.predecessor();

和 parkAndCheckInterrupt(),然后前一个基本不可能抛出异常,

所以唯一可能抛出异常的就是 parkAndCheckInterrupt() 这个方法,而这个唯一可能出现异常的地方是仅当 JVM内部出现问题的时候。

所以在 非公平锁的 lock() 方法下, acquireQueued 方法保证了该线程最终一定会获得锁资源,并且不会走 cancelAcquire(node) 这个方法。

而 cancelAcquire(node) 这个方法并不是针对 lock() 这种普通锁的,而是针对 lockInterruptibly() 这类 可能会在获取到锁资源前主动抛出异常 的方法,在这里加上这个方法只是做一个确保。

这个 node 参数是当前想要竞争锁资源的 node

该方法的功能就是:在保证我的前驱节点是有效节点的情况下,把自己的状态变成无效状态,然后在三种情况下做不同操作

private void cancelAcquire(Node node) {
  // 健壮性判断
  if (node == null)
      return;
  
  // 将当前 node 的线程置空,因为该 node的线程放弃排队竞争锁资源了
  node.thread = null;
  // 获取当前节点的前驱节点
  Node pred = node.prev;
  // 前驱节点的状态 > 0,即表示如果前驱节点已经失效
  while (pred.waitStatus > 0)
      // 找到最近的有效前驱节点作为新的前驱节点
      node.prev = pred = pred.prev;
  // 获取到前驱节点的下一个节点
  Node predNext = pred.next;
  // 将当前节点的状态变成失效状态,给别人看的
  node.waitStatus = Node.CANCELLED;
  
  // 如果当前节点是尾节点,就将尾节点变成前驱节点(即刚找到的有效前驱节点)
  if (node == tail && compareAndSetTail(node, pred)) {
      // 用 CAS方式,将刚设置的尾节点的 next即predNext 设为 null
      compareAndSetNext(pred, predNext, null);
  } else {
    int ws;
    // 如果当前节点是中间节点
    // 如果前驱节点不是头节点,并且前驱节点的线程不是空,并且
    // ws获取前驱节点的状态并判断其是否有效,如果不是 -1,那就把它变成 -1,确保有效
    if (pred != head &&
        ((ws = pred.waitStatus) == Node.SIGNAL ||
         (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
        pred.thread != null) {
        // 获取当前节点的后继节点
        Node next = node.next;
        // 尝试将 前驱节点的后继节点 变成 当前节点的后继节点。前提是后继节点有效
        if (next != null && next.waitStatus <= 0)
            compareAndSetNext(pred, predNext, next);
    } else {
        // 当前节点是头节点的操作
        // 唤醒后继节点
        unparkSuccessor(node);
    }
    node.next = node; // help GC
  }
}

代办

public final boolean release(int arg) {    //AQS的锁释放操作
    if (tryRelease(arg)) {   //可以看到这里调用了tryRelease方法,但是此方法并不是在AQS实现的,而是不同的锁自行实现,因为AQS也不知道你这种类型的锁到底该怎么去解锁
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
​
protected boolean tryRelease(int arg) {
    throw new UnsupportedOperationException();   //AQS中不支持,需要延迟到具体的子类去实现
}
​
protected final boolean tryRelease(int releases) {   
  //ReentrantLock中的AQS Sync实现类,对tryRelease方法进行了具体实现
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

ThreadPoolExecutor

各方法源码学习

线程池核心属性标识

不用常量用平移是因为可能会涉及到 >、< 的判断,用字符串不好判断。

/** 原子性int,该标识有两个意义:
 * 1、声明当前线程池的状态
 * 2、声明线程池中的线程数
 * 高3位是:线程池状态,低29位是:线程池中的线程个数
 */
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// 这个值就是 29,方便后面做位运算。Integer.SIZE 的值就是 32
private static final int COUNT_BITS = Integer.SIZE - 3;
// 通过位运算,得出最大容量。1 左移 29位,然后 -1,就变成了:高三位是0,后29位是1
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;
​
/** 以下 5个是线程池的五种状态。一步步往下变。
 * 111 代表线程池是 RUNNING 状态。即可正常接受任务。值是:高三位是1,后29位是0。
 * -1的二进制(即补码)就是:32位全是1(1 取反+1)
 */
private static final int RUNNING    = -1 << COUNT_BITS;
// 000 SHUTDOWN 状态。不接受新任务,但是内部会正常处理任务,阻塞队列中的也会处理。值是:32位全是0。
private static final int SHUTDOWN   =  0 << COUNT_BITS;
// 001 STOP 状态。不接受新任务,中断正在执行的任务,不处理阻塞队列中的任务。值是:高三位001,后29位0。
private static final int STOP       =  1 << COUNT_BITS;
// 010 TIDYING 是一个过渡状态。代表当前线程池即将 Game Over。
private static final int TIDYING    =  2 << COUNT_BITS;
// 011 TERMINATED 状态。代表线程池已经 Game Over。
private static final int TERMINATED =  3 << COUNT_BITS;
​
// 该方法是得到线程池的状态
private static int runStateOf(int c)     { return c & ~CAPACITY; }
// 得到当前线程池的线程数量。线程池中存活的线程的数量,
private static int workerCountOf(int c)  { return c & CAPACITY; }
// 更新线程数值
private static int ctlOf(int rs, int wc) { return rs | wc; }

线程池的执行方法 execute()

都没有加锁,会存在并发情况。

public void execute(Runnable command) {
  // 健壮性判断
  if (command == null)
      throw new NullPointerException();
      
  // 拿到 32位的原子性int 标识
  int c = ctl.get();
  // 即线程数还没到核心线程数。获取工作线程数,然后判断是否 < 核心线程数
  if (workerCountOf(c) < corePoolSize) {
      // 代表可以创建核心线程。但是并没有加锁的操作
      if (addWorker(command, true))
          return;
      // 如果 if 没进去,代表并发下创建核心线程失败。就重新获取 ctl
      c = ctl.get();
  }
  
  // 如果线程池是 RUNNING状态,就将任务添加到阻塞队列(workQueue)中
  if (isRunning(c) && workQueue.offer(command)) {
    // 再次获取 ctl
    int recheck = ctl.get();
    /** 再次判断线程池是不是 RUNNING状态,
     *  如果不是 RUNNING状态,就移除任务
     *  然后直接执行拒绝策略
     */
    if (!isRunning(recheck) && remove(command))
        reject(command);
    // 如果线程池是 RUNNING状态,但是工作线程为 0,
    // 任务进入阻塞队列后发现线程池就没有能做任务的人了
    else if (workerCountOf(recheck) == 0)
        // 添加一个任务为空的工作线程,来处理阻塞队列中的任务
        // 阻塞队列有任务,但是线程池中没有工作线程,
        addWorker(null, false);
  }
  // 如果线程池不是 RUNNING状态,或者任务添加到阻塞队列失败,
  // 就尝试 创建非核心线程来处理任务,如果这个也失败了,就启用拒绝策略
  else if (!addWorker(command, false))
      reject(command);
}

addWorker(command, true)

该方法就是创建线程池线程,然后有一个 boolean参数来判断是否创建 核心线程。

private boolean addWorker(Runnable firstTask, boolean core) {
  // 这个 retry: 是用来标记这个 for循环的,方便 内部for循环 跳出来
  retry:
  // 经过下面的两个 for死循环,就会成功将工作线程数量标识 +1
  for (;;) {
    // 获取 ctl
    int c = ctl.get();
    // 获取线程池状态
    int rs = runStateOf(c);
    
    /** 如果线程池状态的值 >= SHUTDOWN,即不是 RUNNING状态的话,因为只有它是负数
     *  1、若线程状态是 SHUTDOWN。如果连 SHUTDOWN都不是,就不用去添加线程处理任务了
     *  2、并且传的任务为空(即阻塞队列有任务但线程池为空 这种情况下会传 null)。
     *  是 SHUTDOWN状态,本来就不处理新任务,传的任务又是空,就更不需要创建线程了
     *  3、并且阻塞队列非空。都没有任务了,还创建什么线程
     *  以上 3个条件,只要有一个不满足,就会 return false。即创建工作线程失败
     */
    if (rs >= SHUTDOWN &&
      ! (rs == SHUTDOWN && firstTask == null && !workQueue.isEmpty()))
      return false;
      
    // 进入死循环的内部死循环
    for (;;) {
      // 获取工作线程数
      int wc = workerCountOf(c);
      /** 若线程数已经 >= 线程数总容量,
       *  或者 如果线程数已经超过 核心线程数 或 最大线程数
       *  就 return false,即创建工作线程失败
       */ 
      if (wc >= CAPACITY ||
          wc >= (core ? corePoolSize : maximumPoolSize))
          return false;
      // 通过 CAS操作,将工作线程数 +1,成功的话就退出外层 for循环
      if (compareAndIncrementWorkerCount(c))
          break retry;
      // 失败的话,重新获取 ctl。即并发下失败了
      c = ctl.get();
      /** 重新判断线程池状态是否有变化,
       *  若改变了,就立即开启下次的外层 for循环,去重新判断线程池的状态
       *  如果状态没改变,就继续这层的 for循环,只判断容量,然后尝试创建工作线程
       */
      if (runStateOf(c) != rs)
          continue retry;
    }
  }
  
  // 工作是否开始 = false
  boolean workerStarted = false;
  // 工作是否添加 = false
  boolean workerAdded = false;
  // 申明一个 worker,Worker就是工作线程
  Worker w = null;
  try {
    // 创建 Worker,给 worker传入任务,得到 Worker对象
    w = new Worker(firstTask);
    // 从 worker 中获取线程 t
    final Thread t = w.thread;
    // 如果线程非空(基本一定是非空,这只是一个健壮性判断)
    if (t != null) {
      // 加锁,是线程池全局锁,避免我添加任务时,其他线程 shutdown/Now()了线程池
      final ReentrantLock mainLock = this.mainLock;
      mainLock.lock();
      try {
        // 获取线程池状态
        int rs = runStateOf(ctl.get());
        /** 如果线程池状态是 RUNNING状态,
         *  或者线程池状态是 SHUTDOWN,并且任务是 null。(即创建空任务线程处理阻塞队列)
         */
        if (rs < SHUTDOWN ||
            (rs == SHUTDOWN && firstTask == null)) {
            // 线程是否是运行状态,就抛异常。即我都还没让你工作,你就在运行中了
            if (t.isAlive())
                throw new IllegalThreadStateException();
            // 将工作线程添加到集合中,workers 就是一个 HashSet<Worker>
            // private final HashSet<Worker> workers = new HashSet<Worker>();
            workers.add(w);
            // 获取工作线程个数
            int s = workers.size();
            // 如果工作线程数 > 之前记录的最大工作线程数,就替换一下
            if (s > largestPoolSize)
                largestPoolSize = s;
            // 改变标识,代表添加工作线程成功
            workerAdded = true;
        }
      } finally {
        // 添加成功了,就释放锁
        mainLock.unlock();
      }
      
      // 如果添加线程成功
      if (workerAdded) {
        // 启动工作线程
        t.start();
        // 改变标识,代表启动工作线程成功
        workerStarted = true;
      }
    }
  } finally {
    // 如果启动工作线程失败,就调用 addWorkerFailed方法
    if (! workerStarted)
        addWorkerFailed(w);
  }
  // 返回工作线程是否成功启动
  return workerStarted;
}

workQueue.offer(command)

workQueue 是一个属性,即我们创建线程池时指定的阻塞队列。

getTask() 方法

private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);
        // Check if queue empty only if necessary.
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }
        int wc = workerCountOf(c);
        // Are workers subject to culling?
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }
        try {
          // 这里就是当拿不到任务的时候,阻塞在这里
          Runnable r = timed ?
              // 阻塞指定时间
              workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
              // 一直阻塞
              workQueue.take();
          if (r != null)
              return r;
          timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

processWorkerExit 方法

内部类 Worker:ThreadPoolExecutor的内部类

private final class Worker 
    extends AbstractQueuedSynchronizer 
    implements Runnable{
  Worker(Runnable firstTask) {
    setState(-1);
    // 将传进来的任务给 firstTask
    this.firstTask = firstTask;
    // 通过线程工厂 new 一个线程,把 worker 自己传进去。Worker 就一个 Runnable
    this.thread = getThreadFactory().newThread(this);
  }
  
  // worker 重写了 run()方法,线程 start 的时候就会调用重写的 run()
  public void run() { runWorker(this); }

Worker 的 runWorker 方法

final void runWorker(Worker w) {
  // 获取当前线程
  Thread wt = Thread.currentThread();
  // 拿到任务
  Runnable task = w.firstTask;
  // 
  w.firstTask = null;
  w.unlock();
  
  // 设置标识为 true
  boolean completedAbruptly = true;
  try {
    // 如果任务不为空,就执行任务
    // 如果任务为空,那就通过 getTask() 去阻塞队列中获取任务。直到拿到任务或非核心线程超时
    // 如果 getTask() 没拿到任务就会阻塞在这里,如果是最大线程数的线程,超过空闲时间就会被销毁
    while (task != null || (task = getTask()) != null) {
      // 加锁,避免 shutdown(),保证任务不会中断
      w.lock();
      /** 1、判断当前线程池状态是否 >= STOP,
       *  2、或者 Thread.interrupted() 并且线程池状态是否 >= STOP
       *  并且当前线程未被中断
       *  就将当前线程 interrupt(),中断掉
       */
      if ((runStateAtLeast(ctl.get(), STOP) 
            || (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP))) 
          && !wt.isInterrupted())
        wt.interrupt();
      try {
        // AOP,执行任务前的操作
        beforeExecute(wt, task);
        Throwable thrown = null;
        try {
            // 开始执行任务。就是调用 Worker中的run()
            task.run();
        } catch (RuntimeException x) {
            thrown = x; throw x;
        } catch (Error x) {
            thrown = x; throw x;
        } catch (Throwable x) {
            thrown = x; throw new Error(x);
        } finally {
            // AOP,执行任务后的操作
            afterExecute(task, thrown);
        }
      } finally {
          task = null;
          w.completedTasks++;
          w.unlock();
      }
    }
    completedAbruptly = false;
  } finally {
    // 线程执行完毕后的后续处理
    processWorkerExit(w, completedAbruptly);
  }
}

synchronized 和 volatile 的深入学习

synchronized 和 volatile 的底层的锁其实都是 汇编指令lock;而 lock 这条指令有两个效果,这两个效果保证了 可见性。因为会将数据写回主存,并让缓存失效。

  1. 将当前处理器缓存行的数据写回到系统内存。

  2. 这个写回内存的操作会使在其他 CPU 里缓存了该内存地址的数据无效。 synchronized 底层 lock 了一条 cmpxchg 汇编指令,cmpxchg 汇编指令保证了原子性,所以 synchronized 能保证原子性,lock cmpxchg;

而 volatile 底层 lock 了一条空指令,所以 volatile 不能保证原子性,但是 volatile 加了内存屏障,所以能防止指令重排序,保证有序性,lock 空指令 + 内存屏障;

* 虽然 cmpxchg 保证了原子性,但是这条汇编指令并不是原子性的,lock + cmpxchg 才能保证这条指令的原子性。cmpxchg 保证比较交换,lock 保证 cmpxchg 执行的时候别的汇编指令不会来。
  • java文件执行的方式:

    • JVM,解释一句执行一句,

    • JIT,即时编译器,会把那些执行得比较快的代码,不进行解释执行,而是直接编译成汇编,以后再调用,不用解释了,直接用汇编,这就是 HotSpot 的优化。HotSpot 就是这么来的,热点代码变成汇编。HSDIS 工具,可以反编译 HotSpot 中的汇编码。

  • java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly Hello > 1.txt 这是Linux中的语句。

HotSpot(JVM)中锁资源的四种状态详解

无锁态

  • 在 JVM 启动后,偏向锁默认启动前,new 了一个普通对象的时候,其默认是无锁态,

  • 有 25位没有使用,

  • 有 31位装的是 hashCode,这个 hashCode 叫 identityHashCode,与普通的 hashCode 不同。只有有人调用 hashCode 的时候才会往里装,没人调的话,这里面就没有值。

  • 还有 1位没有使用过。然后还有 4位叫分代年龄,JVM 有 10种垃圾回收器,前面最老的 6种,都涉及到年龄(其实第 7种 G1 也有,但是不如前面 6个明显),使用了分代垃圾回收算法,对象每经过一次 GC,对象的年龄就会增长,就是这 4位分代年龄。

  • 注意:分代年龄 4位,所以年龄最高就是 16(从 0 开始),不可能乱改的。

偏向锁

  • 当偏向锁已经启动后,给对象加锁,默认加的就是偏向锁

  • 因为得到锁,就是把自己的线程ID 放在锁资源的 markword 里面,所以 54位来放置,当前线程的指针,就是线程ID号

  • 2位 Epoch叫 批量撤销,

轻量级锁(就是自旋锁)

  • Java 中轻量级锁 就是 自旋锁,当偏向锁产生竞争时,偏向锁升级成轻量级锁,

  • 62位的指向线程栈中 Lock Record 的指针,这个并不是线程的 ID号, 跟锁重入有关系,synchronized 这把锁默认是可重入的,即两个同步方法,锁的是同一个资源,虽然他们的方法名不相同,但是锁的资源是相同的,即 synchronized锁的方法不同,但是 synchronized锁的资源是相同的,这就是可重入。

  • 每个线程想进入 自旋锁 的时候,自己在自己的线程栈里面,生成一个对象,即 Lock Record(锁记录)。然后线程如果成功把自己的 Lock Record 信息放进 自旋锁锁资源 的 62位指针,那这个线程就超过获得这把 自旋锁锁资源 的使用资格了,

  • 然后当已经获得 自旋锁锁资源的线程,再想进入这个自旋锁的时候,即线程重入的时候,会在自己的线程栈里面再生成一个 Lock Record,即有几个 LR,就代表用了这个锁资源几次。所以进入几次,二进制字节码时,就会有几次 monitorenter,相对应也有几次 monitorexit,

  • 解锁,其实就是依次弹出 Lock Record 的过程。

用户态与内核态、synchronized 的发展历程

JDK 早期,synchronized 叫作重量级锁,因为申请锁资源必须通 kernel,即申请锁资源的时候必须通过系统内核,进行系统调用,

JDK5.0 时候,推出 JUC包,里面大多数使用了 自旋锁,来代替 synchronized,

用户态、内核态

不经过内核态的锁,都可以叫轻量级锁,用户空间的锁。

用户态

普通的程序,运行在用户空间,类似 JVM,并且使用用户空间来做可以保证操作系统比较健壮,不容易被搞死,因为敏感的操作得通过内核(OS操作系统)来做。当做一些比较关键的事情时,还是需要通过老大(OS操作系统,内核)来做,比如:读写网络、读写硬盘、内存映射。

所以说,JDK 早期使用 synchronized,申请这把锁的时候,虽然写在 Java 里面,但是 synchronized 是从操作系统申请对应的锁。

所以所谓的重量级,就是使用了内核。

CAS

就是比较与交换,使用 自旋锁 的方式,来保证操作的原子性,

ABA问题加版本号就可以解决,比如 AtomicStampedReference、AtomicReference,

CAS 的 native底层,native 的 C++底层

CAS 的底层实现:cmpxchg(汇编语言)

atomic 包下面的类保证原子性,主要依靠 Unsafe类的 CAS自旋锁,即 Unsafe类的 native CompareAndSwapInt 方法,其 C++ 实现主要靠 cmpxchg方法。

再跟进去,其里面是 LOCK_IF_MP(%4) "cmpxchg1 %1,(%3)",这个 LOCK_IF_MP 后面跟的是一条 汇编指令,所以这条指令就是 CAS。

cmpxchg1 不是原子性语言,即它是可拆分的,这整一条语句 LOCK_IF_MP cmpxchg1 才能保证数据的可见性和原子性,

CAS 的底层原子性保证:LOCK_IF_MP(汇编语言)

MP:multi processor,即多处理器,多 CPU,即看看你有多少个 CPU,

* 假如只有一个 CPU,即只有一个核的话,那就无所谓了,因为只有一个核,CAS操作一定保证了原子性,一定是顺序进行的,所以执行  一条CAS指令,不可能被打断,那么一定是原子性操作,不用加lock
* 假如是多 CPU,那么要加 lock,所以最终的指令是:lock cmpxchg 而 lock 指令在执行后面的指令的时候,锁定一个北桥信号,不采用锁总线的方式,锁北桥信号 比 锁总线 轻量级一些,即 cmpxchg 执行的过程中,别的指令不可能可以进去,因为锁住了。

JOL工具(Java Object Layout)

Java对象布局,new 出一个对象的时候,显示该对象在堆中内存的布局。

//使用maven引入就可以了
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>

简介

  • markword 标记字的长度固定是 8个字节,

  • 类型指针就是用来指定该对象的类型,无固定长度,

  • 对齐就是用来帮这个对象补成8的倍数,因为JVM读的时候是一块一块读的,这一块就是8个字节,所以对齐就是来补成能被 8 整除,方便JVM读

  • 使用的时候:ClassLayout.parseInstance(类对象).toPrintable(); 变成一个String字符串

给对象上锁(synchronized)的具体含义

使用 jol 查看 synchronized 前后的变化,可以发现,只有 markword 发生了变化。即锁信息,存在了 markword 里面,所以加锁就是修改 markword。

synchronized锁升级

匿名偏向锁的意思就是,你 new 了一个资源出来的时候,这个资源默认就是一个锁资源,偏向锁资源,当该偏向锁没有被某个线程占用,即对象头中用于保存线程指针的位都是 0 的时候,这个偏向锁就是匿名偏向锁。自旋锁 就是一种轻量级锁。

偏向锁 和 轻量级锁 都不会惊动操作系统,只需要在用户空间就可完成,所以比较快,效率比较高。重量级锁 则需要经过 OS。所以使用 偏向锁、轻量级锁 可以大幅度提升效率,相对于一直使用 重量级锁而言。当然不加锁是最快的,但是会产生同步问题。

虽然所有的代码语言都会使用到汇编语言,但它们不需要惊动操作系统的线程调度,直接就 翻译成汇编码 了。

锁升级主线:普通对象 -> 偏向锁 -> 轻量级锁 -> 重量级锁

  1. 先 new 一个普通对象,假如要给他上锁 synchronized,那么上的默认就是 偏向锁。

  2. 偏向锁后如果产生竞争,那么 偏向锁 自动变成 轻量级锁,

  3. 轻量级锁 如果竞争达到要求,那么就变成重量级,

偏向锁

偏向锁 就是 偏向于某个线程的锁,不需要经过内核,贴个 线程ID 就能用锁。

产生的原因: JDK团队发现,大多数人使用 synchronized方法,比如 StringBuffer,都是在单线程环境下运行的,即 synchronized 去调用内核根本没有使用的必要,但是 synchronized 在当时是重量级锁,会惊动操作系统,很浪费性能,所以优化了一下,产生偏向锁:凡是线程第一次得到这把锁,那么这把锁会偏向于它,

偏向锁具体:不惊动操作系统,偏向锁只需要把这个线程的 ID,放到锁资源的 markword 里面就可以了,锁的是哪个资源,就在这个锁资源的 markword 上记录线程的ID。

使用 java -XX:+PrintFlagsFinal -version | grep BiasedLocking 能看到,UseBiasedLocking (使用偏向锁)这个参数的值默认是 true,即默认打开了偏向锁。

但是 JVM 启动有个 BiasedLockingStartupDelay 是 4000ms,即启动延迟 4秒。

偏向锁升级成轻量级锁的条件

有人竞争,偏向锁就升级成轻量级锁,即只要有第 2个人来竞争,就升级。

使用参数来配置偏向锁

设置偏向锁延迟多久启动:-XX:BiasedLockingStartupDelay=毫秒数。

偏向锁延迟 4s启动的原因

偏向锁是在如果只有一个线程的时候,效率很高,因为不需要别的操作,只需要将自己的线程ID 放到 markword 里面就可以了,

但是当你明确知道,某些资源会有好多线程去竞争的时候,启动偏向锁就是一个没有必要的过程。就比如 JVM 刚启动的过程中,很多地方需要线程同步,比如说分配内存对象的时候,所以明明知道竞争一定存在,就不需要去默认启动偏向锁了,启动了也会立马升级。所以延时 4s再启动,等 JVM 启动完了后,再启动偏向锁,来迎接用户的代码。JVM 基本上 4s 一定能启动完成。

偏向锁是否启动、普通对象和匿名偏向

刚刚说了,偏向锁默认延迟 4s 才开启,然后JVM启动的时间一定小于 4s,在 JVM 启动完成 —— 4s 这段时间内 new 出来的对象,都是普通对象,即基本上我们写的代码里面的对象都是普通对象。那种周期性执行或延迟执行的代码创建的对象才是 匿名偏向锁资源,即 4s 后的。

引入偏向锁的目的

在只有单线程执行情况下,尽量减少不必要的轻量级锁执行路径,轻量级锁的获取及释放依赖多次 CAS 原子指令,而偏向锁只依赖一次 CAS 原子指令置换 ThreadID,之后只要判断线程ID 为当前线程即可,

偏向锁使用了一种等到竞争出现,才释放锁的机制,所以其实消除偏向锁的开销还是蛮大的。

如果同步资源或代码一直都是多线程访问的,那么消除偏向锁这一步骤对你来说就是多余的,可以通过 -XX:-UseBiasedLocking=false 来关闭偏向锁,直接轻量级锁。

自旋锁(轻量级锁)

自旋锁,即轻量级锁,也和偏向锁一样,不会惊动操作系统老大。

如果给 4s 之前 new 的对象加锁,那么锁直接升级轻量级锁,因为此时偏向锁还未启动,无法加偏向锁。

如果 4s 后给对象加锁,就是偏向锁,然后如果没产生竞争的时候,锁一直是偏向锁,如果产生竞争,那么 JDK 就会让 锁资源 的 markword 上的 A线程的 ID信息扔掉,锁升级成轻量级锁,

然后 A 和 B 竞争这把锁,A、B线程使用自旋锁的方式去 修改锁资源的 markword信息,即 A、B线程使用自旋锁的方式来试图将自己的线程ID 放进 锁资源的 markword 里面。谁成功了就是抢占到了这把锁。

轻量级锁升级成重量级锁的条件

满足以下条件的其中一条即可升级成重量级锁:

  1. 自旋次数:某个线程的自旋次数超过 10次

  2. 等待的自旋线程个数:等待的自旋线程的个数超过(>) CPU 核数的一半 JDK1.6 之前需要我们手动调优 JVM,即需要我们调以上这两个参数;JDK1.6 之后,有了 自适应自旋,就不需要我们调了,我们最好也不要去调这两个参数。

  • 自适应自旋:JDK 会根据每个线程的情况去判断要不要升级成重量级锁,即自动判断,不需要我们操心了。

引入轻量级锁的目的

在多线程交替执行同步块的情况下,尽量避免重量级锁引起的性能消耗(用户态和核心态转换),但是如果多个线程在同一时刻进入临界区,会导致轻量级锁膨胀升级重量级锁,所以轻量级锁的出现并非是要替代重量级锁,而是更好地提高效率。

重量级锁

使用重量级锁的原因:自旋锁是要使用到 while 操作的,while 操作会消耗 CPU资源,如果竞争特别激烈,就一个线程在用锁,外面 9999个线程在自旋等锁,那么 CPU资源都被自旋消耗完了,这显然不合理,所以当轻度竞争变成重度竞争,轻量级锁升级成重量级锁。

  • 重量级锁:经过操作系统的调度后,这个锁资源会产生等待队列(waitset),然后取消自选线程他们的自旋,让那些线程进入等待队列。

volatile

简介

volatile 的作用

  1. 保证线程间的可见性

  2. 防止指令重排序 线程会把变量拿到自己的工作空间中,然后使用,线程会一直使用工作空间中的值,线程不会主动去主内存读,除非你规定了。因为去主内存读慢。

System.out.print 因为这个是标准输出,并且只有一个,这个方法可以把所有的内存进行同步,然后写出去,所以这个方法能刷内存。

底层如何实现数据一致性—缓存一致性协议

缓存一致性是在 CPU 级别的一致性实现,CPU 的一致性实现有很多方式,比如锁总线,HotSpot 就是用这种霸道的方式。

Java 的 volatile 的底层实现 与 缓存一致性协议 没有关系。

缓存行

缓存行 的概念:因为 CPU 的速度非常快。内存的速度非常慢,所以 CPU 把内容缓存在 内部,这样效率就会很快,所以 CPU 和主内存之间,有很多缓存的概念,一般是三级缓存。L1、L2 在 CPU 内部,所以速度很快。

CPU 读的时候,也是分块读的,一块一块读。从内存一次性读 64个字节,放到 CPU 的缓存里,而 64个字节,这就是缓存行,即读取长度的基本单位。

缓存行越大,局部性空间效率越高,但读取时间慢,反之;现在一次性读 64个字节并不是因为 64位 CPU,而是取的折中值。

缓存一致性协议

缓存一致性协议,有很多种:MSI、MESI(Intel的CPU使用的)、MOSI、Synapse、Firefly、Dragon。不同的 CPU 有不同的缓存一致性协议。

MESI Cache 一致性协议:CPU 每个 cache line 标记四种状态(额外两位):Modified(被修改)、Exclusive(独占)、Shared(共享)、Invalid(无效)

缓存锁实现之一:有些无法被缓存的数据,或者跨越多个缓存行的数据,依然必须使用总线锁。

缓存行带来的问题

在上面那个图里,假如 CPU1 想修改 X,CPU2 想修改 Y,然后 X 和 Y 处于主内存的同一个缓存行中,因为 CPU 的特性,那么 CPU1 拿到了 X 的同时肯定能拿到 Y,同样的,CPU2 拿到 Y 的时候肯定能拿到 X,

  • 然后问题就来了,X 和 Y 都被加了 volatile关键字,那么只要 CPU1 修改了 X,就会立即通知 CPU2:你缓存里面的X失效了,重新去拿;同样的,CPU2 修改了 Y,就会立即通知 CPU1:你缓存里面的 Y失效了,重新去拿。这样就会导致效率低下。

disruptor

有一个ring buffer,单机最快的队列。

CPU 的乱序(并序)执行

即 IO 和非IO指令,一个主要是消耗主内存,另一个主要是消耗 CPU,但是它们都是指令,这就导致了假如不重排序,那么使用 CPU 的指令2 就会一直等 IO 的指令1,但是明明指令1 不使用 CPU,如果串行就会产生浪费,效率低下。所以进行优化:指令1 去 IO 的时候,让指令2 来执行,即使用 CPU。

as-if-serial

即串行(序列化)的意思:单线程里面,内部有可能是前后顺序打乱的,但最终执行的结果和是否打乱没有关系,所以看上去像是序列化的,但内部实际是并行的。

线程A:①a=1; ②x=b; 线程B:③b=1; ④y=a;正常情况下,绝对不可能出现:x=0;y=0;但是重排序情况就会发生。

关于 Object o = new Object(); 指令重排序问题

简介

  1. 半初始化状态:第 0条指令就是去内存里面申请内存空间,此时 m 赋默认值 0,

  2. 全初始化状态:第 4条指令是在调用构造方法,然后 m 设为 8,

  3. 第 7条语句将 t 与内存建立连接,让 t 指向内存空间,

DCL单例为什么一定要加 volatile?

DCL单例就是 Double Check Lock,双重检查锁,因为 volatile 可以禁止指令重排序。

假如线程1 在半初始化后,发生指令重排序,4 和 7 互换,即 t 指向半初始化的内存空间;然后此时,线程2 去拿,但此时 t 已经有指向了,所以线程2 以为已经初始化完成了,然后直接使用此对象,那么就出现错误了,

指令重排序的发生机率特别小,只有百万级并发才有可能会发生,但是是有可能发生的,所以要加上 volatile,防止指令重排序。

符号引用、直接引用

  • 符号引用:常量池里面引用了某个类的某个方法

  • 直接引用:把这个符号引用翻译成那个地址

系统底层如何保证有序性

  1. 内存屏障 sfence lfence mfence 等系统原语

  2. 锁总线 注意:系统原语并不是所有 CPU 都有,有些不支持,甚至有些支持的都不去使用,因为麻烦,他们会偷懒去使用 lock,锁总线。

lock 是所有 CPU 都有的,效率比系统原语低。总线,简单来说就是 CPU 与内存之间交互的线,总线只有一条,总线一锁,多核变单核。

X86 CPU 的底层内存屏障

sfence lfence mfence 都是 X86系统底层的实现原语,s 就是 store 或 save,l 就是 load,mfence 就是全部的屏障。

volatile 是 JVM 级别的内存屏障

对于 volatile 而言,它是 JVM级别的内存屏障,即 volatile 只是内存屏障的一种实现,

然后 JVM 如何实现屏障:JVM 通过 CPU 的底层实现,CPU 的底层则通过系统原语或者 lock 实现。

volatile 要求 JVM,即要求 Java 的虚拟机 HotSpot,凡是对 volatile修饰的变量进行读写的时候必须加屏障,这个屏障是 JVM级别的屏障,

JSR内存屏障就是一种 JVM级别的内存屏障,这个屏障就相当于 在两个指令间加一堵墙,不允许墙前后的指令重排序。

volatile 的内存屏障的实现细节

即你写 volatile变量的时候,上面有一个 SS屏障,下面有一个 SL屏障。

* 即 SS屏障上下:上面的先写(Store)完了,下面的我才能写,
* SL屏障上下:上面的我写完了,下面的才能读(加载),读也是一样

JVM 必须保证的有序性—happens-before原则

在这 8种情况下,禁止指令重排序,这是 JVM级别的规则,并不是底层的实现规则。

线程交替输出问题(生产者消费者问题)

使用 LockSupport 工具类

使用 LockSupport 就像使用 Lock 一样,

LockSupport 比 Condition、wait+notify 好的地方:LockSupport 可以预约叫醒别人,然后如果那个人刚睡,但是发现有人在睡前叫过它,那么它就不睡了。

public class LockSupportTest {
    static Thread t1;
    static Thread t2;
    public static void main(String[] args) {
        char[] charArray1 = "12345".toCharArray();
        char[] charArray2 = "ABCDE".toCharArray();
        t1 = new Thread(() -> {
            for (char c : charArray1) {
                System.out.print(c);
                LockSupport.unpark(t2);
                LockSupport.park();
            }
        }, "t1");
        t2 = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for (char c : charArray2) {
                LockSupport.park();
                System.out.print(c);
                LockSupport.unpark(t1);
            }
        }, "t2");
        t1.start();
        t2.start();
    }
}

使用 ReentrantLock 和 Condition

public class LockAndConditionTest {
    public static void main(String[] args) {
        char[] charArray1 = "12345".toCharArray();
        char[] charArray2 = "ABCDE".toCharArray();
        Lock lock = new ReentrantLock();
        Condition condition1 = lock.newCondition();
        Condition condition2 = lock.newCondition();
        Thread t1 = new Thread(() -> {
            for (char c : charArray1) {
                lock.lock();
                System.out.print(c);
                condition2.signal();
                try {
                    condition1.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (char c : charArray2) {
                lock.lock();
                System.out.print(c);
                condition1.signal();
                try {
                    condition2.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        }, "t2");
        t1.start();
        t2.start();
    }
}

使用 notify()、wait()

public class DailyTestApplication {
    public static void main(String[] args) {
        char[] charArray1 = "12345".toCharArray();
        char[] charArray2 = "ABCDE".toCharArray();
        WaitNotifyBuffer buffer = new WaitNotifyBuffer(1);
        Thread t1 = new Thread(() -> {
            for (char c : charArray1) {
                buffer.print(String.valueOf(c),1,2);
            }
        });
        Thread t2 = new Thread(() -> {
            for (char c : charArray2) {
                buffer.print(String.valueOf(c),2,1);
            }
        });
        
        t1.start();
        t2.start();
    }
    
    static class WaitNotifyBuffer {
        private int flag;
        public WaitNotifyBuffer(int flag) {
            this.flag = flag;
        }
        public void print(String str, int waitFlag, int nextFlag) {
            synchronized (this) {
                while(flag != waitFlag) {
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.print(str);
                flag = nextFlag;
                this.notifyAll();
            }
        }
    }
}
​

使用 LinkedTransferQueue() 同步队列

LinkedTransferQueue 是容量为 1 的队列,谁往这个队列里面放东西,它放进去之后就立即阻塞,直到有人拿走了东西,这个线程才可以开始工作。

transfer 方法就是放进去,这是一个阻塞方法,即放进去之后得阻塞等待至别人拿走,即 take() 方法,take() 方法也是个阻塞方法,得里面有东西才可以拿走,否则就阻塞等待至有人放进去。

public class DailyTestApplication {
    public static void main(String[] args) {
        char[] charArray1 = "12345".toCharArray();
        char[] charArray2 = "ABCDE".toCharArray();
        TransferQueue<Character> queue = new LinkedTransferQueue();
        Thread t1 = new Thread(() -> {
            for (char c : charArray1) {
                try {
                    queue.transfer(c);
                    System.out.print(queue.take());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        Thread t2 = new Thread(() -> {
            for (char c : charArray2) {
                try {
                    System.out.print(queue.take());
                    queue.transfer(c);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        
        t1.start();
        t2.start();
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值