线程和进程的区别?
一个程序下至少有一个进程,一个进程下至少有一个线程,一个进程下也可以有多个线程来增加程序的执行速度。
守护线程是什么?
守护线程是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。在Java中垃圾回收线程就是特殊的守护线程。
Java中多线程实现方式?
继承Thread类,重新run方法
//1,定义一个线程类继承Thread类
public class MyThread extends Thread{
//2,重写run方法
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程执行输出" + i);
}
}
}
public class ThreadTest {
public static void main(String[] args) {
//3,new一个线程对象
Thread t1 = new MyThread();
//4,调用start方法启动线程(执行的是run方法)
t1.start();
}
}
实现Runnable接口,重写run方法
//1,定义一个线程任务类,实现Runnable接口
class MyRunnable implements Runnable{
//2,重写run方法,定义线程执行的任务
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("子线程执行输出 " + i);
}
}
}
public class RunnableTest {
public static void main(String[] args) {
//3,创建一个任务对象
Runnable target = new MyRunnable();
//4,把任务对象交给thread处理
Thread thread = new Thread(target);
//5,启动线程
thread.start();
}
}
实现Callable接口,重写call方法,带有返回值
//线程创建方式三:
//1,定义一个任务类 实现Callable接口 申明线程任务执行完毕后的结果类型
class MyCallable implements Callable<String>{
//2,重写call方法(任务方法)
@Override
public String call() {
int sum = 0;
for (int i = 0; i < 10; i++) {
sum += i;
}
return "sum= "+sum;
}
}
public class CallableTest {
public static void main(String[] args) {
//3,创建Callable任务对象
Callable callable = new MyCallable();
//4,把Callable任务对象,交给FutureTask对象
//FutureTask对象的作用
//4.1,FutureTask实现了Runnable接口,可以交给Thread
//4.2,线程执行完可以通过调用get方法得到线程执行结果
FutureTask futureTask = new FutureTask(callable);
//5,交给线程处理
Thread thread = new Thread(futureTask);
//6,启动线程
thread.start();
try {
//7,获取返回值
String result = (String) futureTask.get();
System.out.println(result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
线程池
线程池–Runnable方式
public class ThreadPoolRunnable {
public static void main(String[] args) {
/**
*int corePoolSize主线程个数:正式工,
*int maximumPoolSize最大线程个数:正式工+临时工,
*long keepAliveTime临时工工作完多久释放,
*TimeUnit unit时间单位,
*BlockingQueue<Runnable> workQueue工作队列:等待区线程最大个数,也就是最多几个任务在等待被线程执行,
*ThreadFactory threadFactory线程工厂,用来生产线程,
*RejectedExecutionHandler handler拒绝策略
*/
ExecutorService pool = new ThreadPoolExecutor(3,5,5, TimeUnit.SECONDS
,new ArrayBlockingQueue<>(6)
,Executors.defaultThreadFactory()
,new ThreadPoolExecutor.AbortPolicy());
for (int i = 1; i <= 11; i++) {
pool.execute(new MyRunnable());
}
//rejected from java.util.concurrent.ThreadPoolExecutor@135fbaa4[Running, pool size = 5, active threads = 5, queued tasks = 6, completed tasks = 0]
//at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
//最大线程容量5+最大等待线程数量6,so,当线程数量>11时,就会报出来
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"正在执行");
}
}
线程池–Callable方式
import java.util.concurrent.*;
public class ThreadPoolCallable {
public static void main(String[] args) {
/**
*int corePoolSize,
*int maximumPoolSize,
*long keepAliveTime,
*TimeUnit unit,
*BlockingQueue<Runnable> workQueue,
*ThreadFactory threadFactory,
*RejectedExecutionHandler handler
*/
ExecutorService pool = new ThreadPoolExecutor(3,5,5, TimeUnit.SECONDS
,new ArrayBlockingQueue<>(6)
,Executors.defaultThreadFactory()
,new ThreadPoolExecutor.AbortPolicy());
//线程池去执行线程,返回Future对象
Future<String> future1 = pool.submit(new MyCallable(5));
Future<String> future2 = pool.submit(new MyCallable(6));
Future<String> future3 = pool.submit(new MyCallable(7));
try {
//返回线程返回值
System.out.println(future1.get());
System.out.println(future2.get());
System.out.println(future3.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class MyCallable implements Callable<String> {
private int n;
public MyCallable(int n) {
this.n = n;
}
@Override
public String call() {
int sum = 0;
for (int i = 0; i < n; i++) {
sum+=i;
}
return Thread.currentThread().getName()+",0~"+n+";sum = "+sum;
}
}
线程池中 submit() 和 execute() 方法有什么区别?
线程池提交执行任务中execute()方法和submit()的区别
接收的参数不同 | 返回值 | 异常的处理 | |
---|---|---|---|
execute() | 只能接收实现Runnable接口类型的任务 | 返回值是void,线程提交后不能得到线程的返回值 | execute()执行Runnable的任务时,run()方法有显式的抛出异常 |
submit() | 既可以接收Runnable类型的任务,也可以接收Callable类型的任务 | 返回值是Future,通过Future的get()方法可以获取到线程执行的返回值,虽然submit()方法可以提交Runnable类型的参数,但执行Future方法的get()时,线程执行完会返回null,不会有实际的返回值,这是因为Runable本来就没有返回值 | submit()执行Runnable/Callable的任务时,run()/call()方法没显式抛出异常,当调用Future的get()方法时,也能打印出任务执行异常信息。 |
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
// 在线程的执行方法里如果没有异常处理,以下几个都不会有异常信息打印,只要不调用Future的get方法
//executorService.submit(new TestRunnable());
//executorService.submit(new TestCallable());
//打印异常信息
// executorService.execute(new TestRunnable());
Future<Integer> future2 = executorService.submit(new TestCallable());
try {
//下面才会打印异常信息
future2.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
线程池
每个任务创建一个线程会有哪些问题?
1,反复创建线程系统开销比较大,每个线程创建和销毁都需要时间,如果任务比较简单,那么可能导致创建和销毁线程消耗资源比线程执行任务本身消耗的资源还要大
(线程池用一些固定的线程一直保持工作状态并反复执行任务)
2,过多的线程会占用过多的内存等资源,还会带来过多的上下文切换,同时还会导致系统不稳定
(线程池根据需要创建线程,控制线程的总数量,避免占用过多的内存资源)
使用线程池的好处
1,线程池可以解决线程生命周期的系统开销问题,同时还可以加快响应时间
2,线程池可以统筹内存和cpu的使用,避免资源使用不当
3,线程池可以统一管理资源
线程池中各个参数含义
import java.util.concurrent.*;
public class ThreadPoolRunnable {
public static void main(String[] args) {
/**
会安排主线程干活,然后将新来的线程任务放到工作队列中,直到工作队列满了之后,
安排临时线程干活,当主线程+临时线程+工作队列都满了的话,
如果再来新的线程任务就根据拒绝策略,拒绝执行
*int corePoolSize主线程个数:正式工,
*int maximumPoolSize最大线程个数:正式工+临时工,
*long keepAliveTime临时工工作完多久释放,
*TimeUnit unit时间单位,
*BlockingQueue<Runnable> workQueue工作队列:等待区线程最大个数,也就是最多几个任务在等待被线程执行,
*ThreadFactory threadFactory线程工厂,用来生产线程,
*RejectedExecutionHandler handler拒绝策略
*/
ExecutorService pool = new ThreadPoolExecutor(3,5,5, TimeUnit.SECONDS
,new ArrayBlockingQueue<>(6)
,Executors.defaultThreadFactory()
,new ThreadPoolExecutor.AbortPolicy());
for (int i = 1; i <= 11; i++) {
pool.execute(new MyRunnable());
}
//rejected from java.util.concurrent.ThreadPoolExecutor@135fbaa4[Running, pool size = 5, active threads = 5, queued tasks = 6, completed tasks = 0]
//at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
//最大线程容量5+最大等待线程数量6,so,当线程数量>11时,就会报出来
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"正在执行");
}
}
线程池4种拒绝策略
AbortPolicy | DiscardPolicy | DiscardOldestPolicy | CallerRunsPolicy |
---|---|---|---|
抛出一个Exception让感知到任务被拒绝了 | 直接丢弃,不会通知 | 丢弃任务队列中存活时间最长的任务 | 当任务无法提交给线程池时,由提交任务的线程自己执行该任务 |
使用Java自带的线程池漏洞?
Executors去创建线程池可能出现系统风险
大量任务需要等待少量线程去处理,导致内存溢出(LinkedBlockingQueue无穷大队列任务会把内存撑爆)
大量线程被创建,导致cpu100%(newCachedThreadPool()会在任务很多时,创建无数个临时线程去处理,线程任务占据cpu资源,导致cpu100%)
FixedThreadPool
Executors.newFixedThreadPool(3); 只有3个正式工干工作,没有临时工
LinkedBlockingQueue无穷大队列任务,可能导致内存溢出OOM,如果有100个任务,就相当于每次最多执行3个,如果有完工的,再接下一个任务
SingleThreadExecutor
Executors.newSingleThreadExecutor(); 1个正式工
LinkedBlockingQueue无穷大队列任务,可能导致内存溢出OOM
CachedThreadPool
Executors.newCachedThreadPool(); 0个正式工干工作,超级大临时线程
SynchronousQueue同步队列任务,每个任务需要被线程接收后,才处理接后续任务
ScheduledThreadPool
DelayWorkQueue同步队列任务,也是一个无界队列,可能导致内存溢出OOM
public class ScheduledTask {
public static void main(String[] args) {
//Timer upgrade,解决Timer单个线程处理时,因个别任务执行异常,导致线程任务异常的情况
ScheduledExecutorService pool = Executors.newScheduledThreadPool(3);
/**
* Runnable command,线程任务
*long initialDelay,多久开始
*long period,每隔多久执行一次
*TimeUnit unit);时间单位
*/
pool.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"正在执行1;"+new Date());
}
},1,2,TimeUnit.SECONDS);
pool.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
//某个线程执行异常不会影响其他线程
System.out.println(10 / 0);
System.out.println(Thread.currentThread().getName()+"正在执行2;"+new Date());
}
},1,2,TimeUnit.SECONDS);
pool.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"正在执行3;"+new Date());
}
},1,2,TimeUnit.SECONDS);
}
}
ForkJoinPool
拆分一个大任务,成为多个小任务
public class SumTask extends RecursiveTask<Long> {
private final int begin;
private final int end;
public SumTask(int begin, int end) {
this.begin = begin;
this.end = end;
}
@Override
protected Long compute() {
long sum = 0;
if(end-begin<100){
for (int i =begin; i<=end; i++){
sum+=i;
}
}else {
//递归拆分成多个任务
int middle = (end+begin)/2;
SumTask sumTask1 = new SumTask(begin,middle);
SumTask sumTask2 = new SumTask(middle+1,end);
sumTask1.fork();
sumTask2.fork();
//等到子任务做完
Long sum1 = sumTask1.join();
Long sum2 = sumTask2.join();
sum = sum1+sum2;
}
return sum;
}
//main方法测试
public static void main(String[] args) throws ExecutionException, InterruptedException {
ForkJoinPool pool = new ForkJoinPool(10);
SumTask task = new SumTask(1,1000000);
ForkJoinTask<Long> future = pool.submit(task);
Long aLong = future.get();
System.out.println(aLong);
}
}
自定义线程池
一般采用ArrayBlockingQueue,底层由数组组成,需要设置容量,且容量不可变。
关闭线程池
pool.shutDown(),不立即关闭线程池,但是等待队列不接受新任务,有新任务就使用拒绝策略进行处理。
pool.isShutDown(),是否开始了线程池关闭的流程
pool.isTerminated(),检测线程是否真正终结
List<> tasks = pool.shutDownNow(),则是将线程池的状态设置为STOP,正在执行的任务则被停止,没被执行任务的则返回
如何停止线程?
interrupt()仅仅起到通知被停止线程的作用
注意:interrupt()打断正在休眠的程序会抛出InterruptedException异常
注意:volatile修饰标记位的方法去停止线程是错误的,因为线程被长时间阻塞的情况,就无法及时感受中断。
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
//t1线程拿不到锁资源,导致变成blocked状态
Thread t1 = new Thread(()->{
//获取当前中断标记位,默认为false
while (!Thread.currentThread().isInterrupted()){
//业务代码
}
System.out.println("t1执行结束");
});
t1.start();
Thread.sleep(1000);
t1.interrupt();//将中断标记位修改为true
}
遇到了InterruptedException应该如何处理
1,可以把异常声明在方法中
2,可以在catch中再次声明中断
线程中 sleep()、wait()、join()、yield()的区别
线程中 sleep()、wait()、join()、yield()的区别
wait()、yield() 和sleep()区别?
sleep() | wait() | yield() |
---|---|---|
Thread类的静态方法 | object类的方法 | yield()方法和sleep()方法类似,而yield方法不需要参数 |
时间等待状态timed_waiting,自动被唤醒,并返回到可运行状态(就绪),不是运行状态 | 等待状态waiting,需要手动唤醒,wait()可以将一个线程挂起,直到超时或者该线程被唤醒 | 线程执行sleep()方法后转入阻塞(blocked)状态,而执行yield()方法后转入就绪(ready)状态 |
持有锁时,执行不会释放锁资源 | 执行后,会释放锁资源,wait(),notify()及notifyAll()只能在synchronized语句中使用 | sleep()方法声明抛出InterruptedException异常,而yield()方法没有声明任何异常 |
可以在持有锁时或不持有锁时,执行 | 必须在持有锁时才能执行 |
notify()和 notifyAll()有什么区别?
notify() | notifyAll() |
---|---|
唤醒一个线程 | 会唤醒所有的线程 |
只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制 | 会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争 |
线程的 run() 和 start()和join()有什么区别?
start() 方法 | run() 方法 | join() |
---|---|---|
用于启动线程,而 start() 只能调用一次 | 用于执行线程的运行时代码,run() 可以重复调用 | 主线程等待子线程的终止。比如主线程代码中创建了10个子线程,主线程会等10个子线程执行完毕后,再执行下边的代码 |
join()主线程等待子线程的终止。主线程创建并启动子线程,如果子线程中要进行大量的耗时运算,主线程将可能早于子线程结束。如果主线程需要知道子线程的执行结果时,就需要等待子线程执行结束了。
join()执行时主线程进入等待状态waiting,一直到子线程执行完毕,主线程进入运行/就绪状态runnable
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1 thread finished");
});
t1.start();
t1.join();//t1线程执行结束,主线程才能继续执行
Thread.sleep(100);
System.out.println("main method finished");
}
Java中线程的状态?
新建状态new
线程被创建但尚未启动的状态,当new Thread()新建一个线程时如果线程没有运行start()方法
运行/就绪状态runnable
线程有可能正在执行,也有可能正在等待分配CPU资源
结束状态terminated
run()方法执行完毕,线程正常退出。
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
});
t1.start();
Thread.sleep(1000);
//线程任务正常执行完,线程就处于TERMINATED状态
System.out.println(t1.getState());//TERMINATED
}
阻塞状态blocked
当synchronized没有拿到资源,被放到entryList中阻塞
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
//t1线程拿不到锁资源,导致变成blocked状态
Thread t1 = new Thread(()->{
synchronized (object){
}
});
//main线程拿到锁资源
synchronized (object){
t1.start();
Thread.sleep(1000);
System.out.println(t1.getState());//BLOCKED
}
}
等待状态waiting
调用wait()将一个线程挂起时,线程需要被notify()或者notifyAll()唤醒。
waiting状态需要notify()或者notifyAll()唤醒,进入阻塞状态blocked,当执行notify()或者notifyAll()的线程释放锁,才有可能进入运行/就绪状态runnable
wait(),notify()及notifyAll()只能在synchronized语句中使用,因为必须保证notify()不会在wait()之前执行,保证代码原子性;另外wait()需要释放synchronized锁,所以只能在synchronized语句中使用。
生产者消费者模型1
public class SharedQueue {
private LinkedList<Object> queue = new LinkedList<>();
private int maxSize;
public SharedQueue(int maxSize) {
this.maxSize = maxSize;
}
public synchronized void enqueue(Object item) throws InterruptedException {
while (queue.size() == maxSize) {
wait(); // 如果队列已满,等待消费者消费
System.out.println(Thread.currentThread().getState());//WAITING
}
queue.addLast(item);
notifyAll(); // 通知等待的消费者
}
public synchronized Object dequeue() throws InterruptedException {
while (queue.isEmpty()) {
wait(); // 如果队列为空,等待生产者生产
}
Object item = queue.removeFirst();
notifyAll(); // 通知等待的生产者
return item;
}
}
生产者消费者模型2
Lock代替synchronized,Condition代替Object的wait、notify、notifyAll,把更复杂的用法,变成了更直观可控的对象方法
public class ReentrantLockTest {
private Queue queue;
private int max;
private ReentrantLock lock = new ReentrantLock();
private Condition notEmpty;
private Condition notFull;
public ReentrantLockTest(int size){
this.max = size;
queue = new LinkedList();
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
public void put(Object o) throws InterruptedException {
lock.lock();
try {
while (queue.size()==max){
notFull.await();//自动释放Lock锁
}
queue.add(o);
notEmpty.signalAll();//唤醒别的线程
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (queue.size()==0){
notEmpty.await();
}
Object item = queue.remove();
notFull.signalAll();
return item;
} finally {
lock.unlock();
}
}
}
时间等待状态timed_waiting
调用sleep(时间)休眠一段时间,sleep(时间)不会释放线程锁。join(时间)或者wait(时间),可以使用notify()或者notifyAll()主动唤醒也可以等待时间结束。
在 Java 程序中怎么保证多线程的运行安全?
方法一:同步代码块(对出现问题的核心代码使用进行加锁)
this指代当前账户对象
实例对象使用this作为锁对象
静态方法使用字节码(类名.class)对象作为锁对象
方法二:同步方法
同步代码块锁范围更小,同步方法锁的范围更大
方法三:Lock锁
private final Lock lock = new ReentrantLock();
synchronized 和 volatile 的区别是什么?
synchronized | volatile |
---|---|
修饰类、方法、代码段 | 变量修饰符 |
可以保证变量的修改可见性和原子性 | 能实现变量的修改可见性,有序性,不能保证原子性 |
可能会造成线程的阻塞 | 不会造成线程的阻塞 |
Java内存模型&可见性、原子性、有序性?
含义 | 解决方法1 | 解决方法2 | |
---|---|---|---|
可见性 | 一个线程对共享变量进行修改,另一个线程立即得到修改后的最新值 | 共享变量加volatile修饰 | synchronized保证可见性,synchronized保证了前一个线程释放锁之后,之前所做的所有修改,都能被获取到同一把锁的下一个线程所看到,也就是能读取到最新的值 |
原子性 | 在一次或多次操作中,要么所有的操作都执行并且不会受其他因素干扰而中断,要么所有的操作都不执行 | synchronized关键字 | |
有序性 | 程序中代码的执行顺序 | 共享变量加volatile | synchronized依然会发生重排序,但能保证同步代码块里面代码一起执行 |
可见性
是指一个线程对共享变量进行修改,另一个线程立即得到修改后的最新值
1,定义一个共享变量flag=true
2,线程a中定义一个while(flag)循环
3,线程b去修改flag=false。
结果:线程a无法读取到修改后的变量值,需要给共享变量加volatile修饰
缓存一致性协议,让共享变量副本失效,重新读取修改后的共享变量
也可以使用synchronized保证可见性,synchronized保证了前一个线程释放锁之后,之前所做的所有修改,都能被获取到同一把锁的下一个线程所看到,也就是能读取到最新的值
public class VolatileTest {
//创建一个共享变量
private static volatile boolean flag = true;
public static void main(String[] args) throws InterruptedException {
//创建一个线程不断读取共享变量
new Thread(() -> {
while(flag){
}
}).start();
Thread.sleep(2000);
//创建一个线程修改共享变量
new Thread(() -> {
flag = false;
System.out.println(flag);
}).start();
}
}
Java Memory Model(JMM)Java内存模型
主内存:存储共享变量
工作内存:当线程1需要操作共享变量时,先复制一份副本到自己的工作内存,处理完,再同步回主内存
原子性
除了long和double之外的基本类型(int、byte、boolean、short、char、float)的读写操作天然具备原子性。加了volatile后,所有变量的读写操作(包括long和double)也具备原子性
在一次或多次操作中,要么所有的操作都执行并且不会受其他因素干扰而中断,要么所有的操作都不执行。
当一个线程对共享变量操作到一半时,另外的线程也有可能来操作共享变量,干扰前一个线程的操作。
1,定义一个共享变量number
2,对象线程类中对number进行1000次++操作
3,启动5个线程来进行操作
4,将5个线程都join(),使加入主线程后再打印number的值
结果:<=5000,需要将number++锁起来
synchronized保证了只有一个线程能拿到锁,进入同步代码块
public class SyncTest {
//定义共享变量number
private static int number = 0;
private static Object object = new Object();
public static void main(String[] args) throws InterruptedException {
//对number进行1000次++
Runnable incre = () -> {
for (int i = 0; i < 1000; i++) {
synchronized (object){
number++;
}
}
};
List<Thread> list = new ArrayList<>();
//使用5个线程来进行
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(incre);
thread.start();
list.add(thread);
}
for (Thread thread : list) {
//子线程加入主线程,主线程等子线程执行完毕再执行
thread.join();
}
System.out.println(number);
}
}
有序性
是指程序中代码的执行顺序。Java在编译和运行时会对代码进行优化,会导致最终的执行顺序不一定就是我们编写代码时的顺序。
同步代码块加synchronized依然会发生重排序,但能保证同步代码块里面代码一起执行
或者给共享变量加volatile,保证共享变量不发生重排序
//单例模式:双重检查加锁
public class Singleton {
//方法1:使用volatile关键字进行修饰保证有序性
private static volatile Singleton singleton = null;
//创建一个私有的构造方法
private Singleton() {
}
//创建单例对象
public static Singleton getSingleton() {
if (singleton == null) {
//当两个线程同时执行到抢锁,线程1实例化对象后,线程2再拿到锁,需要再次检查是否对象为空
synchronized (Singleton.class) {
if (singleton == null) {
/**
new一个对象,基本步骤:
步骤2,3可能会重新排序,导致多线程环境下,一个线程设置内存空间地址给变量时,切换了线程,线程2看到singleton!=null,直接返回了
1,分配内存空间
2,初始化实例对象,主要是初始化实例对象属性的值
3,设置内存空间地址给变量
*/
singleton = new Singleton();
}
}
}
return singleton;
}
}
synchronized 和 Lock 有什么区别?
synchronized | Lock |
---|---|
关键字 | 接口 |
自动释放锁 | 手动释放锁 |
不可中断 | 可以中断thread.interrupt();也可以不中断 |
synchronized不知道当前线程是否获取到锁 | tryLock()返回值可以知道线程有没有拿到锁 |
锁住方法和代码块 | 只能锁住代码块 |
互斥锁 | 读写锁ReenreantReadWriteLock,读读操作时是共享锁,读写、写写操作时是互斥锁 |
非公平锁(线程a释放锁后,随机将锁给其他线程) | ReentrantLock可以控制是否是公平锁或非公平锁 |
synchronized 和 ReentrantLock 区别是什么?
synchronized | ReentrantLock |
---|---|
不需要手动释放和开启锁 | 必须手动获取与释放锁 |
可用于修饰方法、代码块 | 只适用于代码块锁 |
悲观锁、非公平锁 、互斥锁 | 悲观锁、可以指定公平锁或者非公平锁 、互斥锁 |
基于ObjectMonitor实现的 | 底层基于AQS实现的 |
竞争激烈时,存在锁升级概念 | 竞争激烈时,推荐ReentrantLock去实现,不存在锁升级概念 |
说一下 synchronized 底层实现原理?
monitorenter、monitorexit
基于对象实现的,对象头中有个markword,markword中64个bit位存储锁的状态、当前哪个线程持有锁。
比如无锁状态,bit位后三位为001;偏向锁bit位记录当前线程id,bit位后三位为101;轻量级锁指向当前线程虚拟机栈lock record的指针,bit位后两位00;重量级锁指向ObjectMonitor,owner指当前持有锁的线程,entryList指当前等待竞争的线程,bit位后两位10。
JDK6之后synchronized优化?
锁消除
比如StringBuffer的append()被synchronized修饰,如果是单线程中使用,编译时会去掉synchronized 关键字,省去加锁解锁的操作。
锁粗化
大量细粒度的synchronized代码块会增加系统开销,通过锁粗化将合并为一个大的synchronized代码块。减少锁竞争次数,降低锁开销,提高系统并发性能。但是可能会延长锁的持有时间,导致其他线程等待资源的时间增加,降低了并发度。
锁膨胀
for循环中大量synchronized代码块,去创建锁、释放锁会将synchronized膨胀到for循环外边。
锁升级
synchronized 锁升级原理
在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,别的线程自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。
锁的升级的目的
锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。
偏向锁
偏向锁不是锁,其实就是某一个线程在没有竞争情况下,是一个线程反复获得同一锁的情况,在锁对象头设置一个threadID。
轻量级锁
也就是自旋锁,乐观锁,CAS,比较并修改,当竞争锁的线程较少时,轻量级锁消耗CPU较少。
重量级锁
线程处于阻塞状态,多个等待线程放在锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争
如何对synchronized优化?
1,减少synchronized的范围
同步代码块中尽量短,减少同步代码块中代码的执行时间,减少锁的竞争。
2,降低synchronized锁的粒度
将一个锁拆分为多个锁提高并发量
3,读写分离
读读时共享锁,读写、写写操作时互斥锁,比如ReenreantReadWriteLock
AQS AbstractQueuedSynchronizer
state变量
在Semaphore中state表示剩余许可证数量
在CountDownLatch工具类里面,state表示需要倒数的数量
在ReentrantLock 中表示锁的占有情况。state=0表示没有线程持有这把锁,state=1表示有一个线程持有当前锁,state>1表示有一个线程多次持有这个锁,也就是重入锁。
AQS队列,先进先出队列,存储等待的线程
node节点有prev前驱节点、waitStatus等待状态、thread、nextWaiter、next后续节点属性
头节点head指向第一个node,也就是当前持有锁线程;尾节点tail指向最后一个node
获取、释放方法
ReentrantLock lock()尝试去获取锁,如果state>0且当前线程不是持有锁的线程,就处于阻塞状态,直到获取到锁。
Semaphore的acquire()获取一个许可证,也就是state-1是否大于0
CountDownLatch的await()方法根据state>0线程处于阻塞状态,直到state=0,等待直到倒数结束。
CountDownLatch释放就是countDown()作用是倒数一个数,让state减1
ReenreantReadWriteLock
ReenreantReadWriteLock还是基于AQS实现的,还是对state进行操作,拿到锁资源就去干活,没有拿到锁资源,就去AQS队列排队。
读锁操作
基于state的高16位进行操作。因为读锁是共享锁,所以同一时间会有多个读线程持有读锁。这样就无法确认每个线程读锁重入的次数,所以在高16位+1的同时,使用ThreadLocal记录当前线程锁重入的次数。
写锁操作
基于state的低16位进行操作,重入方式和ReentrantLock一致,依然是对state进行+1操作,只要确认持有锁资源的线程是当前写锁线程即可。
写锁的饥饿问题
当前资源被读锁占领时,如果来了一个写操作,需要在等待队列中排队。
后续来了很多读操作,如果读操作,看到当前锁状态是读锁,直接state+1,那么写操作将要等待很长时间。
为了避免写操作长时间等待问题,当一个读操作需要获取读锁资源时,需要在等待队列中排队,直到等待队列前面的写操作执行完毕后,才能获取锁资源。不允许后续读操作插队。
锁升降级策略
只能从写锁降级为读锁,不能从读锁升级为写锁。
写锁是互斥锁,别的线程不可能也持有写锁,所以可以在持有写锁时,也去持有读锁;
读锁是共享锁,升级为写锁时,需要等待别的读锁都释放资源,当多个读锁线程都想升级为写锁时,会发生死锁现象。
ReenreantLock
lock()
没有返回值,阻塞加锁,如果没有拿到锁,后边的代码不会执行
boolean tryLock()
尝试去加锁,有返回值true或者false,非阻塞,要不要继续执行,就要看代码逻辑了;其实就是自旋锁,不断去尝试去加锁,直到成功。tryLock()是非公平锁,即使ReenreantLock指定为公平锁,tryLock()也会插队去获取锁。
lockInterruptibly()
除非当前线程在获取锁期间被中断,否则便会一直尝试获取直到获取到为止
public class ReentrantLockTest {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new ThreadDemo());
Thread thread1 = new Thread(new ThreadDemo());
thread.start();
thread1.start();
// 是第2个线程中断
//当thread1执行中断时,lock.lockInterruptibly()直接抛出异常停止加锁
thread1.interrupt();
}
static class ThreadDemo implements Runnable {
private static Lock lock = new ReentrantLock();
@Override
public void run() {
try {
lock.lockInterruptibly();
System.out.println(Thread.currentThread().getName() + " 开始");
System.out.println("休息5s----");
TimeUnit.SECONDS.sleep(5);
System.out.println(Thread.currentThread().getName() + "正常结束!");
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " 中断了");
} finally{
lock.unlock();
}
}
}
}
lock.unlock()
内部会把锁的计数器state减1,直到减到0就代表这把锁已经完全释放了
说一下 atomic 的原理?
atomic 主要利用 CAS (Compare And Swap) 和 volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
CAS机制?
一种乐观锁的实现机制,Unsafe类中方法,全称是compare and swap比较并交换,主要功能是保证在多线程环境下,对共享变量修改的原子性。
CAS核心思想:仅当预期值A和当前的内存值V相同时,才将内存值修改为B。
底层是"lock cmpxchg"指令实现,保证原子性。
public class Test1 {
static int b =0;
public static void main(String[] args) throws InterruptedException {
//AtomicInteger原子类
AtomicInteger a = new AtomicInteger(0);//轻量级锁、自旋锁
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
a.incrementAndGet();
b++;
}
});
t1.start();
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
a.incrementAndGet();
b++;
}
});
t2.start();
Thread.sleep(1000);
System.out.println("a="+a);//a=20000
System.out.println("b="+b);//b=18203
}
}
CAS机制的好处
避免了多线程操作共享变量时,如果一个线程尝试获取锁而发现锁已经被其他线程持有,那么这个线程就会被阻塞,直到它获取到锁为止。在底层,这种阻塞通常会导致线程从用户态切换到内核态。这是因为获取锁、等待 I/O 操作、异常处理等都可能需要操作系统的参与,而操作系统在内核态提供了相应的服务。
CAS机制存在的问题
1,自旋次数过多,会占用cpu资源
2,ABA问题
3,线程安全范围不能灵活控制
CAS的ABA问题
以版本号判断值是否被修改
public static void main(String[] args) throws InterruptedException {
//初始值10,初始版本1
AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference(10,1);
//Java 中用来实现线程等待的工具类,它可以让一个或多个线程等待其他线程完成后再继续执行
CountDownLatch countDownLatch = new CountDownLatch(2);
new Thread(() -> {
//compareAndSet 方法尝试将值从 10 改为 11,然后再改回 10,每次操作后输出当前版本号
System.out.println(Thread.currentThread().getName() + " 第一次版本:" + atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(10, 11, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + " 第二次版本:" + atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(11, 10, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + " 第三次版本:" + atomicStampedReference.getStamp());
//countDown()作用是倒数一个数,让state减1
countDownLatch.countDown();
}).start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 第一次版本:" + atomicStampedReference.getStamp());
try {
//第二个线程在睡眠 2 秒后,尝试将值从 10 改为 12,并输出修改是否成功、当前版本号和当前值
TimeUnit.SECONDS.sleep(2);
boolean isSuccess = atomicStampedReference.compareAndSet(10,12, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + " 修改是否成功:" + isSuccess + " 当前版本:" + atomicStampedReference.getStamp() + " 当前值:" + atomicStampedReference.getReference());
countDownLatch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
//await()方法根据state>0线程处于阻塞状态,直到state=0,等待直到倒数结束。
countDownLatch.await();
}
原子类
高并发下LongAdder比AtomicLong效率更高,LongAdder引入分段锁,当竞争不激烈时,所有线程通过CAS对同一个base变量进行修改,当竞争激烈时,LongAdder把不同线程对应到不同cell上,降低了冲突的概率,提高了并发性能。
但是LongAdder只提供了数值的加减操作。
AtomicInteger | LongAccumulator | LongAdder | CountDownLatch | Semaphore |
---|---|---|---|---|
提供原子操作的Integer类,通过线程安全的方式操作加减 | 根据初始值和累加规则,大量计算,求最值、均值、sum值 | 特殊的LongAccumulator,即初始值为0,累加规则为加法 | 让某一个线程等待多个线程的操作完成之后再执行,countDown()、await()子线程计数器减一,直到state=0,主线程解除阻塞 | 信号量,提供若干个许可证,只有拿到许可证才能执行,执行完再手动释放许可证 |
AtomicInteger
public class Test1 {
static int b =0;
public static void main(String[] args) throws InterruptedException {
//AtomicInteger原子类
AtomicInteger a = new AtomicInteger(0);//轻量级锁、自旋锁
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
a.incrementAndGet();
b++;
}
});
t1.start();
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
a.incrementAndGet();
b++;
}
});
t2.start();
Thread.sleep(1000);
System.out.println("a="+a);//a=20000
System.out.println("b="+b);//b=18203
}
}
LongAccumulator
大量计算,并且需要并行计算时
public static void main(String[] args) throws InterruptedException {
List<Integer> integerList = List.of(3, 6, 9, 2, 5, 8, 1, 4, 7, 10, 13, 12, 11, 15, 14, 16);
int numThreads = 8; // 线程数量
int chunkSize = integerList.size() / numThreads; // 每个线程处理的子列表大小
// 初始化 LongAccumulator,使用 lambda 表达式来定义更新函数
LongAccumulator maxAccumulator = new LongAccumulator((x, y) -> Math.max(x, y), Integer.MIN_VALUE);
List<Thread> threads = new ArrayList<>();
// 创建并启动线程
for (int i = 0; i < numThreads; i++) {
int startIndex = i * chunkSize;
int endIndex = (i == numThreads - 1) ? integerList.size() : (startIndex + chunkSize);
List<Integer> subList = integerList.subList(startIndex, endIndex);
Thread thread = new Thread(() -> {
long max = Integer.MIN_VALUE;
for (Integer num : subList) {
max = Math.max(max, num);
}
//将每个线程计算得到的子列表最大值与LongAccumulator中的当前最大值进行比较和更新
maxAccumulator.accumulate(max);
});
threads.add(thread);
thread.start();
}
// 等待所有线程执行完毕
for (Thread thread : threads) {
thread.join();
}
// 获取累加器的当前值,即为列表中的最大值
long max = maxAccumulator.get();
System.out.println("最大值是: " + max);
}
CountDownLatch计数器
可以用于控制一个或多个线程等待多个任务完成后再执行,计数器只能够被减少,不能够被增加。
也可以使用计数器控制多个任务等待一个任务执行结束再执行。
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(3);
Set<Integer> prices = new CountDownLatchDemo().getPrices(pool);
System.out.println(prices);
}
private Set<Integer> getPrices(ExecutorService pool) throws InterruptedException {
Set<Integer> prices = Collections.synchronizedSet(new HashSet<Integer>());
//倒数次数
CountDownLatch downLatch = new CountDownLatch(3);
pool.submit(new Task(101,prices,downLatch));
pool.submit(new Task(102,prices,downLatch));
pool.submit(new Task(103,prices,downLatch));
//主线程开始等待
downLatch.await(3,TimeUnit.SECONDS);
pool.shutdown();
return prices;
}
private class Task implements Runnable{
Integer productId;
Set<Integer> prices;
CountDownLatch downLatch;
public Task(Integer productId, Set<Integer> prices, CountDownLatch downLatch) {
this.productId = productId;
this.prices = prices;
this.downLatch = downLatch;
}
@Override
public void run() {
int price = 0;
try {
Thread.sleep((long) (Math.random()*4000));
price = (int)(Math.random()*4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
prices.add(price);
//子线程计数器减一,直到state=0,主线程解除阻塞
downLatch.countDown();
}
}
}
Semaphore信号量(限制并发访问线程数)
FixedThreadPool虽然可以创建固定大小线程,但是比如除了每天0点需要访问慢服务,其余时间可以允许更多线程访问,就可以多加一个if将符合时间限制时,用信号量去额外限制。
也可以一次性获取或释放多个许可证。比如task1执行很耗资源,要求一次性获取到5个许可证才能执行,task2消耗较少资源,就要求一次性获取1个许可证就能执行。避免了同时执行很多消耗资源的任务
public class SemaphoreDemo {
//信号量,提供3个许可证,只有拿到许可证才能执行
private static Semaphore semaphore = new Semaphore(3);
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(10);
for (int i=0; i<100; i++){
pool.submit(new Task());
}
pool.shutdown();
}
private static class Task implements Runnable{
@Override
public void run() {
try {
//获取许可证
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 拿到了许可证,花费时间执行慢服务");
try {
Thread.sleep((long) (Math.random()*4000));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 执行完毕,释放许可证");
//释放许可证
semaphore.release();
}
}
}
锁的分类
可重入锁、不可重入锁
可重入锁
当前线程获取到A锁,在获取之后尝试再次获取A锁时可以直接拿到的。
Java中提供的synchronized、ReentrantLock 、ReentrantReadWriteLock都是可重入锁
不可重入锁
当前线程获取到A锁,在获取之后尝试再次获取A锁,无法获取到,因为A锁被当前线程占用着,需要等待自己释放锁再获取锁。
乐观锁、悲观锁
悲观锁
获取不到锁资源时,会将当前线程挂起(进入blocked、waiting),线程挂起会涉及到用户态和内核态切换,而这种切换时比较消耗资源的。
用户态:JVM可以自行执行的指令,不需要借助操作系统执行。
内核态:jvm不可以自行执行,需要操作系统才可以执行。
Java中提供的synchronized、ReentrantLock 、ReentrantReadWriteLock都是悲观锁
适用于并发写入多,临界区代码复杂,竞争激烈等场景,这种场景下悲观锁可以避免大量无用的反复尝试等消耗
乐观锁
获取不到锁资源,可以再次让CPU调度,重新尝试获取锁资源。
Atomic原子性类中,就是基于CAS乐观锁实现的,就是乐观锁的一种实现。
适用于大部分是读取,少部分是修改的场景。也适合虽然读写都很多,但是并发不激烈的场景。在这种场景下,乐观锁不加锁的特点能让性能大幅提高
公平锁、非公平锁
Java中提供的synchronized是非公平锁
Java中提供的ReentrantLock 、ReentrantReadWriteLock可以实现公平锁和非公平锁
公平锁
线程A释放到锁资源时,等待队列中线程B、线程C依次去竞争锁资源,这时线程D刚好尝试获取锁,看到等待队列不为null,也会加入等待队列,不会去抢占锁。
//构造方法指定公平锁或非公平锁
ReentrantLock lock = new ReentrantLock(false);
非公平锁
线程A释放到锁资源时,等待队列中线程B处于阻塞状态、这时线程C刚好尝试获取锁,相比于等待唤醒线程B,插队的线程C跳过阻塞状态,当锁代码中执行内容不多时,线程C很快完成任务,当线程B被完全唤醒前,将锁交出去。线程C不会判断等待队列是否有线程正在等待。
互斥锁、共享锁
Java中提供的synchronized、ReentrantLock是互斥锁
Java提供的ReentrantReadWriteLock,有互斥锁也有共享锁。
互斥锁:同一时间点,只会有一个线程持有当前互斥锁。
共享锁:同一时间点,ReentrantReadWriteLock读写锁中,读读操作是共享锁,读写操作是互斥锁。
@Contended注解的作用?
缓存行
CPU读取内存数据时并非一次只读一个字节,而是会读一段64字节长度的连续的内存块(chunks of memory),这些块我们称之为缓存行(Cache line)。
已知long类型占8个字节,缓存行长度为64个字节,那么一个缓存行可以保存8个long型变量,我们已经有了一个long型的x,假设x所在缓存行里还有其他7个long型变量,v1到v7:x, v1, v2, v3, v4, v5 ,v6 ,v7
伪共享
这个缓存行可以被许多线程访问。如果其中一个修改了v2,那么会导致Thread1和Thread2都会重新加载整个缓存行。你可能会疑惑为什么修改了v2会导致Thread1和Thread2重新加载该缓存行,毕竟只是修改了v2的值啊。虽然说这些修改逻辑上是互相独立的,但同一缓存行上的数据是统一维护的,一致性的粒度并非体现在单个元素上。这种不必要的数据共享就称之为“伪共享”(False Sharing)。
@Contended注解
@Contented注解将y移动到远离对象头部的地方,(以避免和x一起被加载到同一个缓存行)。
public class Point {
int x;
@Contended int y;
}
ThreadLocal是什么?有哪些使用场景?
ThreadLocal 为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
ThreadLocal 的经典使用场景是数据库连接和 session 管理等。
每个thread有独立的ThreadLocalMap,ThreadLocalMap包含很多entry对象,entry对象中key是threadlocal这个引用,value是需要操作的变量值
因为key是弱引用,value是强引用,如果下一次操作时没有引用key,就会回收key,导致key为null,value还是之前的强引用。
如果没有调用ThreadLocal的get(),set(),remove(),这些方法会回收key为null的数据。可能造成key为null的数据,一直无法被回收,导致内存泄露。
ThreadLocal
每个Thread对象都持有一个ThreadLocalMap类型的成员变量,ThreadLocalMap可以保存多个kv,k就是比如ThreadLocal<泛型> threadLocal,是一个弱引用,在垃圾回收时会被回收掉,value就是需要保存的值。需要调用remove()方法释放掉。
ThreadLocal与synchronized区别?
ThreadLocal通过让每个线程独享自己的副本,避免了资源的竞争
synchronized用于临界资源的分配,在同一时刻限制最多只有一个线程能访问该资源
每个Thread内有自己的实例副本,不共享
//匿名内部类设置ThreadLocal的value
public class ThreadSafeFormatter {
//利用 ThreadLocal 给每个线程分配自己的 dateFormat 对象
//不但保证了线程安全,还高效的利用了内存
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
//每个线程需要一个独享对象(通常是工具类,典型需要使用的类有SimpleDateFormat和Random)
return new SimpleDateFormat("mm:ss");
}
};
}
public class ThreadLocalTest {
public static ExecutorService threadPool = Executors.newFixedThreadPool(16);
public static void main(String[] args) {
for (int i=0; i<1000; i++){
int newI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date =new ThreadLocalTest().date(newI);
System.out.println(date);
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
threadPool.shutdown();
}
private String date(int seconds) {
Date date=new Date(1000*seconds);
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
return dateFormat.format(date);
}
}
不同方法间的资源共享
public class ThreadLocalTest2 {
//实现不同方法间的资源共享
public static ThreadLocal<User> hold = new ThreadLocal();
public static void main(String[] args) {
User user = new User();
user.setName("zs");
hold.set(user);
process1();
}
private static void process1() {
User user = hold.get();
System.out.println("Service2拿到用户名: " + user.getName());
process2();
}
private static void process2() {
hold.remove();
System.out.println("清除threadLocal中值");
}
}
死锁
死锁产生有四个必要条件
互斥条件:指某资源在一段时间内只能被一个进程使用。比如,打印机就是典型的互斥资源,同一时间只能有一个进程使用。
占有和等待条件:一个进程至少占有一个资源,同时等待获取其他被其他进程占有的资源。
不可剥夺条件:已经分配给一个进程的资源在未使用完之前,不能被剥夺,只能由该进程释放。
循环等待条件:存在一种进程资源的循环等待链,每个进程占有下一个进程所需的至少一个资源。
死锁示例
public class DeadlockDemo {
// 创建两个资源
private static Object resource1 = new Object();
private static Object resource2 = new Object();
public static void main(String[] args) {
// 线程1尝试锁定资源1后,再锁定资源2
new Thread(() -> {
synchronized (resource1) {
System.out.println("线程1锁定资源1");
try {
// 模拟处理资源所需时间
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource2) {
System.out.println("线程1锁定资源2");
}
}
}).start();
// 线程2尝试锁定资源2后,再锁定资源1
new Thread(() -> {
synchronized (resource2) {
System.out.println("线程2锁定资源2");
try {
// 模拟处理资源所需时间
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource1) {
System.out.println("线程2锁定资源1");
}
}
}).start();
}
}
检测死锁
方法1:jstack命令
方法2:ThreadMXBean检测死锁
public class DeadlockDetector {
public static void checkForDeadlocks() {
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
if (deadlockedThreads != null) {
System.out.println("检测到死锁,涉及的线程ID如下:");
for (long threadId : deadlockedThreads) {
System.out.println("线程ID: " + threadId);
}
} else {
System.out.println("未检测到死锁。");
}
}
}
@PostConstruct
public static void test2() {
// 定期检测死锁
while (true) {
try {
Thread.sleep(5000); // 每5秒检查一次
DeadlockDetector.checkForDeadlocks();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
死锁解决方法
锁定顺序
锁定顺序的原理就是按照一定的顺序申请资源。比如,咱们有资源A和资源B,那就规定所有线程都必须先锁定A再锁定B。这样就可以避免循环等待条件的发生。
public class LockOrderDemo {
private static Object resourceA = new Object();
private static Object resourceB = new Object();
public static void main(String[] args) {
// 线程1:按照A -> B的顺序加锁
new Thread(() -> {
synchronized (resourceA) {
System.out.println("线程1锁定资源A");
try {
// 模拟处理资源所需时间
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resourceB) {
System.out.println("线程1锁定资源B");
}
}
}).start();
// 线程2:也按照A -> B的顺序加锁
new Thread(() -> {
synchronized (resourceA) {
System.out.println("线程2锁定资源A");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resourceB) {
System.out.println("线程2锁定资源B");
}
}
}).start();
}
}
锁超时
锁超时是另一种策略,它允许线程在等待锁超过一定时间后放弃,从而避免了无限等待的情况。Java中的ReentrantLock支持带超时的锁请求
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;
public class LockTimeoutDemo {
private static ReentrantLock lock1 = new ReentrantLock();
private static ReentrantLock lock2 = new ReentrantLock();
public static void main(String[] args) {
Thread thread1 = new Thread(new LockTask(lock1, lock2), "Thread1");
Thread thread2 = new Thread(new LockTask(lock2, lock1), "Thread2");
thread1.start();
thread2.start();
}
static class LockTask implements Runnable {
private ReentrantLock firstLock;
private ReentrantLock secondLock;
public LockTask(ReentrantLock firstLock, ReentrantLock secondLock) {
this.firstLock = firstLock;
this.secondLock = secondLock;
}
@Override
public void run() {
try {
// 尝试锁定第一个锁,并设置超时
if (!firstLock.tryLock(50, TimeUnit.MILLISECONDS)) {
System.out.println(Thread.currentThread().getName() + " 无法立即获取锁,放弃并重试");
firstLock.lock();
}
// 模拟处理资源所需时间
Thread.sleep(100);
// 尝试锁定第二个锁,并设置超时
if (!secondLock.tryLock(50, TimeUnit.MILLISECONDS)) {
System.out.println(Thread.currentThread().getName() + " 无法立即获取锁,放弃并重试");
secondLock.lock();
}
System.out.println(Thread.currentThread().getName() + " 成功获取两个锁");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
firstLock.unlock();
secondLock.unlock();
}
}
}
}