Java并发编程笔记之基础总结(二)

一.线程中断

Java 中线程中断是一种线程间协作模式,通过设置线程的中断标志并不能直接终止该线程的执行,而是需要被中断的线程根据中断状态自行处理。

  1.void interrupt() 方法:中断线程,例如当线程 A 运行时,线程 B 可以调用线程 A 的 interrupt() 方法来设置线程 A 的中断标志为 true 并立即返回。设置标志仅仅是设置标志,线程 A 并没有实际被中断,会继续往下执行的。如果线程 A 因为调用了 wait 系列函数或者 join 方法或者 sleep 函数而被阻塞挂起,这时候线程 B 调用了线程 A 的 interrupt() 方法,线程 A 会在调用这些方法的地方抛出 InterruptedException 异常而返回。

 

  2.boolean isInterrupted():检测当前线程是否被中断,如果是返回 true,否者返回 false,代码如下:


public boolean isInterrupted() {
   //传递false,说明不清除中断标志
   return isInterrupted(false);
}


  3.boolean interrupted():检测当前线程是否被中断,如果是返回 true,否者返回 false,与 isInterrupted 不同的是该方法如果发现当前线程被中断后会清除中断标志,并且该函数是 static 方法,可以通过 Thread 类直接调用。另外从下面代码可以知道 interrupted() 内部是获取当前调用线程的中断标志而不是调用 interrupted() 方法的实例对象的中断标志。


public static boolean interrupted() {
    //清除中断标志
    return currentThread().isInterrupted(true);
}


下面看一个线程使用 Interrupted 优雅退出的经典使用例子,代码如下:


public void run(){    
    try{    
         ....    
         //线程退出条件
         while(!Thread.currentThread().isInterrupted()&& more work to do){    
                // do more work;    
         }    
    }catch(InterruptedException e){    
                // thread was interrupted during sleep or wait    
    }    
    finally{    
               // cleanup, if required    
    }    
}


下面看一个根据中断标志判断线程是否终止的例子:


/**
 * Created by cong on 2018/7/17.
 */
public class InterruptTest {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                //如果当前线程被中断则退出循环
                while (!Thread.currentThread().isInterrupted())
                    System.out.println(Thread.currentThread() + " hello");
            }
        });
        //启动子线程
        thread.start();

        //主线程休眠1s,以便中断前让子线程输出点东西
        Thread.sleep(1);

        //中断子线程
        System.out.println("main thread interrupt thread");
        thread.interrupt();

        //等待子线程执行完毕
        thread.join();
        System.out.println("main is over");

    }
}


运行结果如下:

Java并发编程笔记之基础总结(二)

如上代码子线程 thread 通过检查当前线程中断标志来控制是否退出循环,主线程在休眠 1s 后调用 thread 的 interrupt() 方法设置了中断标志,所以线程 thread 退出了循环。

总结:中断一个线程仅仅是设置了该线程的中断标志,也就是设置了线程里面的一个变量的值,本身是不能终止当前线程运行的,一般程序里面是检查这个标志的状态来判断是否需要终止当前线程。

 

二.理解线程上下文切换

在多线程编程中,线程个数一般都大于 CPU 个数,而每个 CPU 同一时刻只能被一个线程使用,为了让用户感觉多个线程是在同时执行,CPU 资源的分配采用了时间片轮转的策略,也就是给每个线程分配一个时间片,在时间片内占用 CPU 执行任务。当前线程的时间片使用完毕后当前就会处于就绪状态并让出 CPU 让其它线程占用,这就是上下文切换,从当前线程的上下文切换到了其它线程。

那么就有一个问题让出 CPU 的线程等下次轮到自己占有 CPU 时候如何知道之前运行到哪里了?

所以在切换线程上下文时候需要保存当前线程的执行现场,当再次执行时候根据保存的执行现场信息恢复执行现场

线程上下文切换时机:

  1.当前线程的 CPU 时间片使用完毕处于就绪状态时候;

  2.当前线程被其它线程中断时候

总结:由于线程切换是有开销的,所以并不是开的线程越多越好,比如如果机器是4核心的,你开启了100个线程,那么同时执行的只有4个线程,这100个线程会来回切换线程上下文来共享这四个 CPU。

 

