github编辑

04-JUC并发编程篇

什么是JUC

源码+官方文档

image-20220227104050720

JUC是 在Java 5.0添加的 java.util.concurrent包的简称,目的就是为了更好的支持高并发任务, 让开发者利用这个包进行的多线程编程时可以有效的减少竞争条件和死锁线程。

  • 业务:普通的线程代码Thread

  • Runnable没有返回值,效率比Callable相对低。

image-20220227104407562

JUC是继承Callable接口的。

线程和进程

线程与进程概念

进程是一个程序,一个进程往往可以包含多个线程(至少一个线程)。

Java默认有2个线程:main线程,GC线程。

线程是进程中的一个方法,比如进程是一个音乐软件,其中后台下载音乐就是一个线程。

对于Java而言:Thread、Runnable、Callable

Java 真的可以开启线程吗? 开不了

并发与并行

并发编程:并发、并行

并发(多线程操作同一个资源):CPU 一核 ,模拟出来多条线程,天下武功,唯快不破,快速交替

并行(多个人一起行走):CPU 多核 ,多个线程可以同时执行; 线程池

并发编程的本质:充分利用CPU的资源

所有的公司都很看重!

企业,挣钱=> 提高效率,裁员,找一个厉害的人顶替三个不怎么样的人;

人员(减) 、技术成本(高)

线程状态

wait与sleep的区别

  • 来与不同的类

    • wait:在Object类中

    • sleep:在Thread类中

  • 关于锁的释放:wait会释放锁,sleep是抱着锁睡觉的,不会释放。

  • 使用的范围是不同的

    • wait只能在同步代码块中使用

    • sleep是在任何地方使用

  • 是否需要捕获异常

    • wait不需要捕获异常

    • sleep需要捕获异常

Lock锁(重点)

多线程操作资源

会出现多个线程争夺资源,而导致顺序不对。

传统的Synchronized

其实就是让多个线程进行排队进行操作。

Lock接口

image-20220227111519349
image-20220227111612827
image-20220227111945757

公平锁:十分公平:可以先来后到

非公平锁:十分不公平:可以插队 (默认)

Synchronized 和 Lock 区别

  1. Synchronized 内置的Java关键字, Lock 是一个Java类

  2. Synchronized 无法判断获取锁的状态,Lock 可以判断是否获取到了锁

  3. Synchronized 会自动释放锁,lock 必须要手动释放锁!如果不释放锁,死锁

  4. Synchronized 线程 1(获得锁,阻塞)、线程2(等待,傻傻的等);Lock锁就不一定会等待下 去;

  5. Synchronized 可重入锁,不可以中断的,非公平;Lock ,可重入锁,可以 判断锁,非公平(可以 自己设置);

  6. Synchronized 适合锁少量的代码同步问题,Lock 适合锁大量的同步代码!

  7. 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好,并且具有更好的扩展性(提供更多的子类)。

思考:锁是什么,如何判断锁是谁的?

生产者和消费者问题

面试的:单例模式、排序算法、生产者和消费者,死锁

生产者和消费者问题Synchronized版本

问题存在,A、B、C、D4个线程!虚假唤醒

这种问题会造成死锁问题。将if判断替换成while判断。

image-20220227232841412

JUC版的生产者和消费者问题

image-20220227233153582

Condition 精准的通知和唤醒线程

通过Condition进行顺序执行任务。实际上是通过Condition精确的监视每一个方法,需要手动设置唤醒的指定线程,以及使当前线程等待。

可以这样理解:班主任通知班长让同学们打卡,班长然后一个一个提醒同学,学生打完卡,汇报给班长,然后班长汇报给班主任。这是顺序的执行,但是中间班长通知所有同学,可以多线程进行。

8锁现象

  • 两个同步方法,一个对象调用,标准情况下,两个线程先打印短信还是电话?发短信

  • sendSms延迟4秒,两个线程先打印发短信还是 打电话?发短信

synchronized 锁的是方法的调用者,也就是对象锁。两个方法持有的是同一把锁,因此谁先拿到锁谁先执行

  • 一个同步方法,一个普通方法,一个对象调用,增加一个普通方法后,先执行发短信还是Hello?普通方法,因为这里没有锁,不是同步方法,不受锁的影响

普通方法没有锁,不需要竞争锁。

  • 两个对象,两个同步方法,发短信还是打电话?打电话

synchronized 锁的是方法的调用者,也就是对象锁。两个对象分别调用两个方法持有的是两把把锁,打电话不需要等待。如果不沉睡,锁的是对象,因为是不同的两个对象,所以并不受锁的影响。

  • 两个静态同步方法,一个对象调用,增加两个静态的同步方法,只有一个对象,先打电话还是发短信?打电话?

