Java基础知识复习(三)
5 Java并发
synchronized
synchronized是jdk提供的jvm层面的同步机制。他解决的是多线程之间访问共享资源的同步问题,它保证再它修饰的方法或代码块同一时间只有一个线程执行。
在早期的Java八本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java的线程是映射到操作系统的原生线程之上的。如果需要挂起或者唤醒一个线程,都需要操作系统帮忙完成,挂起或唤醒一个线程都需要从用户态转换为内核态,这个状态之间的转换需要相对长的时间,时间成本相对较高。
在Java6之后,Java官方对从JVM层面对synchronized做了较大优化,所以现在synchronized锁效率也挺不错了。
如何使用 synchronized 关键字
三种方式:
- 修饰实例方法:作用与当前对象实例的锁,进入同步代码前要获得当前对象实例的锁。
- 修饰静态方法:修饰静态方法其实就是给当前类加锁,因为静态方法是属于类的。所以如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
- 修饰代码块:指定枷锁对象,对给定对象枷锁,进入同步代码块前要获得给定对象的锁。
synchronized关键字底层原理
synchronized代码块底层原理
先编写一份测试代码:
public class Test {
public static void main(String[] args) {
synchronized (Test.class){
System.out.printf("current thread name: %s\n",Thread.currentThread().getName());
}
}
}
然后使用命令对Test.class进行反编译:输入javap -c -v -s -l Test.class
可以看到,在进入synchronized同步代码块时,底层字节码编译出来的指令为monitorenter,而退出synchronized同步代码块时,编译的指令为 monitorexit。当执行monitorenter指令时,当前线程将视图获取objectref(即对象锁) 所对应的 monitor 的持有权,在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的 ,它是一个c++对象:
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0; // 锁的重入次数
_object = NULL; // synchronized的锁对象
_owner = NULL; // 拥有该monitor的线程
_WaitSet = NULL; // 处于wait状态的线程会被加入到此set集合中
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; // 多线程竞争锁时的单向队列
FreeNext = NULL ;
_EntryList = NULL ; // 竞争失败后,陷入阻塞的线程会被加入到此队列
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
如果对象锁monitor的_recursions计数器为0,那么线程就可以成功的获取到这个锁,此时 _recursions计数器置为1,如果当前线程已经用了monitor的持有权的,那它也可以重入这个monitor,此时 _recursions计数器也会+1,倘若其他线程已经拥有monitor的持有权,那么当前线程就会进入阻塞状态,直到当前持有monitor持有权的线程执行完毕,即执行完monitorexit指令,此时持有monitor的线程就会释放monitor,并将monitor的计数器归0,此时其他的线程又可以争抢monitor的持有权了。值得注意的是,编译器将会确保无论方法以哪种形式结束(正常退出或者异常退出),方法中执行过的monitorenter指令都有一条对应的monitorexit指令可以正确配对执行。编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。从字节码中也可以看出多了一个monitorexit指令,它就是异常结束时被执行的释放monitor 的指令。
synchronized方法底层原理
synchronized修饰的方法并没有 monitorenter 指令和 monitorexit 指令修饰,它的同步形式是隐式的,取而代之的是一个ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
Synchroized和ReentrantLock的区别
- Synchroized是基于JVM层面的同步机制,而ReentrantLock是基于Java API层面提供的同步机制。
- Synchroized和Reentrantlock都属于可重入锁。
- ReetrantLock提供了比Synchronized更高级的功能:
- 公平锁
- 更方便的线程间的通信(Condition)
- 等待可中断(在线程等待获取锁的时候可以被中断)
乐观锁
乐观锁对共享的数据很乐观,认为不会发生线程安全的问题,从而不给数据加锁。乐观锁适用于读多写少的环境。常见的例子是mysql的更新使用version控制。
悲观锁
悲观锁对共享的数据很悲观,认为无论什么时候都有可能发生线程安全的问题,所以在每次读写数据的时候都会加锁。
Synchronized属于悲观锁。
独占锁
锁一次只能被一个线程占用使用。
Synchronized和ReetrantLock都是独占锁。
共享锁
锁可以被多个线程持有。
对于ReentrantReadWriteLock而言,它的读锁是共享锁,写锁是独占锁。
公平锁
公平锁指根据线程在队列中的优先级获取锁,比如线程优先加入阻塞队列,那么线程就优先获取锁。
非公平锁
非公平锁指在获取锁的时候,每个线程都会去争抢,并且都有机会获取到锁,无关线程的优先级。
可重入锁(递归锁)
一个线程获取到锁后,如果继续遇到被相同锁修饰的资源,那么就可以继续获取该锁。
Synchronized和Reentrantlock都是可重入锁。
偏向锁
在线程获取偏向锁的时候,jvm会判断对象MarkWord里偏向线程的ID是否为当前线程ID。
如果是,说明当前锁对象处于偏向状态。
如果不是,则JVM尝试CAS把对象的MarkWord的偏向线程ID设置为当前线程ID,
如果设置成功,那么对象偏向当前线程,并将当前对象的锁标志位改为01。
如果设置失败,则说明多线程竞争,将撤销偏向锁,升级为轻量级锁。
偏向锁使用与单线程无所竞争环境(单线程环境)
所以当只有一个线程的时候,默认是偏向锁。
轻量级锁
在线程获取对象锁时,JVM首先会判断对象是否为无锁状态(无锁状态标志位为01)。
如果对象是无锁状态,那么将在线程的栈帧中开辟一块空间用于存储对象的MarkWord,然后将对象的MarkWord复制到栈帧空间去,并使用CAS更新对象的MakrWord为指向线程栈帧的指针。
如果更新成功,那么当前线程获取锁成功,并修改对象的MarkWord标志位为00。
如果更新失败,那么JVM会判断对象的MarkWord是否已经指向线程的栈帧。
如果已经指向,那么线程直接执行同步代码。否则,说明多个线程竞争,将inflate为重量级锁。
轻量级锁使用于多线程无锁竞争环境(多线程轮流执行,并不会发生冲突)
自旋锁
在争夺锁过程中,线程不会停止获取锁,二十通过CAS不断的判断线程是否符合获取锁的条件。
自适应自旋锁
自旋锁意味着线程会不断的消耗cpu资源,短时间还行,长时间意味着资源的浪费。所以自适应自旋锁会有一个自旋的生命周期,过了这个生命周期,线程将不在自旋。
锁消除
锁消除属于Java编译器对程序的一种优化机制。锁消除是指当JVM的JIT编译器检测出一些已经加锁的代码不可能出现共享的数据存在竞争的问题,会消除这样的锁。锁消除的依据来源于逃逸分析算法。如果判断到一段代码,在堆上的数据不会逃逸出去被其他线程访问到,那么就把它们当作栈上的数据,为线程私有的,所以无需加锁。
锁粗化
当虚拟机检测一系列连续的操作都对同一个连续域连续加锁,那么它会把加锁的范围扩大至整个操作的序列外部,保证只加一次锁
public String t(){
StringBuffer stringBuffer = new StringBuffer();
for (int i = 0; i < 100; i++) {
stringBuffer.append(i); // stringBuffer对每一个appen方法都会加锁,如果执行100次,就是100次加锁,显然不太可能
}
return stringBuffer.toString();
}
经过锁粗化的优化后,可能是这样的:
public String t() {
StringBuffer stringBuffer = new StringBuffer();
synchronized (stringBuffer) {
for (int i = 0; i < 100; i++) {
stringBuffer.append(i); // append方法不会再上锁
}
}
return stringBuffer.toString();
}
死锁
死锁是指多个线程在执行过程中,循环等待彼此占用的资源而导致的无限期阻塞的情况。
产生死锁的条件:
- 互斥条件: 一个资源在一段时间内只能被一个进程所持有。
- 不可抢占条件:进程所持有的资源只能由进程自己主动释放,其他资源的申请者不能向进程持有者抢夺资源
- 占有且申请条件:进程已经持有一个资源后,又申请其他资源,但是其他资源已经被其他线程所占有。
- 循环等待条件:进程1有进程2需要申请的资源,进程2有进程1需要申请的资源。那么这两个线程不停的等待彼此持有的资源,又不释放已拥有的资源,陷入循环等待。
死锁的例子:
public class DeadLock {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
Thread t = new Thread(()->{
synchronized (lock1){
try {
System.out.println(Thread.currentThread().getName()+" get lock1 ing!");
Thread.sleep(500);
System.out.println(Thread.currentThread().getName()+" after sleep 500ms!");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" is waiting!");
synchronized (lock2){
System.out.println(Thread.currentThread().getName()+" get lock2 ing!");
}
}
});
Thread t2 = new Thread(()->{
synchronized (lock2){
try {
System.out.println(Thread.currentThread().getName()+" get lock2 ing!");
Thread.sleep(500);
System.out.println(Thread.currentThread().getName()+" after sleep 500ms!");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" is waiting!");
synchronized (lock1){
System.out.println(Thread.currentThread().getName()+" get lock1 ing!");
}
}
});
t.start();
t2.start();
}
}
如何避免死锁?
为了避免死锁,我们只需要破环产生死锁条件的其中一个就行了,但是前两个条件我们无法改变,就只能改变第三个或第四个条件:
- 打破第三个条件:实现资源的有序分配。
- 打破第四个体条件:设置等待超时时间。
volatile
Java中的内存模型
在jdk1.2之前,Java的内存模型实现总是从主内存读取变量 ,是不需要特别的注意的。而在当前的Java内存模型下,线程可以把变量保存本地内存(线程私有)中,而不是从主内存中读写。也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到线程的本地内存当中,那么线程进行计算时就可以直接从它的本地内存读取数据和向其中写入数据,当运算结束之后,再将本地内存中的数据刷新到主存当中。举个简单的例子,比如下面的这段代码:
i = i + 1;
会先从主存当中读取i的值,然后复制一份到本地内存当中,然后线程执行指令对i进行加1操作,然后将数据写入本地内存,最后将本地内存中i最新的值刷新到主存当中。
这段代码在单线程环境下是没有问题的,但是在多线程中运行就有问题了。在多核CPU中,每条线程可能运行于不同的CPU中,因此每个线程都有自己的本地缓存。
比如现在我们有两个线程,我们希望两个线程执行完上面的语句,最终i的值为2,但最终的结果却不是如此。
最终的结果是1,一开始,两个线程分别读取i的值存入自己的本地缓存中,然后线程1进行+1操作,然后把i的最新值1写入到内存中,但是此时线程2的本地缓存中i的值还是0,这样再进行+1操作后,i的值还是1。这就是缓存一致性为题。通常称这种被多个线程访问的变量为共享变量。
如何解决缓存一致性问题
- 加锁,使用synchroized修饰方法或代码块,或者用ReentrantLock为代码块加锁。
- 通过缓存一致性协议
并发编程中的三个概念
1.原子性
原子性:即一个操作,要么执行完整,要么就不执行,在执行的过程中不会被任何因素打断。
2.可见性
可见性是指,在并发编程中,多个线程同时访问同一个变量,其中某一个线程对这个变量进行修改,其他线程能立即看得到修改后的值。
3.有序性
有序性是指,程序执行循序按照代码的先后循序执行。
举个例子:
int a = 1;
int b = 3;
int c = a + b;
这里的有三段代码,正常按我们的理解来说,三段代码的执行顺序应该是1->2->3,但是在Java程序里面,可能不会是这样运行,也有可能是2->1->3
这是为什么呢,这里发生的现象叫做指令重排
3.1 指令重排
处理器为了提高程序运行效率,可能会对输入的代码进行优化,它不保证各个语句的执行顺序是否和我们编写的一样,但是它会保证最终的结果是和正常执行的结果是一样的。
既然上面说到编译器为了提高效率进行了指令重排,那么它是怎么保证最终结果和正常执行的结果是一致的呢?
int a = 1;
int b = 3;
int c = a + b;
假设我们先执行了3语句,但是3语句依赖着两个值:a,b。那么编译器就会去寻找a,b的值,此时编译器就会确保1、2语句在3语句之前执行,这样就保证了最终结果和正常执行的结果是一致的。
volatile保证内存的可见性
上面我们说到,多个线程访问同一个共享变量容易造成缓存一致性问题。那么我们可以通过将变量声明为volatile,这就指示JVM,这个变量是不稳定的,每次使用它都必须从主内存中读取。
说白了,volatile 关键字的主要作用就是保证变量的可见性然后还有一个作用是防止指令重排序。
volatile如何禁止指令重排序,能保证有序性吗
volatile通过提供内存屏障来防止指令重排序。java内存模型回在每个volatile写操作前后都会插入store指令,将工作内存中的变量同步会主内存。在每个volatile读擦操作前后都会插入load指令,从主内存中读取变量。
既然禁止了指令重排序,那么在一定程度上就会保证有序性。
volatile保证原子性吗
既然volatile能保证可见性,那么volatile能保证对变量的操作是原子性吗?
让我们来看一个例子:
i++;
自增操作不是一个原子性操作,自增操作可以分解为3个部分:
1、获取到i的值
2、i的值+1
3、写回内存
虽然这三个操作都是原子性操作,但是合起来它就不是一个原子性操作。volatile能保证可见性,是在对变量的操作完毕, 然后写回主内存,其它内存才知道i的值被修改了,但是在上述的三个操作中,任意一个操作都可能有线程正在运行。
比如线程A正在执行2操作,但是现在线程B过来了,它读取到的值还是i原来值,这时候线程A中i的值和线程B中i的值就不一致了,所以volatile不能保证原子性。要解决这个问题还是需要给操作加锁。
ThreadLocal
ThreadLocal简介
ThreadLocal为每个线程都提供了一份相同的变量副本,每个线程都可以修改这个副本,但不用担心于与其他线程发生数据冲突,实现了线程之间的数据隔离。
ThreadLocal示例
import java.text.SimpleDateFormat;
import java.util.Random;
/**
* @Description : TODO
* @Author : Weleness
* @Date : 2020/06/16
*/
public class ThreadLocalExample implements Runnable{
private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(()->new SimpleDateFormat("yyyyMMdd HHmm"));
public static void main(String[] args) throws InterruptedException {
ThreadLocalExample obj = new ThreadLocalExample();
for (int i = 0; i < 10; i++) {
Thread t = new Thread(obj,""+i);
Thread.sleep(new Random().nextInt(1000));
t.start();
}
}
@Override
public void run() {
System.out.println("Thread Name = "+Thread.currentThread().getName() + " default Formatter = "+formatter.get().toPattern());
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
formatter.set(new SimpleDateFormat());
System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern());
}
}
output:
Thread Name = 0 default Formatter = yyyyMMdd HHmm
Thread Name= 0 formatter = y/M/d ah:mm
Thread Name = 1 default Formatter = yyyyMMdd HHmm
Thread Name = 2 default Formatter = yyyyMMdd HHmm
Thread Name= 1 formatter = y/M/d ah:mm
Thread Name= 2 formatter = y/M/d ah:mm
Thread Name = 3 default Formatter = yyyyMMdd HHmm
Thread Name = 4 default Formatter = yyyyMMdd HHmm
Thread Name= 3 formatter = y/M/d ah:mm
Thread Name= 4 formatter = y/M/d ah:mm
Thread Name = 5 default Formatter = yyyyMMdd HHmm
Thread Name = 6 default Formatter = yyyyMMdd HHmm
Thread Name = 7 default Formatter = yyyyMMdd HHmm
Thread Name= 5 formatter = y/M/d ah:mm
Thread Name= 6 formatter = y/M/d ah:mm
Thread Name = 8 default Formatter = yyyyMMdd HHmm
Thread Name= 8 formatter = y/M/d ah:mm
Thread Name= 7 formatter = y/M/d ah:mm
Thread Name = 9 default Formatter = yyyyMMdd HHmm
Thread Name= 9 formatter = y/M/d ah:mm
可以看到thread-0虽然已经改变了formatter的值,但是thread-1默认格式化程序与初始值相同,其他线程也一样。
ThreadLocal原理
从Thread
类源码入手
public
class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}
Thread
类源码有一个threadLoacls
和一个inheritableThreadLocals
变量,它们都是ThreadLocalMap
的变量,ThreadLoaclMap
是ThreadLoacl
的一个静态内部类,通过Entry
类存储值
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
可以把 ThreadLocalMap
理解为ThreadLoacl
类实现的定制化的HashMap
。默认情况下这两个变量都是null,只有当前线程调用ThreadLoacl
类的set
或get
方法时才会创建它们,实际上调用这两个方法时,调用的是ThreadLocalMap
类对应的get
和set
方法
ThreadLocal
的set()
方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
总结:
- 每一个线程都维护一个
ThreadLocalMap
的引用 ThreadLocalMap
是ThreadLocal
的内部类,用Entry进行存储- 调用
ThreadLocal
的set()
方法时,实际上就是往ThreadLocalMap
设置值,key是ThreadLocal
对象,值是传递过来的泛型变量。 - 调用
ThreadLocal
的get()
方法时,实际上就是往ThreadLocalMap
获取值,key是ThreadLocal
对象 - ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。
ThreadLocal 内存泄漏问题
在ThreadLocalMap中,是用key为ThreadLocal
的弱引用,而value是强引用:
static class Entry extends WeakReference<ThreadLocal<?>> {// 所谓弱引引用指的是被弱引用修饰的类
/** The value associated with this ThreadLocal. */
Object value; // 普通的对象为强引用
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
在来垃圾回收的时候,JVM扫描到的所有弱引用对象都会被JVM回收掉,而value是强引用,则不会被清理掉。这样一来,ThreadLocalMap
中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。但是也不必太过担心, 因为设计者已经想到了这点,所以ThreadLocal会自动处理key 为 null的 value。
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) { // 如果key是null ,则会进行处理
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
使用完 ThreadLocal
方法后 最好手动调用remove()
方法。
线程池
为什么要用线程池
HTTP连接池,数据库连接池,线程池等等都是使用了池化技术。池化技术的思想就是为了减少每次获取资源的消耗,提高对资源的利用率。
线程池提供了一种限制和管理资源(包括执行一个任务)。每个线程池还维护一些基本统计信息,例如已完成任务的数量。
线程池的好处:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等待线程的创建就能立即执行。
- 提供线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
Runnable接口和Callable接口的区别
Runnalbe
自Java1.0以来一直存在,但Callable
仅在Java1.5中引入,目的是为了处理Runnable
不支持的用例。
Runnable
接口不会返回结果或者抛出检查异常,但是Callable
接口可以。所以,如果任务不需要返回结果或抛出异常推荐使用Runnable
接口,这样代码更加简洁。
执行execute()方法和submit()的区别
1.execute()
方法用于提交不需要返回值的仍无,所以无法判断任务是否被线程池成功执行。
2.submit()
方法用于提交需要返回值的任务。线程池会返回一个Future
类型的对象,通过这个Future
对象可以判断任务是否被成功执行,并且可以通过Future
的get()
方法来获取返回值,get()
方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)
方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
如何创建线程池
《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,避免资源耗尽的风险
Executors 返回线程池对象的弊端如下:
- FixedThreadPool 和 SingleThreadExecutor:允许请求的队列长度为 Integer.MAX_VALUE , 可能会堆积大量的请求,从而导致OOM。
- CachedThreadPool 和 ScheduledThreadPool:允许创建的线程数量为 Integer.MAX_VALUE,可能会创建大量线程,从而导致OOM。
方法一:通过构造方法实现
方式二:通过 Executor框架的工具类Executors来实现,我们可以创建三种类型的ThreadPoolExecutor
- FixedThreadPool:该方法返回一个固定线程数量的线程池。该线程池中的 线程数量始终不变。当有一个新的仍无提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有空闲线程时,便处理在任务队列中的任务。
- SingleThreadExecutor:方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
- CachedThreadPool:该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不去欸但那个,但若有空闲线程可以复用,则会优先使用可复用的线程。若线程均在工作,又有新的任务提交,则会创建新的线程处理人物。所有线程在当前任务执行完毕后,将线程池复用。
ThreadPoolExecutor 类分析
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
ThreadPoolExecutor构造函数重要参数分析
ThreadPoolExecutor
3个最重要的参数:
corePoolSize
: 核心线程数定义了最小可以同时允许的线程数量maximumPoolSize
:当队列中存放的任务达到队列容量时,当前可以同时允许的线程数量变为最大线程数。workQueue
:当新任务来的时候会先判断当前允许的线程数是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
ThreadPoolExecutor
其他常见参数
1、keepAliveTime
:当线程池中的线程数量大于corePoolSIze
时,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是等待,直到等待时间超过了keepAliveTime
才会被回收销毁。
2、unit
:keepAliveTime
参数的时间单位。
3、threadFactory
:executor 创建新线程的时候会用到。
4、handler
:饱和策略。当没有任何空闲线程时执行的策略。
ThreadPoolExecutor
饱和策略:
定义:
如果当先同时运行的线程数量达到最大线程数并且队列也已经被填满时,ThreadPoolTaskExecutor
定义一些策略:
ThreadPoolExecutor.AbortPolicy
: 抛出RejectedExecutionException
来拒绝新任务的处理。ThreadPoolExecutor.CallerRunsPolicy
:调用执行自己的线程运行任务。您不会拒绝任务请求。到那时这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能丢弃任何一个任务请求的话,可以使用这个策略。ThreadPoolExecutor.DiscardPolicy
: 不处理新任务,直接丢弃掉。- **
ThreadPoolExecutor.DiscardOldestPolicy
:**此策略将丢弃最早的未处理亏待任务请求。
线程池执行原理
如果我们在代码中放置了10个任务,我们配置的核心线程数为5,等待队列容量为100,所以每次只可能 存在5个任务同时执行,剩下的5个任务会被存放在等待队列中,等待当前某个正在执行的任务执行完后,才会开始执行。
CAS
CAS: Compare And Swap 比较成功并交换。CAS体现的是一种乐观锁机制。CAS涉及到三个元素:指定的内存地址,期盼值和目标值。将指定内存地址的值与期盼值相比较,如果成功就将内存地址的值为目标值。
CAS在Java中的底层实现
CAS在Java中的实现是 juc的atomic包下的Atomicxx原子类。
我们知道普通的自增操作(i++)不是原子性的,但是可以使用AtomicInteger
来保证自增的原子性。
public class Test {
public AtomicInteger i;
public void add() {
i.getAndIncrement();
}
}
我们来看getAndIncrement
的内部:
public final int getAndIncrement() {
return U.getAndAddInt(this, VALUE, 1);
}
再深入到getAndAddInt
():
@HotSpotIntrinsicCandidate
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
return v;
}
这里我们见到了weakCompareAndSetInt()
,它是compareAndSetInt()
方法的一个封装,CAS
缩写的由来compareAndSetInt()
(本人jdk版本jdk12,jdk8是CompareAndSwapInt
),compareAndSetInt()
是一个本地native方法,想要知道怎么实现的需要去hotspot源码中查看。
@HotSpotIntrinsicCandidate
public final boolean weakCompareAndSetInt(Object o, long offset,
int expected,
int x) {
return compareAndSetInt(o, offset, expected, x);
}
先说说getAndAddInt
的实现,它会根据当前Atomic的value在内存中地址获取到当前对象的值,然后再重复此操作,把之前获得的值与第二遍获得的值进行比较,如果两个值相等,就把内存地址的值更新为新值,否则就自旋相比较。
CAS的缺点
- 循环时间开销大:Atomic的CAS并没有进行CAS失败的退出处理,只是单纯的循环比较,如果长时间自旋会给CPU带来非常大的执行开销。
- 只能保证一个共享变量的原子操作:Atomic原子类只能保证一个变量的原子操作,如果是多数据的话,还是考虑使用互斥锁来实现数据同步。
- ABA问题:CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS检查时会发现它的值并没有发生变化,但是实际上变量已经变化了。
解决ABA问题
再juc的Atomic包中提供了AtomicStampReference类,这个类较普通的原子累新增了一个stamp字段,它的作用相当于version(版本号)。每次修改这个引用的值,也会修改stamp的值,当发现stamp的值与期盼的stamp值不一样时,会修改失败,类似于以version实现乐观锁。