【Java】遨游在多线程的知识体系中(一)

前言

因为知识比较多,想把文章字数控制在一定范围之内,本文只有先发一篇多线程部分篇幅,之后的知识也会马上赶出来的,有什么有问题的地方经管提出,会虚心接受,并且认真改正。
在这里插入图片描述


一、第一个多线程程序

1.1、Thread用法一

创建一个子类继承自Thread,重写Thread中的run方法,这个方法内部就包含了这个线程要执行的代码(每一个线程都是一个独立的执行流,要执行一些代码),当线程跑起来了,就会依次执行这个run方法的代码。

class MyThread1 extends Thread{
    
        //自己定义的类
    @Override
    public void run() {
    
    
    	//这里才是具体要执行什么
        System.out.println("hello,Thread!");
    }
}


public class Thread1   {
    
    
    public static void main(String[] args) {
    
    
        //要想创建出这个线程,需要做的两件事
        //1.创建Thread实例,此处创建的是MyThread1

        MyThread1 t = new MyThread1();
        //2.调用Thread的start方法,才是真正在系统内部,创建线程
        t.start();
    }
}

大家要记住在继承了Thread类里面,重写run方法,run方法里面才是具体要执行什么。重写方法快捷键 ctrl+O (光标在那个继承的多线程类)

在这里插入图片描述

调用Thread的start方法,才是真正在系统内部创建线程!!! 调用start就会在系统中创建出一个新的线程!
在这里插入图片描述
mian()方法自身也是通过一个线程来执行的(一个进程里面不可能一个线程也没有,只是得有一个线程),现在通过 t.start()就又创建了一个新的线程

在这里插入图片描述
上面的图,是先执行main的线程,然后调用了t.start()就进start线程输出语句,这个两个线程之间是并发执行的关系(宏观上同时执行)


我们怎么知道他是并发执行的呢,接下来我们可以做个小实验,main线程和MyThread2线程都加上死循环,同时跑就可以看的出来
在这里插入图片描述

