JUC-高并发编程
1、JUC介绍
JUC简介
- JUC就是 java.util .concurrent 工具包的简称。-处理线程的工具包
进程和线程的概念
-
进程与线程
-
进程
- 是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
- 指在系统中正在运行的一个应用程序;程序一旦运行就是进程;进程— —资源分配的最小单位。
-
线程
- 是操作系统能够进行运算调度的最小单位。
- 系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元执行流。线程——程序执行的最小单位。
-
-
线程的状态
-
NEW新建
-
RUNNABLE准备就绪
-
BLOCKED阻塞
-
WAITING不见不散
- 一直等到见面才开始下一步
-
TIMED_WAITING过时不候
- 过了等候时间直接下一步
-
TERMINATED终结
-
-
wait和sleep区别
-
相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。并且他们都可以被interrupted方法中断
-
不同点
- 两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()
- 调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
- 关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。
-
-
并行与并发
-
串行与并行
- 串行表示所有任务都一一按先后顺序进行。串行是一次只能取得一个任务,并执行这个任务。
- 并行意味着可以同时取得多个任务,并同时去执行所取得的这些任务。并行的效率从代码层次上强依赖于多进程/多线程代码,从硬件角度上
则依赖于多核 CPU。
-
并发
- 并发(concurrent)指的是多个程序可以同时运行的现象,更细化的是多进程可以同时运行或者多指令可以同时运行。
-
-
管程(Monitor)
- 监视器-俗称锁
- 是一种同步的机制,保证同一个时间,只有一个线程访问被保护数据或者代码
保证了同一个时刻只有一个进程在管程内活动。 - JVM 中同步是基于进入和退出管程(monitor)对象实现的,每个对象都会有一个管程(monitor)对象,管程(monitor)会随着 java 对象一同创建和销毁。
- 执行线程首先要持有管程对象,然后才能执行方法,当方法完成之后会释放管程,方法在执行时候会持有管程,其他线程无法再获取同一个管程
-
用户线程和守护线程
- 用户线程:平时用到的普通线程,自定义线程
- 守护线程:运行在后台,是一种特殊的线程,比如垃圾回收
2、Lock接口
复习Synchronized
-
Synchronized是Java中的关键字,是一种同步锁
- 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
- 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
- 修饰一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
- 修饰一个类,其作用的范围是 synchronized 后面括号括起来的部分,作用主的对象是这个类的所有对象。
-
多线程编程步骤-上部
- 创建资源类,在资源类创建属性和操作方法
- 创建多个线程,调用资源类的操作方法
可重入锁
-
ReentrantLock
-
某个线程已经获得某个锁,可以再次获取锁而不会出现死锁
-
new ReentrantLock();
- 1、实例化private ReentrantLock lock = new ReentrantLock();–继承时需要加上static
- 2、写一个try-finally,其中try包括的同步代码,并在同步代码前先调用锁定方法–lock.lock();
- 3、在finally中手动释放锁,lock.unlock();
-
创建多线程的方式
-
继承Thread类的方式
-
Thread类的创建
- 创建一个继承于Thread类的子类
- 重写Thread类的run()方法–>将此进程执行的操作声明在run()中
- 创建Thread类的子类对象
- 通过此对象调用start():①启动当前进程 ②调用当前进程的run()
-
Thread类的方法
- start():启动当前线程;调用当前线程的run()
- run(): 通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
- currentThread():静态方法,返回执行当前代码的线程
- getName():获取当前线程的名字
- setName():设置当前线程的名字
- yield():释放当前cpu的执行权
- join():在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。
- stop():已过时。当执行此方法时,强制结束当前线程。
- sleep(long millitime):让当前线程“睡眠”指定的millitime毫秒。在指定的millitime毫秒时间内,当前线程是阻塞状态。
- isAlive():判断当前线程是否存活
-
线程的优先级
-
MAX_PRIORITY:10
MIN _PRIORITY:1
NORM_PRIORITY:5 -->默认优先级- 高优先级的线程高概率的情况下被执行。
-
如何获取和设置当前线程的优先级:
-
-
- getPriority():获取线程的优先级
- setPriority(int p):设置线程的优先级
-
实现Runnable接口
-
实现类的创建
-
创建一个实现了Runnable接口的类
-
实现类去实现Runnable中的抽象方法:run()
-
创建实现类的对象
-
将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
-
通过Thread类的对象调用start()
- 调用了Runnable类型的target的run()方法
-
-
-
实现Callable接口
-
实现类的创建
- 创建一个实现Callable的实现类
- 实现call方法,将此线程需要执行的操作声明在call()中,call()有返回值
- 创建Callable接口实现类的对象
- 将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
- 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
- 可选:获取Callable中call方法的返回值
get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
-
-
使用线程池
-
创建-使用ExecutorService(线程池接口)和Executors(线程池的工厂类)
-
提供指定线程数量的线程池
- Executors.newFixedThreadPool(10);
-
执行指定的线程的操作。需要提供实现Runnable接口(execute())或Callable接口实现类的对象(submit())
-
关闭连接池
-
-
好处
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理
corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAliveTime:线程没任务时最多保持多长时间后会终止
-
3、线程间的通信
多线程编程步骤-中部
-
创建资源类,在资源类创建属性和操作方法
-
在资源类操作方法-交替进行
- 判断
- 干活
- 通知
-
创建多个线程,调用资源类的操作方法
-
下部:防止虚假唤醒问题
Synchronized
- 代码
public class ThreadDemo1 {
public static void main(String[] args) {
Share share = new Share();
new Thread(()->
{
for (int i = 0; i < 10; i++) {
try {
share.add();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"AA").start();
new Thread(()->
{
for (int i = 0; i < 10; i++) {
try {
share.minus();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"BB").start();
new Thread(()->
{
for (int i = 0; i < 10; i++) {
try {
share.add();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"CC").start();
new Thread(()->
{
for (int i = 0; i < 10; i++) {
try {
share.minus();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"DD").start();
}
}
class Share{
//初始值
private int number = 0;
//+1的方法
public synchronized void add() throws InterruptedException {
//判断
while(number != 0 ){
wait();
}
//干活
number++;
System.out.println(Thread.currentThread().getName() +" :"+number);
//通知
notifyAll();
}
public synchronized void minus() throws InterruptedException {
//判断
while (number != 1 ){
wait();
}
//干活
number--;
System.out.println(Thread.currentThread().getName() +" :"+number);
//通知
notifyAll();
}
}
-
问题:wait()方法在if判断语句中会出现虚假唤醒的情况,wait()在哪里睡就会在哪里醒,可能线程在判断之后睡,然后被唤醒后就不会进行判断直接下一步操作。
-
解决方法:将if判断语句改为while语句,这样无论线程从哪里唤醒都会进行判断语句。避免了虚假唤醒。
-
实现同步的基础:Java中的每一个对象都可以作为锁
- 对于普通同步方法,锁是当前实例对象
- 对于静态同步方法,锁是当前类的Class对象
- 对于同步方法块,锁是Synchronized括号里配置的对象
Lock接口
-
方法
-
newCondition()–>返回绑定到此Lock实例的新Condition实例
-
Condition接口
- await()–>造成当前线程在接到信号或被中断之前一直处于等待状态
- signal()–>唤醒一个等待线程
- signalAll()–>唤醒所有等待线程
-
-
代码
class ShareTest{
private int num = 0;
//创建Lock对象
private Lock lock =new ReentrantLock();
private Condition con =lock.newCondition();
//+1
public void incr() throws InterruptedException {
lock.lock();
try{
//判断
while(num != 0){
con.await();
}
//干活
num++;
System.out.println(Thread.currentThread().getName() +" :"+num);
//通知
con.signalAll();
}finally {
lock.unlock();
}
}
public void decr() throws InterruptedException {
lock.lock();
try{
//判断
while(num != 1){
con.await();
}
//干活
num--;
System.out.println(Thread.currentThread().getName() +" :"+num);
//通知
con.signalAll();
}finally {
lock.unlock();
}
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
ShareTest test = new ShareTest();
new Thread(()->
{for (int i = 0; i < 10; i++) {
try {
test.incr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "AA").start();
new Thread(()->
{for (int i = 0; i < 10; i++) {
try {
test.decr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "BB").start();
new Thread(()->
{for (int i = 0; i < 10; i++) {
try {
test.incr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "CC").start();
new Thread(()->
{for (int i = 0; i < 10; i++) {
try {
test.decr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "DD").start();
}
}
-
遵循多线程编程的四步
- 创建资源类,在资源类创建属性和操作方法
-
在资源类操作方法-交替进行
- 判断
- 干活
- 通知
-
创建多个线程,调用资源类的操作方法
-
防止虚假唤醒问题
-
等待语句放在while中
- Synchronized中的wait()
- Lock中的await()
-
-
- 创建资源类,在资源类创建属性和操作方法
4、线程间定制化通信
解决:线程间的通信的代码结果里:谁打印的顺序不确定以及次数不确定。
问题:启动三个线程:A 线程打印 5 次 A,B 线程打印 10 次 B,C 线程打印 15 次 C,按照此顺序循环 10 轮
方法:先给每个线程定义一个标志位,通过标志位来实现定制化的通信,在每个线程里先判断标志位然后是则打印并修改标志位然后通知下一个线程。
- 代码
class ShareResource{
private int flag = 1;
private Lock lock = new ReentrantLock();
private Condition con1 = lock.newCondition();
private Condition con2 = lock.newCondition();
private Condition con3 = lock.newCondition();
public void AA(int loop) throws InterruptedException {
lock.lock();
try{
while(flag != 1){
con1.await();
}
for(int i =1; i <= 5;i++){
System.out.println(Thread.currentThread().getName()+" ::" + "第"+loop+ "轮:"+i);
}
flag = 2;//修改标志位2
con2.signal();//通知BB线程---因为等待的是con2,所以用con2去唤醒
}finally {
lock.unlock();
}
}
public void BB(int loop) throws InterruptedException {
lock.lock();
try{
while(flag != 2){
con2.await();
}
for(int i =1; i <= 10;i++){
System.out.println(Thread.currentThread().getName()+" ::" + "第"+loop+ "轮:"+i);
}
flag = 3;//修改标志位3
con3.signal();//通知CC线程
}finally {
lock.unlock();
}
}
public void CC(int loop) throws InterruptedException {
lock.lock();
try{
while(flag != 3){
con3.await();
}
for(int i =1; i <= 15;i++){
System.out.println(Thread.currentThread().getName()+" ::" + "第"+loop+ "轮:"+i);
}
flag = 1;//修改标志位1
con1.signal();//通知AA线程
}finally {
lock.unlock();
}
}
}
public class ThreadDemo3 {
public static void main(String[] args) {
ShareResource shareResource = new ShareResource();
new Thread(new Runnable() {
@Override
public void run() {
for(int i = 1 ;i <=10 ;i++){
try {
shareResource.AA(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"AA").start();
new Thread(new Runnable() {
@Override
public void run() {
for(int i = 1 ;i <=10 ;i++){
try {
shareResource.BB(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"BB").start();
new Thread(new Runnable() {
@Override
public void run() {
for(int i = 1 ;i <=10 ;i++){
try {
shareResource.CC(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"CC").start();
}
}
5、集合的线程安全
集合线程不安全的演示
-
出现的问题:ConcurrentModificationException -->并发修改异常
-
解决方案-Vector
- Vector是线程安全的,但效率很低
- add 方法被 synchronized 同步修辞,线程安全,没有并发异常
- 一般不采用该方案
-
解决方案-Collections
- ArrayList和HashMap都是线程不安全的,如果程序要求线程安全,我们可以将ArrayList、HashMap转换为线程的,使用synchronizedList(List list) 和 synchronizedMap(Map map)。
- 一般也不常用
-
解决方案-CopyOnWriteArrayList
-
写时复制技术
-
每次写的时候先复制一份,在复制的内容中写入新的内容,再和之前的内容进行合并为新集合,然后再读取新集合–>并发读、独立写
-
- 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();
}
}
- public boolean add(E e) {
-
-
特征
- 1.它最适合于具有以下特征的应用程序:List 大小通常保持很小,只读操作远多于可变操作,需要在遍历期间防止线程间的冲突。
-
- 它是线程安全的。
-
- 因为通常需要复制整个基础数组,所以可变操作(add()、set() 和 remove() 等等)的开销很大。
-
- 迭代器支持 hasNext(), next()等不可变操作,但不支持可变 remove()等操作。
-
- 使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突。在构造迭代器时,迭代器依赖于不变的数组快照。
-
动态数组机制
- 它内部有个“volatile 数组”(array)来保持数据。在“添加/修改/删除”数据时,都会新建一个数组,并将更新后的数据拷贝到新建的数组中,最后再将该数组赋值给“volatile 数组”, 这就是它叫做 CopyOnWriteArrayList 的原因。
- 由于它在“添加/修改/删除”数据时,都会新建数组,所以涉及到修改数据的操作,CopyOnWriteArrayList 效率很低;但是单单只是进行遍历查找的话,效率比较高。
-
线程安全机制
- 通过 volatile 和互斥锁来实现的。
- 通过“volatile 数组”来保存数据的。一个线程读取 volatile 数组时,总能看到其它线程对该 volatile 变量最后的写入;就这样,通过 volatile 提供了“读取到的数据总是最新的”这个机制的保证。
- 通过互斥锁来保护数据。在“添加/修改/删除”数据时,会先“获取互斥锁”,再修改完毕之后,先将数据更新到“volatile 数组”中,然后再“释放互斥锁”,就达到了保护数据的目的。
-
HashSet的不安全演示
- 出现的问题:ConcurrentModificationException -->并发修改异常
- 解决方案:CopyOnWriteArraySet
HashMap的不安全演示
- 出现的问题:ConcurrentModificationException -->并发修改异常
- 解决方案:ConcurrentHashMap
6、多线程锁
锁的八种情况
-
synchronized实现同步的基础:Java中的每一个对象都可以作为锁
- 对于普通同步方法,锁是当前实例对象
- 对于静态同步方法,锁是当前类的Class对象
- 对于同步方法块,锁是Synchronized括号里配置的对象
公平锁与非公平锁
-
非公平锁:出现线程饿死情况、执行效率高
公平锁:阳光普照、执行效率低 -
new ReentrantLock(boolean ss)
-
ss-true:公平锁
- hasQueuedPredecessors()–询问一下
-
ss-false:非公平锁
-
可重入锁–>又称递归锁
-
synchronized(隐式–自动上锁解锁)和Lock(显式–手动上锁解锁)都是可重入锁
- synchronized会出现栈溢出的异常:StackOverflowError
- lock锁可能会出现不释放锁的情况,如果不释放锁就使得下一个线程无法取得锁就一直阻塞。
-
相当于进入外层的锁后,内层可以随意进入。
–>说明里层和外层是同一把锁
死锁
-
概念:两个或者两个以上进程在执行过程中,因为争夺资源而造成一种互相等待的现象,如果没有外力干涉,他们再无法再执行下去。
-
产生死锁的原因
- 系统资源不足
- 进程运行推进顺序不合适
- 资源分配不当
-
验证是否是死锁
-
命令:jps
- 先查看当前的进程
-
命令:jstack
- 再利用jvm自带堆栈跟踪工具查看是否是死锁
-
7、Callable接口
创建多线程的方式
-
实现Callable接口
-
实现类的创建
-
创建一个实现Callable的实现类
-
实现call方法,将此线程需要执行的操作声明在call()中,call()有返回值
-
创建Callable接口实现类的对象
-
将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
-
将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
-
可选:获取Callable中call方法的返回值
get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
-
-
-
Ruunable接口和Callable接口之间的区别
-
是否有返回值
- Runnable接口无返回值
- Callable接口有返回值
-
是否抛出异常
- Runnable接口无法抛出经过检查的异常。
- Callable接口可以抛出经过检查的异常。
-
实现方法名称不同
- Runnable接口是run()方法
- Callable接口是call()方法
-
FutureTask类
-
Runnable接口的实现类
-
构造器可以传递Callable
-
原理
- 在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给 Future 对象在后台完成, 当主线程将来需要时,就可以通过 Future对象获得后台作业的计算结果或者执行状态
- • 一般 FutureTask 多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果
- • 仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法。一旦计算完成,就不能再重新开始或取消计算。get 方法而获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异常。
- • get 只计算一次,因此 get 方法放到最后
Future接口
-
当 call()方法完成时,结果必须存储在主线程已知的对象中,以便主线程可以知道该线程返回的结果。为此,可以使用 Future 对象。
-
将 Future 视为保存结果的对象–它可能暂时不保存结果,但将来会保存(一旦Callable 返回)。
-
方法
-
public boolean cancel(boolean mayInterrupt):用于停止任务。
- 如果尚未启动,它将停止任务。如果已启动,则仅在 mayInterrupt 为 true时才会中断任务。
-
public Object get()抛出 InterruptedException,ExecutionException:用于获取任务的结果。
- 如果任务完成,它将立即返回结果,否则将等待任务完成,然后返回结果。
-
public boolean isDone():如果任务完成,则返回 true,否则返回 false
-
-
要创建线程,需要 Runnable。为了获得结果,需要 future。
8、辅助类-解决线程数量过多时Lock锁的频繁操作
CountDownLatch:减少计数
-
方法
-
CountDownLatch(int count)
- 构造一个用给定计数初始化的CountDownLatch
-
await()
- 使当前线程在锁存器倒计数至0之前一直等待,除非线程被中断
-
countDown()
- 递减锁存器计数,如果计数到达0,则释放所有等待的线程。
-
-
使用
- 创建CountDownLatch对象,设置初始值
- 在线程里调用countDown();
- 最后调用await(),如果为0,则接着执行以下的命令,如果不为0,则一直等待。
CyclicBarrier:循环栅栏
-
一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点。在涉及一组固定大小的线程程序中,这些线程必须不时地互相等待,此时CyclicBarrier很有用。
-
方法
-
CyclicBarrier(int paties,Runnable barrierAction)
- 创建一个新的CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,并在启动barrier时执行给定的屏障操作,该操作由最后一个进入barrier的线程执行。
-
await()
- 在所有参与者都已经在此barrier上调用await方法之前,将一直等待。
-
-
使用
- 创建CyclicBarrier对象
- 如果没有到paties,就调用await()方法,一直等待
Semaphore:信号灯
-
方法
-
acquire()
- 从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断
-
Semaphore(int permits,boolean fair)
- 创建具有给定的许可数和给定的公平设置的Semaphore
-
release()
- 释放一个许可,将其返回给信号量
-
-
使用
- 创建Semaphore对象,设置许可数量
- 抢占acquire()
- 最后释放许可release()
9、ReentrantReadWriteLock读写锁
悲观锁与乐观锁
-
乐观锁
- 支持并发操作,通过版本号来控制并发操作
-
悲观锁
- 不支持并发操作,只能一个一个执行,效率很低
表锁与行锁
- 表锁:对整张表进行锁,只能自己操作该表
- 行锁:对表中的某一行进行锁,只能自己操作该行,但不影响其他线程操作其他行–会出现死锁状况
读锁与写锁
-
读锁:共享锁,会出现死锁状况
- 线程1和线程2在读取同一个数据,线程1想要进行修改等待线程2读完,线程2也要进行修改在等线程1读完,这样会产生死锁情况
-
写锁:独占锁,会出现死锁状况
- 线程1在写第一行数据,线程2在写第二行数据,线程1想要进行操作第二行数据需要等待线程2写完,线程2想要进行操作第一行数据需要等待线程1写完,这样会产生死锁情况
-
读写锁使用
-
线程进入读锁的前提条件
- 没有其他线程的写锁
- 没有写请求, 或者==有写请求,但调用线程和持有锁的线程是同一个(可重入锁)
-
线程进入写锁的前提条件
- 没有其他线程的读锁
- 没有其他线程的写锁
-
读写锁的重要特性
- (1)公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。
- (2)重进入:读锁和写锁都支持线程重进入。
- (3)锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。
-
-
案例
class MyCache{
//创建Map集合
private volatile Map<String,Object> map = new HashMap<>();
private ReadWriteLock rwLock = new ReentrantReadWriteLock() ;
//放数据
public void put(String key,Object value){
//添加写锁
rwLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "写操作:"+key);
//暂停一会
TimeUnit.MICROSECONDS.sleep(300);
//放数据
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "写完了" + key);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
rwLock.writeLock().unlock();
}
}
//取数据
public Object get(String key){
//添加读锁
rwLock.readLock().lock();
Object res = null;
try {
System.out.println(Thread.currentThread().getName() + "读操作:"+key);
//暂停一会
TimeUnit.MICROSECONDS.sleep(300);
//放数据
res = map.get(key);
System.out.println(Thread.currentThread().getName() + "读完了" + key);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//释放读锁
rwLock.readLock().unlock();
}
return res;
}
}
public class ReadWriteDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
for(int i = 1; i <= 5 ;i++ ){
final int num = i;
new Thread(()->
{
myCache.put(num+ "",num+ "");
},String.valueOf(i)
).start();
}
for(int i = 1; i <= 5 ;i++ ){
final int num=i;
new Thread(()->
{
myCache.get(num+ "");
},String.valueOf(i)
).start();
}
}
}
- volatile关键字是防止在共享的空间发生读取的错误。只保证其可见性,不保证原子性;使用volatile指每次从内存中读取数据,而不是从编译器优化后的缓存中读取数据,简单来讲就是防止编译器优化
读写锁的演变
-
演变过程
-
读写锁的降级
public class Demo1 {
public static void main(String[] args) {
ReentrantReadWriteLock rwlock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock rlock = rwlock.readLock();
ReentrantReadWriteLock.WriteLock wlock = rwlock.writeLock();
//1、获取写锁
wlock.lock();
System.out.println("hahhah");
//2、获取读锁
rlock.lock();
System.out.println("---read");
wlock.unlock();
rlock.unlock();
}
}
10、BlockingQueue阻塞队列
阻塞队列的概述
-
通过一个共享的队列,可以使得数据由队列的一端输入,从另外一端输出。
- 当队列是空的,从队列中获取元素的操作将会被阻塞
- 当队列是满的,从队列中添加元素的操作将会被阻塞
- 试图从空的队列中获取元素的线程将会被阻塞,直到其他线程往空的队列插入新的元素
- 试图向已满的队列中添加新元素的线程将会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空,使队列变得空闲起来并后续新增
-
所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤起
阻塞队列的方法
-
放入数据
-
获取数据
阻塞队列的分类
-
ArrayBlockingQueue
- 由数组结构组成的有界阻塞队列。
-
LinkedBlockingQueue
- 由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列。
-
DelayQueue
- 使用优先级队列实现的延迟无界阻塞队列。
-
PriorityBlockingQueue
- 支持优先级排序的无界阻塞队列。
-
SynchronousQueue
- 不存储元素的阻塞队列,也即单个元素的队列。
-
LinkedTransferQueue
- 由链表组成的无界阻塞队列。
-
LinkedBlockingDeque
- 由链表组成的双向阻塞队列
11、ThreadPool线程池
线程池的介绍
-
一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。
-
特点
- 降低资源的消耗
- 提高响应速度
- 提高线程的可管理性
-
线程池的优势
- 线程池做的工作只要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。
线程池的架构
- Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor,Executors,ExecutorService,ThreadPoolExecutor 这几个类
线程池使用方式-都不用,可能会导致OOM异常
-
Executors.newFixedThreadPool(int)
- 一池N线程
-
Executors.newSingleThreadExecutor()
- 一个任务一个任务执行,一池一线程
-
Executors.newCachedThreadPool()
- 线程池根据需求创建线程,可扩容,遇强则强
线程池的底层原理
-
以上三个使用方式都是new ThreadPoolExecutor()
-
七个参数
- corePoolSize 线程池的核心线程数
- • maximumPoolSize 能容纳的最大线程数
- • keepAliveTime 空闲线程存活时间
- • unit 存活的时间单位
- • workQueue 存放提交但未执行任务的队列
- • threadFactory 创建线程的工厂类
- • handler 等待队列满后的拒绝策略
线程池底层工作流程
-
底层工作流程
-
- 在创建了线程池后,线程池中的线程数为零
-
- 当调用 execute()方法添加一个请求任务时,线程池会做出如下判断:
- 2.1 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
- 2.2 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
- 2.3 如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
- 2.4 如果队列满了且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
-
- 当一个线程完成任务时,它会从队列中取下一个任务来执行
-
- 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:
- 4.1 如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。
- 4.2 所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
-
-
四种拒绝策略
- AbortPolicy:直接抛出RejectedExecutionException异常阻止系统正常运行
- CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不抛弃任务,也不会抛出异常,而是将某些任务退回到调用者,从而降低新任务的流量
- Discard OlderstPolicy:抛弃队列中等待最久的任务,然后把当前的任务加入队列中,尝试再次提交当前任务
- Discardpolicy:该策略默默丢弃无法处理的任务,不予任何处理也不抛出异常,如果允许任务丢失,这是最好的一种策略。
自定义线程池–一般常用
- 创建线程池推荐适用 ThreadPoolExecutor 及其 7 个参数手动创建
12、Fork/Join分支合并框架
Fork/Join的概念
-
Fork/Join 它可以将一个大的任务拆分成多个子任务进行并行处理,最后将子任务结果合并成最后的计算结果,并进行输出。
- Fork:把一个复杂任务进行分拆,大事化小
- Join:把分拆任务的结果进行合并
-
举例
- 采用的方法
- 采用的方法
class Mytask extends RecursiveTask<Integer>{
//拆分差值不能超过10,计算10以内的计算
private static final Integer VALUE = 10;
private int begin;
private int end;
private int result;
//创建有参构造
public Mytask(int begin ,int end){
this.begin = begin;
this.end = end;
}
//拆分合并过程
@Override
protected Integer compute() {
//判断相加两个数值是否大于10
if((end - begin)<=VALUE){
for (int i = begin; i <= end ; i++) {
result = result+i;
}
}else{
//获取中间值
int middle = (begin+end)/2;
//拆开左边
Mytask task1= new Mytask(begin,middle);
//拆开右边
Mytask task2 = new Mytask(middle+1,end);
//调用方法拆分
task1.fork();
task2.fork();
//合并结果
result = task1.join()+task2.join();
}
return result;
}
}
public class forkjoinTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Mytask mytask = new Mytask(0, 100);
//创建分支合并对象
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Integer> forkJoinTask = forkJoinPool.submit(mytask);
//获取合并之后的结果
Integer result = forkJoinTask.get();
System.out.println(result);
//关闭池对象
forkJoinPool.shutdown();
}
}
13、CompletableFuture异步回调
CompletetableFuture
-
没有返回值的异步调用
- runAsync()
-
有返回值的异步调用
- supplyAsync()