【JavaEE系列】01_多线程

一、线程概念

1、线程是什么

一个线程就是一个 “执行流”。每个线程之间都可以按照顺讯执行自己的代码。多个线程之间 “同时” 执行着多份代码。

进程包含了线程,一个线程对应一个PCB,一个进程对应一组PCB。

2、为什么要有线程

首先, “并发编程” 成为 “刚需”。

  • 单核 CPU 的发展遇到了瓶颈。 要想提高算力, 就需要多核 CPU。 而并发编程能更充分利用多核 CPU 资源。
  • 有些任务场景需要 “等待 IO”, 为了让等待 IO 的时间能够去做一些其他的工作, 也需要用到并发编程。

其次, 虽然多进程也能实现并发编程, 但是线程比进程更轻量。

  • 创建线程比创建进程更快。
  • 销毁线程比销毁进程更快。
  • 调度线程比调度进程更快。

最后, 线程虽然比进程轻量, 但是人们还不满足, 于是又有了 “线程池”(ThreadPool) 和 “协程”(Coroutine)

3、进程与线程之间的区别

描述进程与线程的区别主要考虑以下四个方面:
1、首先说明进程与线程之间的关系 : 进程中包含一条或多条线程;
2、第二从系统管理与分配资源和CPU调度的角度来分析:进程是系统分配资源的最小单位,线程是CPU调度的最小单位;
3、第三从资源使用角度来分析:进程之间不能共享资源,进程中的线程之间共享进程的所有资源;
单独介绍一下线程的特点:线程的创建、销毁、调度效率比进程更高,并且有自己独立的执行任务。

1、进程包含线程
2、线程比进程更轻量化,创建更快,销毁也更快
3、同一个进程的多个线程之间共用一份内存/文件资源,进程和进程之间,则是独立的内存/文件资源。
4、进程是资源分配的基本单位,线程是调度执行的基本单位。

在这里插入图片描述

4、 Java 的线程和操作系统线程

线程是操作系统中的概念。操作系统内核实现了线程这样的机制, 并且对用户层提供了一些API供用户使用(例如 Linux 的 pthread 库)。
Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装。

二、创建线程

创建线程共有5种写法:
1、继承Thread,重写run方法
2、实现Runnable,重写run方法
3、继承Thread,使用匿名内部类
4、实现Runnable,使用匿名内部类
5、使用lambda表达式

1、方法一:继承Thread类

  1. 继承 Thread 来创建一个线程类
class MyThread extends Thread {
    
    
    @Override
    public void run() {
    
    
        System.out.println("这里是线程运行的代码");
   }
}
  1. 创建 MyThread 类的实例
MyThread t = new MyThread();
  1. 调用 start 方法启动线程
t.start(); // 线程开始运行

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

package threading;

class MyThread extends Thread{
    
    
    @Override
    public void run(){
    
    
        System.out.println("hello thread");
    }
}
//演示多线程的基本创建方式
public class Demo1 {
    
    
    public static void main(String[] args) {
    
    
        MyThread t=new MyThread();
        t.start();//另外启动一个线程来执行Thread中的run方法
    }
}

在这个代码中,只有一个进程,两个线程,一个是main方法对应的主线程(是JVM创建的,main就相当于线程的入口方法),另一个线程是MyThread,run就是这个新线程的入口方法。

并发执行的意思是两边同时执行,各自执行各自的。

在这里插入图片描述
完整代码:

package threading;

