天天看点

并发多线程之线程

目录

    • 用户线程和内核线程
    • Java使用的线程
    • JVM中线程的创建
      • new java.lang.Thread().start()
      • 使用JNI将一个native thread attach到JVM中
    • Java中实现线程的方式
    • 线程的生命周期
    • 线程上下文切换及死锁
      • 线程上下文切换
      • 死锁

什么是线程?

现代操作系统在运行一个程序时,会为其创建一个进程。例如,启动一个Java程序,操作系统就会创建一个Java进程。现代操作系统调度CPU的最小单元是线程,也叫轻量级进程(Light Weight Process),在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上高速切换, 让使用者感觉到这些线程在同时执行。

用户线程和内核线程

用户线程

指不需要内核支持而在用户程序中实现的线程,其不依赖于操作系统核心,应

用进程利用线程库提供创建、同步、调度和管理线程的函数来控制用户线程。另外,用户线程是由应用进程利用线程库创建和管理,不依赖于操作系统核心。不需要用户态/核心态切换,速度快。操作系统内核不知道多线程的存在,因此一个线程阻塞将使得整个进程(包括它的所有线程)阻塞。由于这里的处理器时间片分配是以进程为基本单位,所以每个线程执行的时间相对减少。

并发多线程之线程

内核线程

线程的所有管理操作都是由操作系统内核完成的。内核保存线程的状态和上下

文信息,当一个线程执行了引起阻塞的系统调用时,内核可以调度该进程的其他线程执行。在多处理器系统上,内核可以分派属于同一进程的多个线程在多个处理器上运行,提高进程执行的并行度。由于需要内核完成线程的创建、调度和管理,所以和用户级线程相比这些操作要慢得多,但是仍然比进程的创建和管理操作要快。大多数市场上的操作系统,如Windows, Linux等都支持内核级线程。

并发多线程之线程

Java使用的线程

java在1.2之前使用的是使用的用户线程,后来改变使用内核线程,这也是为什么Thread类中的start0()为native方法,就是去调用操作系统库中创建内核线程。

JVM中线程的创建

在jvm中创建线程有两种方式(注意:这里说创建一个线程是指内核线程,并不是我们常说的继承Thread或者实现Runnable/Callable接口,继承Thread或者实现Runbale/Callable只是java用于指定内核线程要执行的应用程序中要执行的代码。)

new java.lang.Thread().start()

在Thread的start()方法中,调用了其start0()方法,而该方法是一个native的方法,通过调用该方法产生一个内核线程然后再执行Thread的run方法。

其流程大致如下:

  1. 创建对应的JavaThread的instance
  2. 创建对应的OSThread的instance
  3. 创建实际的底层操作系统的native thread
  4. 准备相应的JVM状态,比如ThreadLocal存储空间分配等
  5. 底层的native thread开始运行,调用java.lang.Thread生成的Object 的run()方法
  6. 当java.lang.Thread生成的Object的run()方法执行完毕返回后,或者抛出 异常终止后,终止native thread
  7. 释放JVM相关的thread的资源,清除对应的JavaThread和OSThread

使用JNI将一个native thread attach到JVM中

其大致流程如下:

  1. 通过JNI call AttachCurrentThread申请连接到执行的JVM实例
  2. JVM创建相应的JavaThread和OSThread对象
  3. 创建相应的java.lang.Thread的对象
  4. 一旦java.lang.Thread的Object创建之后,JNI就可以调用Java代码了
  5. 当通过JNI call DetachCurrentThread之后,JNI就从JVM实例中断开连接
  6. JVM清除相应的JavaThread, OSThread, java.lang.Thread对象

Java中实现线程的方式

通过2.3接我们知道,在代码中通过Thread的start0()方法创建一个内核线程,这个线程在相应的资源准备好之后会回调jvm中Thread实例的run()方法,可以说是底层给应用程序开放的一个接口供开发者调用从而实现多线程。

