
之前我们在这篇博客 【Java多线程】:理解线程创建、特性及后台进程 里面已经讲了多线程的基础内容了,现在就要面对多线程的最大问题了,让我们来看看吧
在Java中,任何对象都有生命周期,线程也不例外,它也有自己的生命周期。
public class ThreadState {
public static void main(String[] args) {
for (Thread.State state : Thread.State.values()) {
System.out.println(state);
}
}
}接下来针对线程生命周期中的6种基本状态分别进行详细讲解
(1)新建状态
(2)可运行状态
(3)锁阻塞状态
(4)无限等待状态
(5)计时等待状态
(6)被终止状态
注意:在程序中,通过一些操作,可以使线程在不同状态之间转换

🔥 程序中的多个线程是并发执行的,某个线程若想执行,就必须获得CPU的使用权。Java 虚拟机会按照特定的机制为程序中的每个线程分配CPU的使用权,这种机制被称作线程的调度。
在计算机中,线程调度有两种模型,分别是 分时调度模型 和 抢占式调度模型
🐸 在应用程序中,如果要对线程进行调度,最直接的方式就是设置线程的优先级
线程的优先级用1~10的整数表示,数字越大,优先级越高。除了可以直接使用数字表示线程的优先级,还可以使用Thread 类中提供的 3 个静态常量表示线程的优先级
优先级常量 | 功能描述 |
|---|---|
static int MAX_PRIORITY | 表示线程的最高优先级,值为 10 |
static int MIN_PRIORITY | 表示线程的最低优先级,值为 1 |
static int NORM_PRIORITY | 表示线程的默认优先级,值为 5 |
程序在运行期间,处于就绪状态的每个线程都有自己的优先级。
例如:主线程具有普通优先级。然而线程的优先级不是固定不变的,可以通过调用Thread 类的 setPriority(in newPriority)方法进行设置,该方法中的参数 newPriority 接收的是1~10 的整数或者 Thread类 的 3 个静态常量
下面演示不同优先级的两个线程在程序中的运行情况
class MaxPriority implements Runnable{
public void run(){
for(int i = 0; i < 3; i++){
System.out.println(Thread.currentThread().getName() + "正在输出: " + i);
}
}
}
class MinPriority implements Runnable{
public void run(){
for(int i = 0; i < 3; i++){
System.out.println(Thread.currentThread().getName() + "正在输出: " + i);
}
}
}
public class thread_two {
public static void main(String[] args) {
// 创建两个线程
Thread minPriority = new Thread(new MinPriority(), "优先级较低的线程");
Thread maxPriority = new Thread(new MaxPriority(), "优先级较高的线程");
minPriority.setPriority(Thread.MIN_PRIORITY);
maxPriority.setPriority(Thread.MAX_PRIORITY);
// 开启两个线程
maxPriority.start();
minPriority.start();
}
}
// 运行输出
优先级较高的线程正在输出: 0
优先级较低的线程正在输出: 0
优先级较低的线程正在输出: 1
优先级较低的线程正在输出: 2
优先级较高的线程正在输出: 1
优先级较高的线程正在输出: 2需要注意的是,虽然Java 提供了线程优先级,但是这些优先级需要操作系统的支持。不同的操作系统对优先级的支持是不一样的,操作系统中的线程优先级不会和Java中线程优先级一一对应。
🦋 线程休眠 指让当前线程暂停执行,从运行状态进入阻塞状态,将CPU资源让给其他线程的一种调度方式,可以调用线程的操作方法 sleep()实现线程休眠,sleep() 方法是 java.lang.Thread 类中定义的静态方法
如下:
public static void main(String[] args) {
Thread t = new Thread(() ->{
while (true) {
System.out.println("Hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
while (true) {
System.out.println("Hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}注意:sleep() 是静态方法,只能控制当前正在运行的线程休眠,而不能控制其他线程休眠。当休眠结束之后,线程就会返回 就绪状态,而不是立即开始执行,
🦌 线程插队 指将某个线程插入当前线程中,由两个线程交替执行变成两个线程顺序执行,即一个线程执行完毕之后再执行第二个线程,可以通过调用线程对象的 join() 方法实现线程插队。
假设有两个线程——线程甲和线程乙。
下面通过一个案例演示 join()方法在程序中的使用,如下:
class JoinRunnable implements Runnable{
public void run(){
for(int i = 1; i <= 3; i++){
System.out.println(Thread.currentThread().getName() + "输出:" + i);
}
}
}
public class thread_join {
public static void main(String[] args) throws InterruptedException{
Thread thread = new Thread(new JoinRunnable(), "Thead");
thread.start();
for(int i =1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + "输出: " + i);
if (i == 2) thread.join();
}
}
}
// 输出:
main输出: 1
main输出: 2
Thead输出:1
Thead输出:2
Thead输出:3
main输出: 3
main输出: 4
main输出: 5Thread 类不仅提供了无参数的线程插队方法 join() ,还提供了带有时间参数的线程插队方法 join(long millis)
同样是完成线程合并的操作,join() 和 join(long millis) 还是有区别的。
方法 说明 | 功能 |
|---|---|
public void join() x | 等待线程结束 |
public void join(long millis) | 等待线程结束,最多等 millis 毫秒 |
public void join(long millis, int nanos) | 同理,但可以更高精度 |
package NoteBook;
class JoinRunnable implements Runnable{
public void run(){
for(int i = 1; i <= 3; i++){
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "输出:" + i);
}
}
}
public class thread_join {
public static void main(String[] args) throws InterruptedException{
Thread thread = new Thread(new JoinRunnable(), "Thead");
thread.start();
for(int i =1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + "输出: " + i);
if (i == 2) thread.join(2000);
}
}
}
// 输出
main输出: 1
main输出: 2
Thead输出:1
main输出: 3
Thead输出:2
main输出: 4
main输出: 5
Thead输出:3在上面可以看到,当main线程执行到 i = 2 时,thread线程插队,优先于main线程执行。
🦁 线程让步 是指在某个特定的时间点,让线程暂停抢占CPU资源的行为,即从运行状态或就绪状态转到阻塞状态,从而将CPU资源让给其他线程使用。
下面通过一个案例演示yield()方法在程序来实现线程让步,如下:
class YieldThread extends Thread{
public YieldThread(String name){
super(name);
}
public void run() {
for(int i = 1; i < 5; i++){
System.out.println(Thread.currentThread().getName() + "---" + i);
if(i == 2){
System.out.print("线程让步: ");
Thread.yield();
}
}
}
}
public class thread_yield {
public static void main(String[] args) {
Thread thread1 = new YieldThread("thread1");
Thread thread2 = new YieldThread("thread2");
thread1.start();
thread2.start();
}
}
// 运行结果如下:(答案不固定)
thread1---1
thread1---2
线程让步: thread2---1
thread2---2
线程让步: thread1---3
thread2---3
thread2---4
thread1---4小提示:yield()方法的弊端
这里介绍的线程中断是指在线程执行过程中通过手动操作停止该线程
在Java中执行线程中断有如下两个常用方法:
public void interrupt()。
public boolean isInterrupted()下面通过案例来演示不同生命周期状态下的线程中断
public class thread_interrupt {
public static void main(String[] args){
// 情况一:实例化线程,但是未启动
Thread t = new Thread();
t.interrupt();
System.out.println("未启动线程是否中断 --- " + t.isInterrupted());
// 情况二:线程运行
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0; i < 5; i++){
if(i == 2) {
Thread.currentThread().interrupt();
System.out.println("thread 线程是否中断 ---" + Thread.currentThread().isInterrupted());
}
}
}
});
thread.start();
}
}
// 运行
未启动线程是否中断 --- true
thread 线程是否中断 ---true方法 | 说明 |
|---|---|
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用 Runnable 对象创建线程对象 |
Thread(String name) | 创建线程对象,并命名 |
Thread(Runnable target, String name) | 使用 Runnable 对象创建线程对象,并命名 |
【了解】Thread(ThreadGroup group, Runnable target) | 线程可以被用来分组管理,分好的组即为线程组,这 个目前我们了解即可 |
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");有些属性之前已经讲过,我们这里只是做个小结
属性 | 获取方法 |
|---|---|
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
💫 之前讲过,多线程的并发执行可以提高程序的效率。但是,当多个线程访问共享资源时,也会引发一些安全问题。
还记得我们之前学的 join嘛,下面我们来演示一个例子,如下:
public class Test {
private static int cnt = 0;
public static void main(String[] args) throws InterruptedException{
Object locker = new Object();
Thread t1 = new Thread(() ->{
// 对 count 变量自增 5w 次
for(int i = 0; i < 50000; i++) cnt++;
});
Thread t2 = new Thread(() ->{
// 对 count 变量自增 5w 次
for(int i = 0; i < 50000; i++) cnt++;
});
t1.start();
t2.start();
// 如果没有这个 join,肯定不行,线程还没有自增完,就开始打印,就会导致打印的 cnt 为0
t1.join(); //线程随机调度,如果将 t1.join 放在 start 之后,就会使得调度混乱
t2.join();
// 预期结果应该是 10 w
System.out.println("cnt最后自增的结果是: " + cnt);
}
}运行结果如下:

