深入浅出Java的多线程编程——第二篇

这篇具有很好参考价值的文章主要介绍了深入浅出Java的多线程编程——第二篇。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录

前情回顾

1. 中断一个线程

1.1 中断的API

1.2 小结

2. 等待一个线程

 2.1 等待的API

3. 线程的状态

3.1 贯彻线程的所有状态

3.2 线程状态和状态转移的意义

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

4.1 观察线程不安全

4.2 线程安全的概念

4.3 线程不安全的原因

4.3.1 修改共享数据

4.3.2 原子性

4.3.3 可见性

4.3.4 代码顺序性

4.4 解决之前的线程不安全问题


前情回顾

操作系统、进程和线程_木子斤欠木同的博客-CSDN博客

深入浅出Java的多线程编程——第一篇_木子斤欠木同的博客-CSDN博客

让我们来回顾一下,第一篇多线程的内容:

1. 多线程:

(1)线程的概念

(2)进程和线程的区别

(3)Java代码如何创建线程

①继承Thread重写run

②实现Runnable接口重写run,将该实现类作为参数传给Thread的构造方法

③继承Thread,匿名内部类

④实现Runnable,匿名内部类

⑤lambda表达式

2. Thread的常用属性

start方法,真正从系统这里,创建一个线程,新的线程将会执行run方法。

run方法:表示线程的入口方法是啥(线程启动起来,要执行哪些逻辑)(run方法不是让程序猿调用的,要交给系统去自动调用)【换个角度理解:我们可以把线程的run方法理解为main方法,都是系统去自动调用】

1. 中断一个线程

李四一旦进到工作状态,他就会按照行动指南上的步骤去进行工作,不完成是不会结束的。但有时我们需要增加一些机制,例如老板突然来电话了,说转账的对方是个骗子,需要赶紧停止转账,那张三该如何通知李四停止呢?这就涉及到我们的停止线程的方式了。

本质上来说,让一个线程终止,办法就一种,让该线程的入口方法执行完毕!也就是让run跑完!

目前常见的有以下两种方式:

  • 通过共享的标记来进行沟通
  • 调用 interrupt() 方法来通知【记住,只是通知而已】

示例1:使用自定义的变量来作为标志位

  • 需要给标志位上加 volatile 关键字(这个关键字的功能后面介绍).
    public static volatile boolean isQuit = false;

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            while (!isQuit){
                System.out.println(Thread.currentThread().getName() + " : 别管我,我忙着转正!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + " : 啊!险些误了大事");
        });
        System.out.println(Thread.currentThread().getName() + " : 让李四开始转账");
        t.start();
        Thread.sleep(10*1000);
        System.out.println(Thread.currentThread().getName() + " : 老板来电话了,得赶紧通知李四对方是个骗子!");
        isQuit = true;
    }

深入浅出Java的多线程编程——第二篇,Java的多线程编程,java,开发语言

这里的   public static volatile boolean isQuit = false; 为什么需要加static,因为main函数被static修饰,所以在main内部用到的成员变量要加static

示例2:使用 Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替自定义标志位。

1.1 中断的API

  • Thread 内部包含了一个 boolean 类型的变量作为线程是否被中断的标记。
方法 说明
pubilc void interrupt() 中断对象关联的线程,如果线程正在阻塞,会把线程唤醒,则以异常的方式通知,然后吧标志位设置为true
public static boolean interrupted() 判断当前线程的中断标志位是否设置,调用后清除标志位
public boolean isInterrupted() 判断对象关联的线程的标志位是否设置,调用后不清除标志位
  • 使用 thread 对象的 interrupted() 方法通知线程结束.
public class Thread4 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
           while(!Thread.currentThread().isInterrupted()){
               System.out.println("hello t");
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        t.start();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.interrupt();
    }
}

深入浅出Java的多线程编程——第二篇,Java的多线程编程,java,开发语言

我们可以发现,调用t.interrupt方法的时候,线程并没有真的结束,而是打印了个异常信息,又继续执行了

1.2 小结

interrupt方法的作用:

(1)设置标志位为true

(2)如果该线程正在阻塞中(比如执行了sleep)此时就会把阻塞状态唤醒,通过抛出异常的方式让sleep立即结束

