并发编程1:全面认识 Thread

线程简介

现在操作系统在运行一个程序时,会自动为其创建一个进程,不论是 PC 还是 Android。

一个进程内可以有多个线程,这些线程作为操作系统调度的最小单元,负责执行各种各样的任务,这些线程都拥有各自的计数器、堆栈、局部变量等属性,并且可以访问共享内存

想象一下,如果你的电脑里只有一条线程在执行任务,一旦遇到 I/O 密集的任务,CPU 只能长时等待,效率很低。

如果把一个进程比作一个外卖公司,CPU 就是外卖公司拥有的主要资源(可以当做电动车),那线程(Thread)就是外卖公司中的一位送餐员,Runnable 就是送餐员要执行的任务(一般情况下都是送饭)。

线程创建的三种方式

送餐员最重要的任务就是送餐,我们以代码来演示创建一个送餐员的三种方式:

1.实现 Runnable 接口

public class ThreadTest0 {

    /**
     * 1.实现 Runnable 接口,在 run() 方法中写要执行的任务
     */
    static class Task implements Runnable{
        @Override
        public void run() {
            try {
                Thread.sleep(new Random().nextInt(300
                ));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ": 您的外卖已送达");
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 4; i++) {
            //2.创建一个送餐员线程,然后将任务传递给他,同时起个名
            Thread shixinzhang = new Thread(new Task(), "外卖任务 " + i);
            //3.命令送餐员出发!
            shixinzhang.start();
        }
    }
}

注意,上述代码中调用的是送餐员线程的 start() 方法,然后线程会调用 Task 对象的 run() 方法执行任务。运行结果如下:

外卖任务 3: 您的外卖已送达 外卖任务 1: 您的外卖已送达 外卖任务 0: 您的外卖已送达 外卖任务 2: 您的外卖已送达

可以看到执行任务的是各个线程。如果在 main() 方法中直接调用 run 方法,就相当于主线程直接执行任务,没有在子线程中进行。

直接在 main 中调用 run()

public static void main(String[] args) {
    for (int i = 0; i < 4; i++) {
        //2.创建一个送餐员线程,然后将任务传递给他,同时起个名
        Task task = new Task();
        Thread shixinzhang = new Thread(task, "外卖任务 " + i);
        //3.直接执行任务
        task.run();
    }
}

运行结果:

main: 您的外卖已送达 main: 您的外卖已送达 main: 您的外卖已送达 main: 您的外卖已送达

2.继承 Thread,重写其 run 方法

public class ThreadTest1 {
    /**
     * 继承 Thread,重写 run 方法,在 run 方法中写要执行的任务
     */
    static class DeliverThread extends Thread{

        public DeliverThread(String name) {
            super(name);
        }

        @Override
        public void run() {
            try {
                Thread.sleep(new Random().nextInt(300
                ));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ": 您的外卖已送达");
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 4; i++) {
            //2.创建一个送餐员线程,同时起个名
            DeliverThread shixinzhang = new DeliverThread("外卖任务" + i);
            shixinzhang.start();
        }
    }
}

运行结果:

外卖任务1: 您的外卖已送达 外卖任务3: 您的外卖已送达 外卖任务0: 您的外卖已送达 外卖任务2: 您的外卖已送达

为什么直接继承 Thread 也可以在子线程中执行任务呢?

Thread 源码中我们可以看到, Thread 其实也实现了 Runnable

public class Thread implements Runnable

它内部也有一个 Runnable 的引用,我们调用 start() 方法后送餐员小张就蓄势以待准备出发了,之所以没说“立即出发送餐”,是因为此时可能电动车(CPU)正在被别人使用。

线程 start() 后操作系统会给他分配相关的资源,包括单独的程序计数器(可以理解为送餐员的任务本,上面记录了当前送餐任务的地址和下一个任务的地址)和栈,操作系统会把这个线程作为一个独立的个体进行调度,分配时间片让它执行。

等线程被 CPU 调度后就会执行线程中的 run() 方法,因此我们通过重写 Threadrun() 方法就可以达到在子线程执行任务的目的。

3.实现 Callable 接口,重写 call() 方法,用 FutureTask 获得结果

