目录
-
- JUC——java高级并发
-
- 零、解决线程同步的三大方法
- 一、JUC——java-util-concurrent包
- 二、线程和进程
- 三、Lock锁
- 四、生产者和消费者问题
- 五、syn详解
- 六、集合类不安全!
- 七、线程创建的方式
- 八、常用辅助类
- 九、读写锁
- 十、阻塞队列
- 十一、线程池(!)
- 十二、四大函数接口(java8新特性)
- 十三、Stream流式计算
- 十四、ForkJoin
- 十五、异步回调
- 十六、JMM
- 十七、Volatile关键字
- 十八、单例模式!
- 十九、深入理解CAS(CompareAndSet)
- 十九、ABA问题和原子引用
- 二十、java中的各种锁
- 二十一、死锁
- 面试补充:Volatile、Synchronized、Final深入理解
JUC——java高级并发
思考题:交替执行的思路
- ①wait与notify,一个执行完了就notify,同时自己wait【Plus版就是多个Condition精准通知】
- ②纯标志控制【不推荐,难以检查】
- ③synchronousQueue:同步队列,生产了,必须消费完才能再生产另一个【虽然确实是ABAB,但是打印这个动作可能延后,如果严格要求还是使用方案①】
零、解决线程同步的三大方法
- 互斥量法——锁机制【即Lock、Syn等】
- 事件控制法——阻塞与唤醒【wait、notify】
- 信号量法——生产者消费者模型【此方法归根到底还是前两种方法,或者看成前两种方法的一种应用】
一、JUC——java-util-concurrent包
是一个并发处理的工具包
Runnable:没有返回值,效率比Callable低
二、线程和进程
2.1 程序、进程、线程
程序: 用户编写的代码
进程: 程序的一次执行过程【一个可运行jar包的一次执行过程就是一个进程】
线程: 一个进程往往包含多个线程
java默认几个线程——2个
main线程
gc线程
线程举例:Typora软件,一个线程负责程序输入,一个线程负责自动保存,一个线程负责统计字数等。
2.2 线程管理
要明白,线程的创建不是java所能实现的,其本质是通过C++的本地方法来开启。
对于java而言:三种开启线程的方式Thread、Runnable、Callable、还可以通过Lambda表达式
2.3 并发VS并行
并发:在一核CPU中模拟多个线程,通过快速交替造成同时执行的假象。(宏观多个,微观1个)
并行:多个线程同时执行(始终多个)
8核心16线程:实际上只有8个核心,但是通过超线程技术,让cpu最多可以运行16个线程
并发编程的本质:充分利用CPU的资源
public static void main(String[] args) {
// 得出cpu逻辑核心数,即最大能运行的线程数
System.out.println(Runtime.getRuntime().availableProcessors());
}
2.4 线程状态(java实现的)
获取状态
Thread thread = new Thread();
thread.getState();
// 结果一定是以下枚举之一
状态枚举
public enum State {
// 新生
NEW,
// 运行
RUNNABLE,
// 阻塞
BLOCKED,
// 等待
WAITING,
// 超时等待
TIMED_WAITING,
// 终止
TERMINATED;
}
2.5 wait/sleep区别
①来自不同的类
-
wait——Object类
-
sleep——Thread类【很少用】
我们很少直接使用sleep方法,而是使用TimeUnit方法
TimeUnit.SECONDS.sleep(100);
②锁的释放
- wait——释放锁
- sleep——抱着锁睡觉【不会释放锁】
- TimeUnit睡觉不会释放锁
③使用的范围不同
-
wait:在同步代码块中使用【你得先使用】,否则报
IlleaglMonitorStateException
-
sleep:任何都可使用
三、Lock锁
3.0 原理
Lock锁其实锁的是这个锁本身,就像syn锁对象一样,只不过它锁的是自己。
当多个线程尝试去拿到锁时,发现已经被拿了,就会阻塞!
而且Lock是可重入的,可以多次lock
3.1 锁的体系
3.2 ReentrantLock构造方法
默认是非公平锁。如果传true——公平锁,传false——非公平锁
公平锁:公平——必须先来后到【容易造成效率问题,如3s钟进程等3h进程,低效】
非公平锁:不公平——可以插队(默认)
3.3 Lock的使用
public class Test{
// 1、创建锁
private Lock lock = new ReentrantLock();
public void test(){
// 2、加锁
lock.lock();
try{
// 业务代码
}catch(Exception e){
e.printStackTrace();
}finally{
// 3、解锁
lock.unlock();
}
}
}
//
public static void main(){
Test ticket2 = new Test();
new Thread(()->{
for (int i = 0; i < 50; i++) ticket2.sell();},"A").start();
new Thread(()->{
for (int i = 0; i < 50; i++) ticket2.sell();},"B").start();
new Thread(()->{
for (int i = 0; i < 50; i++) ticket2.sell();},"C").start();
}
3.4 Syn和Lock的区别
1、syn是内置的java关键字、Lock是一个类!
2、syn无法是隐式锁,不利于判断,Lock是显式锁,可以直观判断是否获取到锁
3、syn是全自动的,执行完后会自动释放锁。Lock必须手动释放锁【如果不释放锁,可能会造成死锁问题】
4、tryLock方法
-
使用syn,如果一个线程获得了锁,另一个线程就一直等待。如果拿着锁的线程阻塞了,另一个线程就持续等待。
-
lock锁有一个trylock方法,可以避免等待情况【使用场景:先尝试,尝试成功就执行同步方法,否则先去执行其他的】
5、公平性
- syn一定是非公平的;
- lock可以自己设置公平与非公平
6、syn 适合锁少量的代码同步问题,lock适合大量的同步代码
总结:Lock非常灵活!Syn就像手动挡,而Lock就像自动挡
四、生产者和消费者问题
4.0 问题描述
生产者和消费者在同一时间段内共用同一个存储空间,如下图所示,生产者向空间里存放数据,而消费者取用数据。我们需要通过代码保证该模型的正确执行
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uDY7d52S-1649568717503)(java并发编程——JUC/2011091018554595.gif)]
注意以下几点:
- 消费者消费完了应该等待生产者生产,产品数量不能为负数
- 生产者生产的产品数量应当小于空间容量,当空间容量满时停止生产,将空间使用权交给消费者
4.1 Syn实现
package juc.pc;
/**
* 生产者-消费者问题
* 问题描述:
* 两个线程交替执行操作num,即缓冲区容量为1的情况
* A num+1
* B num-1
* 如果没有线程通信机制,是无法实现的,可以通过《等待-唤醒》来操作
*/
public class OldVersion {
public static void main(String[] args) {
Data data = new Data();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "D").start();
}
}
// 等待-业务-通知
// 判断是否等待,不需要等待就干活,干完就通知
class Data {
private int number = 0;
// +1
public synchronized void increment() throws InterruptedException {
// 这个判断是起一个保证作用,实际上顺序如果比较巧合的话,不会有任何线程被挂起!
while (number != 0) {
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName() + "=>" + number);
// 通知
this.notifyAll();
}
// -1
public synchronized void decrement() throws InterruptedException {
while (number == 0) {
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName() + "=>" + number);
// 通知
this.notifyAll();
}
}
// 曾经疑惑
// 用if:
// 如果生产者还没生产,CPU两次调度消费者怎么办,第二次就接着判断向下了
// 不用担心,因为调度一次后,发现需要等待,就会把这个线程挂起了,除非有别人notify,所以该线程是无法被调度的
// 但是,这个问题并没有彻底解决!
// 如果只有两个线程,确实不会有问题。但如果是多个线程,例如一个生产者,两个消费者,那么生产者A生产完之后notify,会醒来一个消费者B,消费者消费完后又notify,就会把另一个消费者C也唤醒了,此时货物为0,但是C已经经过判断,所以会继续向下执行,就会造成货物数量为-1
// 所以,一定要用while
// 用while之后,循环判断,被唤醒后会再次检查判断条件,从而避免了虚假唤醒的问题
/**
* 虚假唤醒问题:
* 如果用if作为判断wait的条件,那么假如此线程挂起后再次被唤醒,将会从wait之后的代码继续进行!
* 但是用while作为判断条件,该线程再次被唤醒时又回到了判断条件,符合才能继续执行。
*
* 所谓虚假唤醒:就是本不该唤醒你,结果给你唤醒了。
* --------
* 唤醒信号丢失问题:
* 那么,为什么要用notifyAll这种东西呢。因为当有多个线程时,可能会出现信号丢失问题,比如这个例子,有三个线程,两个线程+1,第三个线程-1,那么A线程+1后挂起,又调用B线程,B线程发现已经加过1了,也挂起,然后,C线程-1,唤醒A【先进先出】,C也被挂起。
*
* A被唤醒后,发现条件满足,+1后唤醒的是B【】,接着被挂起到C后面
* B被唤醒后,发现已经+1过了,便挂起。
* 此时三个线程都被永远挂起,再也无法醒来!!
*
* 所以要用notifyAll + while循环,notifyAll唤醒所有的线程,所有线程竞争,不符合运行条件的【即被虚假唤醒的】再次被挂起!从而解决了虚假唤醒的问题。也解决了notify丢失的问题!
*
* 总结:如果真的是随机唤醒的话,notify有概率解决问题,因为只唤醒一个,如果恰好唤醒的是对的,一点问题都没有,如果唤醒错的,则造成notify丢失。
* 引入notifyall会导致所有线程唤醒,此时while就可以让他们回去睡觉,保证程序正常执行!
*/
总结:
1、代码设计思路:
一个Data类,里面放着生产方法和消费方法,通过创建多个线程,每个线程分别只调用生产或消费方法,来模拟生产者和消费者!
这样由于一个实例只有一把锁,所以很安全!
2、当设计多个生产者和多个消费者时,我们通常用while循环检查+notifyAll全部唤醒来解决!
- 为什么while——防止虚假唤醒
- 为什么notifyAll——防止唤醒信号丢失
不足:浪费cpu资源,如果线程很多,notifyall会造成大量切换上下文
4.2 Lock版的实现
①Condition
-
旧的三板斧:syn + wait + notify
-
新的三板斧:lock + await + signal
condition替换了原来老的对象监视器
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
condition.await();
condition.signalAll();
深入理解:
这里的condition是由lock创建的,这一点已经很清楚的告诉我们了为什么wait、notify要配合syn使用。
syn关键字出现的时候,锁的一定是某个对象【即使锁的是Class,其本质也是锁的Class对象】
wait需要知道将当前线程放在哪个对象的等待队列中。所以需要配合监视器【Monitor】。就像这里的condition.await(),就能得知,要将当前线程放在lock对象的等待队列中。
syn锁某个对象时,相当于声明了接下来要监视的就是这个对象。此时再配合wait方法,就知道要将当前线程放在syn的对象中了!
任何对象都有一个monitor,当它被持有后,将处于锁定状态。
syn本质是在代码段首和段位分别添加了monitorenter和monitorexit指令
根据虚拟机规范的要求,在执行monitorenter指令时,首先要去尝试获取对象的锁,如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁【可重入】,把锁的计数器加1;相应地,在执行monitorexit指令时会将锁计数器减1,当计数器被减到0时,锁就释放了。当另一个线程来检查该计数器,发现大于0时,就获取失败
②实现
class Data1 {
private int number = 0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
// +1
public void increment() throws InterruptedException {
lock.lock();
try {
while (number != 0) {
condition.await();
}
number++;
System.out.println(Thread.currentThread().getName() + "=>" + number);
// 通知
condition.signalAll();
} finally {
lock.unlock();
}
}
// -1
public void decrement() throws InterruptedException {
lock.lock();
try {
while (number == 0) {
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName() + "=>" + number);
// 通知
condition.signalAll();
} finally {
lock.unlock();
}
}
}
- lock与wait
如上,wait后,线程会释放锁,而唤醒只是让这些线程可以重新竞争锁,竞争成功的获取到锁,这个过程是隐式的,其他的仍然没有锁,处于等待状态。
当然,这里面的详细过程是,消费者消费了一个,唤醒全部,有可能其他消费者拿到锁了,上CPU,结果循环检查不通过,又wait,同时释放了锁
4.3 condition精准唤醒
问题:传统的notify只能做到随机唤醒,通过condition,我们可以做到精准唤醒
jvm规范种对于notify的规范是随机唤醒,但是实际上取决于每个jvm的实现。而广为使用的hotspot对于notify的实现是先进先出的唤醒!!
这里还是有一些区别的,condition是由lock创建的,但是实质上阻塞的时候是挂在condition上了,即把condition当作资源。但是一定要配合锁使用,即只有持有锁的时候,才有资格去await
class Method {
private Lock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();
State state = State.A;
public void funcA() {
lock.lock();
try {
while (state != State.A) {
condition1.await();
}
System.out.println(Thread.currentThread().getName() + "=>AAAAA");
condition2.signal();
state = State.B;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
五、syn详解
5.0 syn能锁什么
首先要明白,syn只能锁对象
- 类的实例
- 类对象
在此基础上,根据具体情况判断
- 锁static方法,则锁的是类对象;否则锁的是实例
- syn同步块,传入类对象,则锁类对象;否则锁实例
5.1 syn方法
syn方法锁的是方法的调用者,所以如果是static的,就会把类给锁了
public synchronized void sendMs1(){
//锁实例
}
public static synchronized void sendMs2(){
//锁类对象
}
public void sendMs3(){
//普通方法不受锁影响
}
5.2 syn块
public void send(){
synchronized(Test.class){
// 锁类
}
synchronized(this){
// syn(this)就是锁当前实例
}
}
5.3 说明
- 每个实例对象都有一把锁,如果创建了两个对象,则锁一个不影响另一个执行
- 每个类对象只有一把锁,如果类的锁被占用,则必须等待
- 普通方法不受锁的影响
锁类对象的方法有三种
直接syn静态方法
syn (类.class)
syn (this.getClass())
六、集合类不安全!
并发条件下,现有的集合类是不安全的。同时写入会报错:ConcurrentModificationException
根本原因是:modCount和expectedModCount不一致
6.1 ArrayList
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<>();
for (int i = 0; i < 100; i++) {
new Thread(()->{
list.add("a");
}).start();
}
Thread.yield();
System.out.println(list.