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 这条指令有两个效果,这两个效果保证了 可见性。因为会将数据写回主存,并让缓存失效。
-
将当前处理器缓存行的数据写回到系统内存。
-
这个写回内存的操作会使在其他 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。所以使用 偏向锁、轻量级锁 可以大幅度提升效率,相对于一直使用 重量级锁而言。当然不加锁是最快的,但是会产生同步问题。
虽然所有的代码语言都会使用到汇编语言,但它们不需要惊动操作系统的线程调度,直接就 翻译成汇编码 了。
锁升级主线:普通对象 -> 偏向锁 -> 轻量级锁 -> 重量级锁
-
先 new 一个普通对象,假如要给他上锁 synchronized,那么上的默认就是 偏向锁。
-
偏向锁后如果产生竞争,那么 偏向锁 自动变成 轻量级锁,
-
轻量级锁 如果竞争达到要求,那么就变成重量级,
偏向锁
偏向锁 就是 偏向于某个线程的锁,不需要经过内核,贴个 线程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 里面。谁成功了就是抢占到了这把锁。
轻量级锁升级成重量级锁的条件
满足以下条件的其中一条即可升级成重量级锁:
-
自旋次数:某个线程的自旋次数超过 10次
-
等待的自旋线程个数:等待的自旋线程的个数超过(>) CPU 核数的一半 JDK1.6 之前需要我们手动调优 JVM,即需要我们调以上这两个参数;JDK1.6 之后,有了 自适应自旋,就不需要我们调了,我们最好也不要去调这两个参数。
-
自适应自旋:JDK 会根据每个线程的情况去判断要不要升级成重量级锁,即自动判断,不需要我们操心了。
引入轻量级锁的目的
在多线程交替执行同步块的情况下,尽量避免重量级锁引起的性能消耗(用户态和核心态转换),但是如果多个线程在同一时刻进入临界区,会导致轻量级锁膨胀升级重量级锁,所以轻量级锁的出现并非是要替代重量级锁,而是更好地提高效率。
重量级锁
使用重量级锁的原因:自旋锁是要使用到 while 操作的,while 操作会消耗 CPU资源,如果竞争特别激烈,就一个线程在用锁,外面 9999个线程在自旋等锁,那么 CPU资源都被自旋消耗完了,这显然不合理,所以当轻度竞争变成重度竞争,轻量级锁升级成重量级锁。
-
重量级锁:经过操作系统的调度后,这个锁资源会产生等待队列(waitset),然后取消自选线程他们的自旋,让那些线程进入等待队列。
volatile
简介
volatile 的作用
-
保证线程间的可见性
-
防止指令重排序 线程会把变量拿到自己的工作空间中,然后使用,线程会一直使用工作空间中的值,线程不会主动去主内存读,除非你规定了。因为去主内存读慢。
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(); 指令重排序问题
简介
-
半初始化状态:第 0条指令就是去内存里面申请内存空间,此时 m 赋默认值 0,
-
全初始化状态:第 4条指令是在调用构造方法,然后 m 设为 8,
-
第 7条语句将 t 与内存建立连接,让 t 指向内存空间,
DCL单例为什么一定要加 volatile?
DCL单例就是 Double Check Lock,双重检查锁,因为 volatile 可以禁止指令重排序。
假如线程1 在半初始化后,发生指令重排序,4 和 7 互换,即 t 指向半初始化的内存空间;然后此时,线程2 去拿,但此时 t 已经有指向了,所以线程2 以为已经初始化完成了,然后直接使用此对象,那么就出现错误了,
指令重排序的发生机率特别小,只有百万级并发才有可能会发生,但是是有可能发生的,所以要加上 volatile,防止指令重排序。
符号引用、直接引用
-
符号引用:常量池里面引用了某个类的某个方法
-
直接引用:把这个符号引用翻译成那个地址
系统底层如何保证有序性
-
内存屏障 sfence lfence mfence 等系统原语
-
锁总线 注意:系统原语并不是所有 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(); } }