当前位置:网站首页 > 技术博客 > 正文

狂神git笔记



源码+官方文档

JUC是 java util concurrent

面试高频问JUC~!

在这里插入图片描述

java.util 是Java的一个工具包~

业务:普通的线程代码 Thread

Runnable: 没有返回值、效率相比于Callable 相对较低!

在这里插入图片描述

进程:一个程序,.EXE Music.EXE;数据+代码+pcb

一个进程可以包含多个线程,至少包含一个线程!

Java默认有几个线程?2个线程! main线程、GC线程

线程:开了一个进程Typora,写字,等待几分钟会进行自动保存(线程负责的)

对于Java而言:Thread、Runable、Callable进行开启线程的,我们之前。

提问?JAVA真的可以开启线程吗? 开不了的!

 

Java是没有权限去开启线程、操作硬件的,这是一个native的一个本地方法,它调用的底层的C++代码。

并发、并行

并发: 多线程操作同一个资源。

  • CPU 只有一核,模拟出来多条线程,天下武功,唯快不破。那么我们就可以使用CPU快速交替,来模拟多线程。

并行: 多个人一起行走

  • CPU多核,多个线程可以同时执行。 我们可以使用线程池!
 

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

线程有几个状态?

线程的状态:6个状态

 

wait/sleep的区别

1、来自不同的类

wait => Object

sleep => Thread

一般情况企业中使用休眠是:

 

2、关于锁的释放

wait 会释放锁;

sleep睡觉了,不会释放锁;

3、使用的范围是不同的

wait 必须在同步代码块中;

sleep 可以在任何地方睡;

4、是否需要捕获异常

wait是不需要捕获异常;

sleep必须要捕获异常;

传统的Synchronized

 

Lock接口

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

公平锁: 十分公平,必须先来后到~;

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

 

Synchronized 和 Lock区别

  • 1、Synchronized 内置的Java关键字,Lock是一个Java类
  • 2、Synchronized 无法判断获取锁的状态,Lock可以判断
  • 3、Synchronized 会自动释放锁,lock必须要手动加锁和手动释放锁!可能会遇到死锁
  • 4、Synchronized 线程1(获得锁->阻塞)、线程2(等待);

    lock就不一定会一直等待下去,lock会有一个trylock去尝试获取锁,不会造成长久的等待。

  • 5、Synchronized 是可重入锁,不可以中断的,非公平的;Lock,可重入的,可以判断锁,可以自己设置公平锁和非公平锁;
  • 6、Synchronized 适合锁少量的代码同步问题,Lock适合锁大量的同步代码;

锁到底是什么? 如何判断锁的是谁?

Synchronized wait notify可以实现,该方法是传统版本;

我们这次使用lock版本

Synchronized版本

 

问题存在,A线程B线程,现在如果我有四个线程A B C D!

在这里插入图片描述

解决方案if 改为while即可,防止虚假唤醒

这样就不存在问题了:

在这里插入图片描述

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

await、signal 替换 wait、notify

在这里插入图片描述

通过Lock找到Condition

在这里插入图片描述

 

Condition的优势:精准的通知和唤醒的线程!

如果我们要指定通知的下一个进行顺序怎么办呢? 我们可以使用Condition来指定通知进程~

 

在这里插入图片描述

如何判断锁的是谁!锁到底锁的是谁?

锁会锁住:对象、Class

深刻理解我们的锁

  • 问题1:

在这里插入图片描述

结果是:先发短信,如何再打电话!

为什么? 如果你认为是顺序在前? 这个答案是错误的!

  • 问题2:

我们再来看:我们让发短信 延迟4s

在这里插入图片描述

现在结果是什么呢?

结果:还是先发短信,然后再打电话!

why?

原因:并不是顺序执行!是因为synchronized 锁的对象是方法的调用!对于两个方法用的是同一个锁,谁先拿到谁先执行!另外一个则等待!
  • 问题3:

如果我们添加一个普通方法,那么先执行哪一个呢?

在这里插入图片描述

答案是:先执行hello,然后再执行发短信!原因是hello是一个普通方法不受synchronized锁的影响,但是我发现,如果我把发短信里面的延迟4秒去掉,那么就会顺序执行,先执行发短信然后再执行hello,原因应该是顺序执行的原因吧,不是太理解

  • 问题4:

如果我们使用的是两个对象,一个调用发短信,一个调用打电话,那么整个顺序是怎么样的呢?

在这里插入图片描述