注意:一个非常重要的问题,当sleep被唤醒的时候(只有sleep被唤醒才会重置标志位),sleep自动地把isInterrupted标志位给清空了(true - > false),这导致下次循环,循环仍然可以继续执行了~~

有的开关,是按下之后,就按下去了

有的开关,是按下去之后,自动弹起(sleep就属于这种)

一种极端的情况:

如果设置interrupt的时候,恰好sleep刚醒,这个时候赶巧了,执行到下一轮循环条件就直接结束了。但是这种概率非常低,毕竟sleep的时间已经占据了整个循环体的99.999%的时间了

如果需要结束循环,就得在catch中搞个break

public class Thread4 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
           while(!Thread.currentThread().isInterrupted()){
               System.out.println("hello t");
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
                   break;
               }
           }
        });
        t.start();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.interrupt();
    }
}

深入浅出Java的多线程编程——第二篇,Java的多线程编程,java,开发语言

小结一下:

如果sleep执行的时候看到这个标志位是false,sleep正常进行休眠操作

如果当前的标志位为true

sleep无论是刚刚执行还是已经执行了一般,都会触发两件事

(1)立即抛出异常

(2)清空标志位为false 

再下次循环,到sleep

由于当前标志位本身是false,就啥也不干~~

总结到这里,就有小伙伴有疑问了,为什么sleep要清空标志位呢?

目的就是为了让线程自身能够对于线程何时结束,有一个更明确的控制~~

当前,interrupt方法,效果不是让线程立即结束,而是告诉他,你该结束了,至于他是否真的要立即结束还是等会结束,都是由它本线程的代码来控制~~,interrupt只是通知,而非“命令”!

深入浅出Java的多线程编程——第二篇,Java的多线程编程,java,开发语言

深入浅出Java的多线程编程——第二篇,Java的多线程编程,java,开发语言

有的朋友就好奇了,我如果不加sleep,这些能令线程阻塞的代码,那是不是就能让该线程直接结束呢?对的,是可以,但是工作中没人会这么写代码,一个不可控的线程是多么可怕!

这里就可以又引出一个问题,java为啥不强制制定“命令结束”的操作呢?

只要调用interrupt就立即结束?

答:主要是设定成这种,对线程来说非常不友好~,线程t何时结束,一定是t自己要最清楚,交给t自身来决定比较好!

2. 等待一个线程

有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。例如,张三只有等李四转账成功,才决定是否存钱,这时我们需要一个方法明确等待线程的结束。

