目录
1.什么是线程?
2.线程和进程的区别?
3.什么是自旋锁?
4.什么是CAS?
5.什么是乐观锁和悲观锁?
6.什么是原子操作?
7.什么是Callable和Future?
8.ThreadLocal
9.InheritableThreadLocal
10.ThreadPool线程池的好处
11.CountDownLatch
12.CyclicBarrier
13.synchronized和ReentrantLock的区别?
14.Semaphore有什么作用?
15.ReentrantReadWriteLock
16.写一段死锁代码.你在java中如何避免死锁?
17.Java 中volatile关键字是什么?它和 Java 中的同步方法有什么区别?
18.什么是竟态条件?如何解决?
19.既然 start() 方法会调用 run() 方法,为什么我们调用 start() 方法,而不直接调用 run() 方法?
20.java中如何唤醒一个阻塞的线程?
21.什么是不可变类?它对于编写并发应用有何帮助?
22.多线程的上下文切换是什么?
23.死锁,活锁,饿锁
24.java中使用什么线程调度算法?
25java中的线程调度是什么?
26.什么是线程组?为什么java中不建议使用线程组?
27.线程中如何处理某个未处理的异常.
28.java中Executor和Executors区别
29.在windows和linux上分别如何找到占用cpu最多的线程?
30.如何选择线程池的数量
31.线程池的创建
32.线程池的实现原理
线程是操作系统进行运算调度的最小单位,如果一个线程完成一项运算需要100毫秒,那么理论上10个线程完成该任务只需要10毫秒.
一个进程可以有多个线程,线程的粒度要比进程更细,比如我启动一个应用(如QQ),启动的QQ就是一个进程,该进程里可能包含多个线程.
自旋锁是为了保护共享资源而引入的一种锁,它与互斥锁非常类似,在同一时刻只有一个线程可以获得该自旋锁,不同的是在没有竞争到锁的线程不必像互斥锁那样进入休眠等待,而是一直在循环自旋观察,直到拥有自旋锁的线程释放了锁.
CAS(compare and swap) 比较交换,是一种乐观锁的操作,CAS包含三个操作数,一个是V(内存中的值),A(预期原值),B(新值),预期原值A与内存值V相等时,才会更新内存中的值V为新值B.否则该线程自旋,JDK1.8中的ConcurrentHashMap就是就是基于CAS实现的.
乐观锁:如其名,乐观锁认为数据一般情况下不会存在冲突,所以乐观锁一开始不会对资源进行加锁,只有在数据进行提交时才会去判断数据是否存在冲突,在竞争比较小的情况下,乐观锁的性能要高于悲观锁.
悲观锁:悲观锁认为数据冲突一直存在,所以在对数据进行操作前就会对数据进行加锁,确保数据资源被线程独占,没有得到锁的线程进入锁池等待,在竞争激烈的情况下,悲观锁性能要高于乐观锁.
原子操作是一个不受其它影响的操作任务单元,比如int ++ 就不是一个原子操作,线程会先去读取int的值,然后再对其进行++操作,可能一个线程在读取的时候数据的值已经被另外一个线程 ++ 操作过了.
Callable通过call方法提交线程执行后的结果,然后Future通过get方法获取线程执行后的结果.
import java.util.concurrent.*;
public class Test1 {
public static void main(String[] args) throws Exception {
System.out.println("start main thread");
ExecutorService threadPool = Executors.newFixedThreadPool(1);
Future future = threadPool.submit(new Callable() {
@Override
public String call() throws Exception {
System.out.println("start new thread");
Thread.sleep(500);
System.out.println("end new thread");
return "我是返回内容";
}
});
String result = future.get();
System.out.println("线程的返回内容是:"+result);
threadPool.shutdown();
System.out.println("end main thread");
}
}
结果:
ThreadLocal是JDK1.2以后提供的线程局部变量,属于线程私有.
每个线程只能看到自己的ThreadLocal变量.
public class Test2 {
public static void main(String[] args) {
ExecutorService exec = Executors.newFixedThreadPool(4);
for (int i = 0; i <4 ; i++) {
exec.submit(new Runnable() {
private ThreadLocal threadLocal = new ThreadLocal();
@Override
public void run() {
threadLocal.set(Math.random());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(threadLocal.get());
}
});
}
}
}
可以看到,每个线程都私有了自己的随机数,并不会因为同步被覆盖.
InheritableThreadLocal是ThreadLocal的子类,功能跟threadLocal类似,主要是为子线程提供的,使用ThreadLocal作为线程局部变量时,该变量仅能被调用set方法的那个线程通过get方法拿到,如果它的子线程也需要用到set的变量时,子线程是拿不到的,这个时候就需要InheritableThreadLocal.
减少了创建和销毁线程的次数,节省了系统资源,起到了一定的缓冲作用.
可以根据系统当前的承载情况动态的调整工作线程的数量,避免启动过多的线程,每个线程约占1M作用内存,过多的线程会耗尽系统资源.
可以提供更多个性化功能,如定时,定期执行任务.
CountDownLatch是一个类似于计数器功能的线程工具类.给定一个初始值,然后可以通过await()方法阻塞线程,直到调用countDown()将计数器中的值减小至0.
举个实际应用的场景可能会比较容易理解,现在我有一个excel文件,我需要读取其中的所有数据,这个excel中有多个sheet页,我需要一次性把所有sheet页中的数据都取出来,而且要响应时间不能太久,这个时候就可以通过启动多个线程去读取sheet页中的内容,最后汇总.在读取的过程中因为sheet中的内容量不同和线程启动时间的不同会导致有线程先完成,有线程后完成,我们需要等到最后一个线程完成读取后进行汇总,所以在此之前要将先完成的线程阻塞,代码模拟之:
public class Test3 {
public static void main(String[] args) {
//假设我有4个sheet页
final CountDownLatch latch = new CountDownLatch(4);
ExecutorService exec = Executors.newCachedThreadPool();
for (int i = 0; i < 4; i++) {
exec.submit(() -> {
System.out.println(Thread.currentThread().getName()+"我正在读取execel中的内容...");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"我已经读完了...");
latch.countDown();
});
}
try {
latch.await();
System.out.println("全部都读完了,主线程正在汇总中...");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
exec.shutdown();
}
}
}
测试:
CyclicBarrier循环屏障,与CountDownLatch功能类似,不同的是它可以对一组线程多次阻塞,举个例子,例子是我联想的,具体到底有没有大厂这么用,尚不清楚,这里仅作为帮助理解使用.比如现在有80台服务器,在这80台服务器上做了redis集群,有一天因为断电导致集群需要重新配置,在启动这么庞大一个集群前,为了确保万无一失,我需要启动多个线程先分别对这些服务器的外网,端口等做检查,确保外网畅通,还需要判断每台服务器的6379端口是否处于监听状态,还判断XXX...一环扣一环,若所有判断条件都满足后,再进行配置操作,等所有线程完成配置后,再启动整个集群,像类似这样的操作就可以用CyclicBarrier来模拟:
public class Test4 {
public static void main(String[] args) throws Exception {
//指定需要阻塞多少个线程,我这里暂时仅启动3个线程来模拟检查
CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
ExecutorService exec = Executors.newCachedThreadPool();
for (int i = 0; i < 3; i++) {
exec.submit(() -> {
try {
System.out.println(Thread.currentThread().getName() + ":正在进行必要检查...");
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+":已完成检查...");
cyclicBarrier.await();
System.out.println(Thread.currentThread().getName() + ":正在进行相关配置...");
Thread.sleep(200);
System.out.println(Thread.currentThread().getName()+":已完成配置...");
cyclicBarrier.await();
System.out.println(Thread.currentThread().getName()+":一切就绪,准备启动...");
} catch (Exception e) {
e.printStackTrace();
}
});
}
exec.shutdown();
}
}
可重入锁:synchronized和ReentrantLock都是可重入锁,同一个线程可以多次获取同一把锁.
可中断锁:synchronized是不可中断锁,ReentrantLock提供了可中断功能,线程在尝试获取锁的过程中,可以响应中断.
synchronized是Java内置的关键字,锁的获取和释放都由jvm自动完成,用起来比较方便,代码可读性也比较高.
但synchronized也有一些弊端:
比如当前线程在等待获取锁的过程中,如果没有获取到锁,会一直处于阻塞状态;
获取到锁的线程如果进入sleep或者阻塞,其它未获取到锁的线程会一直处于阻塞状态.
synchronized是不公平锁,ReentrantLock默认是不公平锁,但提供了公平锁的实现,公平锁就是所有线程按照申请锁先后顺序获得锁.
ReentrantLock提供了获取锁的超时时间,可以避免死锁,synchronized没有这种机制.
Semaphore是信号量的意思,可以限制某段代码的并发数,在其构造函数中可以传入一个Int类型的参数指定同时可以有多少个线程访问,超出设定数值的线程需要排队等待,直到有线程完成并释放.
Semaphore提供了两个比较重要的方法acquire()和release(),前者是获取许可,后者释放许可,在释放许可前必须先获取到许可.
下面模拟这样一个场景帮助理解,一个车库只有3个停车位,但现在有5辆车子,同一时间段内最多只允许驶入3辆车子,其余的车子只能等有车子驶离了才能进入.
public class Test5 {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 5; i++) {
executorService.submit(() -> {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + ":车子驶入了停车场");
Thread.sleep(Math.round(1000));
System.out.println(Thread.currentThread().getName() + ":车子驶离了停车场");
semaphore.release();
} catch (Exception e) {
e.printStackTrace();
}
});
}
executorService.shutdown();
}
}
可重入读写锁,一把读锁,一把写锁,读锁可以允许多个线程同时进入进行数据的读操作,写锁只允许一个线程对数据进行写操作.
如果一个线程拥有写锁,它还可以持有读锁,持有读锁后写锁被自动降级为读锁.
如果是公平锁,那么无论是读锁还是写锁都要遵循FIFO先进先出原则,如果是非公平锁,则写锁可以无条件插队.
常被用来实现缓存系统,可以先加一把可重入的读锁来获取缓存,如果缓存失效了,解锁读锁,然后添加写锁,写入数据后,获取读锁,释放写锁,然后使用数据,用完之后把读锁释放掉即可.
比如我启动2个线程:线程A和线程B,分别创建2把锁:锁A和锁B,让线程A持有锁A并尝试获取锁B,让线程B持有锁B并尝试获取锁A,在A尝试获取锁B前让线程休眠一会,以便让B线程能够持有锁B,这样一来A线程在等待B线程释放锁B,B线程在等待A线程释放锁A,于是两个线程就这样僵持下去,谁也不肯先释放自己手中的锁.
public class Test6 {
private volatile static Boolean flag = false;
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
Object lock1 = new Object();
Object lock2 = new Object();
for (int i = 0; i < 2; i++) {
executorService.submit(() -> {
if (!flag) {
flag = true;
synchronized (lock1) {
System.out.println(Thread.currentThread().getName() + "锁住资源1,等待获取资源2");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2){
System.out.println("得到资源2");
}
}
} else {
flag = false;
synchronized (lock2){
System.out.println(Thread.currentThread().getName() + "锁住资源2,等待获取资源1");
synchronized (lock1){
System.out.println("得到资源1");
}
}
}
});
}
executorService.shutdown();
}
}
避免死锁的三种方式:
1.控制加锁顺序,像上面的代码我是刻意为之,让线程休眠以此保证线程B比线程A先拿到锁B,这样就产生了死锁,那么如果线程A先拿到锁B,就可以做到啥事没有,所以控制线程获取锁的顺序可以一定程度上避免死锁,我们可以尽量使用ReentrantLock提供的公平锁,控制获取锁的顺序FIFO.
2.控制加锁时间,当死锁发生时,我们可以控制锁的自动解锁时间,这样就可以让死锁变成活锁,ReentrantLock提供了锁的超时时间.
3.检测死锁,我们可以用map记录锁的持有者和请求者,然后进行检测,如果发生了死锁可以让持有锁的线程回退.
使用volatile关键字可以实现变量在线程之间的内存可见性.
每个线程有自己的一块私有内存,每次对变量进行操作时,线程会先去主内存里去读数据,然后在私有线程里对数据进行操作,加了volatile关键字后,会对所有写操作进行"强刷",也就是比如某线程对变量进行了修改,系统会将修改后的变量值强刷进主内存,并让所有其他线程重新读取该主内存中的变量值到线程的私有内存中,以此来实现多个线程之间的内存可见性.
volatile实现内存可见性的方式和同步方法不同,同步方法是通过锁住某块代码/方法,在同一时段内仅允许单个线程对变量进行修改,而volatile是通过"强刷"的方式,在同一时间段内可能有多个线程对该数据进行操作.
volatile不具有原子性,但同步方法具有原子性.
静态条件是指多个线程同时对内存中的同一块数据进行操作,只要有一个线程对数据进行了修改操作,就会发生数据竞争.
竟态条件可以通过volatile关键字,同步锁等解决.
start()
方法会调用 run()
方法,为什么我们调用 start()
方法,而不直接调用 run()
方法?如果直接调用run方法,相当于是调用了Thread类中的run方法,本质上是拿主线程调用了一个类中的方法,并没有启动新的线程.
但如果调用了start方法,Thread类会创建一个新的线程并执行run方法中的代码.
需要分情况,如果是因为IO导致的阻塞,无法唤醒;如果是调用了wait,sleep,jion方法导致的阻塞,可以通过Interrupted抛出InterruptedException异常来唤醒线程.
不可变类一旦被创建后,其成员变量的值就不能被改变.比如JDK提供的String,Integer等包装类,都是不可变类,对并发编程而言,不可变类是线程安全的,对不可变类的操作不需要额外的操作,就可以实现数据在多个线程之间的同步,同时因为不需要额外加锁,可以在性能上有所提高.
Cpu会给每个线程分配执行时间段,称之为时间片,Cpu通过时间片的算法循环执行线程任务,因为时间片非常短,所以循环执行时让我们感觉多个线程是同时执行的,实际上则是当前任务时间片结束后切换至下一个任务,在切换前会对当前任务执行的状态进行保存,然后下次再重新加载,从任务保存到下次加载就是一次上下文切换.
过多的上下文切换会带来性能上的降低,所以在开发中应尽量避免过于频繁的上下文切换,要做到这一点,可以从以下几个方面入手:
无锁并发编程:可以将要处理数据的Id进行hash取模,然后对不同分段的Id用不同的线程进行操作.
使用cas算法:jdk提供了一套原子性操作的数据类型,位于java.concurrent.automic包下,如AutomicInteger,AutomicLong,这些数据类型本身基于cas算法更新数据,无需进行加锁.
避免创建过多线程:在不影响任务完成的前提下,尽量创建较少的线程,避免创建不必要的线程.
协程:可以让一系列相互依赖的携程依次使用CPU资源,使用condition,signal或者wait/notify 来让多个线程分工合作完成某项任务,每次只有一个协程工作,其他协程休眠.
死锁:至少两个线程在竞争资源时造成的一种互相等待现象.
实例:A和B吵架了,于是A抢了B家门的钥匙,说B你今晚休想回家,B也不甘示弱,去抢了A家门的钥匙,说A你今晚也休想回家,咱们玉石俱焚...于是A和B就这么僵持下去,谁也不肯让谁,最后都进不来家门.
活锁:两个优先级相同的线程,相互礼让,最终谁都没有使用资源...
实例:两个"君子"在门口相遇了,A让B先走,B让A先走,就这样像个傻X一样互相谦让着,最后谁都没出门...
饿锁:线程1获得了A资源,线程2也申请获取A资源,当线程1释放A资源后,这时线程3也申请了A资源,然后A资源不巧被分配给了线程3,等线程3释放了A资源后,又来了个线程4也申请A资源,不巧的是A资源又被线程4给竞争到了...如此下去,线程2一直没拿到资源,处于饥饿状态.
实例:现实中这种不公平的例子也太多了,我有几次在食堂打饭就这样,明明自己先来的,却老被后来的先打了饭,心里不是饿,是不爽!
抢占式.当一个线程用完cpu后,操作系统会根据线程优先级,线程饥饿情况等数据计算出一个总的优先级,并分下一个时间片给某个线程执行.
以某种顺序在单个CPU上执行多个线程称之为线程调度,拥有一个好的线程调度,可以很好的发挥系统性能,充分利用CPU资源.
调度类型:
等待和通知:wait/notify
运行和让步:Yield为相同优先级的线程提供让步,让其有机会运行.
睡眠和启用:sleep可以让线程处于休眠
线程的优先级从1-10,数值越大,优先级越高,创建的线程默认优先级是5,继承的线程保持父线程的优先级,也可以通过setPriority来设置线程创建后的优先级.
在java中,为了方便线程管理出现了线程组,一个线程组可以拥有多个子线程和子线程组,在一个进程中线程组是以树的形式存在的,根线程是system,system线程组下是main线程组,默认情况下,第一级应用的自己的线程组是由main线程组创建的.
不推荐线程组是因为线程组中的stop,resume等会导致安全问题,主要是死锁问题,已被官方弃用,另外线程组不是线程安全的.
1.在子线程中进行try catch
2.为线程设置未捕获异常处理器UncaughtExceptionHandler
Thread.setUncaughtExceptionHandler设置当前线程的异常处理
Thread.setDefaultUncaughtExceptionHandler为整个程序设置默认的线程异常处理
Executor是接口,Executors是类
Executor是ExecutorService的父类,ExecutorService对Executor进行了功能上的很多拓展,提供了更多方法.
Executors提供了很多静态方法,帮助我们创建各种线程池.
windows:使用任务管理器查看
linux使用top命令查看
合理的安排线程池中线程的数量可以减少多线程的上下文切换,避免频繁创建/销毁线程等带来的性能损耗,从而提高整体性能.
线程池中的线程数量跟电脑的CPU核数,内存,和系统的业务类型等多方面因素有关,不必指定的过于精确,因为变化因素太多,精确反而不够灵活,一般可以按下面这个公式去估算:
NCPU:CPU的数量
UCPU:期望的CPU使用率 0<=NCP<=1
W/C:等待时间与计算时间的比率
如果期望处理器达到理想的使用率,那么线程池的最优大小为:
线程池大小=NCPU*UCPU(1+W/C)
在java中可以获取NCPU:
Integer NCPU=Runtime.getRuntime().availableProcessors();
线程池的创建尽量不要使用Executors去创建,阿里爸爸代码规范里说的.
原因主要是:
如果使用Executors.newFixedThreadPool()和Executors.newSingleThreadExecutor去创建的话,堆积的请求队列占用的内存可能会非常大,以至于OOM.
如果使用Executors.newCachedThreadPool去创建的话,由于maxPoolSize为Integer.MAX_VALUE,所以可能会创建数量极多的线程,占用大量内存,以至于OOM.
线程池创建的最佳实践应该是通过new ThreadPoolExecutor()来创建,里面的具体参数根据实际情况去填写,eg:
ExecutorService es = new ThreadPoolExecutor(6, 10, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(), (r)-> {
Thread t = new Thread(r);
System.out.println("创建了一个新线程");
return t;
});
线程池处理任务的主要流程如下:
1.判断当前运行的线程work数量是否大于corePoolSize?如果小于则创建线程并执行任务,如果大于则尝试将任务添加进队列
2.判断队列是否已满?如果未满则加入队列里等待被执行,如果已满,则进一步判断核当前corePoolsize是否小于maxPoolSize,
如果小于就创建一个线程去执行,否则拒绝策略生效,拒绝该任务提交.