三天吃透Java并发八股文!

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了三天吃透Java并发八股文!相关的知识,希望对你有一定的参考价值。

本文已经收录到Github仓库,该仓库包含计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享等核心知识点,欢迎star~

Github地址:​​https://github.com/Tyson0314/Java-learning​


线程池

线程池:一个管理线程的池子。

为什么平时都是使用线程池创建线程,直接new一个线程不好吗?

嗯,手动创建线程有两个缺点

  1. 不受控风险
  2. 频繁创建开销大

为什么不受控

系统资源有限,每个人针对不同业务都可以手动创建线程,并且创建线程没有统一标准,比如创建的线程有没有名字等。当系统运行起来,所有线程都在抢占资源,毫无规则,混乱场面可想而知,不好管控。

频繁手动创建线程为什么开销会大?跟new Object() 有什么差别?

虽然Java中万物皆对象,但是new Thread() 创建一个线程和 new Object()还是有区别的。

new Object()过程如下:

  1. JVM分配一块内存 M
  2. 在内存 M 上初始化该对象
  3. 将内存 M 的地址赋值给引用变量 obj

创建线程的过程如下:

  1. JVM为一个线程栈分配内存,该栈为每个线程方法调用保存一个栈帧
  2. 每一栈帧由一个局部变量数组、返回值、操作数堆栈和常量池组成
  3. 每个线程获得一个程序计数器,用于记录当前虚拟机正在执行的线程指令地址
  4. 系统创建一个与Java线程对应的本机线程
  5. 将与线程相关的描述符添加到JVM内部数据结构中
  6. 线程共享堆和方法区域

创建一个线程大概需要1M左右的空间(Java8,机器规格2c8G)。可见,频繁手动创建/销毁线程的代价是非常大的。

为什么使用线程池?

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,可以不需要等到线程创建就能立即执行。
  • 提高线程的可管理性。统一管理线程,避免系统创建大量同类线程而导致消耗完内存。

线程池执行原理?

三天吃透Java并发八股文!_Java面试

  1. 当线程池里存活的线程数小于核心线程数​​corePoolSize​​时,这时对于一个新提交的任务,线程池会创建一个线程去处理任务。当线程池里面存活的线程数小于等于核心线程数​​corePoolSize​​时,线程池里面的线程会一直存活着,就算空闲时间超过了​​keepAliveTime​​,线程也不会被销毁,而是一直阻塞在那里一直等待任务队列的任务来执行。
  2. 当线程池里面存活的线程数已经等于corePoolSize了,这是对于一个新提交的任务,会被放进任务队列workQueue排队等待执行。
  3. 当线程池里面存活的线程数已经等于​​corePoolSize​​了,并且任务队列也满了,假设​​maximumPoolSize>corePoolSize​​,这时如果再来新的任务,线程池就会继续创建新的线程来处理新的任务,知道线程数达到​​maximumPoolSize​​,就不会再创建了。
  4. 如果当前的线程数达到了​​maximumPoolSize​​,并且任务队列也满了,如果还有新的任务过来,那就直接采用拒绝策略进行处理。默认的拒绝策略是抛出一个RejectedExecutionException异常。

线程池参数有哪些?

ThreadPoolExecutor 的通用构造函数:

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler);

1、​​corePoolSize​​:当有新任务时,如果线程池中线程数没有达到线程池的基本大小,则会创建新的线程执行任务,否则将任务放入阻塞队列。当线程池中存活的线程数总是大于 corePoolSize 时,应该考虑调大 corePoolSize。

2、​​maximumPoolSize​​:当阻塞队列填满时,如果线程池中线程数没有超过最大线程数,则会创建新的线程运行任务。否则根据拒绝策略处理新任务。非核心线程类似于临时借来的资源,这些线程在空闲时间超过 keepAliveTime 之后,就应该退出,避免资源浪费。

3、​​BlockingQueue​​:存储等待运行的任务。

4、​​keepAliveTime​​:非核心线程空闲后,保持存活的时间,此参数只对非核心线程有效。设置为0,表示多余的空闲线程会被立即终止。

5、​​TimeUnit​​:时间单位

TimeUnit.DAYS
TimeUnit.HOURS
TimeUnit.MINUTES
TimeUnit.SECONDS
TimeUnit.MILLISECONDS
TimeUnit.MICROSECONDS
TimeUnit.NANOSECONDS

