什么是多线程编程,有什么优缺点?
Java的多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务。
多线程的优势:
- 多线程的好处是可以提高 CPU 的利用率。在多线程程序中,一个线程必须等待的时候,CPU 可以运行其它的线程而不是等待,举个例子:当只有一个线程的时候会导致 CPU 计算时,IO 设备空闲;进行 IO 操作时,CPU 空闲。我们可以简单地说这两者的利用率目前都是 50%左右。但是当有两个线程的时候就不一样了,当一个线程执行 CPU 计算时,另外一个线程可以进行 IO 操作,这样两个的利用率就可以在理想情况下达到 100%了。。这样就大大提高了程序的效率。线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。
- 可以将多核CPU的计算能力发挥到极致,多核 CPU 时代意味着多个线程可以同时运行,举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,CPU 只会一个 CPU 核心被利用到,而创建多个线程就可以让多个 CPU 核心被利用到,这样就提高了 CPU 的利用率。。这减少了线程上下文切换的开销。
- 方便进行业务拆分,提升系统并发能力和性能,在特殊的业务场景下,先天的就适合于并发编程。现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。面对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分 。
存在的问题:
- 内存占用:线程也是程序,所以线程需要占用内存,线程越多占用内存也越多;
- 多线程需要协调和管理,所以需要 CPU 时间跟踪线程(cpu调度);
- 线程安全问题:线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题。
- 其他问题,比如:内存泄漏、上下文切换、死锁等等。**
并发编程的三大特性?
原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,在Java中使用了atomic包和synchronized来确保原子性;
可见性:一个线程对主内存的修改可以及时地被其他线程看到,在Java中使用了synchronized和volatile确保可见性;
有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,在Java中使用了happens-before原则来确保有序性。在执行程序时,为了提高性能,处理器和编译器常常会对指令进行重排序。
拓展1:重排序满足的条件?
- 在单线程环境下不能改变程序运行的结果;
- 存在数据依赖关系的不允许重排序。
所以重排序不会对单线程有影响,只会破坏多线程的执行语义。
拓展2:Java中如何保障重排序不影响单线程?
保障这一结果是因为在编译器,runtime 和处理器都必须遵守as-if-serial语义规则。
-
为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作可能被编译器和处理器重排序。
我们看这个例子,A和C之间存在数据依赖关系,同时B和C之间也存在数据依赖关系。因此在最终执行的指令序列中,C不能被重排序到A和B的前面,如果C排到A和B的前面,那么程序的结果将会被改变。但A和B之间没有数据依赖关系,编译器和处理器可以重排序A和B之间的执行顺序。
对线程安全的理解?如何保证线程安全?
- 专业的描述是,当多个线程访问一个对象时,如果不用进行额外的同步控制或其他的协调操作,调用这个对象的行为都可以获得正确的结果(对比单线程执行的结果),我们就说这个对象是线程安全的。简单说,多线程执行结果与单线程执行难结果相同,即线程安全。
- 线程安全应该是内存的安全,堆是共享内存,进程中的所有线程都能访问到该区域,所以可能造成潜在的安全问题。而栈是每个线程独有的,所以栈是线程安全的。
什么时候需要考虑线程安全?
- 多个线程访问同一个资源(存在资源竞争);
- 资源是有状态的,比如StringBuilder字符串拼接,数据发生改变
如何实现线程安全?
- 避免共享数据结构、共享状态:(1)使用线程local变量(2)使用不可变对象;
- 共享不可避免,通过条件来保证:(1)互斥锁(2)CAS原子操作;
针对上述4种实现线程安全的方式,Java提供的策略:
- ThreadLocal变量;
- 不可变对象有String,CopyOnWrite集合类;
- 互斥锁包括JDK5之前的内置锁,synchronized和JDK5之后的Lock接口;
- J.U.C里面Atom开头的类;
注:引入了线程安全和同步手段会对代码的性能造成一定的影响。
- 一般来说避免共享数据结构是能够比较优雅的解决并发问题,这种程序对多线程更友好,性能也会更高。比如单机的ThreadLocal和分布式的Ator模型。这里面不存在竞争。其次是不可变变量,多线程操作的都是CopyOnWrite,这也是为什么一些动态编程语言如Scala里面的默认数据结构大多数都是不可变的。不可变有不可变的好处,但缺点也是明显的,如果需要频繁对数据修改,那么会创建很多临时对象和占用更多的内存。
- 上面这两种场景,我们一般称为无锁实现,性能很好。如果避免不了共享数据,那么接着性能比较好的就是CAS这种原子操作,这种情况下我们一般也称是无锁的,但其实是利用了操作系统的原子指令来实现的,在竞争不激烈的场景下性能比较好,一般的编程语言都有封装好的工具类。
- 如果竞争激烈,其实性能未必比使用互斥锁高。互斥锁一般也称重量级锁,需要OS干涉线程的调度,适合用于竞争激烈的场景下,这种方式下线程上下文的交换会降级系统的性能,在使用时需要注意。
ThreadLocal的原理和使用场景
通常情况下,我们创建的变量是可以被任何⼀个线程访问并修改的。如果想实现每⼀个线程都有⾃⼰的专属本地变量该如何解决呢?
JDK中提供的 ThreadLocal 类正是为了解决这样的问题。ThreadLocal 类主要解决的就是让每个线程绑定⾃⼰的值,可以将 ThreadLocal 类形象的⽐喻成存放数据的盒⼦,盒⼦中可以存储每个线程的私有数据。如果你创建了⼀个 ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是 ThreadLocal 变量名的由来。
他们可以使⽤ get() 和 set() ⽅法来获取默认值或将其值更改为当前线程所存的副本的值,从⽽避免了线程安全问题。
ThreadLoacl 有一个静态内部类 ThreadLocalMap,其 Key 是 ThreadLocal 对象,值是 Entry 对象,Entry 中只有一个 Object 类的 vaule 值。ThreadLocal 是线程共享的,但 ThreadLocalMap 是每个线程私有的。
ThreadLocal 主要有 set、get 和 remove 三个方法:
- set 方法:ThreadLocal首先会获取当前线程对象,然后再获取当前线程对应的 ThreadLocalMap 类型的对象 map。如果 map 存在就直接设置值,key 是当前的 ThreadLocal 对象,value 是传入的参数。如果 map 不存在就通过 createMap 方法为当前线程创建一个 ThreadLocalMap 对象再设置值。
- get 方法ThreadLocal首先会获取当前线程对象,然后再获取当前线程对应的 ThreadLocalMap 类型的对象 map。如果 map 存在就以当前 ThreadLocal 对象作为 key 获取 Entry 类型的对象 e,如果 e 存在就返回它的 value 属性。如果 e 不存在或者 map 不存在,就调用 setInitialValue 方法先为当前线程创建一个 ThreadLocalMap 对象然后返回默认的初始值 null。
- remove 方法:首先通过当前线程获取其对应的 ThreadLocalMap 类型的对象 map,如果 map 不为空,就解除 ThreadLocal 这个 key 及其对应的 value 值的联系。
存在的问题:
- 线程复用会产生脏数据,由于线程池会重用 Thread 对象,因此与 Thread 绑定的 ThreadLocal 也会被重用。如果没有调用 remove 清理与线程相关的 ThreadLocal 信息,那么假如下一个线程没有调用 set 设置初始值就可能 get 到重用的线程信息。
- ThreadLocal 还存在内存泄漏的问题,由于 ThreadLocal 是弱引用,但 Entry 的 value 是强引用,因此当 ThreadLocal 被垃圾回收后,value 依旧不会被释放。因此需要及时调用 remove 方法进行清理操作。
ThreadLocal正确的使用方法:
- 每次使用完ThreadLocal都调用它的remove()方法清除数据
- 将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。
ps:强引用、软引用、弱引用、虚引用有什么区别?具体使用场景是什么?
Java 中引入四种引用的目的是让程序自己决定对象的生命周期,JVM 是通过垃圾回收器对这四种引用做不同的处理,来实现对象生命周期的改变。
我们都知道 JVM 垃圾回收中,GC判断堆中的对象实例或数据是不是垃圾的方法有引用计数法(判断对象的引用个数)和可达性算法(通过搜索判断对象的引用链是否可达)两种,判定对象是否存活都与"引用"相关。
经典回答:不同的引用类型,主要体现的是对象不同的可达性状态和对垃圾收集的影响。下面四种引用强度依次逐渐减弱:
-
强引用特点:在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。类似 “Object obj = new Object()” 这类的引用。
- 当一个对象被强引用变量引用时,它处于可达状态,是不可能被垃圾回收器回收的,即使该对象永远不会被用到也不会被回收。
- 当内存不足,JVM 开始垃圾回收,对于强引用的对象,就算是出现了 OOM 也不会对该对象进行回收,打死都不收。因此强引用有时也是造成 Java 内存泄露的原因之一。
- 对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显示地将相应(强)引用赋值为 null,一般认为就是可以被垃圾收集器回收。(具体回收时机还要要看垃圾收集策略)。
-
软引用特点:软引用是一种相对强引用弱化了一些的引用,软引用用来描述一些还有用,但并非必需的对象。
- 需要用java.lang.ref.SoftReference 类来实现,可以让对象避免一些垃圾收集。
- 对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中并进行第二次回收。如果这次回收还是没有足够的内存,才会抛出内存溢出异常。对于只有软引用的对象来说:当系统内存充足时它不会被回收,当系统内存不足时它才会被回收。
- 软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收。
-
弱引用特点:弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,无论内存是否充足。被弱引用关联的对象只能生存到下一次垃圾收集发生之前。
- 弱引用需要用java.lang.ref.WeakReference类来实现,它比软引用的生存期更短。
- 对于只有弱引用的对象来说,只要垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
- 官方文档,弱引用常被用来实现规范化映射,JDK 中的 WeakHashMap 就是一个这样的例子
-
虚引用、幻想引用特点;它是最弱的一种引用关系。顾名思义,就是形同虚设,与其他几种引用都不太一样,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。
- 虚引用需要java.lang.ref.PhantomReference 来实现。
- 如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列(RefenenceQueue)联合使用。
- 虚引用的主要作用是跟踪对象垃圾回收的状态。仅仅是就是在这个对象被回收器回收的时候收到一个系统通知或者后续添加进一步的处理。PhantomReference 的 get 方法总是返回 null,因此无法访问对应的引用对象。其意义在于说明一个对象已经进入 finalization 阶段,可以被 GC 回收,用来实现比 finalization 机制更灵活的回收操作。
并发、并行与串行/同步与异步
(1)并发与并行
并发:多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行,即同一时间段,单线程交替执行多个任务 (单位时间内不一定同时执行,逻辑上是同时进行的)。
并行:单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的“同时进行”,即单位时间内,多线程同时执行多个任务。
ps:多线程是程序设计的逻辑层概念,它是进程中并行运行的一段代码。多线程可以实现线程间的切换执行。
(2)并行与串行
并行和串行指的是任务的执行方式。
串行:时间上不可能发生重叠,指多个任务时,各个任务按顺序执行,完成一个之后才能进行下一个。
并行:时间上是重叠的,指多个任务可以同时执行(互不干扰),异步是多个任务并行的前提条件。
(3)同步与异步
同步与异步指的是能否开启新的线程。同步不能开启新的线程,异步可以开启新的线程。
同步就是顺序执行,执行完一个再执行下一个,需要等待、协调运行。
异步就是彼此独立,在等待某事件的过程中继续做自己的事,不需要等待这一事件完成后再工作。线程就是实现异步的一个方式。异步是让调用方法的主线程不需要同步等待另一线程的完成,从而可以让主线程干其它的事情。
异步和多线程并不是一个同等关系 ,异步是最终目的,多线程只是我们实现异步的一种手段。异步是当一个调用请求发送给被调用者,而调用者不用等待其结果的返回而可以做其它的事情。实现异步可以采用多线程技术或则交给另外的进程来处理。
内存泄露与内存溢出的区别
内存溢出 out of memory:是指程序在申请内存时,没有足够的内存空间供其使用,即申请内存时,系统不能满足需求,于是产生了溢出;比如给你一个int类型存储空间,但你却存了long类型的数据,那就是内存溢出。
内存泄露 memory leak:是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
内存泄露最终会导致内存溢出(OOM)!
通俗解释: 内存溢出,就是说,你向系统申请了装10个橘子的篮子(内存)并拿到了,但你却用它来装10个苹果,从而超出其最大能够容纳的范围,于是产生溢出; 内存泄漏,就是说系统的篮子(内存)是有限的,而你申请了一个篮子,拿到之后没有归还(忘记还了或是丢了),于是造成一次内存泄漏。在你需要用篮子的时候,又去申请,如此反复,最终系统的篮子无法满足你的需求,最终会由内存泄漏造成内存溢出。
上下文切换
多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。
时间片是CPU分配给各个线程的时间,因为时间非常短,所以CPU不断通过切换线程,让我们觉得多个线程是同时执行的,时间片一般是几十毫秒。
当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。
概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
频繁的上下文切换?
上下文切换通常是计算密集型(CPU密集型)的,每次切换时,需要保存当前的状态起来,以便能够进行恢复先前状态,而这个切换时非常损耗性能。
也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,Linux上下文切换和模式切换的时间消耗非常少。
减少上下文切换的措施有哪些?
无锁并发编程:可以参照concurrentHashMap锁分段的思想,不同的线程处理不同段的数据,这样在多线程竞争的条件下,可以减少上下文切换的时间。
CAS算法:利用Atomic下使用CAS算法来更新数据,使用了乐观锁,可以有效的减少一部分不必要的锁竞争带来的上下文切换。
使用最少线程:避免创建不需要的线程,比如任务很少,但是创建了很多的线程,这样会造成大量的线程都处于等待状态。
协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。运行在用户空间,效率也比较高。
什么是协程?
什么是线程死锁,如何避免死锁?
(1)什么是线程死锁:
线程死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。比如:线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
代码如下:
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
线程 1通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过Thread.sleep(1000);
让线程1 休眠 1s 为的是让线程 2 得到执行然后获取到 resource2 的监视器锁。线程 1 和线程 2 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁,导致程序不能正常终止。
死锁的必须具备的四个条件:
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。这些永远在互相等待的进程称为死锁进程。
- 互斥条件:一个资源每次只能被一个进程使用。此时若有其他进程请求该资源,则请求进程只能等待。
- 请求与保持条件:进程已经获得了至少一个资源,但又对其他资源发出请求,而该资源已被其他进程占有,此时该进程的请求被阻塞,但又对自己获得的资源保持不放。
- 不剥夺条件:进程已获得的资源在未使用完毕之前,不可被其他进程强行剥夺,只能由自己释放。
- 循环等待条件: 存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。
(2)如何避免线程死锁:
线程死锁的必要条件,破坏其一即可:
- 破坏互斥条件 :“互斥”条件是无法破坏的,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
- 破坏请求与保持条件 :1)采用资源预先分配策略,即进程运行前申请全部资源,满足则运行,不然就等待。 2)每个进程提出新的资源申请前,必须先释放它先前所占有的资源。
- 破坏不剥夺条件 :当进程占有某些资源后又进一步申请其他资源而无法满足,则该进程必须释放它原来占有的资源。
- 破坏循环等待条件 :实现资源有序分配策略,将系统的所有资源统一编号,所有进程只能采用按序号递增的形式申请资源。
资源有序分配举例:
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");
}
}
}, "线程 2").start();
输出结果:
Thread[线程 1,5,main]get resource1
Thread[线程 1,5,main]waiting get resource2
Thread[线程 1,5,main]get resource2
Thread[线程 2,5,main]get resource1
Thread[线程 2,5,main]waiting get resource2
Thread[线程 2,5,main]get resource2
我们分析一下上面的代码为什么避免了死锁的发生?
线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。
多线程交替打印方式
这是一道面试中常考的并发编程的代码题,与它相似的问题有:
- 三个线程T1、T2、T3轮流打印ABC,打印n次,如ABCABCABCABC.......
- 两个线程交替打印1-100的奇偶数
- N个线程循环打印1-100
......
其实这类问题本质上都是线程通信问题,思路基本上都是一个线程执行完毕,阻塞该线程,唤醒其他线程,按顺序执行下一个线程。下面先来看最简单的,如何按顺序执行三个线程。
(1) synchronized+wait/notify
基本思路就是线程A、线程B、线程C三个线程同时启动,因为变量num的初始值为0,所以线程B或线程C拿到锁后,进入while()循环,然后执行wait()方法,线程阻塞,释放锁。只有线程A拿到锁后,不进入while()循环,执行num++,打印字符A,最后唤醒线程B和线程C。此时num值为1,只有线程B拿到锁后,不被阻塞,执行num++,打印字符B,最后唤醒线程A和线程C,后面以此类推。
class Wait_Notify_ACB {
private int num;
private static final Object LOCK = new Object();
private void printABC(int targetNum) {
synchronized (LOCK) {
while (num % 3 != targetNum) { //想想这里为什么不能用if代替while,想不起来可以看公众号上一篇文章
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();
}
}
上述代码输出ABC,那么交替打印ABCn次呢(这里n == 10)。很简单,只需要在上述代码的基础上加一个循环即可!
class Wait_Notify_ACB {
private int num;
private static final Object LOCK = new Object();
private void printABC(int targetNum) {
for (int i = 0; i < 10; i++) {
synchronized (LOCK) {
while (num % 3 != targetNum) { //想想这里为什么不能用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-10的奇偶数,基本思路上面类似,线程odd先拿到锁——打印数字——唤醒线程even——阻塞线程odd,以此循环。
(2)join()
join()方法:在A线程中调用了B线程的join()方法时,表示只有当B线程执行完毕时,A线程才能继续执行。基于这个原理,我们使得三个线程按顺序执行,然后循环多次即可。无论线程1、线程2、线程3哪个先执行,最后执行的顺序都是线程1——>线程2——>线程3。代码如下:
class Join_ABC {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
Thread t1 = new Thread(new printABC(null),"A");
Thread t2 = new Thread(new printABC(t1),"B");
Thread t3 = new Thread(new printABC(t2),"C");
t0.start();
t1.start();
t2.start();
Thread.sleep(10); //这里是要保证只有t1、t2、t3为一组,进行执行才能保证t1->t2->t3的执行顺序。
}
}
static class printABC implements Runnable{
private Thread beforeThread;
public printABC(Thread beforeThread) {
this.beforeThread = beforeThread;
}
@Override
public void run() {
if(beforeThread!=null) {
try {
beforeThread.join();
System.out.print(Thread.currentThread().getName());
}catch(Exception e){
e.printStackTrace();
}
}else {
System.out.print(Thread.currentThread().getName());
}
}
}
}
巨人的肩膀:
https://zhuanlan.zhihu.com/p/358015444
https://netsecurity.51cto.com/art/201901/591186.htm
https://www.codercc.com/backend/basic/juc/base/overview.html
https://mp.weixin.qq.com/s/k_6shN16l9pPR_k3kufLQQ
https://juejin.cn/post/6959078859568316452