public class Thread5 {
    public static void main(String[] args) throws InterruptedException {
        Runnable run = () -> {
          for(int i = 0;i < 3;i++){
              try {
                  System.out.println(Thread.currentThread().getName() + " : 我正在工作!");
                  Thread.sleep(1000);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
            System.out.println(Thread.currentThread().getName() + " : 我的工作结束了!");
        };

        Thread t1 = new Thread(run,"李四");
        Thread t2 = new Thread(run,"张三");
        System.out.println("李四先工作");
        t1.start();
        t1.join();
        System.out.println("李四做完了,张三开始工作!");
        t2.start();
        t2.join();
        System.out.println("王五做完了,张三开始工作!");
        System.out.println("两人都工作结束了!");
    }
}

深入浅出Java的多线程编程——第二篇,Java的多线程编程,java,开发语言

在main线程中调用t1.join()表示main线程要等t1线程跑完,main线程才可以继续执行。

(1)main线程调用t1.join()的时候,如果t1还在运行,此时main线程阻塞,知道t执行完毕(t1的run执行完了),main线程才从阻塞中解除,才继续执行。

(2)main线程调用t.join()的时候,如果t已经结束了,此时join就不会阻塞,会立即执行下去。

如果把两个join方法注释掉,就会CPU的抢占式调用的典型例子:

public class Thread5 {
    public static void main(String[] args) throws InterruptedException {
        Runnable run = () -> {
          for(int i = 0;i < 3;i++){
              try {
                  System.out.println(Thread.currentThread().getName() + " : 我正在工作!");
                  Thread.sleep(1000);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
            System.out.println(Thread.currentThread().getName() + " : 我的工作结束了!");
        };

        Thread t1 = new Thread(run,"李四");
        Thread t2 = new Thread(run,"张三");
        System.out.println("李四先工作");
        t1.start();
//        t1.join();
        System.out.println("李四做完了,张三开始工作!");
        t2.start();
//        t2.join();
        System.out.println("王五做完了,张三开始工作!");
        System.out.println("两人都工作结束了!");
    }
}

深入浅出Java的多线程编程——第二篇,Java的多线程编程,java,开发语言

 2.1 等待的API

方法 说明
public void join() 等待线程结束
public void join(long millis) 等待线程结束,最多等 millis 毫秒
public void join(long millis, int nanos) 同理,但可以更高精度

3. 线程的状态

3.1 贯彻线程的所有状态

线程的状态是一个枚举类型 Thread.State

public class Thread6 {
    public static void main(String[] args) {
        for (Thread.State state:Thread.State.values()) {
            System.out.println(state);
        }
    }
}

深入浅出Java的多线程编程——第二篇,Java的多线程编程,java,开发语言

  • NEW: 安排了工作, 还未开始行动
  • RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作.
  • BLOCKED: 这几个都表示排队等着其他事情
  • WAITING: 这几个都表示排队等着其他事情
  • TIMED_WAITING: 这几个都表示排队等着其他事情
  • TERMINATED: 工作完成了.

3.2 线程状态和状态转移的意义

深入浅出Java的多线程编程——第二篇,Java的多线程编程,java,开发语言

大家不要被这个状态转移图吓到,我们重点是要理解状态的意义以及各个状态的具体意思。

深入浅出Java的多线程编程——第二篇,Java的多线程编程,java,开发语言

举个栗子:
       刚把李四、王五找来,还是给他们在安排任务,没让他们行动起来,就是 NEW 状态;
当李四、王五开始去窗口排队,等待服务,就进入到 RUNNABLE 状态。该状态并不表示已经被银行工。
       作人员开始接待,排在队伍中也是属于该状态,即可被服务的状态,是否开始服务,则看调度器的调度;
       当李四、王五因为一些事情需要去忙,例如需要填写信息、回家取证件、发呆一会等等时,进入
        BLOCKED 、 WATING 、 TIMED_WAITING 状态,至于这些状态的细分,我们以后再详解;
如果李四、王五已经忙完,为 TERMINATED 状态。所以,之前我们学过的 isAlive() 方法,可以认为是处于不是 NEW 和 TERMINATED 的状态都是活着的。

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

本质是因为线程之间的调度顺序的不确定性

4.1 观察线程不安全

public class Thread7 {
    static int count = 0;


    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            for(int i = 0;i < 5000;i++){
                count++;
            }
        });
        Thread t2 = new Thread(() -> {
            for(int i = 0;i < 5000;i++){
                count++;
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(count);
    }

}

深入浅出Java的多线程编程——第二篇,Java的多线程编程,java,开发语言

 

深入浅出Java的多线程编程——第二篇,Java的多线程编程,java,开发语言

深入浅出Java的多线程编程——第二篇,Java的多线程编程,java,开发语言

由于当前这两线程调度的顺序是无序的~~

你也不知道这两线程自增的过程中,到底经历了什么

有多少次是“顺序执行”,有多少次是“交错执行”不知道!

得到的结果是啥也就是不确定的!

这里就引出了一个问题,出现bug之后,得到的结果一定是 <= 10000,或者结果一定是 >= 5000?

深入浅出Java的多线程编程——第二篇,Java的多线程编程,java,开发语言

CPU调用是以原语为单位的!

4.2 线程安全的概念

想给出一个线程安全的确切定义是复杂的,但我们可以这样认为:
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。

4.3 线程不安全的原因

4.3.1 修改共享数据

上面的线程不安全的代码中, 涉及到多个线程针对 counter.count 变量进行修改.
此时这个 count 是一个多个线程都能访问到的 "共享数据"!

深入浅出Java的多线程编程——第二篇,Java的多线程编程,java,开发语言

count 这个变量就是在堆上. 因此可以被多个线程共享访问. 

4.3.2 原子性

深入浅出Java的多线程编程——第二篇,Java的多线程编程,java,开发语言

什么是原子性
我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。
那我们应该如何解决这个问题呢?是不是只要给房间加一把锁,A 进去就把门锁上,其他人是不是就进不来了。这样就保证了这段代码的原子性了。有时也把这个现象叫做同步互斥,表示操作是互相排斥的。 

一条 java 语句不一定是原子的,也不一定只是一条指令

比如刚才我们看到的 n++,其实是由三步操作组成的:

  1. 从内存把数据读到 CPU
  2. 进行数据更新
  3. 把数据写回到 CPU

不保证原子性会给多线程带来什么问题
如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是错误的。

这点也和线程的抢占式调度密切相关. 如果线程不是 "抢占" 的, 就算没有原子性, 也问题不大。

4.3.3 可见性

可见性指一个线程对共享变量值的修改,能够及时地被其他线程看到。

Java 内存模型 (JMM): Java虚拟机规范中定义了Java内存模型.
目的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果.

深入浅出Java的多线程编程——第二篇,Java的多线程编程,java,开发语言

  1. 线程之间的共享变量存在 主内存 (Main Memory).
  2. 每一个线程都有自己的 "工作内存" (Working Memory) .
  3. 当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据.
  4. 当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存. 

由于每个线程有自己的工作内存, 这些工作内存中的内容相当于同一个共享变量的 "副本". 此时修改线程1 的工作内存中的值, 线程2 的工作内存不一定会及时变化.

1) 初始情况下, 两个线程的工作内存内容一致.

深入浅出Java的多线程编程——第二篇,Java的多线程编程,java,开发语言

2) 一旦线程1 修改了 a 的值, 此时主内存不一定能及时同步. 对应的线程2 的工作内存的 a 的值也不一定能及时同步. 

深入浅出Java的多线程编程——第二篇,Java的多线程编程,java,开发语言

这个时候代码中就容易出现问题.

此时引入了两个问题: 

  • 为啥要整这么多内存?
  • 为啥要这么麻烦的拷来拷去?

1) 为啥整这么多内存?
实际并没有这么多 "内存". 这只是 Java 规范中的一个术语, 是属于 "抽象" 的叫法.
所谓的 "主内存" 才是真正硬件角度的 "内存". 而所谓的 "工作内存", 则是指 CPU 的寄存器和高速缓存.

2) 为啥要这么麻烦的拷来拷去?
因为 CPU 访问自身寄存器的速度以及高速缓存的速度, 远远超过访问内存的速度(快了 3 - 4 个数量级, 也就是几千倍, 上万倍)