6、​​ThreadFactory​​:每当线程池创建一个新的线程时,都是通过线程工厂方法来完成的。在 ThreadFactory 中只定义了一个方法 newThread,每当线程池需要创建新线程就会调用它。

public class MyThreadFactory implements ThreadFactory 
private final String poolName;

public MyThreadFactory(String poolName)
this.poolName = poolName;


public Thread newThread(Runnable runnable)
return new MyAppThread(runnable, poolName);//将线程池名字传递给构造函数,用于区分不同线程池的线程

7、​​RejectedExecutionHandler​​:当队列和线程池都满了的时候,根据拒绝策略处理新任务。

AbortPolicy:默认的策略,直接抛出RejectedExecutionException
DiscardPolicy:不处理,直接丢弃
DiscardOldestPolicy:将等待队列队首的任务丢弃,并执行当前任务
CallerRunsPolicy:由调用线程处理该任务

线程池大小怎么设置?

如果线程池线程数量太小,当有大量请求需要处理,系统响应比较慢,会影响用户体验,甚至会出现任务队列大量堆积任务导致OOM。

如果线程池线程数量过大,大量线程可能会同时抢占 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了执行效率。

CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为​​ N(CPU 核心数)+1​​,多出来的一个线程是为了防止某些原因导致的线程阻塞(如IO操作,线程sleep,等待锁)而带来的影响。一旦某个线程被阻塞,释放了CPU资源,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。

I/O 密集型任务(2N): 系统的大部分时间都在处理 IO 操作,此时线程可能会被阻塞,释放CPU资源,这时就可以将 CPU 交出给其它线程使用。因此在 IO 密集型任务的应用中,可以多配置一些线程,具体的计算方法:​​最佳线程数 = CPU核心数 * (1/CPU利用率) = CPU核心数 * (1 + (IO耗时/CPU耗时))​​,一般可设置为2N。

线程池的类型有哪些?适用场景?

常见的线程池有 ​​FixedThreadPool​​、​​SingleThreadExecutor​​、​​CachedThreadPool​​ 和 ​​ScheduledThreadPool​​。这几个都是 ​​ExecutorService​​ 线程池实例。

FixedThreadPool

固定线程数的线程池。任何时间点,最多只有 nThreads 个线程处于活动状态执行任务。

public static ExecutorService newFixedThreadPool(int nThreads) 
return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());

使用无界队列 LinkedBlockingQueue(队列容量为 Integer.MAX_VALUE),运行中的线程池不会拒绝任务,即不会调用RejectedExecutionHandler.rejectedExecution()方法。

maxThreadPoolSize 是无效参数,故将它的值设置为与 coreThreadPoolSize 一致。

keepAliveTime 也是无效参数,设置为0L,因为此线程池里所有线程都是核心线程,核心线程不会被回收(除非设置了executor.allowCoreThreadTimeOut(true))。

适用场景:适用于处理CPU密集型的任务,确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程,即适用执行长期的任务。需要注意的是,FixedThreadPool 不会拒绝任务,在任务比较多的时候会导致 OOM。

SingleThreadExecutor

只有一个线程的线程池。

public static ExecutionService newSingleThreadExecutor() 
return new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());

使用无界队列 LinkedBlockingQueue。线程池只有一个运行的线程,新来的任务放入工作队列,线程处理完任务就循环从队列里获取任务执行。保证顺序的执行各个任务。

适用场景:适用于串行执行任务的场景,一个任务一个任务地执行。在任务比较多的时候也是会导致 OOM。

CachedThreadPool

根据需要创建新线程的线程池。

public static ExecutorService newCachedThreadPool() 
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());

如果主线程提交任务的速度高于线程处理任务的速度时,​​CachedThreadPool​​ 会不断创建新的线程。极端情况下,这样会导致耗尽 cpu 和内存资源。

使用没有容量的SynchronousQueue作为线程池工作队列,当线程池有空闲线程时,​​SynchronousQueue.offer(Runnable task)​​提交的任务会被空闲线程处理,否则会创建新的线程处理任务。

适用场景:用于并发执行大量短期的小任务。​​CachedThreadPool​​允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。