三.线程死锁

什么是线程死锁呢?

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去。

Java并发编程笔记之基础总结(二)

如上图,线程 A 已经持有了资源1的同时还想要资源2,线程 B 在持有资源2的时候还想要资源1,所以线程1和线程2就相互等待对方已经持有的资源,就进入了死锁状态。

那么产生死锁的原因都有哪些,学过操作系统的应该都知道死锁的产生必须具备以下四个必要条件。

  1.互斥条件:指线程对已经获取到的资源进行排它性使用,即该资源同时只由一个线程占用。如果此时还有其它进行请求获取该资源,则请求者只能等待,直至占有资源的线程用毕释放。

  2.请求并持有条件:指一个线程已经持有了至少一个资源,但又提出了新的资源请求,而新资源已被其其它线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己已经获取的资源。

  3.不可剥夺条件:指线程获取到的资源在自己使用完之前不能被其它线程抢占,只有在自己使用完毕后由自己释放。

  4.环路等待条件:指在发生死锁时,必然存在一个线程——资源的环形链,即线程集合{T0,T1,T2,···,Tn}中的 T0 正在等待一个 T1 占用的资源;T1 正在等待 T2 占用的资源,……Tn正在等待已被 T0 占用的资源。

 

下面通过一个例子来说明线程死锁,代码如下:


/**
 * Created by cong on 2018/7/17.
 */
public class DeadLockTest1 {
    // 创建资源
    private static Object resourceA = new Object();
    private static Object resourceB = new Object();

    public static void main(String[] args) {
        // 创建线程A
        Thread threadA = new Thread(new Runnable() {
            public void run() {
                synchronized (resourceA) {
                    System.out.println(Thread.currentThread() + " get ResourceA");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread() + "waiting get ResourceB");
                    synchronized (resourceB) {
                        System.out.println(Thread.currentThread() + "get ResourceB");
                    }
                }
            }
        });
        // 创建线程B
        Thread threadB = new Thread(new Runnable() {
            public void run() {
                synchronized (resourceB) {
                    System.out.println(Thread.currentThread() + " get ResourceB");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread() + "waiting get ResourceA");
                    synchronized (resourceA) {
                        System.out.println(Thread.currentThread() + "get ResourceA");
                    }
                };
            }
        });
        // 启动线程
        threadA.start();
        threadB.start();
    }
}


运行结果如下:

Java并发编程笔记之基础总结(二)

下面分析下代码和结果,其中 Thread-0 是线程 A,Thread-1 是线程 B,代码首先创建了两个资源,并创建了两个线程。

从输出结果可以知道线程调度器先调度了线程 A,也就是把 CPU 资源让给了线程 A,线程 A 调用了 getResourceA() 方法,方法里面使用 synchronized(resourceA) 方法获取到了 resourceA 的监视器锁,然后调用 sleep 函数休眠 1s,休眠 1s 是为了保证线程 A 在执行 getResourceB 方法前让线程 B 抢占到 CPU 执行 getResourceB 方法。

线程 A 调用了 sleep 期间,线程 B 会执行 getResourceB 方法里面的 synchronized(resourceB),代表线程 B 获取到了 objectB 对象的监视器锁资源,然后调用 sleep 函数休眠 1S。

好了,到了这里线程 A 获取到了 objectA 的资源,线程 B 获取到了 objectB 的资源。线程 A 休眠结束后会调用 getResouceB 方法企图获取到 ojbectB 的资源,而 ObjectB 资源被线程 B 所持有,所以线程 A 会被阻塞而等待。而同时线程 B 休眠结束后会调用 getResourceA 方法企图获取到 objectA 上的资源,而资源 objectA 已经被线程 A 持有,所以线程 A 和 B 就陷入了相互等待的状态也就产生了死锁。

 

下面从产生死锁的四个条件来谈谈本案例如何满足了四个条件。

首先资源 resourceA 和 resourceB 都是互斥资源,当线程 A 调用 synchronized(resourceA) 获取到 resourceA 上的监视器锁后释放前,线程 B 在调用 synchronized(resourceA) 尝试获取该资源会被阻塞,只有线程 A 主动释放该锁,线程 B 才能获得,这满足了资源互斥条件。

