sleep是一个静态方法,它有两个重载方法。第一个需要传入休眠的毫秒数,第二个需要传入休眠的毫秒数和纳秒数。主要使用第一个就够了,第二个也是调用了第一个的方法,最后休眠的时间还是按照毫秒数进行的计算。
public static native void sleep(long millis) throws InterruptedException
public static void sleep(long millis, int nanos) throws InterruptedException
sleep方法根据指定的毫秒数休眠,但是也是交由底层实现,它的精度以系统定时器和调度器的精准度为准。sleep调用休眠不会放弃monitor锁的所有权,关于monitor锁后面再做介绍。
sleep休眠会作用于前线程,每个线程间互不影响。
除了sleep外,我们还可以使用TimeUnit,这个工具类对于 时间处理做了很好地换算封装,同时也把sleep方法封装进去。如果我们想要使线程休眠2小时30分钟20秒100毫秒,使用TimeUnit非常容易省去了换算。
TimeUnit.HOURS.sleep(2);
TimeUnit.MINUTES.sleep(30);
TimeUnit.SECONDS.sleep(20);
TimeUnit.MILLISECONDS.sleep(100);
我在前面的例子中使用的都是TimeUnit,非常的方便。
yield方法也是一个静态方法,直接使用 Thread.yield() 调用即可。它是一个无参无返回值的方法,也没有异常处理。
public static native void yield();
yield方法的作用是礼让,意思就是如果有多条线程在运行,当前线程调用了yield表明告诉调度器我愿意放弃当前的CPU,让其他线程先来。当然这种事也看CPU,如果CPU并不紧张,CPU会表示我们正常来就行了,不用切换。
调用yield并不会进入block,而是重新排队,只是放弃了CPU资源而已,这个方法也不是很常用。
与sleep的关系
关于线程优先级有两个方法,一个set,一个get
public final void setPriority(int newPriority)
public final int getPriority()
请注意,优先级并不会让线程先执行,只是让线程调度得到更多的机会。然而这种得到更多调度机会也不一定能更快,这取决于CPU。
理想状态下,优先级高的线程会得到更多的调度机会,举个例子:正常四条线程 1234 会按照1234 1234 1234 这样的顺序被调度,但是如果设置了线程1的优先级高的话,那么可能是安装12134 12314这样的顺序,线程1得到了更多的调度机会。
如果CPU比较慢,优先级可能会获得更多的的CPU时间片。在CPU闲时几乎没什么作用。所以不要在程序设计中依赖优先级,可能会让你失望。
优先级使用的例子
public static void main(String[] args) {
Thread t1 = new Thread(()->{
while (true){
System.out.println("t1");
}
});
t1.setPriority(2);
Thread t2 = new Thread(()->{
while (true){
System.out.println("t2");
}
});
t2.setPriority(10);
t1.start();
t2.start();
}
这里设置t2优先级高一些,在打印中t2 出现的频率也是高一些。当然每次运行的结果都会不一样,取决于CPU的调度。
我们看一下setPriority方法源码
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
//省略中间代码···
public final void setPriority(int newPriority) {
ThreadGroup g;
checkAccess();
if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
throw new IllegalArgumentException();
}
if((g = getThreadGroup()) != null) {
if (newPriority > g.getMaxPriority()) {
newPriority = g.getMaxPriority();
}
setPriority0(priority = newPriority);
}
}
通过源码我们可以看到,线程的优先级最小为1 最大为 10。如果超出范围则会报IllegalArgumentException异常。
如果是设置的值大于线程所在group的优先级,也是设置失败 ,使用当前group的最高优先级。这里优先级与ThreadGroup也是有很大关系。
默认的优先级为5,因为main线程的优先级就是5。main线程创建的线程优先级默认就是5.
获取线程id的方法
public long getId()
线程的ID在JVM中是唯一的,并且是从0开始逐次递增的。当在main中创建了一个线程,你会发现id并不是从0开始的,这是为啥?JVM在启动的时候,已经开辟了很多个线程,自增序列已经使用了,所以自己创建的线程肯定不是从0开始的
获取前线程的方法,这个方法是一个静态方法。方法比较简单,但是使用也是非常的频繁。
public static native Thread currentThread();
类加载器涉及到两个方法
public ClassLoader getContextClassLoader()
public void setContextClassLoader(ClassLoader cl)
getContextClassLoader() :获取线程上下文的类加载器,简单来说就是这个线程是由哪个类加载器加载的,如果没有修改,那么这个类加载器与父线程是同样的类加载器。
setContextClassLoader() :java类加载器后门,可以设置线程的类加载器,这个方法可以打破java类加载器的父委托机制。
关于线程上下文类加载器在之后再进行详细说明
这是一个非常重要的API,也是经常使用的一个方法。相关的API有
public void interrupt()
public static boolean interrupted()
public boolean isInterrupted()
当线程进入阻塞时,调用阻塞线程的interrupt方法可以打断阻塞。
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
try {
TimeUnit.MINUTES.sleep(1);
} catch (InterruptedException e) {
System.out.println("be interrupt");
}
});
thread.start();
TimeUnit.SECONDS.sleep(2);
thread.interrupt();
}
执行结果
上面开启了一个线程,并且预计休眠1分钟。不过在两秒后,我们在主线程调用子线程的interrupt方法,子线程的阻塞被打断,执行输出 “be interrupt” 。
interrupt的实现方式是怎样的呢?在线程内部有一个interrupt flag标识,如果一个线程被阻塞,那么flag标识被设置。当线程正在阻塞,调用interrupt 方法将其中断,flag标识会被清除。这一点后面再做详细介绍。
如果一个线程已经是死亡状态的话,调用interrupt方法会被直接忽略。
isInterrupt是Thread的一个成员方法,用作判断当前线程是否被中断。该方法仅仅是对interrupt标识的一个判断,不会影响标识发生改变。
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
while (true){
}
});
thread.setDaemon(true);
thread.start();
System.out.printf("Thread is interrupt %s\n",thread.isInterrupted());
thread.interrupt();
System.out.printf("Thread is interrupt %s\n",thread.isInterrupted());
}
结果:
我们开启了一个线程,并且在线程内有一个死循环。给线程设置为守护线程,线程运行结束会自动停止。默认的标识是false,当执行interrupt之后,标识变为true。如果在线程中写sleep方法的话,结果又会不一样。因为sleep是中断方法,会捕捉到中断信号。
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(){
@Override
public void run() {
while (true){
try {
TimeUnit.MINUTES.sleep(1);
} catch (InterruptedException e) {
System.out.printf("I am interrupted %s\n",isInterrupted());
}
}
}
};
thread.setDaemon(true);
thread.start();
TimeUnit.MILLISECONDS.sleep(10);
System.out.printf("Thread is interrupted %s\n",thread.isInterrupted());
thread.interrupt();
System.out.printf("Thread is interrupted %s\n",thread.isInterrupted());
}
结果
在run方法中我们使用了sleep这个中断方法,它会捕捉中断信号,并且擦除interrupt标识。我们看到输出结果开始为false,中断后变为true,在sleep中 又重置为false。其实也不难理解,可中断方法捕捉到interrupt信号之后,为了不影响线程中其他方法执行,将线程interrupt标识复位是一种很合理的设计。
interrupted是一个静态方法,也用作判断当前线程是否被中断。但是和成员方法isInterrupt还是有很大区别。调用该方法会直接擦除掉线程的interrupt标识,如果当前线程被打断了,那么第一次调用interrupted会返回true,并且重置标识为false。再次调用还会返回false。除非又一次调用interrupt。
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(){
@Override
public void run() {
while (true){
System.out.println(Thread.interrupted());
}
}
};
//thread.setDaemon(true);
thread.start();
TimeUnit.MILLISECONDS.sleep(10);
thread.interrupt();
}
查看源码,可以看到isInterrupt和interrupted都是调用了同一个方法
private native boolean isInterrupted(boolean ClearInterrupted);
参数ClearInterrupted主要用来控制是否擦除线程interrupt的标识。
public boolean isInterrupted() {
return isInterrupted(false);
}
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
再思考一个问题,如果先调用了interrupt,然后在调用sleep这种阻塞方法,会出现什么情况呢?答案是sleep马上会被打断。但是这里也是视情况而定。如果我们在调用interrupt后调用interrupted方法,那么sleep并不会被打断。如果调用的是isInterrupt或者不调用,都会导致sleep打断。这里就是由于interrupt 标识决定的。
线程的join也是一个很重要的方法,这个方法也是一个可中断方法,使用interrupt方法可以打断并捕捉到中断信号。
关于join有三个方法
public final void join() throws InterruptedException
public final synchronized void join(long millis, int nanos) throws InterruptedException
public final synchronized void join(long millis) throws InterruptedException
sleep方法是使当前线程进入休眠等待,而join则是使其他线程进入等待。比如现在有两条线程A和B,在线程B中调用线程A的join方法,线程B会进入等待,直到线程A生命周期结束或到达指定时间。
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for (int i=0;i<10;i++){
System.out.println("线程1##"+i);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(()->{
for (int i=0;i<10;i++){
System.out.println("线程2##"+i);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t3 = new Thread(()->{
for (int i=0;i<10;i++){
System.out.println("线程3##"+i);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t1.join();
t2.start();
t3.start();
}
结果
这里代码虽然多些,但是也是比较简单。我创建了三个线程,每个线程都输出从0-9的数。然后启动线程。这里我调用了t1线程的join,可以看到先执行了线程1,然后再交替执行线程2线程3。
这里其实不是很严谨,join阻塞的线程其实不是线程3,而是主线程main。调用t1.join()后,主线程阻塞,因为主线程是当前的调用线程。主线程阻塞,线程2线程3并没有被启动,所以不会有输出。我们换一下join的位置,放到线程2启动之后,在看结果就会发现,线程1和线程2交替输出,线程3等到线程1执行完成后开始执行。是由于主线程阻塞结束执行了线程3的start方法。
jdk有个废弃的方法stop,这个方法已经不推荐使用。官方给出的原因是该方法关闭的线程时可能不会释放掉monitor锁,不建议使用。下面再说一下其他停止线程的方式
线程在运行完成之后,会自动结束生命周期,这种就是放任式的结束。如果不能确保一定能退出,或者逻辑比较复杂,那么还是要有其他的退出方式。
我们可以使用中断信号interrupt来控制线程是否结束。前面有说,当调用interrupt方法后,interrupt flag标识会发生变化。我们可以利用这个来控制流程。
Thread thread = new Thread(){
@Override
public void run() {
System.out.println("线程开始");
while (!isInterrupted()){
//Do something
}
System.out.println("线程结束");
}
};
thread.start();
TimeUnit.SECONDS.sleep(3);
thread.interrupt();
由于线程的interrupt标识可能会被擦除,或者一些其他的中断但是不想结束线程,所以我们还会使用volatile修饰的开关flag关闭线程。
public class Demo9 extends Thread {
private volatile boolean closed = false;
@Override
public void run() {
System.out.println("线程开始");
while (!closed){
//Do something
}
System.out.println("线程结束");
}
public void close(){
this.closed = true;
}
public static void main(String[] args) throws InterruptedException {
Demo9 thread = new Demo9();
thread.start();
TimeUnit.SECONDS.sleep(3);
thread.close();
}
}
在这里定义了一个closed变量,并且使用volatile修饰。关于volatile后面会详细说明。这是一个革命性的关键字,它是java原子变量以及并发包的基础。
我们可以通过捕捉checked异常,从而在运行过程中进入到catch处理中,不再执行后面的代码,然后退出线程
/* 自己定义的一种异常,线程可以先去捕获自定义异常*/
class ExitException extends Exception {
public ExitException(String message) {
super(message);
}
}
public static void main(String[] args) {
Thread thread = new Thread() {
int i = 0;
@Override
public void run() {
try {
while (true) {
if (i > 3) {
throw new ExitException();
}
System.out.println(i);
i++;
}
} catch (ExitException e) {
//捕获到,若不执行就会推出
System.out.println("捕获异常信号推出线程");
}
}
};
thread.start();
}
我们就是利用异常会跳转到catch的特性,控制线程内的逻辑,从而达到终止线程的目的。
有时候,我们得需要借助jconsole这样的监测工具来判断线程的状态。有时候我们发现线程一直没有输出,也没有什么动静。这时候不一定就是线程死亡,也有可能是进入线程休眠或者死锁。特别是死锁,这个开发中会经常见到。我争取坚持写博客,能够把前面所有想单独说的地方都说一下