class MyThread extends Thread{
    
    
    @Override
    public void run(){
    
    
        while(true){
    
    
            System.out.println("hello thread");
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}
/**
 * @author Susie-Wen
 * @version 1.0
 * @description:
 * @date 2022/8/18 9:30
 */

//演示多线程的基本创建方式
public class Demo1 {
    
    
    public static void main(String[] args) {
    
    
        MyThread t=new MyThread();
        t.start();
        while(true){
    
    
            System.out.println("hello mian");
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}

在这里插入图片描述

2、方法二:实现Runnable接口

  1. 实现 Runnable 接口
class MyRunnable implements Runnable {
    
    
    @Override
    public void run() {
    
    
        System.out.println("这里是线程运行的代码");
   }
}
  1. 创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为 target 参数
Thread t = new Thread(new MyRunnable());
  1. 调用 start 方法
t.start(); // 线程开始运行

完整代码:

package threading;

class MyRunnabbe implements Runnable{
    
    
    @Override
    public void run(){
    
    
        while(true){
    
    
            System.out.println("hello thread");
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}
//通过重写Runnable来实现创建线程
public class Demo2 {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        MyRunnabbe runnabbe=new MyRunnabbe();
        Thread t=new Thread(runnabbe);
        t.start();
        while(true){
    
    
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

在这里插入图片描述
在这里插入图片描述

把任务提取出来,目的仍然是为了解耦合,前面继承Thread写法,就把线程要完成的工作和线程本身耦合在一起了,假设未来要对这个代码进行调整(不用多线程了,用其他方式)代码改动就比较大。
而Runnable这种写法,就只是需要把Runnable传给其他的实体即可。

如果想搞多线程,都干一样的活,这个时候也更适合使用Runnable。

3、其他类型

  • 匿名内部类创建 Thread 子类对象
// 使用匿名类创建 Thread 子类对象
Thread t1 = new Thread() {
    
    
    @Override
    public void run() {
    
    
        System.out.println("使用匿名类创建 Thread 子类对象");
   }
};
  • 匿名内部类创建 Runnable 子类对象

匿名内部类的实例,作为构造方法的参数。

// 使用匿名类创建 Runnable 子类对象
Thread t2 = new Thread(new Runnable() {
    
    
    @Override
    public void run() {
    
    
        System.out.println("使用匿名类创建 Runnable 子类对象");
   }
});
  • lambda 表达式创建 Runnable 子类对象
// 使用 lambda 表达式创建 Runnable 子类对象
Thread t3 = new Thread(() -> System.out.println("使用匿名类创建 Thread 子类对象"));
Thread t4 = new Thread(() -> {
    
    
    System.out.println("使用匿名类创建 Thread 子类对象");
});

4、多线程的优势

package threading;

/**
 * @author Susie-Wen
 * @version 1.0
 * @description:
 * @date 2022/8/18 14:03
 */
public class Demo5 {
    
    
    public static final long COUNT=100_0000_0000L;

    public static void main(String[] args) throws InterruptedException {
    
    
        serial();
        concurrency();
    }
    //串行执行任务
    public static void serial(){
    
    
        //计算毫秒级别的时间戳
        long begin=System.currentTimeMillis();
        long a=0;
        for(long i=0;i<COUNT;i++){
    
    
            a++;
        }
        a=0;
        for(long i=0;i<COUNT;i++){
    
    
            a++;
        }
        long end=System.currentTimeMillis();
        System.out.println("串行执行时间间隔:"+(end-begin)+"ms");
    }
    //并发执行任务
    public static void concurrency() throws InterruptedException {
    
    
        long begin=System.currentTimeMillis();

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

        Thread t2=new Thread(()->{
    
    
           long a=0;
           for(long i=0;i<COUNT;i++){
    
    
               a++;
           }
        });

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

        t1.join();
        t2.join();//join阻塞等待线程的结束

        long end=System.currentTimeMillis();
        System.out.println("并行执行时间间隔:"+(end-begin)+"ms");
    }
}

在这里插入图片描述

代码运行结果:

在这里插入图片描述

同样的操作:
1、一个线程串行执行:6638ms
2、两个线程并发执行:3448ms

并发执行=微观上的并行+并发
这两个线程执行过程中,多少次是并行的(确实快了),多少次是并发的(总时间并没有减少,反而因为调度开销变长了)

  • 如果计算量大,计算的久,创建线程的开销就更不明显(忽略不计)。
  • 如果计算量小,计算的快,创建线程的开销影响更大,多线程的提升,就更不明显。

5、jconsole查看java进程

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在相应的路径下找到:jconsole.exe(用来辅助调试程序的工具),双击打开。
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
此时就可以查看:
在这里插入图片描述

在这里插入图片描述

三、Thread 类及常见方法

Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关联。

用我们上面的例子来看,每个执行流,也需要有一个对象来描述,类似下图所示,而 Thread 类的对象就是用来描述一个线程执行流的,JVM 会将这些 Thread 对象组织起来,用于线程调度,线程管理。

在这里插入图片描述

1、 Thread 的常见构造方法

方法 说明
Thread() 创建线程对象
Thread(Runnable target) 使用 Runnable 对象创建线程对象
Thread(String name) 创建线程对象,并命名
Thread(Runnable target, String name) 使用 Runnable 对象创建线程对象,并命名
【了解】Thread(ThreadGroup group,Runnable target) 线程可以被用来分组管理,分好的组即为线程组,这个目前我们了解即可
public class Demo6 {
    
    
    public static void main(String[] args) {
    
    
        Thread t=new Thread(()->{
    
    
           while(true){
    
    
               System.out.println("hello");
               try {
    
    
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
    
    
                   e.printStackTrace();
               }
           }
        },"这是俺的线程");
        t.start();
    }
}

在这里插入图片描述

2、 Thread 的几个常见属性

属性 获取方法
获取ID getId()
获取线程的名称 getName()
状态,获取线程的状态 getState()
优先级,获取线程的优先级 getPriority()
是否后台线程 isDaemon()
是否存活 isAlive()
是否被中断 isInterrupted()

1、getId():这个属性获取的Id是Java中给Thread对象安排的身份标识,和操作系统内核中的PCB的pid,以及和操作系统提供的线程API中的线程id都不是一回事。身份标识可以有多个,在不同的环境下,使用不同的标识。

对于一个线程来说:
(1)在JVM中有一个id
(2)在操作系统的线程API有个id
(3)内核PCB中还有一个id
这三个id的效果是一样的,都是为了区分身份,但是是在不同的环境当中使用的。

2、getName():这里获取的name是刚才在构造方法里面指定的name

3、isDaemon():判断是否是后台线程,因为默认创建的线程是"前台线程",前台线程会组织进程退出,如果main运行完了,前台线程还没完,则进程不会退出。如果main等其他的前台线程执行完了,这个时候,即使后台线程没执行完,进程也会退出。

在这里插入图片描述测试属性:

package threading;

public class Demo7 {
    
    
    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();
        System.out.println(t.getId());
        System.out.println(t.getName());
        System.out.println(t.getPriority());
        System.out.println(t.getState());
        System.out.println(t.isDaemon());
        System.out.println(t.isAlive());
        System.out.println(t.isInterrupted());
    }
}

在这里插入图片描述

3、启动一个线程start

只用调用start才会真正创建线程,不调用start,就没有创建线程(在内核里创建PCB)。

  • 覆写 run 方法是提供给线程要做的事情的指令清单。
  • 线程对象可以认为是把 李四、王五叫过来了。
  • 而调用 start() 方法,就是喊一声:”行动起来!“,线程才真正独立去执行了。

start()和run()的区别:

1、作用功能不同:
run方法的作用是描述线程具体要执行的任务; start方法的作用是真正的申请系统线程;
2、运行结果不同:
run方法是一个类中的普通方法,主动调用和调用普通方法一样,会顺序执行一次; start调用方法后, start方法内部会调用Java本地方法(封装了对系统底层的调用)真正的启动线程,并执行run方法中的代码,run 方法执行完成后线程进入销毁阶段。

  • 直接调用run,则并没有创建线程,只是在原来的线程中运行的。
  • 调用start,则是创建了线程,在新线程中执行代码(和原来的线程是并发的)。

在这里插入图片描述
调用 start 方法, 才真的在操作系统的底层创建出一个线程。

run方法执行完了,线程就提前结束了,有没有办法让线程提前一点结束呢?有的,就是通过线程中断的方式来进行的。(本质仍然是让run方法尽快结束,而不是run执行一半强制结束)

这就取决于run里面具体是如何实现的了。

1、直接自己定义一个标志位,作为线程是否结束的标记。
2、还可以使用标准库里面自带的一个标志位。

4、中断一个线程interrupt

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

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

  1. 通过共享的标记来进行沟通
  2. 调用 interrupt() 方法来通知

在Java中,中断线程并非是强制的,而是由线程自身的代码来进行判定处理的。

线程自身能怎么处理呢?
1、立即结束线程
2、不理会
3、稍后理会

public class Demo10 {
    
    
    public static void main(String[] args) {
    
    
        Thread t=new Thread(()->{
    
    
            while(!Thread.currentThread().isInterrupted()){
    
    
                System.out.println("hello thread");
                try {
    
    
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                    //方式一:立即结束线程
                    break;
                    //方式二:啥都不做,不理会,使得线程继续执行
                    //方式三:线程稍后处理
/*                    Thread.sleep(1000);
                    break;*/
                }

            }
        });
        t.start();
        try{
    
    
            Thread.sleep(3000);
        }catch(InterruptedException e){
    
    
            e.printStackTrace();
        }
        t.interrupt();
        System.out.println("设置让t线程结束");
    }
}

在这里插入图片描述
在这里插入图片描述

【1】使用自定义的变量来作为标志位

  • 需要给标志位上加 volatile 关键字
public class ThreadDemo {
    
    
    private static class MyRunnable implements Runnable {
    
    
        public volatile boolean isQuit = false;
        @Override
        public void run() {
    
    
            while (!isQuit) {
    
    
                System.out.println(Thread.currentThread().getName()
                        + ": 别管我,我忙着转账呢!");
                try {
    
    
                    Thread.sleep(1000);
               } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
               }
           }
            System.out.println(Thread.currentThread().getName()
                    + ": 啊!险些误了大事");
       }
   }
    public static void main(String[] args) throws InterruptedException {
    
    
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target, "李四");
        System.out.println(Thread.currentThread().getName()
                + ": 让李四开始转账。");
        thread.start();
        Thread.sleep(10 * 1000);
        System.out.println(Thread.currentThread().getName()
                + ": 老板来电话了,得赶紧通知李四对方是个骗子!");
        target.isQuit = true;
   }
}

【2】使用 Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替自定义标志位

Thread 内部包含了一个 boolean 类型的变量作为线程是否被中断的标记

方法 说明
public void interrupt() 中断对象关联的线程,如果线程正在阻塞,则以异常方式通知,否则设置标志位
public static boolean interrupted() 判断当前线程的中断标志位是否设置,调用后清除标志位
public boolean isInterrupted() 判断对象关联的线程的标志位是否设置,调用后不清除标志位
public class ThreadDemo {
    
    
    private static class MyRunnable implements Runnable {
    
    
        @Override
        public void run() {
    
    
            // 两种方法均可以
            while (!Thread.interrupted()) {
    
    
            //while (!Thread.currentThread().isInterrupted()) {
    
    
                System.out.println(Thread.currentThread().getName()
                        + ": 别管我,我忙着转账呢!");
                try {
    
    
                    Thread.sleep(1000);
               } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                    System.out.println(Thread.currentThread().getName()
                            + ": 有内鬼,终止交易!");
                    // 注意此处的 break
                    break;
               }
           }
            System.out.println(Thread.currentThread().getName()
                    + ": 啊!险些误了大事");
       }
   }
    public static void main(String[] args) throws InterruptedException {
    
    
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target, "李四");
        System.out.println(Thread.currentThread().getName()
                + ": 让李四开始转账。");
        thread.start();
        Thread.sleep(10 * 1000);
        System.out.println(Thread.currentThread().getName()
                + ": 老板来电话了,得赶紧通知李四对方是个骗子!");
        thread.interrupt();
   }
}

thread 收到通知的方式有两种:

  1. 如果线程因为调用 wait/join/sleep 等方法而阻塞挂起,则以 InterruptedException 异常的形式通
    知,清除中断标志。
  • 当出现 InterruptedException 的时候, 要不要结束线程取决于 catch 中代码的写法. 可以选择
    忽略这个异常, 也可以跳出循环结束线程
  1. 否则,只是内部的一个中断标志被设置,thread 可以通过
  • Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志
  • Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志

这种方式通知收到的更及时,即使线程正在 sleep 也可以马上收到。

【3】观察标志位是否清除

标志位是否清除, 就类似于一个开关

标志位是否清除, 就类似于一个开关:
Thread.isInterrupted() 相当于按下开关, 开关自动弹起来了. 这个称为 “清除标志位”。
Thread.currentThread().isInterrupted() 相当于按下开关之后, 开关弹不起来, 这个称为"不清除标志位"。

  • 使用 Thread.isInterrupted() , 线程中断会清除标志位
public class ThreadDemo {
    
    
    private static class MyRunnable implements Runnable {
    
    
        @Override
        public void run() {
    
    
            for (int i = 0; i < 10; i++) {
    
    
                System.out.println(Thread.interrupted());
           }
       }
   }
    public static void main(String[] args) throws InterruptedException {
    
    
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target, "李四");
        thread.start();
        thread.interrupt();
   }
}
true // 只有一开始是 true,后边都是 false,因为标志位被清
false
false
false
false
false
false
false
false
false
  • 使用 Thread.currentThread().isInterrupted() , 线程中断标记位不会清除
public class ThreadDemo {
    
    
    private static class MyRunnable implements Runnable {
    
    
        @Override
        public void run() {
    
    
            for (int i = 0; i < 10; i++) {
    
    
 System.out.println(Thread.currentThread().isInterrupted());
           }
                  }
   }
    public static void main(String[] args) throws InterruptedException {
    
    
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target, "李四");
        thread.start();
        thread.interrupt();
   }
}
true // 全部是 true,因为标志位没有被清
true
true
true
true
true
true
true
true
true

5、等待一个线程join

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

public class ThreadDemo {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        Runnable target = () -> {
    
    
            for (int i = 0; i < 10; i++) {
    
    
                try {
    
    
                    System.out.println(Thread.currentThread().getName() 
                                       + ": 我还在工作!");
                    Thread.sleep(1000);
               } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
               }
           }
            System.out.println(Thread.currentThread().getName() + ": 我结束了!");
       };
        Thread thread1 = new Thread(target, "李四");
        Thread thread2 = new Thread(target, "王五");
        System.out.println("先让李四开始工作");
        thread1.start();
        thread1.join();
        System.out.println("李四工作结束了,让王五开始工作");
        thread2.start();
        thread2.join();
        System.out.println("王五工作结束了");
   }
}
方法 说明
public void join() 等待线程结束
public void join(long millis) 等待线程结束,最多等 millis 毫秒
public void join(long millis, int nanos) 同理,但可以更高精度

