聊聊线程

聊聊线程

1.volatile和synchronized对比?

1).volatile仅能使用在变量级别;
synchronized则可以使用在变量、方法、和类级别的
2).volatile仅能实现变量的修改可见性,并不能保证原子性;

​ synchronized则可以保证变量的修改可见性和原子性
3).volatile不会造成线程的阻塞;
​ synchronized可能会造成线程的阻塞。
4).volatile标记的变量不会被编译器优化;
​ synchronized标记的变量可以被编译器优化

2.java编写一个导致死锁的程序?

参考:死锁案例

3.ConcurrentHashMap的并发度是什么?

16

4.sleep和wait方法的对比?

1)sleep是Thread线程类的方法,而wait是Object顶级类的方法

2)sleep可以在任何地方使用,而wait只能在同步方法或者同步块中使用

3)sleep,wait调用后都会暂停当前线程并让出cpu的执行时间,但不同的是sleep不会释放当前持有的对象的锁资源,到时间后会继续执行,而wait会放弃所有锁并需要notify/notifyAll后重新获取到对象锁资源后才能继续执行

4)sleep需要捕获或者抛出异常,而wait/notify/notifyAll不需要

5.什么是线程池, 为什么要用线程池,java中线程池的实现原理?

线程池:java.util.concurrent.Executors提供了一个 java.util.concurrent.Executor接口的实现用于创建线程池

作用:1.减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。

​ 2.可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

线程池常用方法

线程池构造函数

1
2
3
4
5
6
7
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)

里面有个maximumPoolSize(最大线程数),会发现线程会出现达到corePoolSize(核心线程数)后,就不再创建工作线程了,并没有到达maximumPoolSize,这是为什么?这就要先了解下任务execute过程。

再来回答一下上面的问题,不难理解是因为这个workQueue(工作队列)是无界的,只要工作队列没满,达到核心线程数量的线程都是被丢到工作队列中,这时候的最大线程数量就是形同虚设。

线程池终止

1:调用shutdown后,不再接收新的任务,会被拒绝执行。已经执行的任务会等待执行完毕。

2:调用shutdownNow后,不再接收新的任务,会被拒绝执行。正在执行的任务抛出InterruptedException: sleep interrupted,处于等待队列的任务也不再执行,shutdownNow.size()会返回处于等待队列的未结束的任务大小。

如何定义合适的线程数量

计算型任务(加减乘除运算,计算hash值等纯内存操作,占用CPU较高的操作):CPU数量的1-2倍,如8核,线程池数量可为16。

IO型任务:相对于计算型任务,需要多一些线程,要根据具体的IO阻塞时长进行考量决定。如tomcat中默认的最大线程数为:200。也可考虑根据需要在一个最小数量和最大数量间自动增减线程数,利用newCacheThreadPool。

具体的线程数可观察CPU的利用率,在达到80%,为最佳。

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

7.线程和进程的区别?

8.讲讲CyclicBarrier 和 CountDownLatch?

9.讲讲ThreadLocal?

数据都被封闭在各自的线程之中,就不需要同步,这种通过将数据封闭在线程中而避免使用同步的技术就称为线程封闭。

线程封闭具体的体现有:ThreadLocal、局部变量

ThreadLocal是一个线程级别变量,每个线程都有一个ThreadLocal就是每个线程都拥有了自己独立的一个变量,也就是说每个线程访问这个变量拿到的值是不一样的,竞争条件被彻底消除了,在并发模式下是绝对安全的变量。

用法:ThreadLocal var = new ThreadLocal();

会自动在每一个线程上创建一个T的副本,副本之间彼此独立,互不影响。

可以用ThreadLocal存储一些参数,以便在线程中的多个方法中使用,用来代替方法传参的做法。

10.讲讲FutureTask?

11.如何写代码来解决生产者消费者问题?

买包子

1:调用suspend挂起目标线程,通过resume可以恢复线程执行—————易造成死锁,已弃用

2:wait方法导致当前线程等待,加入该对象的等待集合中,并且放弃当前持有的对象锁

​ notify/notifyAll方法唤醒一个或所有正在等待这个对象锁的过程

注意:虽然wait会自动解锁,但是对顺序有要求,如果在notify被调用之后,才开始wait方法的调用,线程会用于处于WAITING状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void waitNotifyTest() throws Exception {
// 启动线程
new Thread(() -> {
synchronized (this) {
while (baozidian == null) { // 如果没包子,则进入等待状态
try {
System.out.println("1、进入等待");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println("2、买到包子,回家");
}).start();
// 3秒之后,生产一个包子
Thread.sleep(3000L);
baozidian = new Object();
synchronized (this) {
this.notifyAll();
System.out.println("3¡¢Í¨ÖªÏû·ÑÕß");
}
}

3:线程调用park则等待”许可”,unpark方法为指定线程提供”许可”,不要求pard和unpard的调用顺序,多次调用unpark之后,在调用park,线程会直接运行。但不会叠加,也就是说连续多次调用park方法,第一次会拿到”许可”直接运行,后续调用会进入等待。(与suspend相同,同步代码中不会释放锁)———易写出死锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void parkUnparkTest() throws Exception {
// 启动线程
Thread consumerThread = new Thread(() -> {
while (baozidian == null) { // 如果没有包子,则进入等待
System.out.println("1¡¢½øÈëµÈ´ý");
LockSupport.park();
}
System.out.println("2、买到包子,回家");
});
consumerThread.start();
// 3秒之后,生产一个包子
Thread.sleep(3000L);
baozidian = new Object();
LockSupport.unpark(consumerThread);
System.out.println("3、通知消费者");
}

伪唤醒

之前代码中用if语句来判断,是否进入等待状态,是错误的❌

官方建议应该在循环中检查等待条件,原因是处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。

伪唤醒是指线程并非因为notify、notifyall、unpark等api调用而唤醒,是更底层原因导致的。

1
2
3
4
5
6
7
8
9
10
11
//wait
synchronized(obj){
while(<条件判断>)
obj.wait();
...//执行后续操作
}
//park
while(<条件判断>)
LockSupport.park();
...//执行后续操作
}

12.线程状态

New、Runnable、Block、Waiting、Timed Waiting、Terminated

13.线程终止

1:不推荐使用stop方法,会破坏线程安全。
2:使用interrupt方法中断线程为什么是安全的,因为如果线程在没有执行完成的时候被中断,会抛出InterruptedException,由开发者捕获异常,再做处理。
3:在循环执行的时候可通过标志位,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Demo4 extends Thread {
public volatile static boolean flag = true;

public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
try {
while (flag) { // 标志位
System.out.println("ÔËÐÐÖÐ");
Thread.sleep(1000L);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
// 3秒后,将状态标志改为False,代表不继续运行
Thread.sleep(3000L);
flag = false;
System.out.println("³ÌÐòÔËÐнáÊø");
}
}

14.线程是不是越多越好,为什么有线程池出现

1、线程在java中是一个对象,更是操作系统的资源,线程创建、销毁需要时间。如果创建时间+销毁时间>执行任务时间 就很不合算。

2、java对象占用堆内存,操作系统线程占用系统内存,根据jvm规范,一个线程默认最大栈大消息1m,这个栈空间是需要从系统内存中分配的。线程过多,会消耗很多内存。

3、操作系统需要频繁切换线程上下文(大家都想被运行),影响性能。

线程池的退出,就是为了方便控制线程数量。