ScheduledThreadPoolExecutor

在给定的延迟后运行任务,或者定期执行任务。在实际项目中基本不会被用到,因为有其他方案选择比如​​quartz​​。

使用的任务队列 ​​DelayQueue​​ 封装了一个 ​​PriorityQueue​​,​​PriorityQueue​​ 会对队列中的任务进行排序,时间早的任务先被执行(即​​ScheduledFutureTask​​ 的 ​​time​​ 变量小的先执行),如果time相同则先提交的任务会被先执行(​​ScheduledFutureTask​​ 的 ​​squenceNumber​​ 变量小的先执行)。

执行周期任务步骤:

  1. 线程从 ​​DelayQueue​​ 中获取已到期的 ​​ScheduledFutureTask(DelayQueue.take())​​。到期任务是指 ​​ScheduledFutureTask​​的 time 大于等于当前系统的时间;
  2. 执行这个 ​​ScheduledFutureTask​​;
  3. 修改 ​​ScheduledFutureTask​​ 的 time 变量为下次将要被执行的时间;
  4. 把这个修改 time 之后的 ​​ScheduledFutureTask​​ 放回 ​​DelayQueue​​ 中(​​DelayQueue.add()​​)。

三天吃透Java并发八股文!_Java面试_02

适用场景:周期性执行任务的场景,需要限制线程数量的场景。

进程线程

进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间。

线程是比进程更小的执行单位,它是在一个进程中独立的控制流,一个进程可以启动多个线程,每条线程并行执行不同的任务。

线程的生命周期

初始(NEW):线程被构建,还没有调用 start()。

运行(RUNNABLE):包括操作系统的就绪和运行两种状态。

阻塞(BLOCKED):一般是被动的,在抢占资源中得不到资源,被动的挂起在内存,等待资源释放将其唤醒。线程被阻塞会释放CPU,不释放内存。

等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。

超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。

终止(TERMINATED):表示该线程已经执行完毕。

三天吃透Java并发八股文!_Java面试_03

图片来源:Java并发编程的艺术

讲讲线程中断?

线程中断即线程运行过程中被其他线程给打断了,它与 stop 最大的区别是:stop 是由系统强制终止线程,而线程中断则是给目标线程发送一个中断信号,如果目标线程没有接收线程中断的信号并结束线程,线程则不会终止,具体是否退出或者执行其他逻辑取决于目标线程。

线程中断三个重要的方法:

1、java.lang.Thread#interrupt

调用目标线程的​​interrupt()​​方法,给目标线程发一个中断信号,线程被打上中断标记。

2、java.lang.Thread#isInterrupted()

判断目标线程是否被中断,不会清除中断标记。

3、java.lang.Thread#interrupted

判断目标线程是否被中断,会清除中断标记。

private static void test2() 
Thread thread = new Thread(() ->
while (true)
Thread.yield();

// 响应中断
if (Thread.currentThread().isInterrupted())
System.out.println("Java技术栈线程被中断,程序退出。");
return;


);
thread.start();
thread.interrupt();

创建线程有哪几种方式?

  • 通过扩展​​Thread​​类来创建多线程
  • 通过实现​​Runnable​​接口来创建多线程
  • 实现​​Callable​​接口,通过​​FutureTask​​接口创建线程。
  • 使用​​Executor​​框架来创建线程池。

继承 Thread 创建线程代码如下。run()方法是由jvm创建完操作系统级线程后回调的方法,不可以手动调用,手动调用相当于调用普通方法。

/**
* @author: 程序员大彬
* @time: 2021-09-11 10:15
*/
public class MyThread extends Thread
public MyThread()


@Override
public void run()
for (int i = 0; i < 10; i++)
System.out.println(Thread.currentThread() + ":" + i);



public static void main(String[] args)
MyThread mThread1 = new MyThread();
MyThread mThread2 = new MyThread();
MyThread myThread3 = new MyThread();
mThread1.start();
mThread2.start();
myThread3.start();

Runnable 创建线程代码

/**
* @author: 程序员大彬
* @time: 2021-09-11 10:04
*/
public class RunnableTest
public static void main(String[] args)
Runnable1 r = new Runnable1();
Thread thread = new Thread(r);
thread.start();
System.out.println("主线程:["+Thread.currentThread().getName()+"]");



