文章目录
声明:
本博客是本人在学习《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 类实现多线程编程:
-
创建一个自定义的线程类
public class MyThread extends Thread { @Override public void run() { super.run(); System.out.println("MyThread"); } }
-
测试类
public class MyThreadTest { public static void main(String[] args) { MyThread myThread = new MyThread(); myThread.start(); System.out.println("运行结束"); } }
运行结果
运行结束 MyThread
分析:MyThread 类的 run 方法执行的时间比较晚,这也说明使用多线程技术时,代码的运行结果与代码的执行顺序或调用顺序是无关的。线程是一个子任务,CPU 以随机的方式来调用线程中的 run 方法。
下面演示线程调用的随机性:
-
创建一个自定义的线程类
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(); } } } }
-
测试类
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() 方法的执行顺序不代表线程的启动顺序:
-
创建一个自定义的线程类
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(); } }
-
测试类
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 接口来实现多线程编程:
-
创建一个自定义的线程类
public class MyThread4 implements Runnable{ @Override public void run() { System.out.println("MyThread4"); } }
-
测试类
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 实例变量与线程安全
自定义线程类中的实例变量针对其他线程可以有共享和不共享之分
-
不共享的情况
下面通过一个示例来看一下数据不共享的情况:
-
创建一个自定义的线程类
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); } } }
-
测试类
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 变量的值,这样情况就是变量不共享
-
-
共享数据的情况
共享数据的情况就是多个线程可以访问同一个变量,比如在实现投票功能的软件时,多个线程可以同时处理同一个人的票数
下面通过一个示例来看一下数据共享的情况:
-
创建一个自定义的线程类
public class MyThread6 extends Thread { private int count = 5; @Override public void run() { super.run(); count--; System.out.println("由" + this.currentThread().getName() + "计算,count = " + count); } }
-
测试类
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 操作。更改后的代码如下:
-
创建一个自定义的线程类
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); } }
-
测试类
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-- 联合使用时还是会产生 “非线程安全问题”,下面通过一个示例来看一下:
-
创建一个自定义的线程类
public class MyThread8 extends Thread { private int i = 5; @Override public void run() { super.run(); System.out.println("i = " + (i--) + " threadName = " + this.currentThread().getName()); } }
-
测试类
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() 方法可返回代码块正在被哪个线程调用的信息
下面通过一个示例进行说明:
-
创建一个自定义的线程类
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()); } }
-
测试类
public class MyThread9Test { public static void main(String[] args) { MyThread9 myThread9 = new MyThread9(); myThread9.start(); } }
运行结果
构造方法的打印:main run 方法的打印:Thread-0
分析:MyThread 类的构造函数是被 main 线程调用的,而 run 方法是被名称为 Thread-0 的线程调用的
下面再看一个比较复杂的实例:
-
创建一个自定义的线程类
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"); } }
-
测试类
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() 方法的功能是判断当前的线程是否处于活动状态,活动状态就是线程已经启动且尚未终止
下面通过一个示例进行说明:
-
创建一个自定义的线程类
public class MyThread11 extends Thread { @Override public void run() { super.run(); System.out.println("run = " + this.isAlive()); } }
-
测试类
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 的差异,下面通过一个示例进行说明:
-
创建一个自定义的线程类
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"); } }
-
测试类
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() 返回的线程。
下面通过一个示例来说明:
-
创建一个自定义的线程类
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(); } } }
-
测试类
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 线程:
-
测试类
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() 方法的作用是取得线程的唯一标识。
下面通过一个示例来说明:
-
创建一个测试类
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