public class CallableTest {
    /**
     * 实现 Callable 接口
     */
    static class DeliverCallable implements Callable<String> {
        /**
         * 执行方法,相当于 Runnable 的 run, 不过可以有返回值和抛出异常
         * @return
         * @throws Exception
         */
        @Override
        public String call() throws Exception {
            Thread.sleep(new Random().nextInt(10000));
            System.out.println(Thread.currentThread().getName() + ":您的外卖已送达");
            return Thread.currentThread().getName() + " 送达时间:" + System.currentTimeMillis() + "\n";
        }
    }

    /**
     * Callable 作为参数传递给 FutureTask,FutureTask 再作为参数传递给 Thread(类似 Runnable),然后就可以在子线程执行
     * @param args
     */
    public static void main(String[] args) {
        List<FutureTask<String>> futureTasks = new ArrayList<>(4);
        for (int i = 0; i < 4; i++) {
            DeliverCallable callable = new DeliverCallable();
            FutureTask<String> futureTask = new FutureTask<>(callable);
            futureTasks.add(futureTask);

            Thread thread = new Thread(futureTask, "送餐员 " + i);
            thread.start();
        }

        StringBuilder results = new StringBuilder();
        futureTasks.forEach(futureTask -> {
            try {
                //获取线程返回结果,没返回就会阻塞
                results.append(futureTask.get());
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        });
        System.out.println(System.currentTimeMillis() + " 得到结果:\n" + results);
    }
}

第三种创建线程的方式与前两种的不同之处在于,以 Callable 作为任务,而不是 Runnable,这种方式的好处是可以获得结果响应中断

Callable, FutureFutureTask 的内容我会另开一篇文章专门介绍。

运行结果:

送餐员 3:您的外卖已送达 送餐员 1:您的外卖已送达 送餐员 0:您的外卖已送达 送餐员 2:您的外卖已送达 1487998155430 得到结果: 送餐员 0 送达时间:1487998155076 送餐员 1 送达时间:1487998150453 送餐员 2 送达时间:1487998155430 送餐员 3 送达时间:1487998149779

线程的基本属性

1.优先级

Thread 有个优先级字段:private int priority

操作系统采用时间片(CPU 单次执行某线程的时间)的形式来调度线程的运行,线程被 CPU 调用的时间超过它的时间片后,就会发生线程调度。

线程的优先级可以在一定程度上影响它得到时间片的多少,也就是被处理的机会。

Java 中 Thread 的优先级为从 1 到 10 逐渐提高,默认为 5。

有长耗时操作的线程,一般建议设置低优先级,确保处理器不会被独占太久; 频繁阻塞(休眠或者 I/O)的线程建议设置高优先级。

 public final static int MIN_PRIORITY = 1;

 //线程的默认优先级
 public final static int NORM_PRIORITY = 5;

 public final static int MAX_PRIORITY = 10;

线程优先级只是对操作系统分配时间片的建议。 虽然 Java 提供了 10 个优先级别,但不同的操作系统的优先级并不相同,不能很好的和 Java 的 10 个优先级别对应。>所以我们应该使用 MAX_PRIORITY、MIN_PRIORITY 和 NORM_PRIORITY 三个静态常量来设定优先级,这样才能保证程序最好的可移植性。

2.守护线程

Java 中,线程也分三六九等。守护线程相当于小弟,做一些后台调度、支持性工作,比如 JVM 的垃圾回收、内存管理等线程都是守护线程。

Thread 中有个布尔值标识当前线程是否为守护线程:

private boolean     daemon = false;

同时也提供了设置和查看当前线程是否为守护线程的方法:

public final void setDaemon(boolean on) {
    checkAccess();
    if (isAlive()) {
        throw new IllegalThreadStateException();
    }
    daemon = on;
}

public final boolean isDaemon() {
    return daemon;
}

Daemon 属性需要在调用线程的 start() 方法之前调用。

一个进程中,如果所有线程都退出了,Java 虚拟机就会退出。注意了,这里的“所有”就不包括守护线程,也就是说,当除守护线程外的其他线程都结束后,Java 虚拟机就会退出,然后将守护进程终止。

这里需要注意的是,由于上述特性,Java 虚拟机退出后,在守护线程中的 finally 块中的代码不一定执行。

举个例子:

public class DaemonTreadTest0 {
    static class DaemonThread extends Thread{
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                System.out.println(Thread.currentThread().getName() + " finally is called!");
            }
        }
    }

    public static void main(String[] args) {
        DaemonThread thread = new DaemonThread();
        thread.setDaemon(true);
        thread.start();
    }
}

