Java并发编程(二)—— 线程的状态

1 线程的状态

线程的六个状态:创建、就绪、运行、等待/超时等待、阻塞以及死亡:

  • 创建状态(NEW):生成线程对象之后,调用start()方法之前,线程处于创建状态;
  • 就绪状态(READY):线程对象调用了start()方法之后,就进入了就绪状态,但是还要和其它线程竞争 CPU,只有获得了 CPU 才可以执行任务。另外,在线程运行之后,从等待或者睡眠中被激活之后,也会处于就绪状态;
  • 运行状态(RUNNABLE):处于就绪状态的线程得到 CPU 时间片之后,就会进入运行状态,开始执行 run() 方法当中的代码; 运行状态和就绪状态可以通过系统调度完成状态改变,运行状态的代码可以调用yield()方法,转为就绪状态。
  • 等待状态(WAITING):调用 wait() 方法,线程进入等待状态。一个处于等待状态的线程正在等待另一个线程执行某个特定的动作。用 notify()/notifyAll() 方法会唤醒线程,使线程从等待状态进入就绪状态或者运行状态。等待状态是没有时间戳的,如果没有被唤醒,会一直等待下去。
  • 超时等待状态(主动进入)(TIMED_WAITING):可以限定等待时间 — wait(time),如果时间到了,即便是没有被唤醒,也会从等待状态恢复到就绪状态或运行状态。调用 sleep(time) 会进入计时等待状态(等待的线程主动释放 CPU,wait释放锁,sleep不释放锁)
  • 阻塞状态(被动进入)(BLOCKED):正在运行的线程因为某些原因被剥夺了 CPU,比如说,该线程试图得到一个锁,而该锁正在被其它线程持有,没能拿到锁,就会进入阻塞状态,重新获取到锁,就会进入就绪状态,可以去和其它线程竞争 CPU;(阻塞状态的线程会被剥夺 CPU,不会释放锁,线程仍然处于内存中)
  • 死亡状态(TERMINATED):如果一个线程的run()方法执行结束或者调用stop()方法后,该线程就会死亡。对于已经死亡的线程,就无法再使用start()方法令其进入就绪;
// Thread.java
public enum State {
    NEW, // 线程已创建,但还没有start
    RUNNABLE, // 处于可运行状态,一切就绪
    BLOCKED, // 处于阻塞状态,比如等待某个锁的释放
    WAITING, // 处于等待状态
    TIMED_WAITING, // 超时等待状态
    TERMINATED; // 终止运行
}

Java线程状态变迁如图所示:

线程的生命周期

从图中可以看到,线程创建之后,调用start()方法开始运行。当线程执行wait()方法之 后,线程进入等待状态。进入等待状态的线程需要依靠其它线程的通知才能够返回到运行状态,而超时等待状态相当于在等待状态的基础上增加了超时限制,也就是超时时间到达时将会返回到运行状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到阻塞状态。线程在执行完Runnable.run()的方法之后将会进入到终止状态。

注意:Java将操作系统中的运行和就绪两个状态合并称为运行状态。阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态,但是阻塞在java.concurrent包中Lock接口的线程状态却是等待状态,因为java.concurrent包中Lock接口对于阻塞的实现均使用了LockSupport类中的相关方法。

挂起(等待,阻塞)进程在操作系统中可以定义为暂时被淘汰出内存的进程,机器的资源是有限的,在资源不足的情况下,操作系统对在内存中的程序进行合理的安排,其中有的进程被暂时调离出内存,当条件允许的时候,会被操作系统再次调回内存,重新进入等待被执行的状态即就绪态。

2 Android 中的线程类 Thread

public class Thread implements Runnable {
  	...
}

public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

通过以上代码可以知道 Thread 继承了 Runnable,侧面说明线程是”可执行代码“。 Runnable 是一个接口类,提供了唯一的方法 run()

2.1 相关属性
public class Thread implements Runnable {
    /*
     * Thread ID
     */
    private final long tid;
  
  	/* For generating thread ID 全局变量,默认值是 0 */
    private static long threadSeqNumber;
  
  	/*
  	 * 线程名
  	 */
  	private volatile String name;
  
  	public Thread(Runnable target) {
        this(null, target, "Thread-" + nextThreadNum(), 0); // 1
    }
  
  	private Thread(ThreadGroup g, Runnable target, String name,
                   long stackSize, AccessControlContext acc,
                   boolean inheritThreadLocals) {
      	if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name; // 2
       	...
        /* Set thread ID */
        this.tid = nextThreadID(); // 3
    }
  
  	private static synchronized long nextThreadID() {
        return ++threadSeqNumber;
    }
  
