《Java 多线程编程核心技术》笔记——第1章 Java 多线程技能(一)

声明:

本博客是本人在学习《Java 多线程编程核心技术》后整理的笔记,旨在方便复习和回顾,并非用作商业用途。

本博客已标明出处,如有侵权请告知,马上删除。

本章主要介绍 Thread 类中的核心方法,Thread 类的核心方法较多,读者应该着重掌握如下关键技术点:

  • 线程的启动
  • 如何使线程暂停
  • 如何使线程停止
  • 线程的优先级
  • 线程安全相关的问题

1.1 进程和多线程的概念及线程的优点

  • 进程是受操作系统管理的基本运行单元

  • 线程是在进程中独立运行的子任务

    比如:QQ.exe 这个进程运行时就有很多子任务(好友视频,下载文件,发送表情)在同时运行,其中每一项任务都可以理解成是线程在工作

  • 多线程的优点:可以最大限度的利用 CPU 的空闲时间来处理其它的任务,提高 CPU 的利用率

    比如:一边让操作系统处理正在由打印机打印的数据,一边使用 Word 编辑文档,而 CPU 在这些任务间不停地切换,由于切换速度非常快,给使用者的感受就是这些任务似乎在同时运行。

以下是单任务运行环境的模型图:

在这里插入图片描述

  • 在单任务运行环境中,任务 1 和任务 2 是两个完全独立,互不相关的任务,任务 1 是在等待远程服务器返回数据,这时 CPU 一直处于等待状态,而任务 2 必须在任务 1 运行结束后才可以运行,虽然任务 2 执行时间仅有 1 秒,但却有 10 秒的等待时间,系统运行效率大幅降低
  • 单任务运行环境的特点就是排队执行,也就是同步
  • 单任务运行环境的缺点,即 CPU 利用率大幅降低

以下是多任务运行环境的模型图:

在这里插入图片描述

  • 在多任务运行环境中,CPU 完全可以在任务 1 和 任务 2 之间来回切换,使任务 2 不必等到 10 秒再运行,系统的运行效率大大得到提升
  • 使用多线程就是在使用异步

1.2 使用多线程

一个进程正在运行时,至少会有一个线程正在运行,比如调用 main 方法的线程就是这样的:

public class Test {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName());
    }
}

运行结果

main

分析:这说明一个叫 main 的线程正在执行 main() 方法中的代码

1.2.1 继承 Thread 类

实现多线程编程的方式主要有两种,一种是继承 Thread 类,一种是实现 Runnable 接口

接下来我们看一下继承 Thread 类实现多线程编程:

  1. 创建一个自定义的线程类

    public class MyThread extends Thread {
        @Override
        public void run() {
            super.run();
            System.out.println("MyThread");
        }
    }
    
    
  2. 测试类

    public class MyThreadTest {
        public static void main(String[] args) {
            MyThread myThread = new MyThread();
            myThread.start();
            System.out.println("运行结束");
        }
    }
    
    

    运行结果

    运行结束
    MyThread
    

分析:MyThread 类的 run 方法执行的时间比较晚,这也说明使用多线程技术时,代码的运行结果与代码的执行顺序或调用顺序是无关的。线程是一个子任务,CPU 以随机的方式来调用线程中的 run 方法。

