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

一.线程中断

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 优雅退出的经典使用例子,代码以下:ide

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");

    }
}

运行结果以下:ui

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

总结:中断一个线程仅仅是设置了该线程的中断标志,也就是设置了线程里面的一个变量的值,自己是不能终止当前线程运行的,通常程序里面是检查这个标志的状态来判断是否须要终止当前线程。操作系统

 

二.理解线程上下文切换

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

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

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

线程上下文切换时机:

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

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

总结:因为线程切换是有开销的,因此并非开的线程越多越好,好比若是机器是4核心的,你开启了100个线程,那么同时执行的只有4个线程,这100个线程会来回切换线程上下文来共享这四个 CPU。

 

三.线程死锁

什么是线程死锁呢?

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

如上图,线程 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();
    }
}

运行结果以下:

下面分析下代码和结果,其中 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");
                    }
                };
            }
        });

运行结果以下:

如上代码可知修改了线程 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");
    }
}

运行结果以下:

如上代码在 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");
    }
}

运行结果以下:

如上在启动线程前设置线程为守护线程,从输出结果可知 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 进程结束那么就设置子线程为用户线程。

相关文章
相关标签/搜索