Java并发编程(三)—— 线程的创建、启动与终止

1 线程的属性

线程有一些基本的属性,如idname、以及 priority

  • id:线程id用于标识不同的线程,编号可能被后续创建的线程使用,编号是只读属性,不能修改;
  • name:线程的名称,默认值是Thread-(id)
  • daemon:是守护线程还是用户线程,可以通过setDaemon(true)把线程设置为守护线程。守护线程通常用于执行不重要的任务,比如监控其他线程的运行情况,GC线程就是一个守护线程。 setDaemon()要在线程启动前设置,否则JVM会抛出非法线程状态异常,可被继承。
  • priority:线程调度器会根据这个值来决定优先运行哪个线程(不保证),优先级的取值范围为1~10,默认值是 5,可被继承。Thread中定义了下面三个优先级常量:
    • 最低优先级:MIN_PRIORITY = 1
    • 默认优先级:NORM_PRIORITY = 5
    • 最高优先级:MAX_PRIORITY = 10

daemon [ˈdiːmən] 守护进程;后台程序 priority [praɪˈɔːrəti] 优先,优先权

Daemon线程被用作完成支持性工作,但是在Java虚拟机退出时Daemon线程中的finally块并不一定会执行,示例如下:

public class Daemon {
  public static void main(String[] args) {
    Thread thread = new Thread(new DaemonRunner(), "DaemonRunner");
    thread.setDaemon(true);
    thread.start();
  }

  static class DaemonRunner implements Runnable {
    @Override
    public void run() {
      try {
        SleepUtils.second(10);
      } finally {
        System.out.println("DaemonThread finally run.");
      }
    }
  }
}

运行Daemon程序,可以看到在终端或者命令提示符上没有任何输出。main线程(非Daemon线程)在启动了线程DaemonRunner之后随着main方法执行完毕而终止,而此时Java虚拟机中已经没有非Daemon线程,虚拟机需要退出,Java虚拟机中的所有Daemon线程都需要立即终止, 因此DaemonRunner立即终止,但是DaemonRunner中的finally块并没有执行。

注意:在构建Daemon线程时,不能依靠finally块中的内容来确保执行关闭或清理资源的逻辑。

一个Java程序从main()方法开始执行,然后按照既定的代码逻辑执行,看似没有其他线程参与,但实际上Java天生就是多线程的, 因为执行main()方法的是一个名称为main的线程。

一个Java程序的运行不仅仅是main()方法的运行,而是main线程和多个其他线程的同时运行。

2 在Java中创建多线程

以下是Thread.java中的注释:

/*
 * There are two ways to create a new thread of execution. 
 * One is to declare a class to be a subclass of <code>Thread</code>. 
 * This subclass should override the <code>run</code> method of class <code>Thread</code>. 


 * The other way to create a thread is to declare a class that implements the   
 * <code>Runnable</code> interface. 
 * That class then implements the <code>run</code> method.
*/
  
public class Thread implements Runnable {
    public Thread() { }
    public Thread(Runnable target) { }
    Thread(Runnable target, AccessControlContext acc) { }
    public Thread(ThreadGroup group, Runnable target) { }
    public Thread(String name) { }
    public Thread(ThreadGroup group, String name) { }
    Thread(ThreadGroup group, String name, int priority, boolean daemon) { }
    public Thread(Runnable target, String name) { }
    public Thread(ThreadGroup group, Runnable target, String name) { }
    public Thread(ThreadGroup group, Runnable target, String name, long stackSize) { }
}

因此,线程的创建方式只有以下两种:

2.1 继承Thread
  1. 定义一个Thread类的子类,重写run()方法,在run()方法中实现线程要执行的相关逻辑
  2. 创建自定义的线程子类对象
  3. 调用子类实例的start()方法来重启线程
class MyThread extends Thread {
    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "run方法正在执行");
    }
}


