文章

《Java 并发与多线程》

《Java 并发与多线程》

一、多线程基础

1. 并发带来的挑战

(1)上下文切换

CPU 通过时间片轮转算法来循环执行线程任务,需要切换任务时,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。

减少上下文切换的方法:

  • 无锁并发编程
  • CAS 算法
  • 使用最少线程
  • 使用协程

(2)死锁

概念:一组进程中的每一个进程都无限期的等待被其他进程占用着的资源,且永远不会释放。

死锁产生的必要条件:

  1. 互斥条件:资源不能被共享,只能由一个进程使用
  2. 请求与保持条件:已经得到资源的进程可以再次申请新的资源
  3. 不剥夺条件:已经分配的资源不能从相应的进程中被强制剥夺
  4. 环路等待条件:存在一个进程等待队列形成环路

避免死锁的方法:

  • 破坏请求与保持条件:一次性申请所有的资源
  • 破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源
  • 破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放

2. 理论基础

(1)线程的生命周期

线程的生命周期包括五个状态:新建、就绪、运行、阻塞、销毁

状态说明
新建刚使用 new 方法创建出来的线程
就绪调用线程的 start() 方法后,线程处于等待 CPU 分配资源阶段
运行就绪的线程被调度并获得 CPU 资源时,便进入运行状态,run() 方法定义了线程的操作和功能
阻塞在运行状态时,可能因为某些原因(如 sleep()、wait())导致线程进入阻塞状态,需要通过其他机制(如 notify()、notifyAll())将其唤醒
销毁线程正常执行完毕、被提前强制性终止或出现异常导致结束,线程被销毁,释放资源

📌 注意:唤醒的线程不会立刻执行 run 方法,它们要再次等待 CPU 分配资源进入运行状态

(2)等待通知范式(经常和 synchronized 连用)

等待通知的方法在每个 Java 对象上都有,因为它们在超类 Object 上定义。

相关方法:

  • 等待方法:wait()、wait(long)、wait(long, int)
  • 通知方法:notify()、notifyAll()

经典例子:Thread.join()

(3)ThreadLocal 的使用

通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量,就使用 ThreadLocal。

ThreadLocal 是一个线程变量,它是一个以 ThreadLocal 对象作为键,任意对象作为值的键值对结构,存储在每一个线程中。


二、Java 并发编程

1. 并发编程三个核心概念

概念说明
原子性一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行
有序性程序执行的顺序按照代码的先后顺序执行,禁止指令重排序
可见性当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值

2. 并发编程的底层实现原理

(1)volatile 关键字

特点:

  • 只能用于修饰共享变量
  • 具备可见性、有序性
  • 对单一 volatile 变量的读写具有原子性,但类似于 volatile++ 这种复合操作不具备原子性
  • 使用恰当的情况下比 synchronized 的使用和执行成本更低,因为它不会引起线程上下文的切换和调度

加锁原理:

观察加入 volatile 关键字和没有加入 volatile 关键字时所生成的汇编代码发现,加入 volatile 关键字时,会多出一个 lock 前缀指令。

lock 前缀指令实际上相当于一个内存屏障(也称内存栅栏),内存屏障会提供 3 个功能:

  1. 它会强制将对缓存的修改操作立即写入主存
  2. 如果是写操作,它会导致其他 CPU 中对应的缓存行无效(可见性)
  3. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面(有序性)

为何要有可见性?

如果线程 A 对数值 +1,那么线程 B 想要操作数值的时候,需要先在自己的工作内存读取。但是由于 volatile 的修饰,造成变量在线程 B 缓存中失效,所以线程 B 必须去公共内存读取最新的值。

有序性体现:

比如单例模式中,那个单例用 volatile 修饰,因为 new 关键字不是一个原子操作,可以分为:

  1. 给对象在堆内存中分配空间
  2. 初始化这个对象
  3. 栈(等号左边)指向堆里面的内存空间

主要是防止 2 和 3 重排序。


(2)synchronized 关键字(JVM 提供)

实现同步的基础(Java 中的每一个对象都可以作为锁):

  • 同步普通方法:锁的是当前对象
  • 同步静态方法:锁的是当前类的 Class 对象
  • 同步方法块:锁的是 synchronized 括号里配置的对象

加锁原理(JVM 基于进入和退出 Monitor 对象来实现方法同步和代码块同步):

