Thread线程类

Java虚拟机中使用线程实现并发逻辑,JVM中的线程是和操作系统中的线程对应的。本篇笔记我们介绍如何在Java语言中创建和使用线程。

使用Thread类创建线程

Java中使用线程首先要创建Thread线程类,然后执行其start()方法。创建线程类的方法有2种:一种是实现Runnable接口,我们可以实现该接口的run()方法编写线程逻辑,然后将其作为参数传入Thread类;另一种是继承Thread类并重写run()方法。两种方法没有区别,出于软件工程性考虑我们一般使用前者。

方式1:实现Runnable接口

public class MyThread1 implements Runnable {
    @Override
    public void run() {
        System.out.println("hello from my thread 1");
    }
}

方式2:继承Thread

public class MyThread2 extends Thread {
    @Override
    public void run() {
        System.out.println("hello from my thread 2");
    }
}

创建线程并执行:

public static void main(String[] args) {
  Thread t1 = new Thread(new MyThread1());
  Thread t2 = new MyThread2();
  t1.start();
  t2.start();
}

注意Thread类和Runnable的关系:Thread类实现了Runnable接口,Runnable接口中只有一个方法run(),这个方法中的代码就是在新线程中执行的代码。因此使用继承Thread的方式创建线程,就必须重写run()方法。

看了上面例子那么问题来了,虽然Thread实现了Runnable接口,但是上面代码中创建t1线程,既new了自定义的MyThread1,又new了已经实现Runnable接口的Thread类,到底哪个类的Runnable被执行?对于这个问题我们可以看看Thread类的代码:

public class Thread implements Runnable {
    private Runnable target;
    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
}

原来JDK中Thread类中有个target属性,它就是new Thread(Runnable target)时传入的参数,Threadrun()方法啊会判断这个target,如果它不为null就会执行target中的run()。那么,我们是不是也可以这样写呢?

Thread t2 = new Thread(new MyThread2());

注:MyThread2继承Thread类。

答案是可以的,但是这样写不好,这不符合软件工程。Runnable是接口,接口描述一组行为,这样写传入的是一个具有该行为的对象实例,而这个实例除了表示Runnable的行为以外毫无意义。因此最佳实践:通常情况下,建议使用实现Runnable接口的方式创建线程。如果仅仅想创建一个在新线程中执行的函数,可以在new Thread时传入匿名内部类(或JDK8新引入的Lambda表达式)实现Runnable

例子:

Thread t4 = new Thread(new Runnable() {
  @Override
  public void run() {
    System.out.println("hello from my thread");
  }
});

线程安全问题

我们知道多个线程可以共享一些数据,也可以不共享,如果存在共享的数据处理不好就可能产生线程安全问题,此外在Java这种面向对象语言中情况还要更复杂一些,线程和对象实例我们要区分清楚,下面以例子形式进行说明。这里我们定义一个线程类:

MyThread.java

public class MyThread implements Runnable {
    private int i = 0;

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

没有线程安全问题的写法:

public class Main
{
    public static void main(String[] args)
    {
        Thread t1 = new Thread(new MyThread(), "thread 1");
        Thread t2 = new Thread(new MyThread(), "thread 2");
        Thread t3 = new Thread(new MyThread(), "thread 3");

        t1.start();
        t2.start();
        t3.start();
    }
}

我们实例化了三个MyThread类,每个MyThread有自己的局部变量,因此三个线程都有自己的局部变量,不存在线程安全问题。

运行结果:

三个线程都输出1,这是正确的结果。

有线程安全问题的写法:

public class Main
{
    public static void main(String[] args)
    {

        MyThread myThread = new MyThread();

        Thread t1 = new Thread(myThread, "thread 1");
        Thread t2 = new Thread(myThread, "thread 2");
        Thread t3 = new Thread(myThread, "thread 3");

        t1.start();
        t2.start();
        t3.start();
    }
}

我们只实例化了一个MyThread类,因此三个线程共享同一个变量,此时就产生了线程安全问题:

运行结果1:

运行结果2:

这种写法多次运行甚至会出现不同的运行结果,这种不确定性显然很容易造成严重问题。线程切换是操作系统决定的,我们并不能控制,不同的线程切换顺序可能造成不同的结果,多个线程修改同一个变量会造成冲突。然而,有时候我们还必须要让多个线程共享一个变量,这该如何解决?答案是使用线程同步,Java中可以使用synchronized同步代码块或Lock锁接口来实现线程同步,这将在后面章节中介绍。

join 等待子线程执行完成

多线程编程中,一个比较常见的场景是主线程等待子线程(可能是多个)再继续执行,此时可以使用join()方法。

package com.gacfox.demo;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new MyThread1());
        Thread t2 = new Thread(new MyThread2());
        // 同时启动t1和t2
        t1.start();
        t2.start();
        // 等待t1和t2全部完成
        t1.join();
        t2.join();
    }
}

上面代码中,我们同时启动了t1和t2两个线程,因此两个线程是并发执行的,之后我们等待两个线程全部执行完毕主线程再返回。

package com.gacfox.demo;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new MyThread1());
        Thread t2 = new Thread(new MyThread2());
        // 启动t1
        t1.start();
        // 等待t1完成
        t1.join();
        // 启动t2
        t2.start();
        // 等待t2完成
        t2.join();
    }
}

上面代码稍微调整了代码的顺序,线程的调度逻辑就变了,这里我们先执行线程t1,等t1执行完后才开始执行线程t2。

Thread线程类详解

这里我们再展开介绍一些Thread类相关的概念和具体方法。

Thread.currentThread():该方法返回当前线程的实例引用。例如我们创建线程时,可以给线程命名:

Thread t1 = new Thread(new MyThread(), "thread 1");

此时我们就可以在子线程中用Thread.currentThread()取得当前的线程名:

Thread.currentThread().getName();

Thread.sleep(long millis):该方法会使当前线程睡眠指定毫秒数。注意:这是一个静态方法,只能在某个线程中调用并睡眠当前线程。

isAlive():该方法返回某个线程是否是活动线程。活动线程指已开始且未执行结束的线程,注意线程sleep时也属于活动线程。

getId():返回线程的唯一标识。

setPriority(int priority):线程能够设定优先级,优先级取值1-10,或者使用Thread.MAX_PRIORITYThread.MIN_PRIORITYThread.NORM_PRIORITY这些预定义的常量。理论上,优先级越高的线程,获得CPU执行的时机越多,不过实际上是否优先执行完全由操作系统决定,该方法指定的优先级仅对操作系统有参考意义。此外,一个线程中启动另一个线程,新线程会继承原来线程的优先级。

setDaemon(boolean on):该方法设置线程是否为守护线程,守护线程会在主线程结束时自动结束。创建守护线程时,设置thread.setDaemon(true)就可以了。

为什么要使用守护线程?实际开发中通常有这样的需求,例如:编写一个编辑器,有拼写检查功能。这个“拼写检查”可以放在子线程中,用一个无限循环判断编辑器里的内容。如果使用普通线程实现拼写检查,普通线程不会在主线程结束时自动结束,子线程不结束,程序无法退出,因此需要主线程手动结束子线程再退出,虽然也可以实现,但这就有点麻烦了,因此出现了守护线程的概念。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。
Copyright © 2017-2024 Gacfox All Rights Reserved.
Build with NextJS | Sitemap