Java多线程

发布于:2025-08-19 ⋅ 阅读:(15) ⋅ 点赞:(0)

我们先来聊聊什么是多线程:

1. 基本概念

  • 线程(Thread)线程是程序执行的最小单位。一个线程可以执行一段独立的代码,而多个线程可以并发运行。
  • 多线程(Multithreading):多线程是指程序中同时运行多个线程。Java通过Thread类和Runnable接口支持多线程编程。
  • 并发(Concurrency):多个线程同时运行,共享系统资源。并发可以提高程序的效率,尤其是在多核处理器上。
  • 并行(Parallelism):多个线程在多个处理器上同时运行。并行是并发的一种特例,要求硬件支持多核处理器。

进程指的是一个应用程序,而线程指的是一个进程中的执行场景,一个进程可以启动多个线程

Java中的程序至少拥有两个线程并发,一个是垃圾回收线程,另一个是主线程main方法就是主线程)

进程和线程之间还有什么关系?

进程:可以看成是一所学校。

线程:可以看成是学校里的某个老师。

进程就像是学校这个整体有自己的‘地盘’(内存空间)和运行计划,而线程如同老师,在学校这个大框架下进行具体的工作(比如授课,批改作业),多个老师(线程)能同时为学校(进程)的运转出力。

注意:在Java中,进程A和进程B的内存是完全独立且不共享的。


这就像两个独立的工厂(进程A和B),它们有各自的厂房(内存空间)、设备和原材料。工厂A里的东西(变量、数据等),工厂B是看不到也无法直接使用的,两者之间的内存完全隔离,互不干扰。

再比如:王者荣耀游戏和QQ音乐作为两个独立的进程,就像两个分开的“小世界”。王者运行时占用的内存、游戏数据(比如当前血量、装备信息),QQ音乐根本访问不到;同样,QQ音乐播放的音乐文件、音量设置等,王者也没法直接读取。它们各用各的资源,互不干扰,哪怕其中一个卡了或关闭了,另一个通常也能正常运行。

接下来我们来聊聊线程之间的关系

线程A和线程B,堆内存方法区内存是共享的,但他们的栈内存是各自独立的,一个线程一个栈。

这就相当于我们每一个线程都有自己独立的发挥空间,就像每个人都有自己的工位,我们可以在各自的工位上进行工作,而且我们多人工作是互不干扰的,这就是多线程并发。

还是以刚才的学校来举例:

我们学校的老师每天都要来上班打卡吧,这里我们学校还是进程,每位老师还是线程,每个老师在不同时刻都有着不同的事物要忙,老师A要批改作业,老师B要上课,老师C要开会,每个老师各忙各的,你不需要等我,我也不需要等你,使用多线程就能大大提高效率。

Java的多线程机制就是为了提高处理事务的效率而存在的。

主线程(main)结束后,其他线程会结束吗?答案是不会的,刚刚我们也提到了我们的线程之间是互不影响的,当我们的主线程结束后我们其他的线程任然可能还在压栈弹栈(这里的“压栈”和“弹栈”是线程执行过程中对方法调用栈的操作,简单理解就是:
 
 压栈:当线程执行一个新的方法时,会把这个方法的信息(比如参数、局部变量、返回地址等)“压”入自己的栈中,相当于在栈顶新增一层记录。
弹栈:当方法执行完毕后,这层信息会从栈顶“弹”出去,栈顶回到上一层方法的位置。

 
比如一个线程正在执行一连串方法(A→B→C),执行C时就是“压栈”到C;C执行完回到B,就是C“弹栈”;B执行完回到A,B再“弹栈”,直到所有方法执行完,栈为空,线程才结束。
 
所以主线程的main方法结束(主栈空了),但其他线程可能还在执行自己的方法(不断压栈、弹栈),因此整个进程未必会结束。)

对于单核CPU来说,真的可以做到多线程并发吗?

对于单核CPU来说,无法做到真正意义上的“同时并发”,因为单核CPU在同一时刻只能执行一个线程的指令。
 
但从用户感知或程序运行效果来看,它能通过一种“快速切换”的方式,模拟出多线程并发的效果:
CPU会给每个线程分配极短的时间片(比如毫秒级),执行一小段指令后,迅速切换到另一个线程继续执行。由于切换速度极快,人类感官无法察觉这种停顿,就会觉得多个线程在“同时”运行(比如一边听歌一边打字,感觉两者是并行的)。
 
简单说:单核CPU的“多线程并发”是“伪并发”,靠快速切换模拟同时性;而多核CPU才能实现多个线程真正在同一时刻并行执行

那什么是真正的多线程并发:

真正的多线程并发指的是多个线程在同一时间点真正同时执行,其核心是利用硬件的多核处理器资源,让不同线程在不同的CPU核心上并行运行,从而实现效率的提升。
 