关于这里最多等待几毫秒的方法:
服务器开发经常要处理客户端的请求,根据请求计算生成响应,就需要用到“等待时间”,称为"超时时间",客户端发了请求过来,等待响应,这个等待就不能无限的等,可以根据需要,约定1000ms,500ms,如果时间之内响应没有回来,客户端会直接提示“等待超时”。
(在实际开发当中会很少使用无限等待的方法,大多数都是指定了最大的等待时间,避免程序因为死等而出现卡死这样的情况)

6、获取当前线程引用

方法 说明
public static Thread currentThread(); 返回当前线程对象的引用
public class ThreadDemo {
    
    
    public static void main(String[] args) {
    
    
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName());
   }
}

7、 休眠当前线程

sleep:指定休眠的时间,让线程休息一会。
因为线程的调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的。

方法 说明
public static void sleep(long millis) throws InterruptedException 休眠当前线程 millis毫秒
public static void sleep(long millis, int nanos) throws InterruptedException 可以更高精度的休眠
public class ThreadDemo {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        System.out.println(System.currentTimeMillis());
        Thread.sleep(3 * 1000);
        System.out.println(System.currentTimeMillis());
   }
}

四、Thread的状态

1、观察线程的所有状态

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

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

2、线程状态和状态转移的意义

