本章介绍Thread类中的核心方法。重点掌握如下关键技术点:
- 线程的启动
- 如何使线程暂停
- 如何使线程停止
- 线程的优先级
- 线程安全相关的问题
1.1 进程和多线程的概念及线程的优点
讲到线程,我们要先介绍进程。
我们可以将一个正在操作系统中运行的exe程序理解成一个“进程”。进程是受操作系统管理的基本运行单元。
线程可以理解成是在进程中独立运行的 子任务。比如,QQ.exe运行时就有很多的子任务在同时运行,比如:传文件、听音乐、发送表情等功能都有对象的线程在后台默默地运行。使用多线程有什么优点呢? 我们知道“多任务操作系统”。使用多任务操作系统,可以最大限度地利用CPU的空闲时间来处理其他的任务,比如一边让操作系统处理正在打印机打印的数据,一边使用Word编辑文档。而CPU在这些任务之间不停地切换,由于切换的速度非常快,给使用者的感受就是这些任务似乎在同时运行。所以,使用多线程技术后,可以在同一时间内运行更多不同种类的任务。
为了更加有效地理解多线程的优势,看一下如图1-3所示的单任务的模型图,理解一下单任务的缺点。
在图1-3中,任务1和任务2是两个完全独立、互不相关的任务,任务1是在等待远程服务器返回数据,一边进行后期的处理,这时CPU一直处于等待状态,一直在“空运行”。如果任务2是在10秒之后被运行,虽然执行任务2用的时间非常短,仅仅是1秒,但也必须在任务1运行结束之后才可以运行任务2。本程序是运行在单任务环境中,所以任务2有非常长的等待时间,系统运行效率答复降低。单任务的特点就是排队执行,也就是同步,就像在cmd中输入一条命令后,必须等待这条命令执行完才可以执行下一条命令一样。这就是单任务环境的缺点,即CPU利用率大幅降低。
而多任务的环境如图1-4所示。
多任务,cpu可以在任务1和任务2之间来回切换,使任务2不必等到10秒再运行,系统的运行效率大大得到提升。这就是要使用多线程技术、要学习多线程的原因。这是多线程技术的优点,使用多线程就是在使用异步。
1.2 使用多线程
一个进程在运行时至少会有1个线程在运行。多任务,cpu可以在任务1和任务2之间来回切换,使任务2等到10秒再运行,系统的运行效率大大得到提升。使用多线程就是在使用异步。1.2.1 继承Thread类
在Java的JDk开发包中,已经自带了对多线程技术的支持。实现多线程编程的方式主要有两种,继承Thread类&实现Runnable接口。
我们看看Thread的源码:
Thread类实现了Runnable接口,它们之间具有多态关系。
其实,使用继承Thread类的方式创建新线程时,最大的局限就是不支持多继承。用Thread和Runnable两种方式创建的线程在工作时的性质是一样的,没有本质区别。
创建自定义线程类MyThread.java。重写run方法。在run方法中,写线程要执行的任务的代码:
public class MyThread extends Thread {
@Override
public void run() {
super.run();
System.out.println("MyThread");
}
}
测试类:
public class Client {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
System.out.println("运行结束!");
}
}
运行结果:
运行结果说明,在使用多线程技术时,代码的运行结果与代码执行顺序或调用顺序是无关的。
线程是一个子任务,cpu以不确定的方式,或者说是以随机的时间来调用线程中的run方法。
Thread.java类中的start()方法的作用是,通知“线程规划器”此线程已经准备就绪,等待调用线程对象的run()方法。这个过程其实就是让系统安排一个时间来调用Thread中的run()方法,也就是使线程得到运行,启动线程,具有异步执行的效果。如果调用代码thread.run()就不是异步执行了,而是同步,那么此线程对象并不交给“线程规划器”来进行处理,而是由main主线程来调用run()方法,也就是必须等run()方法中的代码执行完后才可以执行后面的代码。执行start()方法的顺序不代表线程启动的顺序。
1.2.2 实现Runnable接口
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("运行中!");
}
}
如何使用这个MyRunnable呢?我们看一下Thread.java的构造函数:
有两个构造函数可以传递Runnable接口,说明构造函数支持传入一个Runnable接口的对象。
public class Client {
public static void main(String[] args) {
Runnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
System.out.println("运行结束!");
}
}
Thread.java类实现了Runnable接口,那也就意味着构造函数Thread(Runnable target)不光可以传入Runnable接口的对象,还可以传入一个Thread类的对象,这样做完全可以将一个Thread对象中的run()方法交由其他的线程进行调用。
1.2.3 实例变量与线程安全
共享数据的情况就是多个线程可以访问同一个变量。比如在实现投票功能的软件时,多个线程可以同时处理同一个人的票数。演示数据共享的情况:
public class MyThread extends Thread {
private int count = 10;
@Override
public void run() {
super.run();
count--;
System.out.println("由 " + this.currentThread().getName() + ",计算,count=" + count);
}
}
public class Client {
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread a = new Thread(myThread, "A");
Thread b = new Thread(myThread, "B");
Thread c = new Thread(myThread, "C");
Thread d = new Thread(myThread, "D");
Thread e = new Thread(myThread, "E");
Thread f = new Thread(myThread, "F");
Thread g = new Thread(myThread, "G");
Thread h = new Thread(myThread, "H");
Thread i = new Thread(myThread, "I");
Thread j = new Thread(myThread, "J");
a.start();
b.start();
c.start();
d.start();
e.start();
f.start();
g.start();
h.start();
i.start();
j.start();
}
}
线程A和C打印出的count值是一样的,说明A和C同时对count进行处理,产生了“非线程安全”问题。而我们想要得到的打印结果却不是重复的,而是依次递减的。
某些JVM中,i--的操作要分成如下3步:
- 取得原有i值
- 计算i-1
- 对i进行赋值
其实这个示例就是典型的销售场景:5个销售员,每个销售员卖出一个货品后不可以得出相同的剩余数量,必须在每一个销售员卖完一个货品后其他销售员才可以在新的剩余物品数上继续减1操作。这时就需要使多个线程之间进行同步,也就是用按顺序排队的方式进行减1操作。
更改代码如下:
public class MyThread extends Thread {
private int count = 10;
@Override
synchronized public void run() {
super.run();
count--;
System.out.println("由 " + this.currentThread().getName() + ",计算,count=" + count);
}
}
在run()方法前加入synchronized关键字,使多个线程在执行run方法时,以排队的方式进行处理。当一个线程调用run前,先判断run方法有没有被上锁,如果上锁,说明有其他线程正在调用run方法,必须等其他线程对run方法调用结束后才可以执行run方法。这样也就实现了排队调用run方法的目的,也就达到了按顺序对count变量减1的效果了。Synchronized可以在任意对象及方法上加锁,而加锁的这段代码称为“互斥区”或“临界区”。
当一个线程想要执行同步方法里面的代码时,线程首先尝试去拿这把锁,如果能够拿到这把锁,那么这个线程就可以执行synchronized里面的代码。如果不能拿到这把锁,那么这个线程就会不断地尝试拿这把锁,直到能够拿到为止,而且是有多个线程同时去争抢这把锁。
非线程安全主要是指多个线程对同一个对象中的同一个实例变量进行操作时会出现值被更改、值不同步的情况,进而影响程序的执行流程。
1.2.4 留意i--与System.out.println()的异常
虽然println()方法在内部是同步的,但i—的操作却是在进入println()之前发生的,所以有发生非线程安全问题的概率。所以,还是应该继续使用同步方法。1.3 currentThread()方法
currentThread()方法可返回代码段正在被哪个线程调用的信息。
public class Client {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName());
}
}
结果说明,main方法被名为main的线程调用。
继续试验,创建MyThread.java类:
public class MyThread extends Thread {
public MyThread() {
System.out.println("构造方法的打印:" + Thread.currentThread().getName());
}
@Override
synchronized public void run() {
System.out.println("run方法的打印:" + Thread.currentThread().getName());
}
}
运行client:
public class Client {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
MyThread.java类的构造函数是被main线程调用的,而run方法时被名称为Thread-0的线程调用的,run方法时自动调用的方法。
修改client代码:
public class Client {
public static void main(String[] args) {
MyThread myThread = new MyThread();
// myThread.start();
myThread.run();
}
}
1.4 isAlive()方法
isAlive()的作用是测试线程是否处于活动状态。
public class MyThread extends Thread {
@Override
synchronized public void run() {
System.out.println("run=" + this.isAlive());
}
}
public class Client {
public static void main(String[] args) {
MyThread myThread = new MyThread();
System.out.println("begin==" + myThread.isAlive());
myThread.start();
System.out.println("end==" + myThread.isAlive());
}
}
活动状态就是线程已经启动且尚未终止。线程处于正在运行或准备开始运行的状态,就认为线程时“存活”的。
1.5 sleep()方法
public class MyThread extends Thread {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " begin");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " end");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
1.6 getId()方法
public class Client {
public static void main(String[] args) {
Thread runThread = Thread.currentThread();
System.out.println(runThread.getName() + " " + runThread.getId());
}
}
1.7 停止线程
停止线程时在多线程开发中很重要的技术点。停止一个线程意味着在线程处理完任务之前停掉正在做的操作,也就是放弃当前的操作。停止一个线程可以使用Thread.stop()方法,但是这个方法时不安全的,并且已经被废弃了。
大多数停止一个线程的操作使用Thread.interrupt()方法,但是还需要加入一个判断才可以完成线程的停止。
Java中有以下3中方法可以终止正在运行的线程:
- 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
- 使用stop方法强行终止线程,但是不推荐使用。因为stop和suspend及resume一样,都是作废过期的方法,使用它们可能产生不可预料的结果。
- 使用interrupt方法中断线程
1.7.1 停止不了的线程
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 500000; i++) {
System.out.println("i=" + (i + 1));
}
}
}
public class Client {
public static void main(String[] args) {
try {
MyThread thread = new MyThread();
thread.start();
Thread.sleep(2000);
thread.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
结果,打印了500000行,说明没有停止线程。
1.7.2 判断线程是否是停止状态
- this.interrupted(): 测试当前线程是否已经中断。当前线程是指运行this.interrupted()方法的线程。
- this.isInterrupted(): 测试线程是否已经中断。
1.7.3 能停止的线程——异常法
1.7.4 在沉睡中停止
1.7.5 能停止的线程——暴力停止
1.7.6 方法stop()与java.lang.ThreadDeath异常
1.7.7 释放锁的不良后果
1.7.8 使用return停止线程
1.8 暂停线程
1.8.1 Suspend与resume方法的使用
1.8.2 Suspend与resume方法的确定——独占
1.8.3 Suspend与resume方法的缺点——不同步
1.9 yield方法
1.10 线程的优先级
1.10.1 线程优先级的继承特性
public class MyThread1 extends Thread {
@Override
public void run() {
System.out.println("mythread1 priority=" + this.getPriority());
MyThread2 thread2 = new MyThread2();
thread2.start();
}
}
public class MyThread2 extends Thread {
@Override
public void run() {
System.out.println("mythread2 priority=" + this.getPriority());
}
}
public class Client {
public static void main(String[] args) {
MyThread1 thread1 = new MyThread1();
thread1.setPriority(6);
thread1.start();
}
}
1.10.2 优先级具有规则性
1.10.3 优先级具有随机性
1.11 守护线程
public class MyThread extends Thread {
private int i = 0;
@Override
public void run() {
try {
while (true) {
i++;
System.out.println("i=" + i);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Client {
public static void main(String[] args) {
try {
MyThread thread = new MyThread();
thread.setDaemon(true);
thread.start();
Thread.sleep(5000);
System.out.println("我离开thread对象也不再打印了,也就是停止了!");
} catch (Exception e) {
e.printStackTrace();
}
}
}
1.12 本章小结
本章介绍了Thread类的API