文章目录
- 1 名词解释:同步,异步,阻塞,非阻塞,同步阻塞,异步阻塞,异步非阻塞
- 2 解释下多线程
- 3 创建线程的三种方法
- 4 多线程同步机制
- 5 线程的几种可用状态(生命周期)(7种)
- 6 线程锁对象详解
- 7 同步方法的实现方式
- 8 什么是死锁(deadlock)?如何避免死锁?
- 9 sleep方法和wait方法的区别?
- 10 线程局部变量ThreadLocal
- 11 什么是守护线程?
- 12 synchronized和volatile比较
- 13 乐观锁和悲观锁的理解及如何实现?原理和应用场景?
- 14 为什么要用线程池
- 15 3个方法代表了Servlet的生命周期
- 16 线程的 run()和 start()有什么区别?
- 17 如何唤醒一个阻塞的线程?
- 18 在 Java 程序中怎么保证多线程的运行安全?
- 19 线程之间如何通信及线程之间如何同步
- 20 说说自己是怎么使用 synchronized 关键字,在项目中用到了吗 ?
- 21 单例模式了解吗?解释一下双重检验锁方式实现单例模式
- 22 新建 T1、T2、T3 三个线程,如何保证它们按顺序执行?
- 23 形成死锁的四个必要条件是什么 ?
- 24 线程 B 怎么知道线程 A 修改了变量
- 25 创建线程池的参数有哪些
- 26 如何创建线程池
- 27 线程池的执行流程
- 28 如何合理分配线程池大小?
- 29 线程池启动线程 submit()和 execute()方法有什么不同?
- 30 如果你提交任务时,线程池队列已满,这时会发生什么
- 31 多线程中 synchronized 锁升级的原理是什么?
- 32 什么是 CAS ?
- 33 CAS 的会产生什么问题?
- 34 什么是偏向锁 ?
- 35 什么是轻量级锁 ? 什么是重量级锁?
- 36 什么是自旋锁 ? 自旋锁存在什么问题 ?
- 37 synchronized 和 Lock 有什么区别?
- 38 volatile 关键字的作用
- 39 ThreadLocal的底层原理
- 40 项目中哪里使用的ThreadLocal
- 41 使用ThreadLocal可能会产生什么问题?如何解决 ?
1 名词解释:同步,异步,阻塞,非阻塞,同步阻塞,异步阻塞,异步非阻塞
同步与异步的是针对读写操作是谁完成和执行操作的先后顺序来区分的.
同步:读写由应用程序完成,本次操作完成之后,才会进行下面的操作.
异步:读写由操作系统完成,完成之后,回调或者事件通知应用程序,本次操作没有完成直接返回,进行下面的操作
阻塞和非阻塞是从函数调用的角度来区分操作是否挂起.
阻塞:读写没有就绪或者读写没有完成,则该函数会一直等待.(当一次操作需要等待某种条件才能继续进行时,将该操作直接挂起)
非阻塞:函数会立即返回,然后让应用程序轮询.(等待条件时,直接返回)
同步IO:导致请求进程阻塞,直到IO操作完成
异步IO:不导致请求进程阻塞
异步非IO:请求不阻塞进程
参考链接
https://www.cnblogs.com/freakkkkk/p/10875236.html
http://t.csdn.cn/3s9Sl
2 解释下多线程
线程:线程是程序的执行路径,或者可以说是程序的控制单元
一个进程可能包含一个或多个进程,当一个进程存在多条执行路径时,就可以将该执行方式称为多线程.
线程的执行方式大致可以分为就绪(wait),执行(run),阻塞(block)三个状态,多个线程在运行中相互抢夺资源,造成线程在上述的三个状态之间不断的相互转换.
3 创建线程的三种方法
1.继承Thread类,重写父类的run方法.
2.实现runnable接口
3.使用ExecutorService,Callable,Future实现有返回结果的多线程.
4 多线程同步机制
1.在需要同步的方法的方法签名中加入synchronized关键字
2.使用synchronized块对需要进行同步的代码块进行同步
3.使用JDK5中提供的java.util.concurrent.lock包中的Lock对象.
5 线程的几种可用状态(生命周期)(7种)
就绪(Runnable):线程准备运行,不一定立马就能开始执行。
运行中(Running):进程正在执行线程的代码。
等待中(Waiting):线程处于阻塞的状态,等待外部的处理结束。
睡眠中(Sleeping):线程被强制睡眠。
I/O阻塞(Blocked on I/O):等待I/O操作完成。
同步阻塞(Blocked on Synchronization):等待获取锁。
死亡(Dead):线程完成了执行。
6 线程锁对象详解
当多个线程同时访问共享数据时,可能会导致数据不同步,甚至错误,
所以需要用锁来保证数据同一时刻只能由一个线程来操作.
适用于密集型操作 需要资源保持同步 需要用到线程锁
优缺点:
优点:保证资源同步
缺点:有等待肯定会慢
7 同步方法的实现方式
1 同步方法 : 有synchronized关键字修饰的方法。
2 同步代码块 : 有synchronized关键字修饰的语句块。
3 使用特殊域变量(volatile)实现线程同步 : volatile关键字为域变量的访问提供了一种免锁机制
4 使用重入锁实现线程同步
5 使用局部变量threadlocal实现线程同步
6 使用阻塞队列实现线程同步
7 使用原子变量实现线程同步
8 什么是死锁(deadlock)?如何避免死锁?
两个进程都在等待对方执行完毕才能继续往下执行的时候就发生了死锁。
避免死锁的方式就是:指定获取锁的顺序,并强制线程按照指定的顺序获取锁。
9 sleep方法和wait方法的区别?
来自不同的类:wait()方法是Object类的方法,sleep方法是Thread类的方法。
对于锁的占用情况不同:sleep方法没有释放锁,而 wait 方法释放了锁,
使用范围: wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用)
唤醒方式:sleep()方法的线程通常是睡眠一定时间后,自动醒来。调用wait()方法,必须采用notify()或者notifyAll()方法唤醒
10 线程局部变量ThreadLocal
ThreadLocal 的作用和目的:
用于实现线程内的数据共享,相同程序代码下多个模块在同一个线程中运行时要共享一份数据(每一个线程都有一个副本,以确保每一个线程都能且只能访问自己线程内部的副本变量)
ThreadLocal 的应用场景:
订单处理包含一系列操作
银行转账包含一系列操作
同一段代码被不同的线程调用运行时
11 什么是守护线程?
守护线程(即daemon thread),被称为服务线程或后台线程,它服务于其他的用户线程,当程序中的用户线程全部执行结束后,守护线程也会跟随结束.
1、守护线程,比如垃圾回收线程,就是最典型的守护线程。
2、用户线程,就是应用程序里的自定义线程
用户自定义线程
1、应用程序里的线程,一般都是用户自定义线程。
2、用户也可以自定义守护线程,只需要调用Thread类的设置方法设置一下即可。
12 synchronized和volatile比较
多线程的三个特性
原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值.
有序性:程序执行的顺序按照代码的先后顺序执行.
volatile不需要加锁,比synchronized更轻便,不会阻塞线程
synchronized既能保证可见性和有序性,又能保证原子性,因为被synchronized修饰的代码段可以防止被多个线程同时执行.
而volatile只能保证可见性和有序性,无法保证原子性.
volatile只能作用于变量,使用范围较小.synchronized可以用在变量、方法、类、同步代码块等,使用范围比较广.
volatile不会造成线程阻塞,synchronized可能会造成线程阻塞.
在性能方面synchronized关键字是防止多个线程同时执行一段代码,就会影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized.
13 乐观锁和悲观锁的理解及如何实现?原理和应用场景?
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,
这样别人想拿这个数据就会阻塞直到它拿到锁
悲观锁,先获取锁,再进行业务操作,必须在事务中使用。
乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,
但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制
乐观锁,先进行业务操作,只在最后实际更新数据时进行检查数据是否被更新过。
并不会对数据加锁,而是通过对比数据的时间戳或者版本号,来实现乐观锁需要的版本判断。
14 为什么要用线程池
减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务
15 3个方法代表了Servlet的生命周期
servlet的生命周期: 加载类 -> 实例化(为对象分配空间) -> 初始化(为对象赋值) -> 请求响应(服务阶段) ->销毁
1、init方法:负责初始化Servlet对象。
2、service方法:负责响应客户的请求。
3、destroy方法:当Servlet对象退出生命周期时,负责释放占用的资源。
16 线程的 run()和 start()有什么区别?
run() 调用Thread中定义的方法,需要并行处理的代码放在run方法中,start方法启动线程后自动调用run方法,
start() 启动线程
17 如何唤醒一个阻塞的线程?
notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,
并不能确切的唤醒某一 个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关;
notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,
而是让它 们竞争,只有获得锁的线程才能进入就绪状态
18 在 Java 程序中怎么保证多线程的运行安全?
使用安全类,比如 java.util.concurrent 下的类,使用原子类AtomicInteger
使用自动锁 synchronized。
使用手动锁 Lock。
19 线程之间如何通信及线程之间如何同步
通过在线程之间共享对象就可以了,然后通过 wait/notify/notifyAll 、 await/signal/signalAll 进行唤起和等待.
20 说说自己是怎么使用 synchronized 关键字,在项目中用到了吗 ?
synchronized 关键字可以用在如下三个地方 :
修饰实例方法
修饰静态方法
修饰代码块
项目开发过程中很少使用synchronized 关键字修饰 , 因为效率比较低 , 会阻塞, 对于互联网项目来说不太合适 , 一般在单例模式中使用synchronized 实现双重检测锁
21 单例模式了解吗?解释一下双重检验锁方式实现单例模式
饿汉 懒汉 双重检查锁
public class Singleton {
//采用volatile修饰
private volatile static Singleton singleton;
//构造方法私有化
private Singleton(){}
//双重校验锁
public static Singleton getInstance(){
//先判断对象是否已经实例过,没有实例化过才进入加锁代码
if(singleton == null){
//类对象加锁
synchronized(Singleton.class){
//再次判断
if (singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
singleton 采用 volatile 修饰是很有必要的,因为 singleton = new Singleton() 这句话可以分为三步:
- 为 singleton 分配内存空间;
- 初始化 singleton;
- 将 singleton 指向分配的内存空间。
但是由于JVM具有指令重排的特性,执行顺序有可能变成 1-3-2。 指令重排在单线程下不会出现问题,但是在多线程下会导致一个线程获得一个未初始化的实例。例如:线程T1执行了1和3,此时T2调用 getInstance() 后发现 singleton 不为空,因此返回 singleton, 但是此时的 singleton 还没有被初始化。
使用 volatile 会禁止JVM指令重排,从而保证在多线程下也能正常执行
©著作权归作者所有:来自51CTO博客作者不倒翁88的原创作品,请联系作者获取转载授权,否则将追究法律责任
双重校验锁实现单例模式(对象单例,线程安全)
https://blog.51cto.com/u_13003060/2379049
22 新建 T1、T2、T3 三个线程,如何保证它们按顺序执行?
用 join 方法
public class ThreadJoinDemo {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> System.out.println("t1"));
Thread t2 = new Thread(() -> System.out.println("t2"));
Thread t3 = new Thread(() -> System.out.println("t3"));
t1.start();
t1.join(); // 等待 t1 执行完成
t2.start();
t2.join(); // 等待 t2 执行完成
t3.start();
t3.join(); // 等待 t3 执行完成
}
}
使用单个线程池,将t1,t2,t3按照顺序提交给单个线程池
使用CountDownLatch(闭锁):使用 CountDownLatch(闭锁)方法可以保证线程的顺序执行。CountDownLatch 是一个同步工具类,它可以让某个线程等待多个线程完成各自的工作之后再继续执行。具体来说,我们可以使用两个 CountDownLatch 对象 latch1 和 latch2 分别控制 T1、T2、T3 线程的执行顺序。
初始时,latch1 的计数器为1,latch2 的计数器为2。当 T1 执行完毕时,调用 latch1.countDown() 方法通知 T2 可以开始执行;当 T2 执行完毕时,调用 latch2.countDown() 方法通知 T3 可以开始执行。这样就可以确保 T1、T2、T3 按照顺序依次执行。
在 main 方法中,我们分别启动 T1、T2、T3 线程,然后等待它们执行完毕。需要注意的是,在使用 CountDownLatch 方法时,需要确保计数器的值正确设置,并且每个线程都能够独立执行完成,并且不会相互影响,以避免出现死锁等问题。
————————————————
版权声明:本文为CSDN博主「fighting!899」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_71921932/article/details/130504095
23 形成死锁的四个必要条件是什么 ?
线程死锁:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
产生条件:
(1)互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
(2)请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
(3)不剥夺条件:线程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
(4)环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。
(人话版)互斥条件:在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,就只能等 待,直至占有资源的进程用毕释放。
占有且等待条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进 程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
不可抢占条件:别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过 来。
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。(比如一个进程集合,A在 等B,B在等C,C在等A)
预防:
(1)破坏互斥条件: 不能破坏,因为锁大部分的场景就是同时只能允许一个线程获得
(2)破坏请保持条件:防止线程在持有资源后并申请其他资源的情况。线程一次性申请所有需要的资源。申请成功就运行,不成功就等待。
缺点:线程会提前申请后期运行需要的资源,会导致其他线程无法获取对应的资源,很多时候是其他线程是不会造成死锁的。会造成线程的饥饿,资源的利用率太低。
(3)破坏不可剥夺条件:可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源
(4)破坏环路等待条件:资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反
24 线程 B 怎么知道线程 A 修改了变量
volatile 修饰变量
synchronized 修饰修改变量的方法
wait/notify
while 轮询, 例如CAS
25 创建线程池的参数有哪些
ExecutorService pool = new ThreadPoolExecutor(
3, //核心线程数有3个
5, //最大线程数有5个。 临时线程数=最大线程数-核心线程数=5-3=2
8, //临时线程存活的时间8秒。 意思是临时线程8秒没有任务执行,就会被销毁掉。
TimeUnit.SECONDS,//时间单位(秒)
new ArrayBlockingQueue<>(4), //任务阻塞队列,没有来得及执行的任务在,任务队列中等待
Executors.defaultThreadFactory(), //用于创建线程的工厂对象
new ThreadPoolExecutor.CallerRunsPolicy() //拒绝策略
);
26 如何创建线程池
使用ThreadPoolExecutor类
使用Excutors工具类创建
27 线程池的执行流程
提交一个任务到线程池中,线程池的处理流程如下:
1 判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没 有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个 流程。
2 线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队 列里。如果工作队列满了,则进入下个流程。
3 判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任 务。如果已经满了,则交给饱和策略来处理这个任务。
版权声明:本文为CSDN博主「文丑颜不良啊」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/jiaomubai/article/details/125259594
28 如何合理分配线程池大小?
CPU密集型时,任务可以少配置线程数,大概和机器的cpu核数相当,这样可以使得每个线程都在 执行任务
IO密集型时,大部分线程都阻塞,故需要多配置线程数,2*cpu核数
29 线程池启动线程 submit()和 execute()方法有什么不同?
1.提交方式
submit()方法是定义在ExecutorService接口中的,它允许开发人员提交一个Callable或Runnable对象给线程池来执行,返回一个Future对象,可以用于检索结果或取消任务.
execute()方法是定义在Executor接口中的,只接收Runnable对象,并且没有返回类型.
所以submit()方法更加灵活,可以处理带返回值的任务,而execute()只能处理不带返回值的任务.
2.异常处理
submit()方法抛出的异常会被包装在ExecutionException中抛出
execute()方法不会重新抛出异常,需要开发人员自己处理异常
3.队列策略
线程池通常会使用队列来保存已提交但未处理的任务。线程池如何使用这个队列,被称为队列策略。
通过submit()方法提交的任务,会被添加到阻塞队列中,并保留之前提交的任务执行顺序。 execute()方法提交的任务,将会被添加到队列的尾部。这意味着队列中的第一个任务是最早的任务并且先被执行。
4.任务的处理过程与方式
submit()方法在处理任务时,将任务交由一个线程池中的工作线程去处理,而另一个线程(可能是主线程)可以继续做其他事情。负责处理submit()方法提交的任务的线程,当任务结束后会自动返回给线程池并等待下一个任务,从而避免了重复创建和销毁线程的开销。
execute()方法直接在调用execute()方法的调用线程(通常是主线程)中运行,如果当前没有可用线程,则会立即创建新的线程来处理该任务,并在完成任务后销毁线程。(用完就销毁)
5.消息传递方式
submit()方法中提交的任务可以通过Future对象获取执行结果,开发人员可以使用该对象等待线程池中对应的任务完成,并获取其返回值.
execute()方法则没有提供返回值或者其他机制来获取任务的执行情况,因此即便一个任务执行失败了,开发人员也无法了解到在哪里以及什么地方出现了问题。
总结 execute 没有返回值,如果不需要知道线程的结果就使用 execute 方法,性能会好很 多。
submit 返回一个 Future 对象,如果想知道线程结果就使用 submit 提交,
而且它能 在主线程中通过 Future 的 get 方法捕获线程中的异常。
30 如果你提交任务时,线程池队列已满,这时会发生什么
有两种可能:
1 如果使用的是无界队列 LinkedBlockingQueue,也就是无界队列的话,
没关系,继续添加任务到 阻塞队列中等待执行,因为 LinkedBlockingQueue 可以近乎认为是一个无穷大的队列,可以无限存放 任务
2 如果使用的是有界队列比如 ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue 中ArrayBlockingQueue 满了,
会根据maximumPoolSize 的值增加线程数量,如果增加了线程数量 还是处理不过来,
ArrayBlockingQueue 继续满,那么则会使用拒绝策略RejectedExecutionHandler 处理满了的任务,默认是 AbortPolicy
31 多线程中 synchronized 锁升级的原理是什么?
1.锁升级的原理
在Java中,锁共有4种状态,级别从低到高依次为:无状态锁,偏向锁,轻量级锁和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级
synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,
在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,
再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,
如果不一致,则升级偏向锁为 轻量级锁,通过自旋循环一定次数来获取锁,
执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。
2、锁的升级的目的
锁升级,是为了降低使用锁带来的性能消耗。
Java 1.6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而降低使用锁带来的性能消耗。
3.锁的升级不可逆
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁,且锁升级的顺序是不可逆的。
32 什么是 CAS ?
CAS就是Compare And Swap翻译就是比较并替换
CAS 是一种基于锁的操作,而且是乐观锁。在 java 中锁分为乐观锁和悲观锁
CAS是通过无限循环来获取数据的,如果在第一轮循环中,a 线程获取地址里面的值被b 线程修改了,那么 a 线程需要自旋到下次循环才有可能机会执行
CAS操作的就是乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止 , 可以理解成一个无阻塞多线程争抢资源的模型
版权声明:本文为CSDN博主「卷心菜sss」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/s12345sj/article/details/126538557
33 CAS 的会产生什么问题?
ABA 问题: 从 Java1.5 开始 JDK 的 atomic包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。
循环时间长开销大: 对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源, 效率低于 synchronized。
不能保证代码块的原子性: 当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量 操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁。
34 什么是偏向锁 ?
偏向锁,顾名思义,它会偏向于第一个访问锁的线程,
如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,
减少加锁/解锁的一些CAS操作(比 如等待队列的一些CAS操作),这种情况下,就会给线程加一个偏向锁。
如果在运行过程中,遇 到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
35 什么是轻量级锁 ? 什么是重量级锁?
轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,
当第二个线程加入锁争用的时候,轻量级锁就会升级为重量级锁;
重量级锁是synchronized ,是 Java 虚拟机中最为基础的锁实现。
在这种状态下,Java 虚拟机会阻 塞加锁失败的线程,并且在目标锁被释放的时候,唤醒这些线程
版权声明:本文为CSDN博主「逍遥ovo」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/m0_56651882/article/details/119717738
36 什么是自旋锁 ? 自旋锁存在什么问题 ?
很多 synchronized 里面的代码只是一些很简单的代码,执行时间非常快,
此时等待的线程都加锁 可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。
既然 synchronized 里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在 synchronized 的边界做忙循环,这就是自旋
存在问题
如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。
上面Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。
37 synchronized 和 Lock 有什么区别?
1 synchronized是Java内置关键字,在JVM层面,Lock是个Java类;
2 synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。
3 synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁; 而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
38 volatile 关键字的作用
Java 提供了 volatile 关键字来保证可见性和禁止指令重排。确保一个线程的修改能对其他线程是可见的。
它会保证修改的值会立即被更新到主内存中,当有其他线程需要读取时,它会去内存中读取新值。
volatile 可以提供线程间的可见性 , 但是不能保证操作的原子性
版权声明:本文为CSDN博主「FighterLiu」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/xinghui_liu/article/details/124379221
39 ThreadLocal的底层原理
表面理解:他其实就是一个本地线程,可以实现线程绑定技术,简单来说就是将一个对象存入当前线程中,在后续执行流程中,任意位置都可以获得对象数据,从而实现线程内的数据共享。
底层理解:ThreadLocal是一种线程隔离机制,它保证了多线程访问下,对共享变量访问的安全性,在解决多线程访问共享变量情况下一般方法都是加锁,保证同一时刻只有一个线程对共享变量进行修改,但是加锁会带来一个性能下降问题,所有ThreadLocal采用了一个空间换时间的思想,他在每个线程里边有一个容器,这个容器是用来储存共享变量的一个副本,然后每个线程只对自己的副本进行操作,就不会发生线程安全问题,又避免使用锁的一个开销,它底层是由map结构存储的,属于一个强引用,所以使用完要remove,不然会一直存在,容器易造成内存泄漏。
————————————————
版权声明:本文为CSDN博主「程序猿的打怪日常」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/library_yb/article/details/129419425
40 项目中哪里使用的ThreadLocal
1 我经历的项目中, 一般用ThreadLocal保存登录用户信息 ,
在项目中提供一个过滤器 , 拦截到所有的请求, 获取请求中的UserId头 , 构建User对象保存到ThreadLocal
2 在ThreadLocal中保存一些当前请求的响应信息 , 在返回的时候还是通过过滤器把响应信息从ThreadLocal中取出来, 返回给客户端
3 我们锁使用的很多框架内容也使用了ThreadLocal机制 ,
例如Spring的事务控制就会将连接放入到ThreadLocal , Seata的分布式事务控制也会将全局事务XID存入到ThreadLocal
可以参考下面的文章
快醒醒,Cookie + Session 的时代已经过去了
41 使用ThreadLocal可能会产生什么问题?如何解决 ?
可能会产生内存泄露问题 : 因为当ThreadLocal对象使⽤完之后,应该要 把设置的key,value,也就是Entry对象进⾏回收,但线程池中的线程不会回收,⽽线程对象是通过 强引⽤指向ThreadLocalMap,ThreadLocalMap也是通过强引⽤指向Entry对象,线程不被回收, Entry对象也就不会被回收,从⽽出现内存泄漏,解决办法是,在使⽤了ThreadLocal对象之后,⼿ 动调⽤ThreadLocal的remove⽅法,⼿动清除Entry对象