static方法类一加载就会执行,synchronized 锁的是Class对象,所以两个方法持有一把锁,谁先得到谁先执行

  • 两个对象!增加两个静态的同步方法, 先打印 发短信?打电话

static方法类一加载就执行,synchronized 锁的是Class对象即类所,两个方法持有两把把锁,而打电话不沉睡4秒

  • 1个静态的同步方法,1个普通的同步方法 ,一个对象,先打印 发短信?打电话? 打电话

原因:静态同步方法和普通同步方法分别是类锁和对象锁,相当于两把锁,普通同步方法不要等待。

  • 1个静态的同步方法,1个普通的同步方法 ,两个对象,先打印 发短信?打电话?打电话

原因:静态同步方法和普通同步方法分别是类锁和对象锁,相当于两把锁,普通同步方法不要等待。

小结:

  • 普通带锁方法:锁对象,同一对象下的才按顺序执行,如果是同一个类下的不同对象则不受影响。

  • 普通不带锁方法:不受任何影响。

  • 静态带锁方法:锁类,同一个类下的所有对象的所有带锁方法都得按顺序执行。

集合类不安全

List不安全

CopyOnWriteArrayList比Vector高效,因为Vector使用synchronized会速度慢,而CopyOnWriteArrayList使用了lock锁。

Set不安全

CopyOnWriteArraySet是线程安全版本的Set实现,它的内部通过一个CopyOnWriteArrayList来代理读写等操作,使得CopyOnWriteArraySet表现出了和CopyOnWriteArrayList一致的并发行为,他们的区别在于数据结构模型的不同,set不允许多个相同的元素插入容器中。

HashSet的底层实现是HashMap。

Map不安全

image-20220228153835218

Callable

image-20220228161855884
  1. 可以有返回值

  2. 可以抛出异常

  3. 方法不同,run()/call()

image-20220228162344875
image-20220228162514092
image-20220228162542285
  • 有缓存

  • 结果可能需要等待,会阻塞。

常用的辅助类(必会)

CountDownLatch

image-20220301100447109
  • countDownLatch.countDown()是数量减1

  • countDownLatch.await()用于等待计数器归零,然后向下执行

每次有线程调用countDown()数量减一,假设计数器变为0,countDownLatch.await()被唤醒,才可以继续往下执行。

CyclicBarrier

image-20220301103255922

类似于加法计数器。

Semaphore

image-20220301103757160

semaphore:信号量

实例:抢车位,6辆车只有3个停车位

  • semaphere.acquire()获得,假设如果已经满了,就会等待到被释放为止。

  • semaphere.release()释放,会将当前信号量进行释放,然后唤醒等待线程。

作用:多个共享资源的互斥的使用!并发限流,控制到最大的线程数。

读写锁

通过ReadWriteLock readWriteLock = new ReentrantReadWriteLock()更细粒度的去控制读写操作。

  • 独占锁(写锁) 一次只能被一个线程占有

  • 共享锁(读锁) 多个线程可以同时占有

阻塞队列

image-20220301112423794

阻塞队列:

image-20220301112448849
image-20220301112503811

BlockingQueue BlockingQueue 不是新的东西

image-20220301113111569

学会使用队列

添加、移除

四组API

方式
抛出异常
有返回值,不抛出异常
阻塞 等待
超时等待

添加

add

offer()

put()

offer(,,)

移除

remove

poll()

take()

poll(,)

检测队首元素

element

peek

-

-

SynchronousQueue同步队列

没有容量,进去一个元素,必须等待取出来之后,才能再往里面放一个元素!

  • take与put方法

线程池(重点)

池化技术

程序的运行会占用系统的资源!

池化技术是优化资源的使用!(线程池、连接池、内存池、对象池)

池化技术:事先准备好一些资源,要有人来使用,就来我这里来拿,用完之后还回来。

线程池的好处:1. 降低资源的消耗;2. 提高响应的速度;3. 方便管理

核心作用:线程复用、可以控制最大并发数、管理线程。

线程池面试:三大方法、7大参数、4种拒绝策略

三大方法

image-20220301155045471
  • 创建单个线程:newSingleThreadExecutor

  • 创建固定线程池大小:newFixedThreadPool

  • 创建可缓存线程池:newCachedThreadPool

都是通过ThreadPoolExecutor实现的。

7大参数

源码分析

本质上是调用了ThreadPoolExecutor,而里面的参数就是所谓的7大参数。

image-20220301162244452

手动创建一个线程池

四种解决策略

小结

