一.认识线程

1.1什么是线程

线程是操作系统能够进行运算调度的最小单位,被包含在进程之中,是进程中的实际运作单位。一个进程可以包含多个线程,所有线程共享进程的资源(如内存、文件描述符等),但每个线程拥有独立的程序计数器、栈和寄存器组。

⼀个线程就是⼀个 “执行流”. 每个线程之间都可以按照顺序执行自己的代码. 多个线程之间 “同时” 执行着多份代码,main()⼀般被称为主线程(Main Thread)。

1.2为啥要有线程(线程的作用与优势)

线程是操作系统能够进行运算调度的最小单位,被包含在进程之中,是进程中的实际运作单位。线程的出现主要是为了提高程序的执行效率和资源利用率。

1.2.1提高程序并发性

线程允许程序在同一时间内执行多个任务。通过将任务分解为多个线程,可以充分利用多核处理器的计算能力。例如,一个网络服务器可以同时处理多个客户端请求,每个请求由一个独立的线程处理。

1.2.2资源共享与通信效率

同一进程内的多个线程共享进程的内存空间和资源,线程间的通信比进程间通信更高效。共享内存使得数据交换无需通过复杂的IPC机制,减少了系统开销。

1.2.3响应性与用户体验(javaEE进阶后面会讲到)

在图形用户界面(GUI)应用中,主线程负责界面渲染,而工作线程处理耗时操作(如文件读写或网络请求)。这种分工避免了界面冻结,提升了用户体验。

1.2.4资源开销较低(重点理解)

创建和销毁线程比进程更轻量级,就是更快。线程的上下文切换成本低于进程切换,因为线程共享相同的地址空间和系统资源。

1.2.5简化复杂任务设计

多线程模型可以将复杂任务分解为多个并行执行的子任务。例如,数据处理任务可以分割为多个线程并行处理,再合并结果,显著缩短总执行时间。

1.3线程与进程的区别

进程是操作系统资源分配的基本单位,拥有独立的地址空间、文件描述符、系统资源等PCB属性。每个进程运行在独立的内存空间中,相互隔离,一个进程崩溃通常不会影响其他进程。

线程是CPU调度的基本单位,属于进程的一部分,共享同一进程的资源(如内存、文件等)。一个进程可以包含多个线程,这些线程共享进程的地址空间和资源。

根据以上定义,就是说进程是包含线程的,一个进程至少有一个线程,就是主线程,并且一个进程中只有一个主线程。

值得注意的是进程与进程之间不共享内存空间,同一个进程的线程之间共享同一片内存空间。

进程是系统分配资源的最小单位,线程是系统调度的最小单位。

1.3.1资源占用与开销

进程需要独立的内存空间和系统资源,创建和销毁进程的开销较大。上下文切换时,需要保存和恢复整个进程的状态(如内存映射、寄存器等),效率较低。

线程共享进程的资源,创建和销毁线程的开销较小。上下文切换时只需保存线程的寄存器状态,效率更高。但线程间共享资源可能导致竞争条件,需要同步机制(如锁)来避免冲突。

1.3.2稳定性与隔离性

进程具有更强的隔离性,一个进程崩溃不会直接影响其他进程。适合需要高稳定性的场景,如服务端程序。

线程共享进程资源,一个线程崩溃可能导致整个进程退出。调试多线程程序更复杂,需处理竞态条件和死锁问题。

也就是说一个进程出问题了,不会影响其他进程,所以说进程具有较强的隔离性,较稳定。

但是一个线程出问题,那么该线程所在的进程就可能出问题,导致整个进程崩溃,所以说线程隔离性较弱,不稳定。

1.3.3适用场景

进程适合需要高隔离性、独立运行的任务,例如浏览器中每个标签页作为独立进程运行,避免单个页面崩溃影响整个浏览器。

线程适合需要高效协作、频繁通信的任务,例如Web服务器用多线程处理并发请求,共享资源以减少开销。

二.创建线程

方法一.继承Thread类

