小小白祈祷中...

前言

上一章咱介绍了线程同步,了解了解决线程安全的基本思想----“队列与锁”。

在前几章的介绍中,我们时不时地会使用到sleep()这个方法,知道它可以通过使线程休眠来扩大问题发生的可能性,使开发者能够迅速定位到bug的位置。它是Thread类中一个比较重要的静态方法,那么本章就来介绍一下Thread类中一些常用的方法。

在介绍Thread类里面的方法之前,我们来回顾一下线程的五大状态:

img请注意体会,当调用下面的这些方法时,线程的状态是如何变化的

sleep()

sleep()方法有两种重载:

  1. public static native void sleep(long millis)
  2. public static native void sleep(long millis , int nanos)

我们之前使用的都是第一种,它的参数单位使用的是毫秒(ms),表示让当前线程休眠多少毫秒。

如果你想更为精确,可以采用第二种,它第二个参数为第一个参数的基础上附加多少纳秒(取值在0~999999之间),源码如图:

img对于sleep(),我们需要了解一下内容:

  1. 当线程调用Thread.sleep()方法时,会立即使当前线程进入指定时间的休眠,变成阻塞状态,时间一过,该线程会立即进入可运行态(注意不是运行态),之后的运行看CPU调度

  2. 在实现多线程的各种方式中,除了继承Thread类的线程类可以直接调用sleep()方法,其它方式都需要通过Thread.sleep()方式来调用。

  3. sleep()的作用:

    1). 通过使线程休眠来扩大问题发生的可能性,使开发者能够迅速定位到bug的位置

​ 2). 适用于程序要求频率慢的场景。比如在做网页爬虫搜集资料时,部分网页接口的请求频率有一定限制。如果调用接口频率太快,容易被错误识别导致封号。因此咱可以用sleep()方法随机休眠来保证调用安全。

  1. 调用sleep()方法不会释放对象的锁

yield()

yield意为“礼让”,该方法作用是让当前线程放弃CPU的资源,将它让给相同优先级的其他线程。调用yield方法后,当前线程会立即进入可运行态

我们来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* @author sixibiheye
* @date 2021/8/29
* @apiNote yield()方法用法示例
*/

public class Yield implements Runnable{
//测试礼让线程
//注意:礼让不一定成功,礼让会使当前线程进入可运行态,执行谁,看CPU调度
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"线程开始执行~");
//礼让
Thread.yield();
System.out.println(Thread.currentThread().getName()+"线程结束执行~");
}
public static void main(String[] args) {
Yield myyield = new Yield();
new Thread(myyield,"A").start();
new Thread(myyield,"B").start();
}
}

上述程序的输出结果为:

img上述结果表明,A线程确实成功地将CPU使用权“礼让”给了B线程。 但是由于A线程放弃的时间不确定,也许刚刚放弃CPU的使用权,就又获得了CPU的使用权。所以虽然有时发出了“礼让”请求,也会出现“礼让失败”的情况:

img所以使用yield()的实际意义不大,了解即可。

setPriority() & getPriority()

java中,每个线程都有一个优先级,当CPU调度新的线程时,会优先调度优先级高的线程。优先级设置为1~10的整数,数字越大,优先级越高,被CPU调度执行的机会越大。此外Java中也提供了三个常量来代表三个常用的优先级:

  • 最低优先级 1 :MIN_PRIORITY
  • 普通优先级 5 :NORM_PRIORITY
  • 最高优先级 10 : MAX_PRIORITY

请注意,Java中默认的线程优先级是父线程的优先级(不是普通优先级NORM_PRIORITY),虽然主线程的优先级默认是普通优先级NORM_PRIORITY

可以使用Thread类中的 setPriority() 和 getPriority() 方法来设置和获取某线程的优先级。我们来看一个简单的例子:

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
/**
* @author sixibiheye
* @date 2021/9/1
* @apiNote 设定线程优先级
*/
public class setPriority extends Thread{

@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "线程启动~,优先级为:" + Thread.currentThread().getPriority());
}
public static void main(String[] args) {

Thread threadA = new setPriority();
Thread threadB = new setPriority();
Thread threadC = new setPriority();

threadA.setPriority(MAX_PRIORITY);
threadB.setPriority(NORM_PRIORITY);
threadC.setPriority(MIN_PRIORITY);

threadA.start();
threadB.start();
threadC.start();
}
}

运行结果:

img可以看到,优先级越高的线程会优先被CPU调度执行。但是本质上讲,这与操作系统以及jvm的版本有关,有可能即使设置了线程优先级也不会产生任何作用。

比如上面那个程序,多运行几次,你会发现,设置优先级不一定能输出理想的结果:

img如上图,甚至恰恰相反的结果都有可能出现。因此,在实际应用中,优先级使用的意义也不大。

wait() & notify() & notifyAll()

wait() & notify()是线程通信里两个非常常用的方法,它们是Object类中的方法,却与线程通信息息相关。关于线程通信,前面学过的synchronized可以主动协调线程间的执行关系,但是,两个线程间,有时还存在被动的通信关系。

比如在一个餐馆里,服务员和厨师的关系便是一个典型的被动通信关系。在厨师没有做完菜肴之前,服务员只能处于等待状态,只有当厨师做完菜肴并且给服务员发出一个通知-----菜做完毕,服务员收到“通知”后才能开始上菜。

这种“等待与通知”的被动通信关系广泛存在于生产生活之中。

映射到线程里就是,如果一个线程需要使用到另外一个线程的执行结果,那么它就必须处于等待状态,直到另一个线程执行完毕并给它发出通知,它收到通知之后,才开始执行

这种“等待与通知”的被动通信关系在许多地方也叫生产者和消费者模式。我们来看一段代码:

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
/**
* @author sixibiheye
* @date 2021/9/1
* @apiNote wait()的使用
*/
public class Wait_Notify {
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
Thread thread1 = new ThreadDemo1(obj);
thread1.start();
}
}

class ThreadDemo1 extends Thread{
private Object obj = new Object();
public ThreadDemo1(Object obj){
this.obj = obj;
}
@Override
public void run() {
synchronized (obj){
System.out.println("A线程等待前的一行...");
//让A线程等待
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("A线程等待后的一行...");
}
}
}

运行结果:

img注意左边的红色正方形按钮,说明程序还未终止,一直处于等待状态

java中,对于上述的“等待和通知”的被动通信关系,等待对应于wait()通知对应于notify()和notifyAll()

关于wait(),有以下几个要点需要注意:

  • wait()方法是Object类中的方法,其作用是使当前线程进入wait状态(将当前线程放入该对象的“预执行队列”中,等待状态可以理解为阻塞状态),并且在wait()方法所在代码行处停止执行,直到被 notify()notifyAll() 通知或被中断为止。
  • 在调用wait()方法前,当前线程必须获得该对象的锁,也就是说,只能在同步方法或者同步代码块中调用wait()方法
  • 在调用wait()方法后,当前线程会立即释放该对象的锁,使得处于wait状态的线程能获得这把锁。

关于notify()和notifyAll(),也有几个要点需要注意:

  • notify()方法是用来通知处于wait状态的线程-----可以取得当前对象的锁了。
  • 调用notify()方法之后,如果处于wait状态的线程不止一个,则CPU会随机调度一个线程向其发出通知------可以取得当前对象的锁了。
  • notifyAll()则是通知所有处于wait状态的线程------你们可以开始竞争当前对象的锁了。
  • 与调用wait()方法不同,在调用notify()notifyAll()方法后,当前线程不会立即释放该对象的锁,而是要等当前线程执行完synchronized代码块后,才会释放锁,这时处于wait状态的线程才能获取到锁。

请看下面的代码,从代码里理解上述内容:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* @author sixibiheye
* @date 2021/9/1
* @apiNote wait()/notify()的使用
*/
public class Wait_Notify {
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
Thread thread1 = new ThreadDemo1(obj);
thread1.start();
Thread.sleep(1000);
Thread thread2 = new ThreadDemo2(obj);
thread2.start();
}
}

class ThreadDemo1 extends Thread{
private Object obj = new Object();
public ThreadDemo1(Object obj){
this.obj = obj;
}
@Override
public void run() {
synchronized (obj){
System.out.println("A线程等待前的一行...");
//让A线程等待
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("A线程等待后的一行...");
}
}
}

class ThreadDemo2 extends Thread{
private Object obj = new Object();
public ThreadDemo2(Object obj){
this.obj = obj;
}
@Override
public void run() {
synchronized (obj){
System.out.println("B线程通知前的一行...");
//B线程唤醒其它线程
obj.notify();
System.out.println("B线程通知后的一行...");
}
}
}

运行结果:

img这个运行结果完美地诠释了上面的几个要点,请小伙伴们细细体会。

此外,wait()方法可以有一个参数:wait(long millis)。比如wait(1000),它的含义是,将对象的锁释放,同时使自己进入1000ms的阻塞状态,1000ms过后如果锁没有被其他线程占用,则再次得到锁,然后wait()方法结束,执行后面的代码。如果1000ms内锁被其他线程占用,则等待其他线程释放锁,自己进入可运行态

与无参的wait()方法不同的是,有参的wait()方法并不需要其他线程执行 notify()或者 notifyAll() 来唤醒,只要超过了设定时间,线程会自动解除阻塞状态