最大线程到底如何定义?

  1. CPU密集型,几核就是几,可以保持CPU的效率最高

  1. IO 密集型:设置大于判断程序中十分耗IO的线程数量,比如程序中有15个大型任务,io十分占用资源!

四大函数式接口(必需掌握)

必须掌握:lambda表达式、链式编程、函数式接口、Stream流式计算

函数式接口:只有一个方法的接口

Java 8为函数式接口引入了一个新注解@FunctionalInterface,主要用于编译级错误检查,加上该注解,当你写的接口不符合函数式接口定义的时候,编译器会报错。

四大函数式接口:Consumer、Function、Predicate、Supplier

Function函数接口

image-20220302100830467

Predicate:断定型接口,有一个输入参数,返回值只能是布尔值

image-20220302101129560

Consumer消费型接口

image-20220302101635771

Supplier供给型接口

image-20220302101916603

Stream流式计算

什么是Stream流式计算

大数据分为存储+计算。

集合、MySQL的本质是用来存储东西的。

计算都应该交给流来实现。

  • 创建User类

  • 测试

ForkJoin

什么是ForkJoin

ForkJoin在JDK1.7出现的,并行执行任务!提高效率,适合大数据量。

ForkJoin是将一个大任务拆分为多个子任务进行操作。就是分而治之思想。

image-20220302104603326

ForkJoin特点:工作窃取

这个里面维护的都是双端队列。

image-20220302104943515
image-20220302123459105
image-20220302123517176

异步回调

Future设计的初衷:对某个未来的事件结果进行建模,类似于ajax

image-20220302131442134

JMM

请你谈谈volatile的理解

volatile是java虚拟机提供轻量级的同步机制

  1. 包装可见性

  2. 不保证原子性

  3. 禁止指令重排

什么是JMM

JMM是java内存模型,不存在的东西,就是一个概念/约定。

关于JMM的一些同步约定:

  • 线程解锁前,必须把共享变量立刻刷回主存。

  • 线程加锁前,必须读取主存中的最新值到工作内存中。

  • 加锁和解锁是同一把锁

线程 工作内存、主内存

8种操作:

在这里插入图片描述

8种原子操作如下:

  • lock(锁定):作用于主内存,它把一个变量标记为一条线程独占状态;

  • read(读取):作用于主内存,它把变量值从主内存传送到线程的工作内存中,以便随后的load动作使用;

  • load(载入):作用于工作内存,它把read操作的值放入工作内存中的变量副本中;

  • use(使用):作用于工作内存,它把工作内存中的值传递arrow-up-right给执行引擎,每当虚拟机遇到一个需要使用这个变量的指令时候,将会执行这个动作;

  • assign(赋值):作用于工作内存,它把从执行引擎获取的值赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的指令时候,执行该操作;

  • store(存储):作用于工作内存,它把工作内存中的一个变量传送给主内存中,以备随后的write操作使用;

  • write(写入):作用于主内存,它把store传送值放到主内存中的变量中。

  • unlock(解锁):作用于主内存,它将一个处于锁定状态的变量释放出来,释放后的变量才能够被其他线程锁定;

Java内存模型还规定了执行上述8种基本操作时必须满足如下规则:

(1)不允许read和load、store和write操作之一单独出现(即不允许一个变量从主存读取了但是工作内存不接受,或者从工作内存发起会写了但是主存不接受的情况),以上两个操作必须按顺序执行,但没有保证必须连续执行,也就是说,read与load之间、store与write之间是可插入其他指令的。

(2)不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。

(3)不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。

(4)一个新的变量只能从主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。

(5)一个变量在同一个时刻只允许一条线程对其执行lock操作,但lock操作可以被同一个条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。

(6)如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。

(7)如果一个变量实现没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量。

(8)对一个变量执行unlock操作之前,必须先把此变量同步回主内存(执行store和write操作)。

问题: 程序不知道主内存的值已经被修改过了

image-20220303102013332

Volatile

  • 保证可见性

  • 不保证原子性

原子性:不可分割

线程A在执行任务的时候,不能被打扰的,也不被分割,要么同时成功,要么同时失败。

如果不加lock和synchronized,怎么保证原子性。

image-20220303103036113

使用原子类,解决原子性问题

这些类的操作直接底层操作系统挂钩。在内存中修改至,Unsafe是一个很特殊的类。

  • 指令重排

指令重排:写的程序,计算机并不是按照写的程序去执行的。

源代码-> 编译器优化的重排->指令并行也可能会重排->内存系统也会重排->执行

所期望的是1234步骤进行顺序执行,但是计算机会进行指令重排(比如2134,1324也可以执行)。