在这里插入图片描述

在这里插入图片描述

public class Demo11 {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        Thread t=new Thread(()->{
    
    
           for(int i=0;i<10000_0000;i++){
    
    
               //啥都不干
           }
        });
        //t开始之前,得到的就是NEW
        System.out.println(t.getState());
        t.start();
       // Thread.sleep(50);
        //t正在工作,得到的是RUNNABLE
        System.out.println(t.getState());
        t.join();
        //t结束之后,得到的状态就是terminate
        System.out.println(t.getState());
    }
}

在这里插入图片描述
在这里插入图片描述

  • 刚把李四、王五找来,还是给他们在安排任务,没让他们行动起来,就是 NEW 状态;
  • 当李四、王五开始去窗口排队,等待服务,就进入到 RUNNABLE 状态。该状态并不表示已经被银行工作人员开始接待,排在队伍中也是属于该状态,即可被服务的状态,是否开始服务,则看调度器的调度;
  • 当李四、王五因为一些事情需要去忙,例如需要填写信息、回家取证件、发呆一会等等时,进入BLOCKED 、 WATING 、 TIMED_WAITING 状态;
  • 如果李四、王五已经忙完,为 TERMINATED 状态。

所以,之前我们学过的 isAlive() 方法,可以认为是处于不是 NEW 和 TERMINATED 的状态都是活着的。

