Java线程及线程池浅析

本线程相关知识皆是基于jdk1.8版本。文章偶尔会提到之前版本,并且比较不一致的地方。

1.JVM线程

1.1 线程内存模型

java线程和进程之间额关系

java天生就是多线程的,多个线程共享堆和方法区,单个线程里面又有自己私有的虚拟机栈本地方法栈程序计数器

  • 虚拟机栈 每个java方法在执行的时候都会创建一个栈帧,用来存储局部变量、操作数栈、常量池引用等。从方法调用直至执行完成的过程,其实就是虚拟机栈不断入栈出栈的过程。虚拟机栈是线程私有的,会随着线程的创建而创建,随着线程的消亡而消亡。
  • 本地方法栈 本地方法栈和虚拟机栈一样,只是服务的方法不一样。本地方法栈是为Native方法而服务,虚拟机栈是为字节码(.class文件)而服务。 为了保证线程中的方法不被其他线程修改,所以本地方法栈也是线程私有的。
  • 程序计数器 程序计数器记录的是程序下一个指令的地址。 每一个线程执行到哪一个步骤,执行到哪一个指令,都是由程序计数器记录的。线程在切换回来之后会从程序计数器指向的指令开始执行。程序计数器是私有的,目的是为了保证程序能够顺利执行,线程切换之后能够回到原先执行的位置,而不被其他线程干扰。 程序计数器不会出现OOM错误,随线程创建而创建,消亡而消亡。
  • 堆和方法区 堆和方法区是线程共享的资源,堆是进程中最大的一块地址,主要存放着新创建的对象。方法区主要存放的是已被加载的类信息、常量、静态变量、即时编译后的代码等。

1.2 run()\call()\submit()\execute()方法

  • run() 方法是java.lang.Runable接口中; call() 方法在 java.util.concurrent.Callable接口中;submit() 方法是在 java.util.concurrent.ExecutorService接口中;execute() 方法是在java.util.concurrent.Executor 接口中;

  • run()\executor()方法不需要返回值,不需要抛出异常;call()\submit()方法有返回值,并且会抛出异常;

  • 在源码中,ExecutorService extends Executorabstract class AbstractExecutorService implements ExecutorService ,在AbstractExecutorService 有具体的submit() 方法的实现,并且调用了 execute()方法。如下代码所示:

    /**
     * @throws RejectedExecutionException {@inheritDoc}
     * @throws NullPointerException       {@inheritDoc}
     */
    public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }
    
    /**
     * @throws RejectedExecutionException {@inheritDoc}
     * @throws NullPointerException       {@inheritDoc}
     */
    public <T> Future<T> submit(Runnable task, T result) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task, result);
        execute(ftask);
        return ftask;
    }
    

2.线程中的几个关键字

2.1 synchronized关键字

synchronized 关键字解决的是多个线程之间访问资源同步性。被synchronized 修饰的方法或者代码块能够保证在任意时刻都只能够被一个线程访问。

在早期的jdk版本,synchronized 属于重量级锁,依赖于操作系统的锁机制实现(Mutex Lock),所以效率比较底下。如果要挂起或者唤醒一个线程,都需要依赖操作系统来实现,操作系统实现线程切换需要从用户态切换至内核态,这个切换需要较长时间,效率比较低。在jdk1.6之后,java对JVM的锁实现了大量优化, synchronized 的效率有了巨大的提升。jdk1.6引入了大量锁技术来优化,比如 自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁 等技术来减少锁操作的开销。

2.1.1 synchronized的使用方式

synchronized 可以修饰实例方法、静态方法、代码块。下面使用一个 双校验锁实现单例模式 的例子简单说明 synchronized 的使用方法。

/**
 * 双重校验锁实现单例模式。
 * 1.volatile使得单例实例不会被重排
 * 2.synchronized给对象加锁,保证多线程下都是一个实例
 */
public class SynSingleDemo {

    //私有变量
    //volatile的作用:使用volatile可以禁止JVM指令重排(按照下面的三步初始化实例),保证多线程下都能够正常运行
    //JVM具有指令重排的特性,多线程情况下可能会使得指令不按照单线程的顺序执行,从而出现空指针错误。
    private volatile static SynSingleDemo singleDemo;

    //私有的构造函数
    private SynSingleDemo(){}

