相关知识详解"/>
JUC相关知识详解
JUC
1、JMM (java 内存模型)
1.1、什么是JMM?
JMM 本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式
1.2、JMM关于同步的规定:
- 线程解锁前,必须把共享变量的值刷新回主内存
- 线程加锁前,必须读取主内存的最新值到自己的工作内存
- 加锁解锁是同一把锁
1.3、JMM原理的理解:
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方成为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。
1.4、JMM产生的并发问题:
- 可见性:就是某个线程对主内存内容的更改,应该立刻通知到其它线程。
- 原子性:指一个操作是不可分割的,不能执行到一半,就不执行了。
- 有序性:就是指令是有序的,不会被重排。
2、volatile关键字
2.1、什么是volatile?
volatile 关键字是Java提供的一种轻量级同步机制。它能够保证可见性和有序性,但是不能保证原子性。
保证可见性
class MyData{// int number=0;volatile int number=0;AtomicInteger atomicInteger=new AtomicInteger();public void setTo60(){this.number=60;}//此时number前面已经加了volatile,但是不保证原子性public void addPlusPlus(){number++;}public void addAtomic(){atomicInteger.getAndIncrement();}
}//volatile可以保证可见性,及时通知其它线程主物理内存的值已被修改
private static void volatileVisibilityDemo() {System.out.println("可见性测试");MyData myData=new MyData();//资源类//启动一个线程操作共享数据new Thread(()->{System.out.println(Thread.currentThread().getName()+"\t come in");try {TimeUnit.SECONDS.sleep(3);myData.setTo60();System.out.println(Thread.currentThread().getName()+"\t update number value: "+myData.number);}catch (InterruptedException e){e.printStackTrace();}},"AAA").start();while (myData.number==0){//main线程持有共享数据的拷贝,一直为0}System.out.println(Thread.currentThread().getName()+"\t mission is over. main get number value: "+myData.number);
}
不能保证原子性代码:
比如一条number++的操作,会形成3条指令
getfield //读
iconst_1 //++常量1
iadd //加操作
putfield //写操作
假设有3个线程,分别执行number++,都先从主内存中拿到最开始的值,number=0,然后三个线程分别进行操作。假设线程0执行完毕,number=1,也立刻通知到了其它线程,但是此时线程1、2已经拿到了number=0,所以结果就是写覆盖,线程1、2将number变成1。
解决的方式就是:
- 对
addPlusPlus()
方法加锁。 - 使用
java.util.concurrent.AtomicInteger
类。
2.2、AtomicInteger
AtomicInteger 是什么,为什么能解决原子性问题? 原子整型类,依赖于CAS ,具体见下面部分。
2.3、指令重排序?
指令重排序出现的原因?
计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做一些优化,对没有产生数据依赖性的指令进行重新排序。
指令重排序会造成的后果?
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
案例1:
int a,b,x,y = 0两个线程同时进行操作线程1 线程2
x = a; y = b;
b = 1; a = 2;线程1进行的操作: 将a赋值于x,再讲1赋值于b,
线程2进行的操作: 将b赋值于y,再讲2赋值于a,因为线程1和线程2中的操作,都没有数据依赖的关系,有发生指令重排序的结果可能是,
结果可能是:x = 0; y = 0;b=1;a=2如果发生了指令重排序线程1 线程2
b = 1; a = 2;
x = a; y = b;线程1进行的操作: 将1值于b,讲a值于x
线程2进行的操作: 将2于a,讲b值于y结果可能是:x=2,y=1,a=2,b=1
案例2:
public class ReSortSeqDemo{int a = 0;boolean flag = false;public void method01(){a = 1;//语句1flag = true;//语句2}public void method02(){if(flag){a = a + 5; //语句3}System.out.println("retValue: " + a);//可能是6或1或5或0}}
这也就说明在多线程环境下,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的。
volatile是如何实现禁止指令重排序?
主要是内存屏障,是一组CPU指令,用于实现对内存操作的顺序限制,
内存屏障有两个作用:
-
阻止屏障两侧的指令重排序;
-
强制把写缓冲区/高速缓存(也就是每个线程的工作区)中的脏数据等写回主内存,让缓存中相应的数据失效。
如果你的字段是volatile,Java内存模型将在写操作前插入一个StoreStore屏障指令,写操作后插入一个StoreLoad屏障指令,在读操作前插入一个LoadLoad屏障指令,在读操作前后插入LoadStore屏障.
-
在每一个volatile写操作前面插入一个StoreStore屏障。这确保了在进行volatile写之前前面的所有普通的写操作都已经刷新到了内存。
-
在每一个volatile写操作后面插入一个StoreLoad屏障。这样可以避免volatile写操作与后面可能存在的volatile读写操作发生重排序。
-
在每一个volatile读操作后面插入一个LoadLoad屏障。这样可以避免volatile读操作和后面普通的读操作进行重排序。
-
在每一个volatile读操作后面插入一个LoadStore屏障。这样可以避免volatile读操作和后面普通的写操作进行重排序。
volatile 写操作:
volatile 读操作:
2.4、使用volatile 做过哪些?
单例模式 DCL:双端检索机制
public class SingletonDemo{private SingletonDemo(){}private volatile static SingletonDemo instance = null;public static SingletonDemo getInstance() {if(instance == null) {synchronized(SingletonDemo.class){if(instance == null){instance = new SingletonDemo(); }}}return instance;}
}
使用volatile 关键字的原因?
private volatile static SingletonDemo instance = null;
首先创建一个对象分为以下3步完成
- memory = allocate(); //1.分配对象内存空间
- instance(memory); //2.初始化对象
- instance = memory; //3.设置instance指向刚分配的内存地址,此时instance != null
在多线程的环境下,步骤2和步骤3不存在数据依赖关系,编辑器或者处理器会产生指令重排序的可能,也就是说当步骤2和步骤3发生重排序的时候
会导致这个对象刚分配完地址,对象还未初始化完成就返回,导致返回的数据为null,产生线程安全的问题。
3、CAS
3.1、什么是CAS?
CAS是指Compare And Swap,比较并交换,是一种很重要的同步思想,它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值
3.2、CAS的底层原理?
UnSafe类+CAS思想[自旋锁]
3.3、UnSafe类
UnSafe类是CAS的核心类 由于Java 方法无法直接访问底层 ,需要通过本地(native)方法来访问,UnSafe相当于一个后门,基于该类可以直接操作特定的内存数据.UnSafe类在于sun.misc包中,其内部方法操作可以向C的指针一样直接操作内存,因为Java中CAS操作依赖于unSafe类的方法.另外UnSafe类中所有的方法都是native修饰的,也就是说UnSafe类中的方法都是直接调用操作底层资源执行响应的任务
/*** Atomically increments by one the current value.** @return the previous value*/public final int getAndIncrement() {// this指的是当前对象,valueOffset指的是当前对象的内存偏移量,可以根据内存偏移地址获取数据return unsafe.getAndAddInt(this, valueOffset, 1);}// volatile保证了数据的可见性private volatile int value;
Unsafe的getAndAddInt()是一个本地方法。
UnSafe.getAndAddInt()源码解释:
- var1 AtomicInteger对象本身。
- var2 该对象值得引用地址。
- var4 需要变动的数量。
- var5是用过var1,var2找出的主内存中真实的值。
- 用该对象当前的值与var5比较:
- 如果相同,更新var5+var4并且返回true,
- 如果不同,因为value被volatile修饰,所以线程之间的修改值的操作是可见的,会取出最新值,然后再比较,直到更新完成。
假设线程A和线程B两个线程同时执行getAndAddInt操作(分别跑在不同CPU上) :
- Atomiclnteger里面的value原始值为3,即主内存中Atomiclnteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存。
- 线程A通过getIntVolatile(var1, var2)拿到value值3,这时线程A被挂起。
- 线程B也通过getintVolatile(var1, var2)方法获取到value值3,此时刚好线程B没有被挂起并执行compareAndSwapInt方法比较内存值也为3,成功修改内存值为4,线程B结束。
- 这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值数字3和主内存的值数字4不一致,说明该值己经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新来一遍了。
- 线程A重新获取value值,因为变量value被volatile修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwaplnt进行比较替换,直到成功。
3.5、面试题 讲一讲AtomicInteger,为什么要用CAS,而不是synchronize?
AtomicInteger 主要是基于CAS思想,CAS核心类是Unsafe类,Unsafe类中的方法都是native修饰的,通过该类可以直接操作特定内存的数据,CompareAndSwap,它是一条CPU并发原语。在Unsafe类中,调用该类的CompareAndSwap方法,JVM会帮我们实现出该方法汇编指令,这是一种完全依赖于硬件的功能,通过它实现了原子操作。synchronize使用的是锁机制,并发性比较差,而Unsafe使用并发原语保证了原子性,这个是依赖于硬件的,是原子性的。并发性比较高
3.6、CAS的缺点
// ursafe.getAndAddInt
public final int getAndAddInt(Object var1, long var2, int var4){int var5;do {var5 = this.getIntVolatile(var1, var2);// 如果一直比较并交换失败 就会继续尝试,一直长时间不成功,会给cpu造成很大的开销,而且 只能操作一个对象,锁可以操作多个对象}while(!thispareAndSwapInt(varl, var2, var5,var5 + var4));return var5;
}
1. 循环时间长开销很大
2. 只能保证一个共享变量的原子性
3. ABA问题的产生
3.7、ABA问题
ABA问题是什么?
所谓ABA问题,就是比较并交换的循环,存在一个时间差,由于这个时间差导致数据变化产生的问题。
比如t1,t2两个线程都需要对某一个共享变量value做操作,初始值为A,但是由于t1的执行的时间长,t2将value的值更改为B后,再次更改为A,这是t1才到达,看到值为A,于是就操作更新成功了,但是不知道这个A中间发生了变化,这就是ABA问题。
什么是原子引用?
AutomicReference 的作用是封装自定义的对象,让对这个对象做的操作都是原子性的。
解决ABA问题的方法?
使用 AtomicStampedReference 带有版本号机制的原子引用,他在进行CAS操作的时候,不仅要比较值,还要比较版本号,如果版本号不一致,就更新失败,反之亦然。
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;public class ABADemo {/*** 普通的原子引用包装类*/static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);// 传递两个值,一个是初始值,一个是初始版本号static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);public static void main(String[] args) {System.out.println("============以下是ABA问题的产生==========");new Thread(() -> {// 把100 改成 101 然后在改成100,也就是ABAatomicReferencepareAndSet(100, 101);atomicReferencepareAndSet(101, 100);}, "t1").start();new Thread(() -> {try {// 睡眠一秒,保证t1线程,完成了ABA操作TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}// 把100 改成 101 然后在改成100,也就是ABASystem.out.println(atomicReferencepareAndSet(100, 2019) + "\t" + atomicReference.get());}, "t2").start();/try {TimeUnit.SECONDS.sleep(2);} catch (Exception e) {e.printStackTrace();}System.out.println("============以下是ABA问题的解决==========");new Thread(() -> {// 获取版本号int stamp = atomicStampedReference.getStamp();System.out.println(Thread.currentThread().getName() + "\t 第一次版本号" + stamp);// 暂停t3一秒钟try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}// 传入4个值,期望值,更新值,期望版本号,更新版本号atomicStampedReferencepareAndSet(100, 101, atomicStampedReference.getStamp(),atomicStampedReference.getStamp() + 1);System.out.println(Thread.currentThread().getName() + "\t 第二次版本号" + atomicStampedReference.getStamp());atomicStampedReferencepareAndSet(101, 100, atomicStampedReference.getStamp(),atomicStampedReference.getStamp() + 1);System.out.println(Thread.currentThread().getName() + "\t 第三次版本号" + atomicStampedReference.getStamp());}, "t3").start();new Thread(() -> {// 获取版本号int stamp = atomicStampedReference.getStamp();System.out.println(Thread.currentThread().getName() + "\t 第一次版本号" + stamp);// 暂停t4 3秒钟,保证t3线程也进行一次ABA问题try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}boolean result = atomicStampedReferencepareAndSet(100, 2019, stamp, stamp + 1);System.out.println(Thread.currentThread().getName() + "\t 修改成功否:" + result + "\t 当前最新实际版本号:"+ atomicStampedReference.getStamp());System.out.println(Thread.currentThread().getName() + "\t 当前实际最新值" + atomicStampedReference.getReference());}, "t4").start();}
}
执行结果
============以下是ABA问题的产生==========
true 2019
============以下是ABA问题的解决==========
t3 第一次版本号1
t4 第一次版本号1
t3 第二次版本号2
t3 第三次版本号3
t4 修改成功否:false 当前最新实际版本号:3
t4 当前实际最新值100
4、集合类的线程不安全
4.1、ArrayList
ArrayList不是线程安全类,在多线程同时写的情况下,会抛出
java.util.ConcurrentModificationException`异常。
private static void listNotSafe() {List<String> list=new ArrayList<>();for (int i = 1; i <= 30; i++) {new Thread(() -> {list.add(UUID.randomUUID().toString().substring(0, 8));System.out.println(Thread.currentThread().getName() + "\t" + list);}, String.valueOf(i)).start();}
}
解决方法:
- 使用
Vector
(ArrayList
所有方法加synchronized
,太重)。 - 使用
Collections.synchronizedList()
转换成线程安全类。 - 使用
java.concurrent.CopyOnWriteArrayList
(推荐)。
CopyOnWriteArrayList: 这是JUC的类,通过写时复制来实现读写分离。
CopyOnWrite容器即写时复制的容器。待一个容器添加元素的时候,不直接往当前容器Object[]添加,而是先将当前容器Object[]进行copy,复制出一个新的容器Object[] newELements,然后新的容器Object[ ] newELements里添加元素,添加完元素之后,再将原容器的引用指向新的容器setArray (newELements)。
这样做的好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁(区别于Vector和Collections.synchronizedList()),因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
4.2、hashSet
hashSet的底层数据结构是什么?HashMap.put()需要传两个参数,而
HashSet.add()`只传一个参数,这是为什么?
hashSet的底层结构是hashMap,实际上HashSet.add()
就是调用的HashMap.put()
,add的元素就是key,Value是一个Object
对象。
跟ArrayList类似,HashSet
和TreeSet
都不是线程安全的,与之对应的有CopyOnWriteSet
这个线程安全类。
CopyOnWriteSet的底层结构实际上就是一个CopyOnWriteArrayList
数组。
4.3、hashMap
hashMap也是线程不全的,ConcurrencyHashMap是线程安全的。
4、java锁
4.1、公平锁/非公平锁
公平锁:就是多个线程按照申请锁的顺序来获取锁,类似排队,先到先得。
非公平锁:指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后中请的线程比先中请的线程优先获取锁。
公平锁/非公平锁的区别:
公平锁就是很公平,在并发坏境中.每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁.否则就会加入到等待队列中.以后会按照FIFO的规则从队列中取到自己。
非公平锁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式在队列中进行排队。
非公平锁的优点是吞吐量比公平锁更大。
Synchronized和
ReentrantLock默认都是非公平锁。
ReentrantLock在构造的时候传入
true`则是公平锁
4.2、可重入锁/递归锁
可重入锁又叫递归锁,也就是同一个线程在外层方法获得锁时,进入内层方法会自动获取锁。
可重入锁可以避免死锁的问题。
ReentrantLock/synchronized就是一个典型的可重入锁。
4.3、自旋锁
自旋锁,就是尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取。自己在那儿一直循环获取,就像“自旋”一样。
好处是减少线程切换的上下文开销,
缺点是会消耗CPU。
CAS底层的getAndAddInt
就是自旋锁思想。
自旋锁代码验证
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;public class SpinLockDemo {// 现在的泛型装的是Thread,原子引用线程AtomicReference<Thread> atomicReference = new AtomicReference<>();public void myLock() {// 获取当前进来的线程Thread thread = Thread.currentThread();System.out.println(Thread.currentThread().getName() + "\t come in ");// 开始自旋,期望值是null,更新值是当前线程,如果是null,则更新为当前线程,否者自旋while(!atomicReferencepareAndSet(null, thread)) {//摸鱼}}public void myUnLock() {// 获取当前进来的线程Thread thread = Thread.currentThread();// 自己用完了后,把atomicReference变成nullatomicReferencepareAndSet(thread, null);System.out.println(Thread.currentThread().getName() + "\t invoked myUnlock()");}public static void main(String[] args) {SpinLockDemo spinLockDemo = new SpinLockDemo();// 启动t1线程,开始操作new Thread(() -> {// 开始占有锁spinLockDemo.myLock();try {TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();}// 开始释放锁spinLockDemo.myUnLock();}, "t1").start();// 让main线程暂停1秒,使得t1线程,先执行try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}// 1秒后,启动t2线程,开始占用这个锁new Thread(() -> {// 开始占有锁spinLockDemo.myLock();// 开始释放锁spinLockDemo.myUnLock();}, "t2").start();}
}
4.4、读写锁/独占/共享锁
独占锁:指该锁一次只能被一个线程所持有。对ReentrantLock和Synchronized而言都是独占锁。
共享锁:指该锁可被多个线程所持有。
读写锁:读是共享锁,写是独占锁 ,读的共享锁可保证并发读是非常高效的,写的独占锁保证数据的原子性。
ReentrantReadWriteLock
代码演示
package com.lun.concurrency;import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;class MyCache2 {private volatile Map<String, Object> map = new HashMap<>();private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();public void put(String key, Object value) {// 创建一个写锁rwLock.writeLock().lock();try {System.out.println(Thread.currentThread().getName() + "\t 正在写入:" + key);try {// 模拟网络拥堵,延迟0.3秒TimeUnit.MILLISECONDS.sleep(300);} catch (InterruptedException e) {e.printStackTrace();}map.put(key, value);System.out.println(Thread.currentThread().getName() + "\t 写入完成");} catch (Exception e) {e.printStackTrace();} finally {// 写锁 释放rwLock.writeLock().unlock();}}public void get(String key) {// 读锁rwLock.readLock().lock();try {System.out.println(Thread.currentThread().getName() + "\t 正在读取:");try {// 模拟网络拥堵,延迟0.3秒TimeUnit.MILLISECONDS.sleep(300);} catch (InterruptedException e) {e.printStackTrace();}Object value = map.get(key);System.out.println(Thread.currentThread().getName() + "\t 读取完成:" + value);} catch (Exception e) {e.printStackTrace();} finally {// 读锁释放rwLock.readLock().unlock();}}public void clean() {map.clear();}}public class ReadWriteWithLockDemo {public static void main(String[] args) {MyCache2 myCache = new MyCache2();// 线程操作资源类,5个线程写for (int i = 1; i <= 5; i++) {// lambda表达式内部必须是finalfinal int tempInt = i;new Thread(() -> {myCache.put(tempInt + "", tempInt + "");}, String.valueOf(i)).start();}// 线程操作资源类, 5个线程读for (int i = 1; i <= 5; i++) {// lambda表达式内部必须是finalfinal int tempInt = i;new Thread(() -> {myCache.get(tempInt + "");}, String.valueOf(i)).start();}}
}
4.5、多线程面试题
题目: 多线程之间按顺序调用 实现A->B->C三个线程启动,要求如下: AA 打印5次 BB打印10次 CC 打印15次 循环10次
package com.example.test.test;import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;/* ** @program: testQueue* @description* @author: swq* @create: 2021-04-06 10:07**/
public class ThreadTest {// 题目 多线程之间按顺序调用 实现A->B->C三个线程启动,要求如下// AA 打印5次 BB打印10次 CC 打印15次 循环10次// 资源类static class ShareData {// 标识位private int number = 1;// 锁private Lock lock = new ReentrantLock();//条件private Condition c1 = lock.newCondition();private Condition c2 = lock.newCondition();private Condition c3 = lock.newCondition();/*** 根据标识位输出次数** @param count* @param inputNumber* @param doCondition* @param needCondition*/private void doContent(int count, int inputNumber, Condition doCondition, Condition needCondition) {try {// 加锁lock.lock();// 阻塞while (this.number != inputNumber) {doCondition.await();}// 干活for (int i = 1; i <= count; i++) {System.out.println(Thread.currentThread().getName() + "\t" + i);}if (number == 3) {number = 1;} else {number = number + 1;}// 唤醒needCondition.signal();} catch (Exception e) {e.printStackTrace();} finally {lock.unlock();}}}public static void main(String[] args) {ShareData shareData = new ShareData();new Thread(() -> {for (int i = 0; i < 10; i++) {shareData.doContent(5, 1, shareData.c1, shareData.c2);}}, "AAA").start();new Thread(() -> {for (int i = 0; i < 10; i++) {shareData.doContent(10, 2, shareData.c2, shareData.c3);}}, "BBB").start();new Thread(() -> {for (int i = 0; i < 10; i++) {shareData.doContent(15, 3, shareData.c3, shareData.c1);}}, "CCC").start();}}
4.6、Synchronized和Lock的区别:
synchronized
关键字和java.util.concurrent.locks.Lock
都能加锁,两者有什么区别呢?
- 原始构成:
sync
是JVM层面的,底层通过monitorenter
和monitorexit
来实现的。Lock
是JDK API层面的。(sync
一个enter会有两个exit,一个是正常退出,一个是异常退出) - 使用方法:
sync
不需要手动释放锁,而Lock
需要手动释放。 - 是否可中断:
sync
不可中断,除非抛出异常或者正常运行完成。Lock
是可中断的,通过调用interrupt()
方法。 - 是否为公平锁:
sync
只能是非公平锁,而Lock
既能是公平锁,又能是非公平锁。 - 绑定多个条件:
sync
不能,只能随机唤醒。而Lock
可以通过Condition
来绑定多个条件,精确唤醒。
Synchronized和Lock的区别?
- 原始构成
synchronized属于JVM层面,属于java的关键字,monitorenter(底层是通过monitor对象来完成,其实wait/notify等方法也依赖于monitor对象 只能在同步块或者方法中才能调用 wait/ notify等方法)
Lock是具体类(java.util.concurrent.locks.Lock)是api层面的锁
- 使用方法:
synchronized:不需要用户去手动释放锁,当synchronized代码执行后,系统会自动让线程释放对锁的占用。
ReentrantLock:则需要用户去手动释放锁,若没有主动释放锁,就有可能出现死锁的现象,需要lock() 和 unlock() 配置try catch语句来完成 - 等待是否中断
synchronized:不可中断,除非抛出异常或者正常运行完成。
ReentrantLock:可中断,可以设置超时方法
设置超时方法,trylock(long timeout, TimeUnit unit)
lockInterrupible() 放代码块中,调用interrupt() 方法可以中断 - 加锁是否公平
synchronized:非公平锁
ReentrantLock:默认非公平锁,构造函数可以传递boolean值,true为公平锁,false为非公平锁 - 锁绑定多个条件Condition
synchronized:没有,要么随机,要么全部唤醒
ReentrantLock:用来实现分组唤醒需要唤醒的线程,可以精确唤醒,而不是像synchronized那样,要么随机,要么全部唤醒
使用lock的好处?
-
锁绑定多个条件Condition,可以精确唤醒
-
等待可中断,可以设置超时方法
5、CountDownLatch/CyclicBarrier/Semaphore
5.1、CountDownLatch
一般被称为门栓计数器,CountDownLatch`内部维护了一个计数器,只有当计数器==0时,某些线程才会停止阻塞,开始执行。
CountDownLatch主要有两个方法,
countDown()来让计数器-1,
await()来让线程阻塞。当
count==0`时,阻塞线程自动唤醒。
当一个或多个线程调用await()时,调用线程会被阻塞。其它线程调用countDown()会将计数器减1(调用countDown方法的线程不会阻塞),当计数器的值变为零时,因调用await方法被阻塞的线程会被唤醒,继续执行。
5.2、CyclicBarrier
CyclicBarrier的字面意思就是可循环(Cyclic)使用的屏障(Barrier),它要求做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过CyclicBarrier的await方法
CyclicBarrier与CountDownLatch的区别:CyclicBarrier可重复多次,而CountDownLatch只能是一次。
5.3、Semaphore
信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制
正常的锁(concurrency.locks或synchronized锁)在任何时刻都只允许一个任务访问一项资源,而 Semaphore允许n个任务同时访问这个资源
6、阻塞队列
6.1、什么是阻塞队列?
首先它是一个队列,当阻塞队列为空时,获取(take)操作是阻塞的;当阻塞队列为满时,添加(put)操作是阻塞的。
什么是阻塞?:所谓阻塞,在某些情况下余挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒。
阻塞队列的好处:阻塞队列不用手动控制什么时候该被阻塞,什么时候该被唤醒,简化了操作。
阻塞队列的种类:
- ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
- LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为Integer.MAX_VALUE)阻塞队列。
- PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
- DelayQueue:使用优先级队列实现妁延迟无界阻塞队列。
- SynchronousQueue:不存储元素的阻塞队列。
- LinkedTransferQueue:由链表结构绒成的无界阻塞队列。
- LinkedBlockingDeque:由链表结构组成的双向阻塞队列。
需要注意的是LinkedBlockingQueue
虽然是有界的,但有个巨坑,其默认大小是Integer.MAX_VALUE
,高达21亿,一般情况下内存早爆了(在线程池的ThreadPoolExecutor
有体现)
BlockingQueue的核心方法
方法类型 | 抛出异常 | 特殊性 | 阻塞 | 超时 |
---|---|---|---|---|
插入 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除 | remove() | poll() | take() | poll(time,unit) |
检查 | element() | peek() | 不可用 | 不可用 |
6.2、阻塞队列生产者消费者版本
package com.example.test.test;import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;class ShareData {private volatile Boolean FLAG = true;private AtomicInteger atomicInteger = new AtomicInteger();private BlockingQueue<String> blockingQueue = null;public ShareData(BlockingQueue<String> blockingQueue) {this.blockingQueue = blockingQueue;System.out.println(blockingQueue.getClass().getName() + "\t");}public void myProducer() {String data = "";boolean result = false;while (FLAG) {try {data = atomicInteger.incrementAndGet() + "";result = blockingQueue.offer(data, 2, TimeUnit.SECONDS);if (result) {System.out.println(Thread.currentThread().getName() + "生产者 生产" + atomicInteger + "成功");TimeUnit.SECONDS.sleep(1);} else {System.out.println(Thread.currentThread().getName() + "生产者 生产" + atomicInteger + "失败");}} catch (InterruptedException e) {e.printStackTrace();}}}public void myConsumer() {String result = "";while (FLAG) {try {result = blockingQueue.poll(2, TimeUnit.SECONDS);if (result == null || "".equals(result)) {FLAG = false;System.out.println(Thread.currentThread().getName() + "消费者 消费 超过2秒 结束运行");return;} else {System.out.println(Thread.currentThread().getName() + "消费者 消费" + result + "成功");}} catch (InterruptedException e) {e.printStackTrace();}}}public void stop() {FLAG = false;System.out.println(Thread.currentThread().getName() + "叫停运行");}}public class ThreadTest02 {public static void main(String[] args) {ShareData shareData = new ShareData(new ArrayBlockingQueue<>(5));new Thread(() -> {shareData.myProducer();}, "AAA").start();new Thread(() -> {shareData.myConsumer();}, "BBB").start();try {TimeUnit.SECONDS.sleep(5);shareData.stop();} catch (InterruptedException e) {e.printStackTrace();}}
}
7、线程池
7.1、什么是线程池?
线程池就是创建若干个可执行的线程放入一个池(容器)中,有任务需要处理时,会提交到线程池中的任务队列,处理完之后线程并不会被销毁,而是仍然在线程池中等待下一个任务
7.2、为什么要使用线程池?
- 降低资源消耗。通过重复利用己创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。使用线程池可以进行统一的分配,调优和监控。
7.3、线程池的创建方式
-
使用Executor创建线程池
newFixedThreadPool:使用
LinkedBlockingQueue
实现,定长线程池。public static ExecutorService newFixedThreadPool(int nThreads) {return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()); }
newSingleThreadExecutor:使用
LinkedBlockingQueue
实现,一池只有一个线程。public static ExecutorService newSingleThreadExecutor() {return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>())); }
newCachedThreadPool:使用
SynchronousQueue
实现,变长线程池。public static ExecutorService newCachedThreadPool() {return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>()); }
-
自定义线程池
package com.song.gulimall.product.config;import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration;import java.util.concurrent.*;/* ** @program: gulimall* @description* @author: swq* @create: 2021-03-24 23:15**/ @Configuration public class MyThreadPoolConfig {@Beanpublic ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties pool) {return new ThreadPoolExecutor(pool.getCoreSize(),pool.getMaxSize(),pool.getKeepAliveTime(),TimeUnit.SECONDS,new LinkedBlockingDeque<>(100000),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());} }package com.song.gulimall.product.config;import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component;/* ** @program: gulimall* @description* @author: swq* @create: 2021-03-24 23:20**/ @ConfigurationProperties(prefix = "spring.thread") @Component @Data public class ThreadPoolConfigProperties {private Integer coreSize;private Integer maxSize;private Integer keepAliveTime;}
总结:
- 项目必须使用自定义的线程池,
- FixedThreadPool和SingleThreadPool:允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
- CachedThreadPool和ScheduledThreadPool:允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
7.4、ThreadPoolExecutor
创建线程池的7个参数?
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:线程池中的常驻核心线程数,在创建了线程池后,当有请求任务来之后,就会安排池中的线程去执行请求任务,近似理解为今日当值线程,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中
- maximumPoolSize:线程池能够容纳同时执行的最大线程数,此数值必须大于等于1
- keepAliveTime:多余的空闲线程的存活时间。当前线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime值时,多余空闲线程会被销毁直到只剩下corePoolSize个线程为止
- unit:keepAliveTime的单位
- workQueue:任务队列,被提交但尚未被执行的任务(阻塞队列)
- threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程一般用默认的即可
- handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数时,如何来拒绝
7.5、线程池的拒绝策略
什么是线程池的拒绝策略?
等待队列也已经排满了,再也塞不下新任务了,同时线程池中的max线程也达到了,无法继续为新任务服务,阻止新任务进入的一种方式。
- AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行
- CallerRunsPolicy:调用者运行,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退给调用者,从而降低新任务的流量
- DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务
- DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种方案
7.6、线程池的底层原理
架构:
流程图:
)]
解释:
-
在创建了线程池后,等待提交过来的任务请求。当调用execute()方法添加一个请求任务时,线程池会做如下判断:
-
如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
-
如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
-
如果这时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
-
如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
-
当一个线程完成任务时,它会从队列中取下一个任务来执行。
-
当一个线程无事可做超过一定的时间(keepAliveTime)时,线程池会判断:
-
如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉,所以线程池的所有任务完成后它最终会收缩到corePoolSize的大小
总结:
当向线程池提交一个任务后,如果当前的线程数小于核心线程数(corePoolSize),那么就会立即创建线程执行任务,如果当前线程数大于核心线程数,而且阻塞队列容量未满的情况下,会放入到阻塞队列中,如果阻塞队列也满了,但是当前的线程运行数小于最大线程数,那么就会进行扩容,创建非核心线程来立即执行这个任务,如果阻塞队列满了,当前的运行线程数也达到了最大值,那么线程池就会启动拒绝策略。如果有线程的闲置时间超过线程的最大存活时间,那么就会关闭这个线程。
7.7、线程池的参数(最大线程数)
如何查看机器的逻辑处理器个数
System.out.println(Runtime.getRuntime().availableProcessors());
CPU 密集型
-
CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。
-
CPU密集型任务配置尽可能少的线程数量,一般公式:CPU核数+1个线程的线程池
IO 密集型
-
由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如CPU核数*2。
-
IO密集型,即该任务需要大量的IO,即大量的阻塞。在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。
所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。 -
IO密集型时,大部分线程都阻塞,故需要多配置线程数:参考公式:CPU核数/(1-阻塞系数),阻塞系数在0.8~0.9之间,比如8核CPU:8/(1-0.9)=80个线程数
8、死锁
8.1、什么是死锁?
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象
8.2、产生死锁主要原因
- 系统资源不足
- 进程运行推进的顺序不合适
- 资源分配不当
8.3、发生死锁的四个条件
- 互斥条件,线程使用的资源至少有一个不能共享的。
- 至少有一个线程必须持有一个资源且正在等待获取一个当前被别的线程持有的资源。
- 资源不能被抢占。
- 循环等待。
8.4、如何解决死锁问题
破坏发生死锁的四个条件其中之一即可
8.5、手写死锁
package com.example.test.test;import java.util.concurrent.TimeUnit;class LockData implements Runnable {private String lockA;private String lockB;public LockData(String lockA, String lockB) {this.lockA = lockA;this.lockB = lockB;}@Overridepublic void run() {synchronized (lockA) {System.out.println(Thread.currentThread().getName() + "持有 lockA 想要获取 lockB");try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}synchronized (lockB) {System.out.println(Thread.currentThread().getName() + "持有 lockB 想要获取 lockA");}}}
}public class DeadLockDemo {public static void main(String[] args) {String lockA = "lockA";String lockB = "lockB";new Thread(new LockData(lockA, lockB), "AAA").start();new Thread(new LockData(lockB, lockA), "BBB").start();}
}
8.6、怎么诊断死锁
- 在控制台 jps -l 命令定位进程号
- jstack 进程号 找到死锁查看
主要原因
- 系统资源不足
- 进程运行推进的顺序不合适
- 资源分配不当
8.3、发生死锁的四个条件
- 互斥条件,线程使用的资源至少有一个不能共享的。
- 至少有一个线程必须持有一个资源且正在等待获取一个当前被别的线程持有的资源。
- 资源不能被抢占。
- 循环等待。
8.4、如何解决死锁问题
破坏发生死锁的四个条件其中之一即可
8.5、手写死锁
package com.example.test.test;import java.util.concurrent.TimeUnit;class LockData implements Runnable {private String lockA;private String lockB;public LockData(String lockA, String lockB) {this.lockA = lockA;this.lockB = lockB;}@Overridepublic void run() {synchronized (lockA) {System.out.println(Thread.currentThread().getName() + "持有 lockA 想要获取 lockB");try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}synchronized (lockB) {System.out.println(Thread.currentThread().getName() + "持有 lockB 想要获取 lockA");}}}
}public class DeadLockDemo {public static void main(String[] args) {String lockA = "lockA";String lockB = "lockB";new Thread(new LockData(lockA, lockB), "AAA").start();new Thread(new LockData(lockB, lockA), "BBB").start();}
}
8.6、怎么诊断死锁
- 在控制台 jps -l 命令定位进程号
- jstack 进程号 找到死锁查看
更多推荐
JUC相关知识详解
发布评论