经过编译后分别形成 monitorenter 和 monitorexit 这两个字节码指令(成对出现):

  • monitorenter 指令是在编译后插入到同步代码块的开始位置
  • monitorexit 是插入到方法结束处和异常处

任何对象都有一个 monitor 与之关联,当且一个 monitor 被持有后,它将处于锁定状态:

  • 执行 monitorenter 指令时首先尝试获取对象的锁
  • 如果这个对象没被锁定或者当前线程已经拥有了那个对象的锁,就把锁的计数器 +1
  • 执行 monitorexit 指令时就 -1
  • 如果计数器为 0,就释放锁
  • 如果另一个线程获取锁失败,就阻塞

锁状态(锁升级:四种锁状态由低到高,只能升级锁,不能降级):

  1. 无锁状态
  2. 偏向锁状态
  3. 轻量级锁状态
  4. 重量级锁状态

🎯 目的:提高获得锁和释放锁的效率

synchronized 锁升级的流程(解决获取锁与释放锁的成本):

  1. 无锁:创建一个对象时就是无锁状态

  2. 偏向锁:
    • 适用于单一个线程多次访问同一同步代码块
    • 为了减少获取锁的代价才使用
    • 对象头中的 mark word 中的偏向锁会标记这个线程
    • 当这个线程再次访问同步块时,只需要比对偏向锁标记,不再需要 CAS 操作来加锁与解锁
    • 如果 CAS 尝试修改偏向锁标记不成功,撤销偏向锁
  3. 轻量级锁:
    • 多个线程竞争锁,假设线程 1 先获取轻量级锁,CAS 修改 markword
    • 线程 2 尝试获取锁,尝试修改 markword 指向自己,但是线程 1 占据锁
    • 所以线程 2 自旋(while true)一段时间,尝试获取锁
    • 当 CAS 达到一定的次数,一直等不到轻量级锁释放,则锁膨胀为重量级锁
  4. 重量级锁:
    • 如果能获取到重量级锁,当前线程 CAS 修改 markword
    • 如果获取不到,自己会阻塞,等到线程 1 释放锁并唤醒阻塞的线程 2
    • 然后重新竞争锁

锁慢的原因:

Java 线程是映射到操作系统原生线程之上的,如果阻塞或者唤醒一个线程,都需要操作系统完成,这就需要从用户态转换成内核态。这是很耗费时间的。

💡 JDK 1.6 之前 synchronized 是一个重量级锁,1.7 之后 JVM 做了优化,有了锁升级策略,就不再是一个单纯的重锁了。


(3)CAS 算法(Compare And Swap - 比较与替换)

CAS 是一种有名的无锁算法,无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步。

锁内容包含三个:

  • V:变量的内存地址
  • A:旧的预期值
  • B:新值

加锁机制:

当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V 的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。

CAS 带来的问题:

  1. ABA 问题
  2. CPU 开销大(高并发情况下,内存值更新频繁,一直有线程在循环)
  3. 只能保证一个共享变量的原子操作(CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效)

(4)Lock 接口与 AQS

1)Lock 接口

提供显式的锁的获取与释放(synchronized 隐式),并且提供了一些 synchronized 不具备的功能:

  • 尝试非阻塞获取锁
  • 能被中断获取锁
  • 超时获取锁
2)AQS(AbstractQueuedSynchronizer - 队列同步器)

简介:

AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如:

  • ReentrantLock
  • Semaphore
  • ReentrantReadWriteLock
  • FutureTask

实现原理:

AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。

状态信息通过 protected 类型的 getState、setState、compareAndSetState 进行操作。

核心思想:

  • 如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态
  • 如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中

AQS 对资源的共享方式:

Exclusive(独占)

只有一个线程能执行,获得共享资源的使用权,又可分为公平锁和非公平锁:

  • 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
  • 非公平锁:当线程要获取锁时,先通过两次 CAS 操作去抢锁,如果没抢到,当前线程再加入到队列中等待唤醒

公平锁和非公平锁只有两处不同(以 ReentrantLock 的两种锁为例):

  1. 非公平锁在调用 lock 方法后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了
  2. 非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面
Share(共享)

多个线程可同时执行,如:

  • Semaphore
  • CountDownLatch
  • CyclicBarrier
  • ReadWriteLock

3)基于 AQS 为模板实现的一些组件
Semaphore(信号量)

synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。

CountDownLatch(倒计时器)

CountDownLatch 允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。

典型使用场景:

