减少资源的开销,可以减少每次创建销毁线程的开销提高响应速度,由于线程已经创建成功提高线程的可管理性
1、计算密集型: 设置线程数为CPU数+1,多了也没有更多的CPU核心来执行
2、IO密集型:通常设置为CPU数*2,因为大部分时间系统在等待IO,CPU空闲时间多,可以适当调大CPU数
try {
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
看到这里就应该知道了,我们的线程在获取任务时,如果队列中已经没有任务,会在此处阻塞keepALiveTime的时间,如果到时间都没有任务,就会return null(不是直接返回null,是最终),然后在runWorker()方法中,执行
processWorkerExit(w, completedAbruptly);
先进先出算法(FIFO,First-In-First-Out)
最短耗时任务优先算法(Shortest Job First, SJF) - 缺点:容易饥饿
时间片轮转算法(Round Robin)
最大最小公平算法( Max-Min Fairness )
不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。
Final 变量在并发当中,原理是通过禁止cpu的指令集重排序,保证了对象的内存可见性, final 域能确保初始化过程的安全性, 防止对象引用在对象被完全构造完成前被其他线程拿到并使用( final 可以保证正在创建中的对象不能被其他线程访问到)
final域的写规则要求编译器在final域的写之后,构造函数return之前插入store-strore屏障
final域的读规则要求编译器在final域的读操作之前插入load-load屏障
Java内存模型(Java Memory Model ,JMM),屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。
Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。
而JMM就作用于工作内存和主存之间数据同步过程。他规定了如何做数据同步以及什么时候做数据同步,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。目的是保证并发编程场景中的原子性、可见性和有序性,解决多线程之间数据正确同步的问题
采用了 CAS 操作,每次从内存中读取数据然后将此数据和+1 后的结果进行,CAS 操作,如果成功就返回结果,否则重试直到成功为止。而 compareAndSet 利用 JNI 来完成CPU 指令cmpxchg的操作
程序会根据当前处理器的类型来决定是否为cmpxchg指令添加lock前缀。如果程序是在多处理器上运行,就为cmpxchg指令加上lock前缀(lock cmpxchg)
lock前缀指令说明
现代cpu的寄存器与内存之间存在L1,L2,L3高速缓存,频繁使用的内存会缓存在高速缓存中,此时以缓存锁定来代替总线锁定,利用缓存一致性机制来保证操作的原子性。从上面二三点我们也可以看出cas同时包含了volatile读和写的内存语义
volatile可以保证线程可见性且禁止指令重排序,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的, 加入volatile关键字时,汇编后会多出一个lock前缀指令。lock前缀指令其实就相当于一个内存屏障
1、LOCK前缀指令使得当前缓存行的数据写回主存
2、写回主存的操作使得其它CPU中缓存了该内存地址的数据无效(缓存一致性原则,嗅探总线上传播的信号来检查自己缓存中的数据是否过期,当发现自己的数据被修改时,强制从主存中重新读取数据)
volatile的优化: linkedTransferQueue,利用字节追加的方式,使得一个节点占64字节,每次锁定只锁定一个缓存行,优化出队入队效率,解决了伪共享的问题
上面说到一个缓存行的大小一般为64个字节大小,这里我们来做一个假设,当缓存行中有两个变量a,b,当一个线程对a变量进行写操作的时候,会将另一个处理器中缓存行中b设置为无效,另一个线程在对b进行写操作的时候,会将其他处理器缓存行中的a变量设置为无效,这样就产生了写冲突,也叫伪共享,填充到64字节后,不同节点的写不会冲突
原理:
synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性
底层实现:
1)同步代码块是使用monitorenter和monitorexit指令实现的, ,当且一个monitor被持有之后,他将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁;
2)同步方法(在这看不出来需要看JVM底层实现)依靠的是方法修饰符上的ACC_SYNCHRONIZED实现。
最终都是通过JVM层面监视器锁的获取实现
Java对象头和monitor是实现synchronized的基础!synchronized存放的位置:synchronized用的锁是存在Java对象头里的。
其中, Java对象头包括:
As-if-serial语义保证单线程中执行结果不会改变,happens-before原则保证正确同步的多线程之间执行结果不会改变
乐观锁:比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。
悲观锁:比较适合写入操作比较频繁的场景,如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。
乐观锁:乐观锁的特点先进行业务操作,不到万不得已不去拿锁。即“乐观”的认为拿锁多半是会成功的,因此在进行完业务操作需要实际更新数据的最后一步再去拿一下锁就好。
悲观锁:悲观锁的特点是先获取锁,再进行业务操作,即“悲观”的认为获取锁是非常有可能失败的,因此要先确保获取锁成功再进行业务操作。通常所说的“一锁二查三更新”即指的是使用悲观锁。
锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。 注意锁可以升级不可降级
偏向锁:当没有竞争出现时,默认会使用偏向锁。JVM 会利用 CAS 操作(compare and swap),在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。
自旋锁:自旋锁 for(;;)结合cas确保线程获取取锁。就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。怎么等待呢?执行一段无意义的循环即可(自旋)。
轻量级锁:当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁
重量级锁:重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高
synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级
队列式同步器。这个抽象类对于JUC并发包非常重要,JUC包中的ReentrantLock,,Semaphore,ReentrantReadWriteLock,CountDownLatch 等等几乎所有的类都是基于AQS实现的。
AQS 中有两个重要的东西,一个以Node为节点实现的链表的队列(CHL队列),还有一个STATE标志,并且通过CAS来改变它的值
state状态如下(暂时只需要知道如果这个值 大于0 代表此线程取消了等待,0为初始化状态):
static final int CANCELLED = 1:// 代码此线程取消了争抢这个锁
static final int SIGNAL = -1;: // 官方的描述是,其表示当前node的后继节点对应的线程需要被唤醒
static final int CONDITION = -2; // 本文不分析condition,所以略过吧,下一篇文章会介绍这个
static final int PROPAGATE = -3; // 同样的不分析,略过吧
独占非公平锁加锁过程:
public final void acquire(int arg) { // 此时 arg == 1
// 首先调用tryAcquire(1)一下,名字上就知道,这个只是试一试
// 因为有可能直接就成功了呢,也就不需要进队列排队了,
// 对于公平锁的语义就是:本来就没人持有锁,根本没必要进队列等待(又是挂起,又是等待被唤醒的)
if (!tryAcquire(arg) &&
// tryAcquire(arg)没有成功,这个时候需要把当前线程挂起,放到阻塞队列中。
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
selfInterrupt();
}
}
tryAcquire尝试加锁
如果state == 0
公平锁锁 判断等待队列为空 再cas设置state
非公平锁锁 直接cas设置state
如果state>0 ,再判断当前获取锁的线程(current == getExclusiveOwnerThread()) 是不是当前线程 是, state++(重入)
addWaiter(加锁失败,存在锁竞争,尝试CAS入阻塞等待队列,CAS设置自己为队尾),即enq操作是个循环CAS入等待队列的操作
循环中, 若队列为空,创建头节点,再循环CAS入队直到成功
3、acquireQueued方法中,判断如果当前节点是阻塞队列队头,尝试抢锁,
抢锁成功时,设置自己为队列头结点,同时返回
抢锁失败时,判断是否需要挂起(根据前驱节点waitStatus判断)
waitStatus=-1 标识状态正常,需要被挂起 返回true
waitStatus>0 标识前驱节点已经取消抢锁,往前找,直到找到节点的waitStatus<0 返回false
waitStatus = 0 此时CAS将前驱节点的waitStatus设置为Node.SIGNAL(也就是-1)
4、如果需要被挂起,利用LockSupport挂起线程
独占锁解锁过程:
public final boolean release(int arg) {
// 往后看吧
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
// 回到ReentrantLock看tryRelease方法
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
// 是否完全释放锁
boolean free = false;
// 其实就是重入的问题,如果c==0,也就是说没有嵌套锁了,可以释放了,否则还不能释放掉
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
大概大家也可以猜到,Semaphore 其实也是 AQS 中共享锁的使用,因为每个线程共享一个池嘛。
套路解读:创建 Semaphore 实例的时候,需要一个参数 permits,这个基本上可以确定是设置给 AQS 的 state 的,然后每个线程调用 acquire 的时候,执行 state = state - 1,release 的时候执行 state = state + 1,当然,acquire 的时候,如果 state = 0,说明没有资源了,需要等待其他线程 release
countDown() 方法每次调用都会将 state 减 1,直到 state 的值为 0;而 await 是一个阻塞方法,当 state 减为 0 的时候,await 方法才会返回。await 可以被多个线程调用,所有调用了 await 方法的线程阻塞在 AQS 的阻塞队列中,等待条件满足countdown将state-1使得(state == 0),将线程从队列中一个个唤醒过来
CyclicBarrier 相比 CountDownLatch 来说,要简单很多,其源码没有什么高深的地方,它是 ReentrantLock 和 Condition 的组合使用
/** The lock for guarding barrier entry */
private final ReentrantLock lock = new ReentrantLock();
// CyclicBarrier 是基于 Condition 的
// Condition 是“条件”的意思,CyclicBarrier 的等待线程通过 barrier 的“条件”是大家都到了栅栏上
private final Condition trip = lock.newCondition();
// 参与的线程数
private final int parties;
await方法
// 注意到这里,这个是从 count 递减后得到的值
int index = --count;
// 如果等于 0,说明所有的线程都到栅栏上了,准备通过
if (index == 0) { // tripped
非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面
StampedLock的特点
所有获取锁的方法,都返回一个邮戳(Stamp),Stamp为0表示获取失败,其余都表示成功;
所有释放锁的方法,都需要一个邮戳(Stamp),这个Stamp必须是和成功获取锁时得到的Stamp一致;
StampedLock是不可重入的;(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)
支持锁升级跟锁降级
可以乐观读也可以悲观读
使用有限次自旋,增加锁获得的几率,避免上下文切换带来的开销
乐观读不阻塞写操作,悲观读,阻塞写得操作
StampedLock的优点
相比于ReentrantReadWriteLock,吞吐量大幅提升
StampedLock的缺点
api相对复杂,容易用错
内部实现相比于ReentrantReadWriteLock复杂得多