并发编程线程基础.md

PunkLu 2019年08月22日 227次浏览

上下文切换

单核CPU也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片很短,CPU不停切换线程执行,才会让我们感觉到多个线程是同时执行。

CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。

上下文切换会影响多线程的执行速度。

串行并不一定比并行慢,当数据量很大时,因为并行有频繁的上下文切换,可能导致并行速度比串行要慢(ConcurrencyTest类)。

减少上下文切换

减少上下文切换的方法有无锁并发编程、CAS算法、使用最少线程和使用协程。

1、无锁并发编程
    多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。
2、CAS算法
    Java的Atomic包使用CAS算法来更新数据,而不需要加锁。
3、使用最少线程
    尽量避免不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。
4、协程
    在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

减少上下文切换实战

通过减少大量waiting的线程,来减少上下文切换次数
1、用jstack命令dump线程信息,看看pid为3117的进程里的线程都在做什么
    sudo -u admin /usr/local/java/binjstack 3117 > /usr/local/javalogs/dump17
2、统计所有线程分别处于什么状态,onobject-monitor状态为waiting状态
    grep java.lang.Thread.State dump17 | awk '{print $2$3$4$5}' | sort | uniq -c
3、打开dump文件查看处于WAITING(onobject-monitor)的线程在做什么,发现这些线程全是JBOSS的工作线程,在await,说明JBOSS线程池里接收到的任务太少,大量线程都闲着。
4、减少JBOSS的工作线程数,找到JBOSS的线程池配置信息,将maxThreads降到100。
5、重启JBOSS,再dump线程信息,再统计WAITING的线程,发现减少了175个。WAITING的线程少了,系统上下文切换到次数就会少,因为每一次从WAITING到RUNNABLE都会进行一次上下文的切换。

死锁

代码见DeadLockDemo。
避免死锁的几个常见方法:
1、避免一个线程同时获取多个锁
2、避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
3、尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制
4、对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况

假如线程必须获取多个资源,为了避免死锁,可以通过资源分配有序性来避免。也就是说假如线程A和线程B都需要资源1,2,3....n时,对资源进行排序,线程A和线程B只有在获取了资源n-1时才能去获取资源n。

线程

线程是进程中的一个实体,线程本身是不会独立存在的。进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,线程则是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程
共享进程的资源。

操作系统在分配资源时是把资源分配给进程的,但是CPU资源比较特殊,它是被分配到线程的,因为真正要占用CPU运行的是线程,所以也说线程是CPU分配的基本单位。

在Java中,当启动main函数时其实就启动了一个JVM的进程,而main函数所在的线程就是这个进程中的一个线程,也称主线程。

一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈区域。

程序计数器是一块内存区域,用来记录线程当前要执行的指令地址。线程是占用CPU执行的基本单位,而CPU一般是使用时间片轮转方式让线程轮询占用的,所以当前线程CPU时间片用完后,要让出CPU,等下次轮到自己的时候再执行。
程序计数器就是为了记录该线程让出CPU时的执行地址的,待再次分配到时间片时线程就可以从自己私有的计数器指定地址继续执行。需要注意的是,如果执行的是native方法,那么pc计数器记录的是undefined地址,只有执行的是
Java代码时pc计数器记录的才是下一条指令的地址。

另外每个线程都有自己的栈资源,用于存储该线程的局部变量,这些局部变量是该线程私有的,其他线程是访问不了的,除此之外栈还用来存放线程的调用栈帧。

堆是一个进程中最大的一块内存,堆是被进程中的所有线程共享的,是进程创建时分配的,堆里面主要存放使用new操作创建的对象实例。

方法区则用来存放JVM加载的类、常量及静态变量等信息,也是线程共享的。

线程通知与等待

wait()函数
    当一个线程调用一个共享变量的wait()方法时,该调用线程会被阻塞挂起,直到发生下面几件事情之一才返回:
    1、其他线程调用了该共享对象的notify()或者notifyAll()方法
    2、其他线程调用了该线程的interrupt()方法,该线程抛出InterruptException异常返回
    