3、观察线程的状态和转移

【1】关注 NEW 、 RUNNABLE 、 TERMINATED 状态的转换

  • 使用 isAlive 方法判定线程的存活状态
public class ThreadStateTransfer {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        Thread t = new Thread(() -> {
    
    
            for (int i = 0; i < 1000_0000; i++) {
    
    
           }
       }, "李四");
        System.out.println(t.getName() + ": " + t.getState());;
        t.start();
        while (t.isAlive()) {
    
    
            System.out.println(t.getName() + ": " + t.getState());;
       }
        System.out.println(t.getName() + ": " + t.getState());;
   }
}

【2】关注 WAITING 、 BLOCKED 、 TIMED_WAITING 状态的转换

public static void main(String[] args) {
    
    
    final Object object = new Object();
    Thread t1 = new Thread(new Runnable() {
    
    
        @Override
        public void run() {
    
    
            synchronized (object) {
    
    
                while (true) {
    
    
                    try {
    
    
                        Thread.sleep(1000);
                   } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                   }
               }
           }
       }
   }, "t1");
    t1.start();
    Thread t2 = new Thread(new Runnable() {
    
    
        @Override
        public void run() {
    
    
            synchronized (object) {
    
    
                System.out.println("hehe");
           }
       }
   }, "t2");
    t2.start();
}