① 某一线程在开始运行前等待 n 个线程执行完毕:

  • 将 CountDownLatch 的计数器初始化为 n:new CountDownLatch(n)
  • 每当一个任务线程执行完毕,就将计数器减 1:countdownlatch.countDown()
  • 当计数器的值变为 0 时,CountDownLatch 上 await() 的线程就会被唤醒

② 实现多个线程开始执行任务的最大并行性:

  • 注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行
  • 类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑
  • 做法是初始化一个共享的 CountDownLatch 对象,将其计数器初始化为 1:new CountDownLatch(1)
  • 多个线程在开始执行任务前首先 coundownlatch.await()
  • 当主线程调用 countDown() 时,计数器变为 0,多个线程同时被唤醒
CyclicBarrier(循环栅栏)

CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。

CyclicBarrier 默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用 await() 方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。

应用场景:

CyclicBarrier 可以用于多线程计算数据,最后合并计算结果的应用场景。比如我们用一个 Excel 保存了用户所有银行流水,每个 Sheet 保存一个帐户近一年的每笔银行流水,现在需要统计用户的日均银行流水:

  • 先用多线程处理每个 sheet 里的银行流水
  • 都执行完之后,得到每个 sheet 的日均银行流水
  • 最后,再用 barrierAction 用这些线程的计算结果,计算出整个 Excel 的日均银行流水
4. CyclicBarrier 和 CountDownLatch 的区别
特性CountDownLatchCyclicBarrier
使用次数计数器,只能使用一次计数器提供 reset 功能,可以多次使用
等待方式一个或多个线程等待其他多个线程完成某件事情之后才能执行多个线程互相等待,直到到达同一个同步点,再继续一起执行
侧重点“一个线程(多个线程)等待”,其他的 N 个线程在完成”某件事情”之后,可以终止,也可以等待多个线程,在任意一个线程没有完成,所有的线程都必须等待
本质计数器,线程完成一个记录一个,计数不是递增而是递减更像是一个阀门,需要所有线程都到达,阀门才能打开,然后继续执行

3. 线程安全

1. 悲观锁(synchronized、ReentrantLock 实现)

特点:总是假设最坏的情况,每次去写数据的时候都认为别人会在此期间修改,所以每次在写数据的时候都会上锁,这样别人想读写这个数据就会阻塞直到它拿到锁。

适用场景:多写场景,冲突一般较多

(1)synchronized

已在前面详细介绍。

(2)ReentrantLock(Java 实现)

原理:

  • 该类是基于 AQS 模板的,基于类的 API
  • 有公平锁和非公平锁之分
  • 它和 synchronized 都是可重入锁

与 synchronized 用法差不多,但是新加了几个特性:

  • 等待可中断
  • 公平锁
  • 锁绑定多个条件

在并发量高的时候,JDK 1.6 之前的 synchronized 要落后于 ReentrantLock,但是在 JDK 1.6 之后就基本持平。


2. 乐观锁(CAS 算法、版本号机制实现)

特点:总是假设最好的情况,每次去读数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新。

适用场景:多读场景,冲突一般较少

(1)CAS 算法

已在前面详细介绍。

(2)版本号机制

一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数,当数据被修改时,version 值会加一。

工作流程:

  • 当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值
  • 在提交更新时,若刚才读取到的 version 值为当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功

3. CAS 和 synchronized 的使用场景

简单的来说:

  • CAS 适用于写比较少的情况下(多读场景,冲突一般较少)
  • synchronized 适用于写比较多的情况下(多写场景,冲突一般较多)

详细对比:

  • 对于资源竞争较少(线程冲突较轻)的情况,使用 synchronized 同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗 CPU 资源;而 CAS 基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能

  • 对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized


4. 可重入锁与不可重入锁

  • 可重入锁:自己这个线程可以在不释放锁的前提下多次获取这个锁,state 自增 1
  • 不可重入锁:不可以有上面这样的操作

4. JDK 并发容器

  1. ConcurrentHashMap
  2. CopyOnWriteArrayList
  3. ConcurrentLinkedQueue
  4. BlockingQueue
  5. ConcurrentSkipListMap

5. 线程池

1)线程池工作流程