上述代码中将线程设置为守护线程,由于 main 线程启动 DaemonThread 后就结束,此时虚拟机中没有非守护线程,虚拟机也会退出,守护进程被终止,但是它的 finally 块中的内容却没有被调用。

如果将setDaemon方法注释掉,就会发现有运行结果:

Thread-0 finally is called!

因此,守护线程中不能依靠 finally 块进行资源关闭和清理。

线程的生命周期

线程具有如下几个状态:

线程状态

介绍

备注

NEW

新创建

还未调用 start() 方法;还不是活着的 (alive)

RUNNABLE

就绪的

调用了 start() ,此时线程已经准备好被执行,处于就绪队列;是活着的(alive)

RUNNING

运行中

线程获得 CPU 资源,正在执行任务;活着的

BLOCKED

阻塞的

线程阻塞于锁或者调用了 sleep;活着的

WAITING

等待中

线程由于某种原因等待其他线程;或者的

TIME_WAITING

超时等待

与 WAITING 的区别是可以在特定时间后自动返回;活着的

TERMINATED

终止

执行完毕或者被其他线程杀死;不是活着的

有几点注意:

  • Java 中的 Thread 运行状态没有 RUNNING 这一步,运行中的线程状态是 RUNNABLE
  • 三个让线程进入 WAITING 状态的方法
    • Object.wait()
    • Thread.join()
    • LockSupport.park()
    • Lock.lock()

Java 中关于“线程是否活着”的定义

Thread 中有个判断是否为活着的方法:

public final native boolean isAlive()

Java 中线程除了 NEW 和 TERMINITED 状态,其他状态下调用 isAlive() 方法均返回 true,也就是活着的。

线程的关键方法

1.Thread.sleep()

Thread.sleep() 是一个静态方法:

public static native void sleep(long millis) throws InterruptedException;

sleep() 方法:

  • 使当前所在线程进入阻塞
  • 只是让出 CPU ,并没有释放对象锁
  • 由于休眠时间结束后不一定会立即被 CPU 调度,因此线程休眠的时间可能大于传入参数
  • 如果被中断会抛出 InterruptedException

注意上面的第一条!由于 sleep 是静态方法,它的作用时使当前所在线程阻塞。因此最好在线程内部直接调用 Thread.sleep(),如果你在主线程调用某个线程的 sleep() 方法,其实阻塞的是主线程!

2.Object.wait()

Thread.sleep() 容易混淆的是 Object.wait() 方法。

Object.wait() 方法:

  • 让出 CPU,释放对象锁
  • 在调用前需要先拥有某对象的锁,所以一般在 synchronized 同步块中使用
  • 使该线程进入该对象监视器的等待队列

3.Thread.yield()

Thread. yield() 也是一个静态方法:

public static native void yield();

“Thread.yield() 表示暂停当前线程,让出 CPU 给优先级与当前线程相同,或者优先级比当前线程更高的就绪状态的线程。

  • 和 sleep() 方法不同的是,它不会进入到阻塞状态,而是进入到就绪状态。
  • yield() 方法只是让当前线程暂停一下,重新进入就绪的线程池中。

yield() 一般使用较少。

4.Thread.join()

Thread.join() 表示线程合并,调用线程会进入阻塞状态,需要等待被调用线程结束后才可以执行。

线程的合并的含义就是将几个并发执行线程的线程合并为一个单线程执行。

比如下述代码:

Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("thread is running!");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
});
thread.start();
thread.join();
System.out.println("main thread ");

我们在主线程调用了 thread.join() 方法,该线程会在输出一句话后休眠 5 秒,等该线程结束后主线程才可以继续执行,输出最后一句结果:

thread is running! main thread

Thread.join 源码:

//无参方法
public final void join() throws InterruptedException {
    join(0);
}
//有参方法,表示等待 millis 毫秒后自动返回
public final synchronized void join(long millis)
throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) {
        while (isAlive()) {
            wait(0);
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}
//有参方法,表示等待 millis + (nanos - 50000) 毫秒后结束
public final synchronized void join(long millis, int nanos)
throws InterruptedException {

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (nanos < 0 || nanos > 999999) {
        throw new IllegalArgumentException(
                            "nanosecond timeout value out of range");
    }

    if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
        millis++;
    }

    join(millis);
}