  	public long getId() {
        return tid;
    }
  
}

通过注释 1 处可以知道,在创建 Thread 实例时,如果未显示设置线程名称,则默认线程名称为 Thread- 加上一个递增的数值 threadSeqNumberthreadSeqNumber 在每次调用 new Thread(...) 时是自动递增的,其作用只是对于未显示配置线程名的线程在 name 上做一个简单的区分。注意,这里并不是真正的线程 ID,在 Logcat 中打印的 getId() 也不是这个 ID,而是 Dalvik 中的线程 ID。

public class Thread implements Runnable {		
    	private int priority;

      /**
       * The minimum priority that a thread can have. 最低优先级
       */
      public static final int MIN_PRIORITY = 1;

     /**
       * The default priority that is assigned to a thread. 默认优先级
       */
      public static final int NORM_PRIORITY = 5;

      /**
       * The maximum priority that a thread can have. 最高优先级
       */
      public static final int MAX_PRIORITY = 10;
  
  		public String toString() {
        ThreadGroup group = getThreadGroup();
        if (group != null) {
            return "Thread[" + getName() + "," + getPriority() + "," +
                           group.getName() + "]";
        } else {
            return "Thread[" + getName() + "," + getPriority() + "," +
                            "" + "]";
        }
    }
}

线程调度器会根据 priority 来决定优先运行哪个线程(不保证)。Thread 中定义了三个优先级常量 1510,默认是 5 可以通过以下代码进行验证:

public class Test {
    public static void main(String[] args) {
        Thread mt = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("running");
            }
        });
        mt.start();
        System.out.println(mt);
        System.out.println("thread id: " + mt.getId());
        System.out.println("thread name: " + mt.getName());
    }

}

// running
// Thread[Thread-0,5,main]
// thread id: 13
// thread name: Thread-0

守护线程还是用户线程:

public class Thread implements Runnable {
  	
  	/**
  	 * Whether or not the thread is a daemon thread.
  	 * 当前线程是否是守护线程
  	 */
  	private boolean daemon = false; 
  
  	/**
  	 * The requested stack size for this thread, or 0 if the creator did not specify a stack size.
  	 * It is up to the VM to do whatever it likes with this number; some VMS will ignore it.
  	 * 线程栈大小,默认是 0。即使使用默认的线程栈大小(由 Dalvik 中的全局变量 gDvm.stackDize 决定)
  	 */
  	private long stackSize;
  	
   	/**
   	 * What will be run.
   	 * 一个 Runnable 对象,Thread.run() 方法中会调用 target.run() 方法,这是线程真正处理事务的地方
   	 */
  	private Runnable target;
  
  	public final void setDaemon(boolean on) {
        checkAccess();
        if (isAlive()) {
            throw new IllegalThreadStateException();
        }
        daemon = on;
    }
}

可以通过 setDaemon(true) 把线程设置为守护线程。 守护线程通常用于执行不重要的工作,比如监控其他线程的运行情况。GC 线程就是守护线程。setDaemon(...) 要在线程启动前设置,否则 JVM 会抛出非法线程状态。

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

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

    static class DaemonRunnable implements Runnable {
        @Override
        public void run() {
            try {
                Thread.sleep(10 * 1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                System.out.println("daemon thread finally run");
            }
        }
    }
}

运行程序,在终端或者命令提示符上没有任何输出。主线程在启动了线程 thread 后,随着 main(...) 方法执行完毕,Daemon 线程也需要终止,因此 finally 块中的代码并没有执行。因此,在构建 Daemon 线程时,不能依靠 finally 块中的内容来确保关闭或清理资源。

总结:

  • id:线程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
2.2 常用方法

Thread 的常用方法

2.2.1 thread.start()方法和thread.run()方法

thread.start() 方法可以使线程从创建状态转为就绪状态,如果可以获得 CPU,则进入运行状态。thread.run() 方法中是线程启动之后需要执行的代码。

public class Thread implements Runnable {

  	public synchronized void start() {
        if (started)
            throw new IllegalThreadStateException();

        group.add(this);

        started = false;
        try {
            nativeCreate(this, stackSize, daemon);
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
             
            }
        }
    }


    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
}
2.2.2 thread.interrupt()Thread.interrupted()方法和Thread.isInterrupted()方法的区别?

thread.interrupt() 的作用是中断此线程(此线程不一定是当前线程,而是指调用该方法的 Thread 实例所代表的线程),但实际上只是给线程设置一个中断标志,线程仍会继续运行。

