多线程面试专题最后篇章,喜欢的jym点个收藏关注喔 前面两部分链接: 2024年java面试准备--多线程篇(1) 2024年java面试准备--多线程篇(2)
只有调用了 start()方法,才会表现出多线程的特性,不同线程的 run()方法里面的代码交替执行。如果只是调用 run()方法,那么代码还是同步执行的,必须等待一个线程的 run()方法里面的代码全部执行完毕之后,另外一个线程才可以执行其 run()方法里面的代码。
Synchronized 关键字,Lock 锁实现,分布式锁等。
死锁就是两个线程相互等待对方释放对象锁
wait/notify
实现Callable 接口
保证线程同时执行可以用于并发测试。可以使用倒计时锁CountDownLatch实现让三个线程同时执行。
将其计数器初始为1,多个线程在开始执行任务前首先countdownlatch.await(),当主线程调用countDown()方法时,计数器变为0,多个线程同时被唤醒执行任务。
代码如下所示:
ExecutorService executorService = Executors.newCachedThreadPool();
CountDownLatch countDownLatch = new CountDownLatch(1);
executorService.submit(()->{
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程A执行,执行时间:" + System.currentTimeMillis());
});
executorService.submit(()->{
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程B执行,执行时间:" + System.currentTimeMillis());
});
executorService.submit(()->{
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程C执行,执行时间:" + System.currentTimeMillis());
});
countDownLatch.countDown();
打印内容如下,通过时间可以证明三个线程是同时执行的。
线程A执行,执行时间:1617811258309
线程C执行,执行时间:1617811258309
线程B执行,执行时间:1617811258309
让三个线程同时执行,也可以使用栅栏 CyvlivBarrier 来实现,当三个线程都到达栅栏处,才开始执行。
使用 join() 方法可以保证线程的顺序执行。在 Java 中,join() 方法是用来等待一个线程执行完成的方法,当调用某个线程的 join() 方法时,当前线程会被阻塞,直到该线程执行完成后才会继续执行。
具体来说,我们可以在 T1 线程结束时调用 T2 的 join() 方法,这样 T2 就会等待 T1 执行完成后再开始执行;同理,在 T2 结束时调用 T3 的 join() 方法,以确保 T3 在 T2 执行完成后才开始执行。这样就可以保证 T1、T2、T3 按照顺序依次执行。
使用 CountDownLatch(闭锁)方法可以保证线程的顺序执行。CountDownLatch 是一个同步工具类,它可以让某个线程等待多个线程完成各自的工作之后再继续执行。
@Test
public void testUseCountDownLatch() {
ExecutorService executorService = Executors.newCachedThreadPool();
CountDownLatch aLatch = new CountDownLatch(1);
CountDownLatch bLatch = new CountDownLatch(1);
CountDownLatch cLatch = new CountDownLatch(1);
executorService.submit(() -> {
try {
aLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 10; i++) {
System.out.println("A - " + i);
}
bLatch.countDown();
});
executorService.submit(() -> {
try {
bLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 10; i++) {
System.out.println("B - " + i);
}
cLatch.countDown();
});
executorService.submit(() -> {
try {
cLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 10; i++) {
System.out.println("C - " + i);
}
});
aLatch.countDown();
}
private volatile int count = 0;
/**
* 使用一个变量进行判断执行哪个线程。没有轮到的线程在不停循环,没有停止线程
*/
public void useVolatile() {
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.submit(() -> {
while (true) {
if (count == 0) {
for (int i = 0; i < 5; i++) {
System.out.println("A - " + i);
}
count = 1;
break;
}
}
});
executorService.submit(() -> {
while (true) {
if (count == 1) {
for (int i = 0; i < 5; i++) {
System.out.println("B - " + i);
}
count = 2;
break;
}
}
});
executorService.submit(() -> {
while (true) {
if (count == 2) {
for (int i = 0; i < 5; i++) {
System.out.println("C - " + i);
}
count = 3;
break;
}
}
});
}
使用单个线程池可以保证t1、t2、t3顺序执行,因为单个线程池只有一个工作线程,每次只会执行一个任务。我们可以将t1、t2、t3三个任务按照顺序提交给单个线程池,这样就可以确保它们按照顺序依次执行。
实现三个线程交错打印,可以使用ReentrantLock以及3个Condition来实现
execute没有返回值,如果不需要知道线程的结果就使用execute方法,性能会好很多。
submit返回一个Future对象,如果想知道线程结果就使用submit提交,而且它能在主线程中通过Future的get方法捕获线程中的异常。
死锁
死锁是多线程中最差的一种情况,多个线程相互占用对方的资源的锁,而又相互等对方释放锁,此时若无外力干预,这些线程则一直处理阻塞的假死状态,形成死锁。举个例子,A同学抢了B同学的钢笔,B同学抢了A同学的书,两个人都相互占用对方的东西,都在让对方还给自己自己再还,这样一直争执下去等待对方还而又得不到解决,老师知道此事后就让他们相互还给对方,这样在外力的干预下他们才解决,当然这只是个例子没有老师他们也能很好解决,计算机不像人如果发现这种情况没有外力干预还是会一直阻塞下去的。
活锁
活锁这个概念大家应该很少有人听说或理解它的概念,而在多线程中这确实存在。活锁恰恰与死锁相反,死锁是大家都拿不到资源都占用着对方的资源,而活锁是拿到资源却又相互释放不执行。当多线程中出现了相互谦让,都主动将资源释放给别的线程使用,这样这个资源在多个线程之间跳动而又得不到执行,这就是活锁。
饥饿
我们知道多线程执行中有线程优先级这个东西,优先级高的线程能够插队并优先执行,这样如果优先级高的线程一直抢占优先级低线程的资源,导致低优先级线程无法得到执行,这就是饥饿。当然还有一种饥饿的情况,一个线程一直占着一个资源不放而导致其他线程得不到执行,与死锁不同的是饥饿在以后一段时间内还是能够得到执行的,如那个占用资源的线程结束了并释放了资源。
无锁
无锁,即没有对资源进行锁定,即所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。无锁典型的特点就是一个修改操作在一个循环内进行,线程会不断的尝试修改共享资源,如果没有冲突就修改成功并退出否则就会继续下一次循环尝试。所以,如果多个线程修改同一个值必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。之前的文章我介绍过JDK的CAS原理及应用即是无锁的实现。
原子性
原子性是指一个线程的操作是不能被其他线程打断,同一时间只有一个线程对一个变量进行操作。在多线程情况下,每个线程的执行结果不受其他线程的干扰,比如说多个线程同时对同一个共享成员变量n++100次,如果n初始值为0,n最后的值应该是100,所以说它们是互不干扰的,这就是传说的中的原子性。但n++并不是原子性的操作,要使用AtomicInteger保证原子性。
可见性
可见性是指某个线程修改了某一个共享变量的值,而其他线程是否可以看见该共享变量修改后的值。在单线程中肯定不会有这种问题,单线程读到的肯定都是最新的值,而在多线程编程中就不一定了。每个线程都有自己的工作内存,线程先把共享变量的值从主内存读到工作内存,形成一个副本,当计算完后再把副本的值刷回主内存,从读取到最后刷回主内存这是一个过程,当还没刷回主内存的时候这时候对其他线程是不可见的,所以其他线程从主内存读到的值是修改之前的旧值。像CPU的缓存优化、硬件优化、指令重排及对JVM编译器的优化,都会出现可见性的问题。
有序性
我们都知道程序是按代码顺序执行的,对于单线程来说确实是如此,但在多线程情况下就不是如此了。为了优化程序执行和提高CPU的处理性能,JVM和操作系统都会对指令进行重排,也就说前面的代码并不一定都会在后面的代码前面执行,即后面的代码可能会插到前面的代码之前执行,只要不影响当前线程的执行结果。所以,指令重排只会保证当前线程执行结果一致,但指令重排后势必会影响多线程的执行结果。虽然重排序优化了性能,但也是会遵守一些规则的,并不能随便乱排序,只是重排序会影响多线程执行的结果。
什么是守护线程?与守护线程相对应的就是用户线程,守护线程就是守护用户线程,当用户线程全部执行完结束之后,守护线程才会跟着结束。也就是守护线程必 须伴随着用户线程,如果一个应用内只存在一个守护线程,没有用户线程,守护线程自然会退出。
举例,GC垃圾回收线程:就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是VM上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。