synchronized底层实现原理


synchronized

1、线程安全问题的主要诱因:

  • 存在共享数据(也称临界资源)
  • 存在多条线程共同操作这些共享数据

2、解决问题的根本方法:

  • 同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再对共享数据进行操作

互斥锁的特性

  • 互斥性:即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程的协调机制,这样在同一时间只有一个线程对需要同步的代码块(复合操作)进行访问。互斥性也称为操作的原子性。
  • 可见性:必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值) ,否则另一个线程可能是在本地缓存的某个副本上继续操作,从而
    引起不一致。

synchronized锁的不是代码,锁的都是对象

获取的锁的分类(获取对象锁和获取类锁)

获取对象锁的两种用法

  • 1.同步代码块( synchronized (this) , synchronized (类实例对象)) ,锁是小括号()中的实例对象。
  • 2.同步非静态方法 ( synchronized method ) , 锁是当前对象的实例对象。

获取类锁的两种用法

  • 1.同步代码块( synchronized (类.class) ) , 锁是小括号()中的类对象(Class对象)。
  • 2.同步静态方法 ( synchronized static method ) , 锁是当前对象的类对象(Class对象)。

对象锁和类锁的总结

  • 1.有线程访问对象的同步代码块时,另外的线程可以访问该对象的非同步代码块;
  • 2.若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象的同步代码块的线程会被阻塞;
  • 3.若锁住的是同一个对象.一个线程在访问对象的同步方法时,另一个访问对象同步方法的线程会被阻塞;
  • 4.若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象同步方法的线程会被阻塞,反之亦然;
  • 5.同一个类的不同对象的对象锁互不干扰;
  • 6.类锁由于也是一种特殊的对象锁,因此表现和上述1,2,3,4一致,而由于一个类只有一把对象锁,所以同一个类的不同对象使用类锁将会是同步的;
  • 7.类锁和对象锁互不干扰。

示例代码:

import java.text.SimpleDateFormat;
import java.util.Date;