class Runnable1 implements Runnable
@Override
public void run()
System.out.println("当前线程:"+Thread.currentThread().getName());

实现Runnable接口比继承Thread类所具有的优势:

  1. 可以避免java中的单继承的限制
  2. 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类

Callable 创建线程代码

/**
* @author: 程序员大彬
* @time: 2021-09-11 10:21
*/
public class CallableTest
public static void main(String[] args)
Callable1 c = new Callable1();

//异步计算的结果
FutureTask<Integer> result = new FutureTask<>(c);

new Thread(result).start();

try
//等待任务完成,返回结果
int sum = result.get();
System.out.println(sum);
catch (InterruptedException | ExecutionException e)
e.printStackTrace();





class Callable1 implements Callable<Integer>

@Override
public Integer call() throws Exception
int sum = 0;

for (int i = 0; i <= 100; i++)
sum += i;

return sum;

使用 Executor 创建线程代码

/**
* @author: 程序员大彬
* @time: 2021-09-11 10:44
*/
public class ExecutorsTest
public static void main(String[] args)
//获取ExecutorService实例,生产禁用,需要手动创建线程池
ExecutorService executorService = Executors.newCachedThreadPool();
//提交任务
executorService.submit(new RunnableDemo());



class RunnableDemo implements Runnable
@Override
public void run()
System.out.println("大彬");

什么是线程死锁?

线程死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象。若无外力作用,它们都将无法推进下去。

如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方持有的资源,所以这两个线程就会互相等待而进入死锁状态。

三天吃透Java并发八股文!_Java并发_04

下面通过例子说明线程死锁,代码来自并发编程之美。

public class DeadLockDemo 
private static Object resource1 = new Object();//资源 1
private static Object resource2 = new Object();//资源 2

public static void main(String[] args)
new Thread(() ->
synchronized (resource1)
System.out.println(Thread.currentThread() + "get resource1");
try
Thread.sleep(1000);
catch (InterruptedException e)
e.printStackTrace();

System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2)
System.out.println(Thread.currentThread() + "get resource2");


, "线程 1").start();

new Thread(() ->
synchronized (resource2)
System.out.println(Thread.currentThread() + "get resource2");
try
Thread.sleep(1000);
catch (InterruptedException e)
e.printStackTrace();

System.out.println(Thread.currentThread() + "waiting get resource1");
synchronized (resource1)
System.out.println(Thread.currentThread() + "get resource1");


, "线程 2").start();

代码输出如下:

Thread[线程 1,5,main]get resource1
Thread[线程 2,5,main]get resource2
Thread[线程 1,5,main]waiting get resource2
Thread[线程 2,5,main]waiting get resource1

线程 A 通过 ​​synchronized​​ (resource1) 获得 resource1 的监视器锁,然后通过 ​​Thread.sleep(1000)​​。让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。

线程死锁怎么产生?怎么避免?

死锁产生的四个必要条件

  • 互斥:一个资源每次只能被一个进程使用
  • 请求与保持:一个进程因请求资源而阻塞时,不释放获得的资源
  • 不剥夺:进程已获得的资源,在未使用之前,不能强行剥夺
  • 循环等待:进程之间循环等待着资源

避免死锁的方法

  • 互斥条件不能破坏,因为加锁就是为了保证互斥
  • 一次性申请所有的资源,避免线程占有资源而且在等待其他资源
  • 占有部分资源的线程进一步申请其他资源时,如果申请不到,主动释放它占有的资源
  • 按序申请资源

线程run和start的区别?

  • 当程序调用​​start()​​方法,将会创建一个新线程去执行​​run()​​方法中的代码。​​run()​​就像一个普通方法一样,直接调用​​run()​​的话,不会创建新线程。
  • 一个线程的 ​​start()​​ 方法只能调用一次,多次调用会抛出 java.lang.IllegalThreadStateException 异常。​​run()​​ 方法则没有限制。

线程都有哪些方法?

start

用于启动线程。

getPriority

获取线程优先级,默认是5,线程默认优先级为5,如果不手动指定,那么线程优先级具有继承性,比如线程A启动线程B,那么线程B的优先级和线程A的优先级相同

setPriority

设置线程优先级。CPU会尽量将执行资源让给优先级比较高的线程。