通过源码可以发现,Thread.join 是通过 synchronized + Object.wait() 实现的

Thread.join 的应用场景是:当一个线程必须等待其他线程执行完毕才能继续执行,比如合并计算。

线程的中断

有时候我们需要中断一个正在运行的线程,一种很容易想到的方法是在线程的 run() 方法中加一个循环条件:

public class ThreadInterruptTest1 {
    static class InterruptThread extends Thread{
        private boolean running;

        public InterruptThread(boolean running) {
            this.running = running;
        }

        public boolean isRunning() {
            return running;
        }

        public void setRunning(boolean running) {
            this.running = running;
        }

        @Override
        public void run() {
            while (running){
                System.out.println(Thread.currentThread().getName() + " is running");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        InterruptThread thread = new InterruptThread(true);
        thread.start();

        Thread.sleep(5000);
        thread.setRunning(false);
    }
}

上面的代码中线程 InterruptThread 有一个标志位 running,当这个标志位为 true 时才可以运行。 因此我们可以通过修改这个标志位为 false 来中断该线程。

其实 Thread 内部也为我们提供了同样的机制 :

方法名

方法介绍

public void interrupt()

试图中断调用线程,设置中断标志位为 false

public boolean isInterrupted()

返回调用线程是否被中断

public static boolean interrupted()

返回当前线程是否被中断的状态值,同时将中断标志位复位(设为 false)

1.public void interrupt()

它的作用是设置标志位为 false,能否达到中断调用线程的效果,还取决于该线程是否可以响应中断(说直白些就是吃不吃这套),比如 Runnablerun() 方法就无法响应中断。

因此我们对执行 Runnable 任务的线程调用 interrupt() 方法后,该线程也不会中断,举个例子:

public class ThreadInterruptTest2 {

    static class UnInterruptThread extends Thread{
        public UnInterruptThread(String s) {
            setName(s);
        }

        @Override
        public void run() {
            while (true) {
                System.out.println(Thread.currentThread().getName() + " is running!");
            }
        }
    }

    static class  UnInterruptRunnable implements Runnable{

        @Override
        public void run() {
            while (true) {
                System.out.println(Thread.currentThread().getName() + " is running!");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        UnInterruptThread thread = new UnInterruptThread("无法中断的线程");
//        Thread thread = new Thread(new UnInterruptRunnable(), "无法中断");
        thread.start();

        //先让它执行一秒
        Thread.sleep(1000);

        thread.interrupt();

        //不立即退出
        Thread.sleep(3000);
    }
}

这两种方式创建的线程,在调用 thread.interrupt() 方法后仍然会继续执行!

这时就需要用到上面 Thread 提供的第二个关于中断的方法 isInterrupted() 了。

2.public boolean isInterrupted()

我们可以通过 isInterrupted() 知道调用线程是否被中断,以此来作为线程是否运行的判断标志。

isInterrupted() 在刚创建时默认为 false 不用多说; 线程有许多方法可以响应中断(比如 Thread.sleep()Thread.wait()),这些方法在收到中断请求、抛出 InterruptedException 之前,JVM 会先把该线程的中断标志位复位,这时调用 isInterrupted 将会返回 false; 线程结束后,线程的中断标志位也会复位为 false。

举个例子:

/**
 * 线程中断练习
 * Created by zhangshixin on 17/2/25.
 * http://blog.csdn.net/u011240877
 */
public class ThreadInterruptTest {
    /**
     * 调用 Thread.sleep() 方法的线程,线程如果在 sleep 时被中断,会抛出 InterruptedException
     * 我们在代码中进行捕获,并且查看 JVM 是否将中断标志位重置
     */
    static class SleepThread extends Thread{
        public SleepThread(String s) {
            setName(s);
        }
        @Override
        public void run() {
            while (!isInterrupted()){
                try {
                    Thread.sleep(500);
                    System.out.println(Thread.currentThread().getName() + System.currentTimeMillis());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    System.out.println("SleepRunner 在 sleep 时被中断了,此时中断标志位为:" + isInterrupted());
                }
            }
        }
    }

    /**
     * 希望通过这个线程了解:线程运行结束后,中断标志位会重置
     */
    static class BusyThread extends Thread{

        public BusyThread(String s) {
            setName(s);
        }

