JUC多线程及高并发
- 1.1 volatile是什么
- 1.2 JMM内存模型之可见性
- 1.3 volatile的三个特性
- 1.4 volatile的应用场景
- 2.1 CAS是什么
- 2.2 CAS底层原理
- 2.3 CAS的缺点
- 3.1 _AtomicReference原子引用
-
- 3.2 _AtomicStampedReference版本号原子引用
- 3.3 ABA问题的解决
- 4.1并发修改异常
- 4.2写时复制
- 4.3 set
- 4.4 HashMap
- 5.1 公平锁和非公平锁
- 5.2 java锁之可重入锁和递归锁理论知识
- 5.3 java锁之可重入锁和递归锁代码验证
- 5.4 java锁之自旋锁理论知识
- 5.5 java锁之自旋锁代码验证
- 5.6 java锁之读写锁理论知识
- 5.7 java之读写锁代码验证
- 6.1CountDownLatch
- 6.2 CyclicBarrier
- 6.3 Semaphore
- 7.1 阻塞队列理论
- 7.2 阻塞队列接口结构和实现类
- 7.3 BlockingQueue的核心方法
- 7.4 阻塞队列api之抛出异常组
- 7.5 阻塞队列api之返回布尔值组
- 7.6 阻塞队列aoi之阻塞和超时控制组
- 7.7 阻塞队列之同步SychronousQueue
- 7.8 线程通信之生产者消费者传统版
- 7.9 线程通信之生产者消费者阻塞队列版
- 8.1 区别
- 8.2 锁绑定多个条件Condition
- 10.1 线程池使用及优势
- 10.2 线程池三个常用方式
- 10.3 线程池7大参数入门简介
- 10.4 线程池7大参数深入介绍
- 10.5 线程池底层工作原理
- 10.6 线程池的4种拒绝策略理论简介
- 10.7 线程池中实际使用哪一个拒绝策略
- 10.8 线程池的手写改造和拒绝策略
- 10.9 线程池配置合理线程数
- 11.1 什么是死锁
- 11.2 产生死锁主要原因
- 11.3 发生死锁的四个条件
- 11.4 如何解决死锁问题
- 11.5 查看是否死锁工具
1.volatile关键字
Java并发编程:volatile关键字解析:https://www.cnblogs.com/dolphin0520/p/3920373.html
1.1 volatile是什么
volatile是java虚拟机提供的轻量级的同步机制
1.2 JMM内存模型之可见性
JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
JMM关于同步的规定:
线程解锁前,必须把共享变量的值刷新回主内存
线程加锁前,必须读取主内存的最新值到自己的工作内存
加锁解锁是同一把锁
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:
1.3 volatile的三个特性
1.3.1 保证可见性
通过前面对JMM的介绍,我们知道各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存进行操作后再写回到主内存中的。
这就可能存在一个线程AAA修改了共享变量X的值但还未写回主内存时,另外一个线程BBB又对主内存中同一个共享变量X进行操作,但此时A线程工作内存中共享变量x对线程B来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题
1.验证volatile的可见性
1.1 假如 int number = 0; number 变量之前根本没有添加volatile关键字修饰,没有可见性
1.2 添加了volatile,可以解决可见性问题
代码验证:
/** * volatile可以保证可见性,及时通知其他主线程,主物理内存的值已经被修改 */ private static void seeOkByVolatile() { myData myData = new myData(); new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\t come in"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } myData.addTo60(); System.out.println(Thread.currentThread().getName() + "\t " + myData.num); }, "AAA").start(); while (myData.num == 0) { } System.out.println(Thread.currentThread().getName() + "\t mission is over, value of num is:" + myData.num); }
1.3.2 不保证原子性
2.验证volatile不保证原子性
2.1 原子性指的是什么?
不可分割,完整性,也即某个线程正在做某个具体业务,中间不可以被加塞或者被分割,需要整体完整
要么同时成功,要么同时失败
2.2 如何解决?
加sync
使用我们的juc 下的AtomicInteger
理论解释
number++在多线程下是非线程安全的。
我们可以将代码编译成字节码,可看出number++被编译成3条指令。
假设我们没有加 synchronized那么第一步就可能存在着,三个线程同时通过getfield命令,拿到主存中的 n值,然后三个线程,各自在自己的工作内存中进行加1操作,但他们并发进行 iadd 命令的时候,因为只能一个进行写,所以其它操作会被挂起,假设1线程,先进行了写操作,在写完后,volatile的可见性,应该需要告诉其它两个线程,主内存的值已经被修改了,但是因为太快了,其它两个线程,陆续执行 iadd命令,进行写入操作,这就造成了其他线程没有接受到主内存n的改变,从而覆盖了原来的值,出现写丢失,这样也就让最终的结果少于20000。
问题解决:
可加synchronized解决,但它是重量级同步机制,性能上有所顾虑。
如何不加synchronized解决number++在多线程下是非线程安全的问题?使用AtomicInteger。
代码验证:
public class volatileDemo { public static void main(String[] args) { AtomicByVolatile(); } private static void AtomicByVolatile() { myData myData = new myData(); for (int i=0;i<20;i++){ new Thread(()->{ for (int j = 1;j <= 1000;j++){ myData.addPlusPlus(); myData.addMyAtomic(); } },String.valueOf(i)).start(); } while (Thread.activeCount() > 2){ Thread.yield(); } System.out.println(Thread.currentThread().getName() + "\t int type,finally num is :" + myData.num); System.out.println(Thread.currentThread().getName() + "\t AtomicInteger type,finally num is :" + myData.atomicInteger); } } class myData { volatile int num = 0; public void addTo60() { this.num = 60; } public void addPlusPlus() { this.num++; } AtomicInteger atomicInteger = new AtomicInteger(0); public void addMyAtomic(){ atomicInteger.getAndIncrement(); } }
控制台输出结果:
main int type,finally num is :17914
main AtomicInteger type,finally num is :20000
1.3.3 禁止指令重排序
指令重排:多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
1.4 volatile的应用场景
1.4.1 单例模式在多线程环境下可能存在安全问题
单例模式下的双重校验锁(DCL:Double Check Lock):
单例模式DCL代码:
public class SingletonDemo { private static SingletonDemo instance = null; private SingletonDemo(){ System.out.println(Thread.currentThread().getName() + "构造方法执行了"); } public static SingletonDemo getInstance(){ if (instance == null){ synchronized (SingletonDemo.class){ if (instance == null){ instance = new SingletonDemo(); } } } return instance; } }
DCL(双端检锁)机制不一定线程安全,原因是有指令重排序的存在,加入volatile可以禁止指令重排。
原因在于某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化。
Instance = new SingletonDemo();可以分成以下三步完成(伪代码):
memory = allocate(); //1.分配对象内存空间
instance(memory); //2.初始化对象
instance = memory; //3.设置instance指向刚分配的内存地址,此时instance != null
步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。
memory = allocate(); //1.分配对象内存空间
instance = memory;//3.设置instance指向刚分配的内存地址,此时instance! =null,但是对象还没有初始化完成!
instance(memory);//2.初始化对象
但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。
所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。
1.4.2 单例模式下的volatile
public class SingletonDemo { private volatile static SingletonDemo instance = null; private SingletonDemo(){ System.out.println(Thread.currentThread().getName() + "构造方法执行了"); } public static SingletonDemo getInstance(){ if (instance == null){ synchronized (SingletonDemo.class){ if (instance == null){ instance = new SingletonDemo(); } } } return instance; } public static void main(String[] args){ //单线程 System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance()); System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance()); System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance()); //多线程 for (int i=1;i<=10;i++){ new Thread(()->{ SingletonDemo.getInstance(); },String.valueOf(i)).start(); } } }
2. CAS(Compare And Set)
2.1 CAS是什么
示例程序:
public class CASDemo { public static void main(String[] args){ AtomicInteger atomicInteger = new AtomicInteger(5); System.out.println(atomicInteger.compareAndSet(5,2019) + "\t current value is :" +atomicInteger); System.out.println(atomicInteger.compareAndSet(5,2020) + "\t current value is :" +atomicInteger); } }
输出结果:
true current value is :2019
false current value is :2019
2.2 CAS底层原理
atomiclnteger.getAndIncrement();源码
public final int getAndIncrement() { return U.getAndAddInt(this, VALUE, 1); }
2.2.1 Unsafe
是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。
注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务。
变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。
变量value用volatile修饰,保证了多线程之间的内存可见性。
2.2.2 CAS底层原理
CAS的全称为Compare-And-Swap,它是一条CPU并发原语。
它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。(原子性)
UnSafe.getAndAddInt()源码解释:
var1 AtomicInteger对象本身。
var2 该对象值得引用地址。
var4 需要变动的数量。
var5是用过var1,var2找出的主内存中真实的值。
用该对象当前的值与var5比较:
如果相同,更新var5+var4并且返回true,
如果不同,继续取值然后再比较,直到更新完成。
假设线程A和线程B两个线程同时执行getAndAddInt操作(分别跑在不同CPU上) :
1.Atomiclnteger里面的value原始值为3,即主内存中Atomiclnteger的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继续执行compareAndSwaplnt进行比较替换,直到成功。
2.2.2 底层汇编
Unsafe类中的compareAndSwapInt,是一个本地方法,该方法的实现位于unsafe.cpp中。
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)
UnsafeWrapper("Unsafe_CompareAndSwaplnt");
oop p = JNlHandles::resolve(obj);
jint* addr = (jint *)index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e))== e;
UNSAFE_END
//先想办法拿到变量value在内存中的地址。
//通过Atomic::cmpxchg实现比较替换,其中参数x是即将更新的值,参数e是原内存的值。
2.3 CAS的缺点
1.循环时间长开销很大
// ursafe.getAndAddInt public final int getAndAddInt(Object var1, long var2, int var4){ int var5; do { var5 = this.getIntVolatile(var1, var2); }while(!this.compareAndSwapInt(varl, var2, var5,var5 + var4)); return var5; }
我们可以看到getAndAddInt方法执行时,有个do while,如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。
2.只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是,对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
3.引出来ABA问题
3 ABA问题
ABA问题怎么产生的
CAS会导致“ABA问题”。
CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。
比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。
尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。
3.1 _AtomicReference原子引用
public class AtomicReferenceDemo { public static void main(String[] args){ User zhangsan = new User("zhangsan", 20); User lisi = new User("lisi", 23); AtomicReference<User> userAtomicReference = new AtomicReference<>(); userAtomicReference.set(zhangsan); System.out.println(userAtomicReference.compareAndSet(zhangsan,lisi) + "\t" + userAtomicReference.get().toString()); System.out.println(userAtomicReference.compareAndSet(zhangsan,lisi) + "\t" + userAtomicReference.get().toString()); } }
3.2 _AtomicStampedReference版本号原子引用
原子引用 + 新增一种机制,那就是修改版本号(类似时间戳),它用来解决ABA问题。
3.3 ABA问题的解决
ABA问题程序演示及解决方法演示:
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(()->{ atomicReference.compareAndSet(100,101); atomicReference.compareAndSet(101,100); },"t1").start(); new Thread(()->{ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(atomicReference.compareAndSet(100,2019) + "\t "+ atomicReference.get()); },"t2").start(); System.out.println("==============以下是ABA问题的解决==============="); new Thread(()->{ System.out.println("第1次版本号"+ atomicStampedReference.getStamp()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp() + 1); System.out.println("第2次版本号"+atomicStampedReference.getStamp()); atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1); System.out.println("第3次版本号"+atomicStampedReference.getStamp()); },"t3").start(); new Thread(()->{ int stamp = atomicStampedReference.getStamp(); System.out.println("第1次版本号"+ stamp); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } boolean result = atomicStampedReference.compareAndSet(100, 101, stamp, stamp + 1); System.out.println("是否修改成功:"+result); },"t4").start(); } }
4.集合类不安全
4.1并发修改异常
public class ContainerNotSafeDemo { public static void main(String[] args) { List<String> list = new ArrayList<>(); for (int i = 0; i < 30; i++) { new Thread(() -> { list.add(UUID.randomUUID().toString().substring(0, 8)); System.out.println(list); }, String.valueOf(i)).start(); } } }
上述程序会抛java.util.ConcurrentModificationException
解决方法之一:Vector
解决方法之二:List<String> strings = Collections.synchronizedList(new ArrayList<String>());
解决方法之三:new CopyOnWriteArrayList<>();
4.2写时复制
public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { private static final long serialVersionUID = 8673264195747942595L; /** The lock protecting all mutators */ final transient ReentrantLock lock = new ReentrantLock(); /** The array, accessed only via getArray/setArray. */ private transient volatile Object[] array; /** * Appends the specified element to the end of this list. * * @param e element to be appended to this list * @return {@code true} (as specified by {@link Collection#add}) */ 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(); } } }
CopyOnWrite容器即写时复制的容器。待一个容器添加元素的时候,不直接往当前容器Object[]添加,而是先将当前容器Object[]进行copy,复制出一个新的容器Object[] newELements,然后新的容器Object[ ] newELements里添加元素,添加完元素之后,再将原容器的引用指向新的容器setArray (newELements)。
这样做的好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁【2021年之后源码修改了】(区别于Vector和Collections.synchronizedList()),因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
4.3 set
HashSet也是线程不安全的
解决方法:
- Collections.synchronizedSet(new HashSet<>())
- CopyOnWriteArraySet<>()(推荐)
4.4 HashMap
HashSet也是线程不安全的
解决方法:
- HashTable
- Collections.synchronizedMap(new HashMap<>())
5.java锁
5.1 公平锁和非公平锁
公平锁―是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到。
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后中请的线程比先中请的线程优先获取锁。在高并发的情况下,有可能会造成优先级反转或者饥饿现象
两者区别
公平锁
Threads acquire a fair lock in the order in which they requested it.
公平锁就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己。
非公平锁
a nonfair lock permits barging: threads requesting a lock can jump ahead of the queue of waiting threads if the lockhappens to be available when it is requested.
非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。
Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。
非公平锁的优点在于吞吐量比公平锁大。
对于Synchronized而言,也是一种非公平锁
5.2 java锁之可重入锁和递归锁理论知识
可重入锁(也叫做递归锁)
指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
也即是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块。
ReentrantLock/synchronized就是一个典型的可重入锁。
可重入锁最大的作用是避免死锁
5.3 java锁之可重入锁和递归锁代码验证
Synchronized可入锁演示程序
class Phone { public synchronized void sendSMS() throws Exception{ System.out.println(Thread.currentThread().getName() + "\t invoked sendSMS()"); // 在同步方法中,调用另外一个同步方法 sendEmail(); } public synchronized void sendEmail() throws Exception{ System.out.println(Thread.currentThread().getId() + "\t invoked sendEmail()"); } } public class SynchronizedReentrantLockDemo { public static void main(String[] args) { Phone phone = new Phone(); // 两个线程操作资源列 new Thread(() -> { try { phone.sendSMS(); } catch (Exception e) { e.printStackTrace(); } }, "t1").start(); new Thread(() -> { try { phone.sendSMS(); } catch (Exception e) { e.printStackTrace(); } }, "t2").start(); } }
输出结果:
t1 invoked sendSMS()
t1 invoked sendEmail()
t2 invoked sendSMS()
12 invoked sendEmail()
ReentrantLock可重入锁演示程序
class Phone2 implements Runnable{ Lock lock = new ReentrantLock(); /** * set进去的时候,就加锁,调用set方法的时候,能否访问另外一个加锁的set方法 */ public void getLock() { lock.lock(); try { System.out.println(Thread.currentThread().getName() + "\t get Lock"); setLock(); } finally { lock.unlock(); } } public void setLock() { lock.lock(); try { System.out.println(Thread.currentThread().getName() + "\t set Lock"); } finally { lock.unlock(); } } @Override public void run() { getLock(); } } public class ReentrantLockDemo { public static void main(String[] args) { Phone2 phone = new Phone2(); /** * 因为Phone实现了Runnable接口 */ Thread t3 = new Thread(phone, "t3"); Thread t4 = new Thread(phone, "t4"); t3.start(); t4.start(); } }
输出结果:
t3 get Lock
t3 set Lock
t4 get Lock
t4 set Lock
5.4 java锁之自旋锁理论知识
自旋锁(Spin Lock)
是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU
提到了互斥同步对性能最大的影响阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态完成,这些操作给系统的并发性能带来了很大的压力。同时,虚拟机的开发团队也注意到在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程 “稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。
5.5 java锁之自旋锁代码验证
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(!atomicReference.compareAndSet(null, thread)) { //摸鱼 } } public void myUnLock() { // 获取当前进来的线程 Thread thread = Thread.currentThread(); // 自己用完了后,把atomicReference变成null atomicReference.compareAndSet(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(); } }
输出结果:
t1 come in
t2 come in
t1 invoked myUnlock()
t2 invoked myUnlock()
5.6 java锁之读写锁理论知识
独占锁:指该锁一次只能被一个线程所持有。对ReentrantLock和Synchronized而言都是独占锁
共享锁:指该锁可被多个线程所持有。
多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行。但是,如果有一个线程想去写共享资源来,就不应该再有其它线程可以对该资源进行读或写。
对ReentrantReadWriteLock其读锁是共享锁,其写锁是独占锁。
读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。
5.7 java之读写锁代码验证
实现一个读写缓存的操作,假设开始没有加锁的时候,会出现什么情况
class MyCache { private volatile Map<String, Object> map = new HashMap<>(); public void put(String key, Object value) { 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 写入完成"); } public void get(String key) { 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); } } public class ReadWriteWithoutLockDemo { public static void main(String[] args) { MyCache myCache = new MyCache(); // 线程操作资源类,5个线程写 for (int i = 0; i < 5; i++) { final int tempInt = i; new Thread(() -> { myCache.put(tempInt + "", tempInt + ""); }, String.valueOf(i)).start(); } // 线程操作资源类, 5个线程读 for (int i = 0; i < 5; i++) { final int tempInt = i; new Thread(() -> { myCache.get(tempInt + ""); }, String.valueOf(i)).start(); } } }
6.CountDownLatch/CyclicBarrier/Semaphore
6.1CountDownLatch
让一线程阻塞直到另一些线程完成一系列操作才被唤醒。
CountDownLatch主要有两个方法(await(),countDown())。
当一个或多个线程调用await()时,调用线程会被阻塞。其它线程调用countDown()会将计数器减1(调用countDown方法的线程不会阻塞),当计数器的值变为零时,因调用await方法被阻塞的线程会被唤醒,继续执行。
假设一个自习室里有7个人,其中有一个是班长,班长的主要职责就是在其它6个同学走了后,关灯,锁教室门,然后走人,因此班长是需要最后一个走的,那么有什么方法能够控制班长这个线程是最后一个执行,而其它线程是随机执行的
public class CountDownLatchDemo { public static void main(String[] args) throws InterruptedException { // 计数器 CountDownLatch countDownLatch = new CountDownLatch(6); for (int i = 0; i <= 6; i++) { new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\t 上完自习,离开教室"); countDownLatch.countDown(); }, String.valueOf(i)).start(); } countDownLatch.await(); System.out.println(Thread.currentThread().getName() + "\t 班长最后关门"); } }
6.2 CyclicBarrier
CyclicBarrier的字面意思就是可循环(Cyclic)使用的屏障(Barrier)。它要求做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过CyclicBarrier的await方法。
CyclicBarrier与CountDownLatch的区别:CyclicBarrier可重复多次,而CountDownLatch只能是一次。
程序演示集齐7个龙珠,召唤神龙。
public class CyclicBarrierDemo { public static void main(String[] args) { //定义一个循环屏障,参数1:需要累加的值,参数2 需要执行的方法 CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> { System.out.println("召唤神龙"); }); for (int i=0;i<7;i++){ final int temp=i; new Thread(()->{ System.out.println(Thread.currentThread().getName()+"收集到第"+temp+"颗龙珠"); try { // 先到的被阻塞,等全部线程完成后,才能执行方法 cyclicBarrier.await(); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } },String.valueOf(i)).start(); } } }
输出结果:
2收集到第2颗龙珠
3收集到第3颗龙珠
1收集到第1颗龙珠
5收集到第5颗龙珠
4收集到第4颗龙珠
6收集到第6颗龙珠
0收集到第0颗龙珠
召唤神龙
6.3 Semaphore
信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。
正常的锁(concurrency.locks或synchronized锁)在任何时刻都只允许一个任务访问一项资源,而 Semaphore允许n个任务同时访问这个资源。
模拟一个抢车位的场景,假设一共有6个车,3个停车位
public class SemaphoreDemo { public static void main(String[] args) { //初始化一个信号量为3,默认是false 非公平锁, 模拟3个停车位 Semaphore semaphore = new Semaphore(3); //模拟六部汽车 for (int i = 1; i <= 6; i++) { new Thread(() -> { try { // 代表一辆车,已经占用了该车位 semaphore.acquire(); System.out.println(Thread.currentThread().getName() + "\t抢到车位"); TimeUnit.SECONDS.sleep(3); System.out.println(Thread.currentThread().getName() + "\t停车三秒后离开车位"); } catch (InterruptedException e) { e.printStackTrace(); } finally { // 释放停车位 semaphore.release(); } }, String.valueOf(i)).start(); } } }
输出结果:
4 抢到车位
1 抢到车位
3 抢到车位
3 停车三秒后离开车位
1 停车三秒后离开车位
4 停车三秒后离开车位
5 抢到车位
2 抢到车位
6 抢到车位
5 停车三秒后离开车位
6 停车三秒后离开车位
2 停车三秒后离开车位
7.阻塞队列
7.1 阻塞队列理论
1. 阻塞队列有没有好的一面
2. 不得不阻塞,你如何管理
阻塞队列,顾名思义,首先它是一个队列,而一个阻塞队列在数据结构中所起的作用大致如下图所示:
线程1往阻塞队列中添加元素,而线程2从阻塞队列中移除元素。
当阻塞队列是空时,从队列中获取元素的操作将会被阻塞。
当阻塞队列是满时,往队列里添加元素的操作将会被阻塞。
试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素。
同样试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程从列中移除一个或者多个元素或者完全清空队列后使队列重新变得空闲起来并后续新增。
为什么用?有什么好处?
在多线程领域:所谓阻塞,在某些情况下余挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒
为什么需要BlockingQueue?
好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了
在Concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。
7.2 阻塞队列接口结构和实现类
架构介绍:
种类分析:
ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为Integer.MAX_VALUE)阻塞队列。
PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
DelayQueue:使用优先级队列实现妁延迟无界阻塞队列。
SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列。
LinkedTransferQueue:由链表结构绒成的无界阻塞队列。
LinkedBlockingDeque:由链表结构组成的双向阻塞队列。
7.3 BlockingQueue的核心方法
7.4 阻塞队列api之抛出异常组
public class BlockingQueueDemo { public static void main(String[] args) { BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<String>(3); System.out.println(blockingQueue.add("a")); System.out.println(blockingQueue.add("b")); System.out.println(blockingQueue.add("c")); try { System.out.println(blockingQueue.add("x")); }catch (Exception e){ System.out.println(e); } System.out.println(blockingQueue.element()); System.out.println(blockingQueue.remove()); System.out.println(blockingQueue.remove()); System.out.println(blockingQueue.remove()); try { System.out.println(blockingQueue.remove()); }catch (Exception e){ System.out.println(e); } try { System.out.println(blockingQueue.element()); }catch (Exception e){ System.out.println(e); } } }
输出结果:
true
true
true
java.lang.IllegalStateException: Queue full
a
a
b
c
java.util.NoSuchElementException
java.util.NoSuchElementException
7.5 阻塞队列api之返回布尔值组
public class BlockingQueueDemo { public static void main(String[] args) { BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<String>(3); System.out.println(blockingQueue.offer("a")); System.out.println(blockingQueue.offer("b")); System.out.println(blockingQueue.offer("c")); System.out.println(blockingQueue.offer("c")); System.out.println(blockingQueue.poll()); System.out.println(blockingQueue.poll()); System.out.println(blockingQueue.poll()); System.out.println(blockingQueue.poll()); } }
输出结果:
true
true
true
false
a
b
c
Null
7.6 阻塞队列aoi之阻塞和超时控制组
阻塞:
public class BlockingQueueDemo { public static void main(String[] args) throws InterruptedException { BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3); new Thread(()->{ try { blockingQueue.put("a"); blockingQueue.put("b"); blockingQueue.put("c"); blockingQueue.put("c");//将会阻塞,直到主线程take() System.out.println("it was blocked."); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); TimeUnit.SECONDS.sleep(2); try { blockingQueue.take(); blockingQueue.take(); blockingQueue.take(); blockingQueue.take(); System.out.println("Blocking..."); blockingQueue.take();//将会阻塞 } catch (InterruptedException e) { e.printStackTrace(); } } }
输出结果:
it was blocked.
Blocking...
超时控制:
public class BlockingQueueDemo { public static void main(String[] args) throws InterruptedException { BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3); System.out.println(blockingQueue.offer("a",2L,TimeUnit.SECONDS)); System.out.println(blockingQueue.offer("b",2L,TimeUnit.SECONDS)); System.out.println(blockingQueue.offer("c",2L,TimeUnit.SECONDS)); System.out.println(blockingQueue.offer("c",2L,TimeUnit.SECONDS)); } }
输出结果:
true
true
true
False
7.7 阻塞队列之同步SychronousQueue
SynchronousQueue没有容量。
与其他BlockingQueue不同,SynchronousQueue是一个不存储元素的BlockingQueue。
每一个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然。
public class BlockingQueueDemo { public static void main(String[] args) throws InterruptedException { BlockingQueue<String> 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 (InterruptedException e) { e.printStackTrace(); } },"AAA").start(); new Thread(()->{ try { try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } blockingQueue.take(); System.out.println(Thread.currentThread().getName()+"\t take 1"); try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } blockingQueue.take(); System.out.println(Thread.currentThread().getName()+"\t take 2"); try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } blockingQueue.take(); System.out.println(Thread.currentThread().getName()+"\t take 3"); } catch (InterruptedException e) { e.printStackTrace(); } },"BBB").start(); } }
输出结果:
AAA put 1
BBB take 1
AAA put 2
BBB take 2
AAA put 3
BBB take 3
7.8 线程通信之生产者消费者传统版
阻塞队列用在哪里?
1.生产者消费者模式
传统版(synchronized, wait, notify)
阻塞队列版(lock, await, signal)
2.线程池
3.消息中间件
实现一个简单的生产者消费者模式
代码实现:
class ShareData{ private int number = 0; private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); public void increment() throws InterruptedException { lock.lock(); try { while (number != 0){ condition.await(); } number ++; System.out.println(Thread.currentThread().getName() + "\t" +number); condition.signalAll(); }catch (Exception e){ e.printStackTrace(); }finally { lock.unlock(); } } public void decrement() throws InterruptedException { lock.lock(); try { while (number == 0){ condition.await(); } number --; System.out.println(Thread.currentThread().getName() + "\t" +number); condition.signalAll(); }catch (Exception e){ e.printStackTrace(); }finally { lock.unlock(); } } } public class ProdConsumer_TraditionalDemo { public static void main(String[] args) { ShareData shareData = new ShareData(); new Thread(()->{ for (int i=0 ;i<5;i++){ try { shareData.increment(); } catch (InterruptedException e) { e.printStackTrace(); } } },"AA").start(); new Thread(()->{ for (int i=0 ;i<5;i++){ try { shareData.decrement(); } catch (InterruptedException e) { e.printStackTrace(); } } },"BB").start(); } }
输出结果:
AA 1
BB 0
AA 1
BB 0
AA 1
BB 0
AA 1
BB 0
AA 1
BB 0
7.9 线程通信之生产者消费者阻塞队列版
代码实现:
class MyResource{ private volatile boolean FLAG = true; private AtomicInteger atomicInteger = new AtomicInteger(); BlockingQueue<String> blockingQueue = null; public MyResource(BlockingQueue<String> blockingQueue) { this.blockingQueue = blockingQueue; System.out.println(blockingQueue.getClass().getName()); } public void prod() throws InterruptedException { String data = null; boolean retValue ; while (FLAG){ data = atomicInteger.incrementAndGet()+""; retValue = blockingQueue.offer(data,2L, TimeUnit.SECONDS); if (retValue){ 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" +"停止生产蛋糕"); } public void consumer() throws InterruptedException { String retValue =null; while (FLAG){ retValue = blockingQueue.poll(2L,TimeUnit.SECONDS); if (null != retValue && !retValue.equalsIgnoreCase("")){ System.out.println(Thread.currentThread().getName()+"\t" +"消费蛋糕" +retValue +"成功"); }else{ FLAG = false; System.out.println(Thread.currentThread().getName()+"\t" +"消费蛋糕" +retValue +"失败"); } } System.out.println(Thread.currentThread().getName()+"\t" +"停止消费蛋糕"); } public void stop(){ this.FLAG = false; } } public class ProduceConsumer_BlockQueueDemo { public static void main(String[] args) { MyResource myResource = new MyResource(new ArrayBlockingQueue<String>(3)); new Thread(()->{ System.out.println("生产线程启动"); try { myResource.prod(); System.out.println(); System.out.println(); } catch (InterruptedException e) { e.printStackTrace(); } },"produce").start(); new Thread(()->{ System.out.println("消费线程启动"); try { myResource.consumer(); System.out.println(); System.out.println(); } catch (InterruptedException e) { e.printStackTrace(); } },"consumer").start(); try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(); System.out.println(); System.out.println(); System.out.println("5秒钟时间到,活动结束"); myResource.stop(); } }
输出结果:
java.util.concurrent.ArrayBlockingQueue
生产线程启动
消费线程启动
produce 生产蛋糕1成功
consumer 消费蛋糕1成功
produce 生产蛋糕2成功
consumer 消费蛋糕2成功
produce 生产蛋糕3成功
consumer 消费蛋糕3成功
produce 生产蛋糕4成功
consumer 消费蛋糕4成功
produce 生产蛋糕5成功
consumer 消费蛋糕5成功
produce 生产蛋糕6成功
consumer 消费蛋糕6成功
produce 生产蛋糕7成功
consumer 消费蛋糕7成功
produce 生产蛋糕8成功
consumer 消费蛋糕8成功
produce 生产蛋糕9成功
consumer 消费蛋糕9成功
5秒钟时间到,活动结束
produce 停止生产蛋糕
consumer 消费蛋糕null失败
consumer 停止消费蛋糕
8. Synchronized和Lock有什么区别
8.1 区别
1.synchronized属于JVM层面,属于java的关键字
monitorenter(底层是通过monitor对象来完成,其实wait/notify等方法也依赖于monitor对象 只能在同步块或者方法中才能调用 wait/ notify等方法)
Lock是具体类(java.util.concurrent.locks.Lock)是api层面的锁
2.使用方法:
synchronized:不需要用户去手动释放锁,当synchronized代码执行后,系统会自动让线程释放对锁的占用。
ReentrantLock:则需要用户去手动释放锁,若没有主动释放锁,就有可能出现死锁的现象,需要lock() 和 unlock() 配置try catch语句来完成
3.等待是否中断
synchronized:不可中断,除非抛出异常或者正常运行完成。
ReentrantLock:可中断,可以设置超时方法
设置超时方法,trylock(long timeout, TimeUnit unit)
lockInterrupible() 放代码块中,调用interrupt() 方法可以中断
4.加锁是否公平
synchronized:非公平锁
ReentrantLock:默认非公平锁,构造函数可以传递boolean值,true为公平锁,false为非公平锁
5.锁绑定多个条件Condition
synchronized:没有,要么随机,要么全部唤醒
ReentrantLock:用来实现分组唤醒需要唤醒的线程,可以精确唤醒,而不是像synchronized那样,要么随机,要么全部唤醒
8.2 锁绑定多个条件Condition
实现场景
多线程之间按顺序调用,实现 A-> B -> C 三个线程启动,要求如下:
AA打印5次,BB打印10次,CC打印15次
紧接着
AA打印5次,BB打印10次,CC打印15次
…
来10轮
代码实现:
class ShareResources{ private int number = 1; private Lock lock = new ReentrantLock(); private Condition condition1 = lock.newCondition(); private Condition condition2 = lock.newCondition(); private Condition condition3 = lock.newCondition(); public void print5(){ lock.lock(); try { while (number != 1){ condition1.await(); } for (int i=1;i<=5;i++){ System.out.println(Thread.currentThread().getName()+"\t" + i); } number =2; condition2.signal(); }catch (Exception e){ e.printStackTrace(); }finally { lock.unlock(); } } public void print10(){ lock.lock(); try { while (number != 2){ condition2.await(); } for (int i=1;i<=10;i++){ System.out.println(Thread.currentThread().getName()+"\t" + i); } number =3; condition3.signal(); }catch (Exception e){ e.printStackTrace(); }finally { lock.unlock(); } } public void print15(){ lock.lock(); try { while (number != 3){ condition3.await(); } for (int i=1;i<=15;i++){ System.out.println(Thread.currentThread().getName()+"\t" + i); } number =1; condition1.signal(); }catch (Exception e){ e.printStackTrace(); }finally { lock.unlock(); } } } public class SyncAndReentrantLockDemo { public static void main(String[] args) { ShareResources shareResources = new ShareResources(); new Thread(()->{ for (int i=0;i<10;i++){ shareResources.print5(); } },"AA").start(); new Thread(()->{ for (int i=0;i<10;i++){ shareResources.print10(); } },"BB").start(); new Thread(()->{ for (int i=0;i<10;i++){ shareResources.print15(); } },"CC").start(); } }
9. Callable接口
Callable接口,是一种让线程执行完成后,能够返回结果的。
代码实现:
class MyThread implements Callable<Integer>{ @Override public Integer call() throws Exception { System.out.println("-----------come in callable"); return 1024; } } public class CallableDemo { public static void main(String[] args) throws ExecutionException, InterruptedException { MyThread myThread = new MyThread(); FutureTask<Integer> futureTask = new FutureTask<>(myThread); new Thread(futureTask,"AA").start(); int result01 = 100; Integer result02 = futureTask.get(); //要求获得Callable线程的计算结果,如果没有计算完成就要去强求,会导致堵塞,直到计算完成 System.out.println("------result-----:"+(result01 + result02)); } }
10. 线程池
10.1 线程池使用及优势
线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。
它的主要特点为:线程复用,控制最大并发数,管理线程。
优点:
降低资源消耗。通过重复利用己创建的线程降低线程创建和销毁造成的消耗。
提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
10.2 线程池三个常用方式
Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类。
了解
Executors.newScheduledThreadPool()
Executors.newWorkStealingPool(int) - Java8新增,使用目前机器上可用的处理器作为它的并行级别
重点
Executors.newSingleThreadExecutor()
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }
主要特点如下:
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。
newSingleThreadExecutor将corePoolSize和maximumPoolSize都设置为1,它使用的LinkedBlockingQueue。
Executors.newFixedThreadPool(int)
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
主要特点如下:
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newFixedThreadPool创建的线程池corePoolSize和maximumPoolSize值是相等的,它使用的LinkedBlockingQueue。
Executors.newCachedThreadPool()
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
主要特点如下:
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newCachedThreadPool将corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,使用的SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。
public class MyThreadPoolDemo { public static void main(String[] args) { //ExecutorService threadPool = Executors.newFixedThreadPool(5); //ExecutorService threadPool = Executors.newSingleThreadExecutor(); ExecutorService threadPool = Executors.newCachedThreadPool(); try { for (int i=1;i<=10;i++){ threadPool.execute(()->{ System.out.println(Thread.currentThread().getName() + "\t 办理业务"); }); } } catch (Exception e){ e.printStackTrace(); }finally { threadPool.shutdown(); } } }
10.3 线程池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; }
10.4 线程池7大参数深入介绍
1.corePoolSize:线程池中的常驻核心线程数
在创建了线程池后,当有请求任务来之后,就会安排池中的线程去执行请求任务,近似理解为今日当值线程。
当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。
2.maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1
3.keepAliveTime:多余的空闲线程的存活时间。
当前线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime值时,多余空闲线程会被销毁直到只剩下corePoolSize个线程为止
4.unit:keepAliveTime的单位。
5.workQueue:任务队列,被提交但尚未被执行的任务。
6.threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程一般用默认的即可。
7.handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数( maximumPoolSize)时如何来拒绝请求执行的runnable的策略。
10.5 线程池底层工作原理
1.在创建了线程池后,等待提交过来的任务请求。
2.当调用execute()方法添加一个请求任务时,线程池会做如下判断:
如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
如果这时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
3.当一个线程完成任务时,它会从队列中取下一个任务来执行。
4.当一个线程无事可做超过一定的时间(keepAliveTime)时,线程池会判断:
如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉,所以线程池的所有任务完成后它最终会收缩到corePoolSize的大小。
10.6 线程池的4种拒绝策略理论简介
等待队列也已经排满了,再也塞不下新任务了同时,线程池中的max线程也达到了,无法继续为新任务服务,这时候我们就需要拒绝策略机制合理的处理这个问题。
JDK拒绝策略:
AbortPolicy(默认):直接抛出 RejectedExecutionException异常阻止系统正常运知。
CallerRunsPolicy:"调用者运行"一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务。
DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种方案。
以上内置拒绝策略均实现了RejectedExecutionHandler接口。
10.7 线程池中实际使用哪一个拒绝策略
(超级大坑警告)你在工作中单一的/固定数的/可变的三种创建线程池的方法,你用那个多?
答案是一个都不用,我们生产上只能使用自定义的
Executors 中JDK已经给你提供了,为什么不用?
阿里巴巴《Java 开发手册》
3.【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
说明:线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。 如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
4.【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 返回的线程池对象的弊端如下:
1) FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2) CachedThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
10.8 线程池的手写改造和拒绝策略
public class MyThreadPoolDemo { public static void main(String[] args) { doSomething(myThreadPoolExecutor(new ThreadPoolExecutor.AbortPolicy())); doSomething(myThreadPoolExecutor(new ThreadPoolExecutor.CallerRunsPolicy())); doSomething(myThreadPoolExecutor(new ThreadPoolExecutor.DiscardOldestPolicy())); doSomething(myThreadPoolExecutor(new ThreadPoolExecutor.DiscardPolicy())); } public static ExecutorService myThreadPoolExecutor( RejectedExecutionHandler handler){ return new ThreadPoolExecutor( 2, 5, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<>(3), Executors.defaultThreadFactory(), handler); } public static void doSomething(ExecutorService threadPool){ try { for (int i = 1; i <= 9; i++) { threadPool.execute(() -> { System.out.println(Thread.currentThread().getName() + "\t执行业务"); }); } TimeUnit.SECONDS.sleep(1L); System.out.println(); System.out.println(); } catch (Exception e) { e.printStackTrace(); } finally { threadPool.shutdown(); } }
输出结果:
pool-1-thread-1 执行业务
pool-1-thread-5 执行业务
pool-1-thread-1 执行业务
pool-1-thread-4 执行业务
pool-1-thread-3 执行业务
pool-1-thread-2 执行业务
pool-1-thread-1 执行业务
pool-1-thread-5 执行业务
java.util.concurrent.RejectedExecutionException: Task com.test.MyThreadPoolDemo$$Lambda$1/1078694789@3b9a45b3 rejected from java.util.concurrent.ThreadPoolExecutor@7699a589[Running, pool size = 5, active threads = 5, queued tasks = 1, completed tasks = 1]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
at com.test.MyThreadPoolDemo.doSomething(MyThreadPoolDemo.java:29)
at com.test.MyThreadPoolDemo.main(MyThreadPoolDemo.java:9)
pool-2-thread-1 执行业务
pool-2-thread-1 执行业务
pool-2-thread-3 执行业务
pool-2-thread-1 执行业务
pool-2-thread-1 执行业务
pool-2-thread-4 执行业务
pool-2-thread-3 执行业务
pool-2-thread-2 执行业务
pool-2-thread-5 执行业务
pool-3-thread-1 执行业务
pool-3-thread-2 执行业务
pool-3-thread-1 执行业务
pool-3-thread-2 执行业务
pool-3-thread-1 执行业务
pool-3-thread-3 执行业务
pool-3-thread-4 执行业务
pool-3-thread-5 执行业务
pool-4-thread-1 执行业务
pool-4-thread-2 执行业务
pool-4-thread-1 执行业务
pool-4-thread-2 执行业务
pool-4-thread-1 执行业务
pool-4-thread-4 执行业务
pool-4-thread-3 执行业务
pool-4-thread-5 执行业务
10.9 线程池配置合理线程数
合理配置线程池你是如何考虑的?
CPU密集型
CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。
CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上,无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就那些。
CPU密集型任务配置尽可能少的线程数量:
一般公式:(CPU核数+1)个线程的线程池
lO密集型
由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如CPU核数 * 2。
IO密集型,即该任务需要大量的IO,即大量的阻塞。
在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。
所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。
IO密集型时,大部分线程都阻塞,故需要多配置线程数:
参考公式:CPU核数/ (1-阻塞系数)
阻塞系数在0.8~0.9之间
比如8核CPU:8/(1-0.9)=80个线程数
11. 死锁编码及定位分析
11.1 什么是死锁
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够碍到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。
11.2 产生死锁主要原因
系统资源不足
进程运行推进的顺序不合适
资源分配不当
11.3 发生死锁的四个条件
互斥条件,线程使用的资源至少有一个不能共享的。
至少有一个线程必须持有一个资源且正在等待获取一个当前被别的线程持有的资源。
资源不能被抢占。
循环等待。
11.4 如何解决死锁问题
破坏发生死锁的四个条件其中之一即可。
产生死锁的代码(根据发生死锁的四个条件):
class MyTask implements Runnable{ private String lockA; private String lockB; public MyTask(String lockA, String lockB) { this.lockA = lockA; this.lockB = lockB; } @Override public void run() { synchronized (lockA){ System.out.println(Thread.currentThread().getName()+"\t 拥有资源" +lockA+"\t 尝试获取资源"+lockB); try { TimeUnit.SECONDS.sleep(1L); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lockB){ System.out.println(Thread.currentThread().getName()+"\t 拥有资源"+lockB+"\t 尝试获取资源"+lockA); } } } } public class DeadLockDemo { public static void main(String[] args) { String lockA = "lockA"; String lockB = "lockB"; new Thread(new MyTask(lockA,lockB),"threadAAA").start(); new Thread(new MyTask(lockB,lockA),"threadBBB").start(); } }
程序卡死,未出现同时持有的字样。
11.5 查看是否死锁工具
jps命令定位进程号
jstack找到死锁查看
E:\workspace_idea\test>jps -l
5312
1668 sun.tools.jps.Jps
4616 org.jetbrains.jps.cmdline.Launcher
4412 org.jetbrains.kotlin.daemon.KotlinCompileDaemon
5884 com.test.DeadLockDemo
E:\workspace_idea\test>jstack 5884
2022-01-01 20:27:06
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.40-b25 mixed mode):
Java stack information for the threads listed above:
===================================================
"threadBBB":
at com.test.MyTask.run(DeadLockDemo.java:25)
- waiting to lock <0x00000000d6144c98> (a java.lang.String)
- locked <0x00000000d6144cd0> (a java.lang.String)
at java.lang.Thread.run(Thread.java:745)
"threadAAA":
at com.test.MyTask.run(DeadLockDemo.java:25)
- waiting to lock <0x00000000d6144cd0> (a java.lang.String)
- locked <0x00000000d6144c98> (a java.lang.String)
at java.lang.Thread.run(Thread.java:745)
Found 1 deadlock.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?