public class OnlyMain {

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
        System.out.println(Thread.currentThread().getName() + "run方法正在执行");
    }   
}
// mainrun方法正在执行
// Thread-0run方法正在执行
2.2 实现Runnable接口
  1. 定义Runnable接口实现类MyRunnable,并重写run()方法
  2. 创建MyRunnable的实例myRunnable,以myRunnable作为target创建Thread对象,Thread才是真正的线程对象。
  3. 调用线程对象的start()方法
class MyRunnable implements Runnable {

    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "run方法正在执行");
    }
}

public class OnlyMain {

    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
        System.out.println(Thread.currentThread().getName() + "run方法正在执行");
    }
}
// mainrun方法正在执行
// Thread-0run方法正在执行

ThreadRunnable的区别:Thread是对线程的抽象,Runnable是对任务的抽象。Thread是实现了Runnable接口,是对Runnable的扩展。 以下是Thead的源码:

public class Thread implements Runnable { }

实现RunnableThread哪个相比更好:

  • 从代码的角度来看,Java是单继承实现,所以Runnable比较好
  • 从扩展性角度来看:继承方式(Thread)—— 线程对象和任务对象耦合在一起;实现方式(Runnable)——线程对象和任务对象是分离的,耦合性较低,扩展性较高。

为了避免Java中单继承的限制,增强程序的健壮性,代码可以被多个线程共享。线程池只能放入实现Runnable类线程,不能直接放入继承Thread的类中—— 所以,Runnable更好。

Callable为什么不算呢?因为把Callable交给Thread去执行的时候,本质上是把Callable包装成了一个FutureTask,而FutureTask实现了RunnableFuture这个接口,而RunnableFuture派生与Runnable这个接口——所以,实际上是把Callable包装成一个Runable交给一个Thread去执行。

class MyCallable implements Callable<String> {

    @Override
    public String call() throws Exception {
        Thread.sleep(1000);
        System.out.println(Thread.currentThread().getName() + "-call方法正在运行");
        return "Hello World";
    }
}

public class OnlyMain {

    public static void main(String[] args) {
        MyCallable myCallable = new MyCallable();
        FutureTask<String> futureTask = new FutureTask(myCallable);
        Thread thread = new Thread(futureTask);
        thread.start();
        System.out.println(Thread.currentThread().getName() + "-run方法正在执行");
    }
}

// main-run方法正在执行
// Thread-0-call方法正在运行

以下是CallableFutureTaskRunnableFuture相关源码:

public interface Callable<V> {
  	V call() throws Exception;
}

public class FutureTask<V> implements RunnableFuture<V> {
  public FutureTask(Callable<V> callable) {
    if (callable == null)
      throw new NullPointerException();
    this.callable = callable;
    this.state = NEW;       // ensure visibility of callable
  }
}

public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}

3 启动和终止线程

3.1 构造线程

在运行线程之前首先要构造一个线程对象,线程对象在构造的时候需要提供线程所需要的属性,如线程所属的线程组、线程优先级、是否是Daemon线程等信息。 以下是Thead的源码:

public class Thread implements Runnable {

  public Thread() {
    init(null, null, "Thread-" + nextThreadNum(), 0);
  }

  public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
  }

  private void init(ThreadGroup g, Runnable target, String name,
                    long stackSize, AccessControlContext acc) {
    if (name == null) {
      throw new NullPointerException("name cannot be null");
    }
    this.name = name;
    // 当前线程就是该线程的父线程
    Thread parent = currentThread();
    if (g == null) {
      g = parent.getThreadGroup();
    }
    g.addUnstarted();
    this.group = g;
    // 将daemon、priority属性设置为父线程的对应属性
    this.daemon = parent.isDaemon();
    this.priority = parent.getPriority();
    this.target = target;
    init2(parent);
    this.stackSize = stackSize;
    // 分配一个线程ID
    tid = nextThreadID();
  }
}

