壹、B站狂神juc
1、什么是JUC
java.util.concurrent 包是在并发编程中使用的工具类,有以下三个包:
2.进程和线程回顾
进程 / 线程是什么?
进程:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
线程:通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义,线程可以利用进程所有拥有的资源。在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。
白话:
进程:就是操作系统中运行的一个程序,QQ.exe, music.exe, word.exe ,这就是多个进程
线程:每个进程中都存在一个或者多个线程,比如用word写文章时,就会有一个线程默默帮你定时自动保存。
并发 / 并行是什么?
做并发编程之前,必须首先理解什么是并发,什么是并行。
线程的状态:Java的线程有6种状态:可以分析源码:
wait / sleep 的区别
3、Lock锁
传统的 synchronized
使用 juc.locks 包下的类操作 Lock 锁 + Lambda 表达式
synchronized 和 lock 区别
4、生产者和消费者
线程间的通信 , 线程之间要协调和调度
生产者和消费者 synchroinzed 版 :
问题升级:防止虚假唤醒,4个线程,两个加,两个减
新版生产者和消费者写法
、
闲聊常见笔试题:手写单例模式、手写冒泡排序、手写生产者消费者
精确通知顺序访问
5、8锁的现象
1、标准访问,请问先打印邮件还是短信?
2、邮件方法暂停4秒钟,请问先打印邮件还是短信?
3、新增一个普通方法hello()没有同步,请问先打印邮件还是hello?
4、两部手机、请问先打印邮件还是短信?
5、两个静态同步方法,同一部手机,请问先打印邮件还是短信?
6、两个静态同步方法,2部手机,请问先打印邮件还是短信?
7、一个普通同步方法,一个静态同步方法,同一部手机,请问先打印邮件还是短信?
8、一个普通同步方法,一个静态同步方法,2部手机,请问先打印邮件还是短信?
小结
6、集合类不安全
list 不安全
单线程
多线程
set 不安全
map 不安全
7、Callable
多线程中,第3种获得多线程的方式,Callable。它与Runnable有什么区别呢?
怎么调用callable
8、常用辅助类
8.1、CountDownLatch
8.2、CyclicBarrier
作用:和上面的减法相反,这里是加法,好比集齐7个龙珠召唤神龙,或者人到齐了再开会!
8.3、Semaphore
翻译:Semaphore 信号量;信号灯;信号作用:抢车位
9、读写锁
独占锁(写锁):指该锁一次只能被一个线程锁持有。对于ReentranrLock和 Synchronized 而言都是独占锁。
共享锁(读锁):该锁可被多个线程所持有。
对于ReentrantReadWriteLock其读锁时共享锁,写锁是独占锁,读锁的共享锁可保证并发读是非常高效的。
10.阻塞队列
阻塞队列
接口结构图
SynchronousQueue 同步队列
11、线程池
池化技术
为什么使用线程池
10 年前单核CPU电脑,假的多线程,像马戏团小丑玩多个球 ,CPU 需要来回切换。现在是多核电脑,多个线程各自跑在独立的CPU上,不用切换效率高
ThreadPoolExecutor 七大参数
线程池用哪个?生产中如何设置合理参数
12、四大函数式接口
java.util.function , Java 内置核心四大函数式接口,可以使用lambda表达式
13、Stream流式计算
流(Stream)到底是什么呢?
14、分支合并
什么是ForkJoin
核心类
15、异步回调
16、JMM
17、volatile
volatile是不错的机制,但是也不能保证原子性
18、深入单例模式
1、饿汉式
2.懒汉式
3.静态内部类
4、万恶的反射
5.枚举
19、深入理解CAS
CAS : 比较并交换
CAS 底层原理?如果知道,谈谈你对UnSafe的理解?
问题:这个UnSafe类到底是什么? 可以看到AtomicInteger源码中也是它!
20、原子引用
原子类 AtomicInteger 的ABA问题谈谈?原子更新引用知道吗?
原子引用 AtomicReference
要解决ABA问题,我们就需要加一个版本号
版本号原子引用,类似乐观锁
演示ABA问题:
解决方案:
21、Java锁
贰、拉钩多线程
并发编程简介
java是一个支持多线程的开发语言。多线程可以在包含多个CPU核心的机器上同时处理多个不同的任务,优化资源的使用率,提升程序的效率。在一些对性能要求比较高场合,多线程是java程序调优的重要方面。比较重要的内容除了算法,就是并发编程。并发编程是最能体现一个程序员功底的方面之一。
Java并发编程主要涉及以下几个部分:
1. 并发编程三要素
原子性:即一个不可再被分割的颗粒。在Java中原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。
有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)
可见性:当多个线程访问同一个变量时,如果其中一个线程对其作了修改,其他线程能立即获取到最新的值。
2. 线程的五大状态
创建状态:当用 new 操作符创建一个线程的时候
就绪状态:调用 start 方法,处于就绪状态的线程并不一定马上就会执行 run 方法,还需要等待CPU的调度
运行状态:CPU 开始调度线程,并开始执行 run 方法
阻塞状态:线程的执行过程中由于一些原因进入阻塞状态比如:调用 sleep 方法、尝试去得到一个锁等等
死亡状态:run 方法执行完 或者 执行过程中遇到了一个异常
3. 悲观锁与乐观锁
悲观锁:每次操作都会加锁,会造成线程阻塞。
乐观锁:每次操作不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止,不会造成线程阻塞。
4. 线程之间的协作
线程间的协作有:wait/notify/notifyAll等
5. synchronized 关键字
synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
①修饰一个代码块:被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象
②修饰一个方法:被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象
③修饰一个静态的方法:其作用的范围是整个静态方法,作用的对象是这个类的所有对象
④修饰一个类:其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。
6. CAS
CAS全称是Compare And Swap,即比较替换,是实现并发应用到的一种技术。操作包含三个操作数—内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。CAS存在三大问题:ABA问题,循环时间长开销大,以及只能保证一个共享变量的原子操作。
7. 线程池
如果我们使用线程的时候就去创建一个线程,虽然简单,但是存在很大的问题。如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。线程池通过复用可以大大减少线程频繁创建与销毁带来的性能上的损耗。
常见的多线程问题有:
1. 重排序有哪些分类?如何避免?
2. Java 中新的Lock接口相对于同步代码块(synchronized block)有什么优势?如果让你实现一
个高性能缓存,支持并发读取和单一写入,你如何保证数据完整性。
3. 如何在Java中实现一个阻塞队列。
4. 写一段死锁代码。说说你在Java中如何解决死锁。
5. volatile变量和atomic变量有什么不同?
6. 为什么要用线程池?
7. 实现Runnable接口和Callable接口的区别
8. 执行execute()方法和submit()方法的区别是什么呢?
9. AQS的实现原理是什么?
10. java API中哪些类中使用了AQS?
11. ...
并发编程课程很多内容会从JDK源码解析相关原理。
主要内容包括:
1. 多线程&并发设计原理
并发核心概念
并发的问题
JMM内存模型
2. JUC
并发容器
同步工具类
Atomic类
Lock与Condition
3. 线程池与Future
线程池的实现原理
线程池的类继承体系
ThreadPoolExecutor
Executors工具类
ScheduledThreadPool Executor
CompletableFuture用法4. ForkJoinPool
ForkJoinPool用法
核心数据结构
工作窃取队列
ForkJoinPool状态控制
Worker线程的阻塞-唤醒机制
任务的提交过程分析
工作窃取算法:任务的执行过程分析
ForkJoinTask的fork/join
ForkJoinPool的优雅关闭
5. 多线程设计模式
Single Threaded Execution模式
Immutable模式
Guarded Suspension模式
Balking模式
Producer-Consumer模式
Read-Write Lock模式
Thread-Per-Message模式
Worker Thread模式
Future模式
一、多线程&并发设计原理
1 多线程回顾
1.1 Thread和Runnable
1.1.1 Java中的线程
创建执行线程有两种方法:1.扩展Thread 类。2.实现Runnable 接口。
1.1.2 Java中的线程:特征和状态
1. 所有的Java 程序,不论并发与否,都有一个名为主线程的Thread 对象。执行该程序时, Java虚拟机( JVM )将创建一个新Thread 并在该线程中执行main()方法。这是非并发应用程序中唯一的线程,也是并发应用程序中的第一个线程。
2. Java中的线程共享应用程序中的所有资源,包括内存和打开的文件,快速而简单地共享信息。但是必须使用同步避免数据竞争。
3. Java中的所有线程都有一个优先级,这个整数值介于Thread.MIN_PRIORITY(1)和Thread.MAX_PRIORITY(10)之间,默认优先级是Thread.NORM_PRIORITY(5)。线程的执行顺序并没有保证,通常,较高优先级的线程将在较低优先级的钱程之前执行。
4. 在Java 中,可以创建两种线程:
守护线程。
非守护线程。
区别在于它们如何影响程序的结束。
Java程序结束执行过程的情形:
程序执行Runtime类的exit()方法, 而且用户有权执行该方法。
应用程序的所有非守护线程均已结束执行,无论是否有正在运行的守护线程。
守护线程通常用在作为垃圾收集器或缓存管理器的应用程序中,执行辅助任务。在线程start之前调用isDaemon()方法检查线程是否为守护线程,也可以使用setDaemon()方法将某个线程确立为守护线程。
5. Thread.States类中定义线程的状态如下:
NEW:Thread对象已经创建,但是还没有开始执行。
RUNNABLE:Thread对象正在Java虚拟机中运行。
BLOCKED : Thread对象正在等待锁定。
WAITING:Thread 对象正在等待另一个线程的动作。
TIME_WAITING:Thread对象正在等待另一个线程的操作,但是有时间限制。
TERMINATED:Thread对象已经完成了执行。
getState()方法获取Thread对象的状态,可以直接更改线程的状态。
在给定时间内, 线程只能处于一个状态。这些状态是JVM使用的状态,不能映射到操作系统的线程状态。
线程状态的源码:
1.1.3 Thread类和Runnable 接口
Runnable接口只定义了一种方法:run()方法。这是每个线程的主方法。当执行start()方法启动新线程时,它将调用run()方法。
Thread类其他常用方法:
获取和设置Thread对象信息的方法:
①getId():该方法返回Thread对象的标识符。该标识符是在钱程创建时分配的一个正整数。在线程的整个生命周期中是唯一且无法改变的。
②getName()/setName():这两种方法允许你获取或设置Thread对象的名称。这个名称是一个String对象,也可以在Thread类的构造函数中建立。
③getPriority()/setPriority():你可以使用这两种方法来获取或设置Thread对象的优先级。
④isDaemon()/setDaemon():这两种方法允许你获取或建立Thread对象的守护条件。
⑤getState():该方法返回Thread对象的状态。
interrupt():中断目标线程,给目标线程发送一个中断信号,线程被打上中断标记。
interrupted():判断目标线程是否被中断,但是将清除线程的中断标记。
isinterrupted():判断目标线程是否被中断,不会清除中断标记。
sleep(long ms):该方法将线程的执行暂停ms时间。
join():暂停线程的执行,直到调用该方法的线程执行结束为止。可以使用该方法等待另一个Thread对象结束。
setUncaughtExceptionHandler():当线程执行出现未校验异常时,该方法用于建立未校验异常的控制器。
currentThread():Thread类的静态方法,返回实际执行该代码的Thread对象。
join示例程序(a程序在执行,突然调用b程序的join方法就行执行完b程序再执行a程序):
1.1.4 Callable
Callable 接口是一个与Runnable 接口非常相似的接口。Callable 接口的主要特征如下。
①接口。有简单类型参数,与call()方法的返回类型相对应。
②声明了call()方法。执行器运行任务时,该方法会被执行器执行。它必须返回声明中指定类型的对象。
③call()方法可以抛出任何一种校验异常。可以实现自己的执行器并重载afterExecute()方法来处理这些异常。
可以有返回值
可以处理任何异常
1.2 synchronized关键字
1.2.1 锁的对象
synchronized关键字“给某个对象加锁”,示例代码:
等价于=
实例方法的锁加在对象myClass上;静态方法的锁加在MyClass.class上。
1.2.2 锁的本质
如果一份资源需要多个线程同时访问,需要给该资源加锁。加锁之后,可以保证同一时间只能有一个线程访问该资源。资源可以是一个变量、一个对象或一个文件等。
1.2.3 实现原理
锁如何实现?
在对象头里,有一块数据叫Mark Word。在64位机器上,Mark Word是8字节(64位)的,这64位中有2个重要字段:锁标志位和占用该锁的thread ID。因为不同版本的JVM实现,对象头的数据结构会有各种差异。
1.3 wait与notify
1.3.1 生产者−消费者模型
生产者-消费者模型是一个常见的多线程编程模型,如下图所示:
1.如何阻塞?
办法1:线程自己阻塞自己,也就是生产者、消费者线程各自调用wait()和notify()。
办法2:用一个阻塞队列,当取不到或者放不进去数据的时候,入队/出队函数本身就是阻塞的。
2.如何双向通知?
办法1:wait()与notify()机制。
办法2:Condition机制。
单个生产者单个消费者线程的情形:
多个生产者单个消费者线程的情形:
1.3.2 为什么必须和synchronized一起使用
在Java里面,wait()和notify()是Object的成员函数,是基础中的基础。为什么Java要把wait()和notify()放在如此基础的类里面,而不是作为像Thread一类的成员函数,或者其他类的成员函数呢?
先看为什么wait()和notify()必须和synchronized一起使用?请看下面的代码:
或者下面代码
1.3.3 为什么wait()的时候必须释放锁
1.3.4 wait()与notify()的问题
以上述的生产者-消费者模型来看,其伪代码大致如下:
生产者在通知消费者的同时,也通知了其他的生产者;消费者在通知生产者的同时,也通知了其他消费者。原因在于wait()和notify()所作用的对象和synchronized所作用的对象是同一个,只能有一个对象,无法区分队列空和列队满两个条件。这正是Condition要解决的问题。
1.4 InterruptedException与interrupt()方法
1.4.1 Interrupted异常
什么情况下会抛出Interrupted异常
假设while循环中没有调用任何的阻塞函数,就是通常的算术运算,或者打印一行日志,如下所示。
这个时候,在主线程中调用一句thread.interrupt(),请问该线程是否会抛出异常?不会。
只有那些声明了会抛出InterruptedException的函数才会抛出异常,也就是下面这些常用的函数:
本来sleep1000毫秒,现在改变状态为100毫秒,就抛出异常,但是一抛出异常,中断状态就变成false了
1.4.2 轻量级阻塞与重量级阻塞
能够被中断的阻塞称为轻量级阻塞,对应的线程状态是WAITING或者TIMED_WAITING;而像synchronized 这种不能被中断的阻塞称为重量级阻塞,对应的状态是 BLOCKED。如图所示:调用不同的方法后,一个线程的状态迁移过程。
初始线程处于NEW状态,调用start()开始执行后,进入RUNNING或者READY状态。如果没有调用任何的阻塞函数,线程只会在RUNNING和READY之间切换,也就是系统的时间片调度。这两种状态的切换是操作系统完成的,除非手动调用yield()函数,放弃对CPU的占用。
一旦调用了图中的任何阻塞函数,线程就会进入WAITING或者TIMED_WAITING状态,两者的区别只是前者为无限期阻塞,后者则传入了一个时间参数,阻塞一个有限的时间。如果使用了synchronized关键字或者synchronized块,则会进入BLOCKED状态。
不太常见的阻塞/唤醒函数,LockSupport.park()/unpark()。这对函数非常关键,Concurrent包中Lock的实现即依赖这一对操作原语。
因此thread.interrupted()的精确含义是“唤醒轻量级阻塞”,而不是字面意思“中断一个线程”。
thread.isInterrupted()与Thread.interrupted()的区别
因为 thread.interrupted()相当于给线程发送了一个唤醒的信号,所以如果线程此时恰好处于WAITING或者TIMED_WAITING状态,就会抛出一个InterruptedException,并且线程被唤醒。而如果线程此时并没有被阻塞,则线程什么都不会做。但在后续,线程可以判断自己是否收到过其他线程发来的中断信号,然后做一些对应的处理。
这两个方法都是线程用来判断自己是否收到过中断信号的,前者是实例方法,后者是静态方法。二者的区别在于,前者只是读取中断状态,不修改状态;后者不仅读取中断状态,还会重置中断标志位。
1.5 线程的优雅关闭
1.5.1 stop与destory函数
1.5.2 守护线程
daemon线程和非daemon线程的对比:
1.5.3 设置关闭的标志位
开发中一般通过设置标志位的方式,停止循环运行的线程。
但上面的代码有一个问题:如果MyThread t在while循环中阻塞在某个地方,例如里面调用了object.wait()函数,那它可能永远没有机会再执行 while( ! stopped)代码,也就一直无法退出循环。
此时,就要用到InterruptedException()与interrupt()函数。
2 并发核心概念
2.1 并发与并行
2.2 同步
2.3 不可变对象
2.4 原子操作和原子变量
2.5 共享内存与消息传递
3 并发的问题
3.1 数据竞争
如果有两个或者多个任务在临界段之外对一个共享变量进行写入操作,也就是说没有使用任何同步机制,那么应用程序可能存在数据竞争(也叫做竞争条件)。
在这些情况下,应用程序的最终结果可能取决于任务的执行顺序。
假设有两个不同的任务执行了同一个modify方法。由于任务中语句的执行顺序不同,最终结果也会不同。
modify方法不是原子的, ConcurrentDemo 也不是线程安全的。
3.2 死锁
3.3 活锁
3.4 资源不足
3.5 优先权反转