• 多线程初阶(一)


    目录

    前言:

    认识多线程

    创建线程

    run方法和start区别

    继承Thread类

    实现Runnable接口

    匿名内部类实现继承Thread类

    匿名内部类实现Runnable接口实例

    Lambda表达式

    中断线程

    等待线程

    线程休眠

    线程状态

    线程状态之间切换

    代码观察线程的状态

    线程安全

    线程安全测试

    解析

    线程安全总结

             小结:


    前言:

        多线程在实际开发中会经常使用到,它可以对于硬件资源充分的利用,提升代码的执行效率。这样会对我们开发过程中提供了很大便利。

    认识多线程

        上篇文章讲解了并发和并行的区别,目的就是将硬件资源得到充分利用。线程实际在操作系统内核中调度是抢占式调度,随机执行的。每个线程在cpu里执行时,都是以指令的方式去执行一个线程的,一个线程会包含多条指令。

        线程在cpu里执行具体是并发还是并行,我们是不确定的,具体实现是由操作系统内核实现的。当操作系统调度一个线程,会执行多条指令。进行线程切换的时候,这个时间点是不确定的,两个线程的指令会随机组合,有无数种可能。正是由于这种抢占式调度,随机执行的方式,对于代码的执行顺序就会有很大的不确定性,这就带来了线程安全问题。

        解决线程安全问题,我们就需要对于一部分指令,保证其原子性。使另一个线程阻塞等待,通过加锁实现。

    创建线程

    run方法和start方法区别

        start是启动一个线程,当这个线程的pcb被cpu调度时,这个线程就真实存在了。线程当被调度时执行的代码就是run方法里的代码体,当run方法执行结束时,线程也就结束了,这时候线程的pcb也就释放了,但是线程的引用还在。

    继承Thread类

        继承Thread类,实现父类引用子类实例。

    1. class MyThread extends Thread {
    2. @Override
    3. public void run() {
    4. while (true) {
    5. System.out.println("aaaa");
    6. try {
    7. Thread.sleep(1000);
    8. } catch (InterruptedException e) {
    9. e.printStackTrace();
    10. }
    11. }
    12. }
    13. }
    14. public class ThreadDemo1 {
    15. public static void main(String[] args) throws InterruptedException {//主线程
    16. //创建线程
    17. Thread thread = new MyThread();
    18. //启动线程
    19. thread.start();
    20. while (true) {
    21. System.out.println("bbbb");
    22. Thread.sleep(1000);
    23. }
    24. }
    25. }

        注意:这里主线程和thread引用的线程里面都有死循环代码,我们可以通过jconsole工具查看线程的一些状态。

     实现Runnable接口

        实现Runable接口,将实例传入Thread的构造方法。这样可以将线程具体要做的事和引用分离开,解耦合。

    1. class MyRunable implements Runnable {
    2. @Override
    3. public void run() {
    4. System.out.println("aaaa");
    5. }
    6. }
    7. public class ThreadDemo2 {
    8. public static void main(String[] args) {
    9. //runable描述了这个线程要干什么
    10. Runnable runnable = new MyRunable();
    11. //创建线程
    12. Thread thread = new Thread(runnable);
    13. thread.start();
    14. }
    15. }

    匿名内部类实现继承Thread类

    1. public class ThreadDemo3 {
    2. public static void main(String[] args) {
    3. //使用匿名内部类
    4. //匿名内部类为Thread的子类
    5. //创建了子类的实例,让thread引用
    6. Thread thread = new Thread() {
    7. @Override
    8. public void run() {
    9. System.out.println("aaaa");
    10. }
    11. };
    12. thread.start();
    13. }
    14. }

    匿名内部类实现Runnable接口实例

    1. public class ThreadDemo4 {
    2. public static void main(String[] args) {
    3. Thread thread = new Thread(new Runnable() {
    4. @Override
    5. public void run() {
    6. System.out.println("aaaa");
    7. }
    8. });
    9. thread.start();
    10. }
    11. }

    Lambda表达式

        由于Runnable为函数式接口,因此可以使用lanbda表达式。

    1. //lambda表达式实现函数式接口Runable(实例其函数式接口对象)
    2. public class ThreadDemo5 {
    3. public static void main(String[] args) {
    4. Thread thread = new Thread(() -> {
    5. System.out.println("aaa");
    6. });
    7. thread.start();
    8. }
    9. }

    中断线程

        注意:终止线程只是通知说线程该终止了,但具体要不要终止是线程里说了算的。

        可以手动设置标志位,通过另一个线程来改变这个标志位,进一步来决定run方法的结束,控制线程的终止。

    1. public class ThreadDemo7 {
    2. private static boolean flag = true;
    3. public static void main(String[] args) throws InterruptedException {
    4. Thread thread = new Thread(new Runnable() {
    5. @Override
    6. public void run() {
    7. //设置标志位,其他线程只要改变标志位,就可以结束run方法,结束本线程
    8. while (flag) {
    9. System.out.println("aaaa");
    10. try {
    11. Thread.sleep(1000);
    12. } catch (InterruptedException e) {
    13. e.printStackTrace();
    14. }
    15. }
    16. }
    17. });
    18. thread.start();
    19. Thread.sleep(3000);
    20. flag = false;
    21. }
    22. }

        1)使用自带的isInterrupted方法设置标志位。

        2)interrupt方法控制标志位。

        注意:isInterrupted方法初始值默认为false,可以通过interrupt设置为true。但是如果通过interrupt设置标志位的时候,这个线程处于sleep(),TIMED_WAITING状态时,就会唤醒线程。这时候sleep就会抛出一个异常,并且清空标志位,改回false。接下来要不要中断就看我们代码的结构了。

    1. public class ThreadDemo7 {
    2. public static void main(String[] args) throws InterruptedException {
    3. Thread thread = new Thread(new Runnable() {
    4. @Override
    5. public void run() {
    6. //可以获得当前线程的引用
    7. //isInterrupted()线程是否中断,默认为false
    8. while (!Thread.currentThread().isInterrupted()) {
    9. try {
    10. Thread.sleep(1000);
    11. } catch (InterruptedException e) {
    12. e.printStackTrace();
    13. //也可等待一会再中断线程
    14. try {
    15. Thread.sleep(2000);
    16. } catch (InterruptedException ex) {
    17. ex.printStackTrace();
    18. }
    19. //跑完异常可以主动中断线程
    20. break;
    21. }
    22. System.out.println("aaaaa");
    23. }
    24. }
    25. });
    26. thread.start();
    27. Thread.sleep(3000);
    28. //当设置另一个线程标志位时(会设置为true),如果这个线程正处于休眠状态,sleep就会抛异常,并且清空标志位(改回标志位为false)
    29. //sleep清空标志位的原因:当触发sleep唤醒线程后,这个线程的状态就交给我们自己了吗,类似于代码“暂停”的做法,都会清空标志位(wait,join)
    30. thread.interrupt();//设置标志位,告诉线程该中断了
    31. }
    32. }

    等待线程

        由于线程的抢占式执行,随机调度。不可以确定线程的执行顺序,但是我们可以通过一个线程等待一个线程(阻塞),来控制线程的结束时间。java里使用join方法。

        当线程在就绪队列里,这个线程就可以被操作系统内核调度。线程一旦阻塞就会进入阻塞队列,直到阻塞结束时,才会被调入就绪队列。

    1. public class ThreadDemo8 {
    2. public static void main(String[] args) throws InterruptedException {
    3. Thread thread = new Thread(new Runnable() {
    4. @Override
    5. public void run() {
    6. for(int i = 0; i < 3; i++) {
    7. System.out.println("aaa");
    8. try {
    9. Thread.sleep(1000);
    10. } catch (InterruptedException e) {
    11. e.printStackTrace();
    12. }
    13. }
    14. }
    15. });
    16. thread.start();
    17. Thread.sleep(5000);
    18. //线程等待(阻塞)
    19. //当主线程走到join时就会阻塞,直到thread线程执行结束,才会执行主线程(thread线程肯定比main线程先结束)
    20. //可设置参数为最大阻塞时间
    21. thread.join();
    22. System.out.println("bbb");
    23. }
    24. }

        注意:当主线程执行到join就会阻塞,进入阻塞队列。直到thread线程执行结束,主线程才会进入就绪队列,就可以被操作系统内核调度。

    线程休眠

        线程休眠会由就绪队列换到阻塞队列。当指定的休眠时间结束,就可以由阻塞队列回到就绪队列。这样才可以被调度,所以休眠时间会大于等于我们指定的时间。java里使用sleep()方法,参数以毫秒位单位。

        注意:上述代码里使用的sleep均为线程休眠。

    线程状态

        1)NEW:有线程对象,但没有启动线程。

        2)RUNNABLE:1)线程正在cpu上执行 2)线程处于就绪队列,随时可以被调度。

        3)TERMINATED:线程结束,pcb已经释放,但是线程对象还在。

        线程阻塞时状态:

        4)TIMED_WAITING:线程阻塞,处于sleep,wait,join等。

        5)BLOCKED:等待锁产生的阻塞。

        6)WAITING:等待其他线程来通知。

    线程状态之间切换

     注意:

        线程状态之间切换主线是由NEW ---> RUNNABLE ---> TERMINATED。阻塞时的一些状态都是线程已经执行起来了,在这个基础上的一些不同方式的阻塞。线程在等待锁产生的阻塞就是BLOCKED。线程遇到sleep,join,wait等方式阻塞是TIMED_WAITING状态。当线程需要其他线程来通知时是WAITING状态。

    代码观察线程的状态

    1. public class ThreadDemo9 {
    2. public static void main(String[] args) throws InterruptedException {
    3. Thread thread = new Thread(new Runnable() {
    4. @Override
    5. public void run() {
    6. for(int i = 0; i < 3; i++) {
    7. try {
    8. Thread.sleep(2000);
    9. } catch (InterruptedException e) {
    10. e.printStackTrace();
    11. }
    12. System.out.println("aaa");
    13. }
    14. }
    15. });
    16. //NEW
    17. //有线程对象,但是没有启动线程
    18. System.out.println(thread.getState());
    19. thread.start();//一个线程只能start一次
    20. //RUNNABLE
    21. //可执行的,1.就绪队列 2.正在cpu上执行的
    22. for(int i = 0; i < 10; i++) {
    23. System.out.println(thread.getState());
    24. }
    25. //TIMED_WAITING
    26. //当线程处于sleep时,这个时间获取线程状态
    27. //BLOCKED
    28. //等待锁产生的最阻塞
    29. //WAIT
    30. //等待其他人来通知
    31. //(这三种状态都是在描述不同的阻塞状态)
    32. Thread.sleep(8000);
    33. //TERMINATED
    34. //线程执行结束,pcb已经释放,但对象还在
    35. System.out.println(thread.getState());
    36. }
    37. }

    线程安全

    线程安全测试

    1. class Cumsum {
    2. public int a = 0;
    3. public void add() {
    4. a++;
    5. }
    6. }
    7. public class ThreadDemo15 {
    8. public static void main(String[] args) {
    9. Cumsum cumsum = new Cumsum();
    10. Thread t1 = new Thread(new Runnable() {
    11. @Override
    12. public void run() {
    13. for(int i = 0; i < 5000; i++) {
    14. cumsum.add();
    15. }
    16. }
    17. });
    18. Thread t2 = new Thread(new Runnable() {
    19. @Override
    20. public void run() {
    21. for(int i = 0; i < 5000; i++) {
    22. cumsum.add();
    23. }
    24. }
    25. });
    26. t1.start();
    27. t2.start();
    28. try {
    29. t1.join();
    30. t2.join();
    31. } catch (InterruptedException e) {
    32. e.printStackTrace();
    33. }
    34. System.out.println(cumsum.a);
    35. }
    36. }

        注意:这里有两个线程t1和t2,分别针对a进行累加5000次,结果很显然不是10000。为什么呢?

    解析

        当两个线程都启动时,它们是并发执行的。线程是抢占式调度,随机执行的。执行一次a++需要有三条指令:1.首先将内存中数据读到cpu内存中(load) 2.将寄存器中数据加一(add) 3.将寄存器中数据写回内存(save)。

        由于这种抢占式调度,随机执行。这三条指令可以有无数种组合,只要第一个线程load完,没有save,这个期间第二个线程再去读内存中的数据,就会造成脏读问题(例如MySQL中的脏读),那么最终两个线程执行一次循环只会累加一次。如果在这个期间第二个线程执行了多次这三条指令,那么最终第一个线程执行一次循环,第二个线程执行多次循环也只累加一次。

        只有当第一个线程save完,第二个线程再去读内存中的数据,然后save。每个线程在读数据时保证在上一个线程save之后,这样数据就是正确的,这样的作法其实就是保证了这三条指令的原子性,可以通过加锁实现指令的原子性。

    1. //线程安全测试
    2. class Cumsum {
    3. public int a = 0;
    4. synchronized public void add() {
    5. a++;
    6. }
    7. }
    8. public class ThreadDemo15 {
    9. public static void main(String[] args) {
    10. Cumsum cumsum = new Cumsum();
    11. Thread t1 = new Thread(new Runnable() {
    12. @Override
    13. public void run() {
    14. for(int i = 0; i < 5000; i++) {
    15. cumsum.add();
    16. }
    17. }
    18. });
    19. Thread t2 = new Thread(new Runnable() {
    20. @Override
    21. public void run() {
    22. for(int i = 0; i < 5000; i++) {
    23. cumsum.add();
    24. }
    25. }
    26. });
    27. t1.start();
    28. t2.start();
    29. try {
    30. t1.join();
    31. t2.join();
    32. } catch (InterruptedException e) {
    33. e.printStackTrace();
    34. }
    35. System.out.println(cumsum.a);
    36. }
    37. }

        注意:加锁之后,数据就正确了。下篇详细介绍。

    线程安全总结:

        1)根本原因:抢占式执行,随机调度。

        2)代码结构,多个线程修改一个变量产生线程安全问题。多个线程修改多个变量,多个线程读同一个变量,一个线程修改一个变量都不会产生线程安全问题。

        3)原子性,保证一些指令不可拆分(加锁)。

        4)内存可见性问题(后面介绍)。

        5)指令重排序(编译器优化出bug)。

    小结:

        多线程的学习我们需要理解线程之间的关系,理清它们执行的逻辑,分析代码。这样会对我们学习有很大帮助。

  • 相关阅读:
    stylegan3相关代码报错解决
    【Linux】重定向|重新理解Linux下一切皆文件
    将网址转化为map或对象,方便取值
    ​Word处理控件Aspose.Words功能演示:在 Python 中将 Word 文档转换为 EPUB​
    数据结构初阶--单链表(讲解+类模板实现)
    线段树的区间修改
    Android Banner - ViewPager 02
    如何管理付费媒体预算:分配、风险与扩展
    图像处理:推导Canny边缘检测算法
    Linux OS源的问题记录
  • 原文地址:https://blog.csdn.net/weixin_62353436/article/details/128110760