校招笔记(一)_Java_多线程
我的校招记录:校招笔记(零)_写在前面 ,以下是校招笔记总目录。
备注 | ||
---|---|---|
算法能力(“刷题”) | 这部分就是耗时间多练习,Leetcode-Top100 是很好的选择。 | 补充练习:codeTop |
计算机基础(上)(“八股”) | 校招笔记(一)__Java_Java入门 | C++后端后续更新 |
校招笔记(一)__Java_面对对象 | ||
校招笔记(一)__Java_集合 | ||
校招笔记(一)__Java_多线程 | ||
校招笔记(一)__Java_锁 | ||
校招笔记(一)__Java_JVM | ||
计算机基础(下)(“八股”) | 校招笔记(二)__计算机基础_Linux&Git | |
校招笔记(三)__计算机基础_计算机网络 | ||
校招笔记(四)__计算机基础_操作系统 | ||
校招笔记(五)__计算机基础_MySQL | ||
校招笔记(六)__计算机基础_Redis | ||
校招笔记(七)__计算机基础_数据结构 | ||
校招笔记(八)__计算机基础_场景&智力题 | ||
校招笔记(九)__计算机基础_相关补充 | ||
项目&实习 | 主要是怎么准备项目,后续更新 |
1.4 多线程
0.三个线程,如何让他们按顺序打印A、B、C?如果是循环打印10次呢?依次打印1~100呢?交替打印奇偶10次呢?
0.1 synchronized + 条件变量
-
synchronized + 条件变量(循环打印1次)
思路如下:
- 新建三个线程1、2、3 ,分别负责打印A、B、C;同时,每个线程对应一个条件变量:
num%3==当前线程条件变量?
; - 把打印的代码用synchronized 加锁,每个线程拿到锁后,while循环判断是否满足当前线程的条件变量 ;
- 如果满足 ,便执行后续逻辑打印当前线程,之后notifyall()唤醒其它线程;
- 如果不满足,执行wait等待释放锁;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43import java.util.*;
class Wait_Notify_ACB
{
private int num;
private static final Object LOCK = new Object();
private void printABC(int targetNum)
{ // targetNum : 每个线程应该满足的对应条件变量
synchronized (LOCK)
{
while (num % 3 != targetNum)
{ // while :线程阻塞在wait(),再次被唤醒应该先检查下是否满足条件,因为这个时候未必轮到当前线程执行;用if会导致直接往下执行。
try
{
LOCK.wait();
} catch (InterruptedException e)
{
e.printStackTrace();
}
}
num++;
System.out.print(Thread.currentThread().getName());
LOCK.notifyAll();
}
}
public static void main(String[] args)
{
Wait_Notify_ACB wait_notify_acb = new Wait_Notify_ACB ();
new Thread(() ->
{
wait_notify_acb.printABC(0);
}, "A").start();
new Thread(() ->
{
wait_notify_acb.printABC(1);
}, "B").start();
new Thread(() ->
{
wait_notify_acb.printABC(2);
}, "C").start();
}
} - 新建三个线程1、2、3 ,分别负责打印A、B、C;同时,每个线程对应一个条件变量:
-
synchronized + 条件变量(循环打印10次)
打印10次,只需让三个线程,尝试去竞争10次锁即可。所以在synchronized前加上for循环即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46import java.util.*;
class Wait_Notify_ACB
{
private int num;
private static final Object LOCK = new Object();
private void printABC(int targetNum)
{ // targetNum : 每个线程应该满足的对应条件变量
for(int i = 0 ; i<10 ;i++)
{
synchronized (LOCK)
{
while (num % 3 != targetNum)
{ // while :线程阻塞在wait(),再次被唤醒应该先检查下是否满足条件,因为这个时候未必轮到当前线程执行;用if会导致直接往下执行。
try
{
LOCK.wait();
} catch (InterruptedException e)
{
e.printStackTrace();
}
}
num++;
System.out.print(Thread.currentThread().getName());
LOCK.notifyAll();
}
}
}
public static void main(String[] args)
{
Wait_Notify_ACB wait_notify_acb = new Wait_Notify_ACB ();
new Thread(() ->
{
wait_notify_acb.printABC(0);
}, "A").start();
new Thread(() ->
{
wait_notify_acb.printABC(1);
}, "B").start();
new Thread(() ->
{
wait_notify_acb.printABC(2);
}, "C").start();
}
} -
synchronized + 条件变量(打印1~100)
设置一个全局变量,synchronized 中加锁函数对num+1,保证只要每次只有一个线程能执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42class Wait_Notify_100 {
private int num;
private static final Object LOCK = new Object();
private int maxnum = 10;
private void printABC(int targetNum) {
while (true) {
synchronized (LOCK) {
while (num % 3 != targetNum) { //想想这里为什么不能用if代替,想不起来可以看公众号上一篇文章
if(num >= maxnum){
break;
}
try {
LOCK.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if(num >= maxnum){
break;
}
num++;
System.out.println(Thread.currentThread().getName() + ": " + num);
LOCK.notifyAll();
}
}
}
public static void main(String[] args) {
Wait_Notify_100 wait_notify_100 = new Wait_Notify_100 ();
new Thread(() -> {
wait_notify_100.printABC(0);
}, "thread1").start();
new Thread(() -> {
wait_notify_100.printABC(1);
}, "thread2").start();
new Thread(() -> {
wait_notify_100.printABC(2);
}, "thread3").start();
}
} -
synchronized + 条件变量(奇偶打印10次)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31class Wait_Notify_Odd_Even{
private Object monitor = new Object();
private volatile int count;
Wait_Notify_Odd_Even(int initCount) {
this.count = initCount;
}
private void printOddEven() {
synchronized (monitor) {
while (count < 10) {
try // 这里不用while循环判断,是因为下次notifyAll的一定是奇数或偶数
{
System.out.print( Thread.currentThread().getName() + ":");
System.out.println(++count);
monitor.notifyAll();
monitor.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//防止count=10后,while()循环不再执行,有子线程被阻塞未被唤醒,导致主线程不能退出
monitor.notifyAll();
}
}
public static void main(String[] args) throws InterruptedException {
Wait_Notify_Odd_Even waitNotifyOddEven = new Wait_Notify_Odd_Even(0);
new Thread(waitNotifyOddEven::printOddEven, "odd").start();
Thread.sleep(10); //为了保证线程odd先拿到锁
new Thread(waitNotifyOddEven::printOddEven, "even").start();
}
}
0.2 join
join()
方法**:在A线程中调用了B线程的join()方法时,表示只有当B线程执行完毕时,A线程才能继续执行**。
基于这个原理,我们使得三个线程按顺序执行,然后循环多次即可。无论线程1、线程2、线程3哪个先执行,最后执行的顺序都是线程1——>线程2——>线程3。
代码如下:
1 | class Join_ABC { |
0.3 Lock
该方法很容易理解,不管哪个线程拿到锁,只有符合条件的才能打印。
1 | class Lock_ABC { |
0.4 Semaphore
Semaphore:用来控制同时访问某个特定资源的操作数量,或者同时执行某个制定操作的数量。Semaphore内部维护了一个计数器,其值为可以访问的共享资源的个数。
- 一个线程要访问共享资源,先使用
acquire()
方法获得信号量,如果信号量的计数器值大于等于1,意味着有共享资源可以访问,则使其计数器值减去1,再访问共享资源。如果计数器值为0,线程进入休眠; - 当某个线程使用完共享资源后,使用
release()
释放信号量,并将信号量内部的计数器加1,之前进入休眠的线程将被唤醒并再次试图获得信号量。
1 | class SemaphoreABC { |
1.4.1 线程基本
1. 什么是线程和进程?如何保证线程安全?
1.1 什么是线程和进程?
-
什么是进程?
进程是程序的⼀次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行⼀个程序即是⼀个进程从创建,运行到消亡的过程。
如下图所示,在 windows 中通过查看任务管理器的方式,我们就可以清楚看到 window 当前运行的进程(.exe ⽂件的运行)。在Windows操作系统中,一个程序只对应一个进程,里面可以有一个或多个线程。
-
什么是线程?
线程与进程相似,但线程是⼀个⽐进程更小的执行单位。⼀个进程在其执行的过程中可以产⽣多个线程。
- 与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有⾃⼰的程序计数器、虚拟机栈和本地方法栈
所以系统在产⽣⼀个线程,或是在各个线程之间作切换⼯作时,负担要⽐进程小得多,也正因为如此,线程也被称为轻量级进程。
⼀个 Java 程序的运行是 main 线程和多个其他线程同时运行。
1.2 如何保证线程安全?
- 原子性,简单说就是相关操作不会中途被其他线程干扰,一般通过同步机制实现;
- 可见性,是一个线程修改了某个共享变量,其状态能够立即被其他线程知晓,通常被解释为将线程本地状态反映到主内存上,volatile 就是负责保证可见性的;
- 有序性,是保证线程内串行语义,避免指令重排等。
1.3 为什么程序计数器、虚拟机栈和本地方法栈是线程私有的?
-
程序计数器为什么是私有
在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪⼉了。
所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。
-
虚拟机栈和本地方法栈为什么私有
-
虚拟机栈: 每个 Java 方法在执行的同时会创建⼀个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直⾄执行完成的过程,就对应着⼀个栈帧在 Java 虚拟机栈中⼊栈和出栈的过程。
-
本地方法法栈: 和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合⼆为⼀。
所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。
-
2.创建线程的方式(非线程池)? 哪种最好?
一般有三种方式来进行创建:
-
继承Thread类:(1)定义Thread类的子类,并重写该类的run方法(2)创建Thread子类的实例对象 (3)调用对象start()方法
- 优点:编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用this即可获取当前线程;
- 缺点:因为线程类已经继承了Thread类,Java语言是单继承的,所以就不能再继承其他父类了。
-
实现Runnable接口:(1)定义runnable接口的实现类,并重写该接口的run()方法 (2)创建实现类的实例对象(3)调用线程对象的start()方法来启动该线程
-
通过Callable和Future创建线程:(1)创建Callable接口的实现类,并实现call()方法(2)创建Callable实现类的实例,并使用FutureTask类来包装Callable对象(3)使用FutureTask对象作为Thread对象的target创建并启动新线程(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
其中 实现Runnalbe接口更好 ,使用实现Runnable接口的方式创建的线程可以处理同一资源,从而实现资源的共享。
3.如何停止一个正在运行的线程?
-
使用退出标志,使线程正常退出,也就是当run方法完成后线程终止;
-
使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期作废的方法;
-
使用interrupt方法中断线程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31class MyThread extends Thread
{
volatile boolean stop = false;
public void run() {
while (!stop) {
System.out.println(getName() + " is running");
try {
sleep(1000);
} catch (InterruptedException e) {
System.out.println("week up from blcok...");
stop = true; // 在异常处理代码中修改共享变量的状态
}
}
System.out.println(getName() + " is exiting...");
}
}
class InterruptThreadDemo3
{
public static void main(String[] args) throws InterruptedException
{
MyThread m1 = new MyThread();
System.out.println("Starting thread...");
m1.start();
Thread.sleep(3000);
System.out.println("Interrupt thread...: " + m1.getName());
m1.stop = true; // 设置共享变量为true
m1.interrupt(); // 阻塞时退出阻塞状态
Thread.sleep(3000); // 主线程休眠3秒以便观察线程m1的中断情况
System.out.println("Stopping application...");
}
}
4.什么是Daemon线程?它有什么意义?
所谓后台(daemon)线程,也叫守护线程,是指在程序运行的时候在后台提供一种通用服务的线程,并且这个线程并不属于程序中不可或缺的部分。
- 意义:JVM的垃圾回收线程就是Daemon线程,Finalizer也是守护线程。
5.说说CyclicBarrier和CountDownLatch的区别?
两个看上去有点像的类,都在java.util.concurrent
下,都可以用来表示代码运行到某个点上,二者的区别在于:
- CyclicBarrier的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行CountDownLatch则不是,某线程运行到某个点上之后,只是给某个数值**-1**而已,该线程继续运行;
- CyclicBarrier只能唤起一个任务,CountDownLatch可以唤起多个任务
- CyclicBarrier可重用,CountDownLatch不可重用,计数值为0该CountDownLatch就不可再用了
6.请你简要说明一下线程的基本状态以及状态之间的关系?
- 新建状态:new语句创建的线程对象处于新建状态,仅被分配了内存;
- 等待状态: 当线程在new之后,并且在调用start方法前,线程处于等待状态 ;
- 就绪状态: 其他线程调用它的**start()**方法,该线程就进入就绪状态,只差等待cpu的使用权 ;
- 运行状态: 线程占用CPU,执行程序代码 ;
- 阻塞状态: 阻塞状态是指线程因为某些原因放弃CPU,暂时停止运行。阻塞状态分为三种:
- 等待阻塞: 运行的线程执行**wait()**方法,JVM会把该线程放入等待池中;
- 同步阻塞: 运行的线程在获取对象同步锁时,若该同步锁被别的线程占用,则JVM会把线程放入锁池中;
- 其他阻塞: 运行的线程执行Sleep()方法,或者发出I/O请求时,JVM会把线程设为阻塞状态。
- 死亡状态: 线程执行完run()方法中的代码,或者遇到了未捕获的异常,就会退出run()方法,结束生命周期
7. notify()和notifyAll()有什么区别?
- notify可能会导致死锁(why?),而notifyAll则不会;
- 使用notifyall,可以唤醒 所有处于wait状态的线程,使其重新进入锁的争夺队列中,而notify只能唤醒一个。
8. sleep()和wait() 有什么区别?
-
继承的类:sleep()方法,我们首先要知道该方法是属于Thread类中的;而wait()方法,则是属于Object类中的;
sleep() 是 Thread 类静态方法,可以使 当前 线程阻塞,并指定暂停时间,重点在于理解 当前。休眠时间期满后,该线程不一定会立即执行被CPU调度,只是到了就绪状态。
-
是否释放锁:sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。在调用sleep()方法的过程中,线程不会释放对象锁;当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池;
-
用处: Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行;
-
再次唤醒: wait() 方法被调用后,线程不会⾃动苏醒,需要别的线程调用同⼀个对象上的 notify() 或者notifyAll() 方法。sleep() 方法执行完成后,线程会⾃动苏醒。
8.1 Thread类中的yield方法有什么作用?
yield() 也是 Thread 类静态方法,使当前线程变为就绪状态 (sleep() 是阻塞),使得其他线程更多机会获取CPU。执行yield()的线程有可能在进入到暂停状态后马上又被执行。
8.2 java 中Wait、Sleep和Yield方法的区别?
https://www.jianshu.com/p/25e959037eed
9. volatile 是什么?可以保证有序性吗?
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存;
- 禁止进行指令重排序。
能保证有序性,因为禁止指令重排。
10. 为什么wait, notify 和 notifyAll这些方法不在thread类里面?
https://www.zhihu.com/question/321674476
明显的原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。
- 简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。
11.为什么wait和notify方法要在同步块中调用?
- wait()方法强制当前线程释放对象锁。这意味着在调用某对象的wait()方法之前,当前线程必须已经获得该对象的锁。因此,线程必须在某个对象的同步方法或同步代码块中才能调用该对象的wait()方法;
- 在调用对象的notify()和notifyAll()方法之前,调用线程必须已经得到该对象的锁。因此,必须在某个对象的同步方法或同步代码块中才能调用该对象的notify()或notifyAll()方法。
12. (没细看)Java中interrupted 和 isInterruptedd方法的区别?
interrupted() 和 isInterrupted()的主要区别是前者会将中断状态清除而后者不会。Java多线程的中断机制是用内部标识来实现的,调Thread.interrupt()来中断一个线程就会设置中断标识为true。当中断线程调用静态方法Thread.interrupted()来检查中断状态时,中断状态会被清零。而非静态方法isInterrupted()用来查询其它线程的中断状态且不会改变中断状态标识。简单的说就是任何抛出InterruptedException异常的方法。
1.4.2 线程池
1.请你解释一下什么是线程池(thread pool)?
线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。 除此之外:
- 提高性能,利用多线程压榨CPU算力;
- 提高线程的可管理性,限制线程数量并可以进行统一的分配、调优和监控。
1.1 请介绍一下使用线程池任务执行过程?
核心参数:其中corePool
是核心线程池(保活线程),maximumPool
是总线程池(保活线程+工作线程),blockQueue
是等待任务队列,rejectedExecutionHandler
是拒绝策略:
流程:corePoolSize→队列→maximumPool→拒绝策略
- 未达到corePoolSize时,核心线程池会开辟新线程运行任务(可以使用已有线程吗?),任务结束后线程不销毁;
- 达到corePoolSize,而任务队列未满时,新任务提交到等待队列,线程空闲时间超过keepAliveTime时被销毁;
- 任务队列也满了,但未超过最大线程数,新建工作线程执行任务;
- 超过最大线程数时,按拒绝策略处理,包括:抛出异常、使用调用者线程运行任务、丢弃新任务、丢弃队列头任务等。
1.2 线程池会回收核心线程吗?
会,可以通过allowCoreThreadTimeOut参数来进行设置。
-
先回忆一下整体线程池流程
-
excute方法
我们的任务放到线程池后,是从调用execute执行开始的。
-
核心是addWorker办法, 里面最最重要的就是初始化Worker同时启动thread。
-
Worker实现了Runnable接口,我们直接看它的run方法,看截图的③处标记,抽离出来一个runWorker方法:
也就是说:①处是一个while循环,getTask方法就是从线程池队列取任务,如果取不到任务就会执行
②一旦跳出while循环,即进入到processWorkExit方法,这就是回收Worker
-
-
getTask方法
可以看到,①判断是否回收线程的条件,timed ,有两种情况要回收线程:
- wc>corePoolSize ,当前线程数大于核心线程数
- allowCoreThreadTimeOut,核心线程超时,所以核心线程是会被回收的
然后②处就是从任务队列取任务了,带了timeOut参数的poll方法超时 ,未能从任务队列获取任务即返回null,从而实现最终的线程回收。
不是processWorkExit处理吗,怎么在getTask处理,还是得好好看看。
2.请介绍一下什么是生产者消费者模式?
生产者消费者问题是线程模型中的经典问题:
- 生产者和消费者在同一时间段内共用同一存储空间,生产者向空间里生产数据,而消费者取走数据。
3.线程池的拒绝策略有哪些?
主要有4种拒绝策略:
- AbortPolicy:直接丢弃任务,抛出异常,这是默认策略
- CallerRunsPolicy:只用调用者所在的线程来处理任务
- DiscardOldestPolicy:丢弃等待队列中最旧的任务,并执行当前任务
- DiscardPolicy:直接丢弃任务,也不抛出异常
4.如何创建一个线程池(四大方法)?相关参数(七大参数)是什么?
java通过Executors提供四大方法:
- newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程;
- newFixedThreadPool:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待;
- newScheduledThreadPool:创建一个定长线程池,支持定时及周期性任务执行;
- newSingleThreadExecutor: 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
七大参数设置:
-
corePoolSize:当使用了
LinkedBlockingQueue = new LinkedBlockQueue
的时候,队列长度默认无限长,会导致线程数量永远等于corePoolSize
,任务激增时任务响应时间也激增; -
maximumPoolSize:线程池中线程个数,增加线程的公式:;
-
keepAliveTime:线程最大(空闲)存活时间;
-
rejectedExecutionHandler:线程被拒绝的解决方案,可以自己重写;
-
workQueue : 阻塞队列;
-
unit:keepAliveTime的单位 ;
-
threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程一般默认即可
5.线程池核心线程数corePoolSize怎么设置呢?
分为CPU密集型和IO密集型来考虑:
-
CPU密集型。这种任务消耗的主要是 CPU 资源,可以将线程数设置为
N(CPU 核心数)+1
。比 CPU 核心数多出来的一个线程 :是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
-
IO密集型。这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 :
核心线程数=CPU核心数量*2
6.(不熟悉)Java线程池中队列常用类型有哪些?
ArrayBlockingQueue
:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序;LinkedBlockingQueue
:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于 ArrayBlockingQueue ;SynchronousQueue
: 一个不存储元素的阻塞队列;PriorityBlockingQueue
: 一个具有优先级的无限阻塞队列,PriorityBlockingQueue 也是基于最小二叉堆实现。
7.有三个线程T1,T2,T3,如何保证顺序执行?
因为在每个线程的run方法中用join()方法限定了三个线程的执行顺序。
1 | public class JoinTest2 |
1.4.3 多线程
1.请简述一下实现多线程同步的方法? 为什么需要使用多线程?使用多线程会带来什么问题?
-
同步方法:可以使用synchronized、lock、volatile和ThreadLocal来实现同步。
-
为什么需要使用多线程?
- 减少上下文切换开销(共享进程的堆和方法区 ,注意,减少≠没有)
- 利用好多线程机制可以大大提高系统整体的并发能⼒以及性能
- 多核时代 :多核时代多线程主要是为了提高 CPU 利用率。举个例子:假如我们要计算⼀个复杂的任务,我们只用⼀个线程的话,CPU 只会⼀个 CPU 核⼼被利用到,而创建多个线程就可以让多个 CPU 核⼼被利用到,这样就提高了 CPU 的利用率。
-
带来的问题?
- 内存泄漏、上下⽂切换、死锁还有受限于硬件和软件的资源闲置问题。
2. 什么是线程安全?如何保证线程安全?
好文:一文搞懂CAS
-
什么是线程安全?
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
-
如何保证线程安全?
- 使用 synchronized 关键字 。开销比较大
- 使用Lock 锁 。高并发场景下,使用 Lock 锁要比使用synchronized 关键字,在性能上得到极大的提高。因为 Lock 底层是通过 AQS + CAS 机制来实现的。
- 使用 Atomic 原子类 。使用 Lock 方式,一旦 unlock() 方法使用不规范,可能导致死锁。 Atomic 原子类,因为其底层基于 CAS 乐观锁来实现的,性能较高。
- 使用Volatile?不可行! 因为无法保证原子性。
3.线程安全需要保证几个基本特征?
- 原子性,简单说就是相关操作不会中途被其他线程干扰,一般通过同步机制实现。
- 可见性,是一个线程修改了某个共享变量,其状态能够立即被其他线程知晓,通常被解释为将
线程本地状态反映到主内存上,volatile 就是负责保证可见性的。 - 有序性,是保证线程内串行语义,避免指令重排等。
4. 【待补充】如何在线程安全的情况下实现一个计数器?
可以使用加锁,比如synchronized或者lock。也可以使用Concurrent包下的原子类。
5.多线程中的i++线程安全吗?请简述一下原因?
不安全,因为i++不是原子性操作。i++分为读取i值,对i值加一,再赋值给i++,执行期中任何一步都是有可能被其他线程抢占的。
6.介绍一下ThreadLocal原理?
当多线程访问共享可变数据时,涉及到线程间同步的问题,并不是所有时候,都要用到共享数据,所以就需要线程封闭出场了。数据都被封闭在各自的线程之中,就不需要同步,这种通过将数据封闭在线程中而避免使用同步的技术称为线程封闭。
ThreadLocal 是 Java 里一种特殊变量,它是一个线程级别变量,每个线程都有一个 ThreadLocal 就是每个线程都拥有了自己独立的一个变量,竞态条件被彻底消除了,在并发模式下是绝对安全的变量。
-
使用示例
在下面例子中,TreadLocal 做到多个线程对同一对象 set 操作,但是 get 获取的值还都是每个线程 set 的值,体现了线程的封闭。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45public class ThreadLocalDemo {
/**
* ThreadLocal变量,每个线程都有一个副本,互不干扰
*/
public static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();
public static void main(String[] args) throws Exception {
new ThreadLocalDemo().threadLocalTest();
}
public void threadLocalTest() throws Exception {
// 主线程设置值
THREAD_LOCAL.set("wupx");
String v = THREAD_LOCAL.get();
System.out.println("Thread-0线程执行之前," + Thread.currentThread().getName() + "线程取到的值:" + v);
new Thread(new Runnable() {
public void run() {
String v = THREAD_LOCAL.get();
// 此时新建线程Thread-0取到的是null,意味着不同线程取到的值不同的
System.out.println(Thread.currentThread().getName() + "线程取到的值:" + v);
// 设置 threadLocal
THREAD_LOCAL.set("huxy");
v = THREAD_LOCAL.get();
System.out.println("重新设置之后," + Thread.currentThread().getName() + "线程取到的值为:" + v);
System.out.println(Thread.currentThread().getName() + "线程执行结束");
}
}).start();
// 等待所有线程执行结束
Thread.sleep(3000L);
v = THREAD_LOCAL.get();
System.out.println("Thread-0线程执行之后," + Thread.currentThread().getName() + "线程取到的值:" + v);
}
}
/* 输出结果 */
// 虽然Thread-0已经改变了值,但是main取到的值依旧是一样的。
/*
Thread-0线程执行之前,main线程取到的值:wupx
Thread-0线程取到的值:null
重新设置之后Thread-0线程取到的值为:huxy
Thread-0线程执行结束
Thread-0线程执行之后,main线程取到的值:wupx
*/ -
ThreadLocal原理
ThreadLocal有一个静态内部类ThreadLocalMap,ThreadLocalMap又包含了一个Entry数组,Entry本身是一个弱引用,他的key是指向ThreadLocal的弱引用,Entry具备了保存key value键值对的能力。
-
ThreadLocalMap
ThreadLocalMap 是 ThreadLocal 的静态内部类,当一个线程有多个 ThreadLocal 对象时时,需要一个容器来管理多 ThreadLocal,ThreadLocalMap 的作用就是管理线程中多个 ThreadLocal。
从源码中看到 ThreadLocalMap 其实就是一个简单的 Map 结构,底层是数组,有初始化大小,也有扩容阈值大小,数组的元素是 Entry,Entry 的 key 就是 ThreadLocal 的引用,value 是 ThreadLocal 的值。ThreadLocalMap 解决 hash 冲突的方式采用的是线性探测法,如果发生冲突会继续寻找下一个空的位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32static class ThreadLocalMap {
/**
* 键值对实体的存储结构
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
// 当前线程关联的 value,这个 value 并没有用弱引用追踪
Object value;
/**
* 构造键值对
*
* @param k k 作 key,作为 key 的 ThreadLocal 会被包装为一个弱引用
* @param v v 作 value
*/
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// 初始容量,必须为 2 的幂
private static final int INITIAL_CAPACITY = 16;
// 存储 ThreadLocal 的键值对实体数组,长度必须为 2 的幂
private Entry[] table;
// ThreadLocalMap 元素数量
private int size = 0;
// 扩容的阈值,默认是数组大小的三分之二
private int threshold;
} -
Set方法
调用 ThreadLocal对象.set(value),实际在调用当前线程的ThreadLocalMap对象.set(this,value) ,会把当前
threadLocal
对象作为 key,想要保存的对象作为 value,存入 map。set 方法的流程主要是:
- 先获取到当前线程的引用
- 利用这个引用来获返回当前线程到 ThreadLocalMap
- 如果 map 为空,则去创建一个 ThreadLocalMap
- 如果 map 不为空,就利用 ThreadLocalMap 的
set (this当前对象,value)
添加键值对
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25/**
* 为当前 ThreadLocal 对象关联 value 值
*
* @param value 要存储在此线程的线程副本的值
*/
public void set(T value) {
// 返回当前ThreadLocal所在的线程
Thread t = Thread.currentThread();
// 返回当前线程持有的map
ThreadLocalMap map = getMap(t);
if (map != null) {
// 如果 ThreadLocalMap 不为空,则直接存储<ThreadLocal, T>键值对
// 此时this是ThreadLocal对象,这是在ThreadLocal类中
map.set(this, value);
} else {
// 否则,需要为当前线程初始化 ThreadLocalMap,并存储键值对 <this, firstValue>
createMap(t, value);
}
}
/**
* 返回当前线程 thread 持有的 ThreadLocalMap对象
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
} -
Get方法
调用ThreadLocal对象.get() 方式时,实际在调用ThreadLocalMap对象.getEntry(this) 方法。this是当前ThreadLocal对象。
get 方法的主要流程为:
- 先获取到当前线程的引用
- 获取当前线程内部的 ThreadLocalMap对象
- 如果 map 存在,则获取当前 ThreadLocal 对应的 value 值
- 如果 map 不存在或者找不到 value 值,则调用 setInitialValue() 进行初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23/**
* 返回当前 ThreadLocal 对象关联的值
*
* @return
*/
public T get() {
// 返回当前 ThreadLocal 所在的线程
Thread t = Thread.currentThread();
// 从线程中拿到 ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// 从 map 中拿到 entry
ThreadLocalMap.Entry e = map.getEntry(this);
// 如果不为空,读取当前 ThreadLocal 中保存的值
if (e != null) {
T result = (T) e.value;
return result;
}
}
// 若 map 为空,则对当前线程的 ThreadLocal 进行初始化,最后返回当前的 ThreadLocal 对象关联的初值,即 value
return setInitialValue();
} -
resize 方法
当前线程的 ThreadLocalMap 中的 ThreadLocal 的个数超过容量阈值时,ThreadLocalMap 就要开始扩容了。
1
// 略
-
7.为什么ThreadLocal
造成内存泄漏?
ThreadLocalMap 是 ThreadLocal 的静态内部类,当一个线程有多 个ThreadLocal 时,需要一个容器来管理多个 ThreadLocal,ThreadLocalMap 的作用就是管理线程中多个 ThreadLocal。
源码中看到 ThreadLocalMap 其实就是一个简单的 Map 结构:
底层是数组
Entry[] table
,数组的元素是 Entry类:Entry 的两个属性, key 是 ThreadLocal类型的引用,value 是 ThreadLocal 的值。
- 原因:
ThreadLocalMap
的key为弱引用(有用但非必需,下一次GC会被回收),value为强引用(GC过程不会被回收),有可能造成key被GC,value没被GC,ThreadLocalMap
中出现null
为key的Entry
,产生内存泄漏(软引用:有用但非必需,内存溢出之前被回收); - 解决: 调用
set()
、get()
和remove()
方法时,会自动清理掉key为null
的记录,但使用ThreadLocal
方法后手动remove()
。
8.什么是多线程中的上下文切换?
一个线程让出处理器使用权,就是“切出”;另外一个线程获取处理器使用权,就是“切入”。在这个切入切出的过程中,操作系统会保存和恢复相关的进度信息,这个进度信息就是我们常说的“上下文”,上下文中一般包含了寄存器的存储内容以及程序计数器存储的指令内容。
9.请问什么是死锁(deadlock)?
死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程(线程)。
10. JAVA中如何确保N个线程可以访问N个资源,但同时又不导致死锁?
最简单方法:指定获取锁的顺序,并强制线程按照指定的顺序获取锁。
因此,如果所有的线程都是以同样的顺序加锁和释放锁,就不会出现死锁了。或者通过破坏死锁的四个条件:
- 互斥 : 不可能破坏。
- 破坏请求和保持条件:进程必须 等所有要请求的资源都空闲时才能申请资源, 这种方法会使资源浪费严重 。允许进程获取初期所需资源后,便开始运行,运行过程中再逐步释放自己占有的资源。
- 破坏不可抢占条件: 方法代价大,实现复杂。
- 破坏循坏等待条件 :对各进程请求资源的顺序做一个规定,避免相互等待。这种方法对资源的利用率比前两种都高,但是前期要为设备指定序号,新设备加入会有一个问题,其次对用户编程也有限制。