//创建一个新的类,让这个类继承标准库的Thread类
class MyThread extends Thread{
    //重写Thread类中的run方法
    @Override
    public void run(){
        while(true){
            System.out.println("hello thread");
            //休息1000ms==1s
            //使用sleep方法时会发生中断异常
            try{
                Thread.sleep(1000);
            }catch(InterruptedException e){
                throw new RuntimeException(e);
            }
        }
    }
}
public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        //创建一个类实例
        Thread t=new MyThread();
        //启动线程
        t.start();
        //若执行下面代码,线程将不会开启,只会执行run方法
        //t.run();

        while(true){
            System.out.println("hello main");
            //sleep是静态方法,只能是Thread自己调用,对象不能调用
            Thread.sleep(1000);
        }
    }
}

方法二.实现Runnable接口

class MyRunnable implements Runnable {
    @Override
    public void run() {
        while (true) {
            System.out.println("hello thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

public class Demo2 {
    public static void main(String[] args) throws InterruptedException {
        MyRunnable r = new MyRunnable();
        Thread t = new Thread(r);
        t.start();

        while (true) {
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

方法三.匿名内部类创建Thread子类对象


public class Demo3 {
    //创建匿名内部类来创建一个线程
    public static void main(String[] args) throws InterruptedException{
        Thread t=new Thread(){
            @Override
            public void run(){
                while(true){
                    System.out.println("hello thread");
                    try{
                        Thread.sleep(1000);
                    }catch(InterruptedException e){
                        throw new RuntimeException();
                    }
                }
            }
        };
        //记住,调用start才是真正创建线程
        while(true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

方法四.匿名内部类创建Runnable子类对象

public class Demo4 {
    public static void main(String[] args) throws InterruptedException {
//        Runnable runnable=new Runnable() {
//            @Override
//            public void run() {
//
//            }
//        };
//        Thread t=new Thread(runnable);

        Thread t=new Thread(new Runnable() {
            @Override
            public void run() {
                while(true){
                    System.out.println("hello thread");
                    try{
                        Thread.sleep(1000);
                    }catch(InterruptedException e){
                        throw new RuntimeException(e);//运行时异常
                    }
                }
            }
        });
        t.start();//开启线程

        while(true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

方法五.lambda表达式创建Runnable子类对象(最为常见)


public class Demo5 {
    //用lammbaa表达式创建一个线程
    public static void main(String[] args)  throws InterruptedException{
        Thread t=new Thread(()->{
           //里面的内容相当于run方法里面的内容
           while(true){
               System.out.println("hello thread");
               try{
                   Thread.sleep(1000);
               }catch(InterruptedException e){
                   throw new RuntimeException(e);//运行时异常
               }
           }
        });
        //上面的都是子线程

        //下面的是主线程
        t.start();
        while(true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

使用lambda表达式创建多个线程


public class Demo6 {
    //使用lambaa表达式创建多个线程
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(()->{
           while(true){
               System.out.println("hello thread1");
               try{
                   Thread.sleep(1000);
               }catch(InterruptedException e){
                   throw new RuntimeException(e);
               }
           }
        },"线程1");
        Thread t2=new Thread(()->{
            while(true){
                System.out.println("hello thread2");
                try{
                    Thread.sleep(1000);
                }catch(InterruptedException e){
                    throw new RuntimeException(e);
                }
            }
        },"线程2");
        Thread t3=new Thread(()->{
            while(true){
                System.out.println("hello thread3");
                try{
                    Thread.sleep(1000);
                }catch(InterruptedException e){
                    throw new RuntimeException(e);
                }
            }
        },"线程3");

        t1.start();
        t2.start();
        t3.start();

        while(true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
        //接下来上面的四个语句会交替执行
    }
}

三.Thread类及其方法

Thread 类是 JVM ⽤来管理线程的⼀个类,换句话说,每个线程都有⼀个唯⼀的 Thread 对象与之关
联。
⽤我们上⾯的例⼦来看,每个执⾏流,也需要有⼀个对象来描述,类似下图所⽰,⽽ Thread 类的对象
就是⽤来描述⼀个线程执⾏流的,JVM 会将这些 Thread 对象组织起来,⽤于线程调度,线程管理。

3.1Thread类常见的方法

方法 说明
Thread() 创建线程对象
Thread(Runnable target)

使用Runnable对象创建线程对象

Thread(String name) 创建线程对象,并命名
Thread(Runnable target,String name) 使用Runnable对象创建xian'cheng
Thread t1=new Thread();
Thread t2=new Thread(new Runnable());
Thread t3=new Thread("这是我的名字);
Thread t4=new Thread(new Runnable(),"这是我的名字");

3.2Thread类常见的属性

ID 是线程的唯⼀标识,不同线程不会重复
名称是各种调试⼯具⽤到
状态表⽰线程当前所处的⼀个情况,下⾯我们会进⼀步说明
优先级⾼的线程理论上来说更容易被调度到
关于后台线程,需要记住⼀点:JVM会在⼀个进程的所有⾮后台线程结束后,才会结束运⾏。
是否存活,即简单的理解,为 run ⽅法是否运⾏结束了

3.3如何获取当前线程的引用

方法 说明
public static Thread currentThread() 返回当前线程对象引用

 public class ThreadDemo {
        public static void main(String[] args) {
                Thread thread = Thread.currentThread();
                System.out.println(thread.getName());
        }
}

3.4如何休眠当前线程

也是我们⽐较熟悉⼀组⽅法,有⼀点要记得,因为线程的调度是不可控的,所以,这个⽅法只能保证
实际休眠时间是⼤于等于参数设置的休眠时间的。
public class ThreadDemo {
        public static void main(String[] args) throws InterruptedException {
               //throws InterruptedException是因为sleep()方法可能会中断
                  System.out.println(System.currentTimeMillis());
                    Thread.sleep(3 * 1000);
                  System.out.println(System.currentTimeMillis());
         }
}

3.5线程中断


    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            Thread cur = Thread.currentThread();
            System.out.println("子线程启动,准备循环执行任务...");

            // 核心:用 isInterrupted() 控制循环
            while (!cur.isInterrupted()) {
                System.out.println("hello thread");
                try {
                    // 模拟长时间任务/阻塞操作
                    Thread.sleep(1000); // 改成1秒方便测试
                } catch (InterruptedException e) {
                    // 1. 收到中断信号,打印日志
                    System.out.println("子线程收到中断信号,准备退出...");

                    // 2. 关键:恢复中断标志位(因为sleep被中断时会自动清除标志位)
                    cur.interrupt();

                    // 3. 释放资源、收尾工作(比如关闭文件、网络连接等)
                    System.out.println("正在释放资源...");
                    // 这里可以写你的清理逻辑

                    // 4. 跳出循环,优雅退出
                    //不需要break,因为此时循环条件已经为假了
                }
            }

            // 循环结束,线程收尾
            System.out.println("子线程已安全退出 ✅");
        });

下面还有一种写法,不推荐

public static void main(String[] args) {
        Thread t=new Thread(()->{
            Thread cur=Thread.currentThread();//获取当前线程的引用
            while(!cur.isInterrupted()){
                System.out.println("hello thread");
                try{
                    Thread.sleep(1000000);
                }catch(InterruptedException e){
                    //throw new RuntimeException(e);
                    //0
                     e.printStackTrace();
                    //退出之前做一些释放资源工作
                }
            }
        });

上面这个代码Thread.sleep()被中断时的特殊行为:

线程在sleep()休眠时,如果被调用interrupt()会发生两件事:

1.立即抛出异InterruptionException异常

2.z自动清除线程的中断标记(把原来的true改成false)

所以上面的代码的循环不会停止,会一直打印"hello thread"

3.6操作系统管理线程的两个关键队列

就绪队列(Ready Queue)

就绪队列是操作系统中用于管理准备执行但尚未获得CPU资源的进程或线程的队列。这些进程已经具备运行条件,只需等待CPU调度器分配时间片即可执行。就绪队列通常采用优先级调度、时间片轮转等算法决定下一个运行的进程。

进程进入就绪队列的条件包括:新创建的进程完成初始化、阻塞状态的进程被唤醒(如I/O操作完成)、时间片用完的进程被重新调度等。操作系统通过维护就绪队列实现多任务的高效切换。

阻塞队列(Blocked Queue)

阻塞队列用于管理因等待某事件(如I/O完成、信号量释放等)而暂时无法执行的进程。这些进程会主动放弃CPU资源,直到外部条件满足后被唤醒。阻塞队列中的进程不参与CPU调度,直到其等待的事件发生。

进程进入阻塞队列的典型场景包括:请求未被满足的资源(如文件读写)、主动休眠(sleep)、等待同步信号(如锁或信号量)等。一旦事件完成,进程会被移至就绪队列重新等待调度。

核心区别

  • 状态依赖:就绪队列中的进程仅需CPU资源即可运行;阻塞队列中的进程需等待外部事件。
  • 调度参与:就绪队列参与CPU调度决策;阻塞队列不参与调度。
  • 转换方向:阻塞队列的进程被唤醒后进入就绪队列;就绪队列的进程可能因时间片耗尽或主动阻塞而离开。

例如,在Java的线程模型中,Runnable状态对应就绪队列,WAITINGBLOCKED状态对应阻塞队列。

结合这两个队列,解释一下sleep(0)和sleep()

sleep(0) 与 sleep() 的区别

sleep(0)
调用 sleep(0) 会主动让出当前线程的剩余时间片,但不会真正进入休眠状态。操作系统会立即重新调度该线程,可能继续执行当前线程(如果优先级最高),也可能切换到其他就绪线程。
适用于需要避免忙等待的场景,例如在自旋锁中短暂让出 CPU,就是说主动放弃CPU使用权。

sleep()
不带参数的 sleep() 通常会导致语法错误,因标准库中 sleep 需要明确的时间参数(如秒或毫秒)。例如 sleep(1) 会让线程休眠至少 1 秒。
休眠期间线程完全暂停,不参与 CPU 调度,直到指定时间结束或信号中断。

3.7等待一个线程-join()

有时,我们需要等待⼀个线程完成它的⼯作后,才能进⾏⾃⼰的下⼀步⼯作。例如,张三只有等李四
转账成功,才决定是否存钱,这时我们需要⼀个⽅法明确等待线程的结束。

public class Demo10 {
    private static long result=0;
    public static void main(String[] args) throws InterruptedException{
        Thread t=new Thread(()->{
            for (int i = 0; i <100 ; i++) {
                result+=i;
            }
            System.out.println("t线程计算完毕");
        });
        t.start();
        // 给 t 线程留有一定的执行时间.
        // Thread.sleep(1);
        // 使用 join 是更优的解法.
        // join 等待 t 线程结束.
        //由于线程的随机调度,可能for循环还没有走完就打印result
        //使用join等待t线程执行完才打印result
        t.join();
        System.out.println(result);
    }
}

四.线程的状态

4.1观察线程的所有状态

线程的状态是⼀个枚举类型 Thread.State
public class ThreadState {
      public static void main(String[] args) {
          for (Thread.State state : Thread.State.values()) {
                 System.out.println(state);
          }
     }
}
NEW: 安排了⼯作, 还未开始⾏动
RUNNABLE: 可⼯作的. ⼜可以分成正在⼯作中和即将开始⼯作.
BLOCKED: 这⼏个都表⽰排队等着其他事情
WAITING: 这⼏个都表⽰排队等着其他事情
TIMED_WAITING: 这⼏个都表⽰排队等着其他事情
TERMINATED: ⼯作完成了.

4.2下面的图会好理解一点

五.多线程带来的风险-线程安全问题(重点)

5.1观察线程不安全

public class Demo13 {
    private static int count = 0;

    //private static Object locker = new Object();
    //private static Object locker2 = new Object();

    public static void main(String[] args) throws InterruptedException {
        // 创建两个线程, 分别对同一个变量进行 5w 次的 ++ 操作.
        // 最终主线程打印结果.

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
               // synchronized (locker) {
                    count++;
                //}
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                //synchronized (locker) {
                    count++;
                //}
            }
        });
        //必须是多个线程共用同一把锁,互斥执行,一个一个来,才能保证线程安全
        t1.start();
        t2.start();
        // 让主线程等待, 等待上述的两个线程结束
        t1.join();
        t2.join();

        System.out.println("count = " + count);
    }
}

上面这个代码目的是求0到5000的整数依次两次相加,但结果一直都比正确结果要小。

这就是线程问题。

5.2线程安全问题:

一个进程的多个线程,共享同一份内存资源.如果两个线程都尝试修改同一个变量,就可能出现冲突,使代码出现Bug.

5.2.1为什么count++会出现问题

看似count++只进行了一步操作,但在CPU中每个操作都对应三个指令。

1.load:把内存的count加载到寄存器中

2.add:把寄存器里的数据+1

3.save:把寄存器里的数据写回数据

并且这三步操作不是原子操作!操作系统可能在任何一步打断线程,切换到另一个线程执行,就会导致冲突。

举个例子

时间点 线程t1在做什么 线程t2在做什么 内存里的count值
0 0(初始值)
1 load(count)->把内存里的0读到t1的寄存器里,寄存器值=0 0
2 CPU切到t2线程,t1线程被挂起 load(count)->把内存里的0读到t2的寄存器里,寄存器值=0 0
3 CPU切回t1 0
4 add->t1的寄存器值变成0+1=1 0
5 save->把t1线程寄存器里的1写回内存,内存里的count变为1 1
6 CPU切回t2 1
7 add->t2的寄存器里的值变成0+1=1(注意!他之前的load的值是0) 1
8 save->把t2寄存器里的1写回内存,内存里的count还是1

1

t2在t1还没把save写回内存的时候,就已经load了count的值

所以t2后续的add和save,都是基于它自己读到的0在操作,而不是t1写回后的1

最终两个线程都把1写回内存,相当于count++被合并成一次,结果就是1,而不是预期的2;

5.3线程安全的5大根源

5.3.1操作系统线程调度是随机的

1.线程什么时候上CPU,什么时候被切下来,完全有操作系统调度决定,程序员无法控制

2.刚才遇到的count++被打断,结果被覆盖,就是因为调度器在load/add/save三步中间,把CPU切给了另一个线程

3.这是线程安全问题的“土壤”,后面的问题都是在这个基础上发生的

5.3.2多个线程同时修改同一个共享变量

场景 会不会出问题 原因
单线程修改一个变量 没问题 没有竞争,不会被打断
多个线程修改不同变量 没问题 变量不共享,互不干扰
多线程同时读取统一变量 没问题 读取不会改变变量的值,不会冲突
多线程同时修改同一变量 出问题 这是线程安全问题的必要条件

5.3.3修改操作不是原子的(count++分三步,不是原子性的)

1.count++不是一条CPU指令,而是load->add->save三步操作

2.解决方法:用锁(synchronized/Lock)或者原子类(AtomicInteger),把这三步变成"原子操作",中间不被打断

5.3.4内存可见性问题(线程看不到其他线程的修改)

1.每个线程都有自己的工作内存(寄存器/缓存),修改变量时,会先写到自己的缓存里,再异步刷新到主内存

2.这就导致:线程A修改里变量的值,但线程B看不到,读到还是自己缓存里的旧值

3.解决方法:用volatile关键字修饰变量,强制线程每次都从主内存读,修改后立刻写回主内存,保证所有线程都能看到最新值

5.3.5指令重排(CPU/编译器打乱代码执行顺序)

这是CPU/编译器为了优化性能,会打乱代码执行顺序,只要逻辑结果不变就行了

看代码

   int a=1;
   int b=2;

CPU可能会先执行b=2,再执行a=1

单线程下结果没问题,但多线程下可能会出现意外情况

 1.线程A执行a=1和b=2,CPU重排之后,先写b=2

2.线程B看到b=2,以为a也已经被赋值了,结果读到的a还是默认值0

3.解决方法:volatile关键字也能禁止指令重排序,保证代码按你写的顺序执行

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