【从零开始学Java | 第四十一篇】深入多线程

目录

前言

一、线程的生命周期

二、线程的安全问题

1.什么是线程的安全问题

2.问题举例

三、解决线程的安全问题

1.同步代码块

2.同步方法

3.lock锁


前言

在上一篇博客中,已经掌握了如何创建和启动一个 Java 线程。但是,当成百上千个线程同时在系统中,如果不了解它们的生命周期,不掌握它们抢夺资源的规矩,那么将会造成巨大的灾难。

一、线程的生命周期

很多教程喜欢用操作系统的 5 种状态来解释 Java 线程,这其实容易造成混淆。在 Java 的世界里(具体来说是 java.lang.Thread.State 枚举中),官方明确定义了线程的 6 种状态

这 6 种状态的流转构成了线程的一生:

  1. NEW (新建状态)

    • 你刚 new 出来一个 Thread 对象,但还没有调用 start() 方法。此时它还只是内存里的一个普通 Java 对象,操作系统还没为它分配底层线程资源。
  2. RUNNABLE (可运行状态)

    • 调用了 start() 方法后进入此状态。注意,Java 中的 RUNNABLE 包含了操作系统的"就绪(Ready)"和"运行中(Running)"两种状态。处于这个状态的线程,可能正在 CPU 上狂奔,也可能正在排队等待操作系统的调度分配时间片。
  3. BLOCKED (阻塞状态)

    • 这是因为"锁"而停滞的状态。 当线程试图进入一个被 synchronized 关键字保护的代码块,但这个锁正被其他线程霸占着,它就会进入 BLOCKED 状态,在门外排队。
  4. WAITING (无限期等待状态)

    • 线程主动罢工,除非被别人唤醒,否则永远等下去 。通常是因为调用了 Object.wait()(不带超时时间)、Thread.join() 或者 LockSupport.park()。它在等另一个线程执行特定的操作(比如调用 notify())。
  5. TIMED_WAITING (限期等待状态)

    • WAITING 类似,但它有个结束时间。时间一到,就算没人叫它,它也会自己醒来。比如调用了 Thread.sleep(1000),它就会在这个状态待上 1000 毫秒。
  6. TERMINATED (终止/死亡状态)

    • 线程的 run() 方法正常执行完毕,或者因为发生未捕获的异常而意外终止。死亡的线程绝对不能再次调用 start()

二、线程的安全问题

1.什么是线程的安全问题

多个线程 同时访问同一个共享资源 (例如同一个对象的成员变量、同一个数据库记录),并且至少有一个线程在对该资源进行修改操作时,如果程序的最终执行结果偏离了我们的预期,这就是线程安全问题。

引发问题的核心原因有三个:

  • 可见性问题:一个线程修改了共享变量,另一个线程没能立刻看到最新值(CPU 缓存导致)。

  • 原子性问题 :一个看似简单的操作(如 count++),在底层其实分成了"读取-计算-赋值"三步,如果在这三步中间被其他线程插队,数据就乱了。

  • 有序性问题:编译器或 CPU 为了优化性能,擅自打乱了代码的执行顺序(指令重排)。

2.问题举例

需求:某电影院再卖票,共有100张票,而它有3个窗口卖票,设计一个程序模拟该电影院卖票。

首先定义一个卖票线程

java 复制代码
public class MyThread extends Thread{

    static int ticket = 0;
    @Override
    public void run() {
        while(true){
            if(ticket < 100){
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(getName() + "正在卖第" + ticket + "张票!");
                ticket ++;
            }else {
                break;
            }
        }
    }
}

可以看到尽管我们将ticket设置为静态变量,程序还是会卖同一张票,这是因为一个线程修改了共享变量,另一个线程没能立刻看到最新值。

三、解决线程的安全问题

1.同步代码块

就像给门票上了一把锁,规定同一时刻只能有一个线程进去执行卖票操作,其他线程必须在门外等待。

格式:

synchronized(锁){

操作共享数据的代码

}

特点1:锁默认打开,有一个线程进去了,锁自动关闭

特点2:里面的代码全部执行完毕,线程出来,锁自动打开

java 复制代码
public class MyThread extends Thread{

    static int ticket = 1;

    //锁对象一定要是唯一的
    static Object obj = new Object();
    @Override
    public void run() {
        while(true){
            synchronized (obj){
                if(ticket <= 100){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println(getName() + "正在卖第" + ticket + "张票!");
                    ticket ++;
                }else {
                    break;
                }
            }
        }
    }
}

细节1:synchronized锁必须在while(true)内部,因为如果在外面的话,如果窗口1抢夺到cpu资源,此时上锁,其他进程无法抢夺cpu,窗口1会在这个代码块内部一直执行,一直卖票,直到break,才会解锁,其他窗口才能进行抢夺cpu资源。

细节2:锁必须是唯一的,标志着锁住了一个共享的操作对象(数据库等)。

2.同步方法

就是把synchronized关键字加到方法上。

格式:

修饰符 synchronized 返回值类型 方法名(参数){......}

特点1:同步方法是锁住方法里面所有的代码

特点2:锁对象不能自己指定

非静态:this

静态:当前类的字节码文件对象

java 复制代码
public class MyRunnable implements Runnable{
    int ticket = 0;
    @Override
    public void run() {
        while(true){
            synchronized (MyRunnable.class){
                if (method()) break;
            }
        }
    }

    private synchronized boolean method() {
        if(ticket == 100){
            return true;
        }else {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket +"张票");
            ticket++;
        }
        return false;
    }
}

3.lock锁

虽然我们理解了同步代码块和同步方法中的锁对象,但是我们无法体会到上锁和解锁操作,因为这两个操作是系统帮我们实现的,为了更清晰的表达锁的概念,JDK5后提供了一个新的锁对象,可以通过**lock()unlock()**来获得锁和释放锁。

Lock只是一个接口,不能直接实例化,采用它的实现类ReenTrantLock来实例化

java 复制代码
public class MyThread extends Thread {

    static int ticket = 1;

    //锁对象一定要是唯一的
    static Object obj = new Object();
    static Lock lock = new ReentrantLock();
    @Override
    public void run() {
        while (true) {
//            synchronized (obj){
            lock.lock();
            try {
                if (ticket <= 100) {
                    Thread.sleep(100);
                    System.out.println(getName() + "正在卖第" + ticket + "张票!");
                    ticket++;
                } else {
                    break;
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();
            }
//            }
        }
    }
}
相关推荐
xuhaoyu_cpp_java1 小时前
MyBatis学习(一)
java·经验分享·笔记·学习·mybatis
wuxinyan1232 小时前
Java面试题50:Kubernetes 全栈知识体系之一
java·kubernetes·面试题
覆东流2 小时前
第7天:Python小项目
开发语言·后端·python
不吃肥肉的傲寒2 小时前
Graphify安装与结合claude code使用指南
java·python·ai编程·图搜索
seven97_top2 小时前
Tomcat的架构设计和启动过程详解
java·tomcat
qq_254617772 小时前
attribute((constructor)) 在C/C++中的应用
开发语言·c++
xyq20242 小时前
HTML5 Input 类型详解
开发语言
云深麋鹿2 小时前
C++ | 多态
开发语言·c++
我是无敌小恐龙2 小时前
Java SE 零基础入门 Day05 类与对象核心详解(封装+构造方法+内存+变量)
java·开发语言·人工智能·python·机器学习·计算机视觉·数据挖掘