    //非线程安全,在方法内部使用同步,实现线程安全
    public static SynSingleDemo getSingleDemo() {
        //没有对象,则实例化一个对象返回
        if(singleDemo == null){
            //给类对象加锁,多线程情况下都能确保获得的是一个实例
            synchronized (SynSingleDemo.class){
                if(singleDemo == null)
                    //分三步 1.分配内存空间给singleDemo
                    //2.实例化singleDemo
                    //3.将singleDemo指向分配的内存地址
                    singleDemo = new SynSingleDemo();
            }
        }
        return singleDemo;
    }
}

2.1.2 synchronized 的底层原理

  • 修饰代码块时 synchronized 同步代码块的实现使用的是monitorenter和monitorexit指令,monitorenter 指向同步代码块开始的位置,monitorexit 指向同步代码块结束的位置。
  • 修饰方法时 synchronized 修饰方法时使用的不是monitorenter和monitorexit指令,取而代之的是 ACC_SYNCHRONIZED标识 ,该标识指明了该方法是一个同步方法,JVM会根据该标识辨别方法是不是一个同步方法,进而执行相应的调用。

2.1.3 synchronized和ReenTrantLock的区别

  • 两者都是可重入锁 可重入锁:自己可以再次获取自己的内部锁。如果一个线程已经获取了某一个对象的锁,此时这个对象的锁还没有释放,当这个线程还想再次获取这个对象时,是可以直接获取的。如果不可重入的话,就会造成死锁。同一个线程每次获取锁、锁的计数器都会+1,当锁计数器为0时就会释放锁。

  • synchronized是在JVM中实现,ReenTrantLock是在API中实现。 synchronized 依赖于JVM实现,所有对 synchronized 的优化都是在JVM中;但是ReenTrantLock 是使用jdk API实现,即使用lock()、unlock()方法配合try/catch语句来实现,能够看源码的。

  • ReenTrantLock增加了一些高级功能:

    1.等待可中断机制 正在等待的线程可以放弃等待,去完成其他事情。使用lock.lockinterruptibty() 来实现。

    2.可实现公平锁 可以指定是公平锁还是非公平锁,默认是非公平的。公平锁是先等待的线程先获得锁。 synchronized 只能是非公平锁

    3.可实现选择通知 synchronized 结合wait()\notify()\notifyAll()方法可以实现等待\通知机制,如果执行notifyAll()方法时会通知所有出于等待的线程,会有很大的效率问题,不能够选择性通知。ReenTrantLock 可以实现选择性通知,通过结合Condition实例,线程对象与Condition实例相关联,从而实现可选择通知。

2.2 volatile关键字

volatile关键字的作用是保证变量的可见性,当变量值发生改变时,其他线程立刻可以看到变量的改变,避免变量出现脏读的现象。此外,还可以防止JVM指令重排。

  • volatile不能够用作线程安全计数器。线程安全计数器需要一原子方式执行,而volatile关键字不能保证原子性

2.2.1 并发编程的三个特性

  • 原子性 要么所有的操作全部得到执行并且不被中断干扰,要么所有的操作都不执行。synchronized 可以保证代码片段的原子性。
  • 可见性 当一个变量对共享变量做了修改,那么所有的其他线程也能够立马看见此共享变量的修改。volatile 可以保证共享变量的可见性。
  • 有序性 代码执行的过程,并不全是按照编码的顺序执行,它的执行顺序可能在JVM中因优化重排而被修改。volatile 可以保证代码不会被重排,保证顺序执行。

2.2.2 synchronized和volatile关键字的区别

synchronizedvolatile 两个关键字是互相补充的,并非对立存在。他们主要有以下区别:

  • synchronized 主要用于控制访问资源的同步性,主要用于修饰方法、代码块;volatile 只能用于修饰变量。
  • volatile 是轻量级锁,性能要好于 synchronizedvolatile 不会发生阻塞,而 synchronized 是阻塞的。
  • volatile 主要是用于只能够保证数据的可见性,不能够保证数据的原子性;synchronized 则都可以保证。

3.ThreadLocal

3.1 ThreadLocal简介

通常情况下,我们声明的变量是每一个线程都能够访问并且修改的,这在多线程情况下会很不安全。那么有没有一种技术可以使得各个线程拥有自己的本地变量,而不受其他线程的干扰呢?ThreadLocal就是用来定义多线程情况下,各个线程都能绑定自己的本地变量值,本地变量只会被当前线程修改,其他线程不会修改,从而达到线程安全的目的。