class  MyThread2 extends Thread{
    
    
    @Override
    public void run() {
    
    
        while(true){
    
    
            System.out.println("Hello,thread");
            //由于会很快执行下去,我们加一个小延迟
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}

public class Thread2 {
    
    
    public static void main(String[] args) {
    
    
        //这是主线程
        MyThread2 t2= new MyThread2();
        t2.start();  //执行t2线程

        //以下是执行主线程
        while(true){
    
    
            System.out.println("hello , main");
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}










在这里插入图片描述

执行流程:
在这里插入图片描述

我们也可以看的出它并不是你执行一次,我执行一次,偶尔会你两条我两条,多个线程之间执行的先后顺序并不是完全确定的,当1s时间到了之后,到底系统先唤醒哪个线程,不确定的!!(取决操作系统内部调度代码的具体实现)
在这里插入图片描述

如果多线程之间没有手动控制先后顺序这个时候让我多个线程之间执行是:“随机顺序”,这个是多线程编程的万恶之源。


1.2 、Jconsole工具使用

我们除了上面看线程的运行, 还有什么可以直观的看见这两个代码的线程吗

使用JDK自带的Jconsole工具 ,JDK中包含了很多的exe程序,这些都是开发/运行/调试 所 实用的一些工具
在这里插入图片描述

在这里插入图片描述

下面的图罗列的是计算机上(代码要运行起来才可以看见)的java进程,没有罗列所有,只是java进程(毕竟是java的JDK)
在这里插入图片描述

没有名字的一般是系统搞的进程,现在自己的代码连接
在这里插入图片描述

在这里插入图片描述
jconsole其实相当一个“监控程序”,能够看到一个java进程内部很多的详细信息,类似于医院的x光片一样
在这里插入图片描述
和我们代码相关的就只有2个其他是jvm自带的:
在这里插入图片描述
这俩线程,是代码密切相关的,其他是jvm自动创建的线程。这些线程就完成了一些辅助工作,比如统计jvm内部一些情况信息,并通过网络的形式通过给其他的程序,再比如进行垃圾回收的操作,也需要在这里通过一些特定的线程来完成。


我们点击线程主要看的是状态和堆栈追踪
在这里插入图片描述
在这里插入图片描述
类似与进程,线程也是有自己的状态的,TIMED_WAITING 这个状态就是“阻塞状态”(睡眠的状态)这个状态就是由sleep方法引起的。

堆栈跟踪的信息是你点击的那一瞬间信息
在这里插入图片描述


1.3、Thread用法二

创建一个类实现Runnable接口(也是标准库自带的一个接口),也是重写run方法

创建Thread实例,然后把刚才的Runnable实例给设置进去

//Runnable接口表示一个”任务“
class MyRunnable implements Runnable{
    
    
    @Override
    public void run() {
    
    
        //重新写run方法 描述了任务具体要进行的工作
        while(true){
    
    
            System.out.println("hello thread");
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}

public class Demo3 {
    
    

    public static void main(String[] args) {
    
    
        MyRunnable myRunnable = new MyRunnable();
        Thread t = new Thread(myRunnable);
        t.start();


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

也可以直接把MyRunnable放进去
在这里插入图片描述
这种写法和上面的写法十分相似 第一种写法是通过继承Thread实现的,第二种是通过实现Runnable实现的(通过Runnable这种方式,相当于把要执行的任务和Thread类进行了分离(解耦合)) 一般建议采取第二种写法。


1.4、Thread用法三 / 用法四

其实和上面的用法一和用法二没有本质的区别,写法换了个模样(匿名内部类)

public class Demo4 {
    
    
    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) {
    
    
                        e.printStackTrace();
                    }
                }
            }
        };
        
        t.start();
        
        while (true){
    
    
            System.out.println("hello main");

            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}

注意分号位置
在这里插入图片描述

这个就是创建了一个匿名子类(匿名内部类)继承自Thread,接下来调用start()


此处是Runnable的匿名内部类并创建出了实例 直接交给Thread来进行使用

public class Demo5 {
    
    
    public static void main(String[] args) {
    
    
        Thread t  = new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                System.out.println("hello thread");
                try {
    
    
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
        });

        t.start();

        System.out.println("hello main");
        try {
    
    
            Thread.sleep(1000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}


1.5、Thread用法五

还可以使用lambda表达式

public class Demo6 {
    
    
    public static void main(String[] args) {
    
    
        Thread t=  new Thread(()->{
    
    
            while(true) {
    
    
                System.out.println("hello thread");
                try {
    
    
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
        });
        t.start();
        while(true){
    
    
            while(true) {
    
    
                System.out.println("hello main");
                try {
    
    
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
        }
    }
}

使用lambda表达式代替了Runnable 更简洁一些


以上这些用法总结在一起都是一样的

  1. 描述清楚要执行的任务是什么
  2. 把这个任务加到一个Thread实例中,并调用start方法

上面的写法都是调用start引起了run的执行,如果咋们直接调用run会怎么样呢?

public class Demo7 {
    
    
    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) {
    
    
                        e.printStackTrace();
                    }
                }
            }
        };

        //不直接start 直接run
        t.run();

        while (true){
    
    
            System.out.println("hello main");

            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}

在这里插入图片描述

一直打印的是hello thread,没有打印hello main,因为当前run只是一个普通方法的调用,没有创建出新的线程,

当前代码只有一个main这样的主线程,要main线程执行完第一个循环才可以执行后续的代码,

但是第一个循环是死循环,永远执行不到第二个循环了,(不是并发执行了,而是串行执行)

start这个方法创建了新线程,新线程中执行run方法,run方法自身不具备创建线程的能力,仍然在旧的线程中执行。

从Jconsole里看只有main线程,没有Thread-0了

在这里插入图片描述

经典面试题:start和run有啥区别?


1.6、多线程的优势-增加运行速度

并发编程到底有什么用呢?能够解决什么问题?

并发编程(多线程)最明显的优势,就是针对“cpu密集型”的程序,能够提高效率

public class Demo8 {
    
    
    //串行执行
    public static void serial(){
    
    
        //针对两个整数进行反复的自在增操作
        //通过currentTimeMills可以记录当前的系统时间戳(毫秒级)
        long  beg = System.currentTimeMillis();
        long a  =0;
        for (long i = 0 ; i<10_0000_0000;i++){
    
      //10_0000_0000 (十亿)
            a++;
        }

        long b  =0;
        for (long i = 0 ; i<10_0000_0000;i++){
    
    
            b++;
        }
        long  end = System.currentTimeMillis();
        System.out.println("消耗时间:"+(end - beg) + "ms");
    }


    public static void main(String[] args) {
    
    
        serial();
    }
}

经过几次运行,时间差不多在640多ms(串行执行)
在这里插入图片描述

多线程执行结果:

public class Demo8 {
    
    
    //串行执行
    public static void serial(){
    
    
        //针对两个整数进行反复的自在增操作
        //通过currentTimeMills可以记录当前的系统时间戳(毫秒级)
        long  beg = System.currentTimeMillis();
        long a  =0;
        for (long i = 0 ; i<10_0000_0000;i++){
    
    
            a++;
        }

        long b  =0;
        for (long i = 0 ; i<10_0000_0000;i++){
    
    
            b++;
        }
        long  end = System.currentTimeMillis();
        System.out.println("消耗时间:"+(end - beg) + "ms");
    }

    public static void concurrency() throws InterruptedException {
    
    

        //这里的记时要记录两个线程执行的最慢时间(整体执行结束时间)
        long  beg = System.currentTimeMillis();

        Thread t1 = new Thread(()->{
    
    
            long a  =0;
            for (long i = 0 ; i<10_0000_0000;i++){
    
    
                a++;
            }
        });

        t1.start();

        Thread t2 = new Thread(()->{
    
    
            long b  =0;
            for (long i = 0 ; i<10_0000_0000;i++){
    
    
                b++;
            }
        });

        t2.start();


        //join的效果等待线程结束, t1.join意思就是等到t1执行完了,才会返回继续往下走

        //等上面的两个线程执行完 在往下走进行计时
        t1.join();
        t2.join();
        //因为是并发执行t1 t2 没有执行完毕就到end了 所以要换一种

        long end = System.currentTimeMillis();
        System.out.println("消耗时间:"+(end  - beg) + "ms");
    }

    public static void main(String[] args) throws InterruptedException {
    
    
        //serial();
        concurrency();
    }
}

可以看见时间要比串行少,注意代码中的注释中 join方法,之后也会继续讲解
在这里插入图片描述
在实际开发中,有时候,如果执行的任务时间太长了就可以考虑是否能使用多线程来进行任务拆分,提高执行的速度!!,


二、Thread 类及常见方法

2.1、Thread 的常见构造方法

在这里插入图片描述

Thread(String name ) 通过name属性就可以给线程起个名字,方便程序猿来调试.(name对于代码执行没有任何)

public class Demo9 {
    
    
    public static void main(String[] args) {
    
    
        Thread t = new Thread(() -> {
    
    
            while (true) {
    
    
                System.out.println("hello , thread");
                try {
    
    
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
        }, "mythread");

        t.start();

        while (true) {
    
    
            System.out.println("hello,main");

            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }

    }

}

在这里插入图片描述
可以看见我们取的名字可以使用了,而且有效果了。这样我们就可以方便区分线程是哪一个,尤其是线程多了的时候,以下还有常见的属性
在这里插入图片描述

  • ID 是线程的唯一标识,不同线程不会重复
  • 名称是各种调试工具用到
  • 状态表示线程当前所处的一个情况,下面我们会进一步说明
  • 优先级高的线程理论上来说更容易被调度到
  • 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行
  • 是否存活,即简单的理解,为 run 方法是否运行结束了
  • 线程的中断问题,下面我们进一步说明

isAlive()

Thread t 这个变量和操作系统内核中的线程,生命周期不是完全相同的,t被创建了,内核里不一定有对应的线程,得是t.start调用了,才会有对应的线程。

  • 内核中的线程被销毁了(执行结束了)t不一定销毁,内核线程的run方法执行完了,也就结束了,t的话要等到Gc来进行释放
  • 拿到一个t变量,不能断言出内核里的对应线程也是同样存在,但是可以通过isAlive()来判断
  • true,内核线程存在
  • false 内核的线程已经执行完了or还没有开始执行

2.2、控制线程的几个具体操作

2.2.1 创建线程.start()方法

在操作系统内部创建一个出一个新的线程!

一定要记住只有调用了start才有新线程的出现

  • star和run的区别:
  • start才是真正驱使操作系统创建出新的线程,run只是描述线程要执行的任务,只是一个普通方法
  • 在Thread的子类中重写的run方法,就会被start里面创建的新线程来执行
  • start内部调用操作系统提供的api,创建线程,然后线程执行run方法

2.2.2 中断线程(让线程结束)

如果线程run完了,就让线程结束
在这里插入图片描述
像这样的无限循环的情况是结束不了的(当然如果强行终止进程,线程也就没有了)

  • 如果要用常规手法结束,这里提供2种方法
  1. 手动设置一个标志位来作为循环的判定条件
public class Demo10 {
    
    
    //通过这个变量来控制线程是否结束
    private static boolean isQuit =false;

    public static void main(String[] args) {
    
    
        Thread t = new Thread(()->{
    
    

            while(!isQuit){
    
    
                System.out.println("hello thread");
                try {
    
    
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }

        });
        t.start();

        //可以在main线程中通过修改isQuit的值来影响线程的输出
        try {
    
    
            Thread.sleep(5000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        isQuit =true;
    }
}

5秒后结束。
在这里插入图片描述
其实这个方法是有一定的缺陷的(后面在说),上面的isQuit是在t线程读取,在main中修改了。


  1. 我们可以使用第二种方法是Thread线程中自己提供的一个标准位~

我们虽然不可以直接引用这个标志位变量,我们可以通过一些方法来进行读写~

  • 可以通过Thread中的isInterrupted()方法来判定标志位是否为true(为true表示线程应该退出
  • 可以通过Thread中的interrupt方法来把这个标志位设为true

在这里插入图片描述

可以看见这个报异常了还是在继续运行,我们来看看怎么了

public class Demo11 {
    
    
    public static void main(String[] args) {
    
    
        Thread t =new Thread(){
    
    
            @Override
            public void run() {
    
    
                while(!this.isInterrupted()){
    
    
                    System.out.println("hello thread");
                    try {
    
    
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
            }
        };

        t.start();
        try {
    
    
            Thread.sleep(5000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }

        //休眠5秒后来控制t线程结束
        t.interrupt();
    }
}

在这里插入图片描述
当我们触发这个中断的时候也就只是打了个日志又继续启动了,并没有真的停下

在t线程中存在两种情况

  1. 执行打印和while循环判定(线程处于就绪状态)
  2. 进行sleep(线程处于阻塞状态/休眠状态)

调用interrupt方法的时候,如果线程处于就绪状态,此时是直接修改线程中的对应标志位

如果线程处于阻塞状态的时候,此时会引起InterruptedException

  • InterruptedException就是sleep可能会发生的异常

在这里插入图片描述

这个操作只是单纯的打印日志,没有打印完了还是会继续执行后面的代码。

之所以线程没有结束是catch里面没有作为,当前的catch的代码相当把这个interrupt操作给忽略了

interrupt这个方法,说是中断线程,但是并不是直接就立即马上的杀死线程,具体怎么退出还是线程来决定

类似于你在游戏,你妈叫你去买酱油,接下来有几种情况,一:立马就去,二:打完这把在去,三:装做没听见,以上都是自己决定的

interrupt代码处理方法,也是三种不同的处理方式
在这里插入图片描述

我们在异常那里加上break;

效果:
在这里插入图片描述

问:为什么不把interrupt设计成,一调用就把对应的线程给干掉呢?

答:在历史上是存在过的,这种操作不好,t和mian是并发执行的关系,当main执行interrupt的时候,t执行到哪里了,这个是不确定的,可能t快干完了,突然中断,还是不好收场的。


2.2.3 等待一个线程-join()

等待指定的线程执行完

  • 比如在mian线程中,调用t.join(),就是让main来等待t执行完毕(注意关系)
  • 当调用到join的时候方法就会阻塞等待,效果和sleep、类似的,代码执行到join就不会继续往下走了,会进入等待,等到t线程执行完(t的run方法结束了,join才能继续执行)
  • 通过join可以控制线程结束的先后循序,(手动控制,让谁先结束,谁后结)

通过前面的列子知道,多线程的顺序是不确定的,(抢占执行),这种不确定性,给代码添加了很多偶然性和随机性,可能会让代码出现一些奇怪的问题。

此处的join就是手动控制线程顺序的一种办法,控制的是结束的顺序。

public class Demo12 {
    
    
    public static void main(String[] args) {
    
    
        Thread t = new Thread(()->{
    
    
           for (int i = 0;i<5;i++){
    
    
               System.out.println("hello thread");
               try {
    
    
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
    
    
                   e.printStackTrace();
               }
           }
        });
        t.start();

        System.out.println("t 线程还没有结束");
        //主线程中使用join进行等待,t.join

        try {
    
    
            t.join();
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println("t 线程已经结束");
    }
}

在这里插入图片描述
进入join方法线程将会阻塞等待,不会立马执行 sout:线程已结束,要等t线程5次跑完才会到这里来说已经结束。

  • 不带参数的等待的join版本的等待策略是死等,不见不散
  • 带参数的join版本 通过参数可以指定一个等待时间(ms),这个时间就是join的最多等待多久时间(超时等待),没有等到就结束

在这里插入图片描述


2.2.4 获取当前线程引用

在某某个线程的代码中,拿到当前这个线程对应的Thread对象的引用,才可以做后续一些操作,很多和线程相关的操作,都是依赖这样的引用。

  1. 通过继承Thread,来创建线程 此时直接在run方法通过this,就能拿线程实例。
    在这里插入图片描述
  2. 更常见是的是使用Thread里面的静态方法,currentTread(),哪个线程调用这个静态方法,就能够返回哪个线程的Thread实列引用。这种获取方式,可以在任何代码中使用

在这里插入图片描述

这样的方式会发现this调用不了那个方法
在这里插入图片描述

因为仔细一看当前的run不是Thread的方法,而是Runnable的方法,因此this也就是指向的是Runnable,当然没有Thread类的属性和方法

为了解决这个问题,我们需要使用Thread的静态方法,currentThread()

在这里插入图片描述
此时run仍是Runnable的方法,但是通过这个Thread的currentThread来获取到线程实例的。


2.2.5 休眠当前线程

也是我们比较熟悉一组方法,有一点要记得,因为线程的调度是不可控的,所以,这个方法只能保证实
际休眠时间是大于等于参数设置的休眠时间的

在这里插入图片描述
下面来说一下这个休眠在操作系统到底大概是个什么 样的情况

进程管理,pcb是来描述一个进程,使用双向链表来组织这些pcb的,这个说法是针对的是,一个进程只有一个线程的情况,更多的时候,是一个进程中有多个线程,每个线程对应一个pcb,此时一个进程对应了一组pcb,操作系统是以pcb为单位进行调度执行的。

如下图所示:
在这里插入图片描述
阻塞队列中的pcb不会理解被系统调度到cpu执行在这个阻塞队列中唯一的意义就是等,等到时机成熟了之后,这个pcb就回到就绪队伍中。

sleep时间到了,只是意味着这个pcb回到了就绪队列,并不是就直接在cpu上执行。sleep(1000)只是回到就绪队列中,至于多久运行,还是得看cpu的系统调度


三、线程的状态

3.1 观察线程的所有状态

讲进程状态的时候只是说了两个状态,就绪,阻塞 / 睡眠,实际上对于线程来说,存在的状态是更详细的,并且java对于线程的状态封装,并且给出了一些具体的描述,咱们去了解java中的线程状态,是非常有用的,很多时候调试一些多线程程序是用的上的。

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: 工作完成了

java标准库把线程状态给分成很多种,但是这些最关键的仍然就是两种,就绪,和阻塞,java把阻塞又细分了。


NEW:把Thread对象创建出来了,但是内核里面的线程还没有创建

public class Demo {
    
    
    private static Object locker = new Object();
    public static void main(String[] args) {
    
    
        Thread t =new Thread(()->{
    
    

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

在这里插入图片描述


TERMINATED:内核里的线程已经结束了,然后Thread对象还在

public class Demo {
    
    
    private static Object locker = new Object();
    public static void main(String[] args) {
    
    
        Thread t =new Thread(()->{
    
    
           
        });
        System.out.println(t.getState());
        t.start();

        //由于t的run方法啥都没有干,一瞬间就完成了
        //这里sleep2000就是充分等待t执行完毕
        try {
    
    
            Thread.sleep(2000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }

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

在这里插入图片描述


RUNNABLE:就绪状态

public class Demo {
    
    
    public static void main(String[] args) {
    
    
        Thread t =new Thread(()->{
    
    
            //这个代码在里面啥都不写
            while(true){
    
    

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

        //由于t的run方法啥都没有干,一瞬间就完成了
        //这里sleep2000就是充分等待t执行完毕
        try {
    
    
            Thread.sleep(2000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }

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

在这里插入图片描述


TIMED_WAITING:通过sleep产生

public class Demo {
    
    
    public static void main(String[] args) {
    
    
        Thread t =new Thread(()->{
    
    
            //这个代码在里面啥都不写
            while(true){
    
    
                try {
    
    
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
        });
        System.out.println(t.getState());
        t.start();

        //由于t的run方法啥都没有干,一瞬间就完成了
        //这里sleep2000就是充分等待t执行完毕
        try {
    
    
            Thread.sleep(2000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }

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

在这里插入图片描述

接下来还有2个状态我们留到后面在说


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

在这里插入图片描述
我们可以画一个线程状态之间的转换关系图
在这里插入图片描述


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

整个多线程最关键的点来了!!!
在这里插入图片描述

4.1 观察线程不安全

编写多线程的时候,如果当前代码中因为多线程随机调度顺序,导致程序出现bug,就称为线程不安全,如果我们的多线程代码,不管系统按照啥情况来调度,也不会导致bug,就称为“线程安全”

一个经典的线程不安全案例:两个线程进行变量累加。

class Count{
    
    
    public int count = 0;

    public void increase(){
    
    
        count++;
    }
}

public class Demo15 {
    
    

    private static Count counter =new Count();

    public static void main(String[] args) throws InterruptedException {
    
    
        //创建两个线程,两个线程分别对counter调用5w次 increase操作
        Thread t1 = new Thread(()->{
    
    
            for (int i =0; i<50000 ;i++){
    
    
                counter.increase();
            }
        });

        Thread t2 = new Thread(()->{
    
    
            for (int i =0; i<50000 ;i++){
    
    
                counter.increase();
            }
        });
        t1.start();
        t2.start();

        //阻塞等待线程累加结束
        // 如果是t2先执行完t1后执行完也没事, t1.join ,main就会阻塞等待t1线程,这个时候t2执行完了,t1还没有执行完
        // 过了一会,t1线程执行完了,于是t1.join就返回,继续调用t2.join,由于t2已经执行完了的t2.join 可以立马返回,不必阻塞等待

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

        System.out.println(counter.count);
    }
}

两次累加应该是10万,可是后面的结果是:

在这里插入图片描述
可以看见结果并不是10w,而且每次执行结果不太一样,值有可能会出现10w,但是情况很小这个bug这是因为线程的调度的随机性导致的。

为什么呢,我们来看看count++到底干了什么

  1. 把内存中的值,读到cpu寄存器中(load)
    在这里插入图片描述
  2. 把寄存器中的0进行加一(add)
    在这里插入图片描述
    3.把寄存器中的1写回到内存中(save)
    在这里插入图片描述

如果是两个线程,同时操作这个count,此时由于线程之间的随机调度的过程,就可能产生不一样的结果。

如果线程按照这个流程走那还是没有问题的在这里插入图片描述

在这里插入图片描述
由于操作系统的调度是随机的上面情况可能发生,下面的这样情况也可能发生在这里插入图片描述
在这个情况中,add了两次但是count还是1,因为当t2 add之后保存了是个1,但是 t1里面还是0,加1也还是1。
在这里插入图片描述
这里情况有很多种不安全的情况,只有在串行的时候情况才安全。

所以代码结果不正确,触发都是串行的时候才正确,但是情况太小了出现的概率。
在这里插入图片描述


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

一个典型的解决方案就是加锁,通过加锁操作,就可以把上述的“无序”,给变成“有序”,把上面说的“随机”,变成“确定”。

java里面给线程加锁的方案有很多,其中一个最常用的方案叫做,synchronize(内置关键字)

在这里插入图片描述
加了这个synchronize关键字之后,就相当于进increase方法,就先加锁,出了这个increase方法就解锁。

class Count{
    
    
    public int count = 0;

  synchronized  public void increase(){
    
    
        count++;
    }
}

public class Demo15 {
    
    

    private static Count counter =new Count();

    public static void main(String[] args) throws InterruptedException {
    
    
        //创建两个线程,两个线程分别对counter调用5w次 increase操作
        Thread t1 = new Thread(()->{
    
    
            for (int i =0; i<50000 ;i++){
    
    
                counter.increase();
            }
        });

        Thread t2 = new Thread(()->{
    
    
            for (int i =0; i<50000 ;i++){
    
    
                counter.increase();
            }
        });
        t1.start();
        t2.start();

        //阻塞等待线程累加结束
        // 如果是t2先执行完t1后执行完也没事, t1.join ,main就会阻塞等待t1线程,这个时候t2执行完了,t1还没有执行完
        // 过了一会,t1线程执行完了,于是t1.join就返回,继续调用t2.join,由于t2已经执行完了的t2.join 可以立马返回,不必阻塞等待

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

        System.out.println(counter.count);
    }
}


在这里插入图片描述
现在图变成这样:只有遇见unlock才会执行下一个线程,不然会触发阻塞等待的线程
在这里插入图片描述
(像ATM机一样,进去了一个人把门锁了,就不可以进去第二个人了,要第一个人出来,第二个人才可以进去)


我们引入多线程,目的就是为了实现并发编程(提高速度),当加锁了之后,数据结果是对了,但是并发性降低了,速度也就慢下来了,追求熟读,还是追求准确?两个并发的线程,可能各自要完成的任务很多,总体来说线程还是很有意义的,接下来看看我们怎么解决上面的加锁之后的问题吧?
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_46874327/article/details/127579939