使用多线程

在 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() 方法强行终止线程,但是不推荐使用这个方法,因为 stopsuspendresume 一样,都是作废过期的方法,使用它们可能产生不可预料的结果。
    • 使用 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日

results matching ""

    No results matching ""