interrupt

告诉线程,你应该中断了,具体到底中断还是继续运行,由被通知的线程自己处理。

当对一个线程调用 interrupt() 时,有两种情况:

  1. 如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。
  2. 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true。不过,被设置中断标志的线程可以继续正常运行,不受影响。

interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行。

join

等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。

yield

暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。

sleep

使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,线程自动转为Runnable状态。

volatile底层原理

​volatile​​是轻量级的同步机制,​​volatile​​保证变量对所有线程的可见性,不保证原子性。

  1. 当对​​volatile​​变量进行写操作的时候,JVM会向处理器发送一条​​LOCK​​前缀的指令,将该变量所在缓存行的数据写回系统内存。
  2. 由于缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己的缓存是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行置为无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存中。

来看看缓存一致性协议是什么。

缓存一致性协议:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,就会从内存重新读取。

​volatile​​关键字的两个作用:

  1. 保证了不同线程对共享变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
  2. 禁止进行指令重排序

指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。Java编译器会在生成指令系列时在适当的位置会插入​​内存屏障​​指令来禁止处理器重排序。插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。对一个volatile字段进行写操作,Java内存模型将在写操作后插入一个写屏障指令,这个指令会把之前的写入值都刷新到内存。

synchronized的用法有哪些?

  1. 修饰普通方法:作用于当前对象实例,进入同步代码前要获得当前对象实例的锁
  2. 修饰静态方法:作用于当前类,进入同步代码前要获得当前类对象的锁,synchronized关键字加到static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁
  3. 修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁

synchronized的作用有哪些?

原子性:确保线程互斥的访问同步代码;

可见性:保证共享变量的修改能够及时可见;

有序性:有效解决重排序问题。

synchronized 底层实现原理?

synchronized 同步代码块的实现是通过 ​​monitorenter​​ 和 ​​monitorexit​​ 指令,其中 ​​monitorenter​​ 指令指向同步代码块的开始位置,​​monitorexit​​ 指令则指明同步代码块的结束位置。当执行 ​​monitorenter​​ 指令时,线程试图获取锁也就是获取 ​​monitor​​的持有权(monitor对象存在于每个Java对象的对象头中, synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因)。

其内部包含一个计数器,当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 ​​monitorexit​​ 指令后,将锁计数器设为0 ,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止

synchronized 修饰的方法并没有 ​​monitorenter​​ 指令和 ​​monitorexit​​ 指令,取得代之的确实是​​ACC_SYNCHRONIZED​​ 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ​​ACC_SYNCHRONIZED​​ 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

volatile和synchronized的区别是什么?

  1. ​volatile​​只能使用在变量上;而​​synchronized​​可以在类,变量,方法和代码块上。
  2. ​volatile​​至保证可见性;​​synchronized​​保证原子性与可见性。
  3. ​volatile​​禁用指令重排序;​​synchronized​​不会。
  4. ​volatile​​不会造成阻塞;​​synchronized​​会。

ReentrantLock和synchronized区别

  1. 使用synchronized关键字实现同步,线程执行完同步代码块会自动释放锁,而ReentrantLock需要手动释放锁。
  2. synchronized是非公平锁,ReentrantLock可以设置为公平锁。
  3. ReentrantLock上等待获取锁的线程是可中断的,线程可以放弃等待锁。而synchonized会无限期等待下去。
  4. ReentrantLock 可以设置超时获取锁。在指定的截止时间之前获取锁,如果截止时间到了还没有获取到锁,则返回。
  5. ReentrantLock 的 tryLock() 方法可以尝试非阻塞的获取锁,调用该方法后立刻返回,如果能够获取则返回true,否则返回false。

wait()和sleep()的异同点?

相同点

  1. 它们都可以使当前线程暂停运行,把机会交给其他线程
  2. 任何线程在调用wait()和sleep()之后,在等待期间被中断都会抛出​​InterruptedException​

不同点

  1. ​wait()​​是Object超类中的方法;而​​sleep()​​是线程Thread类中的方法
  2. 对锁的持有不同,​​wait()​​会释放锁,而​​sleep()​​并不释放锁
  3. 唤醒方法不完全相同,​​wait()​​依靠​​notify​​或者​​notifyAll ​​、中断、达到指定时间来唤醒;而​​sleep()​​到达指定时间被唤醒
  4. 调用​​wait()​​需要先获取对象的锁,而​​Thread.sleep()​​不用

