当前位置: 首页 > news >正文

八股训练--JUC

目录

一、引言

二、多线程

1.使用多线程要注意什么问题

2.保证数据一致性的方案

3.线程的创建方式有哪些

4.怎么启动线程

5.如何停止一个线程的运行

6.Java线程状态有哪些

7.sleep和wait的区别

8.blocked和waiting的区别

9.不同线程之间是如何通信的

10.线程间通信方式有哪些

三、并发安全

1.介绍一下juc包下常用的类

2.如何保证多线程安全

3.Java有哪些锁

4.synchronized锁的是什么

5.介绍一下CountDownLatch

6.synchronized和reentrantLock的使用

7.synchronized和reentrantlock的区别

8.synchronized的锁升级

9.JVM对synchronized的优化

10.介绍一下AQS

state状态

FIFO队列

实现获取/释放

11.介绍一下ThreadLocal

12.CAS是什么,有什么缺点

13.volatile关键字的作用

14.死锁产生的条件是什么,如何解决

四、线程池

1.线程池的工作原理

2.线程池的参数

3.线程池的分类

4.shutdown和shutdownNow的区别

5.提交给线程池的任务可以被撤回吗

五、场景题

1.交替打印200以内的奇偶数

2. 交替打印1,2,3的倍数

3.假设两个线程并发读写同一个整型变量,初始值为零,每个线程加50次,结果可能是什么?

六、总结


一、引言

  本篇文章将介绍Java的JUC包下的一些锁的信息。

二、多线程

1.使用多线程要注意什么问题

1.原子性:提供互斥访问,同一时刻只能由一个线程对数据进行操作。Java中主要是提供了一个atomic包和synchronized关键字进行的

2.可见性:一个线程对主内存的内容修改,能够被其他的线程看见。Java中主要通过volatile和synchronized关键字保证可见性

3.有序性:由于存在指令重排序,所以为了保证有序性,java中使用了happens-before来实现。

2.保证数据一致性的方案

1.事务管理:通过事务的特性(ACID)等机制控制事务中的所有操作要么一起成功,要么一起失败。

2.锁机制:通过Java的锁对公共内存进行一定的控制,通过Java中的synchronized,reentrantlock,其他锁机制控制并发访问。

3.版本控制:通过乐观锁的方式,给数据增加一个版本号,每次对数据进行操作的时候就对版本信息进行更改

3.线程的创建方式有哪些

1.继承Thread:重写run方法实现,不建议使用,因为继承只能继承一个,如果这个类还有其他要实现的东西就不适合。

2.实现runnable接口:实现runnable接口的重写run方法,来创建一个线程

3.实现callable接口和FutureTask:类似于runnable,但是callable可以自定义返回值和自定义异常的抛出,执行callable任务需要将它包装进一个FutureTask,因为Thread类只接受Runnable的参数,而FutureTask实现了Runnable接口

4.线程池:主要是利用了juc下面的Executors类,为了避免一些线程频繁的创建和销毁,就创建一个线程池,任务来的时候就用线程池中的线程来处理,没有任务就将线程放入线程池中。线程池能够有效控制运行的线程数量,防止因创建过多线程导致的系统资源耗尽(如内存溢出)。主要目的还是提高系统的效率

4.怎么启动线程

无论是哪种方式取创建一个线程,启动线程都是调用Thread类的start方法。

5.如何停止一个线程的运行

停止一个线程方法还是挺多的。主要是通过将其布尔类型的中断状态给修改了,初始情况是false

1.调用interrupt()方法:在run方法中判断对象的interrupted的状态,如果是中断状态就直接抛出异常,达到中断线程的操作

2.使用sleep()方法:让线程休眠一定的时间

6.Java线程状态有哪些

new:刚创建

runnable:运行中

blocked:阻塞中

waiting:等待其他线程唤醒

timed_waiting:具有等待时间的等待

terminated:线程完成执行,终止状态

7.sleep和wait的区别

1.sleep是Thread类下的一个方法,wait是object类下的一个方法

2.调用sleep之后不用释放锁,调用wait方法会将锁释放掉

3.可以在任意位置调用sleep,而wait必须在同步块中(持有锁)

4.sleep在到达一定时间之后会自动唤醒,而wait要等其他线程notify或者notifyall或者到达超市时间才能进行