使用 jconsole 可以看到 t1 的状态是 TIMED_WAITING , t2 的状态是 BLOCKED

修改上面的代码, 把 t1 中的 sleep 换成 wait:

public static void main(String[] args) {
    
    
    final Object object = new Object();
    Thread t1 = new Thread(new Runnable() {
    
    
        @Override
        public void run() {
    
    
            synchronized (object) {
    
    
                try {
    
    
                    // [修改这里就可以了!!!!!]
                    // Thread.sleep(1000);
                    object.wait();
               } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
               }
           }
       }
   }, "t1");
 ...
}

使用 jconsole 可以看到 t1 的状态是 WAITING

【3】结论:

  • BLOCKED 表示等待获取锁, WAITING 和 TIMED_WAITING 表示等待其他线程发来通知
  • TIMED_WAITING 线程在等待唤醒,但设置了时限; WAITING 线程在无限等待唤醒

【4】yield() 让出 CPU

Thread t1 = new Thread(new Runnable() {
    
    
    @Override
    public void run() {
    
    
可以看到:
1. 不使用 yield 的时候, 张三李四大概五五开
2. 使用 yield, 张三的数量远远少于李四
结论:
yield 不改变线程的状态, 但是会重新去排队. 
4. 多线程带来的的风险-线程安全 (重点)
4.1 观察线程不安全
        while (true) {
    
    
            System.out.println("张三");
            // 先注释掉, 再放开
            // Thread.yield();
       }
   }
}, "t1");
t1.start();
Thread t2 = new Thread(new Runnable() {
    
    
    @Override
    public void run() {
    
    
        while (true) {
    
    
            System.out.println("李四");
       }
   }
}, "t2");
t2.start();
  1. 不使用 yield 的时候, 张三李四大概五五开
  2. 使用 yield 时, 张三的数量远远少于李四

结论:yield 不改变线程的状态, 但是会重新去排队。

五、多线程带来的的风险-线程安全

线程安全的意思,是在多线程各种随机的调度顺序下,代码没有bug,都能符合预期的方式来执行。
如果在多线程随机调度下代码出现bug,此时就认为是线程不安全的。

1、观察线程不安全

//演示线程不安全
class Counter{
    
    
    int count=0;
    public void increase(){
    
    
        count++;
    }
}
public class Demo12 {
    
    
    private static Counter counter=new Counter();
    public static void main(String[] args) throws InterruptedException {
    
    
        //两个线程,每个线程都针对这个counter来进行5W次自增。
        //预期结果是10W
        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();
        t1.join();
        t2.join();
        System.out.println("counter:"+counter.count);
    }
}