需要注意的是,如果调用wait()方法的线程没有事先获取该对象的监视器锁,则调用wait()方法时调用线程会抛出IllegalMonitorStateException异常。

线程获取一个共享变量的监视器锁:
1、执行synchronized同步代码块时,使用该共享变量作为参数。
    synchronized (共享变量){
        // dosomething
    }
2、调用该共享变量的方法,并且该方法使用了synchronized修饰
    synchronized void add(int a,int b){
        // doSomething
    }
    
需要注意的是,一个线程可以从挂起状态变为可以运行状态(也就是被唤醒),即使该线程没有被其他线程调用notify()、notifyAll()方法进行通知,或者被中断,或者等待超时,这就是所谓的虚假唤醒。

虚假唤醒在应用实践中很少发生,但要防患于未然,做法就是不停地去测试该线程被唤醒的条件是否满足,不满足则继续等待,也就是说在一个循环中调用wait()方法进行防范。退出循环的条件是满足了唤醒该线程的条件。

synchronized (obj){
    while(条件不满足){
        obj.wait();
    }
}

wait(long timeout)函数
    该方法相比wait()方法多了一个超时参数,它的不同之处在于,如果一个线程调用共享对象的该方法挂起后,没有在指定的timeout ms时间内被其他线程调用该共享变量的notify()或者notifyAll()方法唤醒,
    那么该函数还是会因为超时而返回。如果将timeout设置为0则和wait()方法效果一样,因为在wait方法内部就是调用了wait(0)。

notify()函数
    一个线程调用共享对象的notify()方法后,会唤醒一个在该共享变量上调用wait()系列方法被挂起后的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。
    
    此外,被唤醒的线程不能马上从wait方法返回并继续执行,它必须在获取了共享对象的监视器锁后才可以返回,也就是唤醒它的线程释放了共享变量上的监视器锁后,被唤醒的线程也不一定会获取到共享对象的
    监视器锁,这是因为该线程还需要和其他线程一起竞争该锁,只有该线程竞争到了共享变量的监视器锁后才可以继续执行。
    
    类似wait系列方法,只有当前线程获取到了共享变量的监视器锁后,才可以调用共享变量的notify()方法,否则会抛出异常。

notifyAll()函数
    notifyAll()方法会唤醒所有在该共享变量上由于调用wait系列方法而被挂起的线程。

让线程睡眠的sleep方法: Thread类中有一个静态的sleep方法,当一个执行中的线程调用了Thread的sleep方法后,调用线程会暂时让出指定时间的执行权,也就是在这期间不参与CPU的调度,但是该线程所拥有的监视器资源,比如锁还是持有不让出的。 指定的睡眠时间到了后该函数会正常返回,线程处于就绪状态,然后参与CPU的调度,获取到CPU资源后就可以继续运行了。

示例代码见SleepDemo类。

让出CPU执行权的yield方法: Thread类中有一个静态的yield方法,当一个线程调用yield方法时,实际就是在暗示线程调度器当前线程请求让出自己的CPU使用,但是线程调度器可以无条件忽略这个暗示。 操作系统是为每个线程分配一个时间片来占有CPU的,正常情况下当一个线程把分配给自己的时间片使用完后,线程调度器才会进行下一轮的线程调度,而当一个线程调用了Thread类的静态方法yield时,是在告诉线程调度器自己 占有的时间片中还没有使用完的部分自己不想使用了,这暗示线程调度器现在就可以进行下一轮的线程调度。

当一个线程调用yield方法时,当前线程会让出CPU使用权,然后处于就绪状态,线程调度器会从县城就绪队列里面获取一个线程优先级最高的线程,当然也有可能调度到刚让出CPU的线程。

示例代码见YieldDemo类。

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

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

boolean isInterrupted()方法:检测当前线程是否被中断,如果是返回true,否则返回false。