关键特点:
 
- 硬件支持:依赖多核CPU,每个线程能分配到独立的核心,不存在“轮流占用”同一核心的情况。
- 并行执行:多个线程的指令在物理层面同时被处理,比如一个核心处理线程A,另一个核心同时处理线程B。
- 效率提升:对于CPU密集型任务(如复杂计算),能显著减少总耗时,因为任务被真正分配到多个核心同时推进。
 

关于线程会遇到的几种状态

1.新建状态(New):当线程对象被创建(如通过new Thread())但尚未用start()方法时,线程处于新建状态。此时线程还未开始执行,仅在内存中存在。

2.就绪状态(Runnable):调用start()方法后,线程进入就绪状态。此时线程已具备运行条件,但尚未获得cpu的执行权,等待操作系统的调度。

3.运行状态(Running):当就绪状态的线程获得CPU的资源后,开始执行run()方法中的代码,此时线程处于运行状态。只有处于此状态的线程才会真正执行任务。

4.阻塞状态(Blocked):在运行过程中,线程因某种原因(如调用了sleep(),等待锁,IO操作等)暂时停止执行,进入阻塞状态,当阻塞原因结束后,线程会重新进入就绪状态,等待再次获得CPU资源。

5.死亡状态(Terminated):当线程的run()方法执行完毕,或因异常退出run()方法时,线程进入死亡状态,处于此状态的线程无法再次被启动,调用start()会抛出异常。

什么是api:

其他大佬写好的一些类和方法

线程的构造方法:

我们这里不使用t.run而用t.start是因为使用t.run未能构成多线程结构,我们程序在运行到t.run时就会不停在这里循环打印hello Thread,而使用t.start我们就可以同时在循环t.run打印hello Thread的同时进行打印hello main的打印。

Thread对象和一个操作系统中的线程是一一对应的关系,所以一Thread对象只能start一次

Thread.sleep()方法

是 Java 中的一个方法调用,它属于 Thread 类,用于让当前线程暂停执行一段时间。具体来说,Thread.sleep 方法会让当前线程暂停(阻塞)指定的毫秒数(1000 毫秒等于 1 秒)。

在 IntelliJ IDEA 中,直接使用 Thread.sleep 时需要加上 try-catch 块,是因为 Thread.sleep 方法会抛出一个受检查的异常(InterruptedException)。Java 要求所有受检查的异常必须被显式处理,要么通过 try-catch 块捕获,要么通过 throws 声明抛出。

1

//创建一个新的类来继承标准库的Thread类
class mythread extends Thread {
    //重写父类的run方法

    @Override
    public void run() {
        while (true) {
            System.out.println("hello Thread");
            try {
                Thread.sleep(1000);//休息1000毫秒就是1秒sleep是Thread的静态方法
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
    public class demo1 {
        public static void main(String[] args) {
            //创建一个Thread实例
            Thread t = new mythread();
            //启动线程不是t.run
            t.start();//调用操作系统API再系统中创建一个线程,当创建好后,就会自动执行重写的run
            //主
            while (true) {
                System.out.println("hello main");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

2

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);
        }
    }
}

这两种写法,其实本质是一样的。

区别就是第二种写法更加的解耦合。

在第一种中要执行的任务和线程是绑定在一起的,如果要改成其他形式,那么就需要大规模的代码来实现。

而第二种并没有和线程绑定,使用可以非常方便的迁移到其他的载体上。


3.继承Thread的匿名内部类

public class demo3 {
    public static void main(String[] args) {
        //任务
        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(e);
                    }
                }
            }
        };
        //一定要调用start才是调用任务
        t.start();