答案是:先打电话,后发短信。原因:在发短信方法中延迟了4s,又因为synchronized锁的是对象,但是我们这使用的是两个对象,所以每个对象都有一把锁,所以不会造成锁的等待。正常执行

  • 问题5,6:

如果我们把synchronized的方法加上static变成静态方法!那么顺序又是怎么样的呢?

(1)我们先来使用一个对象调用两个方法!

答案是:先发短信,后打电话

(2)如果我们使用两个对象调用两个方法!

答案是:还是先发短信,后打电话

原因是什么呢? 为什么加了static就始终前面一个对象先执行呢!为什么后面会等待呢?

原因是:对于static静态方法来说,对于整个类Class来说只有一份,对于不同的对象使用的是同一份方法,相当于这个方法是属于这个类的,如果静态static方法使用synchronized锁定,那么这个synchronized锁会锁住整个对象!不管多少个对象,对于静态的锁都只有一把锁,谁先拿到这个锁就先执行,其他的进程都需要等待!

  • 问题7:

如果我们使用一个静态同步方法、一个同步方法、一个对象调用顺序是什么?

在这里插入图片描述

明显答案是:先打电话,后发短信了。

因为一个锁的是Class类模板,一个锁的是对象调用者。后面那个打电话不需要等待发短信,直接运行就可以了。

  • 问题8:

如果我们使用一个静态同步方法、一个同步方法、两个对象调用顺序是什么呢?

在这里插入图片描述

当然答案是:先打电话、后发短信!

因为两个对象,一样的原因:两把锁锁的不是同一个东西,所以后面的第二个对象不需要等待第一个对象的执行。

小结

new 出来的 this 是具体的一个对象

static Class 是唯一的一个模板

List不安全

我们来看一下List这个集合类:

 

会造成:

在这里插入图片描述

ArrayList 在并发情况下是不安全的!

解决方案

1、切换成Vector就是线程安全的啦!

在这里插入图片描述

2、使用Collections.synchronizedList(new ArrayList<>());

 

3、使用JUC中的包:List arrayList = new CopyOnWriteArrayList<>();

 

CopyOnWriteArrayList:写入时复制! COW 计算机程序设计领域的一种优化策略

多个线程调用的时候,list,读取的时候,固定的,写入(存在覆盖操作);在写入的时候避免覆盖,造成数据错乱的问题;

CopyOnWriteArrayListVector厉害在哪里?

Vector底层是使用synchronized关键字来实现的:效率特别低下。

在这里插入图片描述

CopyOnWriteArrayList使用的是Lock锁,效率会更加高效!

在这里插入图片描述

Set不安全

在这里插入图片描述

和List、Set同级的还有一个BlockingQueue 阻塞队列;

Set和List同理可得: 多线程情况下,普通的Set集合是线程不安全的;

解决方案还是两种:

  • 使用Collections工具类的synchronized包装的Set类
  • 使用CopyOnWriteArraySet 写入复制的JUC解决方案
 
HashSet底层是什么?

hashSet底层就是一个HashMap

 

Map不安全

回顾map的基本操作:

 

默认加载因子是0.75,默认的初始容量是16

在这里插入图片描述

同样的HashMap基础类也存在并发修改异常

 

结果同样的出现了:异常java.util.ConcurrentModificationException 并发修改异常

解决方案:

  • 使用Collections.synchronizedMap(new HashMap<>());处理
  • 使用ConcurrentHashMap进行并发处理
TODO:研究ConcurrentHashMap底层原理:

这里我们可以直接去研究一下,这个也是相当重要的。

在这里插入图片描述

1、可以有返回值;
2、可以抛出异常;
3、方法不同,run()/call()

代码测试

传统使用线程方式:

 

使用Callable进行多线程操作:

在这里插入图片描述

Calleable 泛型T就是call运行方法的返回值类型

但是如何使用呢?

Callable怎么放入到Thread里面呢?

源码分析:

在这里插入图片描述

对于Thread运行,只能传入Runnable类型的参数;

我们这是Callable 怎么办呢?

看JDK api文档:

在Runnable里面有一个叫做FutureTask的实现类,我们进去看一下。

在这里插入图片描述

FutureTask中可以接受Callable参数;

在这里插入图片描述

这样我们就可以先把Callable 放入到FutureTask中, 如何再把FutureTask 放入到Thread就可以了。

 

这样我们就可以使用Callable来进行多线程编程了,并且我们发现可以有返回值,并且可以抛出异常。

在这里插入图片描述

注意两个重点:

在这里插入图片描述