线程池的工作流程如下:

  1. 提交任务:当有新任务提交到线程池时
  2. 判断核心线程:
    • 如果当前运行的线程数 < corePoolSize,则创建新的核心线程来执行任务(即使有其他线程处于空闲状态)
    • 如果当前运行的线程数 ≥ corePoolSize,进入下一步判断
  3. 判断任务队列:
    • 如果任务队列(workQueue)未满,则将任务添加到队列中等待执行
    • 如果任务队列已满,进入下一步判断
  4. 判断最大线程数:
    • 如果当前运行的线程数 < maximumPoolSize,则创建新的非核心线程来执行任务
    • 如果当前运行的线程数 ≥ maximumPoolSize,进入下一步
  5. 执行拒绝策略:
    • 如果线程池已达到最大线程数且队列已满,则执行饱和策略(handler)来处理无法执行的任务

线程回收机制:

  • 当线程池中的线程数量 > corePoolSize 时,如果某个线程的空闲时间超过 keepAliveTime,该线程就会被终止并从线程池中移除
  • 核心线程通常不会被回收(除非设置了 allowCoreThreadTimeOut)

2)线程池参数

ThreadPoolExecutor 3 个最重要的参数:

  • corePoolSize:线程池的核心线程数量。核心线程数定义了最小可以同时运行的线程数量
  • maximumPoolSize:线程池的最大线程数。当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数
  • workQueue:任务队列。当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,任务就会被存放在队列中

ThreadPoolExecutor 其他常见参数:

  • keepAliveTime:当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime 才会被回收销毁
  • unit:keepAliveTime 参数的时间单位
  • threadFactory:线程工厂,创建新线程的时候会用到
  • handler:饱和策略,常见的饱和策略:
    • AbortPolicy:直接抛出异常
    • CallerRunsPolicy:只用调用者所在线程来运行任务
    • DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务
    • DiscardPolicy:不处理,丢弃掉

构造方法签名:

1
2
3
4
5
6
7
8
9
public ThreadPoolExecutor(
    int corePoolSize,              // 线程池的核心线程数量
    int maximumPoolSize,           // 线程池的最大线程数
    long keepAliveTime,            // 当线程数大于核心线程数时,多余的空闲线程存活的最长时间
    TimeUnit unit,                 // 时间单位
    BlockingQueue<Runnable> workQueue,  // 任务队列,用来储存等待执行任务的队列
    ThreadFactory threadFactory,   // 线程工厂,用来创建线程,一般默认即可
    RejectedExecutionHandler handler    // 拒绝策略
)

3)线程池的好处

  1. 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗
  2. 提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行
  3. 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配、调优和监控

4)线程池使用的一些方法对比

Runnable 和 Callable 的对比
特性RunnableCallable
返回值没有返回值有返回值
异常处理无法抛出异常可以抛出异常
引入版本Java 1.0Java 1.5
使用场景被线程执行计算得出结果时 return
execute() 和 submit() 的对比(向线程池提交任务)
方法说明
execute()用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功
submit()用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Future.get() 方法来获取返回值,get() 方法会阻塞当前线程直到任务完成,而使用 get(long timeout, TimeUnit unit) 方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完
shutdown() 和 shutdownNow() 的对比(关闭线程池)
方法说明
shutdown()关闭线程池,线程池的状态变为 SHUTDOWN。线程池不再接受新任务了,但是队列里的任务得执行完毕
shutdownNow()关闭线程池,线程的状态变为 STOP。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List
isTerminated() 和 isShutdown()
方法说明
isShutDown()当调用 shutdown() 方法后返回为 true
isTerminated()当调用 shutdown() 方法后,并且所有提交的任务完成后返回为 true

6. 原子类(CAS + volatile 实现)

基本类型

使用原子的方式更新基本类型:

  • AtomicInteger:整型原子类
  • AtomicLong:长整型原子类
  • AtomicBoolean:布尔型原子类

数组类型

使用原子的方式更新数组里的某个元素:

  • AtomicIntegerArray:整型数组原子类
  • AtomicLongArray:长整型数组原子类
  • AtomicReferenceArray:引用类型数组原子类

三、面试题

1. 并行和并发的区别

概念说明
并发同一时间段,多个任务都在执行(单位时间内不一定同时执行)
并行单位时间内,多个任务同时执行

2. 进程、线程、协程

概念说明
进程程序在一个数据集合上的运行过程,它是系统进行资源调度和分配的基本单位
线程线程是进程的一个实体,是被系统独立调度和分配 CPU 的基本单位
协程在一个线程内,协程是比线程更轻量级的存在。不受操作系统管理,完全由程序来控制。因为它不像线程一样需要来回切换,所以可以大大提升性能。协程在共享资源的时候也不加锁,通过状态来判定,所以效率很高。和方法不一样,可以在执行过程中中断转向别的协程