那么Java中有几种实现线程的方法呢?

  1. 继承Thread
    public class MyThread  extends Thread{
    	 @Override
     	public void run() {
       	 	System.out.println("i am a thread, i am running");
    	}
     	public static void main(String[] args) {
       	 	Thread thread = new MyThread();
      	  	// 开启线程
      	  	thread.start();
     	}
    }
               
  2. 通过实现Runable接口
    public class MyThreadImpl implements Runnable {
    
     	public void run() {
        	System.out.println("i am a thread, i am running");
    	}
    
    	public static void main(String[] args) {
       	 	new Thread(new MyThreadImpl()).start();
    	}
    }
               
    通过上面代码我们可以看到,通过实现Runnable接口仍然需要借助Thread类的实例运行start()方法启动线程,因为只有Thread类中存在调用底层库创建内核线程的本地方法start0().
  3. 通过实现Callable(有返回结果)接口
    public class MyThreadCallImpl implements Callable<String> {
    	public String call() throws Exception {
        	System.out.println("i am thread, i can return result, i am running");
        	return "result";
    	 }
    	public static void main(String[] args) throws Exception {
        	FutureTask<String> result = new FutureTask<String>(new MyThreadCallImpl());
        	// 开启线程
        	new Thread(result).start();
       	 	// 主线程阻塞一直等待结果返回
        	String resMsg = result.get();
    	}
    }
               

    通过实现Callable的方式启动线程必须将其实现通过参数传入FutureTask类的实例中,然后再经过创建一个Thread实例将FutureTask作为参数传入并启动来开启线程,那么这个FutureTask究竟是什么?为什么实现Callale开启线程和获取结果都需要经过他呢?

    下图是FutureTask的继承路径那么他是怎么实现有返回结果的呢?其大致流程如下:

    并发多线程之线程
    1. 通过Thread的start()方法启动线程,那么线程就会回调run()方法,而从图中我们可以看到FutureTask是Runnable的实现类,所以会调用FutureTaske类的run()方法;
    2. 在FutureTask的run()方法中,调用参数Callable的实现类的实例的call()方法,并存储在FutureTask的实例中;
    3. 通过FutureTask的get()方法获取Callable实例的call方法返回的结果。

线程的生命周期

线程的生命周期流程如下:

并发多线程之线程

接下来着重说几个方法

  1. wait() 该方法会让出cpu,同时也会释放锁;
  2. sleep() 让线程休眠一段时间,等到预计时间后再恢复执行,线程休眠会让线 程让出cpu的执行权,但不会释放锁,sleep会抛出 InterruptedException,还需要说明的是sleep睡眠的时间不一定就是参数指定的时间,因为在参数指定的时间到达后线程还需要争抢cpu的执行权,这也是需要耗费时间的;
  3. join() join会让线程让出cpu执行权,同时会释放锁,join就是对wait的一

    个封装,我们可以查看Thread类的join(long)方法,有一段关键代码如下:

    while (isAlive()) {
    	wait(0);
    }
               
    其意思就是当前线程存活,那么在被join的线程就一直wait直到join的线程执行完毕,join会抛出会抛出 InterruptedException异常;
  4. yield() 线程让步暂停执行当前的线程对象,并执行其他线程,yield会让当前 线程让出cpu,而不会释放锁。yield()方法无法控制具体交出CPU的时间,并且yield()方法只能让拥有相同优先级的线程有获取CPU的机会;
  5. interrupt() 、isInterrupted()和 interrupted()

    1、interrupt()方法只是将线程状态置为中断状态而已(中断状态为这是为true),它不会中断一个正在运行的线程,此方法只是给线程传递一个中断信号,程序可以根据此信号来判断是否需要终止。

    2、当线程中使用wait()、sleep()、join()导致此线程阻塞,则interrupt()会在线程中抛出InterruptException,并且将线程的中断状态由true置为false。

    3、isInterrupted()获取当前线程的中断标识

    4、interrupted() 获取当前线程的中断标识,并会将当前线程的中断标识设置为false,这是与isInterrupted()的区别

    接下来看几个demo。

    demo1:

    public class ThreadDemo extends Thread{
    
    	public static void main(String[] args) {
        	ThreadDemo demo = new ThreadDemo();
        	demo.start();
    	}
    
    	/**
     	* 运行结果
     	* true
     	* true
     	* false
     	*/
    	public void run() {
        	// 1、设置线程中断状态会true
        	Thread.currentThread().interrupt();
        	// 2、获取当前线程的中断状态,第1步已经设置为true了,所以此处是true
        	System.out.println(this.isInterrupted());
        	// 3、因为在第一步中断状态为true,所以此处为true,当时interrupted()会清除中断状态,
        	System.out.println(Thread.interrupted());
        	// 因为在第3步调用Thread.interrupted()所以此时中断转状态又变成了最开始的false
        	System.out.println(Thread.interrupted());
    	}
    }
               
    demo2:
    public class ThreadDemo2  extends Thread{
    
    	public static void main(String[] args) {
        	ThreadDemo2 demo2 = new ThreadDemo2();
        	demo2.start();
    	}
    
    	/**
     	* 运行结果
     	* true
     	* 异常信息...
     	* false
     	*/
    	@Override
    	public void run() {
        	// 将中断终态设置为true
        	this.interrupt();
        	// 查看当前线程的中断状态(只是查看,不修改)
        	System.out.println(this.isInterrupted());
        	try{
            	Thread.sleep(100);
        	} catch (Exception ex) {
            	ex.printStackTrace();
            	// sleep抛出异常并将中断状态修改为false
            	System.out.println(this.isInterrupted());
        	}
    	}
    }
               
    通过上面的例子我们应该可以更加深刻的了解Thread中的interrupt() 、isInterrupted()和 interrupted()三个方法的关系。