线程 A 首先通过 synchronized(resourceA) 获取到 resourceA 上的监视器锁资源,然后通过 synchronized(resourceB) 等待获取到 resourceB 上的监视器锁资源,这就构造了持有并等待。

线程 A 在获取 resourceA 上的监视器锁资源后,不会被线程 B 掠夺走,只有线程 A 自己主动释放 resourceA 的资源时候,才会放弃对该资源的持有权,这构造了资源的不可剥夺条件。

线程 A 持有 objectA 资源并等待获取 objectB 资源,而线程 B 持有 objectB 资源并等待 objectA 资源,这构成了循环等待条件。

所以线程 A 和 B 就形成了死锁状态。

那么如何避免线程死锁呢?

要想避免死锁,需要破坏构造死锁必要条件的至少一个即可,但是学过操作系统童鞋应该都知道目前只有持有并等待和循环等待是可以被破坏的。

造成死锁的原因其实和申请资源的顺序有很大关系,使用资源申请的有序性原则就可以避免死锁,那么什么是资源的有序性呢,先看一下对上面代码的修改:


   // 创建线程B
        Thread threadB = new Thread(new Runnable() {
            public void run() {
                synchronized (resourceA) {
                    System.out.println(Thread.currentThread() + " get ResourceB");

                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread() + "waiting get ResourceA");
                    synchronized (resourceB) {
                        System.out.println(Thread.currentThread() + "get ResourceA");
                    }
                };
            }
        });


运行结果如下:

Java并发编程笔记之基础总结(二)

如上代码可知修改了线程 B 中获取资源的顺序和线程 A 中获取资源顺序一致,其实资源分配有序性就是指假如线程 A 和 B 都需要资源1,2,3……n 时候,对资源进行排序,线程 A 和 B 只有在获取到资源 n-1 时候才能去获取资源 n。

总结:编写并发程序,多个线程进行共享多个资源时候要注意采用资源有序分配法避免死锁的产生。

 

四守护线程与用户线程

Java 中线程分为两类,分别为 Daemon 线程(守护线程)和 User 线程(用户线程),在 JVM 启动时候会调用 main 函数,main 函数所在的线程是一个用户线程,这个是我们可以看到的线程,其实 JVM 内部同时还启动了好多守护线程,比如垃圾回收线程(严格说属于 JVM 线程)。

那么守护线程和用户线程有什么区别呢?

区别之一是当最后一个非守护线程结束时候,JVM 会正常退出,而不管当前是否有守护线程;也就是说守护线程是否结束并不影响 JVM 的退出。言外之意是只要有一个用户线程还没结束正常情况下 JVM 就不会退出。

那么 Java 中如何创建一个守护线程呢?代码如下:


public static void main(String[] args) {

        Thread daemonThread = new Thread(new  Runnable() {
            public void run() {

            }
        });

        //设置为守护线程
        daemonThread.setDaemon(true);
        daemonThread.start();

} 


可知只需要设置线程的 daemon 参数为 true 即可。

下面通过例子来加深用户线程与守护线程的区别的理解,首先看下面代码:


/**
 * Created by cong on 2018/7/17.
 */
public class UserThreadTest {
    public static void main(String[] args) {

        Thread thread = new Thread(new  Runnable() {
            public void run() {
                for(;;){}
            }
        });

        //启动子线
        thread.start();

        System.out.print("main thread is over");
    }
}


运行结果如下:

Java并发编程笔记之基础总结(二)

如上代码在 main 线程中创建了一个 thread 线程,thread 线程里面是无限循环,运行代码从结果看 main 线程已经运行结束了,那么 JVM 进程已经退出了?从 IDE 的输出结侧上的红色方块说明 JVM 进程并没有退出,另外 Mac 上执行 ps -eaf | grep java 会输出结果,也可以证明这个结论。

这个结果说明了当父线程结束后,子线程还是可以继续存在的,也就是子线程的生命周期并不受父线程的影响。也说明了当用户线程还存在的情况下 JVM 进程并不会终止。

那么我们把上面的 thread 线程设置为守护线程后在运行看看会有什么效果,代码如下:


/**
 * Created by cong on 2018/7/17.
 */
public class DaemonThreadTest {
    public static void main(String[] args) {
        Thread thread = new Thread(new  Runnable() {
            public void run() {
                for(;;){}
            }
        });
        //设置为守护线程
        thread.setDaemon(true);
        //启动子线
        thread.start();
        System.out.print("main thread is over");
    }
}


运行结果如下:

Java并发编程笔记之基础总结(二)

如上在启动线程前设置线程为守护线程,从输出结果可知 JVM 进程已经终止了,执行 ps -eaf |grep java 也看不到 JVM 进程了。这个例子里面 main 函数是唯一的用户线程,thread 线程是守护线程,当 main 线程运行结束后,JVM 发现当前已经没有用户线程了,就会终止 JVM 进程。

Java 中在 main 线程运行结束后,JVM 会自动启动一个叫做 DestroyJavaVM 线程,该线程会等待所有用户线程结束后终止 JVM 进程。

下面通过简单的 JVM 代码来证明这个结论,翻开 JVM 的代码,最终会调用到 JavaMain 这个函数:


int JNICALL
JavaMain(void * _args)
{   
    ...
    //执行Java中的main函数 
    (*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);

    //main函数返回值
    ret = (*env)->ExceptionOccurred(env) == NULL ? 0 : 1;

    //等待所有非守护线程结束,然后销毁JVM进程
    LEAVE();
}


LEAVE 是 C 语言里面的一个宏定义,定义如下:


#define LEAVE() 
    do { 
        if ((*vm)->DetachCurrentThread(vm) != JNI_OK) { 
            JLI_ReportErrorMessage(JVM_ERROR2); 
            ret = 1; 
        } 
        if (JNI_TRUE) { 
            (*vm)->DestroyJavaVM(vm); 
            return ret; 
        } 
    } while (JNI_FALSE)


上面宏的作用实际是创建了一个名字叫做 DestroyJavaVM 的线程来等待所有用户线程结束。

在 Tomcat 的 NIO 实现 NioEndpoint 中会开启一组接受线程用来接受用户的链接请求和一组处理线程负责具体处理用户请求,那么这些线程是用户线程还是守护线程呢?下面我们看下 NioEndpoint 的 startInternal 方法,源码如下:


public void startInternal() throws Exception {

        if (!running) {
            running = true;
            paused = false;

            ...

            //创建处理线程
            pollers = new Poller[getPollerThreadCount()];
            for (int i=0; i<pollers.length; i++) {
                pollers[i] = new Poller();
                Thread pollerThread = new Thread(pollers[i], getName() + "-ClientPoller-"+i);
                pollerThread.setPriority(threadPriority);
                pollerThread.setDaemon(true);//声明为守护线程
                pollerThread.start();
            }
            //启动接受线程
            startAcceptorThreads();
    }

  protected final void startAcceptorThreads() {
        int count = getAcceptorThreadCount();
        acceptors = new Acceptor[count];

        for (int i = 0; i < count; i++) {
            acceptors[i] = createAcceptor();
            String threadName = getName() + "-Acceptor-" + i;
            acceptors[i].setThreadName(threadName);
            Thread t = new Thread(acceptors[i], threadName);
            t.setPriority(getAcceptorThreadPriority());
            t.setDaemon(getDaemon());//设置是否为守护线程,默认为守护线程
            t.start();
        }
  }

  private boolean daemon = true;
  public void setDaemon(boolean b) { daemon = b; }
  public boolean getDaemon() { return daemon; }


如上代码也就是说默认情况下接受线程和处理线程都是守护线程,这意味着当 Tomact 收到 shutdown 命令后 Tomact 进程会马上消亡,而不会等处理线程处理完当前的请求。

总结:如果你想在主线程结束后 JVM 进程马上结束,那么创建线程的时候可以设置线程为守护线程,否则如果希望主线程结束后子线程继续工作,等子线程结束后在让 JVM 进程结束那么就设置子线程为用户线程。

上一篇:Java并发编程笔记之FutureTask源码分析


下一篇:Java并发编程笔记之 CountDownLatch闭锁的源码分析