Runnable和Callable有什么区别?

  • Callable接口方法是​​call()​​,Runnable的方法是​​run()​​;
  • Callable接口call方法有返回值,支持泛型,Runnable接口run方法无返回值。
  • Callable接口​​call()​​方法允许抛出异常;而Runnable接口​​run()​​方法不能继续上抛异常。

线程执行顺序怎么控制?

假设有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行?

可以使用join方法解决这个问题。比如在线程A中,调用线程B的join方法表示的意思就是**:A等待B线程执行完毕后(释放CPU执行权),在继续执行。**

代码如下:

public class ThreadTest 

public static void main(String[] args)

Thread spring = new Thread(new SeasonThreadTask("春天"));
Thread summer = new Thread(new SeasonThreadTask("夏天"));
Thread autumn = new Thread(new SeasonThreadTask("秋天"));

try

//春天线程先启动
spring.start();
//主线程等待线程spring执行完,再往下执行
spring.join();
//夏天线程再启动
summer.start();
//主线程等待线程summer执行完,再往下执行
summer.join();
//秋天线程最后启动
autumn.start();
//主线程等待线程autumn执行完,再往下执行
autumn.join();
catch (InterruptedException e)

e.printStackTrace();




class SeasonThreadTask implements Runnable

private String name;

public SeasonThreadTask(String name)
this.name = name;


@Override
public void run()
for (int i = 1; i <4; i++)
System.out.println(this.name + "来了: " + i + "次");
try
Thread.sleep(100);
catch (InterruptedException e)
e.printStackTrace();



运行结果:

春天来了: 1次
春天来了: 2次
春天来了: 3次
夏天来了: 1次
夏天来了: 2次
夏天来了: 3次
秋天来了: 1次
秋天来了: 2次
秋天来了: 3次

守护线程是什么?

守护线程是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。在 Java 中垃圾回收线程就是特殊的守护线程。

线程间通信方式

1、使用 Object 类的 wait()/notify()。Object 类提供了线程间通信的方法:​​wait()​​、​​notify()​​、​​notifyAll()​​,它们是多线程通信的基础。其中,​​wait/notify​​ 必须配合 ​​synchronized​​ 使用,wait 方法释放锁,notify 方法不释放锁。wait 是指在一个已经进入了同步锁的线程内,让自己暂时让出同步锁,以便其他正在等待此锁的线程可以得到同步锁并运行,只有其他线程调用了​​notify()​​,notify并不释放锁,只是告诉调用过​​wait()​​的线程可以去参与获得锁的竞争了,但不是马上得到锁,因为锁还在别人手里,别人还没释放,调用 ​​wait()​​ 的一个或多个线程就会解除 wait 状态,重新参与竞争对象锁,程序如果可以再次得到锁,就可以继续向下运行。

2、使用 volatile 关键字。基于volatile关键字实现线程间相互通信,其底层使用了共享内存。简单来说,就是多个线程同时监听一个变量,当这个变量发生变化的时候 ,线程能够感知并执行相应的业务。

3、使用JUC工具类 CountDownLatch。jdk1.5 之后在​​java.util.concurrent​​包下提供了很多并发编程相关的工具类,简化了并发编程开发,​​CountDownLatch​​ 基于 AQS 框架,相当于也是维护了一个线程间共享变量 state。

4、基于 LockSupport 实现线程间的阻塞和唤醒。​​LockSupport​​ 是一种非常灵活的实现线程间阻塞和唤醒的工具,使用它不用关注是等待线程先进行还是唤醒线程先运行,但是得知道线程的名字。

ThreadLocal

线程本地变量。当使用​​ThreadLocal​​维护变量时,​​ThreadLocal​​为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程。

ThreadLocal原理

每个线程都有一个​​ThreadLocalMap​​(​​ThreadLocal​​内部类),Map中元素的键为​​ThreadLocal​​,而值对应线程的变量副本。

三天吃透Java并发八股文!_Java面试_05

调用​​threadLocal.set()​​-->调用​​getMap(Thread)​​-->返回当前线程的​​ThreadLocalMap<ThreadLocal, value>​​-->​​map.set(this, value)​​,this是​​threadLocal​​本身。源码如下:

