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

简介: 一.线程中断 Java 中线程中断是一种线程间协作模式,通过设置线程的中断标志并不能直接终止该线程的执行,而是需要被中断的线程根据中断状态自行处理。   1.void interrupt() 方法:中断线程,例如当线程 A 运行时,线程 B 可以调用线程 A 的 interrupt() 方法来设置线程 A 的中断标志为 true 并立即返回。

一.线程中断

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

    }
}


运行结果如下:

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

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

 

二.理解线程上下文切换

在多线程编程中,线程个数一般都大于 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 进程结束那么就设置子线程为用户线程。

目录
相关文章
|
8天前
|
Java
Java基础—笔记—static篇
`static`关键字用于声明静态变量和方法,在类加载时初始化,只有一份共享内存。静态变量可通过类名或对象访问,但推荐使用类名。静态方法无`this`,不能访问实例成员,常用于工具类。静态代码块在类加载时执行一次,用于初始化静态成员。
10 0
|
8天前
|
Java API 索引
Java基础—笔记—String篇
本文介绍了Java中的`String`类、包的管理和API文档的使用。包用于分类管理Java程序,同包下类无需导包,不同包需导入。使用API时,可按类名搜索、查看包、介绍、构造器和方法。方法命名能暗示其功能,注意参数和返回值。`String`创建有两种方式:双引号创建(常量池,共享)和构造器`new`(每次新建对象)。此外,列举了`String`的常用方法,如`length()`、`charAt()`、`equals()`、`substring()`等。
14 0
|
9天前
|
安全 Java 开发者
深入理解Java并发编程:线程安全与性能优化
【4月更文挑战第9天】本文将深入探讨Java并发编程的核心概念,包括线程安全和性能优化。我们将详细解析Java中的同步机制,包括synchronized关键字、Lock接口以及并发集合等,并探讨它们如何影响程序的性能。此外,我们还将讨论Java内存模型,以及它如何影响并发程序的行为。最后,我们将提供一些实用的并发编程技巧和最佳实践,帮助开发者编写出既线程安全又高效的Java程序。
22 3
|
11天前
|
Java
Java 并发编程:深入理解线程池
【4月更文挑战第8天】本文将深入探讨 Java 中的线程池技术,包括其工作原理、优势以及如何使用。线程池是 Java 并发编程的重要工具,它可以有效地管理和控制线程的执行,提高系统性能。通过本文的学习,读者将对线程池有更深入的理解,并能在实际开发中灵活运用。
|
7天前
|
安全 算法 Java
深入理解Java并发编程:线程安全与性能优化
【4月更文挑战第11天】 在Java中,高效的并发编程是提升应用性能和响应能力的关键。本文将探讨Java并发的核心概念,包括线程安全、锁机制、线程池以及并发集合等,同时提供实用的编程技巧和最佳实践,帮助开发者在保证线程安全的前提下,优化程序性能。我们将通过分析常见的并发问题,如竞态条件、死锁,以及如何利用现代Java并发工具来避免这些问题,从而构建更加健壮和高效的多线程应用程序。
|
3天前
|
设计模式 运维 安全
深入理解Java并发编程:线程安全与性能优化
【4月更文挑战第15天】在Java开发中,多线程编程是提升应用程序性能和响应能力的关键手段。然而,它伴随着诸多挑战,尤其是在保证线程安全的同时如何避免性能瓶颈。本文将探讨Java并发编程的核心概念,包括同步机制、锁优化、线程池使用以及并发集合等,旨在为开发者提供实用的线程安全策略和性能优化技巧。通过实例分析和最佳实践的分享,我们的目标是帮助读者构建既高效又可靠的多线程应用。
|
4天前
|
SQL 安全 Java
Java安全编程:防范网络攻击与漏洞
【4月更文挑战第15天】本文强调了Java安全编程的重要性,包括提高系统安全性、降低维护成本和提升用户体验。针对网络攻击和漏洞,提出了防范措施:使用PreparedStatement防SQL注入,过滤和转义用户输入抵御XSS攻击,添加令牌对抗CSRF,限制文件上传类型和大小以防止恶意文件,避免原生序列化并确保数据完整性。及时更新和修复漏洞是关键。程序员应遵循安全编程规范,保障系统安全。
|
5天前
|
Java 编译器
Java并发编程中的锁优化策略
【4月更文挑战第13天】 在Java并发编程中,锁是一种常见的同步机制,用于保证多个线程之间的数据一致性。然而,不当的锁使用可能导致性能下降,甚至死锁。本文将探讨Java并发编程中的锁优化策略,包括锁粗化、锁消除、锁降级等方法,以提高程序的执行效率。
12 4
|
6天前
|
Java 调度 开发者
Java 21时代的标志:虚拟线程带来的并发编程新境界
Java 21时代的标志:虚拟线程带来的并发编程新境界
14 0
|
6天前
|
存储 安全 Java
Java语法掌握:打好编程基础的关键(二)
Java语法掌握:打好编程基础的关键
38 0