最大的区别就是到底要不要释放锁,sleep不释放,wait要释放锁,但是sleep会释放掉cpu时间片,将cpu资源分配给其他处于就绪状态的线程。

8.blocked和waiting的区别

blocked:代表线程尝试去获取一把锁,但是该锁被其他线程所占有了。一旦锁被释放,如果能够抢到锁,那么线程状态变为runnbale状态。blocked是被动触发的

waiting:代表线程在等待其他线程执行某些操作。这种状态下,线程将不会消耗CPU资源,并且不会参与锁的竞争。线程得被显式唤醒,或者到达一定时间。waiting是主动触发的

9.不同线程之间是如何通信的

通过共享变量进行通信,但是为了保证线程安全,通常需要使用到synchronized和volatile关键字。

volatile关键字:可以保证内存可见性,对共享内存进行修改的时候,先修改本地内存再写入到主内存中,对共享内存进行读取的时候,先从主内存读到本地内存

10.线程间通信方式有哪些

1.Object类的wait,notify,notifyall方法

2.lock接口下的condition接口的await,signal,signal方法

3.volatile关键字

4.countdownlatch:有一个记录当前要获取锁的线程数

5.cyclebarrier:保证所有线程都到达某一种状态之后才能继续执行

6.semaphore:控制同时访问特定资源的线程数量

三、并发安全

1.介绍一下juc包下常用的类

线程池相关:

ThreadPoolExecutor:最核心的线程池类,用于创建和管理线程池

Executors:线程池工厂类

并发集合类:

ConcurrentHashMap:线程安全的哈希表,采用分段式锁来保证线程安全,感兴趣的可以去看看源码或者网上搜集一下资料,也是常见考题。

CopyOnWriteArrayList:线程安全的列表,当对数组进行修改的时候会在底层创建一个新数组,用于记录修改操作,此时原数组还可以进行读取,实现了读写分离,适合于读多写少的场景。

同步工具类:

CountDownLatch:允许一个或多个线程等待其他一组线程完成操作后再继续执行。通过计数器实现,当一个线程完成操作后,这个计数器的值就减1,直到0之后再一起执行

CyclicBarrier:也是当多个线程到达某个屏障点之后再继续执行,与CountDownLatch不同的是,CyclicBarrier可以在一个方法中可以重复使用

Semaphore:用于控制同时访问某个资源的线程数量,维护了一个许可计数器,当线程访问某个资源的时候需要先获取许可,如果通过许可,并且成功获取,这个计数器就减一。常用于控制对有限资源的访问,如数据库连接池、线程池中的线程数量等。

原子类:

AtomicInteger:通过原子级指令保证数据的原子性和线程安全性。

AtomicReference:原子引用类,用于对对象引用进行原子操作。

2.如何保证多线程安全

1.使用synchronized关键字:对象锁是通过synchronized关键字锁定对象的监视器(monitor)来实现的。

2.volatie关键字:保证内存可见性

3.Lock接口和ReentrantLock类:juc提供的一个比synchronized更厉害的锁

4.原子类

5.线程局部变量:ThreadLocal类为每一个线程都创建一个独立的变量副本,每个线程都有,就避免了线程竞争

6.并发集合或者并发工具(CountDownLatch)等

3.Java有哪些锁

1.synchronized:最经典的锁,由JVM内部实现,当一个线程进入synchronized代码块或者方法中时,会获取关联对象的锁,离开就释放锁。

synchronized加锁的过程:无所 --> 偏向锁(当没有其他线程竞争时) --> 轻量级锁(数据结构层面的锁,不涉及操作系统)  --> 重量级锁(涉及操作系统上的互斥锁)

2.ReentrantLock:比synchronized有更多的方法,如可中断锁的等待,定时锁等待,公平锁选择等

3.读写锁(ReadWriteLock):允许多个线程去读取数据,只允许一个线程去修改数据

4.乐观锁和悲观锁:

悲观锁:得到数据之后就直接进行加锁,防止数据被其他线程修改

乐观锁:拿到数据之后并不急着加锁,而是通过版本号或者时间戳来检查这个数据是否被其他线程修改过。

5.自旋锁:当没能获取到资源之后,就会选择挂起等待,使用CAS实现,如果长时间挂起等待就会导致CPU的浪费

4.synchronized锁的是什么

