【2】线程的状态&操作

2020-04-08,2020-04-09整理

线程创建

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

1
2
3
4
1. 分发处理发送给给JVM信号的线程
2. 调用对象的finalize方法的线程
3. 清除Reference的线程
4. main线程,用户程序的入口
  • 创建多线程的三种方式
    创建线程的三种方式

需要注意的是:
1.由于java不能多继承可以实现多个接口,因此,在创建线程的时候尽量多考虑采用实现接口的形式;
2.实现callable接口,通过submit提交给ExecutorService返回的是异步执行的结果,
通常也可以利用FutureTask(Callable callable)将callable进行包装,然后FutureTask提交给ExecutorsService
可参考一千万个数高效求和

线程状态

线程的状态,在Thread类中枚举类State中的定义,下面是原文翻译
在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
1. NEW
【尚未启动的线程所处的状态。】
例如:线程对象已经创建了,但还没有调用start()方法
2. RUNNABLE
【在Java虚拟机中执行的线程所处的状态。】
当线程有资格运行,但调度程序还没有把它选定为运行线程时线程所处的状态。
当start()方法调用时,线程首先进入可运行状态,随时可能被CPU调度执行。
3. BLOCKED
【在等待监视器锁时被阻塞的线程所处的状态。】
处于阻塞状态的线程正在等待其他线程释放监视器锁,不能进入同步块/方法
4. WAITING
【无限期等待另一个线程执行特定操作的线程所处的状态。】
例如,对一个对象调用Object.wait()的线程正在等待,另一个线程对该对象调用Object.notify()或Object.notifyAll可以使其唤醒为RUNNABLE状态.
调用了Thread.join()的线程正在等待指定的线程终止,所处的状态。
一个线程由于调用下列方法之一而处于等待状态:
+ Object.wait()
+ Thread.join()
+ LockSupport.park()
5. TIMED_WAITING
【在指定等待时间内等待另一个线程执行某个操作的线程所处的状态。】
线程处于定时等待状态
+ Thread.sleep(long)
+ Object.wait(long)
+ Thread.join(long)
+ LockSupport.parkNanos(long)
+ LockSupport.parkUntil(long)
6. TERMINATED
【退出的线程所处的状态。】
线程执行完了或因异常退出了run()方法。

关于调度问题:

1
2
3
4
1. RUNNABLE状态的线程随时可能被CPU调度执行;
2. 运行状态是获取CPU权限正在进行执行;
3. BLOCKED,WAITING,TIMED_WAITING状态是线程因为某种原因放弃CPU使用权,
暂时停止运行,直到线程进入RUNNABLE状态,才有机会转到运行状态。

线程的状态转换

  • 线程创建之后调用start()方法开始运行
  • 调用wait(),join(),LockSupport.lock()进入等待状态
  • 调用wait(long timeout),sleep(long),join(long),LockSupport.parkNanos(),LockSupport.parkUtil()进入超时等待状态
  • 超时等待状态时间到达后,线程会切换到运行状态(就绪状态)
  • 另外等待状态超时等待状态时可以通过Object.notify(),Object.notifyAll()方法使线程转换到运行状态(就绪状态)
  • 当线程出现资源竞争时,即等待获取锁的时候,线程会进入到阻塞状态
  • 当线程获取锁时,线程进入到运行状态(就绪状态)
  • 线程运行结束后,线程进入到终止状态
  • 状态转换可以说是线程的生命周期
  • 另外需要注意的是
    • 当线程进入到synchronized方法或者synchronized代码块时,线程切换到的是阻塞状态
    • 而使用java.util.concurrent.locks下lock进行加锁的时候线程切换的是等待或者超时等待状态,因为lock会调用LockSupport的方法。

用一个表格将上面六种状态进行一个总结归纳。
在这里插入图片描述

线程操作

除了新建一个线程外,线程在生命周期内还有需要基本操作,而这些操作会成为线程间一种通信方式
例如使用中断(interrupt)方式通知实现线程间的交互等等,下面就将具体说说这些操作。

run和start

start会开启一个新线程,run不会

1
2
3
4
5
对象.start();   会先执行后面的内容,再执行对象中的run方法
对象.run(); 会先执行对象中的run方法,再执行后面的内容
总结
start是开启一个新的线程,在新线程中执行run方法
run是在主线程中执行该方法,和调普通方法一样

interrupt