最后结果并不是我们想象的 10 w,因此就会用到 synchronized 的关键字,我们来学习一下吧
🐅 通过上面可以了解到,线程安全问题其实就是由多个线程同时处理共享资源所导致的。
使用synchronized关键字创建同步代码块的语法格式如下:
synchronized(lock){
处理共享资源的代码块
}解释:
需要加锁来保证为 10 w,因此代码修改如下:

注意:
🐻 同步代码块可以有效解决线程安全问题,当把共享资源的操作放在同步代码块中时,便为这些操作加了同步锁。
synchronized 关键字除了修饰代码块,同样可以修饰方法,被synchronized 关键字修饰的方法称为同步方法。
synchronized 关键字修饰方法的语法格式如下:
synchronized 返回值类型 方法名([参数列表]) {}代码修改如下:
class Counter{
public int count;
synchronized public void increase(){
count++;
}
public void increase2(){ //上下方法等价,上面是下面方法的简化
synchronized (this){
count++;
}
}
}
public class Demo14 {
public static void main(String[] args) throws InterruptedException{
Counter counter = new Counter();
Thread t1 = new Thread(() ->{
for(int i = 0; i < 50000; i++){
counter.increase();
}
});
Thread t2 = new Thread(() ->{
for(int i = 0; i < 50000; i++){
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}这个时候也实现了和同步代码块一样的效果。
多学一招:同步方法的锁
有这样一个场景:一个中国人和一个美国人在一起吃饭,美国人拿了中国人的筷子,中国人拿了美国人的刀叉,两个人开始争执不休:
结果可想而知,两个人都吃不成饭。
这个例子中的中国人和美国人相当于不同的线程,筷子和刀又就相当于锁。两个线程在运行时都在等待对方的锁,这样便造成了程序的停滞,这种现象称为 死锁
public class DeadLocker {
private static Object locker1 = new Object();
private static Object locker2 = new Object();
// 死锁案例:
public static void main(String[] args) {
Thread t1 = new Thread(() ->{
synchronized (locker1){
// 此处的 sleep 很重要,要确保 t1 和 t2 都分别拿到一把锁之后再进行后续操作
try{
Thread.sleep(1000);
} catch (InterruptedException e){
e.printStackTrace();
}
synchronized (locker2){
System.out.println("t1 加锁成功");
}
}
});
Thread t2 = new Thread(() ->{
synchronized (locker2){
try{
Thread.sleep(1000);
} catch (InterruptedException e){
e.printStackTrace();
}
synchronized (locker1){
System.out.println("t2 加锁成功");
}
}
});
t1.start();
t2.start();
}
}运行输出如下:

原因:两个线程都需要对方占用的资源,但是都无法释放自己的锁,于是两个线程都处于挂起状态,因此造成了上面看到的死锁
解决办法如下:
public class DeadLocker {
private static Object locker1 = new Object();
private static Object locker2 = new Object();
// 解决死锁的方法:
// 方式一:不构成嵌套即可
public static void main(String[] args) {
Thread t1 = new Thread(() ->{
synchronized (locker1){
// 此处的 sleep 很重要,要确保 t1 和 t2 都分别拿到一把锁之后再进行后续操作
try{
Thread.sleep(1000);
} catch (InterruptedException e){
e.printStackTrace();
}
}
synchronized (locker2){
System.out.println("t1 加锁成功");
}
});
Thread t2 = new Thread(() ->{
synchronized (locker2){
try{
Thread.sleep(1000);
} catch (InterruptedException e){
e.printStackTrace();
}
}
synchronized (locker1){
System.out.println("t2 加锁成功");
}
});
t1.start();
t2.start();
}
//方式二:约定先用 locker1,后用locker2
public static void main3(String[] args) {
Thread t1 = new Thread(() ->{
synchronized (locker1){
// 此处的 sleep 很重要,要确保 t1 和 t2 都分别拿到一把锁之后再进行后续操作
try{
Thread.sleep(1000);
} catch (InterruptedException e){
e.printStackTrace();
}
synchronized (locker2){
System.out.println("t1 加锁成功");
}
}
});
Thread t2 = new Thread(() ->{
synchronized (locker1){
try{
Thread.sleep(1000);
} catch (InterruptedException e){
e.printStackTrace();
}
synchronized (locker2){
System.out.println("t2 加锁成功");
}
}
});
t1.start();
t2.start();
}
}解决死锁的方法主要有以下几种:
🦋 重入锁(ReentrantLock)的作用类似于synchronized 关键字,synchronized 关键字是通过Java虚拟机实现的,而重入锁通过JDK实现。
重入锁的使用格式如下:
private ReentrantLock reentrantLock = new ReentrantLock();
//加锁
reentrantLock.lock();
//需要锁的数据
//释放锁修改后的代码如下:
package NoteBook;
import java.util.concurrent.locks.ReentrantLock;
class Counter {
public int count;
// 使用 ReentrantLock 代替 synchronized
private final ReentrantLock lock = new ReentrantLock();
public void increase() {
// 使用 lock 进行加锁
lock.lock();
try {
count++;
} finally {
lock.unlock(); // 保证释放锁
}
}
// 静态方法使用 ReentrantLock 来代替 synchronized
private static final ReentrantLock staticLock = new ReentrantLock();
public static void increase3() {
// 使用静态锁来替代 synchronized
staticLock.lock();
try {
// 静态方法的同步内容
} finally {
staticLock.unlock(); // 保证释放静态锁
}
}
public static void increase4() {
// 使用锁住 Counter.class 对象来代替 synchronized
staticLock.lock();
try {
// 静态方法的同步内容
} finally {
staticLock.unlock(); // 保证释放锁
}
}
}
public class thread_reentrant {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
// 创建两个线程,分别执行 increase 方法
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count); // 输出最终结果
}
}从上面可以看出, 运行结果 和 使用 synchronized 结果一致。如果需要在此基础添加几把锁,只需要调用 lock() 方法即可。 需要注意的是,使用重入锁是加了几把锁就必须释放几把锁,否则会使线程处于阻塞状态
为什么 ReentrantLock 更灵活?
结论:
通过将 synchronized 替换为 ReentrantLock,我们实现了与原来代码相同的线程安全效果,只不过是显式控制加锁和释放锁的过程。这样做可以提高代码的可读性和灵活性,尤其在复杂并发操作中,ReentrantLock 提供了更多控制选项。