前言
上一章咱介绍了线程同步,了解了解决线程安全的基本思想----“队列与锁
”。
在前几章的介绍中,我们时不时地会使用到sleep()
这个方法,知道它可以通过使线程休眠来扩大问题发生的可能性,使开发者能够迅速定位到bug的位置。它是Thread类
中一个比较重要的静态方法,那么本章就来介绍一下Thread类
中一些常用的方法。
在介绍Thread类
里面的方法之前,我们来回顾一下线程的五大状态:
请注意体会,当调用下面的这些方法时,
线程的状态是如何变化的
。
sleep()
sleep()方法有两种重载:
- public static native void sleep(long millis)
- public static native void sleep(long millis , int nanos)
我们之前使用的都是第一种,它的参数单位使用的是毫秒(ms)
,表示让当前线程休眠多少毫秒。
如果你想更为精确,可以采用第二种,它第二个参数为第一个参数的基础上附加多少纳秒(取值在0~999999之间),源码如图:
对于
sleep()
,我们需要了解一下内容:
当线程调用
Thread.sleep()
方法时,会立即使当前线程进入指定时间的休眠,变成阻塞状态
,时间一过,该线程会立即进入可运行态
(注意不是运行态),之后的运行看CPU调度
。在实现多线程的各种方式中,除了继承
Thread类
的线程类可以直接调用sleep()
方法,其它方式都需要通过Thread.sleep()
方式来调用。sleep()的作用:
1). 通过使线程休眠来扩大问题发生的可能性,使开发者能够迅速定位到bug的位置
2). 适用于程序要求频率慢的场景。比如在做网页爬虫搜集资料时,部分网页接口的请求频率有一定限制。如果调用接口频率太快,容易被错误识别导致封号。因此咱可以用
sleep()
方法随机休眠来保证调用安全。
调用sleep()方法不会释放对象的锁
。
yield()
yield
意为“礼让”,该方法作用是让当前线程放弃CPU的资源
,将它让给相同优先级
的其他线程。调用yield
方法后,当前线程会立即进入可运行态
。
我们来看一个例子:
1 | /** |
上述程序的输出结果为:
上述结果表明,
A线程
确实成功地将CPU使用权
“礼让”给了B线程
。 但是由于A线程
放弃的时间不确定,也许刚刚放弃CPU的使用权,就又获得了CPU的使用权。所以虽然有时发出了“礼让”请求,也会出现“礼让失败”的情况:
所以使用
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 | /** |
运行结果:
可以看到,优先级越高的线程会优先被
CPU
调度执行。但是本质上讲,这与操作系统
以及jvm的版本
有关,有可能即使设置了线程优先级也不会产生任何作用。
比如上面那个程序,多运行几次,你会发现,设置优先级不一定能输出理想的结果:
如上图,甚至恰恰相反的结果都有可能出现。因此,在实际应用中,优先级使用的意义也不大。
wait() & notify() & notifyAll()
wait() & notify()
是线程通信里两个非常常用的方法,它们是Object类
中的方法,却与线程通信息息相关。关于线程通信,前面学过的synchronized
可以主动协调线程间的执行关系,但是,两个线程间,有时还存在被动的通信关系。
比如在一个餐馆里,服务员和厨师的关系便是一个典型的被动通信
关系。在厨师没有做完菜肴之前,服务员只能处于等待状态,只有当厨师做完菜肴并且给服务员发出一个通知-----菜做完毕,服务员收到“通知”后才能开始上菜。
这种“等待与通知
”的被动通信关系广泛存在于生产生活之中。
映射到线程里就是,如果一个线程需要使用到另外一个线程的执行结果,那么它就必须处于等待状态,直到另一个线程执行完毕并给它发出通知,它收到通知之后,才开始执行
。
这种“等待与通知”的被动通信关系在许多地方也叫生产者和消费者模式。我们来看一段代码:
1 | /** |
运行结果:
注意左边的红色正方形按钮,说明程序还未终止,一直处于
等待状态
。
在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 | /** |
运行结果:
这个运行结果完美地诠释了上面的几个要点,请小伙伴们细细体会。
此外,wait()
方法可以有一个参数:wait(long millis)
。比如wait(1000)
,它的含义是,将对象的锁释放,同时使自己进入1000ms的阻塞状态
,1000ms过后如果锁没有被其他线程占用,则再次得到锁,然后wait()
方法结束,执行后面的代码。如果1000ms内锁被其他线程占用,则等待其他线程释放锁,自己进入可运行态
。
与无参的wait()
方法不同的是,有参的wait()
方法并不需要其他线程执行 notify()
或者 notifyAll()
来唤醒,只要超过了设定时间,线程会自动解除阻塞状态
。
join()
join()
方法出现的一个原因是,当主线程创建并启动子线程后,如果子线程中要进行大量的耗时运算,主线程往往早于子线程结束。如果主线程想等待子线程执行完之后再结束,比如子线程处理一个数据,主线程要获得这个处理后的数据,就必须让子线程执行完毕之后,再结束主线程。
不知小伙伴们发现了没有,这里面也蕴含了“等待和通知”的思想,那就是,在得到子线程“通知”之前,主线程只能处于“等待”状态。因此咱可以用wait()方法来实现。不过,这个不需要我们操心了,Thread
类里提供了join()
方法来实现这个功能。
查看join()
方法的源码,它的方法体中调用了wait()方法
:
我们通过一个简单的例子来学习一下
join()
的使用:
1 | /** |
上述代码中,子线程要对变量count
进行50000次累加操作,显然主线程(main线程
)结束的更快,当主线程结束时子线程还没处理完count
,主线程调用count
的值就会出现意外:
如上图,主线程输出的
count值
明显不是预料的结果50000。 这时就需要让主线程等待子线程执行完毕之后,再结束主线程。将上述的main()
方法修改如下(加上join()
方法):
1 | public static void main(String[] args) throws InterruptedException { |
运行结果:
这样便达到了预想的结果。
其它
Thread类
中还有一些不常用或比较简单的方法,现列举如下:
- Thread Thread.currentThread() :获得当前线程的引用。
- int Thread.activeCount():当前线程所在线程组中活动线程的数目。
- int enumerate(Thread[] tarray) :将当前线程的线程组及其子组中的每一个活动线程复制到指定的数组中。
- boolean holdsLock(Object obj) :当且仅当当前线程在指定的对象上保持有锁时,才返回 true。
- boolean interrupted() :测试当前线程是否已经中断。
- void checkAccess() :判定当前运行的线程是否有权修改该线程。
- getContextClassLoader() :返回该线程的上下文 ClassLoader。
- long getId() :返回该线程的标识符。
- String getName() :返回该线程的名称。
- Thread.State getState() :返回该线程的状态。
- ThreadGroup getThreadGroup() :返回该线程所属的线程组。
- void interrupt() :中断线程。
- boolean isAlive() :测试线程是否处于活动状态。
- boolean isDaemon() :测试该线程是否为守护线程。
- boolean isInterrupted():测试线程是否已经中断。
- void run() :线程启动后执行的方法。
- void setContextClassLoader(ClassLoader cl) :设置该线程的上下文 ClassLoader。
- void setDaemon(boolean on) :将该线程标记为守护线程或用户线程。
- void start():使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
- String toString():返回该线程的字符串表示形式,包括线程名称、优先级和线程组。
sleep() 和 wait() 方法的区别
这个是在多线程中经常被面试的问题。以下可供参考:
sleep()
是Thread类
中的一个静态方法,作用于当前线程。而wait()
是Object类
中的方法,任何实例对象均可以调用,作用于对象本身。sleep()
不会释放锁,也不会占用锁。而wait()
会释放锁,而且调用它的前提是当前线程持有锁。sleep()
方法可以在任何合法的地方调用。而wait()
只能在同步方法或同步代码块中调用。- 对于含参的
sleep()
,不管设定时间内有没有其他线程占用锁,设定时间过后都会使当前线程进入可运行态
。而含参的wait()
在设定时间超过之后,如果有线程占用了锁,则原线程立即进入可运行态
;如果没有线程占用锁,则原线程继续执行(运行态
)。
本章的内容比较多,一时消化不了,需要慢慢理解并且多敲代码加以运用。