八股训练--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,感谢观看!