比如某个代码中要连续 10 次读取某个变量的值, 如果 10 次都从内存读, 速度是很慢的. 但是如果只是第一次从内存读, 读到的结果缓存到 CPU 的某个寄存器中, 那么后 9 次读数据就不必直接访问内存了,效率就大大提高了

那么接下来问题又来了, 既然访问寄存器速度这么快, 还要内存干啥??
答案就是一个字: 贵

深入浅出Java的多线程编程——第二篇,Java的多线程编程,java,开发语言

值的一提的是, 快和慢都是相对的. CPU 访问寄存器速度远远快于内存, 但是内存的访问速度又远远快于硬盘。
对应的, CPU 的价格最贵, 内存次之, 硬盘最便宜。

4.3.4 代码顺序性

什么是代码重排序
一段代码是这样的:

  1. 去前台取下 U 盘
  2. 去教室写 10 分钟作业
  3. 去前台取下快递

如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题,可以少跑一次前台,这种叫做指令重排序。

编译器对于指令重排序的前提是 "保持逻辑不发生变化". 这一点在单线程环境下比较容易判断, 但是在多线程环境下就没那么容易了, 多线程的代码执行复杂程度更高, 编译器很难在编译阶段对代码的执行效果进行预测, 因此激进的重排序很容易导致优化后的逻辑和之前不等价.

重排序是一个比较复杂的话题, 涉及到 CPU 以及编译器的一些底层工作原理, 此处不做过多讨论文章来源地址https://www.toymoban.com/news/detail-731038.html

4.4 解决之前的线程不安全问题

public class Thread7 {
    static int count = 0;