1.作用于实例方法:锁实例

2.作用于静态方法:锁整个class对象

5.介绍一下CountDownLatch

是juc包下的一个工具类,主要目的就是让一个或多个线程等待所有的线程执行完之后,再一起执行。核心就是存在一个计数器,常用于多线程分阶段控制和主线程等待多个子线程就绪的场景。

这个计数器一开始创建的时候会规定其大小,然后一个线程完成任务在等待的时候,计数器就-1,减到零就可以继续执行其他任务了。

6.synchronized和reentrantLock的使用

  synchronized:是JVM内部实现的锁,被称为"监视锁",底层逻辑就是会在同步代码块中添加monitorenter和monitorexit字节码指令,依赖操作系统底层的互斥锁实现,主要作用是保证操作的原子性和内存可见性。执行monitorenter的时候会去获取一把锁,锁的计数器就+1,释放锁之后,锁的计数器就-1,之后在队列中的其他线程再继续竞争锁。

使用场景:一些简单的需求,对某个具体的代码块进行加锁,内置锁的使用

  了解的更深入可以提一下waitSet和entryList。

  reentrantLock:主要是依赖于AQS(abstract queued synchronizier)实现,之后会详细介绍AQS,也是JUC中非常重要的一个抽象类

reentrantlock实现了可中断性,当线程在等待锁释放的过程中,可以被其他线程中断而提前结束等待。同时可以设置超时时间,同时其还支持多个条件变量(通过Condition接口实现

使用场景:高级锁功能需求,性能优化(比synchronized有更好的性能),复杂的操作情况下。

7.synchronized和reentrantlock的区别

1.用法不同:synchronized可以用于普通方法,静态方法,代码块等,reentrantlock只能用于代码块

2.syn是非公平锁,reen本身是非公平锁,但是可以通过修改某个参数使其变成公平锁

3.syn是JVM内部实现的,本质是”监视锁“,通过minotoreter和monitoexit实现,reen是juc包下的一个类,主要是通过抽象类AQS实现的,是Java基础库的一些东西

4.syn在获取资源失败之后,会变成自旋锁一直尝试去获取,但reen会挂起等待一段时间,比较消耗cpu资源。并且reen在遇到死锁的时候可以响应中断,而syn不行

5.syn会自动加锁和释放锁,而reen都得手动进行这些操作。

syn和reen都是可重入锁,但是他们的实现有区别,并不是特别大,reen主要通过一个计数器实现,而syn不仅有计数器还存在一个线程id,syn第一次获取锁是通过CAS的操作获取的锁。

8.synchronized的锁升级

无锁 -> 偏向锁 ->轻量级锁 -> 重量级锁

无锁:偏向锁没有开启的时候的状态,但是现在JVM是默认开启的,但是可以通过自我设置使其关闭。

偏向锁:当一个线程拿到偏向锁的时候,下次想要竞争这个锁只需要拿到线程ID和MarkWord中存储的线程id进行比较,如果相同直接获得这个锁(相当于锁偏向于这个线程),不需要进行CAS操作。

轻量级锁:通过CAS操作实现,注意:在释放锁的时候,要将挂起等待的锁进行唤醒。

重量级锁:当两个以上的线程竞争锁的时候,轻量级锁会变为重量级锁。因为CAS如果没有成功就会一直自旋,非常消耗cpu资源,升级之后,线程会被操作系统调度然后挂起。

9.JVM对synchronized的优化

1.锁膨胀:锁升级的过程,之前只有重量级锁

2.锁消除:某些情况下,如果JVM检测不到某段代码的共享和竞争,就会将锁给消除掉,来提高性能

3.锁粗化:将多个连续的加锁和解锁连接在一起,形成一个更大的锁

4.自适应自旋锁:避免了线程的挂起和恢复操作,因为挂起和恢复是需要从用户态转到内核态的,这个过程比较消耗时间

10.介绍一下AQS

AQS是juc包中的一个抽象类,很多锁都是通过AQS来实现的。

核心思想:存在一个共享资源,如果有线程要来获取就成功获取,如果资源不空闲,就将这个线程放入一个CLH队列的变体中

CLH队列:单向链表,但是AQS中的队列是根据其实现的双向队列FIFO。

AQS使用了一个volatile修饰的同步状态state,FIFO队列完成线程的排队工作,通过CAS进行state状态的修改

AQS实现了许多并发的工具:reentrantlock,cyclicBarrier,CountDownLatch,semapore等

state状态

但是在不同的工具中state代表的东西不同。在Semapore中代表剩余许可数,reen中代表锁的占有情况,countdownLatch中代表倒数的数量

FIFO队列

当资源被占用的时候,其他线程想要获取,就会将这些没有获取到的线程加入到队列中,当锁释放之后,FIFO会挑选一个合适的线程来占有这个刚刚释放的锁

实现获取/释放

在不同的实现类中,这个获取和释放是不同的。semaphore获取是acquire,countdownlatch获取是await,需要其实现类重写tryAcquire和tryRelease

11.介绍一下ThreadLocal

定义及作用:ThreadLocal本身也是一个用于保护线程安全的机制,核心思想是对于一个共享内存,要去使用这个内存的线程都会在其工作内存中有一个线程局部变量,避免了资源的共享和同步问题,这些变量对这个内存的修改仅是工作内存的修改,各自负责各自的东西。

内部结构:Thread类中有一个ThreadLocalMap,里面维护了一个数组,数组的结构又是key-value的形式,key是ThreadLocal本身,value是ThreadLocal的泛型对象值

方法

get方法去检查当前的ThreadLocalMap中有没有于其关联的值,有就返回,没有就去调用initialValue()来初始化这个值,然后将其放入ThreadLocalMap中并返回

set方法:将传入的值和当前线程关联起来,ThreadLocalMap中存储一个键值对,key是ThreadLocal对象本身,value是传入的值

remove方法:会从当前线程的ThreadLocalMap中移除该ThreadLocal对象关联的条目

存在的问题

当线程结束之后,ThreadLocalMap也会销毁,但是ThreadLocal对象本身还存在,要显式调用remove方法,以免出现内存泄漏问题

12.CAS是什么,有什么缺点

介绍:CAS全称是compare and swap,比较和交换,当线程想要修改某个值的时候,获取到当前值之后与本地内存的期望值进行比较,如果相同则对这个值进行修改,如果不同则报错交由程序员进行决定。

缺点:出现ABA问题:从10改成了20又改成了10,这样还是会进行CAS,但是逻辑出错了(通过加入版本号进行解决

循环时间长开销大:自选CAS的操作如果一直自旋,会导致CPU消耗过大

只能保证一个变量的原子性操作:只能对单一变量进行使用,多个变量就无法使用。

13.volatile关键字的作用

1.保证内存可见性:任何线程对于某个资源的修改都要刷新到主内存中,对数据的读取也是先从主内存中读取到工作内存。

2.禁止指令重排序:写写屏障,读写屏障,写读屏障

缺点:只能保证可见性,不能保证原子性,在多线程并发的问题下是不能保证线程安全的。

14.死锁产生的条件是什么,如何解决

1. 占有且请求:一个线程占有一个资源的同时,在请求着另外一个线程所占有的资源

2. 互斥条件:一个资源被一个线程占有了,不能再被另外的线程占有

3.不可剥夺:线程不能强行从另外的线程中剥夺资源过来

4.循环条件:A等待B释放资源,B等待C释放资源

解决方法:破坏其中一个条件,最基础的就是线程1和2要使用资源的顺序是一致的。

四、线程池

1.线程池的工作原理

当一个任务来了,如果线程池中的线程数还没到达核心线程数就创建一个线程执行,如果已经到达核心线程数了,就将任务放入到队列中,如果队列也满了,又有任务来了,就继续创建线程直至到最大线程数。到达最大线程数,还是有很多任务,就会对这些任务进行一系列的淘汰机制。

2.线程池的参数

1.核心线程数   2.最大线程数  3.最大线程数的空闲时间  4.空闲时间的单位 

5.工厂模式:给线程取名字等操作  6.工作队列  7.拒绝策略(不再接收新的,随机淘汰一个,淘汰最老的任务)

3.线程池的分类

1.ScheduledThreadPool:可以设置定期时间的线程池

2.FixedThreadPool:核心线程数和最大线程数一致(相当于没有最大线程数

3.CachedThreadPool:缓存线程池,线程数几乎可以无限增加,执行完了就对线程进行回收,线程数量不固定

4.SingleThreadExecutor:只有一个线程,适合于顺序执行任务的场景

5.SingleThreadScheduledExecutor:只有一个线程的可定时的线程池

4.shutdown和shutdownNow的区别

shutdown:还在执行的继续执行,没有执行的直接中断(更厉害

shutdownNow:等待所有正在执行的任务都执行完成了再退出

5.提交给线程池的任务可以被撤回吗

可以,向线程池提交任务之后会得到一个future对象,这个future对象有方法可以对任务进行取消

五、场景题

1.交替打印200以内的奇偶数

public class test1 {private static final int NUMBER = 200;// 定义一把锁private static Object lock = new Object();// 记录变量private static int currentNum = 1;public static void main(String[] args) {Thread t1 = new Thread(() ->{while (currentNum<NUMBER){synchronized (lock){// 代表其是偶数while (currentNum%2==0){try {lock.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("t1:"+currentNum);currentNum++;lock.notify();}}});Thread t2 = new Thread(() ->{while (currentNum<NUMBER){synchronized (lock){// 代表其是奇数while (currentNum%2!=0){try {lock.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("t2:"+currentNum);currentNum++;lock.notify();}}});t1.start();t2.start();}
}

2. 交替打印1,2,3的倍数

public class test2 {private static final int FINAL_NUMBER = 100;private static Object lock = new Object();private static int currentNum = 1;public static void main(String[] args) {Thread t1 = new Thread(()->{while (currentNum<FINAL_NUMBER){synchronized (lock){while (currentNum%3==1){System.out.println("t1:"+currentNum);currentNum++;lock.notifyAll();}try {lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}}});Thread t2 = new Thread(()->{while (currentNum<FINAL_NUMBER){synchronized (lock){while (currentNum%3==2){System.out.println("t2:"+currentNum);currentNum++;lock.notifyAll();}try {lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}}});Thread t3 = new Thread(()->{while (currentNum<FINAL_NUMBER){synchronized (lock){while (currentNum%3==0){System.out.println("t3:"+currentNum);currentNum++;lock.notifyAll();}try {lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}}});t1.start();t2.start();t3.start();}
}

3.假设两个线程并发读写同一个整型变量,初始值为零,每个线程加50次,结果可能是什么?

在没有设定任何同步机制的情况下,两个线程对于同一个变量的结果可能等于100,也可能小于100,这就是线程安全问题。所以就必须引入同步机制。

六、总结

本篇文章就Java并发的内容以及JUC包的部分内容进行了讲解,大部分内容来源于小林coding:Java并发编程面试题 | 小林coding,感谢观看!

http://www.dtcms.com/a/312104.html

相关文章:

  • 包装类、日期等常用类型
  • C语言数据结构(5)双向链表
  • 深入剖析Nacos:云原生架构的基石
  • Python编程基础与实践:Python基础数据类型入门
  • 中国工程院郑裕国院士确认出席:2025第五届生物发酵营养源高峰论坛生物发酵展
  • CMake基础使用指南
  • QtC++ 调用 tesseract开源库 搭配 Opencv 实现文字识别:从tesseract库基本介绍到实际应用实现
  • 【华为OD机试】计算图形面积
  • 关于Egret引擎的一些思考
  • 单位长度上的RC参数
  • 【补题】Codeforces Round 715 (Div. 1) B. Almost Sorted
  • linux中pthread_t 的值与top -Hp中线程id值的区别
  • 装 饰 器 模 式
  • 深入 Go 底层原理(七):逃逸分析
  • C++ 11 模板萃取
  • 丑数-优先队列/三指针/动态规划
  • Linux 动静态库的制作和使用
  • 深度剖析PyTorch torch.compile的性能曲线与优化临界点
  • SpringBoot 01 IOC
  • PyTorch 张量核心操作——比较、排序与数据校验
  • java实现运行SQL脚本完成数据迁移
  • 通俗易懂解释Java8 HashMap
  • Rust进阶-part1-智能指针概述-box指针
  • 【多模态】DPO学习笔记
  • 嵌入式文件系统
  • Java中Lambda 表达式的解释
  • PCB铜浆塞孔工艺流程
  • 如何快速解决PDF解密新方法?
  • 使用C++实现日志(1)
  • 疏老师-python训练营-Day33 MLP神经网络的训练