线程上下文切换及死锁

在开始之前我们先要了解为什么要用到并发:

并发编程的本质其实就是利用多线程技术,在现代多核的CPU的背景下,催生了并发编程的趋势,通过并发编程的形式可以将多核CPU的计算能力发挥到极致,性能得到提升。除此之外,面对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分 。即使是单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切 换线程执行,让我们感觉多个线程是同时执行的,时间片一般是几十毫秒(ms)。 并发不等于并行:并发指的是多个任务交替进行,而并行则是指真正意义上的“同时进行”。实际上,如果系统内只有一个CPU,而使用多线程时,那么真实系统环境下不能并行, 只能通过切换时间片的方式交替进行,而成为并发执行任务。真正的并行也只能出现在拥有多个CPU的系统中。

并发的优点:

  • 充分利用多核CPU的计算能力;
  • 方便进行业务拆分,提升应用性能;

当然有利就就有弊,引入并发同时也带来了响应的问题:

  1. 高并发下导致频繁的上下文切换;
  2. 临界区下产生安全问题,容易产生死锁导致系统不可用;
  3. 其他。

接下来我们对第1和2这个问题说明一下。

线程上下文切换

上面我们已经了解到线程是通过向cpu申请时间片从而从而获得cpu的执行权,下面我们假设有这么一种情况(不代表所有情况),线程A和线程B,如下图:

并发多线程之线程

这里线程A获取cpu分配的时间片开始执行,当执行一段时间(还未执行完),这时线程B获取到cpu分配的时间片, 为了保证线程A再次执行时可以从正确的位置,并且状态正确的执行,需要将线程A的当前状态保存,那么保存到哪里呢?保存到内核空间的一个叫Tss任务段的内存空间(操作内核空间当然是需要最高权限Ring0,这就是为什么线程上下文切换涉及到由用户态切换到内核态的原因),将线程A运行数据由寄存器保存到Tss任务端,然后加载线程A的指令等信息到寄存器执行线程B,当线程B执行完毕,这时线程A获取到cpu分配的时间片,那么需要从Tss任务段将直线保存的信息加载到寄存器继续执行线程A(这里又涉及到由用户态切换到内核态)。

从上面的流程我们大致了解了线程上下文切换的流程,那么为什么线程上下文的频繁切换会影响性能了吧,频繁的加载和保存数据也是会耗费cpu执行正常流程的时间的。

死锁

多线程还会导致死锁的发生,这里死锁是什么就不解释了,主要说一下最常见的死锁场景,如图:

并发多线程之线程
  1. 线程执行获取锁1;
  2. 线程B执行获取锁2;
  3. 线程A继续执行,要获取锁2,此时线程B持有锁2,那么线程A阻塞直到获取锁2;
  4. 线程B继续执行,要获取锁1,此时线程A持有锁1,那么线程B阻塞直到获取锁1;
  5. 两个线程都阻塞不会继续执行,那么两个线程会一直不释放持有的锁,那么会一直阻塞下去,产生死锁,新进来的线程如果要获取锁1或者锁2都会阻塞从而导致系统不可用。

看一以下代码:

public class DealLockTest {
    public static final String LOCK_A = "A";
    public static final String LOCK_B = "B";
    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            public void run() {
                synchronized (LOCK_A) {
                    try {
                        Thread.sleep(200);
                        System.out.println("获取A锁");
                    } catch (Exception ex) {
                    }
                    synchronized (LOCK_B) {
                        System.out.println("获取B锁");
                    }
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            public void run() {
                synchronized (LOCK_B) {
                    System.out.println("获取B锁");
                    synchronized (LOCK_A) {
                        System.out.println("获取B锁");
                    }
                }
            }
        });
        t1.start();
        t2.start();
    }
}
           

上面例子就是对死锁的复现,一般我们可以通过java提供的工具jps和jstack进行问题的定位,使用jps查找到启动的程序进程:

并发多线程之线程

然后使用jstack查看信息 jstack 8656,死锁信息如下:

并发多线程之线程