高并发相关知识
1.volatile
volatile 是轻量级的同步机制,有三个特性:1.可保证可见性,2不能保证原子性,3.能保证禁止指令重排。
1.保证可见性:例子:(一定要结合JMM内存模型来看)
public class Test01 {
public int num = 0;
public void add20(){
this.num = 20;
}
//验证volatile可见性
//1.1 假如:int num = 0;num前没有被volatile关键字修饰,没有可见性
public static void main(String[] args) {
Test01 myData = new Test01();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t come in");
try {
Thread.sleep(3000);
myData.add20();
System.out.println(Thread.currentThread().getName()+"\t update num value"+myData.num);
} catch (Exception e) {
e.printStackTrace();
}
},"AAA").start();
while(myData.num==0){//这块说明main在操作自己工作内存的来自主内存的副本num。
//main线程就在这里一直等待循环,直到num值不再为0.
}
System.out.println(Thread.currentThread().getName()+"\t mission is over");
}
}
结果:
AAA come in
AAA update num value20
此时程序一直等待 原因:num一直为0,main不知道线程AAA修改了num的值
当在int num 前 加了 volatile后结果为:
AAA come in
AAA update num value20
main mission is over
说明volatile 保证了 num 的可见性。主线程的num副本 改为了20.
2.不能保证原子性:**
案例:
public class Test02 {
volatile int num = 0;
public void add(){
num++;
}
public static void main(String[] args) {
Test02 tt = new Test02();
for(int i=0;i<20;i++){
new Thread(()->{
for(int j=0;j<1000;j++){
tt.add();
}
},String.valueOf(i)).start();
}
//等待所有累加线程都结束
while(Thread.activeCount()>2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"\t finally number value:"+tt.num);
}
}
结果:19871/19885
解决:
public class Test03 {
volatile int num = 0;
AtomicInteger ai = new AtomicInteger();
public void add(){
num++;
}
public void addnew(){
ai.getAndIncrement();
}
public static void main(String[] args) {
Test03 tt = new Test03();
for(int i=0;i<20;i++){
new Thread(()->{
for(int j=0;j<1000;j++){
//tt.add();
tt.addnew();
}
},String.valueOf(i)).start();
}
//等待所有累加的线程都结束。
while(Thread.activeCount()>2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"\t finally number value:"+tt.ai);
}
}
2.2为什么不能保证原子性why**: 当多个线程修改完自己工作内存中的变量值,然后把这个修改后的变量在写入主内存中,在此时,如果某个正在写回去的线程发生了阻塞,这时另外一个线程读取主内存中的值,仍然是变化之前的值,然后进行加1,再写回到内存,这是第一个线程在完成写入工作,就会丢失。没有读取之间写入的值,直接写了进去。写覆盖,写丢了。
2.4 如何解决不保证原子性?**
加synchronized(杀鸡焉用牛刀)
使用 juc 下的AtomicInteger. 见上面代码。
number++ 在多线程下是非线程安全的,如何不加synchronized
3.能保证禁止指令重排**
3.1 示例:
public class ReSortSeqDemo {
int a = 0;
boolean flag = false;
public void method1(){
a = 1;
flag = true;
}
//多线程中线程交替执行,由于编译器优化重排的存在,
//两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
public void method2(){
if(flag){
a = a + 5;
System.out.println("*****retVal:"+a);
}
}
}
解决: volatile 禁止指令重排,从而避免了多线程环境下程序出现乱序执行的现象。
补充: 内存屏障:通过插入内存屏障禁止在内存屏障前后的指令执行的重新排序问题。
2.JMM(java 内存memory 模型model)
它并不真实存在,是一种抽象的,规范定义了程序中各个变量(包括实例字段、静态字段、构成数组对象的元素)
三大特性: 1 可见性 指的是对于各线程修改后的变量写回到主内存中后,对其他所有线程都要读取主内存中的该变量,自己工作内存中的该变量的值作废,将新值赋给各自工作内存中的变量。
2 原子性 指的是不可分割、完整性。即某个线程正在做某个具体业务时,中间不可以被加塞或这分割需要整体完整,要么同时成功,要么同时失败。例如a++,对于共享变量a的操作,实际上会执行三个步骤,1.读取变量a的值 2.a的值+1 3.将值赋予变量a 。 这三个操作中任何一个操作过程中,a的值被人篡改,那么都会出现我们不希望出现的结果。所以我们必须保证这是原子性的。Java中的锁的机制解决了原子性的问题。
3 有序性 计算机在执行程序时,为了提高性能,编译器和处理器常常会对***指令进行重排***,一般分为:编译器优化的重排、指令并行的重排、内存系统的重排。
单线程环境里确保程序最终执行结果和代码顺序执行一致。
处理器在进行重排序时必须要先考虑指令间的数据依赖性。就是说有依赖关系的不能重排(其实单线程就算能重排也不会出现问题,重排结果不会存在任何问题)
多线程环境中线程交替执行,由于编译器优化重排,两个线程之间能否保证一致性是无法确定的,结果无法预测。
JMM对于同步的规定:
1.解锁前,必须把共享变量的值刷回到主内存
2.加锁前,必须读取主内存中的最新值到自己的工作内存
3.加锁解锁是同一把锁
JVM运行程序的实体是线程,没一个线程都有各自的jvm为其创建的工作内存,是每个线程的私有区域,而JAVA内存模型中规定所有变量都存储在主内存中,主内存是共享内存,所有线程都可以访问,但线程对变量的操作必须在自己的工作空间中进行,首先将主内存中的变量的拷贝读取到自己的工作空间中,然后对其进行操作,操作完成后将变量写回到主内存中,不能直接操作主内存中的变量,所以不同线程间无法访问到对方的工作内存,线程间通信(传值)必须通过主内存来完成。
心中有图 图在手机里。
3.在哪里用过volatile
1.单例设计模式
1.1: 在多线程时,这个单例的构造方法被调用了很多次,不能达到目的。
public class SingleDemo {
private static SingleDemo instance = null;
private SingleDemo(){
System.out.println(Thread.currentThread().getName()+"\t 我是构造方法singleDemo");
}
public static SingleDemo getInstance(){
if(instance==null){
instance = new SingleDemo();
}
return instance;
}
public static void main(String[] args) {
for(int i=1;i<=10;i++){
new Thread(()->{
SingleDemo.getInstance();
},String.valueOf(i)).start();
}
}
}
结果:显然有问题
2 我是构造方法singleDemo
3 我是构造方法singleDemo
4 我是构造方法singleDemo
1 我是构造方法singleDemo
5 我是构造方法singleDemo
1.2 解决方案 :高并发下DCL模式(双端检锁机制)
public static SingleDemo getInstance(){
if(instance==null){
synchronized(SingleDemo.class){
if(instance==null){
instance = new SingleDemo();
}
}
}
return instance;
}
但是,这种情况下 任然会存在指令重排问题。就是说给instance复值了,指向一块内存,但是这块内存还没有初始化(放入对象),这时instance虽然不为空,但是里面啥也没有,就返回了,出现了问题。(指令有3个:1.分配空间 2.放入对象 3.用instance指向它,如果指令重排,就有可能出问题)
所以:最终解决方案: 加入volatile
public class SingleDemo {
private static volatile SingleDemo instance = null;
private SingleDemo(){
System.out.println(Thread.currentThread().getName()+"\t 我是构造方法singleDemo");
}
public static SingleDemo getInstance(){
if(instance==null){
synchronized(SingleDemo.class){
if(instance==null){
instance = new SingleDemo();
}
}
}
return instance;
}
public static void main(String[] args) {
for(int i=1;i<=10;i++){
new Thread(()->{
SingleDemo.getInstance();
},String.valueOf(i)).start();
}
}
}
4.CAS
4.1 CAS 是什么?比较和交换(Compare And Swap)是用于实现多线程同步的原子指令。 它将内存位置的内容与给定值进行比较,只有在相同的情况下,将该内存位置的内容修改为新的给定值。 这是作为单个原子操作完成的。 原子性保证新值基于最新信息计算; 如果该值在同一时间被另一个线程更新,则写入将失败。 操作结果必须说明是否进行替换; 这可以通过一个简单的布尔响应(这个变体通常称为比较和设置),或通过返回从内存位置读取的值来完成
CAS是CPU并发原语,他用于判断内存中某个位置的值是否为预期值,如果是则更新为新的值,这个过程是原子的(就是说是线程安全的,不会被分割,是完整的)。
CAS并发原语体现在JAVA中就是sun.misc.Unsafe类中的各个方法。调用Unsafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令,这是一种完全依赖于硬件的功能,通过它实现原子操作。原语的执行必须是连续的,在执行过程中不允许中断,也就是说CAS是一条CPU原子指令,不会造成所谓的数据不一致问题。
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
//这里是底层UnSafe源码。
public final int getAndAddInt(java.lang.Object o,long l,int i){code}
public final int getAndAddInt(Object var1,long var2,int var4){
int var5;
do{
var5 = this.getIntVolatile(var1,var2);
}while(!this.compareAndSwapInt(var1,var2,var5,var5+var4));
return var5;
}
var1 AtomicInteger对象本身
var2 该对象指的引用地址(value)
var4 需要变动的数量 ++操作 就是 1
var5 是用var1 和 var2 找出的主内存中真实的值。
用该对象当前的值与var5进行比较,如果相同跟新var5 + var4 并且返回true;
如果不同,继续取值然后在进行比较。直到更新完成。
假设A、B线程同时执行getAndAddInt操作(分别跑在不同的CPU上)
1.AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value副本分别到各自的工作内存
2.线程A通过getIntVolatile(var1,var2)拿到value值3,这时线程A被挂起。
3.线程B也通过getIntVolatile(var1,var2)方法获得value值3,此时刚好线程B没有挂起并执行了compareAndSwapInt方法,比较内存值也为3,成功修改内存值为4,线程B打完收工,一切OK
4.这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手中的值数字3和主内存中的数字4不一致,说明他的线程被其他线程抢先一步修改过了,那A线程本次修改失败,只能重新来一遍了。
5.线程A重新获得value的值,因为变量value被volatile修饰,所以其他线程对它修改,线程A总是能够看到,线程A继续执行comparaAndSwapInt进行比较替换,直到成功。
这里可以看出底层调用的是UnSafe类的方法,具备原子性。
CAS的应用:
有三个操作数,内存值V 就预期值A 要修改的更新值B。当且仅当预期值A和内存值V相同时,将内存V修改为B,否则什么都不做
**1.**Unsafe类
是CAS核心类,由于Java无法直接访问底层,需要本地方法来访问,用Unsafe中的native方法就可以直接访问内存的数据,CAS操作的执行依赖于Unsafe类的方法。
注意:Unsafe类中所有的方法都是native修饰的,也就是说Unsafe类中的方法直接调用操作系统底层资源执行相应任务。
2.valueOffSet , 表示改变量value值在内存中的偏移地址。
private static final long valueOffset; valueOffset 表示改变量value值在内存中的偏移地址
3.变量value用volatile修饰,保证多线程之间的可见性。
比较并交换,CompareAndSet,下面为实例代码。
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
System.out.println(atomicInteger.compareAndSet(5, 8)+":::::"+atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(5, 6)+":::::"+atomicInteger.get());
}
}
4.2 CAS的底层原理?对UnSafe的理解
CAS的底层原理:CAS底层是调用UnSafe的cas方法,UnSafe类中都是一些native方法。上面都写了。
4.3 CAS的缺点
-
循环时间长,开销大,(比较不成功,循环do while)
-
只能保证一个共享变量的原子操作。
-
引出ABA 问题 : CAS算法实现了一个重要前提需要取出内存中某时刻的数据并在当下时刻比较替换,那么这个时间差 会导致数据变化
比如:一个线程one从内存位置V取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,这是线程one进行CAS操作并发现内存中仍然是A,然后线程one操作成功。
尽管线程one操作成功,但是不代表这个过程就是没有问题的。
5.ABA问题的解决方案,原子更新引用知道吗?
原子引用(就是因为AtomicInteger 这些 只有对Integer这些的操作,不够,此时用原子引用,对自己的User、Student这些使用,自己造如下:),时间戳原子引用。Class AtomicReference AtomicStampedReference
public class AtomicReferenceDemo { public static void main(String[] args) { User z3 = new User("z3",22); User l4 = new User("l4",25); AtomicReference<User> atomicReference= new AtomicReference<>(); atomicReference.set(z3); System.out.println(atomicReference.compareAndSet(z3, l4)+"\t"+atomicReference.get().toString()); System.out.println(atomicReference.compareAndSet(z3, l4)+"\t"+atomicReference.get().toString()); } }
结果
true User [username=l4, age=25] false User [username=l4, age=25] 修改失败 ,和之前对Integer 修改的一样,最终达到User原子目的。正确的结果。就是i++类似。
5.1 ABA问题:
public class ABADemo { static AtomicReference<Integer> atomicReference = new AtomicReference<>(100); public static void main(String[] args) { new Thread(()->{ atomicReference.compareAndSet(100, 101); atomicReference.compareAndSet(101, 100); },"t1").start(); new Thread(()->{ try { Thread.sleep(100); System.out.println(atomicReference.compareAndSet(100, 101)+"\t"+atomicReference.get().toString()); } catch (Exception e) { e.printStackTrace(); } },"t2").start(); } }
结果:
true 101 结果显示可以改变,但是t1的过程中有猫腻 先变了,有改了会来,t2睡了0.1s 不知道发生了什么。
5.2 解决方案 :
public class ABADemo02 { static AtomicStampedReference<Integer> atomicStampReference = new AtomicStampedReference<Integer>(100, 1); public static void main(String[] args) { new Thread(()->{ int stamp = atomicStampReference.getStamp(); System.out.println(Thread.currentThread().getName()+"\t第1次版本号"+stamp); try { Thread.sleep(1000);//这块的目的是为了让t4的第一次时间和t3的第一次一致,保证其点相同。 atomicStampReference.compareAndSet(100, 101, atomicStampReference.getStamp(), atomicStampReference.getStamp()+1); System.out.println(Thread.currentThread().getName()+"\t第2次版本号"+atomicStampReference.getStamp()); atomicStampReference.compareAndSet(101, 100, atomicStampReference.getStamp(), atomicStampReference.getStamp()+1); System.out.println(Thread.currentThread().getName()+"\t第3次版本号"+atomicStampReference.getStamp()); } catch (Exception e) { e.printStackTrace(); } },"t1").start(); new Thread(()->{ int stamp = atomicStampReference.getStamp(); System.out.println(Thread.currentThread().getName()+"\t第1次版本号"+stamp); try { Thread.sleep(3000); } catch (Exception e) { e.printStackTrace(); } boolean result = atomicStampReference.compareAndSet(100, 2019, stamp, stamp+1);//这块一定要写stamp,不能写getStamp方法,这个是对应该线程的。 System.out.println(Thread.currentThread().getName()+"\t修改成功否"+result+"\t当前最新版本号"+atomicStampReference.getStamp()); },"t4").start(); } }
结果:
t1 第1次版本号1 t4 第1次版本号1 t1 第2次版本号2 t1 第3次版本号3 t4 修改成功否false 当前最新版本号3
6.线程不安全类
6.1ArrayList
1.故障现象:java.util.ConcurrentModificationException 并发修改异常。
2.导致原因:并发抢夺修改导致的,参考我们的花名册签名情况,一个人正在写入,另一名同学过来抢,导致数据方面的不一致异常。
3.解决方案:
1.将ArrayList换成Vector ;(不好并,发性下降)
2.Collections.synchronizedList(new ArrayList<>());
3.juc 中CopyOnWriteArrayList 写时复制
4.优化建议(同样的错误不会犯第二次)
解决方案的代码体现(3 juc)
public class CopyOnWrite { private static List<String> list = new CopyOnWriteArrayList<>(); public static void main(String[] args) { for(int i=1;i<100;i++){ final int tempInt = i; new Thread(()->{ list.add(UUID.randomUUID().toString().substring(0,8)); System.out.println(list); },String.valueOf(i)).start(); } } }
解析:
CopyOnWrite容器即写时复制的容器,往一个容器里添加元素的时候,不直接往当前容器Object[] 中添加,而是先将当前的容器Object[]进行copy,复制出一个新的容器Object[] newElements,然后新的容器里添加元素,添加元素后,再将原容器的引用指向setArray(newElements);这样做的好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素,所以CopyOnWrite是一种读写分离的思想,读和写不同的容器。(将读写分离,一般会不发生问题即并发修改异常) public boolean add(E e) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len + 1); newElements[len] = e; setArray(newElements); return true; } finally { lock.unlock(); } }
总结 : 没遇到这方面的问题都按照这4个步骤来解决(这是软实力的提升)。
6.2 HashSet(底层是HashMap )
6.2.1 解决方案:
1.Collections.synchronizedSet(new HashSet<>());
2.CopyOnWriteArraySet<>(); 其实就是用CopyOnWriteArrayList。
6.2.2 为什么不安全?
因为底层是HashMAP S实现的,所以不安全。
6.3 HashMap (底层是hash表,数组加链表)
6.3.1 原因:
是Hash碰撞与扩容导致的。
6.3.2 解决方案:
CoucurrentHashMap (这块没看到??需要学习一遍。)
6.3.3 引申问题 : 为什么HashSet 添加元素是一个,而HashMap添加是两个,还说HashSet底层是HashMap。
因为hashSet添加时,调用的是map的put方法,添加键,值是一个固定值PRESENT。
7. 锁
7.1 公平和非公平锁
7.1.1 是什么?公平锁: 是指多线程按照申请的顺序来获取锁,类是排队打饭,先到先得。
非公共锁:指多线程并不是按照申请的顺序来获取锁 ,有可能后申请的线程比先申请的线程优先获取锁,在高并发的情况下,有可能会造成优先级反转或者饥饿现象。
7.1.2 代码体现:
当我们 Lock lock = new ReentrantLock(); 在括号中加入true就代表公平锁,不加默认是false
是非公平锁。
7.1.3 二者的区别
公平锁,就是很公平,在并发环境下,每个线程在获得锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己。
非公平锁:比较粗鲁,上来就尝试占有锁,如果尝试失败,在采用类似公平锁的那种方式。
注:Synchronized是非公平锁。 非公平锁的有点在于吞吐量比公平锁大。
7.2 可重入锁(递归锁)
7.2.1 是什么?
指的是同一线程外层函数获得锁之后,每层递归函数仍然能获取该锁的代码。在同一个线程在外层方法获得锁的时候,在进入内层方法自动获取锁。 作用 :防止死锁。
也就是说:线程可以进入任何一个它已经拥有的锁所同步着的代码块。
7.2.2 代码实现?
基于synchronized
public class ReenTerLockDemo { public static void main(String[] args) { Phone phone = new Phone(); new Thread(()->{ phone.sendSMS(); },"t1").start(); new Thread(()->{ phone.sendSMS(); },"t2").start(); } } class Phone { public synchronized void sendSMS(){ System.out.println(Thread.currentThread().getName()+"\t invoked sendSMS"); sendMails(); } public synchronized void sendMails(){ System.out.println(Thread.currentThread().getName()+"\t invoked sendMails"); } }
结果:
t1 invoked sendSMS t1 invoked sendMails t2 invoked sendSMS t2 invoked sendMails
基于lock的
public class Phone implements Runnable{ Lock lock = new ReentrantLock(); @Override public void run() { get(); } public void get(){ lock.lock(); try{ System.out.println(Thread.currentThread().getName()+"\t invoke get()"); set(); }finally{ lock.unlock(); } } public void set(){ lock.lock(); try{ System.out.println(Thread.currentThread().getName()+"\t invoke set()"); }finally{ lock.unlock(); } } } class ReenTerLockDemo { public static void main(String[] args) { Phone phone = new Phone(); Thread thread = new Thread(phone,"t1"); thread.start(); } }
结果:
t1 invoke get() t1 invoke set()
总结:对于lock 必须成对出现加锁解锁。
7.3 自旋锁
7.3.1 是甚么? 指尝获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少上下文的切换的消耗,缺点是循环会消耗CPU。
//就是一个线程获得锁,另一个线程自旋而不是等待,好处是不会阻塞,坏处是消耗CPU。
7.3.2 代码体现
/* * 实现一个自旋锁, * * 通过CAS操作完成自选锁,A线程新进来调用mylock方法自己持有锁5秒,B随后进来发现当前线程有锁, * 不是null,所以只能通过自旋等待,直到A释放后B随后抢到。 * * */ import java.util.concurrent.atomic.AtomicReference; public class SpinLockDemo { private static AtomicReference<Thread> arf = new AtomicReference<>(); public void myLock(){ Thread thread = Thread.currentThread(); while(!arf.compareAndSet(null, thread)){ } System.out.println(thread.getName()+"\t come in"); } public void myUnLock(){ Thread thread = Thread.currentThread(); arf.compareAndSet(thread, null); System.out.println(Thread.currentThread().getName()+"\t invoked myUnLock()"); } public static void main(String[] args) { SpinLockDemo lock = new SpinLockDemo(); new Thread(()->{ lock.myLock(); try{ Thread.sleep(5000); lock.myUnLock(); }catch(Exception e){ e.printStackTrace(); } },"A").start(); try { Thread.sleep(1000); } catch (InterruptedException e1) { e1.printStackTrace(); } new Thread(()->{ lock.myLock(); lock.myUnLock(); },"B").start(); } }
7.4 独占锁 读写锁:ReentrantReadWriteLock
7.4.1 是什么?
指该锁一次只能被一个线程持有,对ReentrantLock和Synchronized而言都是独占锁。
共享锁: 该锁可以被多个线程持有
对ReentrantLock : 其读锁是共享锁,写锁是独占锁;
读锁的共享锁可保证并发读取是非常高效的,读写,写读,写写的过程都是互斥的。
7.4.2 代码
public class MyCache { private volatile Map<String,Object> map = new HashMap<>(); //private Lock lock = new ReentrantLock(); private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); public void put(String key,Object value){ lock.writeLock().lock(); System.out.println(Thread.currentThread().getName() + "\t 正在写入" + key); try { Thread.sleep(5000); map.put(key, value); System.out.println(Thread.currentThread().getName()+"\t 写入完成 :"); } catch (InterruptedException e) { e.printStackTrace(); }finally{ lock.writeLock().unlock(); } } public void get(String key){ lock.readLock().lock(); System.out.println(Thread.currentThread().getName()+"\t 正在读取:"); try { Thread.sleep(5000); Object result = map.get(key); System.out.println(Thread.currentThread().getName()+"\t 读取完成:"+result); } catch (InterruptedException e) { e.printStackTrace(); }finally{ lock.readLock().unlock(); } } public static void main(String[] args) { MyCache myCache = new MyCache(); for(int i=1;i<=5;i++){ final int tempInt = i; new Thread(()->{ myCache.put(tempInt+"", tempInt+""); },String.valueOf(i)).start(); } for(int i=1;i<=5;i++){ final int tempInt = i; new Thread(()->{ myCache.get(""+tempInt); },String.valueOf(i+tempInt)).start(); } } }
总结 : 一般的发生ConCurrentModificationException ,是因为读写同时进行,或者在读取的时候数据同时也在改变,所以当我们写的时候不要让其他操作进来,并发的都时候,保证元素不变。比如CopyOnWrite中的操作写时:先复制一份,修改成功就用这份替换原来的,修改不成功丢掉,读只读原来的数据
8. CountDownLatch 、CyclicBarrierDemo 、SemaphoreDemo
8.1 CountDownLatch
8.1.1 先上代码
public class CountDownLatchDemo {
public static void main(String[] args) {
CountDownLatch cdl = new CountDownLatch(6);
for(int i=0;i<6;i++){
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t 上完自习,离开教室");
cdl.countDown();
},String.valueOf(i)).start();
}
try {
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"\t ********班长最后关门走人");
}
}
结果:
3 上完自习,离开教室
2 上完自习,离开教室
1 上完自习,离开教室
5 上完自习,离开教室
4 上完自习,离开教室
0 上完自习,离开教室
main ********班长最后关门走人
情景: 就是我们需要把每次走的人名字记录下来,比如1线程代表谁,而线程2代表谁,此时用到枚举,相当于一个数据库,耦合性还很低,以后就这么写。代码如下:
public enum CountryEnum {
ONE(1,"齐"),TWO(2,"楚"),THREE(3,"燕"),FOUR(4,"韩"),FIVE(5,"赵"),SIX(6,"魏");
private Integer retCode;
private String retMessage;
CountryEnum(Integer retCode,String retMessage){
this.retCode = retCode;
this.retMessage = retMessage;
}
public Integer getRetCode() {
return retCode;
}
public void setRetCode(Integer retCode) {
this.retCode = retCode;
}
public String getRetMessage() {
return retMessage;
}
public void setRetMessage(String retMessage) {
this.retMessage = retMessage;
}
public static CountryEnum foreach_CountryEnum(int index){
CountryEnum[] myArray = CountryEnum.values();
for (CountryEnum element : myArray) {
if(index==element.retCode){
return element;
}
}
return null;
}
}
public class CountDownLatchDemo {
public static void main(String[] args) {
CountDownLatch cdl = new CountDownLatch(6);
for(int i=1;i<=6;i++){
final int tmp = i;
new Thread(()->{
System.out.println(tmp+" "+Thread.currentThread().getName()+"\t被灭了");
cdl.countDown();
},CountryEnum.foreach_CountryEnum(i).getRetMessage()).start();
}
try {
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"\t秦国一统天下了");
}
}
8.1.2 是甚么有什么作用?
让一些线程阻塞,直到另一些线程完成一系列操作后才被唤醒。
CountDownLatch 主要有两个方法,当一个或多个线程调用await方法时,调用线程会被阻塞当其他线程调用countDown方法会将计数器减一,当计数器值为零时,因调用await方法被阻塞的线程会被唤醒,继续执行。
8.2 CyclicBarrier
8.2.1 是甚么,什么用,解释
集齐七颗龙珠就能召唤神龙。
CyclicBarrier cb = new CyclicBarrier(int parties,Runnable barrierAction)
其中,后面的是被阻塞的线程。
8.2.2 代码如下:
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier cb = new CyclicBarrier(7,()->{
System.out.println("*******召唤神龙");
});
for(int i=1;i<=7;i++){
final int tempInt = i;
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t 收集到第:"+tempInt+"龙珠");
try {
cb.await();
} catch (Exception e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
结果:
1 收集到第:1龙珠
2 收集到第:2龙珠
3 收集到第:3龙珠
6 收集到第:6龙珠
5 收集到第:5龙珠
7 收集到第:7龙珠
4 收集到第:4龙珠
*******召唤神龙
8.3 Semaphore
8.3.1 是什么
信号灯,一是用于多个线程共享资源的互斥使用,另一个是用于并发线程的控制。
8.3.2 争车位
代码如下:
public class SemaphoreDemo {
public static void main(String[] args) {
Semaphore sp = new Semaphore(3);//模拟6个停车位
for(int i=1;i<=6;i++){
new Thread(()->{
try {
sp.acquire();//抢夺资源
System.out.println(Thread.currentThread().getName()+"\t抢到车位");
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName()+"\t停车3秒后放弃车位");
} catch (Exception e){
e.printStackTrace();
}finally{
sp.release();//放弃资源
}
},String.valueOf(i)).start();
}
}
}
9. 阻塞队列
9.1 队列+阻塞队列
阻塞队列,首先是一个队列,而一个阻塞队列在数据结构中作用是:线程1网阻塞队列中添加元素,线程而从阻塞队列中移除元素,当阻塞队列是空时,从队列中获取元素将会被阻塞直到添加进入元素,当阻塞队列是满时,往队列里添加元素将会被阻塞,直到其他线程移除元素。
9.2 为什么用,有什么好处?
好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,这一切BlockingQueue一手包办了。
9.3 BlockingQueue的核心用法
方法类型 | 抛出异常 | 特殊值true/false | 阻塞 | 超时 |
---|---|---|---|---|
插入 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除 | remove() | poll() | take() | poll(time,unit) |
检查 | element() | peek() | 不可用 | 不可用 |
简单操作如下:
public class BlockingQueueDemo {
public static void main(String[] args) {
BlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(3);
blockingQueue.offer(1);
blockingQueue.offer(2);
//blockingQueue.offer(3);
//System.out.println(blockingQueue.offer(1));
try {
System.out.println(blockingQueue.offer(3, 2, TimeUnit.SECONDS));
} catch (InterruptedException e) {
e.printStackTrace();
}
//System.out.println(blockingQueue.offer(1));
}
}
9.4 实现类 ArrayBlockingQueue 、LinkedBlockingQueue
9.4.1 SynchronousQueue 没有容量,与其他的BlockingQueue不同, SynchronousQueue是一个不存储元素的BlockingQueue,每一个put必须要等待一个take,否则不能继续添加元素,反之亦然。
代码如下:
public class SynchronousQueueDemo {
public static void main(String[] args) {
BlockingQueue<Integer> blockingQueue = new SynchronousQueue<>();
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + "\t put 1");
blockingQueue.put(1);
System.out.println(Thread.currentThread().getName() + "\t put 2");
blockingQueue.put(2);
System.out.println(Thread.currentThread().getName() + "\t put 3");
blockingQueue.put(3);
} catch (Exception e) {
e.printStackTrace();
}
}, "AAA").start();
new Thread(() -> {
try {
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName() + "\t" + blockingQueue.take());
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName() + "\t" + blockingQueue.take());
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName() + "\t" + blockingQueue.take());
} catch (Exception e2) {
e2.printStackTrace();
}
}, "BBB").start();
}
}
结果:
AAA put 1
BBB 1
AAA put 2
BBB 2
AAA put 3
BBB 3
9.5 用在哪里
1.生产者消费者模式 2.线程池 3.消息中间件
9.6 生产者消费者模式
9.6.1 传统版
class ShareData {
private int number;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void increment()throws Exception{
lock.lock();
while (number!=0) {
condition.await();
}
number++;
System.out.println(Thread.currentThread().getName()+"\t"+number);
condition.signalAll();
lock.unlock();
}
public void decrement() throws Exception{
lock.lock();
while(number!=1){
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName()+"\t"+number);
condition.signalAll();
lock.unlock();
}
}
public class ProducterAndCoutumer {
public static void main(String[] args) {
ShareData sd = new ShareData();
new Thread(()->{
try {
for(int i=1;i<=5;i++){
sd.increment();
}
} catch (Exception e) {
e.printStackTrace();
}
},"AAA").start();
new Thread(()->{
try {
for(int i=1;i<=5;i++){
sd.decrement();
}
} catch (Exception e) {
e.printStackTrace();
}
},"BBB").start();
}
}
9.6.2 阻塞队列版
class MyResource {
private volatile boolean FLAG = true;
private AtomicInteger atomicInteger = new AtomicInteger();
private BlockingQueue<String> blockingQueue = null;
public MyResource(BlockingQueue<String> blockingQueue) {
this.blockingQueue = blockingQueue;
System.out.println(blockingQueue.getClass().getName());
}
public void myProd()throws Exception{
String data = null;
boolean resValue;
while(FLAG){
data = atomicInteger.incrementAndGet() + "";
resValue = blockingQueue.offer(data, 2, TimeUnit.SECONDS);
if (resValue) {
System.out.println(Thread.currentThread().getName() + "\t 插入队列" + data + "成功");
} else {
System.out.println(Thread.currentThread().getName() + "\t 插入队列" + data + "失败");
}
TimeUnit.SECONDS.sleep(1);
}
System.out.println(Thread.currentThread().getName()+"\t大老板叫停了,表示FLAG=false,生产动作结束");
}
public void myCons() throws Exception{
String res = null;
while(FLAG){
res = blockingQueue.poll(2,TimeUnit.SECONDS);
if(null==res||res.equalsIgnoreCase("")){
FLAG = false;
System.out.println(Thread.currentThread().getName()+"\t 超过2秒钟没有收到蛋糕,消费退出" );
return;
}
System.out.println(Thread.currentThread().getName() + "\t 消费队列" + res + "成功");
}
}
public void stop(){
this.FLAG = false;
}
/*
* volatile/CAS/atomicInteger/BlockingQueue/线程交互
* */
public class ProdCoonsumer_BlockingQueueDemo {
public static void main(String[] args) {
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(10);
MyResource mr = new MyResource(blockingQueue);
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t 生产线程启动");
try {
mr.myProd();
} catch (Exception e) {
e.printStackTrace();
}
},"Prod").start();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t 消费线程启动");
try {
mr.myCons();
} catch (Exception e) {
e.printStackTrace();
};
},"Consumer").start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("5秒时间到了,大老板叫停,活动结束");
mr.stop();
}
}
结果:
java.util.concurrent.ArrayBlockingQueue
Prod 生产线程启动
Consumer 消费线程启动
Prod 插入队列1成功
Consumer 消费队列1成功
Prod 插入队列2成功
Consumer 消费队列2成功
Prod 插入队列3成功
Consumer 消费队列3成功
Prod 插入队列4成功
Consumer 消费队列4成功
Prod 插入队列5成功
Consumer 消费队列5成功
5秒时间到了,大老板叫停,活动结束
Prod 大老板叫停了,表示FLAG=false,生产动作结束
Consumer 超过2秒钟没有收到蛋糕,消费退出
总结: volatile/CAS/atomicInteger/BlockingQueue/线程交互。
10. 补充部分
10.1 synchronized与Lock有什么区别?
1. 原始构成:
synchronized关键字属于JVM层面,底层是monitorenter,monitorexit 。 Lock是具体的类。
2.使用方法:
synchronized不需要手动释放锁,当synchronized代码执行后系统会自动释放。ReentrantLock 需要自己去手动释放锁,如果不释放可能会导致死锁现象。
3.等待可中断:
synchronized 不可中断,除非抛出异常或者正常运行完成。
ReentrantLock可中断,1 设置超时方法 tryLock(long timeout,TimeUnit unit)
2 lockInterruptibly() 放代码快中,调用interrupt 方法中断。
4 加锁是否公平
synchronized 非公平锁
ReentrantLock 两者都可以,默认非公平。
5.预绑定多个条件Condition
synchronized 没有
ReentrantLock 用来实现分组唤醒需要唤醒的线程们,可以精确唤醒,而不是想synchronized那要么唤醒一个线程要么唤醒全部线程。
例题:练习ReentrantLock分组唤醒。
/*
* 题目:多线程之间按顺序调用,实现A->B->C三个线程启动,要求如下:
* AA打印五次,BB打印10次,CC打印15次
* 紧接着,AA打印五次,BB打印10次,CC打印15次
* ...
* 来10轮
* */
public class SyncAndReentrantLockDemo {
public static void main(String[] args) {
ShareResource sr = new ShareResource();
for(int i=1;i<=10;i++){
new Thread(()->{
sr.print5();
},String.valueOf(i)).start();
}
}
}
public class ShareResource{
private Lock lock = new ReentrantLock();
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
private Condition c3 = lock.newCondition();
//private volatile boolean flag = false;
private int number = 1;
public void print5(){
lock.lock();
try {
while(number!=1){
c1.await();
}
number = 2;
for(int i=1;i<=5;i++){
System.out.println("A****"+i);
}
c2.signal();
print10();
} catch (Exception e) {
e.printStackTrace();
}finally{
lock.unlock();
}
}
public void print10(){
lock.lock();
try {
while(number!=2){
c2.await();
}
for (int i = 1; i <= 10; i++) {
System.out.println("B***"+i);
}
number = 3;
c3.signal();
print15();
} catch (Exception e) {
e.printStackTrace();
}finally{
lock.unlock();
}
}
public void print15(){
lock.lock();
try {
while(number!=3){
c2.await();
}
for (int i = 1; i <=15; i++) {
System.out.println("C***"+i);
}
number = 1;
c1.signal();
} catch (Exception e) {
e.printStackTrace();
}finally{
lock.unlock();
}
}
}
11.Callable接口 第三种创建线程方法 ,总共有4种
上代码:
public class MyThread implements Runnable{
@Override
public void run() {
}
}
class MyThread2 implements Callable<Integer>{
@Override
public Integer call() throws Exception {
return null;
}
}
对比:一个没返回值,没抛出异常,一个又返回值抛出异常。一个run()方法,一个call方法。
具体用法:
public class MyThread implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println("进入call方法");
return 1024;
}
}
class CallableDemo {
public static void main(String[] args) throws Exception{
FutureTask<Integer> futureTask = new FutureTask<>(new MyThread());
Thread thread = new Thread(futureTask);
thread.start();
System.out.println(futureTask.get());
}
}
结果:
进入call方法
1024
思路:这里用到了适配器设计模式 如下:
Thread类中的构造方法没有Callable,有Runnable接口,所以我们就要把Runnable与Callable联系起来,找Runnable的实现类,看看实现类中有没有Callable的接口参数,找到了FutureTask。里面有一个get方法,是获取call方法的返回值。
补充问题:为什么有了Runnable 还需要Callable?
并发异步导致Callable出现,但我们处理几项任务时,需要一个一个执行这些方法,当有一个耗时很长,我们就需要给他创建一个线程去处理,然后其他处理完的结果与这个结果加在一起就可以了。
public class CallableDemo {
public static void main(String[] args) throws Exception{
FutureTask<Integer> futureTask = new FutureTask<>(new MyThread());
Thread thread1 = new Thread(futureTask);
thread1.start();
int result01 = 100;
while(!futureTask.isDone()){
//就是一般future做的都是耗时的,我们放在这里的循环是当我们做完它后再取结果。
}
int result02 = futureTask.get();
System.out.println("*********result:"+(result01+result02));
}
}
结果:
进入call方法
*********result:1124
再补充 : 放我们在创建一个Thread,但是用的还是之前的futureTask,那么结果不会出现两次,如下
public class CallableDemo {
public static void main(String[] args) throws Exception{
FutureTask<Integer> futureTask = new FutureTask<>(new MyThread());
Thread thread1 = new Thread(futureTask);
thread1.start();
new Thread(futureTask).start();
int result01 = 100;
/*while(!futureTask.isDone()){
}*/
int result02 = futureTask.get();
System.out.println("*********result:"+(result01+result02));
}
}
结果:
进入call方法
*********result:1124
12 线程池
12.1为什么要用线程池,优势
线程池工作主要是控制运行的线程的数量,处理过程中将任务放入队列,在线程创建后启动这些任务,如果线程数量超过最大数量,超出部分的线程排队等候,等待其他线程执行完毕,再从队列中取出任务来执行。
主要特点优势: 线程复用、控制最大并发数、管理线程
优势是:降低资源消耗,提高响应速度,提高线程的可管理性。
12.2 线程池如何使用?
12.2.1 架构说明
JAVA中线程池通过Executor框架实现,该框架中用到了Executor、Executors,ExecutorService,ThreadPoolExecutor这几个类。
有三个需要重点的 (2个java8新出的,了解,这里不写了)
Executors.newFixedThreadPool(nThreads); 执行长期的任务,性能好得多。
Executors.newSingleThreadExecutor(); 一个任务一个任务的执行
Executors.newCachedThreadPool(); 适用:执行多短期异步的小程序或者负责负载较轻的服务。
ExecutorService pool = Executors.newFixedThreadPool(nThreads);
ExecutorService pool = Executors.newCachedThreadPool();
ExecutorService pool = Executors.newSingleThreadExecutor();
pool.execute(Runnable command); //执行线程。
12.2.2 以上三个线程池的底层原理
1.newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
2.newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
3.newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
12.2.3 ThreadPoolExecutor(重中之重)
代码:
public class ThreadPoolExcutorsDemo {
public static void main(String[] args) {
ThreadPoolExecutor pool = new ThreadPoolExecutor(2, 5, 5, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>(3));
pool.execute(()->{
System.out.println(Thread.currentThread().getName());
System.out.println("LAILE");
});
}
}
结果:
pool-1-thread-1
LAILE
底层是什么?
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;
}
七个参数:
-
corePoolSize:线程池中核心常驻线程数。
-
maxmumPoolSize:线程池中能够同时容纳的同时执行的最大线程数,此值必须大于等于1。
-
keepAliveTime: 多余的空闲线程的存活时间。当前空闲时间达到keepAliveTime值时,多余空闲线程会被销毁直到剩下coorPoolSize个线程为止。
-
unit:keepAlive的单位。
-
workQueue:任务队列,被提交但尚未执行的任务。
-
threadFactory:表示生产线程池中的线程厂,相当于工牌。
-
handler:拒绝策略,表示当队列满了并且工作线程大于等于线程吃的最大线程数,就会启动拒绝策略,不允许在连接。
重要解释:
当创建了线程池后,等待提交过来的任务请求。
当调用execute方法后,线程池会做出如下判断 :
如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
如果正在运行的线程数大于等于corePoolSize,将这个任务放入队列;
如果这个时候队列满了且正在运行的线程数小于maxmumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
如果这个时候队列满了且正在运行的线程数大于等于maxmumPoolSize,那么线程池会启动饱和拒绝策略来执行。
当一个线程完成任务时,它会从队列中取下一个任务来执行;
当一个线程无事做而超过一定时间(keepAliveTime)时,线程池会判断:
如果当前运行的线程池数大于corePoolSize,那么这个线程会被停掉。
所以线程池的所有任务完成后最终会收缩到corePoolSize的大小。
13 死锁
13.1 是什么
死锁指的是两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,无外力干涉它们将无法推进下去,线程A持有A试图获得B,线程B持有B试图获得A。
死锁产生的4个必要条件
1、互斥:某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进程就不能再访问,直到该进程访问结束。
2、占有且等待:一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其他进程释放该资源。
3、不可抢占:别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。 4、循环等待:存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源。
class TestThread implements Runnable{ private String lockA; private String lockB; public TestThread(String lockA, String lockB) { super(); this.lockA = lockA; this.lockB = lockB; } @Override public void run() { synchronized(lockA){ System.out.println(Thread.currentThread().getName()+"\t 自己持有"+lockA+",尝试获得"+lockB); try { Thread.sleep(1000); } catch (InterruptedException e){ e.printStackTrace(); } synchronized (lockB) { System.out.println(Thread.currentThread().getName()+"\t 自己持有"+lockB+",尝试获得"+lockA); } } } } public class DeadLockDemo { public static void main(String[] args) { String lockA = "lockA"; String lockB = "lockB"; Thread t1 = new Thread(new TestThread(lockA, lockB),"AAA"); Thread t2 = new Thread(new TestThread(lockB, lockA),"BBB"); t1.start(); t2.start(); } }
13.2 解决方案
找出问题:
jps -l 找出问题的进程编号
jstack 端口 详细信息
再到源代码位置,目录上打cmd
解决 停止程序