JUC并发编程
JUC并发包API包介绍
java.util.concurrent:
- 并发与并行的不同?
- 并行,如同,秒杀一样,多个线程访问同一个资源
- 并行,一堆事情一块去做,如同一遍烧热水,一个拆方便面包装
- java.util.concurrent.atomic
- AtomicInteger原子性引用
- java.util.concurrentlocks
- Lock接口
- ReentrantLock可重入锁
- ReadWriteLock读写锁
JVM(Java Memory Model)
JMM是指java内存模型,不是JVM,不是所谓的栈、堆、方法区。
每个java线程都有自己的工作内存。操作数据,首先从主内存中读,得到一份拷贝,操作完毕后再写回到主内存。
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方成为栈空间);工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储再主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须再工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成
JMM可能带来可见性、原子性和有序性问题。
所谓可见性,就是某个线程对主内存内容的更改,应该立刻通知到其他线程。
所谓原子性,是指一个操作是不可分割的,不能执行到一半,就不执行了。
所有有序性,就是指令是有序的,不会被重排。
volatile关键字
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r9YjYGWt-1630068822029)(E:\笔记\JUC\image-20210821214433490.png)]
volatile关键字是Java提供的一种轻量级同步机制。
- 它能够保证可见性和有序性
- 但是不能保证原子性
- 禁止指令重排
可见性
class MyData {
int number = 0;
//volatile int number = 0;
public void setTo60() {
this.number = 60;
}
}
public class VolatileDemo {
public static void main(String[] args) {
volatileVisibilityDemo();
}
//volatile可以保证可⻅性,及时通知其它线程主物理内存的值已被修改
private static void volatileVisibilityDemo() {
System.out.println("可⻅性测试");
MyData myData = new MyData();
//启动⼀个线程操作主内存中的共享数据 number
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t 执⾏");
//更新number值
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
myData.setTo60();
System.out.println(Thread.currentThread().getName()+"\t 更新
nunber值"+ myData.number);
}, "ThreadA").start();
while (myData.number == 0){
// main线程中number的数据出现死循环,说明读取到的内容⼀直为0
System.out.println(Thread.currentThread().getName());
}
//main线程
System.out.println(Thread.currentThread().getName()+"\t main线程中
获取number值"+ myData.number);
}
}
MyData类是资源类,一开始number变量没有用volatile修饰,所以程序运行的结果是:
可见性测试
ThreadA 执行
ThreadA 更新number值: 60
虽然一个线程把number修改了60,但是main线程持有的仍然是最开始的0,所以一直循环,程序不会结束。
如果对number添加了volatile修饰,运行结果是:
可见性测试
ThreadA 执行
ThreaA 更新number值: 60
main main获取number值: 60
可见某个线程对number的修改,会立刻反映到主内存上。
原子性
原子性指的是什么意思?
不和分割,完整性,也即某个线程正则做某个具体业务时,中间不可以被加塞或者被分割。需要整体完整,要么同时成功,要么同时失败。
class MyData {
//int number = 0;
volatile int number = 0;
public void setTo60(){
this.number = 60;
}
public void addPlusPlus(){
number++;
}
}
public class VolatileDemo {
public static void main(String[] args) {
//volatileVisibilityDemo();
atomicDemo();
}
private static void atomicDemo() {
/*
原⼦性的测试
需求:启动20个线程,每个线程执⾏1000次 number++操作, 问最终20个线程执⾏
完毕后,number值为多少?
*/
MyData myData = new MyData();
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
myData.addPlusPlus(); //number++
}
}, String.valueOf(i)).start();
}
//保证20个线程都执⾏完毕
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println("最终20个线程执⾏完毕后,number值为多少 = " +
myData.number);
}
}
volatile并不能保证操作的原子性。这是因为,比如一条number++的操作,会形成3条指令。
javap -c 包名。类名
javap -c MyData
public void addPlusPlus();
code:
0: aload_0
1: dup
2: getfield #2 // Field number:I //读
5: iconst_1 //++常量1
6: iadd //加操作
7: putfield #2 // Field number:I //写操作
10: return
假设有3个线程,分别执行number++,都先从主内存中拿到最开始的值,number=0,然后三个线程分别进行操作。假设线程0执行完毕,number=1,也立刻通知到了其他线程,但是此时线程1,2已经拿到了number=0,所以结果就是写覆盖,线程1,2将number变成1.
解决的方式就是:
- 对addPlusPlus()方法枷锁。
- 使用java.utl.concurrent.AtomicInteger类。
class MyData {
//int number = 0;
volatile int number = 0
//原⼦类对象
AtomicInteger atomicInteger = new AtomicInteger();
public void setTo60(){
this.number = 60;
}
public void addPlusPlus(){
number++;
}
public void addAtomic(){
// ++操作
atomicInteger.incrementAndGet();
}
}
public class VolatileDemo {
public static void main(String[] args) {
//volatileVisibilityDemo();
atomicDemo();
}
private static void atomicDemo() {
/*
原⼦性的测试
需求:启动20个线程,每个线程执⾏1000次 number++操作, 问最终20个线程执⾏
完毕后,number值为多少?
*/
MyData myData = new MyData();
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
myData.addPlusPlus(); //number++
myData.addAtomic(); // atomicInteger++
}
}, String.valueOf(i)).start();
}
//保证20个线程都执⾏完毕
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println("最终20个线程执⾏完毕后,number值为多少 = " +
myData.number)
System.out.println("最终20个线程执⾏完毕后,atomicInteger值为多少 = " +
myData.atomicInteger);
}
}
结果:可见,由于volatile不能保证原子性,出现了线程重复写的问题,最终结果比20000小。而AtomicInteger可以保证原子性。
原子性测试
main int类型最终number值: 17751
main AtomicInteger类型最终number值: 20000
有序性案例
计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排,一般分为以下三种:
单线程环境里面确保程序最终指向结果和代码顺序执行的结果一致;
处理器在进行重排序时必须要考虑指令直接的数据依赖性;
多线程环境中线程交替执行,由于编译器优化重排的存在,俩个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
volatile可以保证有序性,也就是防止指令重排
所谓指令重排序,就是出于优化考虑,CPU执行指令的顺序跟程序员自己编写的顺序不一致。就好比一份试卷,题号是老师规定的,是程序员规定的,但是考生(CPU)可以先做选择,也可以先做填空。
采用volatile可实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象。
为什么volatile可实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象?
我们先来了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,volatile底层就是用CPU的内存屏障指令来实现的,它有俩个作用
- 一个是保证特定操作的顺序性
- 二是保证变量的可见性。
对volatile变量进行写操作时:会在写操作后加入一条store屏障指令,工作内存中的共享变量值刷新回到主内存。
对Volatile变量进行读操作时:会在读操作前加入一条load屏障指令,从主内存中读取共享变量。
由于编译器和处理器都能够执行指令重排优化。所以如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memor Barrier指令重排序,也就是说通过插入内存屏障可以禁止在内存屏障前后的指令进行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读到这些数据的最新版本。
哪些地方用到过volatile?
单例模式的安全问题
-
传统
-
package example; public class SingleonDemo { private static SingleonDemo instance = null; private SingleonDemo(){ System.out.println(Thread.currentThread().getName()+"构造方法执行了"); } public static SingleonDemo getInstance(){ if (instance == null){ instance = new SingleonDemo(); } return instance; } public static void main(String[] args) { System.out.println(SingleonDemo.getInstance() == SingleonDemo.getInstance()); System.out.println(SingleonDemo.getInstance() == SingleonDemo.getInstance()); System.out.println(SingleonDemo.getInstance() == SingleonDemo.getInstance()); } }
-
改为多线程操作
package example; public class SingleonDemo { private static SingleonDemo instance = null; private SingleonDemo(){ System.out.println(Thread.currentThread().getName()+"构造方法执行了"); } public static SingleonDemo getInstance(){ if (instance == null){ instance = new SingleonDemo(); } return instance; } public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(() -> { SingleonDemo.getInstance(); },Thread.currentThread().getName()).start(); } } }
-
调整后,采用常见的DCL(Double Check Lock)双端检查模式加了同步,但是有线程安全问题。
package example; public class SingleonDemo { private static SingleonDemo instance = null; private SingleonDemo(){ System.out.println(Thread.currentThread().getName()+"构造方法执行了"); } public static SingleonDemo getInstance(){ if (instance == null){ synchronized (SingleonDemo.class){ if (instance == null){ instance = new SingleonDemo(); } } } return instance; } public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(() -> { SingleonDemo.getInstance(); },Thread.currentThread().getName()).start(); } } }
这个漏洞毕竟tricy,很难捕捉,但是是存在的。instance=new SingletonDemo();可以大致分为三步
instance = new SingletonDemo(); public static thread.SingletonDemo getInstance(); Code: 0: getstatic #11 // Field instance:Lthread/SingletonDemo; 3: ifnonnull 37 6: ldc #12 // class thread/SingletonDemo 8: dup 9: astore_0 10: monitorenter 11: getstatic #11 // Field instance:Lthread/SingletonDemo; 14: ifnonnull 27 17: new #12 // class thread/SingletonDemo 步骤1 20: dup 21: invokespecial #13 // Method "<init>":()V 步骤2 24: putstatic #11 // Field instance:Lthread/SingletonDemo;步 骤3 底层Java Native Interface中的C语⾔代码内容,开辟空间的步骤 memory = allocate(); //步骤1.分配对象内存空间 instance(memory); //步骤2.初始化对象 instance = memory; //步骤3.设置instance指向刚分配的内存地址,此时instance != null
剖析:
在多线程的环境下,由于有指令重排序的存在,DCL(双端检锁)机制不一定线程安全,我们可以加入volatile可以禁止指令重排。
原因在与某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化。
memory = allocate();//步骤一,分配对象内存空间 instance(memory);步骤二,初始化对象 insatance = memory;//步骤三,设置instance指向刚分配的内存地址,此时instance != null
步骤二和步骤三不存在数据依赖关系,而且无论重排前还是重排后,程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。
memory = allocate(); //步骤1. 分配对象内存空间 instance = memory; //步骤3.设置instance指向刚分配的内存地址,此时instance != null,但是对象还没有初始化完成! instance(memory); //步骤2.初始化对象
但是指令重排只会保证串行语义的一致性(单线程),并不关心多线程语义一致性。所以,当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。
如果发送指令重排,那么:
- 此时内存以及分配,那么instance=memory不为null
- 碰巧,若遇到线程此时挂起,那么instance(memory)还未执行,对象还未初始化。
- 导致了instance!=null,所以俩次判断都跳过,最后返回的instance没有内容,还没有初始化
解决办法就是对singletondemo对象添加上volatile关键字,禁止指令重排。
-
CAS
看下面代码,此时number前面是加了volatile关键字修饰的,volatile不保证原子性,那么使用AtomicInteger是如何保证原子性的?这里的原来是什么?CAS
class MyData {
volatile int number = 0;
AtomicInteger atomicInteger=new AtomicInteger();
public void addPlusPlus(){
number++;
}
public void addAtomic(){
atomicInteger.getAndIncrement();
}
public void setTo60() {
this.number = 60;
}
}
CAS的全称为Compare-And-Swap,比较并交换,是一种很重要的同步思想。它是一条CPU并发原语。
它的功能是判断主内存某个位置的值是否为跟期望值一样,相同就进行修改,否则一直重试,直到一致为止。这个过程是原子的。
package example;
import java.util.concurrent.atomic.AtomicInteger;
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
System.out.println(atomicInteger.compareAndSet(5,2000)+"\t 当前数据值:"+ atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(5,1024)+"\t 当前数据值:"+ atomicInteger.get());
}
}
第一次修改,期望值为5,主内存也为5,修改成功,为2000.第二次修改,期望值为5,主内存为2000,修改失败,需要重新获取主内存的值。
查看AtomicInteger.getAndIncrement()方法,发现其没有加synchronized也实现了同步。
CAS并发原语体现在JAVA语言中就是sum.misc.Unsafe类中的各个方法。看方法源码,调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖与硬件的功能,通过它实现了原子操作。再次调用,由于CAS是一种系统原语,原语属于操作系统用于范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。
CAS底层原理
AtomicInteger内部的重要参数
- Unsafe
是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后面,基于该类可以直接操作特定内存的数据。Unsafe类存在于sum.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖与Unsafe类的方法。
注意Unsafe类中的所有方法都是natice修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应认为 - 变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。
- 变量value用volatile修饰,保证了多线程直接的内存可见性。
AtomicInteger.getAndIncrement()调用了Unsafe.getAndAddInt()方法。Unsafe类中的方法都是native的,用来像C语言一样从底层操作内存。
CAS缺点
CAS实际上是一种自旋锁:
- 一直循环,开销比较大,可以看到getAndAddInt方法执行时,有个do while,如果失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU代来很大的开销。
- 对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是,对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
- 引出了ABA问题
CAS会导致“ABA问题”
高频面试题 |
---|
1.原子类AtomicInteger的ABA问题?原子更新引用你知道么? |
2.ArrayList是线程不安全的,请编写一个不安全的案例并给出解决方案 |
3.公平锁/非公平锁/可重入锁/递归锁/自旋锁?请手写一个自旋锁 |
4.CountDowLath/CyclicBarrier/Semaphore使用过么? |
5.阻塞队列知道么? |
6.线程池勇敢吗?ThreadPoolExecutor谈谈你的理解?生产上你如何设置合理参数 |
7.死锁编码及定位分析、 |
所谓ABA问题,就是CAS算法实现需要取出内存中某时刻的数据并在当下时刻比较并替换,这里存在一个时间差,那么这个时间差可能代理意向不到的问题。
有这样的需求,比如CAS,只注重头和尾,只要收尾一致就接受
但是有的需求,还看重过程,中间不能发生任何修改,这就引出了AtomicReference原子引用。
AtomicReference原子引用
AtomicInteger对整数进行原子操作,如果是一个POJO呢?可以用AtomicReference来包装这个POJO,使其操作原子化。
public class AtomicReferenceDemo {
public static void main(String[] args) {
User user1 = new User("Jack",25);
User user2 = new User("Tom",21);
AtomicReference<User> atomicReference = new AtomicReference<>();
atomicReference.set(user1);
System.out.println(atomicReference.compareAndSet(user1,user2)+"\t"+atomic
Reference.get()); // true
System.out.println(atomicReference.compareAndSet(user1,user2)+"\t"+atomic
Reference.get()); //false
}
}
ABA问题的解决(AtomicStampedReference类似于时间戳)
使用AtomicStampedReference类可以解决ABA问题。这个类维护了一个版本号 Stamp,在进行CAS操作的时候,不仅要比较当前值,还要比较版本号。只有俩者都相等,才执行更新操作。
解决ABA问题的关键方法:
compareAndSet(V expectedReference,V newReference,int expctedStamp,int newStamp)
V expectedReference,预期值引用
V newReference,新值引用
int expctedStamp预期值时间戳
int newStamp,新值时间戳
package example;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABADemo {
public static void main(String[] args) {
AtomicReference<Integer> atomicReference = new AtomicReference<>
(100);
AtomicStampedReference<Integer> atomicStampedReference = new
AtomicStampedReference<>(100, 1);
System.out.println("======ABA问题的解决======");
new Thread(() -> {
//获取当前数据的版本号
int stamp = atomicStampedReference.getStamp();
System.out.println("ThreadA===第⼀次获取的版本号====" + stamp);
System.out.println("ThreadA===第⼀次获取的数据 == "+atomicStampedReference.getReference());
//TODO 待完成
try {
TimeUnit.SECONDS.sleep(4);
} catch
(InterruptedException e) {
e.printStackTrace();
}
//CAS操作
boolean rssult = atomicStampedReference.compareAndSet(100,
2021,
stamp,
stamp + 1
);
System.out.println("CAS⽐较的结果rssult===" + rssult);
System.out.println("====ThreadA===第⼆次获取的版本号 == "+atomicStampedReference.getStamp());
System.out.println("====ThreadA===第⼆次获取的数据 == "+atomicStampedReference.getReference());
}, "ThreadA").start();
new Thread(() -> {
//获取当前数据的版本号
System.out.println("====ThreadB===第⼀次获取的版本号 == "+atomicStampedReference.getStamp());
System.out.println("====ThreadB===第⼀次获取的数据 == "+atomicStampedReference.getReference());
//CAS操作
try {
TimeUnit.SECONDS.sleep(1);
} catch
(InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(
100,
111,
atomicStampedReference.getStamp(),
atomicStampedReference.getStamp() + 1
);
System.out.println("====ThreadB===第⼆次获取的版本号 == "+atomicStampedReference.getStamp());
System.out.println("====ThreadB===第⼆次获取的数据== "+atomicStampedReference.getReference());
//CAS操作
try {
TimeUnit.SECONDS.sleep(1);
} catch
(InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(
111,
100,
atomicStampedReference.getStamp(),
atomicStampedReference.getStamp() + 1
);
});
}
}
集合类不安全问题
ArrayList与CopyOnWriteArrayList
ArrayList不是线程安全类,在多线程同事写的情况下,会抛出java.util.ConcurrentModificationException异常。
package example;
import java.util.ArrayList;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
public class ArrayListDemo {
public static void main(String[] args) {
//List<String> list = new ArrayList<>();
//List<String> list = new Vector<>();
//List<String> safeList = Collections.synchronizedList(list);
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(Thread.currentThread().getName());
}).start();
}
}
}
解决方法:
- 使用Vector
- 使用Collections.synchronizedList()转换成线程安全类
- 使用java.concurrent.CopyOnWriteArrayList(推荐)
CopyOnWriteArrayList
这是JUC的类,通过写时复制来实现读写分离。比如其add()方法,就是先复制一个新数组,长度为原数组长度+1,然后将新数组最后一个元素设为添加的元素。
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();
}
}
写时复制:
CopyOnrite容器即写时复制的容器。
原理:
往一个容器添加元素的时候,不直接往当前容器Object[]添加,而是先将当前容器Object[]进copy
复制出一个新的容器Object[] newElements,然后新的容器Object[] newElements里添加元素,添加完元素之后,再将原容器的引用指向新的容器setArray(newElements).这样做的好处是可以对CopyOnWrite容器进行并发的读,
而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWriter容器也是一种读写分离的思想,读和写不同的容器
Set与CopyOnWriteArraySet
private static void setNoSafe(){
Set<String> set = new CopyOnWriteArraySet<>();
for(int i=1;i<=30;i++){
new Thread(() -> {
set.add(UUID.randomUUID().toString().substring(0,8));
},String.valueOf(i).start());
}
}
HashSet和HashMap
HashSet源码查看,看默认构造方法,看add()方法
HashSet底层是用HashMap实现的。既然是用HashMap实现的,那HashMap.put()需要传俩个参数,而HashSet.add()只传一个参数,实际上HashSet.add()就是调用的HashMap.put(),只不过Value被写死了,是一个private static final Object对象。
跟List类似,HashSet和TreeSet都不少线程安全的,与之对应的有CopyOnWriteSet这个线程安全类。这个类底层维护了一个CopyOnWriteArrayList数组。
private final CopyOnWriteArrayList<E> al;
public CopyOnWriteArraySet() {
al = new CopyOnWriteArrayList<E>();
}
Map与ConcurrentHashMap
- HashMap源码查看,看默认构造方法,频繁扩容影响效率,看构造方法(初始容量,加载因子)
- Map集合底层是Node数组{数组,链表,红黑树},看Node源码,讲Map存储数据的原理。
HashMap不是线程安全的,HashTable是线程安全的,但是跟Vectory类似,太重量级。所以也有类似CopyOnWriteMap,只不过叫ConcurrentHashMap.
package example;
import java.util.HashMap;
import java.util.UUID;
public class MapNotSafe {
private static void mapNoSafe(){
HashMap<String, String> map = new HashMap<>();
for (int i = 0; i < 30; i++) {
new Thread(() -> {
map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0,8));
System.out.println(Thread.currentThread().getName() + map);
},String.valueOf(i)).start();
}
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pou9MZsc-1630068822031)(E:\笔记\JUC\5c3246e9745944fef297e947f25b5d8-16295980645971.png)]
人工窗口排队购票
package example;
class Ticket{
private int number = 30;
public synchronized void saleTicket(){
if (number > 0){
System.out.println(Thread.currentThread().getName()+number--+"还剩下"+number);
}
}
}
public class SaleTicketDemo {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(()->{
for (int i = 0; i < 30; i++) {
ticket.saleTicket();
}
},"窗口A").start();
new Thread(()->{
for (int i = 0; i < 30; i++) {
ticket.saleTicket();
}
},"窗口B").start();
new Thread(()->{
for (int i = 0; i < 30; i++) {
ticket.saleTicket();
}
},"窗口C").start();
}
}
公平锁非公平锁(火车站人工窗口排队购票)
Lock lock = new ReentrantLock(true);
概念:所谓公平锁,就是多个线程按照申请锁的顺序来获取锁,类似排队,先到先得。而非公平锁,则是多个线程抢夺锁,会导致优先级反转或饥饿现象。
区别:
- 公平锁在获取锁时先查看此锁维护的等待队列,为空或者当前线程是等待队列的队首,则直接占有锁,否则插入到等待队列,FIFO原则。
- 非公平锁比较粗鲁,上了直接先尝试占有锁,失败则采用公平锁方式。非公平锁的优点是吞吐量比公平锁更大。
synchronized和juc.ReentrantLock默认都是非公平锁。ReetrantLock在构造的时候传入true则是公平锁。
package example;
import jdk.nashorn.internal.ir.CallNode;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Ticket{
private int number = 30;
Lock lock = new ReentrantLock(false);
public synchronized void saleTicket(){
//获取锁
lock.lock();
try {
if (number > 0){
System.out.println(Thread.currentThread().getName()+number--+"还剩下"+number);
}
}finally {
// 释放锁
lock.unlock();
}
}
}
public class SaleTicketDemo {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(()->{
for (int i = 0; i < 30; i++) {
ticket.saleTicket();
}
},"窗口A").start();
new Thread(()->{
for (int i = 0; i < 30; i++) {
ticket.saleTicket();
}
},"窗口B").start();
new Thread(()->{
for (int i = 0; i < 30; i++) {
ticket.saleTicket();
}
},"窗口C").start();
}
}
可重入锁/递归锁
可重入锁又叫递归锁,指的同一个线程在外层方法获得锁时,进入内层方法会自动获取锁。也就是,线程可以进入任何一个它以及拥有锁的代码块。比如method01方法里面又method02方法,俩个方法都有同一把锁,得到了method01的锁,就自动得到了method02的锁。
就像有了家门的锁,厕所、书房、厨房就为你敞开了一样。可重入锁可以避免死锁的问题。
package example;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReetrantLockDemo {
public static void main(String[] args) {
}
}
class PhonePlus{
public synchronized void sendEmail(){
System.out.println(Thread.currentThread().getName()+"sendEmai");
sendMessage();
}
private synchronized void sendMessage() {
System.out.println(Thread.currentThread().getName()+"sendMessage");
}
Lock lock = new ReentrantLock();
public void method1(){
lock.lock();
try{
System.out.println(Thread.currentThread().getName()+"method1");
method2();
}finally {
lock.unlock();
}
}
public void method2(){
lock.lock();
try{
System.out.println(Thread.currentThread().getName()+"method1");
method2();
}finally {
lock.unlock();
}
}
}
锁的配对
锁之间要配对,加了几把锁,最后就要接口几把锁,下面的代码编译和运行都没有任何问题,但是锁的数量不匹配会导致死循环。
lock.lock();
lock.lock();
try{
someAction();
}finally{
lock.unlock();
}
自旋锁
所谓自旋锁,就是尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取。自己在那儿一致循环获取,就像自旋一样。这样的好处是减少线程切换的上下文开销,缺点是会消耗CPU。CAS底层的getAndAddInt就是自旋锁思想
package example;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
/**
* 实现一个自旋锁
* 自旋锁好处:循环比较获取直到成功为止,没有类似wait的阻塞。
* 通过CAS操作完成自旋锁,A线程先尽量调用myLock方法自己持有锁5秒钟。
* B随后进来后发现当前线程持有锁,不是null,所以只能通过自旋等待,直到A释放锁后B随后抢到
*/
class MyLock{
AtomicReference<Thread> atomicReference = new AtomicReference<Thread>();
public void lock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName()+"come in...");
//判断当前是否有人使用
while (!atomicReference.compareAndSet(null,thread)){
System.out.println(Thread.currentThread().getName()+"====");
}
}
public void unlock(){
Thread thread1 = Thread.currentThread();
atomicReference.compareAndSet(thread1,null);
System.out.println(Thread.currentThread().getName()+"===unlock");
}
}
public class SpinLockDemo {
public static void main(String[] args) {
MyLock myLock = new MyLock();
new Thread(()->{
myLock.lock();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
myLock.unlock();
},"AA").start();
new Thread(()->{
myLock.lock();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
myLock.unlock();
},"BB").start();
}
}
读写锁/独占/共享
读锁是共享的,写锁是独占的。juc.ReetrantLock和synchronized都是独占锁,独占锁就是一个只能被一个线程所持有。有的时候,需要读写分离,那么就要引入读写锁,
即juc.ReentrantReadWriteLock.
独占锁:指该锁一次只能被一个线程所持有。对ReentrantLock和Synchronize而言都是独占锁
共享锁:指该锁可以被多个线程所持有
对ReentrantReadWriteLock其读锁是共享锁,其写锁是独占锁。
读锁的共享锁可保证并发读是非常高效的,读写、写读、写写的过程是互斥的。
比如缓存,就需要读写锁来控制。缓存就是一个键值对,一下Demo模拟了缓存的读写操作,读的get方法使用了ReentrantReadWriteLock.ReadLock(),写的put方法使用了
ReentrantReadWriteLock.ReadLock()。这样避免了写被打断,实现了多个线程同时读。
package example;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 多个线程同事读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行。
* 但是,如果有一个线程想去写共享资料,就不应该再有其他线程可以对该资源进行读或写
* 小总结:
* 读-读 能共存
* 读-写 不能共存
* 写-写 不能共存
*/
class Cache{
private Map<String,Object> map = new ConcurrentHashMap<String,Object>();
private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
// 哪个线程来 签字
public void put(String key,Object value){
readWriteLock.writeLock().lock();
try{
System.out.println("+==正在签字"+value);
try {
TimeUnit.SECONDS.sleep(1);
System.out.println("签字完成");
} catch (InterruptedException e) {
e.printStackTrace();
}
}finally {
readWriteLock.writeLock().unlock();
}
}
// 获取指定线程 对应的value
public Object get(String key){
Object result = null;
readWriteLock.readLock().lock();
try{
System.out.println("正在看大家的签字:");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
result = map.get("线程"+key);
System.out.println("看完");
}finally {
readWriteLock.readLock().unlock();
}
return result;
}
}
public class ReadWriteLockDemo {
public static void main(String[] args) {
Cache cache = new Cache();
for (int i = 0; i < 5; i++) {
final int temp = i;
new Thread(() -> {
cache.put("线程名"+temp,"数据"+temp);
},String.valueOf(i)).start();
}
for (int i = 0; i < 5; i++) {
final int temp = i;
new Thread(() -> {
cache.get("线程名"+temp);
},String.valueOf(i)).start();
}
}
}
并行编程常用辅助类
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GRBRefFT-1630068822032)(E:\笔记\JUC\image-20210822112726991-16296028488312.png)]
CountDownLatch(倒计时门闩)
CountDownLatch内部维护了一个计数器,只有当计数器==0时,某些线程才会停止阻塞,开始执行。
CountDownLatch主要有俩个方法,countDown()来让计数器-1,await()来让线程阻塞。
当count==0时,阻塞线程自动唤醒。
案例:开门:main线程是班长,6个线程是学生,只有6个线程运行完毕,都离开之后,main线程才会关门。
package example;
import java.util.concurrent.CountDownLatch;
/**
* CountDownLatch的使用
* CountDownLatch主要有俩个方法,当一个或多个线程调用await方法时,这些线程会阻塞。
* 其他线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞),
* 当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行
*/
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
System.out.println("同学离开了教室");
countDownLatch.countDown();
}
countDownLatch.await();
System.out.println(Thread.currentThread().getName()+"关门");
}
}
CyclicBarrier(篱栅)
CountDownLatch是减,而CyclicBarrier是加,理解了CountDownLatch,CyclicBarrier就很容易。比如召集了7个龙珠召唤神龙。
package example;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(20, () -> {
System.out.println("go go go");
});
for (int i = 0; i < 20; i++) {
final int temp = i;
new Thread(() -> {
System.out.println("第"+temp+"个人来了");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
Semaphore(信号灯)
CountDownLatch的问题是不能复用。比如count=2,那么减到0,就不能继续操作了。
而Semaphore可以解决这个问题,比如6辆车3个停车位,对于CountDownLatch只能停3辆车,而Semaphore可以停6辆,车位空出来后,其它车可以占用,这就涉及到了Semaphore.accquire()和Semaphore.release()方法。
package example;
import java.util.concurrent.Semaphore;
/**
* 在信号灯上我们定义俩种操作:
* acquire(获取)当一个线程调用acquire操作时,他要么同成功获取信号量(信号量减1),
* 要么一致等待下去,直到有线程释放信号,或超市。
* release(释放)时间上会将信号量加1,然后唤醒等待的线程。
*
* 信号量主要用于俩个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制
*/
public class SemaphoreDemo {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 7; i++) {
final int temp = i;
new Thread(()->{
// 占用资源
try{
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"抢到了车位。。。");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println(Thread.currentThread().getName()+"释放了车位");
semaphore.release();+
}
},String.valueOf(i)).start();
}
}
}
阻塞队列
概念:
-
在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起1线程又会自动被唤醒。
-
阻塞队列是一个队列,在数据结构中起的作用如下:
当队列是空的,从队列中获取(Take)元素的操作将会被阻塞
当队列是满的,从队列中添加(Put)元素的操作将会被阻塞
试图中空的队列中获取元素的线程将会被阻塞,直到其他线程往空的队列插入新的元素
试图向已满的队列中添加新元素的线程将会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空,使队列变得空闲起来后并后续新增
好处:阻塞队列不用手动控制什么时候该被阻塞,什么时候该被唤醒,简化了操作。
体系:Collection->Queue->BlockingQueue->七个阻塞队列实现类。
类名 | 作用 |
---|---|
ArrayBlockingQueue | 由数组结构构成的有界阻塞队列 |
LinkedBlocingQueue | 由链表结构构成的有界(但默认值为Integer.MAX_VALUE)阻塞队列 |
PriorityBlocingQueue | 支持优先级排序的无界阻塞队列 |
DelayQueue | 使用优先级队列实现的延迟无界阻塞队列 |
SynchronousQueue | 不存储元素的阻塞队列,也即单个元素的队列 |
LinkedTransferQueue | 由链表构成的无界阻塞队列 |
LinkedBlockingDeque | 由链表构成的双向阻塞队列 |
粗体标记的三个用的比较多,许多消息中间件底层就是用它们实现的。
需要注意的是LinkedBlockingQueue虽然是有界的,但有个巨坑,其默认是Integer.MAX_VALUE,高达21亿,一般情况下内存早爆了(在线程池的ThreadPoolExecutor有体现)。
API:
- 抛出异常是指当队列满时,再次插入会抛出异常;
- 返回布尔是指当队列满时,再次插入会返回false,阻塞是指当队列满时,再次插入会被阻塞,直到队列取出一个元素,才能插入。超时是指当一个时限过后,才会插入或者取出。
方法类型 | 抛出异常 | 返回布尔 | 阻塞 | 超时 |
---|---|---|---|---|
插入 | add(E e) | offer(E e) | put(E e) | offer(E e ,Time,TimeUnit) |
取出 | remove() | poll() | take() | poll(Time,TimeUnit) |
队首 | element() | peek() | 无 | 无 |
package example;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
public class BlockingQueueDemo {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
// addAndRemove(queue);
// offerAndPoll(queue);
// putAndTake(queue);
outOfTime(queue);
}
private static void outOfTime(BlockingQueue<String> queue) throws InterruptedException {
System.out.println(queue.offer("aa==",2, TimeUnit.SECONDS));
System.out.println(queue.offer("bb==",2, TimeUnit.SECONDS));
System.out.println(queue.offer("cc==",2, TimeUnit.SECONDS));
// System.out.println(queue.offer("dd==",2, TimeUnit.SECONDS));
System.out.println(queue.poll(3,TimeUnit.SECONDS));
System.out.println(queue.poll(3,TimeUnit.SECONDS));
System.out.println(queue.poll(3,TimeUnit.SECONDS));
System.out.println(queue.poll(3,TimeUnit.SECONDS));
}
private static void putAndTake(BlockingQueue<String> queue) throws InterruptedException {
queue.put("aaa");
queue.put("bbb");
queue.put("ccc");
// queue.put("ddd");
System.out.println(queue.take());
System.out.println(queue.take());
System.out.println(queue.take());
System.out.println(queue.take());
}
private static void offerAndPoll(BlockingQueue<String> queue) {
System.out.println(queue.offer("aa"));
System.out.println(queue.offer("bb"));
System.out.println(queue.offer("cc"));
// System.out.println(queue.offer("dd"));
// System.out.println(queue.peek());
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll());
}
private static void addAndRemove(BlockingQueue<String> queue) {
queue.add("a");
queue.add("b");
queue.add("c");
// queue.add("d");
// System.out.println(queue.element());
System.out.println(queue.remove());
System.out.println(queue.remove());
System.out.println(queue.remove());
// System.out.println(queue.remove());
}
}
SynchronousQueue
队列只有一个元素,如果想插入多个,必须等队列元素取出后,才能插入,只能有一个坑位,用来插一个,详见SynchronousQueueDemo.
package example;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;
public class SynchronousQueueDemo {
public static void main(String[] args) {
BlockingQueue<String> blockingQueue = new SynchronousQueue<String>();
new Thread(() -> {
try{
System.out.println(Thread.currentThread().getName());
blockingQueue.put("1");
System.out.println(Thread.currentThread().getName());
blockingQueue.put("2");
System.out.println(Thread.currentThread().getName());
blockingQueue.put("3");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"师傅").start();
new Thread(()->{
try {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"\t "+blockingQueue.take());
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"\t "+blockingQueue.take());
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"\t "+blockingQueue.take());
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"\t "+blockingQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"顾客").start();
}
}
阻塞队列的应用–生成者消费者
传统模式
传统模式使用Synchronized来进行操作
package example;
class MyCondition{
private int number = 0;
// 老版写法
public synchronized void increment() throws InterruptedException {
// 1.判断
if (number != 0){
this.wait();
}
// 干活
number++;
System.out.println(Thread.currentThread().getName()+"\t"+number);
// 通知
this.notifyAll();
}
public synchronized void decrement() throws InterruptedException {
// 1.判断
if (number == 0){
this.wait();
}
// 2.干活
number--;
System.out.println(Thread.currentThread().getName()+"\t"+number);
// 3.通知
this.notifyAll();
}
}
public class ProdConsumerDemo {
public static void main(String[] args) {
MyCondition myCondition = new MyCondition();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
myCondition.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"师傅").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
myCondition.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"顾客").start();
}
}
生产者消费者防止虚假唤醒
package example;
class MyCondition{
private int number = 0;
// 老版写法
public synchronized void increment() throws InterruptedException {
// 1.判断
while (number != 0){
this.wait();
}
// 干活
number++;
System.out.println(Thread.currentThread().getName()+"\t"+number);
// 通知
this.notifyAll();
}
public synchronized void decrement() throws InterruptedException {
// 1.判断
while (number == 0){
this.wait();
}
// 2.干活
number--;
System.out.println(Thread.currentThread().getName()+"\t"+number);
// 3.通知
this.notifyAll();
}
}
public class ProdConsumerDemo {
public static void main(String[] args) {
MyCondition myCondition = new MyCondition();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
myCondition.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"师傅").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
myCondition.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"师傅2").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
myCondition.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"顾客").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
myCondition.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"顾客2").start();
}
}
新版生成者消费者写法ReentrantLock.Condition
package example;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
class MyCondition{
private int number = 0;
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
// 老版写法
public void increment() throws InterruptedException {
lock.lock();
try {
// 1.判断
while (number != 0){
condition.await();
}
// 干活
number++;
System.out.println(Thread.currentThread().getName()+"\t"+number);
// 通知
// this.notifyAll();
condition.signalAll();
}finally {
lock.unlock();
}
}
public void decrement() throws InterruptedException {
lock.lock();
try {
// 1.判断
while (number == 0){
condition.await();
}
// 2.干活
number--;
System.out.println(Thread.currentThread().getName()+"\t"+number);
// 3.通知
// this.notifyAll();
condition.signalAll();
}finally {
lock.unlock();
}
}
}
public class ProdConsumerDemo {
public static void main(String[] args) {
MyCondition myCondition = new MyCondition();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
myCondition.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"师傅").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
myCondition.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"师傅2").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
myCondition.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"顾客").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
myCondition.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"顾客2").start();
}
}
精准通知顺序访问
package example;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
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();
public void printc1() {
lock.lock();
try {
if (number != 1) {
c1.await();
}
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + i);
// 通知B,第二个
number = 2;
c2.signal();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
// B线程打印10次
public void printc2() throws InterruptedException {
lock.lock();
try {
if (number != 2) {
c2.await();
}
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + i);
}
// 通知c,第三个
number = 3;
c3.signal();
} finally {
lock.unlock();
}
}
//C线程打印15次
public void printc3() {
lock.lock();
try {
//1.判断
if (number != 3) {
c3.await();
}
//2.⼲活
for (int i = 0; i < 15; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i);
}
//3.通知 A, 第1个
number = 1;
c1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
/**
* 备注:多线程之间按顺序调用,实现A->B->C
* 三个线程启动,要求如下:
* A打印5次,B打印10次,c打印15次
* 接着
* A打印5次,B打印10次,C打印15次
* 来10轮
* 1.高内聚低耦合前提下,线程操作资源类
* 2.判断/干活/通知
* 3.多线程交互中,防止虚假唤醒(判断只能用while,不能用if)
* 4.标志位
*/
public class ConditionDemo {
public static void main(String[] args) {
ShareData shareData = new ShareData();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
shareData.printc1();
}
}, "A").start();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
try {
shareData.printc2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
shareData.printc3();
}
}, "C").start();
}
}
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来绑定多个条件,精确唤醒
阻塞队列模式生产者消费者
为什么需要BlockingQueue?
好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办好了,使用阻塞队列后就不需要手动加锁了。
在Concurrent包发布以前,在多线程环境下,我们每个程序员都必须去控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。
package example;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 阻塞队列的,生成者消费者
*/
class MyResource{
private volatile boolean flag = true;//默认开启,进行生成+消费
private AtomicInteger atomicInteger = new AtomicInteger();
ArrayBlockingQueue<String> blockingQueue = null;
public MyResource(ArrayBlockingQueue<String> blockingQueue){
this.blockingQueue = blockingQueue;
}
/*
生成包子
*/
public void myProd() throws InterruptedException {
while (flag){
String data = atomicInteger.incrementAndGet()+"";
boolean resultVale = blockingQueue.offer(data, 2L, TimeUnit.SECONDS);
if (resultVale){
System.out.println(Thread.currentThread().getName() +"插入队列"+ data+"成功" );
}else{
System.out.println(Thread.currentThread().getName()+"插入队列"+data+"失败");
}
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName()+"老板喊停了,Flage已经更新为flase,停止生产");
}
}
/**
* 购买包⼦
*/
public void myCons() throws InterruptedException {
while (flag){
//购买包⼦
String result = blockingQueue.poll(2L, TimeUnit.SECONDS);
if (null == result || "".equals(result)){
// 没有包⼦
System.out.println(Thread.currentThread().getName() + "\t 超过2秒钟没有购买到包⼦, 退出消费。。。");
return;
}
System.out.println(Thread.currentThread().getName() +"\t 购买 包⼦成功===" + result);
}
}
/**
* 老板喊停
*/
public void stop(){
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("老板喊停");
flag = false;
}
}
public class ProdConsBlockQueueDemo {
public static void main(String[] args) {
MyResource myResource = new MyResource(new ArrayBlockingQueue<String>(2));
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"生产线程启动");
try {
myResource.myProd();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"师傅A").start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"生产线程启动");
try {
myResource.myProd();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"师傅B").start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"生产线程启动");
try {
myResource.myProd();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"师傅C").start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"消费线程启动");
try {
myResource.myProd();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"顾客A").start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"消费线程启动");
try {
myResource.myProd();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"顾客B").start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"消费线程启动");
try {
myResource.myProd();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"顾客C").start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"消费线程启动");
try {
myResource.myProd();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"顾客D").start();
myResource.stop();
}
}
第一章 阻塞队列的应用–线程池
线程池基本概念
概念:线程池主要是控制运行线程的数量,将待处理任务放到等待队列,然后创建线程执行这些任务。如果超过了最大线程数,则等待。
为什么用线程池?
10年前单核CPU电脑,假的多线程,像马戏团小丑玩多个球,CPU需要来回切换。
现在是多核电脑,多个线程各自跑在独立的CPU上,不用切换效率高。
线程池的优点:
线程池做的工作只要是控制运行的线程数量,处理过程中将任务放入队列,然后再线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等侯,等其他线程执行完毕,再从队列中取出任务来执行。
线程池的主要特点为:线程复用;控制最大并发数;管理线程。
- 线程复用:不用一直new新线程,重复利用已经创建的线程来降低线程的创建和销毁开销,节省系统资源。
- 提高响应速度:当任务达到时,不用创建新的线程,直接利用线程池的线程。
- 管理线程:可以控制最大并发数,控制线程的创建等。
体系:
Executor->ExecutorService->AbstractExecutorService->ThreadPoolExecutor.
ThreadPoolExecutor是线程池创建的核心类。类似Arrays,Collection工具类,Executor也有自己的工具类Executors。
线程池三种常用创建方式
java中的线程池是通过Executor框架实现的。该框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类。
newFixedThreadPool线程池:
使用LinkedBlockingQueue实现,定长线程。
特点:执行长期任务性能好,创建一个线程池,一池有N个固定的线程,有孤独线程数的线程。
newSingleThreadExecutor线程池:
使用LinkedBlockingQueue实现,一池只有一个线程。
特点:一个任务一个任务的执行,一池一线程。
newCachedThredPool线程池:
使用SynchronousQueue实现,变长线程池。
特点:执行很多短期异步任务,线程池根据需要创建新线程,但在先前构建的线程可用时将重用他们。可扩容,遇强则强。
线程池代码演示
package example;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolDemo {
/**
* 线程池代码演示
*
*/
public static void main(String[] args) {
threadPoolTask(new ThreadPoolExecutor(2,
5,
1L,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy()));
}
private static void threadPoolTask(ThreadPoolExecutor threadPoolExecutor) {
try{
for (int i = 0; i < 10; i++) {
threadPoolExecutor.execute(() -> {
System.out.println(Thread.currentThread().getName());
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
threadPoolExecutor.shutdown();
}
}
}
线程池创建的七个参数
参数 | 意义 |
---|---|
corePoolSize | 线程池中的常驻核心线程数 |
maximumPollSize | 线程池中能够容纳同时指向的最大线程数,此值必须大于等于1 |
keepAliveTime | 多余的空闲线程的存活时间,当前池中线程数量超过corePoolSize时,当空闲时间达到keepAliveTime时,多余线程会被销毁直到只剩下corePoolSize个线程为止 |
unit | keepAliveTime存活时间的单位 |
workQueue | 任务队列,存放已提交但尚未执行的任务 |
threadFactory | 表示生成线程池中工作线程的线程工厂,用于创建线程,一般默认的即可 |
handler | 拒绝策略,表示当队列满了,并且工作线程大于等于线程池的最大线程数时,如何来拒绝请求执行的runnable的策略 |
线程池底层原理
流程:
-
在创建了线程池后,开始等待请求。
-
当调用execute()方法添加一个请求任务时,线程会做出如下判断:
- 如果正在运行的线程数量小于corePoolSize,那么马上创建核心线程运行执行这个任务;
- 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
- 如果这个时候等待队列已满,且正在运行的线程数量小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
- 如果这个时候等待队列已满,且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
3.当一个线程完成任务时,它会从等待队列中取出下一个任务来执行
4.当一个线程无事可做超过一定时间(keepAliveTime)后,线程会判断:
如果当前运行的线程数大于corePoolSize,那么这个非核心线程就会被停掉。当线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。
线程池的拒绝策略
当等待队列满时,且达到最大线程数,再有信任务到来,就需要启动拒绝策略。JDK提供了四种拒绝策略:
- AbortPolicy:默认的策略,直接抛出RejectedExecutionException异常,阻止系统正常运行。
- CallerRunsPolicy:既不会抛出异常,也不会终止任务,而是将任务返回给调用者,从而降低新任务的流量。
- DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交任务。
- DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种策略。
实际生产使用哪一个线程池?
**单一,可变,定长都不用!**原因就是FixedThreadPool和SingleThreadExecutor底层都是用LinkedBlockingQueue实现的,这个队列最大长度为Integer.MAX_VALUE,显然会导致OOM,所有实际生产一般自己通过ThreadPoolExecutor的7个参数,自定义线程池。
ExecutorService threadPool = new ThreadPoolExecutor(
2,
80*2,
1L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
自定义线程池参数选择
对于CPU密集型任务,最大线程数是CPU线程数+1.
对于IO密集型任务,尽量多配点,可以是CPU线程数*2,或者CPU线程数/(1-阻塞系数)。
IO密集型,即该任务需要大量的IO,即大量的阻塞。
再单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费再等待。
所有再IO密集型任务中使用多线程可以大大的加速程序运行,及时再单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。
IO密集型时,大部分线程都阻塞,故需要多配置线程数:
参考公式:CPU核数/(1-阻塞系数)阻塞系数再0.8~0.9直接,比如8核CPU: 8/1-0.9=80个线程数
死锁编码和定位
死锁:是指俩个或俩个以上的进程再执行过程中因争夺资源而造成的一种“相互等待的现象”,若无外力干涉,那它们将都无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因为争夺有限的资源而陷入死锁。
package example;
public class HoldLockThread implements Runnable {
private String lockA;
private String lockB;
public HoldLockThread(String lockA,String lockB){
this.lockA = lockA;
this.lockB = lockB;
}
@Override
public void run() {
synchronized (lockA){
System.out.println(Thread.currentThread().getName() + "自己持有"+lockA + "尝试获取"+ lockB);
synchronized (lockB){
System.out.println(Thread.currentThread().getName() + "自己持有"+lockB + "尝试获取"+ lockA);
}
}
}
public static void main(String[] args) {
String lockA = "lockA";
String lockB = "lockB";
new Thread(new HoldLockThread(lockA, lockB), "ThreadA").start();
new Thread(new HoldLockThread(lockB, lockA), "ThreadB").start();
}
}
死锁的定位
- jps指令:jps -l 可以查看运行的java进程。
- jstack指令: jstack pid可以查看某个java进程的堆栈信息,同时分析出死锁。
LockSupport详解
- 通过park()和unpark(thread)方法来实现阻塞和唤醒线程的操作
- LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程再任意位置阻塞,阻塞之后也有对应的唤醒方法。归根结底,LockSupport调用的Unsafe中的native代码。
- 官网解释:
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语
LockSupport类使用了一种名为Permit的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可,permit只有俩个值1和零,默认是零
可以把许可看出是一种(0,1)信号量(semaphore),但与Semaphore不同的是,许可的累加上限是1
阻塞方法:
- permit默认是0,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,park方法会被唤醒,然后会将permit再次设置为0并返回。
- static void park():底层是unsafe类native方法
- static void park(Object blocker)
唤醒方法(注意这个permit最多只能为1)
- 调用unpark(thread)方法后,就会将thread线程的许可permit设置成1(注意多次调用unpark方法,不会累加,permit值还是1)会自动唤醒thread线程,即之前阻塞种的LockSupport.park()方法会立即返回
- static void unpark()
LockSupport它能解决的痛点
- LockSupport不用持有锁块,不用加锁,程序性能好
- 先后顺序,不容易导致卡死(因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞)
package example;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
/*
(1).阻塞
(permit默认是O,所以⼀开始调⽤park()⽅法,当前线程就会阻塞,直到别的线程将当前线程的
permit设置为1时,
park⽅法会被唤醒,然后会将permit再次设置为O并返回)
static void park()
static void park(Object blocker)
(2).唤醒
static void unpark(Thread thread)
(调⽤unpark(thread)⽅法后,就会将thread线程的许可permit设置成1(注意多次调⽤unpark
⽅法,不会累加,
permit值还是1)会⾃动唤醒thread线程,即之前阻塞中的LockSupport.park()⽅法会⽴即返
回)
static void unpark(Thread thread)
* */
public class LockSupportDemo {
public static void main(String[] args) {
Thread t1 = new Thread(()->{
System.out.println(Thread.currentThread().getName());
LockSupport.park();
/**
* 如果这里有俩个LockSupport.park(),因为permit的值为1,上一行已经使用了permit
* 所以下一行被助手的打开会导致程序处于一直等待的状态
*/
// LockSupport.park();
System.out.println(Thread.currentThread().getName() + "被B唤醒了");
},"A");
t1.start();
// 下面代码注释是为了A线程先执行
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread t2 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "唤醒A线程");
LockSupport.unpark(t1);
},"B");
t2.start();
}
}
面试题
- 为什么可以先唤醒线程后阻塞线程?
因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞 - 为什么唤醒俩次后阻塞俩次,但最终结构还会阻塞线程?
因为凭证的数量最多为1,连续调用俩次unpark和调用异常unpark效果一样,只会增加一个凭证,而调用俩次park却需要消费俩个凭证,证不够,不能放行
为什么要是有LockSupport
传统的等待唤醒机制
- 使用Object中的wait()方法让线程等待,使用Object中的notify方法唤醒线程
- 使用JUC包中Condition的await()方法让线程等待使用signal()方法唤醒线程
- LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程
Object类中wait()和notify()实现线程的等待唤醒
- wait()和notify方法必须要再同步块或同步方法里且成对出现使用。wait和notify方法俩个都去掉同步代码块后看运行效果出现异常情况:
Exception in thread “A” Exception in thread “B”
java.lang.IllegalMonitorStateException - 先wait后notify才可以(如果先notify后wai会出现另一个线程一直处于等待状态)
- synchronized是关键字属于JVM层面。monitorenter(底层是通过monitor对象来完成,其实wait/notify等方法也依赖monitor对象只能在同步块或方法中才能调用wait/notify等方法)
package example;
public class SynchronizedDemo {
//等待线程
public void waitThread() {
// 如果将synchronized(this){}助手,会抛出异常,因为wait和notify一定要在同步块或同步方法中
synchronized (this) {
System.out.println(Thread.currentThread().getName() + "coming");
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "end...");
}
}
//唤醒线程
public void notifyThread() {
synchronized (this) {
System.out.println("唤醒A线程");
notify();
}
}
public static void main(String[] args) {
SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
new Thread(() -> {
// 2.如果把下⾏这句代码打开,先notify后wait,会出现A线程⼀直处于等待状态
// try { TimeUnit.SECONDS.sleep(3); } catch
// (InterruptedException e) {e.printStackTrace();}
synchronizedDemo.waitThread();
}, "A").start();
new Thread(() -> {
synchronizedDemo.notifyThread();
}, "B").start();
}
}
Condition接口中的await和signal方法实现线程等待和唤醒(出现的问题和object中的wait和notify一样)
package example;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockDemo {
static Object object = new Object();
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
new Thread(() -> {
//如果把下⾏这句代码打开,先signal后await,会出现A线程⼀直处于等待状态
//try { TimeUnit.SECONDS.sleep(3); } catch
// (InterruptedException e) {e.printStackTrace();}
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "comming");
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
System.out.println(Thread.currentThread().getName() + "end...");
}, "a").start();
new Thread(()->{
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "唤醒A线程");
condition.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
},"B").start();
}
}
AbstractQueuedSynchronizer之AQS
AQS是什么?
- 是用来构建锁或者其他同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的CLH(FIFO)队列的变种来完成资源获取线程的排队工作,将每条将要抢占资源的线程封装成一个iNODE阶段来实现锁的分配,有一个int类变量表示持有锁的状态,通过CAS完成对status值的修改(0表示没有,1表示阻塞)
- AQS为什么是JUC内容中最重要的基石
(ReetrantLock | CountDownLatch | ReentrantReadWriteLock | Semaphore) - 锁与同步器
- 锁:面向锁的使用者(定义了程序员和锁交互的使用层API,隐藏了实现细节,调用即可)
- 同步器:面向锁的实现者(比如java并发大神Douglee,提出统一规范并简化了锁的实现,屏蔽了同步状态管理,阻塞线程排队和通知,唤醒机制等)
- 如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的节点(Node),通过CAS,自旋以及LockSuport.park()的方式,维护state变量的状态,使并发达到同步的效果。
e) {e.printStackTrace();}
synchronizedDemo.waitThread();
}, “A”).start();
new Thread(() -> {
synchronizedDemo.notifyThread();
}, “B”).start();
}
}
Condition接口中的await和signal方法实现线程等待和唤醒(出现的问题和object中的wait和notify一样)
package example;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockDemo {
static Object object = new Object();
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
new Thread(() -> {
//如果把下⾏这句代码打开,先signal后await,会出现A线程⼀直处于等待状态
//try { TimeUnit.SECONDS.sleep(3); } catch
// (InterruptedException e) {e.printStackTrace();}
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "comming");
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
System.out.println(Thread.currentThread().getName() + "end...");
}, "a").start();
new Thread(()->{
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "唤醒A线程");
condition.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
},"B").start();
}
}
#### AbstractQueuedSynchronizer之AQS
##### AQS是什么?
- 是用来构建锁或者其他同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的CLH(FIFO)队列的变种来完成资源获取线程的排队工作,将每条将要抢占资源的线程封装成一个iNODE阶段来实现锁的分配,有一个int类变量表示持有锁的状态,通过CAS完成对status值的修改(0表示没有,1表示阻塞)
- AQS为什么是JUC内容中最重要的基石
(ReetrantLock | CountDownLatch | ReentrantReadWriteLock | Semaphore)
- 锁与同步器
- 锁:面向锁的使用者(定义了程序员和锁交互的使用层API,隐藏了实现细节,调用即可)
- 同步器:面向锁的实现者(比如java并发大神Douglee,提出统一规范并简化了锁的实现,屏蔽了同步状态管理,阻塞线程排队和通知,唤醒机制等)
- 如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的节点(Node),通过CAS,自旋以及LockSuport.park()的方式,维护state变量的状态,使并发达到同步的效果。