目录
6. 说一下 runnable 和 callable 有什么区别?
9. sleep()、wait(), join()、yield()的区别
10. notify()和 notifyAll()有什么区别?
11. 线程的 run() 和 start() 有什么区别?
15. 线程池中 submit() 和 execute() 方法有什么区别?
23. 多线程中 synchronized 锁升级的原理是什么?
29. synchronized 和 volatile 的区别是什么?
30. synchronized 和 Lock 有什么区别?
31. synchronized 和 ReentrantLock 区别是什么?
34. newFixedThreadPool和newCachedThreadPool
1. java 多线程
一个程序同时执行多个任务。通常每一个任务称为一个线程,它是线程控制的简称。可以同时运行一个以上线程的程序成为多线程程序。
1.1. 什么是进程?什么是线程?
1.1.1. 进程
进程是系统中运行的一个程序,程序一旦运行就是进程。
进程可以看做程序运行的一个实例。进程是系统分配资源的实体,每个进程都有独立的地址空间。
一个进程无法访问另一个进程的变量和数据机构如果想让一个进程访问另一个进程的资源,需要使用进程间通信,比如管道,文件,套接字。
1.1.2. 线程
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
根本区别:进程是操作系统进行资源分配和调度的基本单位,线程是独立调度执行的基本单位,是进程的执行路径
地址空间:进程包含独立的地址空间,线程没有自己独立的地址空间,多个线程共享所属进程的空间
开销:进程之间的切换会有较大的开销,线程之间的切换的开销比较小;创建一个线程比进程开销小
一个线程的死亡,会导致一个进程也死亡,多进程比多线程壮健
资源: 系统在运行的时候会为每个进程分配资源,而不会为线程分配资源,线程所使用的资源来自其所属进程的资源
通信: 线程之间通信比进程之间通信更方便
包含关系:线程是一个轻量级进程,是进程的一部分
适合使用线程的场景:
1. 频繁创建销毁的场景
2. 计算量大,切换频繁的场景
3. 需要速度的场景
适合使用进程的场景:需要稳定安全的场景
1.1.3. 多线程
多线程指在单个程序中可以同时运行多个不同的线程执行不同的任务。
2. 并行和并发有什么区别?
并行(Parallelism)指的是多个任务同时执行。在并行计算中,多个任务可以同时进行,每个任务在不同的处理单元上并行执行,以提高计算速度和效率。并行通常应用于需要处理大量数据或需要执行复杂计算的任务。
并发(Concurrency)指的是多个任务按照时间片轮转(时间间隔)的方式交替执行的。在并发计算中,多个任务在同一个处理单元上交替执行,每个任务都会获得一段处理时间,然后切换到下一个任务,以此类推。并发通常应用于提高系统的响应能力和资源利用率,并支持多用户同时访问和执行任务。
总结区别:
- 并行:单位时间内,多个任务同时执行,多个处理器或者是多核的处理器同时处理多个不同的任务。
- 并发:在同一时间段,多个任务都在执行, 一个处理器同时处理多个任务,宏观上是同时执行,微观上是顺序交替执行。并发不一定等于并行
并发是逻辑上的同时发生(simultaneous),而并行是物理上的同时发生
3. 守护线程是什么?
守护线程(Daemon Thread)是一种在后台运行的线程,它的存在并不会阻止程序的终止。当所有的非守护线程(主线程或其他非守护线程)结束时,守护线程会自动被终止。
在Java中,可以通过将线程的setDaemon(true)方法设置为true来将线程设置为守护线程。示例如下:
Thread daemonThread = new Thread(new Runnable() {
@Override
public void run() {
// 守护线程执行的任务
}
});
daemonThread.setDaemon(true);
daemonThread.start();
守护线程:守护线程则是用来服务用户线程的,如果没有其他用户线程在运行,那么就没有可服务对象,也就没有理由继续下去。如果仅剩守护线程JVM 就会离开
将一个用户线程设置为守护线程的方式是在线程对象创建之前调用线程对象的setDaemon方法。典型的守护线程例子是JVM中的系统资源自动回收线程,当我们的程序中不再有任何运行中的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是Java虚拟机上仅剩的线程时,Java虚拟机会自动离开。
守护线程的生命周期与应用程序的生命周期没有直接的关联,一旦所有的非守护线程结束,守护线程会被强制终止。因此,守护线程在应用程序需要在后台执行某些任务而不影响程序退出的场景下非常有用
4. 创建线程有哪几种方式?
一、继承Thread类创建线程类
(1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
(2)创建Thread子类的实例,即创建了线程对象。
(3)调用线程对象的start()方法来启动该线程。
上述代码中Thread.currentThread()方法返回当前正在执行的线程对象。GetName()方法返回调用该方法的线程的名字
二、通过Runnable接口创建线程类
(1)定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
(2)创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
(3)调用线程对象的start()方法来启动该线程。
三、使用Callable和FutureTask创建线程
(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
FutureTask<Integer> futureTask = new FutureTask<>(callableImpl);
(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。
newThread(futureTask).start();
(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
Thread和Runnable的实质是继承关系,没有可比性。无论使用Runnable还是Thread,都会new Thread,然后执行run方法。用法上,如果有复杂的线程操作需求,那就选择继承Thread,如果只是简 单的执行一个任务,那就实现runnable。
4.1. 线程的常见成员方法
- start():启动线程,使其进入就绪状态,等待CPU调度执行。
- run():定义线程的执行逻辑,当线程被启动后,run() 方法中的代码将会被执行。
- sleep(long millis):使线程休眠指定的毫秒数,暂停线程的执行。static
- setName(String name):设置线程的名称。也可以用构造方法创建线程时设置名字
- getName():获取线程的名称,线程会有默认的名字。
- currentThread(): 获取当前线程对象,static
- setPriority(int priority):设置线程的优先级,优先级范围为1-10,数值越大优先级越高。
- getPriority():获取线程的优先级。默认5
- setDaemon(): 设置守护线程 ,非守护线程结束,守护线程会陆续结束,
- interrupt():中断线程,给线程发送中断请求,线程可以通过检查中断状态来判断是否需要退出。
- isInterrupted():检查线程是否被中断。
- join():(插入线程)等待该线程执行完毕,其他线程会阻塞等待该线程执行完。static
- yield():(出让线程)放弃当前线程的CPU执行权,使得其他线程有机会运行。static
- isAlive():判断线程是否存活,即线程是否启动且未终止。
- wait()、notify() 和 notifyAll():用于线程间的协作与通信,必须在同步代码块或同步方法中使用。
5. 线程安全问题
5.1. synchronized实现同步的方式有哪些?
- 对于普通同步方法,锁的是当前实例的对象(this对象)。
- 对于静态同步方法,锁的则是当前类的class对象。
- 对于同步方法块,锁住的是synchonized括号内new的对象
同步代码块
使用synchronize(锁对象),使线程依次执行
锁对象是唯一的 可以使用MyClass.class代表
同步方法
5.2. Lock锁
Lock是接口,不能直接实例化,可以创建他的实现类,通过多态的方式
采用ReentrantLock
lock():加锁
unlock():解锁
可以将unlock放到finally里
5.3. 分段锁
其实说的简单一点就是:
容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
5.4. 死锁
死锁的必要条件是:1、互斥条件;2、不可剥夺条件(不可抢占);3、部分分配;4、循环等待。
根据产生死锁的四个必要条件,只要使其中之一不能成立,死锁就不会出现。为此,可以采取下列三种预防措施:
1、采用资源静态分配策略,破坏"部分分配"条件;
2、允许进程剥夺使用其他进程占有的资源,从而破坏"不可剥夺"条件;
3、采用资源有序分配法,破坏"环路"条件。
锁嵌套
互相等待释放锁
6. 说一下 runnable 和 callable 有什么区别?
- 同:
都是接口。
- 异:
Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;
Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
7. 如何停止一个正在运行的线程?
调用stop方法,但是这个方法会强行停止正在进行的操作,这个方法是不安全的,现在已经被废弃,所以不建议使用stop方法。使用interrupt中断线程
interrupt()方法是线程类的一个成员方法,它会向目标线程发送中断信号,将线程的中断状态设置为true。被中断的线程可以在适当时机检测中断状态并作出相应的处理,比如停止执行或者退出循环。
stop()方法是一个过时的方法,不推荐使用。它会立即停止线程的执行,并且释放线程所持有的所有锁资源。由于它是强制性的停止方式,不会给线程任何机会去清理和恢复状态,因此容易导致线程安全问题和资源泄漏
调用stop()方法会抛出java.lang.ThreadDeath异常,通常情况下不需要显示捕捉该异常,该异常是 Error 的子类而不是 Exception 的子类,单纯通过捕获所有Exception异常是无法捕捉该异常的。
8. 线程的生命周期?线程有几种状态
API文档中:新建NEW、就绪RUNNABLE、阻塞BLOCKABLE、等待WAITING、计时等待TIMED WAITING、死亡TERMINATED
1.线程通常有五种状态,创建,就绪,运行、阻塞和死亡状态。
2.阻塞的情况又分为三种:
(1)、等待阻塞(等待状态):运行的线程执行wait方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待 池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify或notifyAll方法才能被唤 醒,wait是object类的方法
(2)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放 入“锁池”中。
(3)、其他阻塞(计时等待):运行的线程执行sleep或join方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状 态。当sleep状态超时、join等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。 sleep是Thread类的方法
1.新建状态(New):新创建了一个线程对象。
2.就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start方法。该状态的线程位于 可运行线程池中,变得可运行,等待获取CPU的使用权。
3.运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4.阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进 入就绪状态,才有机会转到运行状态。
5.死亡状态(Dead):线程执行完了或者因异常退出了run方法,该线程结束生命周期。
9. sleep()、wait(), join()、yield()的区别
1.锁池 所有需要竞争同步锁的线程都会放在锁池当中,比如当前对象的锁已经被其中一个线程得到,则其他线 程需要在这个锁池进行等待,当前面的线程释放同步锁后锁池中的线程去竞争同步锁,当某个线程得到 后会进入就绪队列进行等待cpu资源分配。
2.等待池 当我们调用wait()方法后,线程会放到等待池当中,等待池的线程是不会去竞争同步锁。只有调用了 notify()或notifyAll()后等待池的线程才会开始去竞争锁,notify()是随机从等待池选出一个线程放 到锁池,而notifyAll()是将等待池的所有线程放到锁池当中
父类,放锁吗,怎么唤醒,异常
1、sleep 是 Thread 类的静态本地方法,wait 则是 Object 类的本地方法。
2、sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中。
3、sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字
4、sleep不需要被唤醒(休眠之后推出阻塞),但是wait需要(不指定时间需要被别人中断)。
5、sleep 一般用于当前线程休眠,或者轮循暂停操作,wait 则多用于多线程之间的通信。
6、sleep 会让出 CPU 执行时间且强制上下文切换,而 wait 则不一定,wait 后可能还是有机会重新竞 争到锁继续执行的。
wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用。
sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常。
sleep不出让系统资源;wait是进入线程等待池等待,出让系统资源
一般wait不会加时间限制,因为如果wait线程的运行资源不够,再出来也没用,要等待其他线程调用notify/notifyAll唤醒等待池中的所有线程,才会进入就绪队列等待OS分配系统资源。sleep(milliseconds)可以用时间指定使它自动唤醒过来,如果时间不到只能调用interrupt()强行打断。Thread.sleep(0)的作用是“触发操作系统立刻重新进行一次CPU竞争"。
yield()方法只会给优先级相同或更高优先级的线程执行机会。yield强制将当前线程进入就绪状态。
因此完全有可能某个线程调用yield方法暂停后,立即又获得处理器资源被执行。yield方法没有声明抛出任何异常。通俗地说 yield()方法只是把线程的状态由执行状态打回准备就绪状态
join()执行后线程进入阻塞状态,例如在线程B中调用线程A的join(),那线程B会进入到阻塞队 列,直到线程A结束或中断线程
wait需要依赖synchronized关键字
1.线程安全
wait()、notify()和notifyAll()方法必须在同步块或同步方法中调用,以确保在调用这些方法时,当前线程已经获取了对象的监视器锁(即通过synchronized获取锁),这样才能对锁进行释放或唤醒等操作。
2.监视器锁的释放和恢复
当一个线程调用wait()方法时,它会释放当前持有的监视器锁,让其他线程能够获得该锁并执行相关操作。当调用notify()或notifyAll()方法时,被唤醒的线程会重新竞争获取锁,一旦获取到锁,才能继续执行。
3.线程间通信
wait()、notify()和notifyAll()方法是实现线程间通信的关键。通过调用wait()方法,线程可以等待某个条件的满足;而通过notify()或notifyAll()方法,线程可以通知其他等待的线程条件已经满足,从而让它们继续执行。
综上所述,wait()、notify()和notifyAll()方法必须与synchronized关键字一起使用,以确保线程安全、正确释放和恢复监视器锁,并实现有效的线程间通信。
10. notify()和 notifyAll()有什么区别?
- notify(): 当等待池有多个线程时,调用 notify() 方法将会唤醒其中一个等待线程(具体唤醒哪个是不确定的),并使其进入就绪状态。
-
- 如果多个线程等待时,只有一个线程会被唤醒,其他线程仍然处于等待状态。
- 如果没有其他线程在等待池,notify() 方法不会有任何效果。
- notifyAll(): 当等待池有多个线程时,调用 notifyAll() 方法将会唤醒所有等待线程,并使它们进入可运行状态。
-
- 如果有多个线程在等待时,所有等待线程都会被唤醒。
- 如果没有其他线程在等待池,notifyAll() 方法不会有任何效果。
10.1. 等待唤醒机制
11. 线程的 run() 和 start() 有什么区别?
- start()方法来启动线程,真正实现了多线程运行。这时无需等待 run 方法体代码执行完毕,可以直接继续执行下面的代码。
- 通过调用 Thread 类的 start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。
- 方法 run()称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运行 run 函数当中的代码。Run 方法运行结束, 此线程终止。然后 CPU 再调度其它线程。
- run方法是Runnable接口中定义的,start方法是Thread类定义的,而Thread类实现了Runnable接口
12. 创建线程池有哪几种方式?
- 使用 Executors 类的静态方法:
-
- newFixedThreadPool(int nThreads): 创建固定大小的线程池,池中的线程数量固定为指定的数量。
- newCachedThreadPool(): 创建一个可缓存线程池,池中的线程数量可以根据需求进行自动扩展,空闲线程可能会被回收。最大值为Int的最大值
- newSingleThreadExecutor(): 创建单线程的线程池,池中只有一个线程在工作。
这些方法都返回一个线程池的 ExecutorService 对象,可以通过该对象来执行任务并管理线程池。
submit() 提交任务
shotdown() 销毁线程
- 使用 ThreadPoolExecutor 类进行自定义配置:
-
- ThreadPoolExecutor 类提供了更灵活的线程池创建方式,可以通过参数自定义线程池的核心线程数、最大线程数、线程存活时间、任务队列等参数。
- 通过创建 ThreadPoolExecutor 对象,并设置合适的参数,以满足具体需求来创建线程池。
- AbortPolicy是 ThreadPoolExecutor的一个内部类
使用 ThreadPoolExecutor 类进行自定义配置线程池时,您可以指定线程池的核心线程数、最大线程数、线程空闲时间、任务队列等参数。以下是使用 ThreadPoolExecutor 自定义配置线程池的步骤:
- 导入 java.util.concurrent.ThreadPoolExecutor 类。
- 创建 ThreadPoolExecutor 对象,并传入相应的参数:
ThreadPoolExecutor pool = new ThreadPoolExecutor(
3,//核心线程数量
6,//最大线程数量
60,//空闲线程最大存活时间
TimeUnit.SECONDS, //时间单位
new ArrayBlockingQueue<>(3),//任务队列
Executors.defaultThreadFactory(),//创建线程工厂
new ThreadPoolExecutor.AbortPolicy()//任务拒绝策略
);
- 使用 threadPoolExecutor 执行任务:
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
// 执行任务的逻辑
}
});
自定义配置线程池时,可以根据具体的需求来调整参数。核心线程数设置为保持的最小线程数,最大线程数决定了线程池允许创建的最大线程数。当任务数超过核心线程数时,超出的任务会被放入任务队列中等待执行。当任务数超过核心线程数+任务队列容量时,将会创建新的线程执行任务,直到达到最大线程数的限制。
任务拒绝策略: 默认策略 ,丢弃任务抛出rejectedExecutionException异常
13. 为什么用线程池?解释下线程池参数?
- 重用存在的线程,减少对象创建销毁的开销。
JVM中,用户线程与内核线程是1:1的关系,也就是说每次创建线程和回收线程都会进行内核调用
有了线程池就可以重复使用线程资源,大幅降低创建和回收的频率,也可能避免资源耗尽
- 便于管理
线程池可以维护线程ID,线程状态,统计任务执行状态等信息
- 可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
- 提供定时执行、定期执行、单线程、并发数控制等功能
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
corePoolSize: 核心线程的最大值,不能小于0
maximumPoolSize:最大线程数,不能小于等于0,maximumPoolSize >= corePoolSize
keepAliveTime: 空闲线程最大存活时间,不能小于0
unit: 时间单位
workQueue: 任务队列,不能为null
threadFactory: 创建线程工厂,不能为null
handler: 任务的拒绝策略,不能为null
ThreadPoolExecutor.AbortPolicy: 丢弃任务并抛出RejectedExecutionException异常。是默认的策略。
ThreadPoolExecutor.DiscardPolicy: 丢弃任务,但是不抛出异常 这是不推荐的做法。
ThreadPoolExecutor.DiscardOldestPolicy: 抛弃队列中等待最久的任务 然后把当前任务加入队列中。
ThreadPoolExecutor.CallerRunsPolicy: 调用任务的run()方法绕过线程池直接执行。
corePoolSize 代表核心线程数,也就是正常情况下创建工作的线程数,这些线程创建后并不会 消除,而是一种常驻线程
maxinumPoolSize 代表的是最大线程数,它与核心线程数相对应,表示最大允许被创建的线程 数,比如当前任务较多,将核心线程数都用完了,还无法满足需求时,此时就会创建新的线程,但 是线程池内线程总数不会超过最大线程数
keepAliveTime 、 unit 表示超出核心线程数之外的线程的空闲存活时间,也就是核心线程不会 消除,但是超出核心线程数的部分线程如果空闲一定的时间则会被消除,我们可以通过 setKeepAliveTime 来设置空闲时间
workQueue 用来存放待执行的任务,假设我们现在核心线程都已被使用,还有任务进来则全部放 入队列,直到整个队列被放满但任务还再持续进入则会开始创建新的线程 ThreadFactory 实际上是一个线程工厂,用来生产线程执行任务。我们可以选择使用默认的创建 工厂,产生的线程都在同一个组内,拥有相同的优先级,且都不是守护线程。当然我们也可以选择 自定义线程工厂,一般我们会根据业务来制定不同的线程工厂 Handler 任务拒绝策略,有两种情况,第一种是当我们调用 shutdown 等方法关闭线程池后,这 时候即使线程池内部还有没执行完的任务正在执行,但是由于线程池已经关闭,我们再继续想线程 池提交任务就会遭到拒绝。另一种情况就是当达到最大线程数,线程池已经没有能力继续处理新提 交的任务时,这是也就拒绝
如何维护内部线程
实现Runable:Worker对象代表一个异步任务,
继承AQS:Worker需要再处理中断信号时,对状态进行同步
向线程池提交任务
addWorker方法
14. 线程池都有哪些状态?
- Running(运行状态):线程池正常运行,接受新任务并处理已提交的任务。
- Shutdown(关闭状态):线程池已调用 shutdown() 方法进行关闭,不再接受新的任务提交。但是它会继续执行已提交的任务,直到所有任务都完成。
- Stop(停止状态):线程池已调用 shutdownNow() 方法进行停止,会尝试中断正在执行的任务,并且清除任务队列中的未执行任务。
- Tidying(整理状态):线程池处于此状态时,线程已经没有工作任务,线程会转换为Terminate状态,调用terminated方法。
- Terminated(终止状态):线程池彻底终止,已完成关闭过程。
已提交的任务就是阻塞队列中的任务
32位数据,五个状态都只用了高三位,因为TreadPoorExecutor只用了一个int变量来保存线程池状态,以及工作线程数这两个信息,线程状态使用高三位,工作线程数使用低29位,好处就是两个状态值需要同步变化时,直接通过位操作就可以了
比如ReenrantReadWriteLock这个组件中,也只是用了一个int 来组合表示读锁和写锁的个数
15. 线程池中 submit() 和 execute() 方法有什么区别?
在线程池中,submit() 和 execute() 方法都可以用于向线程池提交任务,但它们在返回结果和异常处理上有一些区别。
- submit() 方法:
-
- submit() 方法可以接收并执行 Callable 或 Runnable 类型的任务,并返回一个 Future 对象。
- Future 对象可以用于获取任务的执行结果,可 以通过调用 get() 方法来获取结果,或者通过调用带超时的 get(long timeout, TimeUnit unit) 方法来获取结果并设置超时时间。
- 如果提交的是 Runnable 类型的任务,submit() 方法会返回一个 Future 对象,但该对象的 get() 方法将始终返回 null。
- submit() 方法也可以用于提交异常处理器 Callable 或 Runnable,在任务执行过程中可以通过 Future 对象来处理任务中的异常。
- execute() 方法:
-
- execute() 方法用于执行 Runnable 类型的任务,execute()方法的返回类型是void,它定义在Executor接口中
- execute() 方法不能直接处理任务执行过程中的异常,如果任务抛出异常,线程池内部的异常处理机制会处理异常(如记录日志或终止线程)。
总结来说,submit() 方法可以提交任务并返回一个 Future 对象,可以用于获取任务的执行结果或处理异常。execute() 方法只能提交任务,无法获取任务的执行结果,而且无法直接处理任务的异常。选择使用哪个方法取决于对任务执行结果和异常处理的
16. 在 Java 程序中怎么保证多线程的运行安全?
- 使用 synchronized 关键字:可以使用 synchronized 关键字来修饰方法或代码块,确保在同一时间只有一个线程可以执行被修饰的代码段。这样可以避免多线程并发访问共享资源出现的数据竞争和不一致性问题。
- 使用 ReentrantLock:ReentrantLock 是Java.util.concurrent包中提供的一种可重入锁,与 synchronized 类似。通过显式地获取锁和释放锁,可以控制在同一时间内只有一个线程可以执行被锁保护的代码块。与 synchronized 不同的是,ReentrantLock 提供了更灵活的锁定方式,并且支持公平锁和可中断锁等特性。
- 使用 volatile 关键字:volatile 关键字用于修饰共享的变量,可以保证多个线程之间对该变量的可见性。当一个线程修改了 volatile 变量的值,该变量的新值会立即被写回主内存,而其他线程在读取该变量时会从主内存中重新获取最新值。因此,使用 volatile 可以确保变量的读写操作在多线程环境下的正确性。
- 使用线程安全的数据结构:Java 提供了一些线程安全的数据结构,如 ConcurrentHashMap、CopyOnWriteArrayList 等,它们在内部实现上采用了各种同步机制,可以保证多线程环境下的安全操作。
- 使用并发工具类:Java 提供了一些并发工具类,如 CountDownLatch、Semaphore、CyclicBarrier 等,它们可以协调多个线程的执行顺序和同步点,确保线程按照预期的方式进行协作和同步。
- 使用原子类:Java 提供了一些原子类,如 AtomicInteger、AtomicLong 等,它们提供了线程安全的原子操作,可以避免多线程环境下的数据竞争问题。
17. 线程池队列已满,这时会发生什么
这里区分一下:
1)如果使用的是无界队列LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为LinkedBlockingQueue可以近乎认为是一个无穷大的队列,可以无限存放任务
2)如果使用的是有界队列比如ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue满了,会根据maximumPoolSize的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue继续满,那么则会使用拒绝策略RejectedExecutionHandler处理满了的任务,默认是AbortPolicy,其他的拒绝策略还有丢弃策略,丢弃最近策略
18. 线程池执行过程
19. 什么是CAS?
CAS(Compare And Swap)是一种基于锁的操作,而且是乐观锁。
CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。
CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。
比如有一个对象资源的值为0,A、B线程的Old Value 都是 0 ,new Value 都是 1 ,他们都想把资源对象值该为1,但是A线获取到了资源,改为了1,B过来的时候发现值不等于OldValue,就是陷入了阻塞,但B不会放弃,而是通过自旋,如果对象资源变为0,就还有机会拿到资源,通常会配置自旋次数防止死循环,默认是10。
自旋锁 = 循环+CAS
例如,如果您想要将CAS自旋次数设置为1000次,可以使用如下启动命令:
java -XX:PreBlockSpin=1000 YourApplication
防止同时获取资源,CAS的操作必须是原子性的
各种不同架构的CPU都提供了指令级别的CAS原子操作
比如在x86架构下,通过cmpxchg指令支持cas,在ARM下,通过LL/SC,也就是说,不需要通过操作系统的同步原语 (比如mutex),CPU已经原生地支持了cas,不再依赖锁来进行线程同步,但这并不意味着无锁能够完全替代有锁
Java中如何无锁编程
比如使用多线程打印0到1000,未加锁多条线程打印了相同的值,说明线程间未正确通信,最常规可以使用互斥锁synchronize来加锁,如果无锁同步呢
他是一个本地方法,性能和具体的平台有关
CAS的缺点:
CAS虽然很高效的解决了原子操作问题,但是CAS仍然存在三大问题。
- 循环时间长开销很大。
- 只能保证一个共享变量的原子操作。
- ABA问题
循环时间长开销很大:
我们可以看到getAndAddInt方法执行时,如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销
优点:
可以避免优先级倒置和死锁等危险,竞争比较便宜,协调发生在更细的粒度级别,允许更高程度的并行机制等等
JAVA对CAS的支持:
在JDK1.5 中新增 java.util.concurrent (J.U.C)就是建立在CAS之上的。相对于 synchronized 这种阻塞算法,CAS是非阻塞算法的一种常见实现。所以J.U.C在性能上有了很大的提升。
在java中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加version来获取数据,性能较悲观锁有很大的提高。
java.util.concurrent.atomic 包下的类大多是使用CAS操作来实现的( AtomicInteger,AtomicBoolean,AtomicLong)。
19.1. 什么是自旋
很多synchronized里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然synchronized里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在synchronized的边界做忙循环,这就是自旋。如果做了多次忙循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。
自旋锁是一种线程同步机制,它允许线程在一段时间内反复尝试获取锁,而不是阻塞等待其他线程释放锁。自旋锁的目的是尽量避免线程切换的开销,因为线程阻塞涉及到用户态和内核态切换的问题,特别适用于锁竞争时间很短的场景。
Java中的自旋锁通过java.util.concurrent包中的ReentrantLock类来实现。ReentrantLock类提供了与内置synchronized关键字相似的功能,但它具有更高的灵活性和扩展性。
使用自旋锁,可以通过lock()方法尝试获取锁,如果锁已被其他线程占用,则当前线程会进行自旋,不断尝试获取锁直到成功。一旦获取到锁,线程执行对应的临界区代码,然后通过unlock()方法释放锁。
需要注意的是,自旋锁适用于锁竞争时间短的情况,如果临界区代码执行时间较长或者锁竞争激烈,自旋可能会造成CPU资源的浪费,此时应考虑使用其他线程同步机制。
19.2. CAS的问题
1)CAS容易造成ABA问题
一个线程a将数值改成了b,接着又改成了a,此时CAS认为是没有变化,其实是已经变化过了,而这个问题的解决方案可以使用版本号标识,每操作一次version加1。在java5中,已经提供了AtomicStampedReference来解决问题。
2) 不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。
3)CAS造成CPU利用率增加
之前说过了CAS里面是一个循环判断的过程,如果线程一直没有获取到状态,cpu资源会一直被占用。
19.3. 线程B怎么知道线程A修改了变量
volatile修饰变量
synchronized修饰修改变量的方法
wait/notify
while轮询
- 使用volatile关键字:将被线程A修改的变量声明为volatile,这样线程B在读取该变量时会直接从主内存中获取最新值,而不是从线程的工作内存中获取。这确保了线程B能够及时感知到线程A对变量的修改。
- 使用synchronized关键字或锁:在读写变量的代码块或方法上使用synchronized关键字或其他适当的锁机制,确保线程A和线程B在访问该变量时是互斥的。这样当线程B尝试获取锁时,会等待线程A释放锁,而在释放锁时会将内存中的最新值刷新到主内存,使得线程B可以获得最新的值。
- 使用Atomic类:可以使用Java中的Atomic类,如AtomicInteger、AtomicBoolean等,这些类提供了原子操作的特性,可以确保线程安全地修改变量。线程A通过原子操作更新变量的值,线程B通过原子操作读取最新值,从而实现线程间的通信。
- 使用回调机制或消息队列:线程A在修改完变量后,可以通过回调函数、事件通知或消息队列等方式通知线程B。线程B监听相关事件或消息,一旦接收到通知,就知道变量已经被修改。
20. AQS
volatile int state 判断共享资源是否被占用的一个标记位
如果多个线程进入等待,会形成一个FIFO的双向链表结构的等待队列
独占模式:一旦被占用,其他线程都不能占用锁
共享模式,一旦被占用,其他共享模式下的线程还可以占用
21. 乐观锁与悲观锁
悲观锁认为资源一定会存在竞争冲突问题,悲观锁具备阻塞特性,只有线程拿到了锁,才可以去操作共享资源,其他线程必须等待这个线程释放锁之后才可以尝试获取资源,其他线程是等待状态
运行到阻塞,阻塞到唤醒,涉及到上下文切换,如果频繁发生,会影响性能
synchronize和lock锁都做了优化,都会尝试几次获得锁,减少阻塞
乐观锁认为资源大部分时间不存在竞争问题,所以就不加锁访问,每次只有一个线程可以修改共享资源,其他失败线程不需要停止,而是不断重试,不需要阻塞,也就不涉及到上下文切;但是会占用CPU资源, 比如AtomicInteger,使用了CAS来保证原子性
22. 什么是可重入锁?什么是非可重入锁?
广义上的可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提得是同一个对象或者class),这样的锁就叫做可重入锁。ReentrantLock和synchronized都是可重入锁
与可重入锁相反,不可递归调用,递归调用就发生死锁。看到一个经典的讲解,使用自旋锁来模拟一个不可重入锁,
可重入锁(Reentrant Lock)是一种同步工具,它允许线程在获得锁之后可以再次获得同一个锁,即允许同一个线程多次获取锁而不会造成死锁。
在可重入锁中,每个锁都与一个线程关联,并且跟踪持有锁的线程以及持有的次数。当线程第一次获得锁时,锁的持有次数加一,当线程再次尝试获得同一个锁时,如果该锁已经被该线程持有,则可以再次获得而不会被阻塞。每个获得锁的线程在释放锁时,持有次数减一,当持有次数为零时,锁被完全释放,其他等待获取该锁的线程可以争夺锁。
可重入锁具有以下特性:
- 重入性:同一个线程可以对同一个锁反复获得和释放。
- 公平性:可重入锁可以设置为公平或非公平模式。在公平模式下,锁将按照线程等待的先后顺序来获取锁;在非公平模式下,锁是非按序获取的,有可能新请求的线程成功获取锁,插队而不会等待。
可重入锁的经典实现是Java中的 ReentrantLock 类。使用可重入锁可以有效地控制线程的同步访问,避免死锁情况的发生,并且提供更灵活的锁控制机制。但是,在应用中使用可重入锁时,需要小心避免锁重入的问题,以免造成死锁或逻辑错误。
23. 多线程中 synchronized 锁升级的原理是什么?
在 Java 中,synchronized 锁升级指的是锁的状态从无锁状态(无锁)、偏向锁(Biased Locking)、轻量级锁(Lightweight Locking)到重量级锁(Heavyweight Locking)的转变过程。这种锁的升级机制是为了在不同场景下提供更好的性能。
下面是锁升级的原理:
- 无锁状态(无锁):当一个线程访问一个同步代码块时,如果没有竞争,即没有其他线程同时访问该代码块,就会直接执行代码块,并在执行过程中进行一些轻量级的记录,而不会发生实质性的锁竞争。
- 偏向锁(Biased Locking):如果在无锁状态下,另一个线程也来访问了同步代码块,会触发偏向锁的升级。偏向锁的目的是为了解决无竞争场景下的性能问题。在偏向锁状态下,会将对象头中的标记位设置为偏向锁,并将该线程的标识记录在对象头中,在后续的访问中不需要进行同步操作,直接使用偏向线程即可。
- 轻量级锁(Lightweight Locking):如果在偏向锁状态下,另一个线程也来访问了同步代码块,会触发轻量级锁的升级。轻量级锁是为了解决少量竞争场景下的性能问题。在轻量级锁状态下,会通过 CAS(Compare and Swap)操作尝试将对象头中的标记位设置为锁的指针。如果 CAS 成功,表示获取了锁,并可以继续执行代码块。如果 CAS 失败,表示有其他线程正在竞争锁,会升级为重量级锁。
- 重量级锁(Heavyweight Locking):如果在轻量级锁状态下,CAS 操作依然失败,说明锁竞争激烈,会升级为重量级锁。重量级锁使用操作系统提供的互斥量等机制,在操作系统层面上进行加锁和解锁,确保线程的互斥访问。
锁升级的目的是根据锁的竞争情况来选择合适的锁实现方式,以提高多线程程序的执行效率。在无竞争情况下,采用偏向锁和轻量级锁可以减少不必要的同步操作,提升性能。而在竞争激烈的情况下,使用重量级锁可以确保线程的安全互斥访问。
24. 什么是死锁?
死锁是两个或多个线程互相占用对方的资源,互相等待对方释放资源,导致所有线程都无法继续执行。
死锁发生的条件通常包括以下四个方面:
- 互斥条件(Mutual Exclusion):指某个资源同时只能被一个线程占用,即在一段时间内只能有一个线程访问它。
- 请求与保持条件(Hold and Wait):指一个线程在持有一个资源的同时,还可以请求其他资源,并等待其他线程释放所占用的资源。
- 不可剥夺条件(No Preemption):指线程已经获得的资源只能在自己释放之后才能被其他线程获取,不能被系统剥夺。
- 循环等待条件(Circular Wait):指存在一种循环等待的情况,即线程集合{T1, T2, …, Tn}中的每个线程都在等待集合中的下一个线程持有的资源。
一旦这四个条件同时满足,就有可能发生死锁。当死锁发生时,线程无法继续执行,程序可能会出现假死状态,并且需要通过外部的干预来解决死锁问题,例如强制终止其中一个或多个线程,释放资源,打破循环等待条件,或者使用其他的解决方案。
死锁是多线程编程中常见的问题,因此在设计和实现多线程程序时,需要合理地管理和使用锁资源,避免死锁的发生。
25. 怎么防止死锁?
造成死锁的⼏个原因:
1. ⼀个资源每次只能被⼀个线程使⽤
2. ⼀个线程在阻塞等待某个资源时,不释放已占有资源
3. ⼀个线程已经获得的资源,在未使⽤完之前,不能被强⾏剥夺
4. 若⼲线程形成头尾相接的循环等待资源关系
这是造成死锁必须要达到的4个条件,如果要避免死锁,只需要不满⾜其中某⼀个条件即可。⽽其中前3 个条件是作为锁要符合的条件,所以要避免死锁就需要打破第4个条件,不出现循环等待锁的关系。
在开发过程中:
- 要注意加锁顺序,保证每个线程按同样的顺序进⾏加锁
- 要注意加锁时限,可以针对所设置⼀个超时时间
- 要注意死锁检查,这是⼀种预防机制,确保在第⼀时间发现死锁并进⾏解决
26. ThreadLocal 是什么?有哪些使用场景?
每一个 Thread 对象均含有一个 ThreadLocalMap 类型的成员变量 threadLocals ,它存储本线程中所 有ThreadLocal对象及其对应的值
ThreadLocalMap 由一个个 Entry 对象构成
Entry 继承自 WeakReference> ,一个 Entry 由 ThreadLocal 对象和 Object 构 成。
由此可见, Entry 的key是ThreadLocal对象,并且是一个弱引用。当没指向key的强引用后,该 key就会被垃圾收集器回收 当执行set方法时,ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对 象。再以当前ThreadLocal对象为key,将值存储进ThreadLocalMap对象中。 get方法执行过程类似。ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap 对象。再以当前ThreadLocal对象为key,获取对应的value。 图灵学院 由于每一条线程均含有各自私有的ThreadLocalMap容器,这些容器相互独立互不影响,因此不会存在 线程安全性问题,从而也无需使用同步机制来保证多条线程访问容器的互斥性。
使用场景:
1、在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
2、线程间数据隔离
3、进行事务操作,用于存储线程事务信息。
4、数据库连接,Session会话管理。
27. CountDownLatch
这个同步工具允许一条或多条线程等待其他线程中的一组操作完成后,再继续执行
CountDownLatch初始化count值,指定同步状态已经被占用7次,共享锁被获取了n次,每当子任务完成,state就自减1,直到为0,子任务全部完成,就是一个子任务不断释放锁,主任务不断检查锁的这样一个过程,state为0,tryQuireShared返回1,说明没有子任务持有锁,直接跳出等待,为什么可能返回1,因为主任务可能不止一个,子任务全部完成,主任务都需要被唤醒
主线程中通过await()方法进行等待,避免其他线程执行时间过长,是可以设置超时时间的
其他线程通过调用countDown()方法,直到所有任务都调用了,就告知主线程这里任务已经处理结束,
public class CountDownLatchDemo {
private final static Random RANDOM = new Random();
static class SearchTask implements Runnable{
private Integer id;
private CountDownLatch latch;
private SearchTask(Integer id, CountDownLatch latch){
this.id = id;
this.latch = latch;
}
@Override
public void run() {
System.out.println("开始寻找" + id + "元素");
int seconds = RANDOM.nextInt(10);
try {
Thread.sleep(seconds * 1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("花了" + seconds + "s,找到了" + id + "号元素");
latch.countDown();
}
}
public static void main(String[] args) throws InterruptedException {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
CountDownLatch latch = new CountDownLatch(list.size());
for (Integer id : list) {
Thread thread = new Thread(new SearchTask(id, latch));
thread.start();
}
latch.await();
System.out.println("找到所有元素");
}
}
countDown() 用于告知主线程任务已经完成
返回未完成的任务数
28. 说一下 synchronized 底层实现原理?
在 Java 中,synchronized 是一种用于实现线程安全的关键字,它提供了对共享资源的互斥访问,防止多个线程同时对同一资源进行修改。synchronized 的底层实现原理涉及到对象头(Object Header)和监视器锁(Monitor Lock)( Jdk1.5)。会编译成两个字节码指令monitorenter和moniterexit对其他指令进行了包裹,这两个指令依赖于操作系统的mutex lock指令,Java线程实际上是对操作系统线程的一个映射,每当挂起或唤醒线程都设计到用户态和内核态之间的切换,耗费资源,影响性能
28.1. 锁升级
Java6中引入了无锁 ,偏向锁,轻量级锁,重量级锁,就对应了MarkWord中的四种状态
无锁:资源无需竞争,如果存在竞争,不通过锁定资源同步线程CAS,有难度
偏向锁:如果在无锁状态下,另一个线程也来访问了同步代码块,会触发偏向锁的升级。思想是不通过线程状态切换,也不通过CAS,让对象能认识这个线程,只要认识这个线程,就把锁交出去,如何判断,根据对象头中的锁标志位001代表无锁,101代表偏向锁,根据线程ID判断是否偏向
轻量级锁:如果有多个线程Id偏向同一把锁,就需要升级为轻量级锁,通过栈中执行锁记录的指针,锁标志位为00,通过虚拟机栈用的LocalRecord来存储MarkWord副本和owner指针,通过CAS获取锁,一旦获得,就会复制对象头中的MarkWord到LocalRecord中,将owner指针指向该对象,对象的MarkWord前30bit会形成一个指针指向虚拟机栈LocalRecord,这样他们就互相知道了对方的存在
重量级锁:这时对象已经被锁定,其他线程也要获取对象会进行自旋,不断尝试去获得锁,CPU空转,消耗资源,适应性自旋优化,就是自旋时间不再固定,而是根据上一个自选时间和锁状态来决定,如果一旦自旋等待线程超过一个,就会变成重量级锁,就需要通过Monitor对线程进行监控,完全锁定资源
每个 Java 对象都包含一个对象头,对象头中包含了一些元数据,其中的一部分用于存储与锁相关的信息。
在 HotSpot 虚拟机中,对象头中的一部分被用来存储锁的信息,主要有以下几个关键字段:
- Mark Word(标记字):存储对象的运行时数据,其中的一部分用于存储锁的标记信息。
- Klass Pointer(类指针):指向该对象对应的类的元数据。
- Array Length(数组长度):用于存储数组对象的长度信息。
29. synchronized 和 volatile 的区别是什么?
- 锁机制:synchronized 使用锁机制,而 volatile 关键字没有锁的概念。
- 可见性:synchronized 通过获取锁来实现可见性,volatile 关键字直接从主内存读写数据,保证可见性。
- 原子性:synchronized 可以保证代码块或方法的原子性,volatile 只能保证单个变量读写操作的原子性。
- 适用范围:synchronized 可以用于任意代码块或方法的同步,volatile 只能用于修饰变量。
在多线程环境中,每个线程都有自己的工作内存(或称线程的本地缓存),线程在执行过程中会将共享变量从主内存中读取到自己的工作内存中进行操作,然后再将结果写回主内存。
在没有同步机制的情况下,编译器和处理器可能会对指令进行重排序和优化,这可能导致一个线程在处理变量时使用了过期的缓存值,而不是最新的值。这种情况下,其他线程无法感知到变量值的变化,造成可见性问题。
使用 volatile 关键字修饰的变量具有以下特性:
- 写入操作:当一个线程写入(修改)了一个 volatile 变量的值时,会立即将这个最新值刷新到主内存中,而不是仅仅写入到线程的本地缓存。这样其他线程在读取该变量时,就可以获取到最新值。
- 读取操作:当一个线程读取(读取)了一个 volatile 变量的值时,会从主内存中获取最新的值,而不是从线程的本地缓存中读取。这样可以保证读取到的值是最新的。
30. synchronized 和 Lock 有什么区别?
- 可重入性:synchronized是可重入锁,意味着线程可以多次获取同一个锁,而不会出现死锁。如果一个线程已经获取了某个对象的锁,那么在该线程继续获取该对象的锁时会自动成功。而Lock接口的实现类,如ReentrantLock,也是可重入锁。
- 锁的获取方式:synchronized关键字是隐式获取锁的,当进入synchronized代码块或方法时,线程会自动获取锁,并在执行完后释放锁。Lock接口是显式地获取锁的,需要手动调用lock()方法获取锁,在处理完后再调用unlock()方法释放锁。
- 锁的灵活性:Lock比synchronized提供了更多的灵活性。Lock接口提供了多种实现类,如ReentrantLock、ReentrantReadWriteLock等,在某些场景下可以满足特定的需求,例如可重入性、公平性、读写分离等。
- 同步块的异常处理:在synchronized中,如果发生异常,JVM会自动释放锁。而在使用Lock时,需要在finally块中手动释放锁,以确保锁的释放。
- 等待可中断:通过Lock接口的lockInterruptibly()方法,可以实现等待锁的过程中响应中断。而synchronized在等待锁时,无法响应中断,只能一直等待下去。
31. synchronized 和 ReentrantLock 区别是什么?
- 可重入性:synchronized是可重入锁,意味着线程可以多次获取同一个锁,而不会出现死锁。如果一个线程已经获取了某个对象的锁,那么在该线程继续获取该对象的锁时会自动成功。ReentrantLock同样也是可重入锁,支持线程多次获取同一个锁。
- 锁的获取方式:synchronized关键字是隐式获取锁的,当进入synchronized代码块或方法时,线程会自动获取锁,并在执行完后释放锁。ReentrantLock类是显式地获取锁的,需要手动调用lock()方法获取锁,在处理完后再调用unlock()方法释放锁。
- 锁的灵活性:ReentrantLock比synchronized提供了更多的灵活性。ReentrantLock类提供了一些扩展功能,如可定时的锁等待、公平性等。而synchronized关键字所提供的功能相对简单,无法进行更精细的控制。
- 锁的可中断性:使用ReentrantLock类时,可以通过lockInterruptibly()方法实现可中断的锁获取过程。也就是说,在等待锁的过程中,如果其他线程中断了该线程,它可以响应中断并做出相应处理。而synchronized无法响应中断,只能一直等待下去。
- 性能:在低竞争情况下,synchronized的性能通常比ReentrantLock好,因为synchronized是JVM提供的原生实现,而ReentrantLock是通过在用户态进行加锁操作实现的。但在高并发情况下,ReentrantLock由于提供了更多的灵活性和功能,可能会比synchronized表现更好。
32. 说一下 atomic 的原理?
Atomic类是Java中用于实现原子操作的工具类,它能够确保在多线程环境下对共享变量的操作是线程安全的。Atomic类提供了一些原子操作方法,如原子的读取、写入、递增、递减等操作。
Atomic类的实现原理主要依赖于CPU提供的CAS(Compare and Swap)指令。CAS是一种乐观锁的实现,它通过比较内存中的值与预期值,如果相同则进行更新操作,否则返回失败。CAS指令的操作是原子性的,可以保证多个线程同时执行CAS操作时不会出现数据冲突。
当使用Atomic类进行原子操作时,内部会使用CAS指令来实现线程安全的操作。它通过不断地尝试更新操作,直到成功为止。如果多个线程同时进行原子操作,只有一个线程能够成功执行更新操作,其他线程需要重试。
Atomic类利用了CPU提供的硬件级别的原子指令,避免了使用锁的开销和线程切换的开销,从而提供了高效的原子操作。由于使用硬件级别的原子指令,Atomic类可以保证线程安全性,减少了出现数据竞争和并发问题的可能性。
需要注意的是,Atomic类适用于对单个变量进行原子操作的场景,不适合复合操作和复杂逻辑的原子性控制。对于需要多个变量的原子操作,可以考虑使用锁机制或其他并发工具。此外,Atomic类并不能解决所有的并发问题,仍然需要根据具体情况进行综合考虑和使用其他的并发编程技术。
33. 主内存和工作内存
**主内存(Main Memory)**是指计算机系统中所有线程共享的存储区域。它是物理内存的一部分,用于存储程序代码、全局变量、静态变量等数据。主内存是所有线程可以直接访问的存储区域,在主内存中的数据对所有线程是可见的。
**工作内存(Working Memory)**也称为线程私有内存,是指每个线程独立使用的存储区域。每个线程都有自己的工作内存,用于存储线程执行过程中需要使用的变量、栈信息及运行线程所需的操作数栈等。工作内存是线程独享的,线程之间的数据不直接共享。
多线程编程中,线程通过从主内存复制数据到自己的工作内存中来进行操作。当线程需要读取变量的值时,会从主内存中获取变量的副本到自己的工作内存中进行操作。当线程对变量进行修改后,也是在自己的工作内存中进行修改,最后再将修改后的值刷新回主内存。
主内存和工作内存之间的交互是通过读取、写入操作和内存屏障等机制来实现的。每个线程维护自己的工作内存,并通过内存模型规定的规则来保证与其他线程之间的一致性、可见性和有序性。
总结来说:
- 主内存是所有线程共享的存储区域,包含了程序中的变量、静态变量等数据。
- 工作内存是每个线程独立使用的存储区域,存储了线程执行过程中需要使用的变量及运行时的栈信息等。
- 线程通过从主内存复制数据到工作内存并进行操作,保证了数据的可见性和一致性。
- 通过读取、写入操作和内存屏障等机制,实现了主内存和工作内存之间的交互与同步。
34. newFixedThreadPool和newCachedThreadPool
Executors.newFixedThreadPool 是 Java 中 ExecutorService 接口的一种实现。它用于创建一个固定大小的线程池,该线程池中的线程数量是事先指定的。
Executors.newCachedThreadPool 是 Java 中 ExecutorService 接口的一种实现,用于创建一个可缓存的线程池。这种线程池的特点是,线程数量不固定,并且会根据需要自动调整线程数,.默认最多可以容纳int类型的最大值。
使用 Executors.newCachedThreadPool 方法可以创建一个线程池,该线程池根据需要动态地创建和回收线程,
35. synchronized实现同步的方式有哪些?
Synchronized关键字,Lock锁实现,分布式锁等
1:对于普通同步方法,锁的是当前实例的对象(this对象)。
classA {
synchronizedtest(){
system.out.print("lock object");
}
}
A a = newA();
A a1 = newA();
a.test();
这个时候锁住的是a.然而a1并不会被锁住。
2:对于静态同步方法,锁的则是当前类的class对象。
意思是
class A {
static synchronized test(){
system.out.print("lock object");
}
}
A a = new A();
A a1 = new A();
a.test();
这个时候锁住的是整个A的class,a1也被锁住了。
3:对于同步方法块,锁住的是synchonized括号内new的对象
classA{
test(A a){
synchronized(a){a=newA();}
}
}
A a =new A();
A a1 =new A();
a.test(a1);
这实际上锁的是a1 这个对象。