Thread.interrupted() 的作用是测试当前线程是否被中断(检查中断标志),返回一个 boolean 类型的值并清除中断状态,当再次调用此方法时中断状态已经被清除,将返回一个 false

Thread.isInterrupted() 的作用是只测试此线程是否被中断 ,不清除中断状态。

2.2.3 为什么Thread.stop()Thread.suspend()方法不推荐使用?

当调用 Thread.stop() 方法时会发生两件事:

  • 即刻停止 run() 方法中剩余的全部工作,包括在 catchfinally 语句,并抛出 ThreadDeath 异常(通常情况下此异常不需要显示的捕获),因此可能会导致一些清理性的工作的得不到完成,如文件,数据库等的关闭;
  • 会立即释放该线程所持有的所有的锁,导致数据得不到同步的处理,出现数据不一致的问题;
class Test {

    public static void main(String[] args) throws Exception {
        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());
    }

}

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(10_000);
        this.second = second;
    }

    public String getFirst() {
        return first;
    }

    public String getSecond() {
        return second;
    }
}

// first: 1 second: va

Thread.suspend()Thread.resume()必须要成对出现,否则非常容易发生死锁。这两个操作就好比播放器的暂停和恢复。

不推荐使用 suspend() 去挂起线程的原因,是因为 suspend() 在导致线程暂停的同时,并不会去释放任何锁资源,其它线程就无法访问被它占用的锁,直到对应的线程执行 resume() 方法后,被挂起的线程才能继续,从而其它被阻塞在这个锁的线程才可以继续执行。 但是,如果Thread.resume()操作出现在Thread.suspend()之前执行,那么线程将一直处于挂起状态,同时一直占用锁,这就产生了死锁。

class Test {

    public static void main(String[] args) throws Exception {
        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");
        t2.start();
    }

}

class TestObject {

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

}

// A线程独占资源了
// B线程已启动,但无法进行print方法
2.2.4 如何停止一个正在运行的线程?

使用 thread.interrupt() 产生打断标志位来停止线程。

方案一:捕捉打断标记并且直接 return

class Test {

    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
        try {
            Thread.sleep(200);
            thread.interrupt();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    static class MyThread extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 50_000; i++) {
                if (this.isInterrupted()) {
                    System.out.println("线程终止,停止for循环。");
                    return;
                }
                System.out.println("i = " + (i + 1));
            }
        }
    }

}

方案二:捕捉打断标记,并且抛出异常终止程序

class Test {

    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
        try {
            Thread.sleep(200);
            thread.interrupt();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    static class MyThread extends Thread {
        @Override
        public void run() {
            try {
                for (int i = 0; i < 100_000; i++) {
                    if (this.isInterrupted()) {
                        System.out.println("线程终止,停止for循环。");
                        throw new InterruptedException();
                    }
                    System.out.println("i = " + (i + 1));
                }
            } catch (InterruptedException e) {
                System.out.println("MyThread抛出InterruptedException。");
                e.printStackTrace();
            }
        }
    }

}

方案三:当线程处于sleepparkjoinwait的时候需要在catch块处理异常时自行设置打断标记

当线程处于正常状态的时候,打断会产生打断的标记位,但是在线程处于sleepparkjoinwait等状态时,被打断将不会产生标记位,可以使用trycatch来处理该情况,当程序被打断时,在程序catch并处理打断异常时候可以自己添加打断标记,从而设置打断标记。(两阶段终止模式):

class Test {

    public static void main(String[] args) throws InterruptedException {
        TwoParseTermination twoParseTermination = new TwoParseTermination();
        twoParseTermination.start();
        Thread.sleep(3000);
        twoParseTermination.stop();
    }
}

class TwoParseTermination {
    Thread thread;

    public void start() {
        thread = new Thread(() -> {
            while (true) {
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println("线程结束,正在收尾...");
                    break;
                }
                try {
                    Thread.sleep(500);
                    System.out.println("正在执行监控的功能");
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    e.printStackTrace();
                }
            }
        });
        thread.start();
    }

    public void stop() {
        thread.interrupt();
    }
}
2.2.5 Thread.sleep(long millis)Thread.yield()的区别?

状态的区别:

  • 调用 Thread.sleep(long millis) 会让当前线程从 RUNNABLE 进入 TIMED_WAITING 状态;
  • 调用Thread.yield()会让当前线程放弃 CPU,调度执行其它线程;

调度的区别:

  • 调用 Thread.sleep(long millis) 之后,该线程将进入超时等待状态,分不到CPU的时间片;
  • 调用 Thread.yield() 之后,该线程会让出 CPU 的使用权,但是任务调度器仍然可能分配给该线程时间片,从宏观上只是该线程被分配 CPU 的概率变低了;