在上述过程中,一个新构造的线程对象是由其parent线程来进行空间分配的,而child线程继承了parent是否为Daemon、优先级和加载资源的contextClassLoader以及可继承的ThreadLocal,同时还会分配一个唯一的ID来标识这个child线程。至此,一个能够运行的线程对象就初始化好了,在堆内存中等待着运行。

3.2 启动线程

线程对象在初始化完成之后,调用start()方法就可以启动这个线程。

线程start()方法的含义是:当前线程(即parent线程)同步告知Java虚拟机,只要线程规划器空闲,应立即启动调用start()方法的线程。注意:启动一个线程前,最好为这个线程设置线程名称。

Threadrun()start()的区别?

run() 和普通的成员方法一样,可以被重复调用。但是如果单独调用run方法,则不是在子线程中执行。

start()方法只能被调用一次。调用这个方法后程序会启动一个新的线程来执行run()方法。注意:调用start()方法后,线程处于就绪状态,一旦得到CPU时 间片,就开始执行run()方法。run()方法结束后,线程则立即终止。

3.3 理解中断

中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作。通过调用该线程的interrupt()方法对其进行中断操作。

线程通过检查自身是否被中断来进行响应,通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()对当前线程的中断标识位进行复位。如果该线程已经处于终结状态,即使该线程被中断过,在调用该线程对象的isInterrupted()时依旧会返回false 以下是源码:

public class Thread implements Runnable {
  
  public void interrupt() {
    if (this != Thread.currentThread())
      checkAccess();

    synchronized (blockerLock) {
      Interruptible b = blocker;
      if (b != null) {
        interrupt0();           // Just to set the interrupt flag
        b.interrupt(this);
        return;
      }
    }
    interrupt0();
  }

  @FastNative
  public static native boolean interrupted();

  @FastNative
  public native boolean isInterrupted();
}
  • interrupt():安全中断,发送一个通知告知目标线程将要中断,是否中断由线程本身决定,设置标志位flag = true(中断标志位表示当前线程已经中断)。 因为Java中的线程是协作式的,不是抢占式的,线程通过检查自身的中断标志位是否被置为true来进行响应。
  • Thread.interrupted:判断当前线程是否被中断,并清理中断状态,设置标志位flag = false
  • isInterrupted:判断当前线程是否被中断。
class UserThread extends Thread {
    public UserThread(String name) {
      super(name);
    }

    @Override
    public void run() {
      String threadName = Thread.currentThread().getName();
      System.out.println(threadName + " Thread = start = interrupt: " + isInterrupted());
			// while (!isInterrupted()) {
      while (!interrupted()) {
        System.out.println(threadName + " Thread = while = interrupt: " + isInterrupted());
      }
      System.out.println(threadName + " Thread = end = interrupt: " + isInterrupted());
    }
}

public static void main(String[] args) throws InterruptedException {
    UserThread userThread = new UserThread("cah");
    userThread.start();
    userThread.sleep(2);
    userThread.interrupt();
}

// isInterrupted:
// cah Thread = start = interrupt: false
// cah Thread = while = interrupt: false
// cah Thread = while = interrupt: false
// cah Thread = while = interrupt: false
// cah Thread = while = interrupt: false
// cah Thread = end = interrupt: true

// interrupted
// cah Thread = start = interrupt: false
// cah Thread = while = interrupt: false
// cah Thread = end = interrupt: false

注意:处于死锁状态的线程无法被中断

JavaAPI中可以看到,许多声明抛出InterruptedException的方法(例如Thread.sleep(long millis)方法)。这些方法在抛出InterruptedException之前,Java虚拟机会先将该线程的中断标识位清除,然后抛出InterruptedException,此时调用isInterrupted()方法将会返回false

下面代码中,首先创建了两个线程,SleepThreadBusyThread,前者不停地睡眠,后者一直运行,然后对这两个线程分别进行中断操作,观察二者的中断标识位。

public class Interrupted {

