Java高并发编程详解系列-三
之前的两篇分享中,简单的从概念上和简单的操作上了解了Thread,这篇分享中主要是看看对于Thread的所有API的使用方式,通过解析源码的方式来了解关于Thread的细节的使用方式
引言
首先在了解Thread之前,先了解一个Object类,这个类作为所有类的父类,是对所有对象的抽象。
Object
从代码结构中可以清楚的看到Object为我们提供了12个方法和一个静态方法块
这里我们看一下这些方法都是什么含义?
这里我们需要关注三个方法
notify() 方法:唤醒在此对象监视器上等待的单个线程
notifyAll() 方法:唤醒在此对象监视器上等待的所有线程
wait() 方法:在其他线程调用上面两个方法之前,导致当前线程处于等待。
从这里让我们可以清楚的认识到那些方法是Thread自己的,那些方法是继承自Object类的。而Object类在设计的时候就已经对线程的生命周期有所规划。
sleep()方法
sleep()方法作为线程的Thread的一个静态方法,查看源码之后我们会发现这个方法其实有两个重载方法
public static native void sleep(long millis) throws InterruptedException;
public static void sleep(long millis, int nanos);
这里我们看到第一个方法是被native修饰的,而sleep方法的主要作用就是使得当前线程进入sleep状态指定的秒数,而这个休眠是不会放弃monitor的锁的所有权。
public class ThreadSleep {
public static void main(String[] args){
new Thread (()->{
long startTime = System.currentTimeMillis();
sleep(2_000L);
long endTime = System.currentTimeMillis();
System.out.println(String.format("所花费的时间 %d ms",(endTime-startTime)));
}).start();
long startTime = System.currentTimeMillis();
sleep(3_000L);
long endTime = System.currentTimeMillis();
System.out.println(String.format("所花费的时间 %d ms",(endTime-startTime)));
}
private static void sleep(long ms){
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
}
}
}
对于主线程和子线程都进行了休眠,每个线程的休眠是互相不影响的。而Thread.sleep方法只会使得当前线程进入指定的休眠时间。
而下面这个方法表示在指定的毫秒数加指定的纳秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
public static void sleep(long millis, int nanos)
throws InterruptedException {
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
millis++;
}
sleep(millis);
}
其实我们查看源码会发现到最后两种方式调动的还是同一个方法都是被native标记的方法也就是说操作了底层的内存。在JVM内存分配的时候方法区和堆内存是所有线程共享,当然还有每个线程私有的,而这个sleep就是在这个线程私有的虚拟机栈中。所以说对于每个线程使用Thread.sleep方法是互不影响的。
对于sleep的改进TimeUnit
在JDK1.5之后,在Java中多了一个枚举类型TimeUnit,用它来取代了我们的Thread.sleep方法,而对于TimeUnit来说这里我们就不做详细的介绍了。简单的据几个例子
TimeUnit.SECONDS.sleep(1);
TimeUnit.HOURS.sleep(1);
TimeUnit.MINUTES.sleep(1);
TimeUnit.MILLISECONDS.sleep(1);
分别表示秒、时、分、毫秒。
yield方法
在Threa的中yield方法属于启发式的方法,使用这个方法之后会主动放弃当前CPU的资源,如果CPU资源可用的话就会忽略这个方法的调用。调用者方法的就会使得当前的线程从Running状态切换到Runnable状态,所以说这个方法其实在实际开发中并不是太常用。
public class ThreadYield {
public static void main(String[] args) {
IntStream.range(0,2).mapToObj(ThreadYield::create).forEach(Thread::start);
}
private static Thread create(int index){
return new Thread(()->{
// if (index==0){
// Thread.yield();
// }
System.out.println(index);
});
}
}
我们会发现每次运行的结果都是不太一样的,这个是什么原因造成的呢?首先我们知道两个线程并行执行的到CPU底层实际上是串行执行的,那个线程先获取到CPU资源那个线程就会先进行打印。而使用了yield方法之后就会主动的提示调度器。但是这个也不是绝对的CPU就一定每次都收到这个yield的提示信息。
yield 方法和sleep方法
- sleep会导致当前线程暂停相应的时间,并不会消耗CPU
- yield方法只是对于CPU调度的一个提示,实际的掌握是否调度还是在CPU
- sleep会使得线程有一个短暂的阻塞,会在指定的时间内释放CPU
- sleep一定会让线程进入休眠,但是yield则是需要CPU自己进行判断
- 一个线程sleep,而另一个线程调用interrupt会捕获中断信号。
线程优先级
public final int getPriority()
public final void setPriority(int newPriority)
什么是线程的优先级
对于线程的优先级一个最简单的解释就是那个线程先执行,那个线程后执行,而优先级更高的线程获取到CPU的机会就越大。但是实际上优先级的设置也是一个比较鸡肋的操作。
所以说在设计程序的时候千万不要以为通过线程优先级来改变某些特定的业务的执行顺序。不同的环境会有不同的操作结果。
public class ThreadPriority {
public static void main(String[] args) {
Thread t1 = new Thread(()->{
while (true){
System.out.println("我的优先级是三级");
}
});
t1.setPriority(3);
Thread t2 = new Thread(()->{
while (true){
System.out.println("我的优先级是十级");
}
});
t2.setPriority(10);
t1.start();
t2.start();
}
}
线程优先级的源码分析
首先来看一下setPriority的源码
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);
}
}
一般情况下我们在使用的时候并不会显式的为某个线程指定优先级,更不会让我们编写的某些业务代码依赖于线程优先级,一般我们使用默认的优先级就好了
getId方法
通过这个方法可以获得线程的唯一ID,线程的ID在整个的JVM进程中都会是唯一的存在。并且这个ID是从0开始增加的,但是我们在实际创建的时候并不是从0开始的,这个是因为JVM启动的时候就需要很多的线程来维护,所以说自己创建的线程不会使从0开始。
currentThread()
这个方法返回的是当前线程的一个引用。在很多的场合都会使用到这个方法。
public static native Thread currentThread();
interrupt()
对于线程interrupt,是一个比较重要的方法,这个方法与线程的中断有关,那么什么是线程的中断。例如你在玩手机游戏,突然之间就有一个电话进来了,这个时候你的电话享有对于你这手机资源的最高使用权,所以说你的游戏就必须中断,来支持你的收集资源来完成打电话的操作。这里提供了三个方法
public void interrupt()
public static boolean interrupted()
public boolean isInterrupted()
那么线程中断和线程阻塞的区别在什么地方,调用如下的一些方法会使得当前线程进入到阻塞状态,而调用interrupt方法就可以打断阻塞。
上面的这些方法都会使得当前线程进入到阻塞状态,如果另外的一个线程被调用阻塞线程的interrupt方法,就会打断这种阻塞,这个方法就被称为是可中断的,也就是说打断阻塞并不是结束生命周期。如果一个线程在阻塞状态下被打断则会抛出一个InterruptedException的异常。
public class ThreadInterrupt {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
try {
TimeUnit.MINUTES.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
//
TimeUnit.MILLISECONDS.sleep(2);
thread.interrupt();
}
}
结果
底层实现
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
//同步锁
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}
public interface Interruptible {
void interrupt(Thread var1);
}
从源码中我们可以看到在interrupt方法中存在一个 interrupt flag的标识,如果一个线程被打断这个标识就会被设置,但是如果这个线程正在执行中断方法的时候被阻塞了,就会调用这个中断方法将其中断。这个就会导致整个的flag被清除。
isInterrupted()
判断当前线程是否被中断,该方法仅仅是对于interrupt标识的判断。并不会影响标识的改变。
public boolean isInterrupted() {
return isInterrupted(false);
}
public class ThreadisInterrupted {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(){
@Override
public void run() {
while (true){
//死循环
}
}
};
thread.start();
TimeUnit.MILLISECONDS.sleep(2);
System.out.printf("线程是否是被中断 %s\n", thread.isInterrupted());
thread.interrupt();
System.out.printf("线程是否被中断 %s\n",thread.isInterrupted());
}
}
在代码中定义一个死循环线程,这里就有一个问题为什么不使用sleep而要使用死循环呢?因为我们知道在之前那个图中sleep是一个可中断的方法,所以说使用sleep被中断后会有异常抛出。
public class ThreadisInterruptedNew {
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.println("这里会抛出异常");
}
}
}
};
thread.setDaemon(true);//设置为守护进程
thread.start();
TimeUnit.MILLISECONDS.sleep(2);
System.out.printf("这个线程是否被打断 %s\n",thread.isInterrupted());
thread.interrupt();
TimeUnit.MILLISECONDS.sleep(2);
System.out.printf("这个线程是否被打断 %s\n",thread.isInterrupted());
}
}
interrupted()
这个方法是一个静态方法,也是用于判断线程是否中断,但是和isInterrupted方法还是有很大的区别的,这个方法的调用会直接消除线程的interrupt的标识,如果当前线程被打断了,那么第一次调用这个方法的时候会返回true,并且清除interrupt的标识,第二次以后都会返回false,除非这个线程在此期间再次被打断。
说白了这个方法就是判断线程是否被打断过。如果被打断过则调用第一次的时候为true,第二次以后开始再也没有这状态的变化就会为false,如果想要第二次true的话就需要重新打断一次。
join()
Thread类的join方法也是作为一个重要方法来说,它的特性可以实现很多的功能。例如它是一个可中断方法,如果其他线程对当前线程进行interrupt操作,它也会受到中断信号的影响清除interrupt标识。
join详细介绍
join调用线程A会使得另一个线程B处于等待,直到当前线程A结束生命周期,或者到达指定的时间。这期间对于另外的线程B来讲都是处于Blocked状态。
public class ThreadJoin {
public static void main(String[] args) throws InterruptedException {
//定义线程
List<Thread> threads = IntStream.range(1,3).mapToObj(ThreadJoin::create).collect(toList());
//启动线程
threads.forEach(Thread::start);
//执行join方法
for (Thread thread : threads){
thread.join();
}
//main线程循环输出
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+" "+ i);
shortSleep();
}
}
private static void shortSleep() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private static Thread create(int seq){
return new Thread(()->{
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
shortSleep();
}
},String.valueOf(seq));
}
}
上面代码中创建了两个线程,并且分别启动了join方法,此时会交替输出直到生命周期结束。才会执行主线程。这样的话Join方法会使得当前线程一直执行,直到被另外的线程中断。如果将join方法注释掉之后就会执行主线程的循环。
线程关闭
正常关闭
- 线程的生命周期结束正常关闭
- 通过中断信号关闭线程
- 使用volatile控制
public class FlagThreadExit {
static class MyTask extends Thread{
private volatile boolean closed = false;
@Override
public void run() {
System.out.println("开始工作!");
while (!closed&&!isInterrupted()){
}
System.out.println("结束工作");
}
public void close(){
this.closed = true;
this.interrupt();
}
}
public static void main(String[] args) throws InterruptedException {
MyTask t = new MyTask();
t.start();
TimeUnit.MINUTES.sleep(1);
System.out.println("线程结束");
t.close();
}
}
异常退出
如果在程序运行过程中,出现了异常就会导致线程关闭
进程假死
在很多的情况下进程的假死就是说进程已经存在但是没有任何的反应,这个时候就要怀疑是否是进入了死锁,就要使用一些特殊的手段进行查看了