boolean interrupted()方法:检测当前线程是否被中断,如果是返回true,否则返回false。与isInterrupted()不同的是,该方法如果发现当前线程被中断,则会清除中断标志,
并且该方法是static方法,可以通过Thread类直接调用。

interrupt()方法的妙用:
    当线程为了等待一些特定条件的到来时,一般会调用sleep函数、wait()系列函数或者join()函数来阻塞挂起当前线程。比如一个线程调用了Thread.sleep(3000),那么调用线程会被阻塞,直到3s后才会从阻塞状态变为激活状态。
    但是有可能在3s内条件已被满足,如果一直等到3s后再返回有点浪费时间,这时候可以调用该线程的interrupt方法,强制sleep方法抛出InterruptedException异常而返回,线程恢复到激活状态。
    
    示例代码见InterruptDemo类。

守护线程与用户线程: Java中的线程分为两类,分别为daemon线程(守护线程)和user线程(用户线程)。在JVM启动时会调用main函数,main函数所在的线程就是一个用户线程,其实在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即可。

main函数运行结束后,JVM会自动启动一个叫作DestroyJavaVM的线程,该进程会等待所有用户线程结束后终止JVM进程。

ThreadLocal 多线程访问同一个共享变量时特别容易出现并发问题,特别是在多个线程需要对一个共享变量进行写入时。为了保证线程安全,一般使用者在访问共享变量时需要进行适当的同步。

同步的措施一般是加锁,这就需要使用者对锁有一定的了解。一个替代方案是通过ThreadLocal实现当创建一个变量后,每个线程对其进行访问的时候访问的是自己线程的变量。

ThreadLocal是JDK包提供的,它提供了线程本地变量,也就是如果创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面的
变量,从而避免了线程安全问题。创建一个ThreadLocal变量后,每个线程都会复制一个变量到自己的本地内存。

Thread类中有一个threadLocals和一个inheritableThreadLocals,它们都是ThreadLocalMap类型的b变量,而ThreadLocalMap是一个定制化的Hashmap。在默认情况下,每个线程中的这两个变量都为null,只有当前线程第一次调用
ThreadLocal的set或者get方法时才会创建它们。其实每个线程的本地变量不是存放在ThreadLocal实例里面,而是存放在调用线程的threadLocals变量里面。也就是说,ThreadLocal类型的本地变量存放在具体的线程内存空间中。
ThreadLocal就是一个工具类,它通过set方法把value值放入调用线程的threadLocals里面并存放起来,当调用线程调用它的get方法时,再从当前线程的threadLocals变量里面将其拿出来使用。如果调用线程一直不终止,那么这个本地变量
会一直存放在调用线程的threadLocals变量里面,所以当不需要使用本地变量时可以通过调用ThreadLocal变量的remove方法,从当前线程的threadLocals里面删除该本地变量。另外,每个线程可以关联多个ThreadLocal变量,所以Thread类
中的threadLocals被设计为map结构。

需要特别注意的是,ThreadLocal不支持继承性,也就是说,同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的。因为在子线程中调用ThreadLocal时,实际上是在子线程的ThreadLocal中调用,而不是父线程的ThreadLocal。
如果想要子线程能访问到父线程中的值,可以使用InheritableThreadLocal类。

InheritableThreadLocal: InheritableThreadLocal继承自ThreadLocal,其提供了一个特性,就是让子线程可以访问在父线程中设置的本地变量。

在使用InheritableThreadLocal时,通过声明ThreadLocal具体类型为InheritableThreadLocal类型即可实现子线程可访问父线程的ThreadLocal值。

public static ThreadLocal<String> threadLocal = new InheritableThreadLocal<String>();

需要子线程可以获取父线程ThreadLocal变量的情况:
1、子线程需要使用存放在threadLocal变量中的用户登录信息
2、一些中间件需要把统一的id追踪的整个调用链路记录下来