CAS即Compare And Swap的缩写,翻译成中文就是比较并交换,其作用是让CPU比较内存中某个值是否和预期的值相同,如果相同则将这个值更新为新值,不相同则不做更新,也就是CAS是原子性的操作(读和写两者同时具有原子性),其实现方式是通过借助C/C++调用CPU指令完成的,所以效率很高。 CAS的原理很简单,这里使用一段Java代码来描述
1 2 3 4 5 6 7 8 9
publicbooleancompareAndSwap(int value, int expect, int update) { // 如果内存中的值value和期望值expect一样 则将值更新为新值update if (value == expect) { value = update; returntrue; } else { returnfalse; } }
publicfinalnativebooleancompareAndSwapObject(Object var1, long var2, Object var4, Object var5);
publicfinalnativebooleancompareAndSwapInt(Object var1, long var2, int var4, int var5);
publicfinalnativebooleancompareAndSwapLong(Object var1, long var2, long var4, long var6);
我们拿public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);进行分析,这个方法是比较内存中的一个值(整型)和我们的期望值(var4)是否一样,如果一样则将内存中的这个值更新为var5,参数中的var1是值所在的对象,var2是值在对象(var1)中的内存偏移量,参数var1和参数var2是为了定位出值所在内存的地址。 Unsafe.java在这里发挥的作用有:
privatevoidfastRemove(int index){ modCount++; intnumMoved=size-index-1; if(numMoved>0) System.arraycopy(elementData,index+1,elementData,index,numMoved); elementData[--size]=null;// Let gc do its work }
/** The array, accessed only via getArray/setArray. */ privatetransientvolatile Object[] array;
并且该数组引用是被 volatile 修饰,注意这里仅仅是修饰的是数组引用,其中另有玄机,稍后揭晓。关于 volatile 很重要的一条性质是它能够够保证可见性,关于 volatile 的详细讲解可以看这篇文章。对 list 来说,我们自然而然最关心的就是读写的时候,分别为 get 和 add 方法的实现。
get 方法实现原理
get 方法的源码为:
1 2 3 4 5 6 7 8 9 10 11 12 13
public E get(int index) { return get(getArray(), index); } /** * Gets the array. Non-private so as to also be accessible * from CopyOnWriteArraySet class. */ final Object[] getArray() { return array; } private E get(Object[] a, int index) { return (E) a[index]; }
可以看出来 get 方法实现非常简单,几乎就是一个“单线程”程序,没有对多线程添加任何的线程安全控制,也没有加锁也没有 CAS 操作等等,原因是,所有的读线程只是会读取数据容器中的数据,并不会进行修改。
private Node addWaiter(Node mode) { Nodenode=newNode(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Nodepred= tail; //创建新的节点,并将线程和节点关联。 //将同步队列的尾节点后继节点指向新节点, //将新节点的前驱节点指向尾节点, //新节点称为同步队列的尾节点。 if (pred != null) { node.prev = pred; //CAS操作将新节点插入到,成功则返回,不成功则继续下面的enq方法, //进行死循环CAS插入,直到成功。 if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } //如果上面的CAS操作插入不成功,则调用enq方法 死循环插入 直到成功。 enq(node); return node; }
private Node enq(final Node node) { //死循环 直到插入成功。 for (;;) { Nodet= tail; //如果尾节点为null,说明同步队列还未初始化,则CAS操作新建头节点 if (t == null) { // Must initialize if (compareAndSetHead(newNode())) tail = head; } else { //通过CAS操作将节点插入到同步队列尾部 node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
//节点以“死循环”的方式去获取资源,为什么死循环加了双引号呢?因为循环并不 //是一直让节点无间断的去获取资源,节点会经历 获取资源->失败->线程进入等待 //状态->唤醒->获取资源......,线程在死循环的过程会不断等待和唤醒,即节点的自旋。 finalbooleanacquireQueued(final Node node, int arg) { booleanfailed=true; try { booleaninterrupted=false; for (;;) { //获取节点的前驱节点 finalNodep= node.predecessor(); //如果前驱节点为头结点,表示资源正在前驱节点的手上,那该节点 //去尝试获取资源,如果获取成功则将该节点设置为头结点,并且 //返回。 //如果该节点的前驱节点并不是头节点或者是前驱节点是头结点但是 //该节点获取资源失败,继续往下执行。 if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return interrupted; } //调用shouldParkAfterFailedAcquire函数,将该节点的前驱节点 //的状态设置为SIGNAL,告诉前驱节点我要去“睡觉”了,当资源排 //到你的时候,你就通知我一下让我醒来,即节点做进入等待状态 //的准备。 //当节点做好了进入等待状态的准备,则调用parkAndCheckInterrupt //函数,让该节点进入到等待状态。 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
privatestaticbooleanshouldParkAfterFailedAcquire(Node pred, Node node) { //获取前驱节点的状态。 intws= pred.waitStatus; //如果前驱节点的状态已经为SIGNAL了,即已经做好准备了,那直接返回。 if (ws == Node.SIGNAL) /* * This node has already set status asking a release * to signal it, so it can safely park. */ returntrue; //如果前驱节点的状态为取消状态,则将前驱节点移除队列,循环这个过程 //直到前驱节点不为取消状态为止。 if (ws > 0) { /* * Predecessor was cancelled. Skip over predecessors and * indicate retry. */ do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; //如果前驱节点没有做好准备(标志状态为SIGNAL)、前驱节点也没有被取消, //则使用CAS操作将前驱节点的状态更新为SIGNAL,然后返回false,为什么 //是返回false呢?因为CAS操作并不保证一定能更新成功,返回false的目的 //是让acquireQueued函数再执行一次for循环,这个循环第一可以让该节点 //再尝试获取资源(万一成功了呢 是吧),第二是让acquireQueued函数再调用 //一次shouldParkAfterFailedAcquire函数(即本函数)判断节点的前驱节点是 //否已经设置为SIGNAL状态了。 } else { /* * waitStatus must be 0 or PROPAGATE. Indicate that we * need a signal, but don't park yet. Caller will need to * retry to make sure it cannot acquire before parking. */ compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } returnfalse; }
privatevoidunparkSuccessor(Node node) { /* * If status is negative (i.e., possibly needing signal) try * to clear in anticipation of signalling. It is OK if this * fails or if status is changed by waiting thread. */ //获取头结点状态。 intws= node.waitStatus; //如果状态小于0,即代表有后继节点需要唤醒。 if (ws < 0) //将头结点的状态置为0 因为只需要唤醒一次 compareAndSetWaitStatus(node, ws, 0);
/* * Thread to unpark is held in successor, which is normally * just the next node. But if cancelled or apparently null, * traverse backwards from tail to find the actual * non-cancelled successor. */ Nodes= node.next; //如果头结点的后继节点为空 或者 头结点的后继节点处于取消状态,则从尾部开始往前寻找, //找到一个离头结点最近 且状态不是取消状态的节点。 if (s == null || s.waitStatus > 0) { s = null; for (Nodet= tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } //如果头结点的后继节点不为取消状态,则直接将后继节点唤醒 if (s != null) LockSupport.unpark(s.thread); }
privatevoidsetHeadAndPropagate(Node node, int propagate) { Nodeh= head; // Record old head for check below setHead(node); /* * Try to signal next queued node if: * Propagation was indicated by caller, * or was recorded (as h.waitStatus either before * or after setHead) by a previous operation * (note: this uses sign-check of waitStatus because * PROPAGATE status may transition to SIGNAL.) * and * The next node is waiting in shared mode, * or we don't know, because it appears null * * The conservatism in both of these checks may cause * unnecessary wake-ups, but only when there are multiple * racing acquires/releases, so most need signals now or soon * anyway. */ if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { Nodes= node.next; //如果节点为共享节点,则调用doReleaseShared函数唤醒后继节点。 if (s == null || s.isShared()) doReleaseShared(); } }
privatevoiddoReleaseShared() { /* * Ensure that a release propagates, even if there are other * in-progress acquires/releases. This proceeds in the usual * way of trying to unparkSuccessor of head if it needs * signal. But if it does not, status is set to PROPAGATE to * ensure that upon release, propagation continues. * Additionally, we must loop in case a new node is added * while we are doing this. Also, unlike other uses of * unparkSuccessor, we need to know if CAS to reset status * fails, if so rechecking. */ for (;;) { Nodeh= head; if (h != null && h != tail) { intws= h.waitStatus; //如果节点标识后继节点需要唤醒,则调用unparkSuccessor方法进行唤醒。 if (ws == Node.SIGNAL) { if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; // loop to recheck cases unparkSuccessor(h); } elseif (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } if (h == head) // loop if head changed break; } }
Connected to the target VM, address: '127.0.0.1:57756', transport: 'socket' Exception in thread "Thread-2" Exception in thread "Thread-0" java.lang.NumberFormatException: multiple points at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890) at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110) at java.lang.Double.parseDouble(Double.java:538) at java.text.DigitList.getDouble(DigitList.java:169) at java.text.DecimalFormat.parse(DecimalFormat.java:2056) at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869) at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514) at java.text.DateFormat.parse(DateFormat.java:364) at com.vernon.test.demo.jdk.text.SimpleDateFormatCase$1.run(SimpleDateFormatCase.java:23) at java.lang.Thread.run(Thread.java:748) java.lang.NumberFormatException: multiple points at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890) at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110) at java.lang.Double.parseDouble(Double.java:538) at java.text.DigitList.getDouble(DigitList.java:169) at java.text.DecimalFormat.parse(DecimalFormat.java:2056) at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869) at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514) at java.text.DateFormat.parse(DateFormat.java:364) at com.vernon.test.demo.jdk.text.SimpleDateFormatCase$1.run(SimpleDateFormatCase.java:23) at java.lang.Thread.run(Thread.java:748) Wed Dec 13 15:17:27 CST 2017 Wed Dec 13 15:17:27 CST 2017 Wed Dec 13 15:17:27 CST 2017 Fri Dec 12 15:17:27 CST 2217 Thu Dec 13 15:17:27 CST 2012 Wed Dec 13 15:17:27 CST 2017 Wed Dec 13 15:17:27 CST 2017 Fri Jun 09 15:17:27 CST 5881628 Disconnected from the target VM, address: '127.0.0.1:57756', transport: 'socket'
Linux 是一个多任务操作系统,它支持同时运行的任务数量远大于 CPU 个数。其实这些任务没有真正的同时运行,是因为系统在很短的时间内,将 CPU 轮流分配给它们,造成多任务同时运行的错觉。
而在每个任务运行前,CPU 都需要知道任务从哪里加载、从哪里开始运行,需要系统事先设置好 CPU 寄存器和程序计数器。CPU 寄存器是 CPU 内置的容量小、速度极快的内存。而程序计数器则是用来存储 CPU 正在执行的指令位置、或即将执行的下一条指令位置。它们都是 CPU 在运行任务前必须依赖的环境,也被叫做 CPU 上下文。
上下文切换,就是先把前一个任务的 CPU 上下文保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳到程序计数器所指的新位置,运行新任务。而这些保存下来的上下文,会存储在系统内核中,并在任务重新调度执行时再次加载进来。这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行。
过多的上下文切换,会把 CPU 时间消耗在寄存器、内核栈、虚拟内存等数据的保存和恢复上,从而缩短进程真正运行的时间,导致系统的整体性能大幅下降。
如何查看系统的上下文切换
我们可以通过 vmstat 工具来查看系统的上下文切换情况。vmstat 主要用来分析系统内存使用情况,也常用来分析 CPU 上下文切换和中断的次数。
1 2 3 4 5
# 每隔 5 秒输出 1 组数据 $ vmstat 5 procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 0 0 0 7005360 91564 818900 0 0 0 0 25 33 0 0 100 0 0
我们需要重点关注下列四项内容:
cs(context switch) 是每秒上下文切换的次数。
in(interrupt) 是每秒中断的次数。
r(Running or Runnable) 是就绪队列的长度,也就是正在运行和等待 CPU 的进程数。
Mark Word:记录了对象和锁的有关信息,储存对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁标志位、线程持有的锁、偏向线程ID、偏向时间戳、对象分代年龄等。注意这个Mark Word结构并不是固定的,它会随着锁状态标志的变化而变化,而且里面的数据也会随着锁状态标志的变化而变化,这样做的目的是为了节省空间。
在线程进入同步方法、同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为”01”状态,是否为偏向锁为”0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Recored)的空间,用于储存锁对象目前的Mark Word的拷贝(官方把这份拷贝加了个Displaced前缀,即Displaced Mark Word)。