首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >多线程-线程安全

多线程-线程安全

作者头像
独断万古他化
发布2026-01-15 13:07:17
发布2026-01-15 13:07:17
110
举报
文章被收录于专栏:Java 攻略Java 攻略

一、线程状态

Java 给线程引入了六种状态

线程状态

含义说明

NEW

安排了工作,还未开始行动

RUNNABLE

可工作的,又可以分成正在工作中和即将开始工作

BLOCKED

这几个都表示排队等着其他事情

WAITING

这几个都表示排队等着其他事情

TIMED_WAITING

这几个都表示排队等着其他事情

TERMINATED

工作完成了

  • NEW:创建了Thread对象,但是还没有调用start;
  • TERMINATED:操作系统内部的线程已经销毁了,但是Thread对象还在,线程的入口方法执行完毕。
  • RUNNABLE:一个线程,正在cpu上执行,或者没有在cpu上执行,但是也在就绪队列中。
  • WAITING:表示死等进入的阻塞状态,join()不待参数
  • TIMED_WAITING:带有超时时间的等待
  • BLOCKED:特指由于锁引起的阻塞 eg:
代码语言:javascript
复制
public class Demo13 {
    public static void main(String[] args) throws InterruptedException {

        Thread mainThread = Thread.currentThread();
        Thread t = new Thread(() -> {
           while (true){
               System.out.println(mainThread.getState());
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }
           }
        });

        System.out.println(t.getState());

        t.start();
        t.join(1000);

        System.out.println(t.getState());
    }
}

二、线程安全

一个进程的多个线程,共享同一份内存资源,如果两个线程,都尝试修改某个变量,就可能出现冲突; 某个逻辑单个线程执行是可以的,但是多个线程执行出现问题,这就是线程不安全,反之则线程安全

  • 线程安全问题的原因
  1. [ 根本原因 ] 操作系统对于线程的调度是随机的(没有办法应对)
  2. 两个线程针对同一个变量进行修改操作
  3. 修改操作不是原子的
  4. 内存可见性
  5. 指令重排序

eg:线程不安全例子

代码语言:javascript
复制
public class Demo14 {

    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        //创建两个线程,分别对同一个变量进行5w次 ++ 操作
        //最终主线程打印结果
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });

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

        t1.join();
        t2.join();

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

使用两个线程分别对同一个变量进行5w次 ++ 操作,其结果应该为10w,但运行结果确像是一个<10w的随机值,这就是两个线程对同一变量修改的不安全。count++操作实际是三次指令,将内存值加载到cpu寄存器中,在cpu寄存器中对值进行计算,将寄存器再写入到内存中,由于是三次指令,可能在某一条指令时调度到别的线程,这样的调度穿插过程就可能出现线程安全问题。

2.1 保证线程安全的方法 - 锁
2.1.1 synchronized 关键字 - 监视器锁

锁当前对象

代码语言:javascript
复制
synchronized (obj){
    代码块  
}

(括号中的对象:从语法角度来说可以是任何对象,只要是Object的实例即可;从语义角度来说,两个线程填写相同的对象才会有锁竞争,才会有阻塞效果。)

要点:

  • 进入synchronized { 中的代码块就是加锁,退出 } 就是解锁
  • 加锁操作是防止其他线程" 插队 ",不影响本线程调度出cpu
  • 锁对象,两个线程针对同一个对象加锁才会有锁竞争,锁不同的对象则不会有。
2.1.2 synchronized 的其他写法
  1. 修饰一个实例方法
代码语言:javascript
复制
synchronized public void add(){
            count++;
    }
代码语言:javascript
复制
public void add(){
    synchronized(this){
        count++;
    }
}

此时锁的this对象就是调用add方法的对象

  1. 修饰一个静态方法(针对类对象)
代码语言:javascript
复制
private static void add(){
        synchronized (Demo16.class){
            count++;
        }
    }

无论哪种写法,synchronized 方法针对啥对象加锁不重要,重要的是两个线程是否针对同一个对象加锁!

2.1.3 synchronized 不存在死锁情况

synchronized 对同一线程具有可重入性,不会存在把自己锁死的情况。

  • 理解锁死:一个线程加锁后没有释放锁就再次进行加锁,此时会产生阻塞等待,只到第一次的锁被释放。但是释放锁也是由该线程来完成的,此时该线程已经阻塞了无法进行释放锁,就会一直持续阻塞,此时逻辑就会"卡死",形成死锁。
  • 但是Java中的synchronized 并没有上面的情况,如果第一次加锁成功后,在进行第二次加锁,synchronized 内部会判定第二次加锁的线程是否和第一次加锁是同一个线程,如果是同一个,第二次加锁相当于直接跳过,不做任何处理;如果不是同一个线程,第二次加锁才会真正生效。

死锁的场景:

  1. 一个线程一把锁,连续加锁两次(可重入锁直接解决)
  2. 两个线程两把锁,相互获取对方的锁(eg:车钥匙锁屋子里,屋钥匙锁车里)
  3. n个线程m把锁

死锁的四个必要条件 (打破任意一个就可以避免死锁)

  1. 锁是互斥的(对于synchronized来说改不了)
  2. 锁不可被抢占 (对于synchronized来说改不了)
  3. 请求和保持 例:A线程在获取到locker1的情况下,保持持有locker1的状态(不释放),再尝试获取locker2.
  4. 循环等待 / 环路等待