8.1 CountDownLatch

在这里插入图片描述

其实就是一个减法计数器,对于计数器归零之后再进行后面的操作,这是一个计数器!

 

主要方法:

  • countDown 减一操作;
  • await 等待计数器归零。

await等待计数器为0,就唤醒,再继续向下运行。

8.2 CyclickBarrier

在这里插入图片描述

其实就是一个加法计数器;

 

8.3 Semaphore

Semaphore:信号量

抢车位:

3个车位 6辆车:

 

原理:

semaphore.acquire()获得资源,如果资源已经使用完了,就等待资源释放后再进行使用!

semaphore.release()释放,会将当前的信号量释放+1,然后唤醒等待的线程!

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

先对于不加锁的情况:

如果我们做一个我们自己的cache缓存。分别有写入操作、读取操作;

我们采用五个线程去写入,使用十个线程去读取。

我们来看一下这个的效果,如果我们不加锁的情况!

 

运行效果如下:

 

所以如果我们不加锁的情况,多线程的读写会造成数据不可靠的问题。

我们也可以采用synchronized这种重量锁和轻量锁 lock去保证数据的可靠。

但是这次我们采用更细粒度的锁:ReadWriteLock 读写锁来保证

在这里插入图片描述

 

运行结果如下:

 

阻塞

队列

在这里插入图片描述

阻塞队列jdk1.8文档解释:

在这里插入图片描述

BlockingQueue

blockingQueue 是Collection的一个子类;

什么情况我们会使用 阻塞队列呢?

 

在这里插入图片描述

整个阻塞队列的家族如下:Queue以下实现的有Deque、AbstaractQueue、BlockingQueue;

BlockingQueue以下有Link链表实现的阻塞队列、也有Array数组实现的阻塞队列

如何使用阻塞队列呢?


操作:添加、移除

但是实际我们要学的有:

四组API

方式抛出异常不会抛出异常,有返回值阻塞 等待超时 等待添加addofferputoffer(timenum,timeUnit)移除removepolltakepoll(timenum,timeUnit)判断队列首elementpeek--
 

SynchronousQueue同步队列

同步队列 没有容量,也可以视为容量为1的队列

进去一个元素,必须等待取出来之后,才能再往里面放入一个元素;

put方法 和 take方法;

Synchronized 和 其他的BlockingQueue 不一样 它不存储元素;

put了一个元素,就必须从里面先take出来,否则不能再put进去值!

并且SynchronousQueue 的take是使用了lock锁保证线程安全的。

 

在这里插入图片描述

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

池化技术

程序的运行,本质:占用系统的资源!我们需要去优化资源的使用 ===> 池化技术

线程池、JDBC的连接池、内存池、对象池 等等。。。。

资源的创建、销毁十分消耗资源

池化技术:事先准备好一些资源,如果有人要用,就来我这里拿,用完之后还给我,以此来提高效率。

线程池的好处:

1、降低资源的消耗;

2、提高响应的速度;

3、方便管理;

线程复用、可以控制最大并发数、管理线程;

线程池:三大方法

  • ExecutorService threadPool = Executors.newSingleThreadExecutor();//单个线程
  • ExecutorService threadPool2 = Executors.newFixedThreadPool(5); //创建一个固定的线程池的大小
  • ExecutorService threadPool3 = Executors.newCachedThreadPool(); //可伸缩的
 

7大参数

源码分析

 
 
 

本质:三种方法都是开启的ThreadPoolExecutor

 

在这里插入图片描述

阿里巴巴的Java操作手册中明确说明:对于Integer.MAX_VALUE初始值较大,所以一般情况我们要使用底层的ThreadPoolExecutor来创建线程池。

业务图

在这里插入图片描述

手动创建线程池

// todo

拒绝策略4种

在这里插入图片描述

(1)new ThreadPoolExecutor.AbortPolicy(): //该拒绝策略为:银行满了,还有人进来,不处理这个人的,并抛出异常

超出最大承载,就会抛出异常:队列容量大小+maxPoolSize

在这里插入图片描述

(2)new ThreadPoolExecutor.CallerRunsPolicy(): //该拒绝策略为:哪来的去哪里 main线程进行处理

在这里插入图片描述

(3)new ThreadPoolExecutor.DiscardPolicy(): //该拒绝策略为:队列满了,丢掉异常,不会抛出异常。

在这里插入图片描述

(4)new ThreadPoolExecutor.DiscardOldestPolicy(): //该拒绝策略为:队列满了,尝试去和最早的进程竞争,不会抛出异常