join()

join()方法出现的一个原因是,当主线程创建并启动子线程后,如果子线程中要进行大量的耗时运算,主线程往往早于子线程结束。如果主线程想等待子线程执行完之后再结束,比如子线程处理一个数据,主线程要获得这个处理后的数据,就必须让子线程执行完毕之后,再结束主线程。

不知小伙伴们发现了没有,这里面也蕴含了“等待和通知”的思想,那就是,在得到子线程“通知”之前,主线程只能处于“等待”状态。因此咱可以用wait()方法来实现。不过,这个不需要我们操心了,Thread 类里提供了join()方法来实现这个功能。

查看join()方法的源码,它的方法体中调用了wait()方法

img我们通过一个简单的例子来学习一下join()的使用:

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
35
36
/**
* @author sixibiheye
* @date 2021/8/29
* @apiNote join()方法用法示例
*/

public class Join extends Thread{
private int count;
//getCount()用于主线程获取子线程的数据
public int getCount() {
return count;
}
public Join(int count){
this.count = count;
}
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
count++;
}

System.out.println("count的值为:" + count);

try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("main线程开始启动~");
Join thread = new Join(0);
thread.start();
System.out.println("main线程执行完毕~,count的值为:" + thread.getCount());
}
}

上述代码中,子线程要对变量count进行50000次累加操作,显然主线程(main线程)结束的更快,当主线程结束时子线程还没处理完count,主线程调用count的值就会出现意外:

img如上图,主线程输出的count值明显不是预料的结果50000。 这时就需要让主线程等待子线程执行完毕之后,再结束主线程。将上述的main()方法修改如下(加上join()方法):

1
2
3
4
5
6
7
8
public static void main(String[] args) throws InterruptedException {
System.out.println("main线程开始启动~");
Join thread = new Join(0);
thread.start();
//测试join方法,线程执行join()后,其他线程处于阻塞状态
thread.join();
System.out.println("main线程执行完毕~,count的值为:" + thread.getCount());
}

运行结果:

img这样便达到了预想的结果。

其它

Thread类中还有一些不常用或比较简单的方法,现列举如下:

  1. Thread Thread.currentThread() :获得当前线程的引用。
  2. int Thread.activeCount():当前线程所在线程组中活动线程的数目。
  3. int enumerate(Thread[] tarray) :将当前线程的线程组及其子组中的每一个活动线程复制到指定的数组中。
  4. boolean holdsLock(Object obj) :当且仅当当前线程在指定的对象上保持有锁时,才返回 true。
  5. boolean interrupted() :测试当前线程是否已经中断。
  6. void checkAccess() :判定当前运行的线程是否有权修改该线程。
  7. getContextClassLoader() :返回该线程的上下文 ClassLoader。
  8. long getId() :返回该线程的标识符。
  9. String getName() :返回该线程的名称。
  10. Thread.State getState() :返回该线程的状态。
  11. ThreadGroup getThreadGroup() :返回该线程所属的线程组。
  12. void interrupt() :中断线程。
  13. boolean isAlive() :测试线程是否处于活动状态。
  14. boolean isDaemon() :测试该线程是否为守护线程。
  15. boolean isInterrupted():测试线程是否已经中断。
  16. void run() :线程启动后执行的方法。
  17. void setContextClassLoader(ClassLoader cl) :设置该线程的上下文 ClassLoader。
  18. void setDaemon(boolean on) :将该线程标记为守护线程或用户线程。
  19. void start():使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
  20. String toString():返回该线程的字符串表示形式,包括线程名称、优先级和线程组。

sleep() 和 wait() 方法的区别

这个是在多线程中经常被面试的问题。以下可供参考:

  • sleep()Thread类中的一个静态方法,作用于当前线程。而wait()Object类中的方法,任何实例对象均可以调用,作用于对象本身。
  • sleep()不会释放锁,也不会占用锁。而wait()会释放锁,而且调用它的前提是当前线程持有锁。
  • sleep()方法可以在任何合法的地方调用。而wait()只能在同步方法或同步代码块中调用。
  • 对于含参的sleep(),不管设定时间内有没有其他线程占用锁,设定时间过后都会使当前线程进入可运行态。而含参的wait()在设定时间超过之后,如果有线程占用了锁,则原线程立即进入可运行态;如果没有线程占用锁,则原线程继续执行(运行态)。

本章的内容比较多,一时消化不了,需要慢慢理解并且多敲代码加以运用。

本文作者:LuoYing @ 小小白的笔记屋

本文链接:https://luoying.netlify.app/2021/09/02/99n7yacb/

本文标题:Java多线程详解(深究Thread类)

本文版权:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!