public class Thread implements Runnable {
  
  public static void sleep(long millis) throws InterruptedException {
    sleep(millis, 0);
  }
  
  public static native void yield();
}
2.2.6 为什么 Thread.sleep(long millis)Thread.yield() 是类方法,而 thread.wait()thread.join() 是成员方法?

Thread.sleep(long millis)Thread.yield() 方法在当前正在执行的线程上运行,在其它线程上调用这些方法是没有意义的。也就是说只有本线程才能执行休眠操作,如果 Thread.sleep(long millis) 是成员方法,其它线程可以获得该线程的实例化对象,从而让当前线程强制休眠(释放 CPU 的资源),这样会带来不可预估的后果。因此,Thread.sleep(long millis)Thread.yield() 不可以被其他线程调用,只能被自身线程调用,也是就是必须是自愿才可以。

对于 object.wait()thread.join()object.wait() 是本线程获取锁之后,锁对象调用的 wait() 方法,实际上还是在本线程中使用,thread.join() 在其它线程中调用,因为其本身设计的意义就是其它线程等待该线程完成。

2.2.7 有三个线程T1T2T3,如何保证顺序执行?

确保一个线程启动之后等待他执行完再进行下一个:

t1.start();
t1.join();
t2.start();
t2.join();
t3.start();
t3.join();
2.2.8 Java 中守护线程和本地线程区别?

Java 中的线程分为两种:守护线程(Daemon)和用户线程(User)。任何线程都可以设置为守护线程和用户线程,通过方法Thread.setDaemon(boolean on)true 则把该线程设置为守护线程,默认用户线程。Thread.setDaemon() 必须在 Thread.start() 之前调用,否则运行时会抛出异常。

守护线程的特点是,如果一个进程中的其它用户线程全部运行完毕,那么这时守护线程也会自动结束,比如JVM的垃圾回收线程是一个守护线程,当所有线程已经撤离,不再产生垃圾,守护线程自然就没有任务了。

2.2.9 Thread.sleep(long millis)object.wait() 有什么区别?

Thread.sleep(long millis)Thread类的静态方法,在线程使用Thread.sleep(long millis)方法之后会让出 CPU 的资源,但是不会释放锁资源。

object.wait() 方法是 Object 的方法,只能在同步代码块中被调用,某个线程使用锁对象的 wait 方法,会释放掉该线程的锁资源,同时还有 CPU 使用权,让其它线程去竞争。

Thread.sleep(0) 有什么作用?触发操作系统立刻重新进行一次 CPU 竞争,竞争的结果可能是当前线程仍然获得 CPU 控制权,也可能是别的线程获得 CPU 控制权。

通过以下代码运行时的线程信息,更加深入地理解线程状态:

public class ThreadState {

  public static void main(String[] args) {
    new Thread(new TimeWaiting(), "TimeWaitingThread").start();
    new Thread(new Waiting(), "WaitingThread").start();
    // 使用两个Blocked线程,一个获取锁成功,另一个被阻塞
    new Thread(new Blocked(), "BlockedThread-1").start();
    new Thread(new Blocked(), "BlockedThread-2").start();
  }

  // 该线程不断地进行睡眠
  static class TimeWaiting implements Runnable {
    @Override
    public void run() {
      while (true) {
        SleepUtils.second(100);
      }
    }
  }

  // 该线程在Waiting.class实例上等待
  static class Waiting implements Runnable {
    @Override
    public void run() {
      while (true) {
        synchronized (Waiting.class) {
          try {
            Waiting.class.wait();
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
        }
      }
    }
  }

  // 该线程在Blocked.class实例上加锁后,不会释放该锁
  static class Blocked implements Runnable {
    @Override
    public void run() {
      synchronized (Blocked.class) {
        while (true) {
          SleepUtils.second(100);
        }
      }
    }
  }
}

class SleepUtils {
  public static final void second(long seconds) {
    try {
      TimeUnit.SECONDS.sleep(seconds);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}

参考

(进程/线程)阻塞、挂起、睡眠的区别
挂起和阻塞区别以及sleep和wait的区别
理解线程的挂起、阻塞 和 sleep(转载)
java 多线程 面试题整理(更新…)
线程常用的方法
线程常见的几种方法
#java 常见线程方法
进程的执行和挂起
线程的状态是怎么进行划分的?
Thread中stop(),suspend(),resume()为什么不安全
Thread类中interrupt()、interrupted()和isInterrupted()方法详解
Android虚拟机线程启动过程解析, 获取Java线程真实线程Id的方式

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值