在这里插入图片描述

小结和拓展


如何去设置线程池的最大大小如何去设置?
CPU密集型和IO密集型!

1、CPU密集型:电脑的核数是几核就选择几;选择maximunPoolSize的大小

在这里插入图片描述

我们可以使用代码来来获取逻辑处理器数量。

于是cpu密集型的写法如下:

在这里插入图片描述

2、I/O密集型:

在程序中有15个大型任务,io十分占用资源;I/O密集型就是判断我们程序中十分耗I/O的线程数量,大约是最大I/O数的一倍到两倍之间。

新时代的程序员:lambda表达式、链式编程、函数式接口、Stream流式计算

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

 

在这里插入图片描述

函数型接口可以使用lambda表达式;

代码测试:

Function函数型接口

在这里插入图片描述

 

Predicate断定型接口

在这里插入图片描述

 

Consummer 消费型接口

在这里插入图片描述

 

Supplier供给型接口

在这里插入图片描述

 

什么是Stream流式计算?

存储+计算

存储:集合、MySQL

计算:流式计算~

=== 链式编程 ===
 

什么是ForkJoin?

ForkJoin 在JDK1.7,并行执行任务!提高效率~。在大数据量速率会更快!

大数据中:MapReduce 核心思想->把大任务拆分为小任务!

在这里插入图片描述

ForkJoin 特点: 工作窃取!

实现原理是:双端队列!从上面和下面都可以去拿到任务进行执行!

在这里插入图片描述

如何使用ForkJoin?

  • 1、通过ForkJoinPool来执行
  • 2、计算任务 execute(ForkJoinTask<?> task)

在这里插入图片描述

  • 3、计算类要去继承ForkJoinTask;

ForkJoin的计算类!

 

测试类!

 

在这里插入图片描述

.parallel().reduce(0, Long::sum)使用一个并行流去计算整个计算,提高效率。

在这里插入图片描述

reduce方法的优点:

在这里插入图片描述

Future 设计的初衷:对将来的某个事件结果进行建模!

其实就是前端 --> 发送ajax异步请求给后端

在这里插入图片描述

但是我们平时都使用CompletableFuture

(1)没有返回值的runAsync异步回调
 
(2)有返回值的异步回调supplyAsync
 

whenComplete: 有两个参数,一个是t 一个是u

T:是代表的 正常返回的结果

U:是代表的 抛出异常的错误信息

如果发生了异常,get可以获取到exceptionally返回的值;

请你谈谈你对Volatile 的理解

Volatile 是 Java 虚拟机提供 轻量级的同步机制

1、保证可见性
2、不保证原子性
3、禁止指令重排

什么是JMM?

JMM:JAVA内存模型,不存在的东西,是一个概念,也是一个约定!

关于JMM的一些同步的约定:

1、线程解锁前,必须把共享变量立刻刷回主存;

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

3、加锁和解锁是同一把锁;

线程中分为 工作内存、主内存