image-20220303104718976
image-20220303104734627

volatile可以避免指令重排。

内存屏障,CPU指令。作用:

1、保证特定的操作的执行顺序!

2、可以保证某些变量的内存可见性 (利用这些特性volatile实现了可见性)

image-20220303105352687

volatile是可以保持可见性,不能保证原子性,由于内存屏障,可以保证避免指令重排的现象发生。

彻底玩转单例模式

饿汉式、DCL懒汉式(用到了volatile)

  • 饿汉式

  • DCL懒汉式

  • 静态内部类

单例不安全,可以通过反射进行破坏

  • 枚举

image-20220303150903189

深入理解CAS

什么是CAS

CAS:比较当前工作内存中的值和主内存中的值,如果这个值是期望的,那么就执行操作!如果不是就一直循环,因为其底层是自旋锁。

缺点:

  1. 循环会耗时

  2. 一次性只能保证一个共享变量的原子性

  3. ABA问题

Unsafe

image-20220304102147460
image-20220304102303291
image-20220304102338655

CAS:什么是ABA问题

ABA问题:A被一个线程修改成B然后又被另一个修改成A,实际对象已经发生改变,可以通过增加版本号来改变。

image-20220304103010273

![image-20220502124037974](d:/program files/typora/assets/04-JUC并发编程篇/image-20220502124037974-1661700331033214.png)

原子引用

解决ABA 问题,引入原子引用! 对应的思想:乐观锁!

带版本号的原子操作!

Integer 使用了对象缓存机制,默认范围是 -128 ~ 127 ,推荐使用静态工厂方法 valueOf 获取对象实例,而不是 new,因为 valueOf 使用缓存,而 new 一定会创建新的对象分配新的内存空间;

AtomicStampedReference 注意,如果泛型是一个包装类,注意对象的引用问题。 正常在业务操作,这里面比较的都是一个个对象。

各种锁的理解

公平锁与非公平锁

  • 公平锁:非常公平,不能够插队,必须先来后到

  • 非公平锁:非常不公平,可以插队(默认都是非公平)

可重入锁

可重入锁(递归锁)

image-20220304105408547

Sychronized

Lock

自旋锁

spinlock

自定义锁测试

死锁

什么是死锁

image-20220304124255380

解决问题

  1. 使用jps -l定位进程号

image-20220304125747378
  1. 使用jstack 进程号找到死锁问题

image-20220304125954076

排查问题:

  1. 查看日志

  2. 看下堆栈信息

Synchronized锁升级

  • 无锁

  • 偏向锁:没有线程竞争的情况,锁会偏向于线程A,不需要再次获取,而是直接进入。如果存在多个线程来抢占锁,线程B来抢占所,锁会升级为轻量级锁。(默认4秒后开启,-XX:BiasedLockingStartupDelay=0

  • 轻量级锁:为了避免线程阻塞,通过自旋锁来实现(也就是重试)。 如果重试后,还是抢不到锁,升级为重量级锁。

  • 重量级锁:

    • 用户态到内核态的切换

    • 没有获得锁的线程会阻塞,再被唤醒

JUC面试

互联网三高

  • 高性能:响应(低延时)(缓存、JVM优化)、吞吐(高吞吐量、高并发)(集群、负载均衡)

  • 高扩展

  • 高可用

线程的数量是不是越多越好

不是

  • 单核CPU设定多线程是否有意义

  • 工作线程数是不是设置的越大越好

  • 工作线程数设多少合适

$N_{threads}= N_{CPU}U_{CPU}(1+W/C)$

  • $N_{CPU}$是处理器的核的数目,可以通过Runtime.getRuntime.availableProcessors()得到

  • $U_{CPU}$是期望的CPU的利用率(该值应该介于0~1之间)

  • $W/C$是等待时间与计算时间的比率。

synchronized悲观锁与JUC乐观锁

悲观锁:总感觉有人要自己竞争,因此一直加锁。

乐观锁:总感觉没人和自己竞争,一直不设防。

synchronized本质上是一个悲观锁。

JUC的乐观锁是自旋锁,自旋锁的实现方式是通过CAS实现的。

CAS面试题:ABA问题

就是A被修改为B,然后又被修改为A。

(版本号/boolean)

CAS的原子性问题

CAS操作cpu本身有指令支持-不保障原子性

什么时候使用CAS,什么使用悲观锁

能使用synchronized的时候,就优先先使用syn。

JDK1.5之后,synchronized内部有锁升级过程,偏向锁-> 自旋锁(轻量级)->重量级锁(悲观排队锁)

最后更新于

这有帮助吗?