线程中断
在这里插入图片描述

  • 一个案例
    在这里插入图片描述

join

  • 一个小案例来分析源码
    在这里插入图片描述

  • 下面再用一个例子来说明join方法的控制线程执行顺序
    在这里插入图片描述

sleep

可以写在同步代码块中,也可以写在非同步代码块中。
在这里插入图片描述

wait和notify

在这里插入图片描述

sleep和wait

相同点

1
2
3
1. 都是把运行机会交给其它线程。
2. 任何线程在sleep或wait期间被interrupt都会抛出InterruptedException
3. 都是native方法,底层c++控制

不同点

1
2
3
4
5
6
7
8
1. 所在类不同
wait()是Object类中的实例方法;而sleep()是Thread类中的静态方法
2. 使用的位置不同
wait()方法必须要在synchronized中调用,也就是必须已经获得对象锁,而sleep()方法没有这个限制可以在任何地方种使用。
3. 关键点是对锁的保持不同
wait()方法会释放占有的对象锁,使得该线程进入等待状态。而sleep()方法只是会让出CPU并不会释放掉对象锁
4. 恢复方式不同
wait依靠notify或者notifyAll、或到达指定时间来进入就绪状态;而sleep()则是到达指定的时间后进入就绪状态。

yield

1
2
3
4
5
6
7
public static native void yield();这是一个静态方法,一旦执行,它会是当前线程让出CPU
但是,需要注意的是,让出的CPU并不是代表当前线程不再运行了,如果在下一次竞争中,又获得了CPU时间片当前线程依然会继续运行。
另外,让出的时间片只会分配**给当前线程相同优先级**的线程。

在Java程序中,通过一个整型成员变量Priority来控制优先级,优先级的范围从1~10
在构建线程的时候可以通过**setPriority(int)**方法进行设置,默认优先级为5
优先级高的先执行。

sleep和yield

  • 相同点都是当前线程会交出处理器资源
  • 不同点sleep()交出来的时间片其他线程都可以去竞争,也就是说都有机会获得当前线程让出的时间片。
    而yield()方法只允许与当前线程具有相同优先级的线程能够获得释放出来的CPU时间片。

守护线程

守护线程是一种特殊的线程,就和它的名字一样,它是系统的守护者,在后台默默地守护一些系统服务,比如垃圾回收线程,JIT线程就可以理解守护线程。
与之对应的就是用户线程,用户线程就可以认为是系统的工作线程,它会完成整个系统的业务操作。
用户线程完全结束后就意味着整个系统的业务任务全部结束了,因此系统就没有对象需要守护的了,守护线程自然而然就会退。
当一个Java应用,只有守护线程的时候,虚拟机就会自然退出。

  • Daemon线程的示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    public class DaemonDemo {
    public static void main(String[] args) {
    Thread daemonThread = new Thread(new Runnable() {
    @Override
    public void run() {
    while (true) {
    try {
    System.out.println("i am alive");
    Thread.sleep(500);
    } catch (InterruptedException e) {
    e.printStackTrace();
    } finally {
    System.out.println("finally block");
    }
    }
    }
    });
    daemonThread.setDaemon(true);
    daemonThread.start();

    try {
    //确保main线程结束前能给daemonThread能够分到时间片
    Thread.sleep(800);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }
    /*
    输出结果
    i am alive
    finally block
    i am alive
    */
    上面的例子中daemodThread run方法中是一个while死循环,会一直打印,但是当main线程结束后守护线程daemonThread就会退出所以不会出现死循环的情况。
    main线程先睡眠800ms保证daemonThread能够拥有一次时间片的机会,也就是说可以正常执行一次打印“i am alive”操作和一次finally块中”finally block”操作。
    紧接着main 线程结束后,daemonThread退出,这个时候只打印了”i am alive”并没有打印finnal块中的。
    因此,这里需要注意的是守护线程在退出的时候并不会执行finnaly块中的代码,所以将释放资源等操作不要放在finnaly块中执行,这种操作是不安全的

线程可以通过setDaemon(true)的方法将线程设置为守护线程。
并且需要注意的是设置守护线程要先于start()方法,否则会报如下异常,但是该线程还是会执行,只不过会当做正常的用户线程执行。

1
2
3
Exception in thread "main" java.lang.IllegalThreadStateException
at java.lang.Thread.setDaemon(Thread.java:1365)
at learn.DaemonDemo.main(DaemonDemo.java:19)