8种操作:

  • Read(读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用;
  • load(载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中;
  • Use(使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令;
  • assign(赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中;
  • store(存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用;
  • write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中;
  • lock(锁定):作用于主内存的变量,把一个变量标识为线程独占状态;
  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;

在这里插入图片描述

在这里插入图片描述

JMM对这8种操作给了相应的规定

  • 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
  • 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
  • 不允许一个线程将没有assign的数据从工作内存同步回主内存
  • 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过assign和load操作
  • 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
  • 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
  • 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
  • 对一个变量进行unlock操作之前,必须把此变量同步回主内存

在这里插入图片描述

遇到问题:程序不知道主存中的值已经被修改过了!;

1、保证可见性

 

2、不保证原子性

原子性:不可分割;

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

 

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

在这里插入图片描述

解决方法:使用JUC下的原子包下的class;

在这里插入图片描述

代码如下:

 

这些类的底层都直接和操作系统挂钩!是在内存中修改值。

Unsafe类是一个很特殊的存在;

原子类为什么这么高级?

3、禁止指令重排

什么是指令重排?

我们写的程序,计算机并不是按照我们自己写的那样去执行的

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

处理器在进行指令重排的时候,会考虑数据之间的依赖性!

 

可能造成的影响结果:前提:a b x y这四个值 默认都是0

线程A线程Bx=ay=bb=1a=2

正常的结果: x = 0; y =0;

线程A线程Bx=ay=bb=1a=2

可能在线程A中会出现,先执行b=1,然后再执行x=a;

在B线程中可能会出现,先执行a=2,然后执行y=b;

那么就有可能结果如下:x=2; y=1.

volatile可以避免指令重排:

volatile中会加一道内存的屏障,这个内存屏障可以保证在这个屏障中的指令顺序。

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

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

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

在这里插入图片描述

总结

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

面试官:那么你知道在哪里用这个内存屏障用得最多呢?单例模式

饿汉式、DCL懒汉式

饿汉式

 

DCL懒汉式

 

静态内部类

 

单例不安全, 因为反射

枚举

 

使用枚举,我们就可以防止反射破坏了。

在这里插入图片描述

枚举类型使用JAD最终反编译后源码:

如果我们看idea 的文件:会发现idea骗了我们,居然告诉我们是有有参构造的,我们使用jad进行反编译。

在这里插入图片描述

在这里插入图片描述

 

什么是CAS?

大厂必须深入研究底层!!!!修内功!操作系统、计算机网络原理、组成原理、数据结构

 

在这里插入图片描述

Unsafe类

在这里插入图片描述

在这里插入图片描述

总结:

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

缺点:

  • 循环会耗时;
  • 一次性只能保证一个共享变量的原子性;
  • 它会存在ABA问题

CAS:ABA问题?(狸猫换太子)

在这里插入图片描述

线程1:期望值是1,要变成2;

线程2:两个操作:

  • 1、期望值是1,变成3
  • 2、期望是3,变成1

所以对于线程1来说,A的值还是1,所以就出现了问题,骗过了线程1;

 

解决ABA问题,对应的思想:就是使用了乐观锁~

带版本号的 原子操作!

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

在这里插入图片描述

所以如果遇到,使用大于128的时候,使用原子引用的时候,如果超过了这个值,那么就不会进行版本上升!

在这里插入图片描述

那么如果我们使用小于128的时候:

在这里插入图片描述

正常业务操作中,我们一般使用的是一个个对象,一般情况不会遇到这种情况。


1、公平锁、非公平锁

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

 

非公平锁:非常不公平,允许插队的,可以改变顺序。

 
2、可重入锁

可重入锁(递归锁)

在这里插入图片描述

Synchronized锁

 

lock锁

 
  • lock锁必须配对,相当于lock和 unlock 必须数量相同;
  • 在外面加的锁,也可以在里面解锁;在里面加的锁,在外面也可以解锁;
3、自旋锁

spinlock

 

自我设计自旋锁:

 
 

运行结果:

t2进程必须等待t1进程Unlock后,才能Unlock,在这之前进行自旋等待。。。。

在这里插入图片描述

4、死锁

死锁是什么?

在这里插入图片描述

死锁测试,怎么排除死锁:

 

解决问题

1、使用jps定位进程号,jdk的bin目录下: 有一个jps

命令:

在这里插入图片描述

2、使用 进程进程号 找到死锁信息

在这里插入图片描述

一般情况信息在最后:

在这里插入图片描述

面试,工作中!排查问题!

1、日志

2、堆栈信息

}

 

}

 

运行结果:

t2进程必须等待t1进程Unlock后,才能Unlock,在这之前进行自旋等待。。。。

[外链图片转存中...(img-G3eL8ITC-1595850751626)]

4、死锁

死锁是什么?

在这里插入图片描述

死锁测试,怎么排除死锁:

 

解决问题

1、使用jps定位进程号,jdk的bin目录下: 有一个jps

命令:

[外链图片转存中...(img-LeKLRYJj-1595850751627)]

2、使用 进程进程号 找到死锁信息

[外链图片转存中...(img-kQeUM5lC-1595850751627)]

一般情况信息在最后:

[外链图片转存中...(img-DypkNFBU-1595850751627)]

面试,工作中!排查问题!

1、日志

2、堆栈信息

学习来自:JUC学习

版权声明


相关文章:

  • 网络攻防战是什么2025-01-14 17:30:05
  • sigmoid激活函数作用2025-01-14 17:30:05
  • 倒排索引原理和实现2025-01-14 17:30:05
  • xujc在线编译系统2025-01-14 17:30:05
  • logistic逻辑回归分析2025-01-14 17:30:05
  • 备忘录形式是什么样的2025-01-14 17:30:05
  • debian镜像下载2025-01-14 17:30:05
  • 运算符重载怎么写2025-01-14 17:30:05
  • 电脑阅读软件多种格式哪个好用2025-01-14 17:30:05
  • ssh工具有哪些2025-01-14 17:30:05