        while(true){
            System.out.println("Thread main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

推荐方法lambda(本质是基于函数式接口的匿名内部类):

public class demo5 {
    public static void main(String[] args) {
        Thread t=new Thread(()->{
            while(true){
                System.out.println("hello Thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });//lambda表达式;
        t.start();
        while(true){
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

        }
    }
}

可在最后});这里给线程取名字

像这样用一个逗号加引号引出我们想取的名字。

.

前台线程结束表示整个进程结束,后台线程结束不会阻止整个进程结束

main中线程包括代码中手动创建的线程默认都是前台线程

public class demo7 {
    public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(()->{
            for(int i=0;i<5;i++){
                System.out.println("hello thread");//time为0时会打印第一个
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("t线程结束");
        });
        //在start之前,把t设定为后台线程(守护线程)
       t.setDaemon(true);//变成后台线程
        t.start();
        //主线程调用start就没啥东西了
        Thread.sleep(1000);
        System.out.println("主线程结束");//1秒后主线程结束
        System.exit(88345);//指定退出码
    }
}

这段代码使用了t.setDaemon把前台线程改为后台线程了,而后台线程在运行过后就不能再变为前台线程了。

输入数字让代码结束:

import java.util.Scanner;

public class demo8 {
    private static  boolean running1=true;//定义一个变量
    public static void main(String[] args) {
        //boolean running=true;
        Thread t = new Thread(() -> {
            while (running1) {
                System.out.println("hello Thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("t线程结束");
        });
        t.start();
        System.out.println("请输入数字0来结束");
        Scanner sc = new Scanner(System.in);
        int n=sc.nextInt();//输入数字执行
        if(n==0) {
            running1= false;
        }
    }
}

这段代码我们先定义了一个变量,之后通过输入数字来进行变量的true,false的改变来达到结束我们的t线程,又由于我们的main线程是没有什么代码在循环运行的,所以我们的主线程也会在t线程结束后结束。

在 Java 中,Thread.currentThread().isInterrupted() 默认返回 false

  1. 设置中断状态
    interrupt() 会将线程的中断状态标志设置为 true。可以通过 Thread.currentThread().isInterrupted() 方法检查这个标志

  2. 通过这个我们可以写出这样的代码,同样可以达到输入数字来结束t线程的效果:

import java.util.Scanner;

public class demo9 {
    public static void main(String[] args) {
        Thread t=new Thread(()->{
            while(!Thread.currentThread().isInterrupted()){//isInterrupted()判定是否是true还是false
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();
        Scanner sc=new Scanner(System.in);
        System.out.println("输入0表示让t结束");
        int n=sc.nextInt();
        if(n==0){
            t.interrupt();//interrupt会将线程的中断状态标志设置为true。Thread.currentThread().isInterrupted() 方法检查这个标志
        }
    }
}

关于死锁:

这段代码中我们让t1去获取locker1后再去获取locker2,让t2去获取locker2后再去获取locker1,但是我们运行结果是这样的:

只有前面的一段被打印了而后面的语句一直在等待。

先来了解一下锁的基本概念:

1. 基本概念

synchronized 用于确保多个线程在访问共享资源时,同一时间只有一个线程可以执行特定的代码块或方法。它通过锁定对象或类来实现线程同步。

2.锁的机制

  • 锁的释放

    • 当同步代码块或方法执行完毕时,锁会自动释放。
    • 如果同步代码块中发生异常,锁也会自动释放。
    • 如果线程因为某些原因被中断,锁也会自动释放。

我们来分析上面的代码,我们可以发现我们的代码在执行完“t1获取到locker1”后,想再去获取”t1获取到locker2“但这时我们t2线程中把locker2占用了还未释放锁,而t2要释放锁要先完成“t2获取到locker1”这部操作,但是t1把locker1占用了,所以t2无法完成锁的释放,t1也无法完成锁的释放,两边就都在等待构成了死锁。

我们可以这样来理解:

假设你准备开车出门,但家里和车里都有一些事情需要处理:

  1. 解锁车门:你先解锁了车门,打开后备箱放东西。
  2. 忘记钥匙:你想起家里的垃圾还没丢,于是返回家中。在换鞋时,你把车钥匙放在了鞋柜上,忘记带走。
  3. 锁门:你出门时锁好了家门,但没有拿车钥匙。
  4. 上车:你来到车边,于是顺手把家钥匙放在了车里的杯架上。
  5. 发现钥匙:你意识到自己没有拿车钥匙,于是下车,但车门在你离开时自动锁上了。
  6. 陷入困境:你发现家门被锁,而车钥匙在车里,但车门也锁着。你既无法打开家门,也无法打开车门,陷入了两难的境地。

volatile

synchronized是解决线程安全的一种方案还有一种场景是需要通过volatile来解决的~~

  • 单例模式:在实现单例模式时,确保实例变量的可见性。

多线程下的线程安全问题(编译器优化)“bug”:

我们这段代码本来输入非零的数字我们就可以使t1结束,但是我们在运行代码时却发现输入数字后t1没有变化而是还在运行

我们站在CPU指令来看:

1.load从内存读取flag的值到寄存器中

2.cmp比较寄存器和0之间是否相同,如果相同,继续执行,不同,使用跳转到指定位置。

load操作开销远远大于cmp的开销~~

在执行过程中编译器发现flag每次读到的都是相同的值(1秒已经读了上万次了)

编译器也没发现哪里在修改(虽然在另一个线程中有修改,但是编译器无法分析出另一个线程的执行时机~~

此时编译器做出大胆决定:把load的值优化掉放在了寄存器/缓存中读取flag值~~

之后flag一直都读为0,t2修改flag就没法感知到了。(内存可见性问题)

针对这样的问题我们可以使用volatile(易变的)关键字来修饰,后续编译器针对这个变量的读写操作,就不会设计到优化了~~

volatile没有互斥或者原子性,针对一个线程读一个线程写

这时我们在上面代码while循环中加入一个sleep,却可以达到相同的效果:

t1就结束了

load和cmp和sleep(背后非常多指令要比load消耗很多倍时间)

java中的主内存和工作内存

主内存(main memory):就是一个可以储存数据的单元

工作内存:CPU的寄存器+缓存

like:手机16+256G,16就是工作内存,256就是主内存。

wait和notify

多线程是随机调度的,join只能影响线程结束的顺序

在 Java 中,wait()和notify()是 Object类的方法

使用规则

  1. 必须在synchronized同步块或同步方法中调用这些方法
  2. 调用wait()的线程会释放对象锁,而notify()不会立即释放锁,需等待当前同步块执行完毕
  3. 被唤醒的线程需要重新获取对象锁才能继续执行

wait做的第一件事,先释放对象对应的锁,使用时必须先用synchronized()去加锁。才能去释放锁。而且wait调用的锁和我们加的锁必须是同一个锁。notify也是要和wait在同一个锁才可以使用哦。

wait做的3件事:1.释放锁2.等待其他线程通知(阻塞状态)3.通知到达,从阻塞变成就绪状态,并重新获得锁。(1和2必须同时进行是原子)

wait如果不先释放锁只是等,那么其他线程就没法继续进行(线程饿死),notify也是如此

下面是代码示例:

import java.util.Scanner;

public class demo22 {
    public static void main(String[] args) throws InterruptedException {
        Object locker1=new Object();
        Thread t1=new Thread(()->{
            synchronized(locker1){
                System.out.println("t1wait之前");
                try {
                    locker1.wait();//释放锁,t2才可以拿到锁
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t1wait之后");
            }
        });
        Thread t2=new Thread(()->{
            synchronized(locker1){
                System.out.println("t2notify之前");
                System.out.println("请输入任意内容来出发notify");
                Scanner sc=new Scanner(System.in);
                sc.next();//阻塞,不会释放锁
                    locker1.notify();
                System.out.println("t2notify之后");
            }//t2释放锁,t1才能往下执行
        });
        t1.start();
        t2.start();

    }
}

在这段代码中我们的wait其实是处于死等的状态,如果t2中notify没有给t1的wait回应那么t1就会一直等待。

wait可以限制时间:第二个是毫秒,第三个是毫秒加纳秒(更加精确),使用时原理就是:wait在指定时间之内进行等待,超出时间就不等了继续执行操作,如果在指定时间内收到反馈信息了也可以继续执行操作。

notify也有一个版本:notifyAll

notify如果有若干个等待的线程,随机唤醒一个,而notifyAll则是唤醒全部等待.

单例模式(singleton Pattern)

single Dog单身狗

单例模式 是一种创建型设计模式,其核心是确保某个类在整个应用中只存在一个实例,并提供一个全局访问点。

常见实现方式

1. 饿汉式(线程安全,立即加载)

在类加载时就创建实例,天生线程安全,但可能提前占用资源。

class Singleton{
    //饿汉式
    //加了static,当前成员属性为类属性,在类对象上的类只有一个实例
        private static Singleton instance=new Singleton();
        public  static Singleton getInstance(){
            return instance;
        }
        //单例模式最关键的要点,禁止构造方法被外部使用
        private Singleton(){
        }
    }
    public class demo24 {
        public static void main(String[] args) {
            //此时就不能通过new来获取到这个实例了
          // Singleton s=new Singleton();

           Singleton s1=Singleton.getInstance();
        }
}
2. 懒汉式(线程不安全,延迟加载)

在首次调用 getInstance() 时才创建实例,节省资源(高效),但多线程环境下可能创建多个实例。(比如线程1正在创建一个实例,在if(instance==null){这里执行后,线程2就完成了实例2的创建,而紧随其后实例1也创建好了,那么我们的实例2就会马上被垃圾回收。使用时会出现不安全的情况)

class singletonlazy{
    //懒汉式
    private static singletonlazy instance=null;
    public static singletonlazy  getInstance(){
        if(instance==null){
            instance=new singletonlazy();
        }
        return instance;
    }
    private singletonlazy(){

    }
}


public class demo25 {
    public static void main(String[] args) {
        singletonlazy s1=singletonlazy.getInstance();
        singletonlazy s2=singletonlazy.getInstance();
    }
}

懒汉模式虽然不安全,不安全只是出现在实例化之前。

解决线程安全问题首先考虑的就是加锁

进一步分析:

这个代码只要已进入方法就会加锁,此处代码只有在实例化之前才会有线程安全问题,一但实例化后就没有线程安全问题了。而我们每次加锁就会影响效率

再加一个if语句来判断,可以满足只加锁一次这个条件。

文章过长,这篇文章就先讲到这里了,下期再续~~


网站公告

今日签到

点亮在社区的每一天
去签到