下面演示线程调用的随机性:

  1. 创建一个自定义的线程类

    public class MyThread2 extends Thread {
        @Override
        public void run() {
            super.run();
            for (int i = 0; i < 10; i++) {
                int time = (int) (Math.random() * 1000);
                try {
                    Thread.sleep(time);
                    System.out.println("run="+Thread.currentThread().getName());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    
  2. 测试类

    public class MyThread2Test {
        public static void main(String[] args) {
            MyThread2 myThread2 = new MyThread2();
            myThread2.setName("myThread2");
            myThread2.start();
            for (int i = 0; i < 10; i++) {
                int time = (int) (Math.random() * 1000);
                try {
                    Thread.sleep(time);
                    System.out.println("main=" + Thread.currentThread().getName());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    

    运行结果

    run=myThread2
    main=main
    run=myThread2
    run=myThread2
    main=main
    run=myThread2
    run=myThread2
    main=main
    run=myThread2
    main=main
    run=myThread2
    run=myThread2
    main=main
    run=myThread2
    run=myThread2
    main=main
    main=main
    main=main
    main=main
    main=main
    

分析:为了展示出线程的调用具有随机特性,所以使用随机数的形式来使线程挂起,从而表现出 CPU 执行哪个线程具有不确定性

注意:Thread 类中的 start() 方法的作用是通知 “线程规划器” 此线程已经准备就绪,等待调用线程对象的 run() 方法,具有异步效果。如果调用 thread.run() 就是同步了,那么此线程对象并不交给 “线程规划器” 来进行处理,而是由 main 主线程来调用 run() 方法,也就是说要等到 run() 方法执行完才可以执行后面的代码

下面演示 start() 方法的执行顺序不代表线程的启动顺序:

  1. 创建一个自定义的线程类

    public class MyThread3 extends Thread {
        private int i;
    
        public MyThread3(int i) {
            this.i = i;
        }
    
        @Override
        public void run() {
            System.out.println(i);
            super.run();
        }
    }
    
    
  2. 测试类

    public class MyThread3Test {
        public static void main(String[] args) {
            List<MyThread3> myThread3List = new ArrayList<>();
            for (int i = 0; i < 10; i++) {
                myThread3List.add(new MyThread3(i));
            }
            for (int i = 0; i < 10; i++) {
                myThread3List.get(i).start();
            }
        }
    }
    

    运行结果

    0
    2
    3
    1
    4
    6
    9
    5
    7
    8
    

1.2.2 实现 Runnable 接口

如果想要创建的线程类已经有一个父类了,这时就不能再继承自 Thread 类了,因为 Java 不支持多继承,所以就需要实现 Runnable 接口来应对这样的情况了。

Thread 类有两个构造函数 Thread(Runnable target) 和 Thread(Runnable target, String name),说明构造函数支持传入一个 Runnable 接口的对象。

下面看一下实现 Runnable 接口来实现多线程编程:

  1. 创建一个自定义的线程类

    public class MyThread4 implements Runnable{
        @Override
        public void run() {
            System.out.println("MyThread4");
        }
    }
    
    
  2. 测试类

    public class MyThread4Test {
        public static void main(String[] args) {
            MyThread4 myThread4 = new MyThread4();
            Thread thread = new Thread(myThread4);
            thread.start();
            System.out.println("运行结束");
        }
    }
    
    

    运行结果

    运行结束
    MyThread4
    

另外要说明的是,Thread 类也实现了 Runnable 接口

public class Thread implements Runnable {

这也就意味着 Thread(Runnable target) 不光可以传入 Runnable 对象,还可以传入一个 Thread 类的对象,这样做完全可以将一个 Thread 对象中的 run() 方法交给其它的线程进行调用

1.2.3 实例变量与线程安全

自定义线程类中的实例变量针对其他线程可以有共享和不共享之分

  1. 不共享的情况

    在这里插入图片描述

    下面通过一个示例来看一下数据不共享的情况:

    1. 创建一个自定义的线程类

      public class MyThread5 extends Thread {
          private int count = 5;
      
          public MyThread5(String name) {
              super();
              this.setName(name);
          }
      
          @Override
          public void run() {
              super.run();
              while (count > 0) {
                  count--;
                  System.out.println("由" + this.currentThread().getName() + "计算,count = " + count);
              }
          }
      }
      
      
    2. 测试类

      public class MyThread5Test {
          public static void main(String[] args) {
              MyThread5 a = new MyThread5("A");
              MyThread5 b = new MyThread5("B");
              MyThread5 c = new MyThread5("C");
              a.start();
              b.start();
              c.start();
      
          }
      }
      
      

      运行结果

      由B计算,count = 4
      由A计算,count = 4
      由A计算,count = 3
      由B计算,count = 3
      由B计算,count = 2
      由A计算,count = 2
      由B计算,count = 1
      由C计算,count = 4
      由B计算,count = 0
      由A计算,count = 1
      由A计算,count = 0
      由C计算,count = 3
      由C计算,count = 2
      由C计算,count = 1
      由C计算,count = 0
      

    分析:一共创建了 3 个线程,每个线程都有各自的 count 变量,自己减少自己的 count 变量的值,这样情况就是变量不共享

  2. 共享数据的情况

    在这里插入图片描述

    共享数据的情况就是多个线程可以访问同一个变量,比如在实现投票功能的软件时,多个线程可以同时处理同一个人的票数

    下面通过一个示例来看一下数据共享的情况:

    1. 创建一个自定义的线程类

      public class MyThread6 extends Thread {
          private int count = 5;
      
          @Override
          public void run() {
              super.run();
              count--;
              System.out.println("由" + this.currentThread().getName() + "计算,count = " + count);
          }
      }
      
      
    2. 测试类

      public class MyThread6Test {
          public static void main(String[] args) {
              MyThread6 myThread6 = new MyThread6();
      
              Thread a = new Thread(myThread6, "A");
              Thread b = new Thread(myThread6, "B");
              Thread c = new Thread(myThread6, "C");
              Thread d = new Thread(myThread6, "D");
              Thread e = new Thread(myThread6, "E");
      
              a.start();
              b.start();
              c.start();
              d.start();
              e.start();
          }
      }
      
      

      运行结果

      由B计算,count = 3
      由A计算,count = 3
      由C计算,count = 2
      由D计算,count = 1
      由E计算,count = 0
      

    分析:线程 A 和线程 B 打印出的 count 值都是 3,说明 A 和 B 同时对 count 进行了处理,这就产生了 ”非线程安全“ 问题。“非线程安全” 主要是指多个线程对同一个对象中的同一个实例变量进行操作时,会出现值被更改、值不同步的情况,进而影响到程序的执行流程。

    但我们想要得到的打印结果是依次递减的,该怎么解决非线程安全问题呢? 这时就需要使多个线程之间进行同步,也就是用按顺序排队的方式进行减 1 操作。更改后的代码如下:

    1. 创建一个自定义的线程类

      public class MyThread7 extends Thread {
          private int count = 5;
      
          @Override
          synchronized public void run() {
              super.run();
              count--;
              System.out.println("由" + this.currentThread().getName() + "计算,count = " + count);
          }
      }
      
      
    2. 测试类

      public class MyThread7Test {
          public static void main(String[] args) {
              MyThread7 myThread7 = new MyThread7();
      
              Thread a = new Thread(myThread7, "A");
              Thread b = new Thread(myThread7, "B");
              Thread c = new Thread(myThread7, "C");
              Thread d = new Thread(myThread7, "D");
              Thread e = new Thread(myThread7, "E");
      
              a.start();
              b.start();
              c.start();
              d.start();
              e.start();
          }
      }
      
      

      运行结果

      由A计算,count = 4
      由B计算,count = 3
      由C计算,count = 2
      由D计算,count = 1
      由E计算,count = 0
      

    分析:在 run() 方法前加入 synchronized 关键字,使多个线程在执行 run() 方法时,以排队的方式进行处理。 当一个线程调用 run() 方法前,先判断 run() 方法有没有被上锁,如果上锁,说明有其他线程正在调用 run() 方法,必须等其他线程对 run() 方法调用结束后才可以执行 run() 方法。这样也就实现了排队调用 run() 方法的目的了。

    synchronized 可以在任意对象及方法上加锁,而加锁的这段代码称为 “互斥区” 或 “临界区”。

1.2.4 留意 i-- 与 System.out.println() 的异常

System.out.println() 方法内部是同步的

public void println(String x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}

System.out.println() 与 i-- 联合使用时还是会产生 “非线程安全问题”,下面通过一个示例来看一下:

  1. 创建一个自定义的线程类

    public class MyThread8 extends Thread {
        private int i = 5;
    
        @Override
        public void run() {
            super.run();
            System.out.println("i = " + (i--) + "   threadName = " + this.currentThread().getName());
        }
    }
    
    
  2. 测试类

    public class MyThread8Test {
        public static void main(String[] args) {
            MyThread8 myThread8 = new MyThread8();
            Thread t1 = new Thread(myThread8);
            Thread t2 = new Thread(myThread8);
            Thread t3 = new Thread(myThread8);
            Thread t4 = new Thread(myThread8);
            Thread t5 = new Thread(myThread8);
            t1.start();
            t2.start();
            t3.start();
            t4.start();
            t5.start();
        }
    }
    
    

    运行结果

    i = 5   threadName = Thread-2
    i = 3   threadName = Thread-4
    i = 2   threadName = Thread-5
    i = 4   threadName = Thread-3
    i = 5   threadName = Thread-1
    

分析:虽然 System.out.println() 方法内部是同步的,但 i-- 操作却是在进入 println() 之前发生的,所以还是会产生 “非线程安全问题”。为了防止 “非线程安全问题” 的发生,还是应该继续使用同步方法

1.3 currentThread() 方法

currentThread() 方法可返回代码块正在被哪个线程调用的信息

下面通过一个示例进行说明:

  1. 创建一个自定义的线程类

    public class MyThread9 extends Thread {
        public MyThread9() {
            System.out.println("构造方法的打印:" + Thread.currentThread().getName());
        }
        
        @Override
        public void run() {
            super.run();
            System.out.println("run 方法的打印:" + Thread.currentThread().getName());
        }
    }
    
    
  2. 测试类

    public class MyThread9Test {
        public static void main(String[] args) {
            MyThread9 myThread9 = new MyThread9();
            myThread9.start();
        }
    }
    
    

    运行结果

    构造方法的打印:main
    run 方法的打印:Thread-0
    

分析:MyThread 类的构造函数是被 main 线程调用的,而 run 方法是被名称为 Thread-0 的线程调用的

下面再看一个比较复杂的实例:

  1. 创建一个自定义的线程类

    public class MyThread10 extends Thread {
        public MyThread10() {
            System.out.println("MyThread10---begin");
            // Thread.currentThread().getName() 指的是调用 MyThread10 这个线程类的线程的名称
            System.out.println("Thread.currentThread().getName():" + Thread.currentThread().getName());
            // this.getName() 指的是 MyThread10 这个线程类的名称
            System.out.println("this.getName():" + this.getName());
            System.out.println("MyThread10---end");
        }
    
        @Override
        public void run() {
            super.run();
            System.out.println("run---begin");
            System.out.println("Thread.currentThread().getName():" + Thread.currentThread().getName());
            System.out.println("this.getName():" + this.getName());
            System.out.println("run---end");
        }
    }
    
    
  2. 测试类

    public class MyThread10Test {
        public static void main(String[] args) {
            MyThread10 myThread10 = new MyThread10();
            System.out.println("---------");
            Thread thread = new Thread(myThread10,"A");
            thread.start();
        }
    }
    
    

    运行结果

    MyThread10---begin
    Thread.currentThread().getName():main
    this.getName():Thread-0
    MyThread10---end
    ---------
    run---begin
    Thread.currentThread().getName():A
    this.getName():Thread-0
    run---end
    

1.4 isAlive() 方法

isAlive() 方法的功能是判断当前的线程是否处于活动状态,活动状态就是线程已经启动且尚未终止

下面通过一个示例进行说明:

  1. 创建一个自定义的线程类

    public class MyThread11 extends Thread {
        @Override
        public void run() {
            super.run();
            System.out.println("run = " + this.isAlive());
        }
    }
    
    
  2. 测试类

    public class MyThread11Test {
        public static void main(String[] args) throws InterruptedException {
            MyThread11 myThread11 = new MyThread11();
            System.out.println("begin = " + myThread11.isAlive());
            myThread11.start();
            Thread.sleep(1000);
            System.out.println("end = " + myThread11.isAlive());
        }
    }
    
    

    运行结果

    begin = false
    run = true
    end = false
    

分析:begin = false 是因为 myThread11 线程还未启动,end = false 是因为 myThread11 线程已经终止

在使用 isAlive() 方法时,如果将线程对象以构造参数的方式传递给 Thread 对象进行 start() 启动,运行的结果和前面的示例是有差异的。造成差异的主要原因还是来自于 Thread.currentThread() 和 this 的差异,下面通过一个示例进行说明:

  1. 创建一个自定义的线程类

    public class MyThread12 extends Thread {
        public MyThread12() {
            System.out.println("MyThread12---begin");
            // Thread.currentThread().getName() 指的是调用 MyThread10 这个线程类的线程的名称
            System.out.println("Thread.currentThread().getName():" + Thread.currentThread().getName());
            // Thread.currentThread().isAlive() 指的是调用 MyThread10 这个线程类的线程是否处于活动状态
            System.out.println("Thread.currentThread().isAlive():" + Thread.currentThread().isAlive());
            // this.getName() 指的是 MyThread10 这个线程类的名称
            System.out.println("this.getName():" + this.getName());
            // this.isAlive() 指的是 MyThread10 这个线程类是否处于活动状态
            System.out.println("this.isAlive():" + this.isAlive());
            System.out.println("MyThread12---end");
        }
    
        @Override
        public void run() {
            super.run();
            System.out.println("run---begin");
            System.out.println("Thread.currentThread().getName():" + Thread.currentThread().getName());
            System.out.println("Thread.currentThread().isAlive():" + Thread.currentThread().isAlive());
            System.out.println("this.getName():" + this.getName());
            System.out.println("this.isAlive():" + this.isAlive());
            System.out.println("run---end");
        }
    }
    
    
  2. 测试类

    public class MyThread12Test {
        public static void main(String[] args) {
            MyThread12 myThread12 = new MyThread12();
            System.out.println("---------");
            Thread thread = new Thread(myThread12, "A");
            thread.start();
        }
    }
    
    

    运行结果

    MyThread12---begin
    Thread.currentThread().getName():main
    Thread.currentThread().isAlive():true
    this.getName():Thread-0
    this.isAlive():false
    MyThread12---end
    ---------
    run---begin
    Thread.currentThread().getName():A
    Thread.currentThread().isAlive():true
    this.getName():Thread-0
    this.isAlive():false
    run---end
    
    

1.5 sleep() 方法

sleep() 方法的作用是在指定的毫秒数内让当前 “正在执行的线程” 休眠(暂停执行)。这个 “正在执行的线程” 是指 this.currentThread() 返回的线程。

下面通过一个示例来说明:

  1. 创建一个自定义的线程类

    public class MyThread13 extends Thread {
        @Override
        public void run() {
            super.run();
            try {
                System.out.println("run threadName = " + this.currentThread().getName() + " begin time = " + System.currentTimeMillis());
                Thread.sleep(2000);
                System.out.println("run threadName = " + this.currentThread().getName() + " end time = " + System.currentTimeMillis());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    
  2. 测试类

    public class MyThread13Test {
        public static void main(String[] args) {
            System.out.println("begin time = " + System.currentTimeMillis());
            MyThread13 myThread13 = new MyThread13();
            myThread13.run();
            System.out.println("end time = " + System.currentTimeMillis());
        }
    }
    
    

    运行结果

    begin time = 1573470507836
    run threadName = main begin time = 1573470507837
    run threadName = main end time = 1573470509838
    end time = 1573470509838
    

分析:上面测试类中是直接调用 myThread13.run() 方法,也就是由 main 线程来调用 run() 方法,从运行结果可以看到在 run() 方法中 main 线程暂停了 2000 毫秒

接下来看看使用 start() 方法启动 myThread13 线程:

  1. 测试类

    public class MyThread13_2 {
        public static void main(String[] args) {
            System.out.println("begin time = " + System.currentTimeMillis());
            MyThread13 myThread13 = new MyThread13();
            myThread13.start();
            System.out.println("end time = " + System.currentTimeMillis());
        }
    }
    
    

    运行结果

    begin time = 1573470976618
    end time = 1573470976620
    run threadName = Thread-0 begin time = 1573470976621
    run threadName = Thread-0 end time = 1573470978621
    

分析:由于 main 线程和 myThread13 线程是异步执行的,所以首先执行的是 main 线程打印的 begin time 和 end time,而 myThread13 线程是随后运行的,从运行结果可以看到在 run() 方法中 myThread13 线程暂停了 2000 毫秒

1.6 getId() 方法

getId() 方法的作用是取得线程的唯一标识。

下面通过一个示例来说明:

  1. 创建一个测试类

     public class Test2 {
         public static void main(String[] args) {
             System.out.println(Thread.currentThread().getName());
             System.out.println(Thread.currentThread().getId());
         }
     }
     
    

    运行结果

    main
    1
    

分析:从运行结果来看,说明正在执行 main() 方法中代码的线程名称为 main,线程 id 值为 1

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bm1998

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值