使用多线程
在 Java 的 JDK 开发包中,已经自带了对多线程技术的支持,实现多线程编程的方式主要有两种,一种是继承 Thread 类,另一种是实现 Runnable 接口。
继承 Thread 类
public class Thread implements Runnable
Thread 类实现了 Runnable 接口,它们之间具有多态关系。其实,使用继承 Thread 类的方式创建新线程时,最大的局限就是不支持多继承,因为 Java 语言的特点就是单根继承,所以为了支持多继承,完全可以实现 Runnable 接口的方式,一边实现一边继承。这两种方式创建的线程在工作时的性质是一样的,没有本质的区别。
public MyThread extends Thread{
@Override
public void run(){
}
}
MyThread thread = new MyThread();
thread.start();
实现 Runnable 接口
如果欲创建的线程类已经有一个父类了,这时就不能再继承 Thread 类,所以就需要实现 Runnable 接口来应对这样的情况。
public MyRunnable implements Runnable{
@Override
public void run(){
}
}
Runnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
运行顺序
在使用多线程技术时,代码的运行结果与代码的执行顺序或调用顺序是无关的;执行 start()
方法的顺序不代表线程启动的顺序。
Thread.java 类中的 start()
方法通知 “线程规划器” 此线程已经准备就绪,等待调用线程对象的 run()
方法。这个过程其实就是让系统安排一个时间来调用 Thread 中的 run()
方法,也就是使线程得到运行,启动线程,具有异步执行的效果。如果调用代码 thread.run()
就不是异步执行了,而是同步,那么此线程对象并不是交给 “线程规划器” 来进行处理,而是由 main
主线程来调用 run()
方法,也就是必须等 run()
方法中的代码执行完之后才可以执行后面的代码。
实例变量与线程安全
自定义线程类中的实例变量针对其他线程可以有共享与不共享之分。
- 不共享数据
public class MyThread extends Thread {
private int count = 5;
public MyThread(String name) {
super();
this.setName(name);
}
@Override
public void run(){
super.run();
while (count > 0){
count--;
System.out.println("由" + this.currentThread().getName() + "计算, count=" + count);
}
}
}
MyThread a = new MyThread("A");
MyThread b = new MyThread("B");
MyThread c = new MyThread("C");
a.start();
b.start();
c.start();
一共创建了3个线程,每个线程都有各自的 count变量,互不影响,变量不共享。
- 共享数据
public class MyThread extends Thread {
private int count = 5;
public MyThread(String name) {
super();
this.setName(name);
}
@Override
public void run(){
super.run();
// 不要使用 for,因为使用同步后其他线程就得不到运行的机会,一直由一个线程来进行计算。
count--;
System.out.println("由" + this.currentThread().getName() + "计算, count=" + count);
}
}
MyThread mythread = new MyThread();
Thread a = new Thread(mythread, "A");
Thread b = new Thread(mythread, "B");
Thread c = new Thread(mythread, "C");
a.start();
b.start();
c.start();
计算输出结果。
由 A 计算,count=3
由 B 计算,count=3
由 C 计算,count=2
...
...
从运行的结果可以看到,A、B线程同时对 count 进行处理,产生了 非线程安全1 问题,而我们想要得到的打印结果却是不重复的、依次递减的。
- 该操作可以分为以下3个步骤:
- 取得原有的值(i)
- 计算 i-1
- 对 i 进行赋值
在上述3个步骤中,如果有多个线程同时访问,那么一定会出现非线程安全问题。
这个实例其实就是一个典型的销售场景,我们在开发过程中经常遇到这样的问题,例如:卖火车票、电商库存等等... ... 为了解决非线程安全问题,需要使多个线程之间进行同步(synchronized),按顺序排队的方式进行运算。更改代码如下:
public class MyThread extends Thread {
private int count = 5;
public MyThread(String name) {
super();
this.setName(name);
}
@Override
synchronized public void run(){
super.run();
count--;
System.out.println("由" + this.currentThread().getName() + "计算, count=" + count);
}
}
通过在 run()
方法前加入 synchronized
关键字,使多个线程在执行 run()
方法时,以排队的方式进行处理。当一个线程调用 run()
前,会先判断有没有被上锁,如果上锁,说明其他线程正在调用 run()
,必须等其他线程对 run()
方法调用结束之后才可以执行。 synchronized
可以在任意对象及方法上加锁,而加锁的这段代码称为 “互斥区” 或 “临界区”。
当一个线程想要执行同步方法里的代码时,线程首先尝试去拿这把锁,如果能够拿到这把锁,那么这个线程就可以执行 synchronized
里面的代码。如果不能拿到这把锁,那么这个线程就会不断尝试拿这把锁,直到能够拿到为止,而且是有多个线程同时去争抢这把锁。
停止线程
使用 Java 内置支持多线程的类设计多线程应用是很常见的事情,然而,多线程给开发人员带来了一些新的挑战,如果处理不好就会导致超出预期的行为并且难以定位错误。
停止一个线程意味着在线程处理完任务之前停掉正在做的操作,也就是放弃当前的操作。虽然这看起来非常简单,但是必须做好防范措施,以便达到预期的效果。
停止一个线程可以使用 Thread.stop()
方法,但最好不要使用它,虽然它确实可以停止一个正在运行的线程,但是这个方法是不安全(unsafe),而且是已被弃用作废的(deprecated),在将来的 Java 版本中,这个方法将不可用或不被支持。
大多数停止一个线程的操作使用 Thread.interrupt()
方法,尽管方法的名称是 “停止”、“终止” 的意思,但这个方法不会终止一个正在运行的线程,还需要加入一个判断才可以完成线程的停止。
- 在 Java 中有以下 3 种方法可以终止正在运行的线程
- 使用退出标志,使线程正常退出,也就是当
run()
方法完成后线程终止。 - 使用
stop()
方法强行终止线程,但是不推荐使用这个方法,因为stop
、suspend
、resume
一样,都是作废过期的方法,使用它们可能产生不可预料的结果。 - 使用
interrupt()
方法中断线程。
- 使用退出标志,使线程正常退出,也就是当
如何判断线程是否是停止状态
在 Java 的 SDK 中,Thread.java
类里提供了两种方法。
- this.interrupted():测试当前线程是否已经是中断状态,执行后具有将状态标志置清楚为 false 的功能。
- this.isInterrupted():测试线程 Thread 对象是否已经是中断状态,但不清除状态标志。
示例
方法 interrupt()
和 return
结合使用可以实现停止线程的效果。
public class MyThread extends Thread {
@Override
public void run(){
while (true) {
if (this.isInterrupted()) {
System.out.println("线程停止了。");
return;
}
System.out.println("timer=" + System.currentTimeMillis());
}
}
}
MyThread thread = new MyThread();
thread.start();
Thread.sleep(2000);
thread.interrupt();
timer=1556332705000
timer=1556332706000
线程停止了。
不过建议使用 “抛异常” 的方法来实现线程的停止,因为在 catch
快中还可以将异常向上抛,使线程停止事件得以传播。
public class MyThread extends Thread {
@OVerride
public void run() {
super.run();
try {
for (int i = 0; i < 500000; i++) {
if (this.interrupted()) {
System.out.println("已经是停止状态了,我要退出了。");
throw new InterruptedException();
}
System.out.println("i=" + (i + 1));
}
System.out.println("我在for下面");
} catch (InterruptedException e) {
System.out.println("进入MyThread.java类run方法中的catch。");
e.printStackTrace();
}
}
}
try {
MyThread thread = new MyThread();
thread.start();
Thread.sleep(2000);
thread.interrupt();
} catch (InterruptedException e) {
System.out.println("main catch");
e.printStackTrace();
}
System.out.println("end!");
i=183973
i=183974
i=183975
i=183976
i=183977
已经是停止状态了,我要退出了。
end!
进入MyThread.java类run方法中的catch。
java.lang.InterruptedException
...
...
暂停线程
暂停线程意味着此线程还可以恢复运行,在 Java 多线程中,可以使用 suspend()
方法暂停线程,使用 resume()
方法恢复线程的执行。
suspend()
、resume()
方法缺点:
- 独占:
- 如果使用不当,极易造成公共的同步对象的独占,使得其他线程无法访问公共同步对象。
- 不同步
- 也容易出现因为线程的暂停而导致数据不同步的情况。
线程优先级
线程可以划分优先级,优先级较高的线程得到的 CPU 资源较多,也就是 CPU 优先执行优先级较高的线程对象中的任务。
设置线程优先级有助于帮 “线程规划器” 确定在下一次选择哪一个线程来优先执行。设置线程的优先级使用 setPriority()
方法。在 Java 中,线程的优先级分为 1~10 这 10 个级别,如果小于 1 或大于 10,则 JDK 抛出 throw new IllegalArgumentException()。
thread.setPriority(10);
守护线程
在 Java 线程中有两种线程,一种是用户线程,另一种是守护线程。守护线程是一种特殊的线程,它的特性有 “陪伴” 的含义,当进程中不存在非守护线程了,则守护线程自动销毁。典型的守护线程就是垃圾回收线程,当进程中没有非守护线程了,则垃圾回收线程也就没有存在的必要了,自动销毁。
Daemon
的作用是为其他线程的运行提供便利服务。
MyThread thread = new MyThread();
thread.setDaemon(true);
thread.start();
方法
currentThread()
Thread.currentThread()
方法可以返回代码段正被哪个线程调用的信息。
System.out.println(this.currentThread().getName());
isAlive()
thread.isAlive()
方法的作用是判断当前线程是否处于活动状态。什么是活动状态呢?活动状态就是线程已经启动且尚未终止。线程处于正在运行或准备开始运行状态,就认为线程是 “存活” 的。
sleep()
方法 Thread.sleep()
的作用是在指定的毫秒数内让当前 “正在执行的线程” 休眠(暂停执行)。这个 “正在执行的线程” 是指 this.currentThread()
返回的线程。
getId()
thread.getId()
方法的作用是取得线程的唯一标识。
yield()
Thread.yield()
方法的作用是放弃当前的 CPU 资源,将它让给其他的任务去占用 CPU 执行时间,但放弃的时间不确定,有可能刚刚放弃,马上又获得 CPU 时间片。
更新于2019年04月27日