public void set(T value) 
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);


ThreadLocalMap getMap(Thread t)
return t.threadLocals;


void createMap(Thread t, T firstValue)
t.threadLocals = new ThreadLocalMap(this, firstValue);

调用​​get()​​-->调用​​getMap(Thread)​​-->返回当前线程的​​ThreadLocalMap<ThreadLocal, value>​​-->​​map.getEntry(this)​​,返回​​value​​。源码如下:

public T get() 
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;


return setInitialValue();

​threadLocals​​的类型​​ThreadLocalMap​​的键为​​ThreadLocal​​对象,因为每个线程中可有多个​​threadLocal​​变量,如​​longLocal​​和​​stringLocal​​。

public class ThreadLocalDemo 
ThreadLocal<Long> longLocal = new ThreadLocal<>();

public void set()
longLocal.set(Thread.currentThread().getId());

public Long get()
return longLocal.get();


public static void main(String[] args) throws InterruptedException
ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo();
threadLocalDemo.set();
System.out.println(threadLocalDemo.get());

Thread thread = new Thread(() ->
threadLocalDemo.set();
System.out.println(threadLocalDemo.get());

);

thread.start();
thread.join();

System.out.println(threadLocalDemo.get());

​ThreadLocal​​并不是用来解决共享资源的多线程访问问题,因为每个线程中的资源只是副本,不会共享。因此​​ThreadLocal​​适合作为线程上下文变量,简化线程内传参。

ThreadLocal内存泄漏的原因?

每个线程都有⼀个​​ThreadLocalMap​​的内部属性,map的key是​​ThreaLocal​​,定义为弱引用,value是强引用类型。垃圾回收的时候会⾃动回收key,而value的回收取决于Thread对象的生命周期。一般会通过线程池的方式复用线程节省资源,这也就导致了线程对象的生命周期比较长,这样便一直存在一条强引用链的关系:​​Thread​​ --> ​​ThreadLocalMap​​-->​​Entry​​-->​​Value​​,随着任务的执行,value就有可能越来越多且无法释放,最终导致内存泄漏。

解决⽅法:每次使⽤完​​ThreadLocal​​就调⽤它的​​remove()​​⽅法,手动将对应的键值对删除,从⽽避免内存泄漏。

ThreadLocal使用场景有哪些?

场景1

ThreadLocal 用作保存每个线程独享的对象,为每个线程都创建一个副本,这样每个线程都可以修改自己所拥有的副本, 而不会影响其他线程的副本,确保了线程安全。

这种场景通常用于保存线程不安全的工具类,典型的使用的类就是 SimpleDateFormat。

假如需求为500个线程都要用到 SimpleDateFormat,使用线程池来实现线程的复用,否则会消耗过多的内存等资源,如果我们每个任务都创建了一个 simpleDateFormat 对象,也就是说,500个任务对应500个 simpleDateFormat 对象。但是这么多对象的创建是有开销的,而且这么多对象同时存在在内存中也是一种内存的浪费。可以将simpleDateFormat 对象给提取了出来,变成静态变量,但是这样一来就会有线程不安全的问题。我们想要的效果是,既不浪费过多的内存,同时又想保证线程安全。此时,可以使用 ThreadLocal来达到这个目的,每个线程都拥有一个自己的 simpleDateFormat 对象。

场景2

ThreadLocal 用作每个线程内需要独立保存信息,以便供其他方法更方便地获取该信息的场景。每个线程获取到的信息可能都是不一样的,前面执行的方法保存了信息后,后续方法可以通过 ThreadLocal 直接获取到,避免了传参,类似于全局变量的概念。

比如Java web应用中,每个线程有自己单独的​​Session​​实例,就可以使用​​ThreadLocal​​来实现。

AQS原理

AQS,​​AbstractQueuedSynchronizer​​,抽象队列同步器,定义了一套多线程访问共享资源的同步器框架,许多并发工具的实现都依赖于它,如常用的​三天吃透MySQL面试八股文

三天吃透MySQL面试八股文

三天吃透计算机网络面试八股文

三天吃透MySQL八股文(2023最新整理)

一天吃透TCP面试八股文

java面试八股文之------Java并发夺命23问