最近加班太多,好久没有更新博客了,周末看了下阿里大神并发的书籍,把知识点作个记录。node
一:线程安全的定义算法
当多个线程并发访问某个类时,若是不用考虑运营环境下的调度和交替运行,且不须要额外的辅助,这里认为这个类就是线程安全的。数据库
原子操做的描述:一个操做单元要么所有成功,要么所有失败。后面再看看数据库中关于事务的ACID,这里A表示的就是原子特性,一个事务单元要么成功,要么失败(数据库中的单机事务依据的是undo),与I要作好区分。缓存
二:关于指令重排序安全
JVM能够根据机器的特性(我以为主要是cpu的多级缓存、多核处理),适当的从新排序机器指令,使机器指令更加符合cpu执行特色,发挥机器的最大性能。服务器
三:Happens-Before多线程
没啥好说的了,动做A\B的顺序关系。happens-before有一堆的乱七八糟规则。并发
四:volatile语义app
volatile至关于synchronized的弱实现,说的通白点,就是volatile实现了后者的语义,可是没有后者的锁机制。高并发
volatile不会被缓存在寄存器当中或者其余cpu不可见的地方,每次都是从主存中读取最新的结果值。
可是,可是,volatile并不能保证线程安全。
五:比较重要的概念,CAS
引入这个概念之前,看下咱们用锁解决并发会致使的问题:
1.在高并发场景,加锁、释放锁会致使频繁的上下文切换,引起性能问题。(因为我作游戏服务器,以前测试机器人200同频战斗,io线程数量设置为cpu*2并无cpu的执行效率高)。
2.一个线程持有锁,其余的线程被挂起。这里有可能某个优先级的高的线程等待优先级低的线程,从而致使优先级致使,会不会引起性能风险呢?
什么是独占锁,悲观锁,乐观锁?
独占锁也是悲观锁,synchronized就是悲观锁。反之,更有效的就是乐观锁,假设有冲突就重试,直到完成。
CAS:compare and swap
cas的三个操做数:内存值V,旧的预期值A,要修改的新值B,仅当A==V时,才把V改为B。
现代cpu提供了特殊的指令,能够自动更新共享的数据,而且可以检测到其余线程的干扰。
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
在这里采用了CAS操做,每次从内存中读取数据而后将此数据和+1后的结果进行CAS操做,若是成功就返回结果,不然重试直到成功为止。
而compareAndSet利用JNI来完成CPU指令的操做。
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
总体的过程就是这样子的,利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法。其它原子操做都是利用相似的特性完成的。
CAS引发的ABA问题?
六:关于AQS概念的引入
号称J.U.C最复杂的一个类,我从网上找到一些,后面略过。。。
基本的思想是表现为一个同步器,支持下面两个操做:
获取锁:首先判断当前状态是否容许获取锁,若是是就获取锁,不然就阻塞操做或者获取失败,也就是说若是是独占锁就可能阻塞,若是是共享锁就可能失败。另外若是是阻塞线程,那么线程就须要进入阻塞队列。当状态位容许获取锁时就修改状态,而且若是进了队列就从队列中移除。
while(synchronization state does not allow acquire){
enqueue current thread if not already queued;
possibly block current thread;
}
dequeue current thread if it was queued;
释放锁:这个过程就是修改状态位,若是有线程由于状态位阻塞的话就唤醒队列中的一个或者更多线程。
update synchronization state;
if(state may permit a blocked thread to acquire)
unlock one or more queued threads;
要支持上面两个操做就必须有下面的条件:
状态位的原子操做
这里使用一个32位的整数来描述状态位,前面章节的原子操做的理论知识整好派上用场,在这里依然使用CAS操做来解决这个问题。事实上这里还有一个64位版本的同步器
(AbstractQueuedLongSynchronizer),这里暂且不谈。
阻塞和唤醒线程
标准的JAVA API里面是没法挂起(阻塞)一个线程,而后在未来某个时刻再唤醒它的。JDK 1.0的API里面有Thread.suspend和Thread.resume,而且一直延续了下来。可是这
些都是过期的API,并且也是不推荐的作法。
在JDK 5.0之后利用JNI在LockSupport类中实现了此特性。
LockSupport.park()
LockSupport.park(Object)
LockSupport.parkNanos(Object, long)
LockSupport.parkNanos(long)
LockSupport.parkUntil(Object, long)
LockSupport.parkUntil(long)
LockSupport.unpark(Thread)
上面的API中park()是在当前线程中调用,致使线程阻塞,带参数的Object是挂起的对象,这样监视的时候就可以知道此线程是由于什么资源而阻塞的。因为park()当即返回,因此
一般状况下须要在循环中去检测竞争资源来决定是否进行下一次阻塞。park()返回的缘由有三:
其实第三条就决定了须要循环检测了,相似于一般写的while(checkCondition()){Thread.sleep(time);}相似的功能。
AQS采用的CHL模型采用下面的算法完成FIFO的入队列和出队列过程。
对于入队列(enqueue):采用CAS操做,每次比较尾结点是否一致,而后插入的到尾结点中。
do {
pred = tail;
}while ( !compareAndSet(pred,tail,node) );
对于出队列(dequeue):因为每个节点也缓存了一个状态,决定是否出队列,所以当不知足条件时就须要自旋等待,一旦知足条件就将头结点设置为下一个节点。
while (pred.status != RELEASED) ;
head = node;
AQS里面有三个核心字段:
private volatile int state;
private transient volatile Node head;
private transient volatile Node tail;
其中state描述的有多少个线程取得了锁,对于互斥锁来讲state<=1。head/tail加上CAS操做就构成了一个CHL的FIFO队列。下面是Node节点的属性。
volatile int waitStatus; 节点的等待状态,一个节点可能位于如下几种状态:
- CANCELLED = 1: 节点操做由于超时或者对应的线程被interrupt。节点不该该留在此状态,一旦达到此状态将从CHL队列中踢出。
- SIGNAL = -1: 节点的继任节点是(或者将要成为)BLOCKED状态(例如经过LockSupport.park()操做),所以一个节点一旦被释放(解锁)或者取消就须要唤醒(LockSupport.unpack())它的继任节点。
- CONDITION = -2:代表节点对应的线程由于不知足一个条件(Condition)而被阻塞。
- 0: 正常状态,新生的非CONDITION节点都是此状态。
- 非负值标识节点不须要被通知(唤醒)。
volatile Node prev;此节点的前一个节点。节点的waitStatus依赖于前一个节点的状态。
volatile Node next;此节点的后一个节点。后一个节点是否被唤醒(uppark())依赖于当前节点是否被释放。
volatile Thread thread;节点绑定的线程。
Node nextWaiter;下一个等待条件(Condition)的节点,因为Condition是独占模式,所以这里有一个简单的队列来描述Condition上的线程节点。