Java多线程及线程池
1.volatile
内存模型的相关概念
Java并发编程:volatile关键字解析 - Matrix海子 - 博客园 (cnblogs.com)
在JVM底层volatile是采用“内存屏障”来实现的。
大家都知道,计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。
也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。
举个简单的例子,比如下面的这段代码:i = i + 1; 当线程执行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。
如果有两个线程执行这段代码, 理想结果是i=2;
初始时,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,然后线程1进行加1操作,然后把i的最新值1写入到内存。此时线程2的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程2把i的值写入内存。
最终结果i的值是1,而不是2。这就是著名的缓存一致性问题。通常称这种被多个线程访问的变量为共享变量。
- 通过在总线加LOCK#锁的方式;加LOCK#锁的形式来解决缓存不一致的问题。因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存
- 通过缓存一致性协议;上面的方式会有一个问题,由于在锁住总线期间,其他CPU无法访问内存,导致效率低下。所以就出现了缓存一致性协议。最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
并发编程中的三个概念
- 原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
- 可见性 :可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
- 线程1 执行的代码的变量值,不能立即让线程2 获取到 这就是可见性问题
- 有序性:即程序执行的顺序按照代码的先后顺序执行。举个简单的例子,看下面这段代码:
- 一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。 但是会影响并发线程的执行正确性
三.Java内存模型
在Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。那么Java内存模型规定了哪些东西呢,它定义了程序中变量的访问规则,往大一点说是定义了程序执行的次序。注意,为了获得较好的执行性能,Java内存模型并没有限制执行引擎使用处理器的寄存器或者高速缓存来提升指令执行速度,也没有限制编译器对指令进行重排序。也就是说,在java内存模型中,也会存在缓存一致性问题和指令重排序的问题。
Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。
举个简单的例子:在java中,执行下面这个语句:
i = 10;
执行线程必须先在自己的工作线程中对变量i所在的缓存行进行赋值操作,然后再写入主存当中。而不是直接将数值10写入主存当中。
那么Java语言 本身对 原子性、可见性以及有序性提供了哪些保证呢?
1.原子性
在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。
只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。
请分析以下哪些操作是原子性操作:
x = 10; //语句1
y = x; //语句2
x++; //语句3
x = x + 1; //语句4
- 语句1是直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中。
- 语句2实际上包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及 将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。
- 同样的,x++和 x = x+1包括3个操作:读取x的值,进行加1操作,写入新的值。
不过这里有一点需要注意:在32位平台下,对64位数据的读取和赋值是需要通过两个操作来完成的,不能保证其原子性。但是好像在最新的JDK中,JVM已经保证对64位数据的读取和赋值也是原子性操作了。
从上面可以看出,Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。
2.可见性
- 对于可见性,Java提供了volatile关键字来保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
- 另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
3.有序性
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
在Java里面,可以通过volatile关键字来保证一定的“有序性”。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
另外,Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。
下面就来具体介绍下happens-before原则(先行发生原则):
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
- 锁定规则:一个unLock操作先行发生于后面对同一个锁lock操作
- volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
- 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
下面我们来解释一下前4条规则:
对于程序次序规则来说,我的理解就是一段程序代码的执行在单个线程中看起来是有序的。注意,虽然这条规则中提到“书写在前面的操作先行发生于书写在后面的操作”,这个应该是程序看起来执行的顺序是按照代码顺序执行的,因为虚拟机可能会对程序代码进行指令重排序。虽然进行重排序,但是最终执行的结果是与程序顺序执行的结果一致的,它只会对不存在数据依赖性的指令进行重排序。因此,在单个线程中,程序执行看起来是有序执行的,这一点要注意理解。事实上,这个规则是用来保证程序在单线程中执行结果的正确性,但无法保证程序在多线程中执行的正确性。
第二条规则也比较容易理解,也就是说无论在单线程中还是多线程中,同一个锁如果出于被锁定的状态,那么必须先对锁进行了释放操作,后面才能继续进行lock操作。
第三条规则是一条比较重要的规则,也是后文将要重点讲述的内容。直观地解释就是,如果一个线程先去写一个变量,然后一个线程去进行读取,那么写入操作肯定会先行发生于读操作。
第四条规则实际上就是体现happens-before原则具备传递性。
四.深入剖析volatile关键字
- 一定程度有序
- 可见性
- 不能保证原子性
1.volatile关键字的两层语义
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。
2.volatile保证原子性吗
不能 可以通过synchronized:采用Lock: 或者对数字的操作使用AtomicInteger
public AtomicInteger inc = new AtomicInteger();
3**.volatile能保证有序性吗?**
可以 ;可以保证 在volatile修饰的变量相关语句之前的代码 一定在之前执行,之后的代码在之后执行
//x、y为非volatile变量
//flag为volatile变量x = 2; ``//语句1
y = 0; ``//语句2
flag = true; ``//语句3
x = 4; ``//语句4
y = -1; ``//语句5
由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。
4.volatile的原理和实现机制
前面讲述了源于volatile关键字的一些使用,下面我们来探讨一下volatile到底如何保证可见性和禁止指令重排序的。
下面这段话摘自《深入理解Java虚拟机》:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
五.使用volatile关键字的场景
synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:
1)对变量的写操作不依赖于当前值
2)该变量没有包含在具有其他变量的不变式中
实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。
事实上,我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。
1.状态标记量
volatile boolean flag = ``false``;while(!flag){doSomething();
}public void setFlag() {flag = true ;
}
double check
这种情况下由于 instance = new SingleInstance(); 不能保证原子性;也就是说,这行代码需要处理器分为多步才能完成,其中主要包含两个操作,分配内存空间,引用变量指向内存,由于编译器可能会产生指令重排序的优化操作,所以两个步骤不能确定实际的先后顺序,假如线程A已经指向了内存,但是并没有分配空间,线程A阻塞,那么当线程B执行时,会发现Instance已经非空了,那么这时返回的Instance变量实际上还没有分配内存,显然是错误的。
public class SingleInstance{private static SingleInstance instance = null;private SingleInstance(){}public static SingleInstance getInstance(){if(instance == null){synchronized(SingleInstance.class){if(instance == null){instance = new SingleInstance(); //} }}return instance; }
}
//给instance 变量加上volatile 关键字 保证前后代码的有序性 保证 一定是执行完之后再返回
public class SingleInstance{private static volatile SingleInstance instance = null;private SingleInstance(){}public static SingleInstance getInstance(){if(instance == null){synchronized(SingleInstance.class){if(instance == null){instance = new SingleInstance();} }}return instance; }
}
synchronized 关键字和 volatile 关键字的区别
synchronized 关键字和 volatile 关键字是两个互补的存在,⽽不是对⽴的存在!
- volatile 关键字是线程同步的轻量级实现,所以 volatile 性能肯定⽐ synchronized 关键字 要好。但是 volatile 关键字只能⽤于变量⽽ synchronized 关键字可以修饰⽅法以及代码 块。
- 都能禁止指令重排序
- volatile 关键字能保证数据的可⻅性,但不能保证数据的原⼦性。 synchronized 关键字两 者都能保证。
- volatile 关键字主要⽤于解决变量在多个线程之间的可⻅性,⽽ synchronized 关键字解决 的是多个线程之间访问资源的同步性
- volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化
2.ThreadLocal原理
线程独享变量
hreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意:
- 因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。
- 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。
- 同一个线程是否可以使用多个ThreadLocal
- 可以,因为初始化的时候 table长度为16 理论上可以存放16数组的元素,是entry节点 entry第一个属性 threadLocal对象 第二个属性 要存放的值,同一个线程多个threadLocal get的时候,取出来的同一个threadLocalmap,threadLocalmap有一个长度为16的entry数组 ,每一个entry的key对应的不同的threadlocal对象,取得时候是根据threadlocal对象取得 所以不会取错
- 对于每一个ThreadLocal对象,都有一个final修饰的int型的threadLocalHashCode不可变属性,对于基本数据类型,可以认为它在初始化后就不可以进行修改,所以可以唯一确定一个ThreadLocal对象。同时threadlocak中包含 AtomicInteger 属性 保证并发安全
- 多个线程,同一个threadlocal是否会取错
不会,get方法得时候 ,取的自己线程对象的ThreadLocalMap;Thread t = Thread.currentThread();- hreadLocalMap map = getMap(t);不同的额线程取到的是不同的ThreadLocalMap
ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。
ThreadLocal实现原理
首先 ThreadLocal 是一个泛型类,保证可以接受任何类型的对象。
因为一个线程内可以存在多个 ThreadLocal 对象,所以其实是 ThreadLocal 内部维护了一个 Map ,这个 Map 不是直接使用的 HashMap ,而是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类。而我们使用的 get()、set() 方法其实都是调用了这个ThreadLocalMap类对应的 get()、set() 方法。
public void set(T value) {//value:用户id//当前线程:每个用户访问tomcat都会创建一个线程 A BThread t = Thread.currentThread();//Thread这个类里面有一个属性:threadLocals(类型是ThreadLocalMap,ThreadLocal的静态内部类) 传入A线程拿到的就是A线程threadLocals B。。。。ThreadLocalMap map = getMap(t);//第一次去拿这个属性 为nullif (map != null)map.set(this, value);elsecreateMap(t, value);}void createMap(Thread t, T firstValue) {//t:A或者B this代表当前ThreadLocal对象 firstValue:当前用户的idt.threadLocals = new ThreadLocalMap(this, firstValue);}ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {//entry数组:长度16table = new Entry[INITIAL_CAPACITY];//数组下标:当前设置的值 应该放在数组哪个位子int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);//Entry:key--value key:threadLocal对象 value放的用户idtable[i] = new Entry(firstKey, firstValue);size = 1;setThreshold(INITIAL_CAPACITY);}public T get() {//获取当前线程:A用户拿到就是A线程 B用户拿到的B线程Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);--->return t.threadLocals;if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}return setInitialValue();}
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) return (T)map.get(this); // Maps are constructed lazily. if the map for this thread // doesn't exist, create it, with this ThreadLocal and its // initial value as its only entry. T value = initialValue(); createMap(t, value); return value; }
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
ThreadLocalMap是个静态的内部类:
static class ThreadLocalMap { ........ } 最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。
使用场景
如上文所述,ThreadLocal 适用于如下两种场景
- 每个线程需要有自己单独的实例
- 实例需要在多个方法中共享,但不希望被多线程共享
ThreadLocal 内存泄露问题
- ThreadLocalMap 中使⽤的 key 为 ThreadLocal 的弱引⽤,⽽ value 是强引⽤。
- 所以,如果 ThreadLocal 没有被外部强引⽤的情况下,在垃圾回收的时候,key 会被清理掉,⽽ value 不会 被清理掉。这样⼀来, ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措 施的话,value 永远⽆法被 GC 回收,这个时候就可能会产⽣内存泄露。
- ThreadLocalMap 实现 中已经考虑了这种情况,在调⽤ set() 、 get() 、 remove() ⽅法的时候,会清理掉 key 为 null 的记录。使⽤完 ThreadLocal ⽅法后 最好⼿动调⽤ remove() ⽅法
弱引⽤介绍: 如果⼀个对象只具有弱引⽤,那就类似于可有可⽆的⽣活⽤品。弱引⽤与软引⽤的区别在 于:只具有弱引⽤的对象拥有更短暂的⽣命周期。在垃圾回收器线程扫描它 所管辖的内存 区域的过程中,⼀旦发现了只具有弱引⽤的对象,不管当前内存空间⾜够与否,都会回收它 的内存。不过,由于垃圾回收器是⼀个优先级很低的线程, 因此不⼀定会很快发现那些只 具有弱引⽤的对象。 弱引⽤可以和⼀个引⽤队列(ReferenceQueue)联合使⽤,如果弱引⽤所引⽤的对象被垃 圾回收,Java 虚拟机就会把这个弱引⽤加⼊到与之关联的引⽤队列中。 2.3.21. 线程池 2.3.21.1. 为什么要⽤线程池? 池化技术相⽐⼤家已经屡⻅不鲜了,线程池、数据库连接池、Http 连接池等等都是对这个 思想的应⽤。池化技术的思想主要是为了减少每次获取资源的消耗,提⾼对资源的利⽤率。 static class Entry extends WeakReference>
3.Map和线程安全的Map
重要成员变量
DEFAULT_INITIAL_CAPACITY = 1 << 4; Hash表默认初始容量
MAXIMUM_CAPACITY = 1 << 30;
最大Hash表容量 DEFAULT_LOAD_FACTOR = 0.75f;
默认加载因子 TREEIFY_THRESHOLD = 8;
链表转红黑树阈值 UNTREEIFY_THRESHOLD = 6;
红黑树转链表阈值 MIN_TREEIFY_CAPACITY = 64;
链表转红黑树时hash表最小容量阈值,达不 到优先扩容。
1.7-hashtable = 数组(基础) + 链表
1.8 = 数组 + 链表 + 红黑树
hashmap在jdk1.7以前有回环
jdk1.8虽然加入红黑树但是能转树的概率 百万分之一
public V put(K key, V value) {return putVal(hash(key), key, value, false, true);}调用真正的put方法之前 会先算出key的hashcode final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;//hashMap默认会创建一个长度为16的node数组if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;//n=16 根据当前key的hash值算出在这个数组的下标if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);//没有值直接放入 else {//hash冲突Node<K,V> e; K k;if (p.hash == hash &&//当前放入的key跟原来存在的key相同((k = p.key) == key || (key != null && key.equals(k))))e = p;//替换else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);else {for (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);//判断是否树华,数组长度>=64 ,节点长度>=8break;}if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}if (e != null) { // existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;if (++size > threshold)resize();//判断是否扩容afterNodeInsertion(evict);return null;}concurrentHashmap--- //两个线程同时插入hashmap 算出hash值都等于3 3这个地方正好没有值 可能会出现覆盖 if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))break; //两个线程同时插入hashmap 算出hash值等于 3这个位置有了,
synchronized
ConcurrentHashMap
- 数据结构 ConcurrentHashMap的数据结构与HashMap基本类似,区别在于:
- 1、内部在数据 写入时加了同步机制(分段锁)保证线程安全,读操作是无锁操作;
- 2、扩容时老数据的转移 是并发执行的,这样扩容的效率更高。
- 并发安全控制 Java7 ConcurrentHashMap基于ReentrantLock实现分段锁、
- Java8中 ConcurrentHashMap基于分段锁+CAS保证线程安全,分段锁基于synchronized关键字实现
/** Implementation for put and putIfAbsent */final V putVal(K key, V value, boolean onlyIfAbsent) {//不允许存放空键值if (key == null || value == null) throw new NullPointerException();//2次扰动 获得当前key hash值int hash = spread(key.hashCode());int binCount = 0;for (Node<K,V>[] tab = table;;) {Node<K,V> f; int n, i, fh;if (tab == null || (n = tab.length) == 0)tab = initTable();//算出当前key 应该存放的下标位置:如果下标位置为nullelse if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//多个线程同时算出下标都相等 有可能出现覆盖 于是采用cas达到线程安全//1.并发2.并发的key的hash值相等 3.这个位置没有元素 写的概率小 读多写少if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))break; // no lock when adding to empty bin}else if ((fh = f.hash) == MOVED)tab = helpTransfer(tab, f);else {V oldVal = null;//链表操作 可能出现覆盖:多个线程同时判断下一个为null//这个地方比前面cas的地方 写的概率大一些//应该直接加锁,锁芯是当前下标的第一个元素,只锁住当前链表,效率远远高于hashtablesynchronized (f) {if (tabAt(tab, i) == f) {if (fh >= 0) {binCount = 1;for (Node<K,V> e = f;; ++binCount) {K ek;if (e.hash == hash &&((ek = e.key) == key ||(ek != null && key.equals(ek)))) {oldVal = e.val;if (!onlyIfAbsent)e.val = value;break;}Node<K,V> pred = e;if ((e = e.next) == null) {pred.next = new Node<K,V>(hash, key,value, null);break;}}}else if (f instanceof TreeBin) {Node<K,V> p;binCount = 2;if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,value)) != null) {oldVal = p.val;if (!onlyIfAbsent)p.val = value;}}}}if (binCount != 0) {if (binCount >= TREEIFY_THRESHOLD)treeifyBin(tab, i);if (oldVal != null)return oldVal;break;}}}addCount(1L, binCount);return null;}
5.线程安全集合
- vector:所有的方法都加锁,效率较低,不建议使用
读读的问题,
- CopyOnWriteArrayList / CopyOnWriteArraySet: 写时复制
- 读写分离的思想,写时复制原List写入后返回
- CopyOnWriteArrayList 仅适用于写操作非常少的场景,而且能够容忍读写的短暂不一致
- 内存占用问题 数据一致性问题
- 适用场景 读多写少
java锁
悲观锁与乐观锁
- 悲观锁的意思:比较悲观,我认为我在写的时候一定有人来,把它锁死(加锁的消耗,状态切换),修改场景特别多
- 乐观锁:比较乐观 ,认为我在改的时候应该没人回来,我就不浪费那个加锁的资源,找一个东西来标记,比如如果mysql 修改商品,version ,线程进来先查询版本,如果修改完毕,查看版本,版本跟刚才查询的一致那么就提交修改,并且把版本号+1,如果不一致那么说明已经有人改过了,这次操作作废(while循环下次) ,修改比较少,查询多
- 乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。
公平锁与非公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。
非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。
可重入锁
可重入锁 VS 非可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。
Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。下面用示例代码来进行分析:
独享锁 VS 共享锁
独享锁和共享锁同样是一种概念。我们先介绍一下具体的概念,然后通过ReentrantLock和ReentrantReadWriteLock的源码来介绍独享锁和共享锁。
独享锁也叫排他锁,是指该锁一次只能被一个线程所持有。如果线程T对数据A加上排它锁后,则其他线程不能再对A加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。JDK中的synchronized和JUC中Lock的实现类就是互斥锁。
共享锁是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。
独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
读写锁
读写锁是一个资源能够被多个读线程访问,或者被一个写线程访问但不能同时存在读线程。Java当中的读写锁通过ReentrantReadWriteLock实现。具体使用方法这里不展开。
互斥锁
所谓互斥锁就是指一次最多只能有一个线程持有的锁。在JDK中synchronized和JUC的Lock就是互斥锁
分段锁
ConcurrentHashMap中采用了分段锁
4.synchronized
(1条消息) synchronized 实现原理_liuwg1226的专栏-CSDN博客
synchronized内置锁是一种对象锁(锁的是对象而非引用),作用粒度是对象,可以用来实现对临界资源的同步互斥访问,是可 重入的。 加锁的方式
加锁的方式
- 同步实例方法,锁是当前实例对象
- 同步类方法,锁是当前类对象
- 同步代码块,锁是括号里面的对象
释放锁:1.同步代码块运行完了,2.同步代码块抛异常
synchronized原理
synchronized关键字被编译成字节码后会被翻译成monitorenter 和 monitorexit 两条指令分别在同步块逻辑代码的起始位置 与结束位置
Monitor监视器锁
- 任何一个对象都有一个Monitor与之关联,当且一个Monitor被持有后,它将处于锁定状态。Synchronized在JVM里的实现都是 基于进入和退出Monitor对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的MonitorEnter和 MonitorExit指令来实现。
- monitorenter:每个对象都是一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行 monitorenter指令时尝试获取monitor的所有权,过程如下:
- a. 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor 的所有者;
- b. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;
- c. 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝 试获取monitor的所有权;
- monitorexit:执行monitorexit的线程必须是objectref所对应的monitor的所有者。指令执行时,monitor的进入数减 1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去 获取这个 monitor 的所有权。
- monitorexit,指令出现了两次,
- 第1次为同步正常退出释放锁;
- 第2次为发生异步退出释放锁;
- 通过上面两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来 完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则 会抛出java.lang.IllegalMonitorStateException的异常的原因。
同步方法:
package it.yg.juc.sync;
public class SynchronizedMethod { public synchronized void method() { System.out.println("Hello World!"); }
}
反编译结果:
- 从编译的结果来看,方法的同步并没有通过指令 monitorenter 和 monitorexit 来完成(理论上其实也可以通过这两条指令来 实现),不过相对于普通方法,其常量池中多了 ACC_SYNCHRONIZED 标示符。
- JVM就是根据该标示符来实现方法的同步的: 当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取 monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个
- 两种同步方式本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。
monitor(监视器锁)
- 可以把它理解为 一个同步工具,也可以描述为 一种同步机制,它通常被 描述为一个对象。与一切皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。也就是通常说Synchronized的对象锁,MarkWord锁标识位为10,其中指针指向的是Monitor对象的起始地址。
- 在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的):
ObjectMonitor() {_header = NULL;_count = 0; // 记录个数_waiters = 0,_recursions = 0;_object = NULL;_owner = NULL;_WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet_WaitSetLock = 0 ;_Responsible = NULL ;_succ = NULL ;_cxq = NULL ;FreeNext = NULL ;_EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表_SpinFreq = 0 ;_SpinClock = 0 ;OwnerIsThread = 0 ;}
ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象 ),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时:
- 首先会进入 _EntryList 集合,当线程获取到对象的monitor后,进入 _Owner区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1;
- 若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒;
- 若当前线程执行完毕,也将释放monitor(锁)并复位count的值,以便其他线程进入获取monitor(锁);
同时,Monitor对象存在于每个Java对象的对象头Mark Word中(存储的指针的指向),Synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时notify/notifyAll/wait等方法会使用到Monitor锁对象,所以必须在同步代码块中使用。
监视器Monitor有两种同步方式:互斥与协作。多线程环境下线程之间如果需要共享数据,需要解决互斥访问数据的问题,监视器可以确保监视器上的数据在同一时刻只会有一个线程在访问。
那么有个问题来了,我们知道synchronized加锁加在对象上,对象是如何记录锁状态的呢?答案是锁状态是被记录在每个对象的对象头(Mark Word)中,下面我们一起认识一下对象的内存布局
对象的内存布局
HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
- 对象头:比如 hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间,数组长度(数组对象)等。Java对象头一般占有2个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit,在64位虚拟机中,1个机器码是8个字节,也就是64bit),但是 如果对象是数组类型,则需要3个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。
- 实例数据:存放类的属性数据信息,包括父类的属性信息;
- 对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐;
javap -verbose xxx.class
同步方法会acc_synchronized
同步代码块会出现 moniterenter moniterexit,完成一个异常----moniterexitsynchronized(对象)
对象在内存中得布局: 对象头 实例数据 对齐填充
java对象头:
new Student()----student.class
class metadata address:指向class对象的指针,虚拟机可以确定该对象时哪个class的实例
mark word:对象的信息,标志位等,实现锁的关键 id属性对象的信息: hashcode,分代年龄:gc 垃圾回收锁类型:锁标志位: 01(无) 00(轻量级) 10 轻量级 重量级不同的 还会记录,当前持有该锁的线程是谁
只靠标记是不行:mointer监视器---来完成加锁放锁 管程
ObjectMointer父类 c++实现的,每一个对象都一定会有moniter,出现的时间:伴随着对象的出生而出生,第一次去拿对象锁的时候出生,moniterenter moniterexit
重要属性: waitset:保存抢到该锁以后调用wait方法的线程 entrylist:记录没有抢到锁的线程。多个线程 5个线程来抢锁,某个线程抢到锁,对象mark word id=当前线程的id owner:抢到锁的线程 如果释放锁,owner置空 count:+1 -1
A线程来了 把owner---改为A count+1 ----B线程来了,A线程还在执行 B进入entrylist等待----A线程执行wait方法,进入waitset里面jdk6以前,lock比synchronized效率高很多,mutex lock效率低
jdk6以后做了优化,
B线程去拿锁 拿不到---转等待状态----B想运行又要转回来--浪费了效率自旋:多数时候,拿锁就一哈哈儿,如果去切换得不偿失,就可以让其它线程while(true)不释放cpu还更快 但是锁的时间长,自旋就长,就更消耗了用户可以更改,参数可以不用管
就有了**自适应自旋锁**:由前一次,在同一个锁上自旋时间及锁的持有时间,可以自动调整自旋时间,甚至可以不自旋**锁消除** stringbuffer 自动消除```java
//方法属于线程栈 天生线程安全 内部的东西都是安全的 没必要加锁//jvm 会把锁自动消除:锁消除public void test(){StringBuffer stringBuffer = new StringBuffer();stringBuffer.append("abc");}
```**锁粗化:** 循环多次stringbuffer 每次调用方法都会加锁
StringBuffer stringBuffer = new StringBuffer();public void test(){synchronized(){//锁粗化while(1000次)stringBuffer.append("abc");}}
会在栈里面复制一份moniter的信息,对象头
对象头会有一个指针无锁、偏向锁、轻量级锁、重量级锁
偏向锁:只有一个线程 抢锁,本来抢锁需要很多认证,改一些标记,先把这些标记标记为这个线程,减少加锁的流程
(就偏向于他了 mark word(堆中)的id=当前线程的thread id)
轻量级锁:AB交替执行,偏向锁----轻量级锁
轻量级锁:很多线程大家没有规律抢 ----重量级锁锁的释放依赖moniter:jmm内存模型,1.把数据同步,高速缓冲(栈的内存) 比如读取student对象 修改student对象 写入student对象到堆,另外把其它栈里面有这个对象的全部要求重新读取(可见性) 2.修改对象头信息,锁标志,id,指针
https://blog.csdn.net/javazejian/article/details/72828483
自旋锁,自适应自旋锁
- Java 锁的几种状态并不包括自旋锁,但是当轻量级锁的竞争时,采用的是自旋锁机制。
- 什么是自旋锁:当线程 A 已经获得锁时,线程 B 再来竞争锁,线程 B 不会直接被阻塞,而是在原地循环等待,当线程 A 释放锁后,线程 B 可以马上获得锁。
- 引入自旋锁的原因:因为阻塞和唤起线程都会引起操作系统用户态和核心态的转变,对系统性能影响较大,而自旋等待可以避免线程切换的开销。
- 自旋锁的缺点:自旋等待虽然可以避免线程切花费的开销,但它也会占用处理器的时间。如果持有锁的线程在较短的时间内释放了锁,自旋锁的效果就比较好,如果持有锁的线程很长时间都不释放锁,自旋的线程就会白白浪费 CPU 资源,所以一般线程自旋的次数必须有一个限制,该次数可以通过参数 -XX:PreBlockSpin 调整,一般默认为 10。
- 自适应自旋锁:JDK1.6 引入了自适应自旋锁,自适应自旋锁的自旋次数不在固定,而是由上一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的。如果对于某个锁对象,刚刚有线程自旋等待成功获取到锁,那么虚拟机将认为这次自旋等待的成功率也很高,会允许线程自旋等待的时间更长一些。如果对于某个锁对象,线程自旋等待很少成功获取到锁,那么虚拟机将会减少线程自旋等待的时间。
多个线程竞争同步资源的流程区分
锁 | 优点 | 缺点 | 使用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗,与执行非同步方法差距很小 | 如果线程存在竞争,会额外带来锁撤销的消耗 | 只有一个线程访问同步块的场景 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的相应速度 | 如果始终得不到锁竞争的线程,使用自旋锁会消耗cpu | 追求相应时间,同步块执行速度快 |
重量级锁 | 线程竞争不使用自选,不会消耗cpu | 线程阻塞 ,响应时间较慢 | 同步执行速度较慢 |
无锁 | 没有共享变量,即无竞争 |
无锁
1. 偏向锁
- 偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。
- 偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。
- 所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。
- 但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。
加锁过程
检查对象头中 Mark Word 是否为可偏向状态,如果不是则直接升级为轻量级锁
如果是,判断 Mark Work 中的线程 ThreadId 是否指向当前线程,如果是则该线程就不会再重复获取锁了,执行同步代码块
如果不是,则进行 CAS 操作竞争锁,如果竞争到锁,则将 Mark Work 中的线程 ID 设为当前线程 ID,执行同步代码块
如果竞争失败,升级为轻量级锁
偏向锁的撤销:
只有等到竞争,持有偏向锁的线程才会撤销偏向锁。偏向锁撤销后会恢复到无锁或者轻量级锁的状态。
- 偏向锁的撤销需要到达全局安全点,全局安全点表示一种状态,该状态下所有线程都处于暂停状态
- 判断锁对象是否处于无锁状态,即获得偏向锁的线程如果已经退出了临界区(竞争状态),表示同步代码已经执行完了。重新竞争锁的线程会进行 CAS 操作替代原来线程的 ThreadID
- 如果获得偏向锁的线程还处于临界区之内,表示同步代码还未执行完,将获得偏向锁的线程升级为轻量级锁
- 一句话简单总结偏向锁原理:使用 CAS 操作将当前线程的 ID 记录到对象的 Mark Word 中。
轻量级锁
引入轻量级锁的目的:在多线程交替执行同步代码块时(未发生竞争),避免使用互斥量(重量锁)带来的性能消耗。但多个线程同时进入临界区(发生竞争)则会使得轻量级锁膨胀为重量级锁。
将对象的 Mark Word 复制到栈帧中的 Lock Record 中,并将 Lock Record 中的 owner 指向当前对象,并使用 CAS 操作将对象的 Mark Word 更新为指向 Lock Record 的指针,
轻量级锁的获取流程:
首先判断当前对象是否处于一个无锁的状态,如果是,Java 虚拟机将在当前线程的栈帧建立一个锁记录(Lock Record),用于存储对象目前的 Mark Word 的拷贝
如果第二步执行成功,表示该线程获得了这个对象的锁,将对象 Mark Word 中锁的标志位设置为 “00”,执行同步代码块。
如果第二步未执行成功,需要先判断当前对象的 Mark Word 是否指向当前线程的栈帧,如果是,表示当前线程已经持有了当前对象的锁,这是一次重入,直接执行同步代码块。如果不是表示多个线程存在竞争,该线程通过自旋尝试获得锁,即重复步骤2,自旋超过一定次数,轻量级锁升级为重量级锁
轻量级锁的解锁:
轻量级的解锁同样是通过 CAS 操作进行的,线程会通过 CAS 操作将 Lock Record 中的 Mark Word(官方称为 Displaced Mark Word)替换回来。如果成功表示没有竞争发生,成功释放锁,恢复到无锁的状态;如果失败,表示当前锁存在竞争,升级为重量级锁。
一句话总结轻量级锁的原理:将对象的 Mark Word 复制到当前线程的 Lock Record 中,并将对象的 Mark Word 更新为指向 Lock Record 的指针。
AQS
AQS是AbstractQueuedSynchronizer的简称。AQS 是⼀个⽤来构建锁和同步器的框架,使⽤ AQS 能简单且⾼效地构造出应⽤⼴泛的⼤量的同 步器,⽐如我们提到的 ReentrantLock , Semaphore ,其他的诸如 ReentrantReadWriteLock , SynchronousQueue , FutureTask 等等皆是基于 AQS 的。
AQS 核⼼思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的⼯作线 程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占⽤,那么就需要⼀套线程阻塞 等待以及被唤醒时锁分配的机制,这个机制 AQS 是⽤ CLH 队列锁实现的,即将暂时获取不到锁 的线程加⼊到队列中。
CLH(Craig,Landin,and Hagersten)队列是⼀个虚拟的双向队列(虚拟的双向队列即不存在 队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成⼀个 CLH 锁队列的⼀个结点(Node)来实现锁的分配
AQS 定义两种资源共享⽅式
- Exclusive(独占):只有⼀个线程能执⾏,如 ReentrantLock 。⼜可分为公平锁和⾮公平 锁: 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁 ⾮公平锁:当线程要获取锁时,⽆视队列顺序直接去抢锁,谁抢到就是谁的
- Share(共享):多个线程可同时执⾏,如 CountDownLatch 、 Semaphore、 CyclicBarrier 、 ReadWriteLock 我们 都会在后⾯讲到。
- ReentrantReadWriteLock 可以看成是组合式,因为 ReentrantReadWriteLock 也就是读写锁允许多 个线程同时对某⼀资源进⾏读
LOCK
前面讲到synchronized,如果没有调用阻塞的wait()与join方法,那么只会在下面两种情况释放锁
1.当synchronized修饰的同步代码块执行完毕
2.当synchronized修饰的同步代码块出现异常,jvm会自动释放锁
也知道当多个线程试图获取synchronized锁的时候是没办法被中断的
那么问题就来了,如果现在需求是希望能等待一段时间过后,如果没有获得锁,就不继续等待锁了,很显然
synchronized做不到,于是就有了lock
实现类
ReentrantLock
ReentrantLock,意思是“可重入锁”,关于可重入锁的概念在下一节讲述。ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。下面通过一些实例看具体看一下如何使用ReentrantLock。
void lock():获得锁,如果锁被占用则等待。
void lockInterruptibly():获得锁,但优先响应中断。(留着)
boolean tryLock():尝试获得锁,如果成功,则返回true;失败返回false。此方法不等待,立即返回。
boolean tryLock(long time,TimeUnit):在上面的方法上加上时间
void unlock():释放锁,一定要记得释放锁,出现死锁
Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。
lick不能玩为静态属性 因为静态属性 所有对象共享 ,每个线程执行该方法,都会保存一个副本,那么每个线程的lock。lock获取的是不同的锁Lock lock = ...;//lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。
lock.lock();
try{//处理任务
}catch(Exception ex){}finally{lock.unlock(); //释放锁
}ReentrantLock lock = new ReentrantLock();try { //等待时间,时间单位 可以说时分秒,天 一般 用业务执行过程的时间*2if (lock.tryLock(5, TimeUnit.SECONDS)) {try {}catch (Exception e){throw new RuntimeException(e);}finally {lock.unlock();}}else{//没拿到锁的操作}} catch (InterruptedException e) {//拿锁的过程报错了e.printStackTrace();}//假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
public void method() throws InterruptedException {lock.lockInterruptibly();try { //.....}finally {lock.unlock();}
}
ReadWriteLock
readLock()
writeLock()用来获取读锁和写锁
4.ReentrantReadWriteLock
ReentrantReadWriteLock里面提供了很多丰富的方法,不过最主要的有两个方法
- readLock() 用来获取读锁
- writeLock() 写锁。
读读不互斥,读写互斥。
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
public void get(Thread thread) {rwl.readLock().lock();try {long start = System.currentTimeMillis();while(System.currentTimeMillis() - start <= 1) {System.out.println(thread.getName()+"正在进行读操作");}System.out.println(thread.getName()+"读操作完毕");} finally {rwl.readLock().unlock();}}
Lock和synchronized
synchronized底层实现
synchronized 属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层操作系统的 Mutex Lock 来实现的,而操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方从 JVM 层面对 synchronized 进行了较大优化,所以现在的 synchronized 锁效率也优化得很不错了。Java 6 之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁,
Lock底层实现
Lock底层实现基于AQS实现,采用线程独占的方式,在硬件层面依赖特殊的CPU指令(CAS)。
简单来说,ReenTrantLock的实现是一种自旋锁,通过循环调用CAS操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。想尽办法避免线程进入内核的阻塞状态是我们去分析和理解锁设计的关键钥匙。
区别
- Lock是一个接口,由JDK实现。而synchronized是Java中的关键字,依赖jvm;
- synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生; 而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
- Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
- Lock 可以判断锁的状态,synchronized 不可以判断锁的状态
- Lock可以提高多个线程进行读操作的效率。
- Lock 实现锁的类型是可重入锁、公平锁。synchronized 实现锁的类型是可重入锁,非公平锁
选择
- 在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
Lock 适用于大量同步代码块的场景,synchronized 适用于少量同步代码块的场景
Condition newCondition():自定义条件 await()
condition
它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效
Conditon里面的方法,哪个Conditon的signal方法就只能叫醒自己的await
Conditon中的 await() 对应Object的wait();
Condition中的 signal() 对应Object的notify();
Condition中的 signalAll() 对应Object的notifyAll()。
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();condition.await()//当前线程阻塞
condition.await(5,TimeUnit.SECONDS); //一定时间后自动进入竞争状态
condition.signal(); //随机唤醒一个当前lock对象的等待线程
condition.signalAll(); //唤醒所有等待线程syn..里面wait/notify/notifyall方法,由锁对象调用,等待与唤醒都有对应关系,写代码需要分析
Conditon里面的方法,哪个Conditon的signal方法就只能叫醒自己的awaitTest3 test3 = new Test3();synchronized (test3){test3.wait(5);//让当前线程阻塞 ,等待notify或者notifyall 或者超过等待时间 毫秒test3.notify();test3.notifyAll();}
Semaphore(信号量)用法
- Semaphore 字面意思是信号量的意思,它的作用是控制访问特定资源的线程数目,底层依 赖AQS的状态State
- Semaphore内部维护了一个计数器,其值为可以访问的共享资源的个数。一个线程要访问共享资源,先获得信号量,如果信号量的计数器值大于1,意味着有共享资源可以访问,则使其计数器值减去1,再访问共享资源。
- 如果计数器值为0,线程进入休眠。当某个线程使用完共享资源后,释放信号量,并将信号量内部的计数器加1,之前进入休眠的线程将被唤醒并再次试图获得信号量。
API
信号量,Semaphore可以控同时访问的线程个数,可用做限流
//构造方法
public Semaphore(int permits) { //参数permits表示许可数目,即同时可以允许多少线程进行访问sync = new NonfairSync(permits);
}
//构造方法
public Semaphore(int permits, boolean fair) { //这个多了一个参数fair表示是否是公平的,即等待时间越久的越先获取许可sync = (fair)? new FairSync(permits) : new NonfairSync(permits);
}public void acquire() throws InterruptedException { } //获取一个许可
public void acquire(int permits) throws InterruptedException { } //获取permits个许可
public void release() { } //释放一个许可
public void release(int permits) { } //释放permits个许可这4个方法都会被阻塞,如果想立即得到执行结果,可以使用下面几个方法:public boolean tryAcquire() { }; //尝试获取一个许可,若获取成功,则立即返回true,若获取失败,则立即返回false
public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException { }; //尝试获取一个许可,若在指定的时间内获取成功,则立即返回true,否则则立即返回false
public boolean tryAcquire(int permits) { }; //尝试获取permits个许可,若获取成功,则立即返回true,若获取失败,则立即返回false
public boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException { }; //尝试获取permits个许可,若在指定的时间内获取成功,则立即返回true,否则则立即返回false
示例
import java.util.concurrent.Semaphore;public class SemaphoreTest implements Runnable{//参数permits表示许可数目,即同时可以允许多少线程进行访问//这个多了一个参数fair表示是否是公平的,即等待时间越久的越先获取许可Semaphore semaphore = new Semaphore(2);@Overridepublic void run() {try {//获取一个许可,可以有int参数 ,获取n个许可semaphore.acquire();System.out.println(Thread.currentThread().getName()+"获得令牌");Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}finally {//释放信号量,计数器加1 semaphore.release();}}public static void main(String[] args) {SemaphoreTest semaphoreTest = new SemaphoreTest();new Thread(semaphoreTest,"张三").start();new Thread(semaphoreTest,"李四").start();new Thread(semaphoreTest,"王五").start();}
}
5.CountDownLatch(闭锁)用法
一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。等待多个任务做完再继续做其它事情(协作的)
CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。每当 一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的 线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。
1 CountDownLatch.countDown() 2 CountDownLatch.await();
1.注册用户
1.上传头像---->图片地址 A
2.上传生活昭----->图片地址 B
4.上面的操作都做完了 再插入数据库
import java.util.concurrent.CountDownLatch;public class CountDownLatchTest {public static void main(String[] args) {//所有的线程中都要引入同一个CountDownLatch对象,当都执行latch.countDown();才执行latch.await()后面的方法//等待两个线程任务,也就是要执行两次latch.countDown(); 才会开始执行下面的//让后面的线程处于await状态 当countdoum为零时唤醒 final CountDownLatch latch = new CountDownLatch(2); new Thread(){public void run() {try {System.out.println("子线程"+Thread.currentThread().getName()+"正在执行");Thread.sleep(3000);System.out.println("子线程"+Thread.currentThread().getName()+"执行完毕");latch.countDown(); //----------执行结束} catch (InterruptedException e) {e.printStackTrace();}};}.start();new Thread(){public void run() {try {System.out.println("子线程"+Thread.currentThread().getName()+"正在执行");Thread.sleep(3000);System.out.println("子线程"+Thread.currentThread().getName()+"执行完毕");latch.countDown(); //-------------------执行结束} catch (InterruptedException e) {e.printStackTrace();}};}.start();try {System.out.println("等待2个子线程执行完毕...");latch.await();//必须要等待两个线程来告诉我 执行完毕了 才会继续执行 会阻塞System.out.println("2个子线程已经执行完毕");System.out.println("继续执行主线程");} catch (InterruptedException e) {e.printStackTrace();}}
}
6.CyclicBarrier用法
栅栏类似于闭锁,它能阻塞一组线程直到某个事件的发生。栅栏与闭锁的关键区别在于,所有的线程必须同时到达栅栏位置,才能继续执行。闭锁用于等待事件,而栅栏用于等待其他线程。
栅栏屏障,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程 到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。 CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线 程数量,每个线程调用await方法告CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。
CyclicBarrier使用独占锁来执行await方法,并发性可能不是很高
如果在等待过程中,线程被中断了,就抛出异常。但如果中断的线程所对应的CyclicBarrier不是这代的,比如,在最后一次线程执行signalAll后,并且更新了这个“代”对象。在这个区间,这个线程被中断了,那么,JDK认为任务已经完成了,就不必在乎中断了,只需要打个标记。该部分源码已在dowait(boolean, long)方法中进行了注释。
如果线程被其他的CyclicBarrier唤醒了,那么肯定等于generation,这个事件就不能return了,而是继续循环阻塞。反之,如果是当前CyclicBarrier唤醒的,就返回线程在CyclicBarrier的下标。完成了一次冲过栅栏的过程。该部分源码已在dowait(boolean, long)方法中进行了注释。
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.atomic.AtomicInteger;public class CyCleTest {public static void main(String[] args) {for(int i=0;i<4;i++)new Writer(i+1).start();}static class Writer extends Thread{//private AtomicInteger atomicInteger = new AtomicInteger(0);//创建栅栏public static CyclicBarrier cyclicBarrier=new CyclicBarrier(4);private int i ;public Writer(int i){this.i = i;}@Overridepublic void run() {System.out.println("线程"+Thread.currentThread().getName()+"正在写入数据...");try {Thread.sleep(i*2000); //以睡眠来模拟写入数据操作System.out.println("线程"+Thread.currentThread().getName()+"写入数据完毕,等待其他线程写入完毕");//会阻塞。。。直到 4 个线程都走到这儿cyclicBarrier.await(); } catch (InterruptedException e) {e.printStackTrace();}catch(BrokenBarrierException e){e.printStackTrace();}System.out.println("所有线程写入完毕,继续处理其他任务...");}}
}
Atomic原子类
Atomic 是指⼀个操作是不可中断的。即使是在多个线程⼀ 起执⾏的时候,⼀个操作⼀旦开始,就不会被其他线程⼲扰,通过引入低级别的原子化语义命令(比如compare-and-swap (CAS)),从而能在保证效率的同时保证原子性。 由CAS实现、
基本类:
AtomicInteger
AtomicLong
AtomicBoolean
引用类型:
- AtomicReference.
- AtomicStampedRerence
- AtomicMarkableReference
数组类型:
- AtomicIntegerArray
- AtomicLongArray
- AtomicReferenceArray
属性原子修改器(Updater):
- AtomicIntegerFieldUpdater
- AtomicLongFieldUpdater
- AtomicReferenceFieldUpdater
其中的主要方法:
- get() – 直接中主内存中读取变量的值,类似于volatile变量。
- getAndAdd(int) 增加指定的数据,返回变化前的数据
- set() – 将变量写回主内存。类似于volatile变量。
- lazySet() – 延迟写回主内存。一种常用的情景是将引用重置为null的情况。
- compareAndSet() – 执行CAS操作,成功返回true,失败返回false。
- weakCompareAndSet() – 比较弱的CAS操作,不同的是它不执行happens-before操作,从而不保证能够读取到其他变量最新的值。
AtomicInteger:
get() 直接返回值 getAndAdd(int) 增加指定的数据,返回变化前的数据 getAndDecrement() 减少1,返回减少前的数据 getAndIncrement() 增加1,返回增加前的数据 getAndSet(int) 设置指定的数据,返回设置前的数据 addAndGet(int) 增加指定的数据后返回增加后的数据 decrementAndGet() 减少1,返回减少后的值 incrementAndGet() 增加1,返回增加后的值 boolean compareAndSet(int expect, int update) :如果输入的数值等于预期
值,则以原子方式将该值设置为输入的值。lazySet(int) 仅仅当get时才会set
tomicInteger:以atomic开头的类都叫原子类
new AtomicInteger(0)import java.util.concurrent.atomic.AtomicInteger;public class AtomicIntegerTest implements Runnable{public int i = 0;AtomicInteger atomicInteger = new AtomicInteger(0);@Overridepublic void run() {// synchronized (this) {for (int i1 = 0; i1 < 1000; i1++) {atomicInteger.incrementAndGet();}// }}public static void main(String[] args) {AtomicIntegerTest atomicIntegerTest = new AtomicIntegerTest();for (int i = 0; i < 10; i++) {new Thread(atomicIntegerTest).start();}while (Thread.activeCount()>2){Thread.yield();}System.out.println(atomicIntegerTest.atomicInteger.get());}
}
10.CAS
CAS全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。java.util.concurrent包中的原子类就是通过CAS来实现了乐观锁。
CAS算法涉及到三个操作数:
- 需要读写的内存值 V
- 进行比较的值 A
- 要写入的新值 B
当且仅当 V 的值等于 A 时,CAS通过原子方式用新值B来更新V的值(“比较+更新”整体是一个原子操作),否则不会执行任何操作。一般情况下,“更新”是一个不断重试的操作。
CAS的问题
- ABA问题。CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”。
- JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,具体操作封装在compareAndSet()中。compareAndSet()首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值。
- 循环时间长开销大。CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。
- 只能保证一个共享变量的原子操作。对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。
- Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。
利用cpu底层指令 比较并交换弄成一个指令
乐观锁,sync…悲观锁:读一样会被锁住,效率太低,sync:锁,cpu底层操作锁的切换,意味着线程切换,锁状态切换,
cas会通过类似于while(true)形式一直占着cpu,悲观锁会进行状态切换
CAS适用于写操作少,读操作多
cas:单点登录服务器
缺点:ABA解决不了
先把i改成2 再改为1 你会认为没有改
AtomicStampedReference ABA问题 1A 2B 3A
version:记录操作版本号 解决ABA 1.0—>2.0—>3.0 比较av成功 再比较版本
例如:AtomicInteger,git版本控制
原子类:Unsafe操作底层cpu,通过cpu指令,达到比较并交换
6.死锁
线程之间互相等待对方释放锁,就是死锁
package cn.cdqf.dead;public class MyDeadLock implements Runnable{private MyLock myLock1 = new MyLock();private MyLock myLock2 = new MyLock();@Overridepublic void run() {synchronized (myLock1){//Thread-1获得了myLock1System.out.println(Thread.currentThread().getName()+"获得了myLock1");synchronized (myLock2){System.out.println(Thread.currentThread().getName()+"获得了myLock2");}}synchronized (myLock2){//Thread-0获得了myLock2System.out.println(Thread.currentThread().getName()+"获得了myLock2");synchronized (myLock1){System.out.println(Thread.currentThread().getName()+"获得了myLock1");}}}public static void main(String[] args) {MyDeadLock myDeadLock = new MyDeadLock();new Thread(myDeadLock).start();new Thread(myDeadLock).start();}
}class MyLock{}
解决方法
编号解死锁:对多把锁 按从小到大排序,每个要用多锁的地方,都先锁小的,再锁大的。
package cn.cdqf.deadlock;
//一般syn里面锁芯不用String 因为String会放在字符串常量池,所以容易出现多个地方锁相同
//也少用Integer -128---127被缓存了的 是同一个对象
public class DeadLockTest {private MyLock myLock1 = new MyLock(1);private MyLock myLock2 = new MyLock(2);public void test1(){//一个地方 A在前B在后 另外一个地方B在前 A在后//现在给锁编号 那么永远小的在前if(myLock1.getI()<myLock2.getI()){synchronized (myLock1){try {Thread.sleep(300);} catch (InterruptedException e) {e.printStackTrace();}synchronized (myLock2){}}}else {synchronized (myLock2){try {Thread.sleep(300);} catch (InterruptedException e) {e.printStackTrace();}synchronized (myLock1){}}}}public void test2(){if(myLock1.getI()<myLock2.getI()){synchronized (myLock1){try {Thread.sleep(300);} catch (InterruptedException e) {e.printStackTrace();}synchronized (myLock2){}}}else {synchronized (myLock2){try {Thread.sleep(300);} catch (InterruptedException e) {e.printStackTrace();}synchronized (myLock1){}}}}
}
7.wait/notify/notifyall
wait():等待,释放锁 来源于 object的 notify------waitset
- 调用wait方法 这个线程会进入 moniter waitset()---------依赖对象 syn(A)
- wait notify notifyall 都需要在同步代码块里面运行-------是同步那个锁芯调用的
wait/notify必须要同步代码块中使用
当前所对象的包含的代码中没有wait的线程,没法叫
当前锁对象中的notify/notifyAll只能叫醒,当前锁对象包含中的wait()
一般都用notifyAll:叫醒当前锁对象中所有的wait方法
notify:随机叫一个
BlockingQueue
BlockingQueue,是java.util.concurrent 包提供的用于解决并发生产者 - 消费者问题 的最有用的类,它的特性是在任意时刻只有一个线程可以进行take或者put操作,并且 BlockingQueue提供了超时return null的机制,在许多生产场景里都可以看到这个工具的 身影。
常见的4种阻塞队列
ArrayBlockingQueue 由数组支持的有界队列
队列基于数组实现,容量大小在创建ArrayBlockingQueue对象时已定义好
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>();
基于ReentrantLock保证线程安全,根据Condition实现队列满时的阻塞
LinkedBlockingQueue 由链接节点支持的可选有界队列
是一个基于链表的无界队列(理论上有界)
blockingQueue 的容量将设置为 Integer.MAX_VALUE 。 向无限队列添加元素的所有操作都将永远不会阻塞,[注意这里不是说不会加锁保证线程安 全],因此它可以增长到非常大的容量。 使用无限 BlockingQueue 设计生产者 - 消费者模型时最重要的是 消费者应该能够像生产 者向队列添加消息一样快地消费消息 。否则,内存可能会填满,然后就会得到一 个 OutOfMemory 异常。
BlockingQueue<String> blockingQueue = new LinkedBlockingQueue<>();
PriorityBlockingQueue 由优先级堆支持的无界优先级队列
BlockingQueue<String> blockingQueue = new DelayQueue();
DelayQueue 由优先级堆支持的、基于时间的调度队列
内部基于无界队列PriorityQueue实现,而无界 队列基于数组的扩容实现
入队的对象必须要实现Delayed接口,而Delayed集成自Comparable接口
BlockingQueue<String> blockingQueue = new DelayQueue();
BlockingQueue API
offer(E e): 将给定的元素设置到队列中,如果设置成功返回true, 否则返回false. e的值不能为空,否则抛出空指针异常。offer(E e, long timeout, TimeUnit unit): 将给定元素在给定的时间内设置到队列中,如果设置成功返回true, 否则返回false.add(E e): 将给定元素设置到队列中,如果设置成功返回true, 否则抛出异常。如果是往限定了长度的队列中设置值,推荐使用offer()方法。put(E e): 将元素设置到队列中,如果队列中没有多余的空间,该方法会一直阻塞,直到队列中有多余的空间。take(): 从队列中获取值,如果队列中没有值,线程会一直阻塞,直到队列中有值,并且该方法取得了该值。poll(long timeout, TimeUnit unit): 在给定的时间里,从队列中获取值,如果没有取到会抛出异常。remainingCapacity():获取队列中剩余的空间。remove(Object o): 从队列中移除指定的值。contains(Object o): 判断队列中是否拥有该值。drainTo(Collection c): 将队列中值,全部移除,并发设置到给定的集合中。
java多线程
注意点:
- 如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式。想要启动多线程,必须调用start方法。
- run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU调度决定。
- 一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出异常“IllegalThreadStateException”
1.线程的生命周期
1、新建状态 NEW
- 在程序中用构造方法创建了一个线程对象后,新的线程对象便处于新建状态,此时,它已经有了相应的内存空间和其它资源,但还处于不可运行状态。新建一个线程对象可采用线程构造方法来实现。
- 例如:Thread thread=new Thread();
2、 就绪状态
- 新建线程对象后,调用该线程的start()方法就可以启动线程。当线程启动时,线程进入就绪状态。此时,线程将进入线程队列排队,等待CPU调用,这表明它已经具备了运行条件。
3、运行状态 RUNNABLE
- 当就绪状态的线程被调用并获得处理器资源时,线程就进入了运行状态。此时,自动调用该线程对象的run()方法。run()方法定义了该线程的操作和功能。
4、 阻塞状态
BLOCKED 被阻塞等待监视器锁定的线程处于这个状态
WAITING:正在等待另一个线程执行特定的动作的线程处于这个状态。
TIMED_WAITING:正在等待另一个线程执行动作达到指定等待时间的线程处于这个状态
5、终止状态 TERMINATED
- 状态转换 runable----WAITING
- wait方法,join方法(底层就是wait,notify)
- 调用 LockSupport.park() 方法。其中的 LockSupport 对象,也许你有点陌生,其实 Java 并发包中的锁,都是基于它实现的。当前线程会阻塞,线程的状态会从 RUNNABLE 转换到 WAITING。调用LockSupport.unpark唤醒当前线程
- 状态转换 runable----timedwait
- 调用带超时参数的 Thread.sleep(long millis) 方法;sleep不会释放锁,抱着锁睡觉
- 获得 synchronized 隐式锁的线程,调用带超时参数的 Object.wait(long timeout) 方法;
- 调用带超时参数的 Thread.join(long millis) 方法;
- 调用带超时参数的 LockSupport.parkNanos(Object blocker, long deadline) 方法;
- 调用带超时参数的 LockSupport.parkUntil(long deadline) 方法。
- waiting/timed_waiting-----dead
当线程 A 处于 WAITING、TIMED_WAITING 状态时,如果其他线程调用线程 A 的 interrupt() 方法,会使线程 A 返回到 RUNNABLE 状态,同时线程 A 的代码会触发 InterruptedException 异常。上面我们提到转换到 WAITING、TIMED_WAITING 状态的触发条件,都是调用了类似 wait()、join()、sleep() 这样的方法,我们看这些方法的签名,发现都会 throws InterruptedException 这个异常。这个异常的触发条件就是:其他线程调用了该线程的 interrupt() 方法。
创建线程的三种方式
1.继承Thread
每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体
通过该Thread对象的start()方法来启动这个线程,而非直接调用run()
注意点:
- 如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式。想要启动多线程,必须调用start方法。
- run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU调度决定。
- 一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出异常“IllegalThreadStateException”
public class Test01 {public static void main(String[] args) {//创建线程对象 创建线程时new了多个任务类,所以 this不唯一,用作锁对象是不能互斥MyThread t1 = new MyThread();MyThread t2 = new MyThread();//启动线程t1.start();t2.start();}
}
//任务类继承了Thread类
class MyThread extends Thread{//重写run方法,当前线程抢到cpu资源后,就会执行run方法@Overridepublic void run() {System.out.println("当前线程抢到资源了");}
}//Thread匿名类直接执行,适用于只使用一次public static void main(String[] args) {new Thread(){@Overridepublic void run() {ystem.out.println("子线程");}}.start(); }
2.实现Runnable接口
实现方式的好处
- 避免了单继承的局限性
- 多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。
- Runnable 接⼝不会返回结果或抛出检查异常
public class Test01 {public static void main(String[] args) {//创建Runnable接口实现类对象 作为参数船给Thread类含参构造器Task task = new Task();//使用Thread类含参构造器创建线程对象 使用的是同一个任务类所以this唯一Thread t1 = new Thread(task);Thread t2 = new Thread(task);t1.start();t2.start();}
}
//任务类实现了Runnable接口
class Task implements Runnable{//当前线程抢到cpu资源后,就会执行run方法@Overridepublic void run() {System.out.println("抢到资源了");}
}//使用Runnable的匿名内部类简化创建过程
Thread t2 =new Thread(new Runnable() {@Overridepublic void run() {}},"子线程2");//给子线程命名String name = t2.getName();System.out.println("name = " + name);//子线程2//使用lanbda
new Thread(()-> {System.out.println("lambda实现了多线程"+Thread.currentThread().getName());} ,"线程1").start();
Callable带返回值的
Callable接口可以返回结果,抛出异常
- Callable可以在任务结束的时候提供一个返回值Future对象,Runnable无法提供这个功能
- Callable的call方法分可以抛出异常,而Runnable的run方法不能抛出异常。
- Callable规定的方法是call(),而Runnable规定的方法是run().
Future
Future就是对于具体的Runnable或者Callable任务的执行结果进行查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。异步任务执行完回调方法;
FutureTask
FutureTask是Future接口的一个实现类(唯一的实现类)
- boolean cancel方法用来取消任务,如果取消任务成功则返回true,如果取消任务失败则返回false。
- 参数mayInterruptIfRunning 表示是否允许取消正在执行却没有执行完毕的任务,如果设置true,则表示可以取消正在执行过程中的任务。
- boolean isCancelled 方法表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true。
- boolean isDone方法表示任务是否已经完成,若任务完成,则返回true;
- get()方法用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回;
- get(long timeout, TimeUnit unit)用来获取执行结果,如果在指定时间内,还没获取到结果,就直接返回null。
也就是说Future提供了三种功能:
1)判断任务是否完成;
2)能够中断任务;
3)能够获取任务执行结果。
Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。
import java.util.concurrent.*;
//任务类 可以有 属性 传入参数用作计算
public class CallTest implements Callable<String> {private int temp;public Test2(int temp) {this.temp = temp;}public Test2() {}//重写call方法,可以自定义返回值泛型,不能传入参数 所以只能对属性进行编辑@Overridepublic String call() throws Exception {temp=temp+5;System.out.println(Thread.currentThread().getName()+",执行了");return temp;}//使用FutureTask执行单个任务public static void main(String[] args) {//3.创建Callable接口实现类的对象NumThread numThread = new NumThread();//4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象FutureTask futureTask = new FutureTask(numThread);//5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()new Thread(futureTask).start();try {//6.获取Callable中call方法的返回值//get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。Object sum = futureTask.get();System.out.println("总和为:" + sum);} catch (InterruptedException e) {e.printStackTrace();} catch (ExecutionException e) {e.printStackTrace();}}//使用线程池 执行多个任务public static void main(String[] args) throws ExecutionException, InterruptedException {//定义线程池ExecutorService executorService = Executors.newFixedThreadPool(2);//将任务类传入 并执行Future<String> submit = executorService.submit(new CallTest());//获取执行结果String s = submit.get();System.out.println("获得callable方法返回结果:"+s);//使用lambda表达式Future<String> submit1 = executorService.submit(() -> {System.out.println(Thread.currentThread().getName() + ",执行了");return "天青色等烟雨";});//线程池要关闭executorService.shutdown();}
}
实现 Runnable 接⼝和 Callable 接⼝的区别
Runnable 接⼝不会返回结果或抛出检查异常,但是 Callable 接⼝ 可以。所以,如果任务不需要返回结果或抛出异常推荐使⽤ Runnable 接⼝,这样代码看起来会 更加简洁。
⼯具类 Executors 可以实现 Runnable 对象和 Callable 对象之间的相互转换。
Executors.callable(Runnable task )或 Executors.callable(Runnable task,Object result )
execute()⽅法和 submit()
- execute() ⽅法⽤于提交不需要返回值的任务,所以⽆法判断任务是否被线程池执⾏成功与 否;
- submit() ⽅法⽤于提交需要返回值的任务。线程池会返回⼀个 Future 类型的对象,通过 这个 Future 对象可以判断任务是否执⾏成功,并且可以通过 Future 的 get() ⽅法来获取 返回值, get() ⽅法会阻塞当前线程直到任务完成,⽽使⽤ get((long timeout,TimeUnit unit) ⽅法则会阻塞当前线程⼀段时间后⽴即返回,这时候有可能任务没有执⾏完
4.Thread类常用方法
a.start方法start()用来启动一个线程,当调用start方法后,系统才会开启一个新的线程来执行用户定义的子任务
a.getId用来得到线程ID
a.getName和setName//设置线程优先级,只会影响优先级 但不能决定同优先级线程组成先进先出队列(先到先服务),使用时间片策略对高优先级,使用优先调度的抢占式策略a.setPriority(Thread.MAX_PRIORITY);//10b.setPriority(Thread.NORM_PRIORITY);//5c.setPriority(Thread.MIN_PRIORITY);//1//返回线程优先级a.getPriority() //获取当前线程的对象 (哪个线程调用了run方法,就获取哪个线程的对象)Thread t = Thread.currentThread();
//获取当前线程名称System.out.println(t.getName() + ":" + i);
//休眠 该方法是静态方法,写在哪个线程中,哪个线程就休眠Thread.sleep(1000);//休眠1000毫秒
//获取当前活跃线程数int i = Thread.activeCount();
//礼让的含义:让当前线程退出CPU资源,又立马到争抢资源的状态Thread.yield();
// 线程的合并 - join() 会让t线程先执行完毕t.join();//让t线程加入到当前线程中
//结束线程,会直接结束线程,可能会产生脏数据过时的方法
t.stop
//改变线程状态,,会把一个方法执行完毕,即优雅的结束线程,不会产生脏数据t.interrupt();
//设置该线程名称t.setName(String name):1. 自己写getThreadName/setThreadName2. 调用父类的一个参数的构造方法和getName3. Thread.currentThread().getName()// 使用构造器给线程命名,早Thread子类中建立构造器public 类名(String name){super(name);//Thread变成有参构造了 在new线程对象时可以添加姓名}//获取当前线程状态 false-没有销毁 true-销毁 Thread.currentThread().isInterrupted()//用来设置线程是否成为守护线程和判断线程是否是守护线程。setDaemon和isDaemonJava中的线程分为两类:一种是守护线程,一种是用户线程。
它们在几乎每个方面都是相同的,唯一的区别是判断JVM何时离开。
守护线程是用来服务用户线程的,通过在start()方法前调用
当所有的前台线程都消亡后,守护线程会自动消亡* 注意:垃圾回收器就是守护线程
public class Daemon extends Thread{@Overridepublic void run() {while(true){System.out.println("守护线程正在默默守护着前台线程");try {Thread.sleep(1000);} catch (InterruptedException e) {}}}public static void main(String[] args) throws InterruptedException {Daemon daemon = new Daemon();daemon.setDaemon(true); //将当前线程设置为守护线程daemon.start();
CompletableFuture
CompletableFuture实现了CompletionStage接口和Future接口,前者是对后者的一个扩展,增加了异步回调、流式处理、多个Future组合处理的能力,使Java在处理多任务的协同工作时更加顺畅便利。
细节看后面补充
9.线程池
如果并发的请求数量非常多,但每个线程执行的时间很短,这样就会频繁的创建和销毁 线程,如此一来会大大降低系统的效率。可能出现服务器在为每个请求创建新线程和销毁线 程上花费的时间和消耗的系统资源要比处理实际的用户请求的时间和资源更多。 那么有没有一种办法使执行完一个任务,并不被销毁,而是可以继续执行其他的任务 呢? 这就是线程池的目的了。线程池为线程生命周期的开销和资源不足问题提供了解决方 案。通过对多个任务重用线程,线程创建的开销被分摊到了多个任务上。
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
线程池运行过程
-- 分析ThreadPoolExecutor类的构造方法源码--------------------------------
public ThreadPoolExecutor(int corePoolSize, ------------- 核心线程数量 int maximumPoolSize, ------------- 最大线程数量 long keepAliveTime, ------------- 闲置时间,作用于核心线程数与最大线程数之间的线程TimeUnit unit, ------------- keepAliveTime的时间单位(可以是毫秒、秒....)BlockingQueue<Runnable> workQueue, -- 任务队列ThreadFactory threadFactory, -------- 线程工厂RejectedExecutionHandler handler ---- 达到了线程界限和队列容量时的处理方案(拒绝策略)
) {}执行步骤:1.创建线程池后2.任务提交后,查看是否有核心线程:3.1 没有 -> 就创建核心线程 -> 执行任务 -> 执行完毕后又回到线程池中3.2 有 -> 查看是否有闲置核心线程:4.1 有 -> 执行任务 -> 执行完毕后又回到线程池4.2 没有 -> 查看当前核心线程数是否达到核心线程数量:5.1 否 -> 就创建核心线程 -> 执行任务 -> 执行完毕后又回到线程池中5.2 是 -> 查看任务列表是否装载满:6.1 没有 -> 就放入列表中,等待出现闲置线程6.2 装满 -> 查看是否有普通线程(核心线程数到最大线程数量之间的线程)7.1 没有 -> 就创建普通线程 -> 执行任务 -> 执行完毕后又回到线程池中7.2 有 -> 查看是否有闲置普通线程7.1.1 有 -> 执行任务 -> 执行完毕后又回到线程池中7.1.2 没有 -> 查看现在所有线程数量是否为最大线程数:8.1 是 -> 执行处理方案(默认处理抛出异常)8.2 否 ->就创建普通线程-> 执行任务 -> 执行完毕后又回到线程池中
注:1.为了更好的理解,在这里区分核心线程和普通线程,实际上区分的没这么清楚,都是线程2.默认的处理方案就是抛出RejectedExecutionException
任务队列详解
队列名称 | 详解 |
---|---|
LinkedBlockingQueue无界任务队列 | 使用无界任务队列,线程池的任务队列可以无限制的添加新的任务,而线程池创建的最大线程数量就是你corePoolSize设置的数量,也就是说在这种情况下maximumPoolSize这个参数是无效的,哪怕你的任务队列中缓存了很多未执行的任务,当线程池的线程数达到corePoolSize后,就不会再增加了;若后续有新的任务加入,则直接进入队列等待,当使用这种任务队列模式时,一定要注意你任务提交与处理之间的协调与控制,不然会出现队列中的任务由于无法及时处理导致一直增长,直到最后资源耗尽的问题 |
SynchronousQueue 同步任务队列 直接提交任务队列 | 使用直接提交任务队列,队列没有容量,每执行一个插入操作就会阻塞,需要再执行一个删除操作才会被唤醒,反之每一个删除操作也都要等待对应的插入操作。 任务队列为SynchronousQueue,创建的线程数大于maximumPoolSize时,直接执行了拒绝策略抛出异常。 使用SynchronousQueue队列,提交的任务不会被保存,总是会马上提交执行。如果用于执行任务的线程数量小于maximumPoolSize,则尝试创建新的线程,如果达到maximumPoolSize设置的最大值,则根据你设置的handler执行拒绝策略。因此这种方式你提交的任务不会被缓存起来,而是会被马上执行,在这种情况下,你需要对你程序的并发量有个准确的评估,才能设置合适的maximumPoolSize数量,否则很容易就会执行拒绝策略; |
ArrayBlockingQueue有界任务队列 | 使用有界任务队列,若有新的任务需要执行时,线程池会创建新的线程,直到创建的线程数量达到corePoolSize时,则会将新的任务加入到等待队列中。若等待队列已满,即超过ArrayBlockingQueue初始化的容量,则继续创建线程,直到线程数量达到maximumPoolSize设置的最大线程数量,若大于maximumPoolSize,则执行拒绝策略。在这种情况下,线程数量的上限与有界任务队列的状态有直接关系,如果有界队列初始容量较大或者没有达到超负荷的状态,线程数将一直维持在corePoolSize以下,反之当任务队列已满时,则会以maximumPoolSize为最大线程数上限。 |
PriorityBlockingQueue优先任务队列 | 使用优先任务队列,它其实是一个特殊的无界队列,它其中无论添加了多少个任务,线程池创建的线程数也不会超过corePoolSize的数量,只不过其他队列一般是按照先进先出的规则处理任务,而PriorityBlockingQueue队列可以自定义规则根据任务的优先级顺序先后执行。 |
拒绝策略
ThreadPoolExecutor自带的拒绝策略有四种,都实现了RejectedExecutionHandler接口
比如:new ThreadPoolExecutor.AbortPolicy()
拒绝策略 | 解释 |
---|---|
AbortPolicy | 当有任务添加到线程池被拒绝时,会抛出RejectedExecutionException异常 线程池默认的拒绝策略。必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行。 |
DiscardPolicy | 当有任务添加到线程池被拒绝时,直接丢弃,其他啥都没有 |
CallerRunsPolicy | 当有任务添加到线程池被拒绝时,线程池会将被拒绝的任务添加到线程池正在运行的线程中去运行。 一般并发比较小,性能要求不高,不允许失败。但是,由于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大 |
DiscardOledestPolicy | 当有任务添加到线程池被拒绝时,线程池会丢弃阻塞队列中末尾的任务(最老的任务–第一个添加的任务),然后将被拒绝的任务添加到末尾。 如果项目中有允许丢失任务的需求,可以使用 |
自定义拒绝策略
public class Test {public static void main(String[] args) {ThreadPoolExecutor pool = new ThreadPoolExecutor(1, 1, 1000, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(1), Executors.defaultThreadFactory(), new RejectedExecutionHandler() {@Overridepublic void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {System.out.println(r.toString()+"执行了拒绝策略");}});for (int i = 1; i <= 10; i++) {pool.execute(new Task());}pool.shutdown();}
}
class Task implements Runnable{@Overridepublic void run() {try {Thread.sleep(1000);System.out.println(Thread.currentThread().getName());} catch (InterruptedException e) {e.printStackTrace();}}
}
自定义ThreadPoolExecutor
ThreadPoolExecutor扩展主要是围绕beforeExecute()、afterExecute()和terminated()三个方法的重写, 通过这三个方法我们可以监控每个任务的开始和结束时间,或者其他一些功能。
方法 解释 beforeExecute 线程池中任务运行前执行 afterExecute 线程池中任务运行完毕后执行 terminated 线程池退出后执行 下面我们可以通过代码实现一下
ThreadPoolExecutor pool = new ThreadPoolExecutor(10, 10, 1000, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(10), new ThreadFactory() {int threadNum = 1;@Overridepublic Thread newThread(Runnable r) {Thread t = new Thread(r,"线程"+threadNum++);return t;}}, new ThreadPoolExecutor.AbortPolicy()){@Overrideprotected void beforeExecute(Thread t,Runnable r) {System.out.println("准备执行:"+ Thread.currentThread().getName());}@Overrideprotected void afterExecute(Runnable r,Throwable t) {System.out.println("执行完毕:"+ Thread.currentThread().getName());}@Overrideprotected void terminated() {System.out.println("线程池退出");}};for (int i = 1; i <= 10; i++) {pool.execute(new Task());}pool.shutdown();}
}
class Task implements Runnable{@Overridepublic void run() {try {Thread.sleep(1000);System.out.println("执行中:" + Thread.currentThread().getName());} catch (InterruptedException e) {e.printStackTrace();}}
}
CompletableFuture使用
1.1runAsync(一个Runnable接口,线程池)
runAsync(一个Runnable接口,线程池)
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;public class CompletableFutureTest {private static final AtomicInteger ATOMIC_INTEGER = new AtomicInteger(0);public static void main(String[] args) throws ExecutionException, InterruptedException {ExecutorService executorService = Executors.newFixedThreadPool(10, new ThreadFactory() {@Overridepublic Thread newThread(Runnable r) {//创建线程Thread thread = new Thread(r);thread.setName("cdqf_" + ATOMIC_INTEGER.incrementAndGet());return thread;}});CompletableFuture<Void> voidCompletableFuture = CompletableFuture.runAsync(() -> System.out.println(Thread.currentThread().getName() + ",go go go"), executorService);System.out.println(voidCompletableFuture.get());executorService.shutdown();}}
一个参数
runAsync(一个Runnable接口) 默认使用这个线程池ForkJoinPool.commonPool()
CompletableFuture<Void> voidCompletableFuture = CompletableFuture.runAsync(() -> {try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "执行了");});System.out.println( voidCompletableFuture.get());System.out.println("睡完了.....");
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;public class CompletableFutureTest {private static AtomicInteger atomicInteger = new AtomicInteger(0);public static void main(String[] args) throws ExecutionException, InterruptedException {ExecutorService executorService = Executors.newFixedThreadPool(10, new ThreadFactory() {@Overridepublic Thread newThread(Runnable r) {Thread thread = new Thread(r);thread.setName("cdqf-"+atomicInteger.incrementAndGet());return thread;}});CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(()->{try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"run方法被调用了");},executorService);System.out.println(completableFuture.get());System.out.println("completableFuture runasync执行结束");executorService.shutdown();}}
1.2supplyAsync
- 两个参数 supplyAsync(Supplier supplier,线程池);
- 一个参数 supplyAsync(Supplier supplier)
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;public class CompletableFutureTest {private static AtomicInteger atomicInteger = new AtomicInteger(0);public static void main(String[] args) throws ExecutionException, InterruptedException {ExecutorService executorService = Executors.newFixedThreadPool(10, new ThreadFactory() {@Overridepublic Thread newThread(Runnable r) {Thread thread = new Thread(r);thread.setName("cdqf-"+atomicInteger.incrementAndGet());return thread;}});CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "正在执行");return "hello CompletableFuture";}, executorService);System.out.println(completableFuture.get());System.out.println("带返回值的CompletableFuture执行结束");executorService.shutdown();}}
1.3thenApply()
跟supplyAsync使用同一个线程运行
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;public class CompletableFutureTest {private static AtomicInteger atomicInteger = new AtomicInteger(0);public static void main(String[] args) throws ExecutionException, InterruptedException {ExecutorService executorService = Executors.newFixedThreadPool(10, new ThreadFactory() {@Overridepublic Thread newThread(Runnable r) {Thread thread = new Thread(r);thread.setName("cdqf-"+atomicInteger.incrementAndGet());return thread;}});CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "正在执行");return "hello CompletableFuture";}, executorService).thenApply(s->{System.out.println("thenApply开始执行,执行的线程为:"+Thread.currentThread().getName());System.out.println("supplyAsync方法执行完毕,并获得supplyAsync的返回值:"+s);return s.toUpperCase();});System.out.println(completableFuture.get());System.out.println("带返回值的CompletableFuture执行结束");executorService.shutdown();}}
1.4thenApplyAsync
再上面的基础上,thenApplyAsync可以使用另外的线程执行,当然也可以指定线程池
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;public class CompletableFutureTest {private static AtomicInteger atomicInteger = new AtomicInteger(0);public static void main(String[] args) throws ExecutionException, InterruptedException {ExecutorService executorService = Executors.newFixedThreadPool(10, new ThreadFactory() {@Overridepublic Thread newThread(Runnable r) {Thread thread = new Thread(r);thread.setName("cdqf-"+atomicInteger.incrementAndGet());return thread;}});ExecutorService executorService2 = Executors.newFixedThreadPool(10, new ThreadFactory() {@Overridepublic Thread newThread(Runnable r) {Thread thread = new Thread(r);thread.setName("线程池二-"+atomicInteger.incrementAndGet());return thread;}});CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "正在执行");return "hello CompletableFuture";}, executorService).thenApplyAsync(s->{System.out.println("thenApply开始执行,执行的线程为:"+Thread.currentThread().getName());System.out.println("supplyAsync方法执行完毕,并获得supplyAsync的返回值:"+s);return s.toUpperCase();},executorService2);System.out.println(completableFuture.get());System.out.println("带返回值的CompletableFuture执行结束");executorService.shutdown();executorService2.shutdown();}}
1.5thenAccept()
跟上面的方法类似,只不过没有返回值,参数是消费者接口
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;public class CompletableFutureTest {private static AtomicInteger atomicInteger = new AtomicInteger(0);public static void main(String[] args) throws ExecutionException, InterruptedException {ExecutorService executorService = Executors.newFixedThreadPool(10, new ThreadFactory() {@Overridepublic Thread newThread(Runnable r) {Thread thread = new Thread(r);thread.setName("cdqf-"+atomicInteger.incrementAndGet());return thread;}});ExecutorService executorService2 = Executors.newFixedThreadPool(10, new ThreadFactory() {@Overridepublic Thread newThread(Runnable r) {Thread thread = new Thread(r);thread.setName("线程池二-"+atomicInteger.incrementAndGet());return thread;}});CompletableFuture<Void> completableFuture = CompletableFuture.supplyAsync(() -> {try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "正在执行");return "hello CompletableFuture";}, executorService).thenAccept(s-> System.out.println("获得上面的结果:"+s+",但是自己没有返回值了"));System.out.println(completableFuture.get());System.out.println("带返回值的CompletableFuture执行结束");executorService.shutdown();executorService2.shutdown();}}
thenAcceptAsync跟上面一样只不过可以换线程,也可以换线程池
1.6thenRun()
跟上面方法类似,只不过拿不到参数,参数为一个runnable对象
1.7thenCompose
组装多个有关联关系的异步任务,比如第二个future依赖第一个的id信息,得到最终的结果
当然也可以通过重载方法指定线程池
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;public class CompletableFutureTest {public static void main(String[] args) throws ExecutionException, InterruptedException {CompletableFuture<String> completableFuture = getProjectId().thenCompose(id -> getProjectById(id));System.out.println("获得最终的结果为:"+completableFuture.get());}//根据某某条件获得商品idprivate static CompletableFuture<Integer> getProjectId(){return CompletableFuture.supplyAsync(()->{System.out.println("当前执行的线程为:"+Thread.currentThread().getName());System.out.println("这里面执行了非常多的操作然后得到了一个项目ID");return 10;});}//根据上面获得商品id去查询商品的详细信息private static CompletableFuture<String> getProjectById(int id){return CompletableFuture.supplyAsync(()->{System.out.println("当前执行的线程为:"+Thread.currentThread().getName());System.out.println("这里面执行了非常多的操作然后根据id获得了商品的详细信息,id:"+id);return "华为手机";});}}
1.8thenCombine
组装两个没有参数关联的异步CompletableFuture
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;public class CompletableFutureTest {private static final AtomicInteger ATOMIC_INTEGER = new AtomicInteger(0);public static void main(String[] args) throws ExecutionException, InterruptedException {//插入一条员工信息,一组任务插入头像 一组任务生活照CompletableFuture<String> completableFuture1 = CompletableFuture.supplyAsync(() -> {System.out.println("插入头像,并返回头像文件id");return "abc";});CompletableFuture<String> completableFuture2 = CompletableFuture.supplyAsync(() -> {System.out.println("插入生活照,并返回生活照文件id");return "bbc";});CompletableFuture<String> completableFuture = completableFuture1.thenCombine(completableFuture2, (headId, shzId) -> {try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("获得上面的结果:headId:" + headId + ",shzId:" + shzId);System.out.println("执行数据库插入");return "插入success";});String join = completableFuture.join();System.out.println(join);}
}
1.9allOf
多个任务执行成功
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;public class CompletableFutureTest {private static final AtomicInteger ATOMIC_INTEGER = new AtomicInteger(0);public static void main(String[] args) throws ExecutionException, InterruptedException {CompletableFuture<Void> completableFuture1 = CompletableFuture.runAsync(() -> {try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("第一个task");});CompletableFuture<Void> completableFuture2 = CompletableFuture.runAsync(() -> {try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("第二个task");});CompletableFuture<Void> completableFuture3 = CompletableFuture.runAsync(() -> {try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("第三个task");});//希望上面三个任务都完成才继续后面CompletableFuture<Void> allOf = CompletableFuture.allOf(completableFuture1, completableFuture2, completableFuture3);allOf.join();System.out.println("三个任务全部结束,现在可以继续后面的工作");}
}
2.0anyOf
返回最先执行结束的CompletableFuture
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;public class CompletableFutureTest {private static final AtomicInteger ATOMIC_INTEGER = new AtomicInteger(0);public static void main(String[] args) throws ExecutionException, InterruptedException {CompletableFuture<String> completableFuture1 = CompletableFuture.supplyAsync(() -> {try {Thread.sleep(2000);System.out.println("第一个方法ok");} catch (InterruptedException e) {e.printStackTrace();}return "first blood";});CompletableFuture<String> completableFuture2 = CompletableFuture.supplyAsync(() -> {try {Thread.sleep(3000);System.out.println("第二个方法ok");} catch (InterruptedException e) {e.printStackTrace();}return "second blood";});CompletableFuture<String> completableFuture3 = CompletableFuture.supplyAsync(() -> {try {Thread.sleep(5000);System.out.println("第三个方法ok");} catch (InterruptedException e) {e.printStackTrace();}return "third blood";});CompletableFuture<Object> objectCompletableFuture = CompletableFuture.anyOf(completableFuture1, completableFuture2, completableFuture3);Object o = objectCompletableFuture.get();System.out.println("某一个方法执行结束:"+o);}
}
2.1异常处理
前面有一个出现异常,后面的都不会执行
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;public class CompletableFutureTest {public static void main(String[] args) throws ExecutionException, InterruptedException {CompletableFuture<Void> voidCompletableFuture = CompletableFuture.supplyAsync(() -> {System.out.println("supplyAsync方法执行");throw new RuntimeException("在supplyAsync方法中出现异常");}).thenApply(s ->{System.out.println("因为上面出现了异常我就不会执行了");return "不会执行";}).thenAccept(t -> {System.out.println("前面都没执行更别说我了");});voidCompletableFuture.join();System.out.println("出现异常不执行了");}}
处理方式一exceptionally:出现异常才会执行,返回一个降级结果
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;public class CompletableFutureTest {public static void main(String[] args) throws ExecutionException, InterruptedException {CompletableFuture<String> voidCompletableFuture = CompletableFuture.supplyAsync(() -> {System.out.println("supplyAsync方法执行");throw new RuntimeException("在supplyAsync方法中出现异常");}).thenApply(s ->{System.out.println("因为上面出现了异常我就不会执行了");return "不会执行";}).exceptionally(e->{System.out.println(Thread.currentThread().getName()+"执行过程中出现了异常了"+e);return "凉凉";});System.out.println(voidCompletableFuture.get());;System.out.println("出现异常不执行了");}}
handle():有没有异常都会执行,可以根据判断来返回不同的东西
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;public class CompletableFutureTest {public static void main(String[] args) throws ExecutionException, InterruptedException {CompletableFuture<String> voidCompletableFuture = CompletableFuture.supplyAsync(() -> {System.out.println("supplyAsync方法执行");//throw new RuntimeException("在supplyAsync方法中出现异常");return "没有异常继续下面执行";}).thenApply(s ->{System.out.println("因为上面出现了异常我就不会执行了");return "不会执行";}).handleAsync((result,e)->{if(e!=null) {System.out.println(Thread.currentThread().getName() + "执行过程中出现了异常了" + e);return "凉凉";}else {System.out.println("程序没有出现异常,返回正常结果");return result;}},Executors.newSingleThreadExecutor());System.out.println(voidCompletableFuture.get());;System.out.println("出现异常不执行了");}}
Java多线程及线程池相关推荐
- Java多线程之线程池配置合理线程数
Java多线程之线程池配置合理线程数 目录 代码查看公司服务器或阿里云是几核的 合理线程数配置之CPU密集型 合理线程数配置之IO密集型 1. 代码查看公司服务器或阿里云是几核的 要合理配置线程数首先 ...
- Java多线程之线程池的手写改造和拒绝策略
Java多线程之线程池的手写改造和拒绝策略 目录 自定义线程池的使用 四种拒绝策略代码体现 1. 自定义线程池的使用 自定义线程池(拒绝策略默认AbortPolicy) public class My ...
- Java多线程之线程池7大参数、底层工作原理、拒绝策略详解
Java多线程之线程池7大参数详解 目录 企业面试题 线程池7大参数源码 线程池7大参数详解 底层工作原理详解 线程池的4种拒绝策略理论简介 面试的坑:线程池实际中使用哪一个? 1. 企业面试题 蚂蚁 ...
- Java多线程之线程池详解
Java多线程之线程池详解 目录: 线程池使用及优势 线程池3个常用方式 线程池7大参数深入介绍 线程池底层工作原理 1. 线程池使用及优势 线程池做的工作主要是控制运行的线程的数量,处理过程中将任务 ...
- java 多线程使用线程池_Java多线程:如何开始使用线程
java 多线程使用线程池 什么是线程? (What is a Thread?) A thread is a lightweight process. Any process can have mul ...
- java多线程之线程池简介
前言 池化技术已经屡见不鲜了,比如数据库连接池,大家的项目中应该也用到了线程池.池化技术的好处:降低资源的消耗,提高响应速度,提高线程的可管理性.本篇主要是和大家一起分析下线程池的架构和它的工作流程. ...
- java多线程及线程池使用
Java多线程及线程池的使用 Java多线程 一.Java多线程涉及的包和类 二.Java创建多线程的方式 三.Java线程池 1. 创建线程池ThreadPoolExecutor的7个参数 2. 线 ...
- Java多线程之线程池的参数和配置
在Java多线程编程中,线程池是一种常见的技术,用于管理线程的创建和销毁.线程池中的线程可以被重复利用,从而减少了线程的创建和销毁的开销,提高了程序的性能.在Java中,线程池的参数和配置非常重要,不 ...
- java多线程和线程池
目录 零.java线程理解 0.1 两种线程模型 0.1.1 用户级线程 ULT 0.1.2 内核级线程 KLT--JAVA虚拟机使用的线程模型(KLT) 0.2 java线程与系统内核线程 0.3 ...
- Java多线程估算线程池
原理 多线程的线程池可以提高程序的运行性能,这是毋庸置疑的,但是如何设置线程池的大小,以前一直是靠猜,或者根据经验配置,这两天对系统的线程池进行优化配置时,找到了如何合理地估算线程池大小这篇文章,在这 ...
最新文章
- 【TensorFlow】:Eager Mode(动态图模式)
- Linux操作系统及平台虚拟化技术漫谈
- 940mx黑苹果驱动_专业黑苹果系统安装 win macos双系统10.13/14/15
- pytorch1.7教程实验——对抗示例生成FGSM
- 图像 pipeline_ARADEEPOPSIS:一个基于叶状态语义分割的自动植物表型Pipeline
- struts json序列化遇上replaceAll就出问题
- Apollo使用指南(一)普通应用接入指南
- 饥荒联机版运行不了服务器,饥荒联机版启动服务器出现问题 | 手游网游页游攻略大全...
- 苹果企业版帐号申请记录
- 免费素材下载:一套超棒的免费UI套件
- 创业公司如何做到零成本实现用户快速增长
- Socket.io 的 emit
- 基于java的高校科研管理系统
- RFC 文档(1001-1500)
- 天文学家发现“超级地球”
- 第一次使用Maven,新建Maven项目时更新出错出现Unable to update maven configuration following project...
- CSP-2019day1题解报告
- 基于mbedtls的AES加密(C/C++)
- 框架 --mybatis(ORM映射)-数据库技术
- 【转】抽象语法树简介(AST)