在这里插入图片描述

出现的问题:结果发现每次counter的数量是不同的,并且都小于10W。

原因:
进行count++操作的底层是三条指令在CPU上完成。
1、把内存的数据读取到CPU寄存器中。load
2、把CPU的寄存器中的值,进行+1。add
3、把寄存器当中的值写回到内存中。save

由于当前是两个线程修改一个变量,由于每次修改的三个步骤(不是原子的),由于线程之间的调度顺序是不确定的,因此两个线程在真正执行这些操作的时候,就可能有多种执行的排列顺序。

在这里插入图片描述


在这里插入图片描述

由于在调度过程中,出现“串行执行”两种情况的次数,和其他情况的次数不确定,因此得到的结果就是不确定的值。

考虑极端的情况:
1、如果两个线程之间的调度全是串行执行,则结果就是10W。
2、如果两个线程之间的调度全都是其他情况,一次串行执行都没有,则结果就是5W。

因为最终的结果是5W~10W之间的数字。

六、写作业

问题一:请回答以下代码的输出, 并解释原因:

public static void main(String[] args) {
    
    
    Thread t = new Thread(new Runnable() {
    
    
        @Override
        public void run() {
    
    
            System.out.println("1");
        }
    });
    t.start();
    System.out.println("2");
}

答案:此题可能输出结果为 2 1 或 1 2。由于打印1 和 打印2分别在不同的线程中执行,具体先执行哪个线程是由系统决定的,且无法预测,所以两种情况都有可能。

问题二:
编写代码, 实现多线程数组求和. 给定一个很长的数组 (长度 1000w), 通过随机数的方式生成 1-100 之间的整数. 实现代码,能够创建两个线程, 对这个数组的所有元素求和. 其中线程1 计算偶数下标元素的和, 线程2 计算奇数下标元素的和. 最终再汇总两个和,进行相加 记录程序的执行时间.

public class Thread_2533 {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        // 记录开始时间
        long start = System.currentTimeMillis();
 
        // 1. 给定一个很长的数组 (长度 1000w), 通过随机数的方式生成 1-100 之间的整数.
        int total = 1000_0000;
        int [] arr = new int[total];
        // 构造随机数,填充数组
        Random random = new Random();
        for (int i = 0; i < total; i++) {
    
    
            int num = random.nextInt(100) + 1;
            arr[i] = num;
        }
 
 
        // 2. 实现代码, 能够创建两个线程, 对这个数组的所有元素求和.
        // 3. 其中线程1 计算偶数下标元素的和, 线程2 计算奇数下标元素的和.
        // 实例化操作类
        SumOperator operator = new SumOperator();
        // 定义具体的执行线程
        Thread t1 = new Thread(() -> {
    
    
            // 遍历数组,累加偶数下标
            for (int i = 0; i < total; i += 2) {
    
    
                operator.addEvenSum(arr[i]);
            }
        });
 
        Thread t2 = new Thread(() -> {
    
    
            // 遍历数组,累加奇数下标
            for (int i = 1; i < total; i += 2) {
    
    
                operator.addOddSum(arr[i]);
            }
        });
 
        // 启动线程
        t1.start();
        t2.start();
        // 等待线程结束
        t1.join();
        t2.join();
 
        // 记录结束时间
        long end = System.currentTimeMillis();
        // 结果
        System.out.println("结算结果为 = " + operator.result());
        System.out.println("总耗时 " + (end - start) + "ms.");
    }
}
 
// 累加操作用这个类来完成
class SumOperator {
    
    
    long evenSum;
    long oddSum;
 
    public void addEvenSum (int num) {
    
    
        evenSum += num;
    }
 
    public void addOddSum (int num) {
    
    
        oddSum += num;
    }
 
    public long result() {
    
    
        System.out.println("偶数和:" + evenSum);
        System.out.println("奇数和:" + oddSum);
        return evenSum + oddSum;
    }
}

问题三: 在子线程执行完毕后再执行主线程代码 有20个线程,需要同时启动。 每个线程按0-19的序号打印,如第一个线程需要打印0
请设计代码,在main主线程中,等待所有子线程执行完后,再打印 ok

猜你喜欢

转载自blog.csdn.net/wxfighting/article/details/126398794