3. 多线程能够带来哪些问题?

并发编程的目的就是为了能提高程序的执行效率、提高程序运行速度,但是并发编程并不总是能提高程序运行速度的。

并发编程可能会遇到很多问题,比如:

  • 内存泄漏
  • 上下文切换
  • 死锁
  • 受限于硬件和软件的资源闲置问题

4. sleep() 和 wait() 的区别

特性sleep()wait()
释放锁没有释放锁释放了锁
使用场景通常被用于暂停执行通常被用于线程间交互/通信
唤醒方式执行完成后,线程会自动苏醒。或者可以使用 sleep(long timeout) 超时后线程会自动苏醒被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。或者可以使用 wait(long timeout) 超时后线程会自动苏醒
所属类Thread 类的静态方法Object 类的实例方法

⚠️ 最主要的区别:sleep 方法没有释放锁,而 wait 方法释放了锁


四、线程实战

1. Thread 类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Thread extends Object implements Runnable {
    public Thread();                        // 构造方法
    public Thread(String name);             // name 指定线程名
    public Thread(Runnable target);         // target 指定线程的目标对象,接口类型
    public Thread(Runnable target, String name);
    
    public void run();                      // 线程运行方法,实现 Runnable 接口
    public final String getName();          // 返回线程名字
    public final void setName(String name); // 设置线程名字
    public static int activeCount();        // 当前活动线程数
    public static Thread currentThread();   // 返回当前执行的线程
    public synchronized void start();       // 启动线程对象
}

2. 实现线程的几种方式

方式 1:直接继承 Thread 类

1
2
3
4
5
6
7
8
9
10
class MyThread extends Thread {
    @Override
    public void run() {
        // 线程执行的代码
    }
}

// 使用
MyThread t = new MyThread();
t.start();

方式 2:实现 Runnable 接口

1
2
3
4
5
6
7
8
9
10
class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 线程执行的代码
    }
}

// 使用
Thread t = new Thread(new MyRunnable());
t.start();

方式 3:使用匿名内部类

1
2
3
4
5
6
new Thread(new Runnable() {
    @Override
    public void run() {
        // 线程执行的代码
    }
}).start();

方式 4:使用 Lambda 表达式(Java 8+)

1
2
3
new Thread(() -> {
    // 线程执行的代码
}).start();

3. 休眠线程、守护线程、加入线程、礼让线程

休眠线程(Sleep)

1
Thread.sleep(10000); // 休眠 10 秒

守护线程(Daemon)

1
2
3
4
5
Thread t1 = new Thread(() -> {
    // 线程执行的代码
});
t1.setDaemon(true); // 设置为守护线程
t1.start();

加入线程(Join)

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
public static void main(String[] args) {
    final Thread t1 = new Thread(new Runnable() {
        public void run() {
            for (int i = 0; i < 2; i++) {
                System.out.println("aaaaaaaaaaaaaa");
            }
        }
    });

    Thread t2 = new Thread(new Runnable() {
        public void run() {
            for (int i = 0; i < 10; i++) {
                if (i == 2) {
                    try {
                        t1.join();        // 插队到 t1 执行完
                        // t1.join(10);   // 插队 10 毫秒
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("bbb");
            }
        }
    });
    
    t1.start();
    t2.start();
}

礼让线程(Yield)

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
    Thread t1 = new Thread(new Runnable() {
        public void run() {
            for (int i = 0; i < 2; i++) {
                Thread.yield(); // 礼让,让其他线程先执行
            }
        }
    });
    t1.start();
}

4. 设置优先级

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) {
    final Thread t1 = new Thread(new Runnable() {
        public void run() {
            // 线程 1 执行的代码
        }
    });
    
    Thread t2 = new Thread(new Runnable() {
        public void run() {
            // 线程 2 执行的代码
        }
    });
    
    t1.setPriority(Thread.MAX_PRIORITY);  // 设置最高优先级
    t2.setPriority(Thread.MIN_PRIORITY);  // 设置最低优先级
    
    t1.start();
    t2.start();
}

📚 参考资料:本文内容基于个人学习笔记整理

本文由作者按照 CC BY 4.0 进行授权