如何避免死锁? 打破请求和保持:在代码中尽量避免锁的嵌套 打破循环等待:约定加锁的顺序(把锁进行编号,约定任何一个线程多把锁的时候,都需要按照标号从小到大的顺序来加锁)

2.2 volatile 关键字

volatile 能够保证内存可见性

内存可见性是由于编译器优化导致的,编译器优化又是什么:举个例子,在程序员中写代码的水平是参差不齐的,而写的差占据多数因此就在编译器中加入了”优化机制“,编译器自动分析这一部分代码逻辑,保持代码逻辑不变的前提下,自动修改代码内容,让代码变得更加的高效。

eg:

代码语言:javascript
复制
public class Demo18 {
    private static int flag = 0;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (flag == 0){

            }
            System.out.println("t1 结束");
        });

        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("输入 flag 的值");
            flag = scanner.nextInt();
            System.out.println("t2 结束");
        });
        
        t1.start();
        t2.start();
    }
}
  • 为什么这个代码在t2线程修改了flag的值,但是t1线程并没有结束: 站在cpu指令的角度,,首先load操作从内存读取flag的值到寄存器中,然后比较寄存器和0的值是否相同,如果相同继续执行;不相同使用跳转语句跳转到指定位置。load操作的开销远远大于比较的开销。 在比较处,编译器发现flag每次读到的都是相同的值,且1s足以让这个循环执行上万次,编译器也并没有发现哪里在修改。虽然在另一个线程中有修改,但编译器无法分析出另一个线程的执行时机,此时编译器 就做了一个大胆的决定,把load的操作给优化掉了,所以后续的循环都是从寄存器/缓存中读取flag的值,提高效率,因此即使在t2中修改了flag的值,t1线程也无法感知到。

解决方案:使用volatile 关键字修饰某个变量,此时编译器就知道这个变量" 易变 ",后续编译器针对这个变量的读写操作就不会涉及到优化了。

  • volatile 并没有互斥 / 原子性,适用于一个线程读,一个线程写的情况。无法应对两个线程同时写的情况。虽然和synchronized都是解决线程安全问题,但和synchronized解决的是两种不同的问题。
代码语言:javascript
复制
public class Demo18 {
    private static int flag = 0;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (flag == 0){
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("t1 结束");
        });

        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("输入 flag 的值");
            flag = scanner.nextInt();
            System.out.println("t2 结束");
        });

        t1.start();
        t2.start();
    }
}

加上sleep(1)后为什么代码又可以正确执行了,并不是sleep解决了内存可见性。而是因为本质上内存可见性是由编译器优化带来的,因为引入了sleep,这个代码中的load操作没有被编译器优化掉,因为sleep背后是非常多的指令,消耗的时间比load读取一次多得多,所以即使优化掉load操作也没什么太大作用。所以是sleep影响到了编译器优化,因此内存可见性没有了。

三、wait 和 notify

多线程之间是随机调度的,执行顺序难以知道,而有时我们又希望能够确定多个线程之间的先后执行顺序,而join方法只能确定线程的结束顺序,此时就需要用到wait和notify方法。除此之外,wait和notify还可以解决"线程饿死"问题。 wait(),notify(),notifyAll() 都是Object 类的方法。

3.1 wait 方法

wait 方法让当前线程进入等待状态;wait方法必须搭配synchronized来使用。 wait做的三件事情:

  1. 释放当前的锁
  2. 等待其他线程的通知(进入阻塞状态)
  3. 当通知到达后,从阻塞状态回归到就绪状态,并且重新获取到锁。 1和2之间必须是原子性的
3.2 notify 方法

notify 方法是唤醒等待的线程。 该方法用来通知那些可能等待该对象的线程,对其发出通知并使他们重新获取该对象的锁。如果有多个线程等待,则随机挑选一个wait 状态的线程;在notify 方法后并不会马上释放该对象锁,而是要等待执行notify 方法线程将程序执行完也就是退出同步代码块才会释放对象锁。

代码语言:javascript
复制
public class Demo20 {
    public static void main(String[] args) {

        Object locker1 = new Object();
        Object locker2 = new Object();

        Thread t1 = new Thread(() -> {
            synchronized (locker1){
                System.out.println("t1 wait 之前");
                try {
                    locker1.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t1 wait 之后");
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (locker1){
                System.out.println("t2 notify 之前");
                locker1.notify();
                System.out.println("t2 notify 之后");
            }
        });

        t1.start();
        t2.start();
    }
}

notifyAll 方法:notify方法只是唤醒某⼀个等待线程. 使用notifyAll方法可以⼀次唤醒所有的等待线程。

3.3 wait 和sleep 的区别

都可以让线程阻塞,都可以指定阻塞的时间

  1. wait 的设计就是为了被 notify ,超时时间只是用来保底的;而sleep 就是用来按照一定时间进行阻塞
  2. wait 必须搭配锁使用,而sleep不需要
  3. wait 一进来会先释放锁,然后再获取到锁;sleep 放到锁内部等待时不会释放锁
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-09-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、线程状态
  • 二、线程安全
    • 2.1 保证线程安全的方法 - 锁
      • 2.1.1 synchronized 关键字 - 监视器锁
      • 2.1.2 synchronized 的其他写法
      • 2.1.3 synchronized 不存在死锁情况
    • 2.2 volatile 关键字
  • 三、wait 和 notify
    • 3.1 wait 方法
    • 3.2 notify 方法
    • 3.3 wait 和sleep 的区别
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档