public class SyncThread implements Runnable {

    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        if (threadName.startsWith("A")) {
            async();
        } else if (threadName.startsWith("B")) {
            syncObjectBlock1();
        } else if (threadName.startsWith("C")) {
            syncObjectMethod1();
        } else if (threadName.startsWith("D")) {
            syncClassBlock1();
        } else if (threadName.startsWith("E")) {
            syncClassMethod1();
        }

    }

    /**
     * 异步方法
     */
    private void async() {
        try {
            System.out.println(Thread.currentThread().getName() + "_Async_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + "_Async_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 方法中有 synchronized(this|object) {} 同步代码块
     */
    private void syncObjectBlock1() {
        System.out.println(Thread.currentThread().getName() + "_SyncObjectBlock1: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
        synchronized (this) {
            try {
                System.out.println(Thread.currentThread().getName() + "_SyncObjectBlock1_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getName() + "_SyncObjectBlock1_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * synchronized 修饰非静态方法
     */
    private synchronized void syncObjectMethod1() {
        System.out.println(Thread.currentThread().getName() + "_SyncObjectMethod1: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
        try {
            System.out.println(Thread.currentThread().getName() + "_SyncObjectMethod1_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + "_SyncObjectMethod1_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private void syncClassBlock1() {
        System.out.println(Thread.currentThread().getName() + "_SyncClassBlock1: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
        synchronized (SyncThread.class) {
            try {
                System.out.println(Thread.currentThread().getName() + "_SyncClassBlock1_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getName() + "_SyncClassBlock1_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private synchronized static void syncClassMethod1() {
        System.out.println(Thread.currentThread().getName() + "_SyncClassMethod1: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
        try {
            System.out.println(Thread.currentThread().getName() + "_SyncClassMethod1_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + "_SyncClassMethod1_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

主类:

public class SyncDemo {
    public static void main(String... args) {
        SyncThread syncThread = new SyncThread();
        Thread A_thread1 = new Thread(syncThread, "A_thread1");
        Thread A_thread2 = new Thread(syncThread, "A_thread2");
        Thread B_thread1 = new Thread(syncThread, "B_thread1");
        Thread B_thread2 = new Thread(syncThread, "B_thread2");
        Thread C_thread1 = new Thread(syncThread, "C_thread1");
        Thread C_thread2 = new Thread(syncThread, "C_thread2");
        Thread D_thread1 = new Thread(syncThread, "D_thread1");
        Thread D_thread2 = new Thread(syncThread, "D_thread2");
        Thread E_thread1 = new Thread(syncThread, "E_thread1");
        Thread E_thread2 = new Thread(syncThread, "E_thread2");
        A_thread1.start();
        A_thread2.start();
        B_thread1.start();
        B_thread2.start();
        C_thread1.start();
        C_thread2.start();
        D_thread1.start();
        D_thread2.start();
        E_thread1.start();
        E_thread2.start();
    }
}

运行结果:

B_thread2_SyncObjectBlock1: 10:26:34
D_thread2_SyncClassBlock1: 10:26:34
A_thread1_Async_Start: 10:26:34
E_thread1_SyncClassMethod1: 10:26:34
B_thread1_SyncObjectBlock1: 10:26:34
A_thread2_Async_Start: 10:26:34
D_thread1_SyncClassBlock1: 10:26:34
C_thread1_SyncObjectMethod1: 10:26:34
E_thread1_SyncClassMethod1_Start: 10:26:34
C_thread1_SyncObjectMethod1_Start: 10:26:34
E_thread1_SyncClassMethod1_End: 10:26:35
A_thread1_Async_End: 10:26:35
A_thread2_Async_End: 10:26:35
C_thread1_SyncObjectMethod1_End: 10:26:35
D_thread1_SyncClassBlock1_Start: 10:26:35
B_thread1_SyncObjectBlock1_Start: 10:26:35
D_thread1_SyncClassBlock1_End: 10:26:36
B_thread1_SyncObjectBlock1_End: 10:26:36
D_thread2_SyncClassBlock1_Start: 10:26:36
B_thread2_SyncObjectBlock1_Start: 10:26:36
B_thread2_SyncObjectBlock1_End: 10:26:37
D_thread2_SyncClassBlock1_End: 10:26:37
C_thread2_SyncObjectMethod1: 10:26:37
E_thread2_SyncClassMethod1: 10:26:37
C_thread2_SyncObjectMethod1_Start: 10:26:37
E_thread2_SyncClassMethod1_Start: 10:26:37
E_thread2_SyncClassMethod1_End: 10:26:38
C_thread2_SyncObjectMethod1_End: 10:26:38

synchronized底层实现原理

实现synchronized的基础

  • Java对象头
  • Monitor

对象在内存中的布局

  • 对象头
  • 实例数据
  • 对齐填充

对象头的结构:
**对象头的结构

Mark word
Mark word

Monitor :每个Java对象天生自带了一把看不见的锁

ObjectMonitor的对象列表:

源码地址:https://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/8f2780b3e4fa/src/share/vm/runtime/objectMonitor.hpp

源码

代码示例:

public class SyncBlockAndMethod {
    public void syncsTask() {
        synchronized (this) {
            System.out.println("Hello");
        }
    }
    public synchronized void syncTask() {
        System.out.println("Hello Again");
    }
}

使用以下命令对代码进行编译并查看字节码:

javac SyncBlockAndMethod.java
javap -verbose SyncBlockAndMethod.class

同步代码块

同步方法

什么是重入

从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入。

代码示例:

public void syncsTask() {
        synchronized (this) {
            System.out.println("Hello");
            synchronized (this){
                System.out.println("World");
            }
        }
    }

为什么会对synchronized嗤之以鼻

  • 早期版本中, synchronized属于重量级锁,依赖于Mutex Lock实现
  • 线程之间的切换需要从用户态转换到核心态,开销较大

Java6以后, synchronized性能得到了很大的提升

  • Adaptive Spinning 自适应自旋锁
  • Lightweight Locking 轻量级锁
  • Lock Eliminate 锁消除
  • Biased Locking 偏向锁
  • Lock Coarsening 锁粗化

自旋锁与自适应自旋锁 (Adaptive Spinning)

自旋锁

  • 许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得
  • 通过让线程执行忙循环等待锁的释放,不让出CPU
  • 缺点:若锁被其他线程长时间占用,会带来许多性能上的开销

自适应自旋锁

  • 自旋的次数不再固定
  • 由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定

锁消除

更彻底的优化

  • JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁

代码示例:

public class StringBufferWithoutSync {
    public void add(String str1, String str2) {
        //StringBuffer是线程安全,由于sb只会在append方法中使用,不可能被其他线程引用
        //因此sb属于不可能共享的资源,JVM会自动消除内部的锁
        StringBuffer sb = new StringBuffer();
        sb.append(str1).append(str2);
    }

    public static void main(String[] args) {
        StringBufferWithoutSync withoutSync = new StringBufferWithoutSync();
        for (int i = 0; i < 1000; i++) {
            withoutSync.add("aaa", "bbb");
        }
    }
}

StringBuffer源码:

锁粗化

  • 通过扩大加锁的范围,避免反复加锁和解锁

代码示例:

public class CoarseSync {
    public static String copyString100Times(String target){
        int i = 0;
        StringBuffer sb = new StringBuffer();
        while (i<100){
            sb.append(target);
        }
        return sb.toString();
    }
}

synchronized的四种状态

  • 无锁、偏向锁、轻量级锁、重量级锁.

锁膨胀方向:无锁→偏向锁-→轻量级锁-→重量级锁

偏向锁

减少同一线程获取锁的代价

  • 大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得

核心思想:
如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查Mark Word的锁标记位为偏向锁以及当前线程Id等于Mark Word的ThreadID即可,这样就省去了大量有关锁申请的操作。

缺点:不适用于锁竞争比较激烈的多线程场合

轻量级锁

轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁。

适应的场景:线程交替执行同步块

若存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁

轻量级锁的加锁过程:

(1)在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(LockRecord)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为Displaced Mark Word。这时候线程堆栈与对象头的状态如下图所示:

img

(2)拷贝对象头中的Mark Word复制到锁记录中。

(3)拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤4,否则执行步骤5。

(4)如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如下图所示:

(5)如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。而当前线程便尝试使用自旋来获取锁,自旋咱们前面讲过,就是为了不让线程阻塞,而采用循环去获取锁的过程。

轻量级锁的解锁过程:

  1. 通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word。
  2. 如果替换成功,整个同步过程就完成了。
  3. 如果替换失败,说明有其他线程尝试过获取该锁(此时锁己膨胀),那就要在释放锁的同时,唤醒被挂起的线程。

锁的内存语义

当线程释放锁时,Java内存模型会把该线程对应的本地内存中的共享变量刷新到主内存中;
而当线程获取锁时, Java内存模型会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。

偏向锁、轻量级锁、重量级锁的汇总

偏向锁、轻量级锁、重量级锁总结

synchronized和ReentrantLock的区别

ReentrantLock (再入锁)

  • 位于java.util.concurrent.locks包
  • 和CountDownLatch、Future Task、Semaphore一 样基于AQS实现
  • 能够实现比synchronized更细粒度的控制,如控制fairness
  • 调用lock()之后 ,必须调用unlock()释放锁
  • 性能未必比synchronized高,并且也是可重入的

ReentrantLock公平性的设置

  • ReentrantLock fairLock = new ReentrantLock(true);
  • 参数为true时,倾向于将锁赋予等待时间最久的线程
  • 公平锁:获取锁的顺序按先后调用lock方法的顺序(慎用)
  • 非公平锁:抢占的顺序不一定,看运气
  • synchronized是非公平锁

代码示例:

import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo implements  Runnable{
  // ReentrantLock公平性的设置
    private static ReentrantLock lock = new ReentrantLock(true);
    @Override
    public void run(){
        while (true){
            try{
                lock.lock();
                System.out.println(Thread.currentThread().getName() + " get lock");
                Thread.sleep(1000);
            } catch (Exception e){
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        ReentrantLockDemo rtld = new ReentrantLockDemo();
        Thread thread1 = new Thread(rtld);
        Thread thread2 = new Thread(rtld);
        thread1.start();
        thread2.start();
    }
}

打印结果:

Thread-0 get lock
Thread-1 get lock
Thread-0 get lock
Thread-1 get lock
...

ReentrantLock将锁对象化

  • 判断是否有线程,或者某个特定线程,在排队等待获取锁
  • 带超时的获取锁的尝试
  • 感知有没有成功获取锁

是否能将wait/notify/notifyAll对象化

  • java.util.concurrent.locks.Condition (典型的应用场景:ArrayBlockingQueue)

源码:

public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        this.items = new Object[capacity];
        lock = new ReentrantLock(fair);
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
    }

public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();
            return dequeue();
        } finally {
            lock.unlock();
        }

synchronized和ReentrantLock的区别总结

  • synchronized是关键字 , ReentrantLock是类
  • ReentrantLock可以对获取锁的等待时间进行设置,避免死锁
  • ReentrantLock 可以获取各种锁的信息
  • ReentrantLock可以灵活地实现多路通知
  • 机制: sync操作Mark Word , lock调用Unsafe类的park()方法

什么是Java内存模型中的happens-before

Java内存模型JMM

Java内存模型(即Java Memory Model ,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。

JMM中的主内存和工作内存

JMM中的主内存

  • 存储Java实例对象
  • 包括成员变量、类信息、常量、静态变量等
  • 属于数据共享的区域,多线程并发操作时会引发线程安全问题

JMM中的工作内存

  • 存储当前方法的所有本地变量信息,本地变量对其他线程不可见
  • 字节码行号指示器、Native方法信息
  • 属于线程私有数据区域,不存在线程安全问题

JMM与Java内存区域划分是不同的概念层次

  • JMM描述的是一组规则,围绕原子性,有序性、可见性展开
  • 相似点:存在共享区域(堆、方法区)和私有区域(程序计数器、虚拟机栈、本地方法栈)

主内存与工作内存的数据存储类型以及操作方式归纳

  • 方法里的基本数据类型本地变量将直接存储在工作内存的栈帧结构中
  • 引用类型的本地变量:引用存储在工作内存中,实例存储在主内存中
  • 成员变量、static变量、 类信息均会被存储在主内存中
  • 主内存共享的方式是线程各拷贝- -份数据到工作内存,操作完成后刷新回主内存

JMM如何解决可见性问题

Java内存模型与硬件内存架构的关系

优秀博客:https://blog.csdn.net/javazejian/article/details/72772461

指令重排序需要满足的条件

  • 在单线程环境下不能改变程序运行的结果

  • 存在数据依赖关系的不允许重排序

总结:无法通过happens- before原则推导出来的,才能进行指令的重排序

happens-before的八大原则

1.程序次序规则:

一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;

2.锁定规则:

一个unLock操作先行发生于后面对同一个锁的lock操作;

3.volatile变量规则:

对一个变量的写操作先行发生于后面对这个变量的读操作;

4.传递规则:

如果操作A先行发生于操作B ,而操作B又先行发生于操作C ,则可以得出操作A先行发生于操作C ;

5.线程启动规则:

Thread对象的start()方法先行发生于此线程的每一个动作;

6.线程中断规则:

对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;

7.线程终结规则:

线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join0方法结束、Thread.isAlive(的返回值手段检测到线程已经终止执行;

8.对象终结规则:

一个对象的初始化完成先行发生于他的finalize(方法的开始;

happens-before的概念

A操作的结果需要对B操作可见,则A与B存在happens-before关系

如果两个操作不满足上述任意一个happens-before规则 ,那么这两个操作就没有顺序的保障, JVM可以对这两个操作进行重排序;如果操作A happens-before操作B,那么操作A在内存上所做的操作对操作B都是可见的。

volatile

  • JVM提供的轻量级同步机制
  • 保证被volatile修饰的共享变量对所有线程总是可见的
  • 禁止指令重排序优化
  • 使用volatile不能保证线程安全,需要变量的操作满足原子性

volatile的可见性

volatile变量为何立即可见?

  • 当写一个volatile变量时,JMM会把该线程对应的工作内存中的共享变量值刷新到主内存中;
  • 当读取一个volatile变量时,JMM会把该线程对应的工作内存置为无效,那么就需要从主内存中重新读取该变量。

volatile变量如何禁止重排序优化

  • 对此我们需要先了解内存屏障(Memory Barrier),其作用有二个:

    1. 保证特定操作的执行顺序
    2. 保证某些变量的内存可见性
  • 通过插入内存屏障指令来禁止对内存屏障前后的指令执行重排序优化

  • 强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本

单例的双重检测实现

volatile和synchronized的区别

  1. volatile本质是在告诉JVM当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量, 只有当前线程可以访问该变量,其他线程被阻塞住直到该线程完成变量操作为止
  2. volatile仅能使用在变量级别; syr shronized则可以使用在变量、方法和类级别
  3. volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量修改的可见性和原子性
  4. volatile不会造成线程的阻塞; synchronized可能会造成线程的阻塞
  5. volatile标记的变量不会被编译器优化; synchronized标记的变量可以被编译器优化

CAS ( Compare and Swap )

一种高效实现线程安全性的方法

  • 支持原子更新操作,适用于计数器,序列发生器等场景
  • 属于乐观锁机制,号称lock-free
  • CAS操作失败时由开发者决定是继续尝试,还是执行别的操作

CAS思想

  • 包含三个操作数一 内存位置(V)、预期原值( A )和新值(B)

CAS多数情况下对开发者来说是透明的

  • J.U.C的atomic包提供了常用的原子性数据类型以及引用、数组等相关原子类型和更新操作工具,是很多线程安全程序的首选
  • Unsafe类虽提供CAS服务, 但因能够操纵任意内存地址读写而有隐患
  • Java9以后,可以使用Variable Handle API来替代Unsafe

缺点

  • 若循环时间长,则开销很大
  • 只能保证一个共享变量的原子操作
  • 存在ABA问题,可以通过使用AtomicStampedReference来解决,但由于是通过版本标记来解决所以存在一定程度的性能损耗

代码示例:

public class CASCase {
    public volatile int value;

    public synchronized void add() {
        value++;
    }
}

Java线程池

利用Executors创建不同的线程池满足不同场景的需求

  1. newFixedThreadPool(int nThreads)指定工作线程数量的线程池
  2. newCachedThreadPool()处理大量短时间工作任务的线程池,
    (1)试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;
    (2)如果线程闲置的时间超过阈值,则会被终止并移出缓存;
    (3)系统长时间闲置的时候,不会消耗什么资源
  3. newSingleThreadExecutor()创建唯一的工作者线程来执行任务,如果线程异常结束,会有另一个线程取代它
  4. newSingleThreadScheduledExecutor()与newScheduledThreadPool(int corePoolSize)
    定时或者周期性的工作调度,两者的区别在于单一工作线程还是多个线程
  5. newWorkStealingPool()内部会构建ForkJoinPool ,利用working-stealing算法,并行地处理任务,不保证处理顺序

Fork/Join框架(JDK7提供)

把大任务分割成若干个小任务并行执行,最终汇总每个小任务结果后得到大任务结果的框架

working-stealing算法:某个线程从其他线程的任务队列里窃取任务来执行

Fork/Join框架

为什么要使用线程池?

  1. 减低资源消耗,避免频繁地创建和销毁线程
  2. 提高线程的可管理性,例如可控的线程数量,线程状态的监控和统一创建/销毁线程

J.U.C的三个Executor接口:

  • Executor:运行新任务的简单接口,将任务提交和任务执行细节解耦
  • ExecutorService:具备管理执行器和任务生命周期的方法,提交任务机制更完善
  • ScheduleExecutorService:支持Future和定期执行任务

Executor框架

ThreadPoolExecutor:

线程池执行任务流程图

ThreadPoolExecutor的构造函数

  • corePoolSize :核心线程数量
  • maximumPoolSize :线程不够用时能够创建的最大线程数
  • workQueue :任务等待队列
  • keepAliveTime :抢占的顺序不一定 ,看运气
  • threadFactory :创建新线程, Executors .defaultThreadFactory()
  • handler :线程池的饱和策略
    (1)AbortPolicy :直接抛出异常,这是默认策略
    (2)CallerRunsPolicy :用调用者所在的线程来执行任务
    (3)DiscardOldestPolicy :丢弃队列中靠最前的任务,并执行当前任务
    (4)DiscardPolicy :直接丢弃任务
    (5)实现RejectedExecutionHandler接口的自定 义handler

新任务提交execute执行后的判断

  • 如果运行的线程少于corePoolSize ,则创建新线程来处理任务,即使线程池中的其他线程是空闲的;
  • 如果线程池中的线程数量大于等于corePoolSize且小于maximumPoolSize ,则只有当workQueue满时才创建新的线程去处理任务;
  • 如果设置的corePoolSize和maximumPoolSize相同,则创建的线程池的大小是固定的,这时如果有新任务提交,若workQueue未满,则将请求放入workQueue中,等待有空闲的线程去从workQueue中取任务并处理;
  • 如果运行的线程数量大于等于maximumPoolSize ,这时如果workQueue已经满了.则通过handler所指定的策略来处理任务;

execute执行流程图

线程池的状态

  • RUNNING:能接受新提交的任务,并且也能处理阻塞队列中的任务
  • SHUTDOWN:不再接受新提交的任务,但可以处理存量任务(调用shutdown方法)
  • STOP:不再接受新提交的任务,也不处理存量任务(调用shutdownNow方法)
  • TIDYING:所有的任务都已终止
  • TERMINATED:terminated() 方法执行完后进入该状态

线程池的状态转换过程

工作线程的生命周期:

线程池大小如何选定

  • CPU密集型任务:线程数 = 按照CPU核心数或者CPU核心数 + 1设定
  • I/O密集型任务:线程数 = CPU核心数 * (1 + 平均等待时间 / 平均工作时间)

可参考网站:http://www.ideabuffer.cn/2017/04/04/深入理解Java线程池:ThreadPoolExecutor/


评论
评论
  目录