        @Override
        public void run() {
            while (!isInterrupted()){
                System.out.println(Thread.currentThread().getName() + System.currentTimeMillis());
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SleepThread sleepThread = new SleepThread("SleepRunner:");
        BusyThread busyThread = new BusyThread("BusyRunner:");
        //新创建的线程 中断标志为 false
        System.out.println("SleepThread 新创建时的中断标志位:" + sleepThread.isInterrupted());

        Thread.sleep(2000);
        //启动两个线程
        sleepThread.start();
        busyThread.start();
        //让它们运行一秒
        Thread.sleep(1000);
        //分别中断两个线程
        sleepThread.interrupt();
        busyThread.interrupt();
        //查看线程的中断标志位
        Thread.sleep(2000);
        System.out.println("由于中断标志位变为 true 导致运行结束的线程,中断标志位为: " + busyThread.isInterrupted());

        Thread.sleep(1000);
    }
}

上述代码中 两个线程都使用 isInterrupted 作为循环执行任务的条件,其中 SleepThread 方法调用了 Thread.sleep,这个方法的会响应中断,抛出异常。

运行结果如下:

可以看到:

  • 线程中,在抛出 InterruptedException 前 JVM 的确会重置中断标志位为 false
  • 这将导致以 isInterrupted 方法作为循环执行任务的线程无法正确中断

3.public static boolean interrupted()

Thread.interrupted() 方法是一个静态方法,它会返回调用线程(而不是被调用线程)的中断标志位,返回后重置中断标志位。

因此 Thread.interrupted() 第二次调用永远返回 false。

源码:

public static boolean interrupted() {
    return currentThread().isInterrupted(true);
}

总结

这篇文章总结了 线程的基本概念和关键方法,还有一些不建议使用的方法没有介绍,是因为它们有很多副作用,比如 suspend() 方法在调用后虽然线程会进入休眠状态,却不会释放资源,很容易引发死锁问题;同样,stop() 方法终结一个线程时无法保证这个线程有机会释放资源,也会导致一些不确定问题。

我们可以通过下面的图片整体分析线程的生命周期和主要方法:

相关阅读: 趣谈并发2:认识并发编程的利与弊

欢迎扫描关注微信公众号“安卓进化论”,向高手进击!

Thanks

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏我是攻城师

理解Java并发工具类SynchronousQueue

SynchronousQueue类是JDK5中引入的一个同步队列,这个类比较特殊,因为它虽然是一个队列但实际上并不真正的存储数据,仅仅维护一个线程配对的队列列表...

962
来自专栏java相关

并发基本概念介绍

985
来自专栏Java成长之路

深入理解多线程

多线程是java中比较重要的一部分内容,使用多线程有许多的优点: - 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。 - 程序需要实现一些需...

943
来自专栏Java Edge

Java中的BlockingQueue1 Java中的阻塞队列2 生产者和消费者例子2 Java里的阻塞队列

3626
来自专栏高爽的专栏

Java线程(四):线程中断、线程让步、线程睡眠、线程合并

最近在Review线程专栏,修改了诸多之前描述不够严谨的地方,凡是带有Review标记的文章都是修改过了。本篇文章是插进来的,因为原来没有写,现在...

2240
来自专栏猿天地

面试题之死锁解密

在多线程环境中,我们经常会遇到多个线程访问同一个共享资源的情况,这个时候必须考虑如何维护数据一致性,常见的方式是加锁处理。只有拿到锁的线程才可以访问共享资源,通...

1111
来自专栏xdecode

Java高并发之设计模式.

至于为什么要volatile关键字, 主要涉及到jdk指令重排, 详见之前的博文: Java内存模型与指令重排

661
来自专栏xdecode

线程的基本操作

1466
来自专栏拭心的安卓进阶之路

并发编程1:全面认识 Thread

线程简介 现在操作系统在运行一个程序时,会自动为其创建一个进程,不论是 PC 还是 Android。 一个进程内可以有多个线程,这些线程作为操作系统调度的最小单...

1955
来自专栏JAVA同学会

Zookeeper应用之——栅栏(barrier)

barrier的作用是所有的线程等待,知道某一时刻,锁释放,所有的线程同时执行。举一个生动的例子,比如跑步比赛,所有 运动员都要在起跑线上等待,直到枪声响后,所...

621

扫码关注云+社区