当创建了一个ThreadLocal 变量,那么这个变量在多线程情况下,都会在每个线程下面有一个副本,各个线程通过get()set() 方法能够操作本线程内的变量,而其他线程变量互不影响。

3.2 ThreadLocal原理

ThreadLocal 既然是通过 get()set() 方法操作当前线程各自的内部变量,那么通过这两个方法的源码看下 ThreadLocal 的内部原理是怎么实现的。

3.2.1 源码解析

ThreadLocal get()

//通过ThreadLocal对象调用此方法,以获取当前线程对应变量的值
public T get() {
        //获取当前线程
        Thread t = Thread.currentThread();
        //根据当前线程获取一个 ThreadLocalMap实例
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //从ThreadLocalMap中获取到对应变量的值
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

可以看到,当前线程是通过get() 方法获取到一个ThreadLocalMap 类的对象,并且从中获取到对应变量的值,由此可知,通过ThreadLocal声明的变量,数据都是维护在ThreadLocalMap中的,ThreadLocal实际上就是ThreadLocalMap类的封装类。下面通过源码看看ThreadLocalMap类在ThreadLocal中如何创建的:

getMap()和createMap()

/**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        //返回线程类中的ThreadLocalMap对象
        return t.threadLocals;
    }

    /**
     * Create the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the map
     */
    void createMap(Thread t, T firstValue) {
        //初始化线程类中的ThreadLocalMap对象
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

从代码中看到,ThreadLocalMap的创建或者获取,其实际上就是对当前线程中ThreadLocalMap类型的变量 threadLocals 的创建或者获取过程,即是:一个线程绑定了一个ThreadLocalMap对象,当前线程通过ThreadLocal类来操作ThreadLocalMap,从而实现线程的内部变量功能。下面再看看set() 方法的源码,看看是如何修改变量:

ThreadLocal set()

//修改变量值
public void set(T value) {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取当前线程对应的 ThreadLocalMap数据
        ThreadLocalMap map = getMap(t);
        //修改当前线程中的数据
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

可以看到,就是通过修改当前线程中ThreadLocalMap对象中的数据,达到修改当前线程变量的目的。

3.2.2 原理总结

通过源码可以知道,Thread 类拥有一个 ThreadLocalMap的成员变量,ThreadLocalMap 类是 ThreadLocal 类的静态内部类,ThreadLocalMap是通过一个Entry[]散列表来存放ThreadLocal变量ThreadLocal的弱引用作为key,对应变量的值作为value。 ThreadLocal中的方法实际上就是通过Thread类中的成员变量,对ThreadLocalMap做操作,从而达到线程变量隔离,互不影响的效果。

  • ThreadLocal通过牺牲空间来换取时间,通过每一个线程维护自己的变量,来实现 线程间数据隔离
  • synchronized是通过牺牲时间来换取空间,使用synchronized会有线程阻塞,性能不如使用ThreadLocal。
  • ThreadLocal线程隔离特性可以使用在多方面,比如数据库session、事务操作等。

3.2.3 ThreadLocal内存泄漏问题

ThreadLocalMap中的key值是ThreadLocal对象的弱引用,value就是强引用。如果ThreadLocal的引用没有被外部强引用调用的情况下,垃圾回收的时候会收回此key,但是value值是在当前线程内,且是强引用,所以不会被垃圾回收器回收,就有可能引发内存泄漏问题

解决办法:使用完ThreadLocal后,调用remove() 方法即可。

4.线程池

4.1 使用线程池的优点

  • 降低资源消耗。 通过重复利用已创建的线程,从而降低频繁创建线程所造成的的开销;
  • 提高响应速度。 任务到达时,从线程池里面拿即可立刻执行,减少创建线程消耗的时间;
  • 提高线程的可管理性。 对线程池统一监控、分配和调优。

4.2 创建线程池

方式一:通过 ThreadPoolExecutor 类的构造函数创建

方式二:通过 Executors 类中的方法创建

方式二有几个特别的线程池方法,可以构造不同的线程池:

  • FixedThreadPool:返回一个固定线程数量的线程池。当有任务来临时,若有空闲线程,则立即执行;若无空闲线程,则把任务存放在一个队列中,待有线程空闲之后执行。此任务队列默认是 Integer.MAX_VALUE 个,当等待任务过于多时,可能会因堆积过多请求而导致OOM错误
  • SingleThreadExecutor
  • CachedThreadPool

注意:通过方式二实际上也是调用方式一中的 ThreadPoolExecutor 中的构造函数创建。通常情况下,我们都是通过 ThreadPoolExecutor类来创建线程池,通过此类中构造函数的参数,可以创建符合自己需求的线程池,使得我们更加明白线程池的运行规则,避免资源耗尽的风险。

4.3 ThreadPoolExecutor类

ThreadPoolExecutor 有多个构造函数,但是其他几个构造函数都是调用的下面的构造函数,所以分析下面这个构造函数,即可明白其余构造函数了:

//创建线程池的总的一个构造函数
public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.acc = System.getSecurityManager() == null ?
        null :
    AccessController.getContext();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

4.3.1 构造函数参数说明

  • corePoolSize:线程池中保留的最小线程个数。
  • maximumPoolSize:线程池中最大线程个数。
  • keepAliveTime:空闲线程等待新任务的最大时间,超过这个时间则线程就会被销毁。当然,线程池中依然会保留 corePoolSize 个线程数,即使是空闲线程。
  • unitkeepAliveTime 参数的时间单位。
  • workQueue:任务队列。新来的任务如果没有空闲线程可以执行,则存放在此队列中,直到有新的空闲线程可以执行此任务。
  • threadFactory:创建线程的工厂。
  • handler拒绝策略。某些文档中也称作饱和策略。

4.3.2 拒绝策略

拒绝策略触发场景:

image-20200901113322643

总结来说,当当前任务>(maxPoolSize + workQueue长度),即触发拒绝策略。

ThreadPoolExecutor 中内置了几个拒绝策略:

  • abortPolicy中止策略:线程池的默认策略。直接抛出拒绝执行的异常,打断当前执行流程。在使用这个策略的时候,需要正确处理抛出的异常。
  • CallerRunsPolicy 调用者运行策略:只要线程池没有关闭,就由提交当前任务的线程执行此任务。这种策略一般在不允许失败、并发量小、性能要求不高的场景下使用。
  • DiscardPolicy 丢弃策略:这个策略有一个空实现,直接悄悄丢掉提交的任务。只有当提交的任务无关紧要时,才会使用这个策略。
  • DiscardOdestPolicy 弃老策略:只要线程池未关闭,就尝试执行任务队列头部的任务。这个策略实际上也是丢弃策略,区别就是丢弃老的未执行的任务,而且是待执行优先较高的任务。

5. AQS

5.1 什么是AQS

AQS全称是 java.util.concurrent.locks.AbstractQueuedSynchronizer 即抽象队列同步器。AQS是用来构建锁和同步器的框架,使用AQS可以简单且高效的构造出大量应用广泛的同步器,我们常见的 ReenTranLock、Semaphore 等。当然,我们自己也能够利用AQS构建符合我们自己需求的锁和同步器。

5.2 AQS实现原理

AQS的核心思想是,如果被请求的资源空闲,那么将当前请求资源的线程设置为工作线程;如果请求的资源被占用,那么就把当前线程放到一个先进先出(FIFO)的双向队列中去。

image-20200901174508726

  • AQS是通过内置的FIFO双向队列来完成线程的排队工作(源码中通过节点head和tail记录队首和队尾元素,元素的节点类型为Node类型)。

  • AQS定义了两种资源共享方式。

    1)Exclusive(独占) :只有一个线程能够执行,如 ReenTranLock, 又分成公平锁和非公平锁:公平锁:按照队列顺序执行;非公平锁 无视队列顺序直接抢锁,谁抢到是谁的。

    2)Shared(共享) :多个线程共享,可同时执行。如 Semaphore、CountDownLatch、ReadWriteLock 等。

  • AQS底层使用了模板方法模式(模板方法模式很经典的一个应用)。如果需要自定义同步器,只需要继承此类并重写指定的方法,并且将AQS组合在自定义同步组件中,并调用其模板方法,这些模板方法会调用使用者重写的方法。

posted @ 2021-12-07 11:34  lavendor  阅读(130)  评论(0)    收藏  举报