    public static void main(String[] args) {
        Object o = new Object();
        Thread t1 = new Thread(() -> {

            synchronized (o) {
                for(int i = 0;i < 5000;i++){
                    count++;
                }
            }
        });
        Thread t2 = new Thread(() -> {
            synchronized (o) {
                for(int i = 0;i < 5000;i++){
                    count++;
                }
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(count);
    }

}

到了这里,关于深入浅出Java的多线程编程——第二篇的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包赞助服务器费用

相关文章

  • 深入浅出Java多线程(十三):阻塞队列

    大家好,我是你们的老伙计秀才!今天带来的是[深入浅出Java多线程]系列的第十三篇内容:阻塞队列。大家觉得有用请点赞,喜欢请关注!秀才在此谢过大家了!!! 在多线程编程的世界里,生产者-消费者问题是一个经典且频繁出现的场景。设想这样一个情况:有一群持续

    2024年03月20日
    浏览(7)
  • 深入浅出Java多线程(十一):AQS

    深入浅出Java多线程(十一):AQS

    大家好,我是你们的老伙计秀才!今天带来的是[深入浅出Java多线程]系列的第十一篇内容:AQS( AbstractQueuedSynchronizer )。大家觉得有用请点赞,喜欢请关注!秀才在此谢过大家了!!! 在现代多核CPU环境中,多线程编程已成为提升系统性能和并发处理能力的关键手段。然而

    2024年03月12日
    浏览(11)
  • 【计算机视觉中的多视图几何系列】深入浅出理解针孔相机模型

    【计算机视觉中的多视图几何系列】深入浅出理解针孔相机模型

    温故而知新,可以为师矣! 《计算机视觉中的多视图几何-第五章》-Richard Hartley, Andrew Zisserman. 1.1 投影中心/摄像机中心/光心 投影中心 称为 摄像机中心 ,也称为 光心 。投影中心位于一个欧式坐标系的原点。 1.2 图像平面/聚焦平面 平面 Z = f Z=f Z = f 被称为 图像平面 或 聚焦

    2024年02月03日
    浏览(8)
  • 深入浅出线程池

    线程 (thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际 运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线 程并行执行不同的任务。 既然我们创建了线程,那为何我们直接调用方法和我们调

    2024年02月08日
    浏览(9)
  • 深入浅出C++ ——线程库

    深入浅出C++ ——线程库

      在C++11之前,涉及到多线程问题,都是和平台相关的,比如windows和linux下各有自己的接口,这使得代码的可移植性比较差。C++11中最重要的特性就是对线程进行支持了,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了 原子类 的概念。要使用标准库中

    2024年02月03日
    浏览(31)
  • “深入理解Java的多线程编程“

    多线程编程是指在一个程序中同时运行多个线程,以提高程序的并发性和性能。Java是一门支持多线程编程的强大编程语言,提供了丰富的多线程相关类和接口。 在Java中,可以通过以下方式实现多线程编程: 继承Thread类:创建一个继承自Thread类的子类,并重写run()方法,在

    2024年02月13日
    浏览(10)
  • 【深入浅出C#】章节 8: 网络编程和远程通信

    计算机网络是指连接多台计算机设备,通过通信链路共享资源和信息的系统。它构建了一个相互连接的世界,使得人们可以在不同地点进行数据交换和资源共享。网络编程是指在计算机网络中,使用编程语言进行通信和数据传输的技术。现代应用中,网络编程发挥着重要作用

    2024年02月07日
    浏览(11)
  • 【深入浅出C#】章节 5: 高级面向对象编程:泛型编程和集合类型

    高级面向对象编程是在基础面向对象编程的基础上进一步深入和拓展的一种编程范式。它强调封装、继承和多态的概念,并引入了泛型编程和集合类型等高级特性。高级面向对象编程提供了更灵活、可扩展和可复用的代码结构,能够帮助开发者构建更复杂、更高效的应用程序

    2024年02月16日
    浏览(13)
  • 【深入浅出C#】章节 4: 面向对象编程基础:封装、继承和多态

    封装、继承和多态是面向对象编程中的核心概念,它们对于构建灵活、可扩展和可维护的软件系统至关重要。 封装(Encapsulation)通过将数据和相关操作封装在一个类中,隐藏内部实现细节,并提供公共接口来与外部进行交互。封装有助于保护数据的完整性和安全性,同时提

    2024年02月10日
    浏览(13)
  • 【深入浅出C#】章节 8: 网络编程和远程通信:网络编程和远程通信

    计算机网络是指连接多台计算机设备,通过通信链路共享资源和信息的系统。它构建了一个相互连接的世界,使得人们可以在不同地点进行数据交换和资源共享。网络编程是指在计算机网络中,使用编程语言进行通信和数据传输的技术。现代应用中,网络编程发挥着重要作用

    2024年02月12日
    浏览(10)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包