  public static void main(String[] args) throws InterruptedException {
    // sleepThread不停的尝试睡眠
    Thread sleepThread = new Thread(new SleepRunner(), "SleepThread");
    sleepThread.setDaemon(true);
    // busyThread不停的运行
    Thread busyThread = new Thread(new BusyRunner(), "BusyThread");
    busyThread.setDaemon(true);
    sleepThread.start();
    busyThread.start();
    // 休眠5秒,让sleepThread和busyThread充分运行
    TimeUnit.SECONDS.sleep(5);
    sleepThread.interrupt();
    busyThread.interrupt();
    System.out.println("SleepThread interrupted is " + sleepThread.isInterrupted());
    System.out.println("BusyThread interrupted is " + busyThread.isInterrupted());
    // 防止sleepThread和busyThread立刻退出
    SleepUtils.second(2);
  }


  static class SleepRunner implements Runnable {
    @Override
    public void run() {
      while (true) {
        SleepUtils.second(10);
      }
    }
  }

  static class BusyRunner implements Runnable {
    @Override
    public void run() {
      while (true) {

      }
    }
  }
}
// SleepThread interrupted is false
// BusyThread interrupted is true
// java.lang.InterruptedException: sleep interrupted

从结果可以看出,抛出InterruptedException的线程SleepThread,其中断标识位被清除了, 而一直忙碌运作的线程BusyThread,中断标识位没有被清除。

3.4 过期的suspend()resume()stop()

suspend [səˈspend] 暂停,中止;

线程的暂停、恢复和停止对应在ThreadAPI就是suspend()resume()stop()。但是这些API都是过期的,不建议使用。 以下是源码:

public class Thread implements Runnable {
  @Deprecated // 弃用
  public final void stop() {
    throw new UnsupportedOperationException();
  }

  @Deprecated
  public final void suspend() {
    throw new UnsupportedOperationException();
  }

  @Deprecated
  public final void resume() {
    throw new UnsupportedOperationException();
  }
}

下面代码中,创建了一个线程PrintThread,它以1秒的频率进行打印,而主线程对其进行暂停、恢复和停止操作:

public class Deprecated {
  
  public static void main(String[] args) throws InterruptedException {
    DateFormat format = new SimpleDateFormat("HH:mm:ss");
    Thread printThread = new Thread(new Runner(), "PrintThread");
    printThread.setDaemon(true);
    printThread.start();
    TimeUnit.SECONDS.sleep(3);
    printThread.suspend();
    System.out.println("main suspend PrintThread at " + format.format(new Date()));
    TimeUnit.SECONDS.sleep(3);
    printThread.resume();
    System.out.println("main resume PrintThread at " + format.format(new Date()));
    TimeUnit.SECONDS.sleep(3);
    printThread.stop();
    System.out.println("main stop PrintThread at " + format.format(new Date()));
    TimeUnit.SECONDS.sleep(3);
  }

  static class Runner implements Runnable {
    @Override
    public void run() {
      DateFormat format = new SimpleDateFormat("HH:mm:ss");
      while (true) {
        System.out.println(Thread.currentThread().getName() + " Run at " + format.format(new Date()));
        SleepUtils.second(1);
      }
    }
  }
}

//PrintThread Run at 09:19:38
//PrintThread Run at 09:19:39
//PrintThread Run at 09:19:40
//main suspend PrintThread at 09:19:41
//PrintThread Run at 09:19:44
//main resume PrintThread at 09:19:44
//PrintThread Run at 09:19:45
//PrintThread Run at 09:19:46
//main stop PrintThread at 09:19:47

在执行过程中,PrintThread运行了3秒,随后被暂停,3秒后恢复,最后经过3秒被终止。 通过示例的输出可以看到,suspend()resume()stop()方法完成了线程的暂停、恢复和终止工作,而且非常“人性化”。

stop():即刻停止run()方法中剩余的部分工作,包括在catchfinally语句中的操作,并抛出异常,因此可能导致一些清理性的工作得不到完成,如文件、数据库的关闭等;立即释放该线程所持有的资源(比如锁),导致数据得不到同步处理。 如下所示:

class TestObject {
    private String first = "ja";
    private String second = "va";
  
    public synchronized void print(String first, String second) throws InterruptedException {
        this.first = first;
        Thread.sleep(10000);
        this.second = second;
    }

    public String getFirst() {
        return first;
    }
    public String getSecond() {
        return second;
    }
}

public class OnlyMain {

    public static void main(String[] args) throws InterruptedException {
        TestObject testObject = new TestObject();
        Thread t1 = new Thread(() -> {
            try {
                testObject.print("1", "2");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t1.start();
        Thread.sleep(1000);
        t1.stop();
        System.out.println("first: " + testObject.getFirst() + "  second: " + testObject.getSecond());
    }

}
// first: 1  second: va

从程序的验证结果来看,stop()确实是不安全的:释放该线程持有的所有锁,一般任何进行加锁的代码块都是为了保护数据的一致性,如果在调用stop()后导致该线程所持有的锁突然释放,那么被保护的数据就有可能出现不一致的情况。

resume()&suspend():这两个操作必须要成对出现,否则非常容易发生死锁。suspend()在导致线程暂停(进入睡眠状态、挂起)的同时并不会释放任何资源,其他线程也无法访问被它占用的锁,直到对应的线程执行resume()方法后,被挂起的线程才能继续。 代码如下所示:

class TestObject {

    public synchronized void print() {
        if (Thread.currentThread().getName().equals("A")) {
            System.out.println("A线程独占该资源");
            Thread.currentThread().suspend();
        }
    }
}

public class OnlyMain {

    public static void main(String[] args) throws InterruptedException {
        TestObject testObject = new TestObject();
        Thread t1 = new Thread(() -> testObject.print());
        t1.setName("A");
        t1.start();
        Thread.sleep(1000);

        Thread t2 = new Thread(() -> {
            System.out.println("B已启动,但无法调用print方法");
            testObject.print();
        });
        t2.setName("B");
        t1.start();
    }
}

// A线程独占该资源
// Exception in thread "main" java.lang.IllegalThreadStateException
// at java.lang.Thread.start(Thread.java:708)
// at com.cah.kotlintest.OnlyMain.main(OnlyMain.java:70)

注意:正因为suspend()resume()stop()方法带来的副作用,这些方法才被标注为不建议使用的过期方法,而暂停和恢复操作可以用等待/通知机制来替代。

3.5 安全地终止线程

线程自然终止:run()方法执行完成或者抛出一个未处理的异常导致线程提前结束。

中断状态是线程的一个标识位,而中断操作是一种简便的线程间交互方式,而这种交互方式最适合用来取消或停止任务。除了中断以外,还可以利用一个boolean变量来控制是否需要停止任务并终止该线程。

以下代码中创建了一个线程CountThread,它不断地进行变量累加,而主线程尝试对其进行中断操作和停止操作:

public class Shutdown {

  public static void main(String[] args) throws InterruptedException {
    Runner one = new Runner();
    Thread countThread = new Thread(one, "CountThread");
    countThread.start();
    TimeUnit.SECONDS.sleep(1);
    countThread.interrupt();
    Runner two = new Runner();
    countThread = new Thread(two, "CountTread");
    countThread.start();
    TimeUnit.SECONDS.sleep(1);
    two.cancel();
  }

  private static class Runner implements Runnable {
    private long i;
    private volatile boolean on = true;

    @Override
    public void run() {
      while (on && !Thread.currentThread().isInterrupted()) {
        i++;
      }
      System.out.println("Count i = " + i);
    }

    public void cancel() {
      on = false;
    }
  }
}

//Count i = 557192031
//Count i = 562538988

示例在执行过程中,main线程通过中断操作和cancel()方法均可使CountThread得以终止。 这种通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断地将线程停止,因此这种终止线程的做法显得更加安全和优雅。

线程控制就好比你控制了一个工人为你干活:

  • 挂起:你主动对工人说:你去睡觉吧,等我用的着你的时候会去主动叫你的,然后再干活
  • 睡眠:你主动对工人说:你去睡觉吧,等某时某刻过来接着干活
  • 阻塞:你发现,你的工人没有经过你的允许去睡觉了,但是这不能怪他,因为你没有给他准备干活的工具,他只好去睡觉了。至于工具回来了之后,工人会不会知道,会不会继续干活,时不需要担心的,因为他一旦发现工具回来了,它会自己去干活的。

4 线程优先级

线程的时间片用完了就会发生线程调度,并等待着下次分配。线程分配到的时间片多少也就决定了线程使用处理器资源的多少,而线程优先级就是决定线程需要多处理器资源的线程属性。

Java线程中,通过一个整型成员变量priority来控制优先级,优先级的范围从1~10,在线程构建的时候可以通过setPriority(int)方法来修改优先级,默认优先级是5,优先级高的线程分配时间片的数量要多于优先级低的线程。 设置线程优先级时,针对频繁阻塞(休眠或者I/O操作)的线程需要设置较高优先级,而偏重计算(需要较多CPU时间或者偏运算)的线程则设置较低的优先级,确保处理器不会被独占。在不同的JVM以及操作系统上,线程规划会存在差异, 有些操作系统甚至会忽略对线程优先级的设定,示例代码:

public class MultiThread {

  private static volatile boolean notStart = true;
  private static volatile boolean notEnd = true;

  public static void main(String[] args) throws InterruptedException {
    List<Job> jobs = new ArrayList<>();
    for (int i = 0; i < 10; i++) {
      int priority = i < 5 ? Thread.MIN_PRIORITY : Thread.MAX_PRIORITY;
      Job job = new Job(priority);
      jobs.add(job);
      Thread thread = new Thread(job, "Thread" + i);
      thread.setPriority(priority);
      thread.start();
    }
    notStart = false;
    TimeUnit.SECONDS.sleep(10);
    notEnd = false;
    for (Job job : jobs) {
      System.out.println("Job Priority: " + job.priority + ", Count: " + job.jobCount);
    }
  }

  static class Job implements Runnable {

    private int priority;
    private long jobCount;

    public Job(int priority) {
      this.priority = priority;
    }

    @Override
    public void run() {
      while (notStart) {
        Thread.yield();
      }
      while (notEnd) {
        Thread.yield();
        jobCount++;
      }
    }
  }
}

// Job Priority: 1, Count: 1203153
// Job Priority: 1, Count: 1203077
// Job Priority: 1, Count: 1203070
// Job Priority: 1, Count: 1202243
// Job Priority: 1, Count: 1202562
// Job Priority: 10, Count: 1201401
// Job Priority: 10, Count: 1201750
// Job Priority: 10, Count: 1203001
// Job Priority: 10, Count: 1202480
// Job Priority: 10, Count: 1202363

从输出可以看到线程优先级没有生效,优先级1和优先级10Job计数的结果非常相近,没有明显差距。这表示程序正确性不能依赖线程的优先级高低。

注意:线程优先级不能作为程序正确性的依赖,因为操作系统可以完全不用理会Java线程对于优先级的设定。

参考

https://www.zhihu.com/question/42962803
https://www.jianshu.com/p/7a123f212ca1
https://www.cnblogs.com/mjtabu/p/12694964.html
https://blog.csdn.net/qq_32907195/article/details/107379232?utm_medium=distribute.pc_relevant.none-task-blog-title-3&spm=1001.2101.3001.4242
https://blog.csdn.net/sdfs__sdfsd/article/details/103154221?utm_medium=distribute.pc_relevant.none-task-blog-OPENSEARCH-2.not_use_machine_learn_pai&depth_1-utm_source=distribute.pc_relevant.none-task-blog-OPENSEARCH-2.not_use_machine_learn_pai

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值