Java线程池以及资源同步

·  阅读 379

ThreadPoolExecutor

属性字段

  • volatile int corePoolSize

    • 线程池的基本大小,即在没有任务需要执行的时候线程池的大小,并且只有在工作队列满了的情况下才会创建超出这个数量的线程
    • 在刚创建ThreadPoolExecutor的时候,线程并不会立即启动,而是要等到有任务提交时才会启动,除非调用prestartCoreThread/prestartAllCoreThreads方法,才会事先启动核心线程
  • volatile int maximumPoolSize

    • 线程池中允许的最大线程数,线程池中的当前线程数目不会超过该值
    • 线程池不区分核心线程和非核心线程,线程池是期望达到corePoolSize的并发状态,并允许在不得已情况下超载,达到corePoolSize ~maximumPoolSize 的并发状态
  • volatile long keepAliveTime

    • keepAliveTime是线程池中空闲线程等待工作的超时时间
    • 当线程池中线程数量大于corePoolSize(核心线程数量)或设置了allowCoreThreadTimeOut(是否允许空闲核心线程超时)时,线程会根据keepAliveTime的值进行活性检查,一旦超时便销毁线程。
    • 当keepAliveTime设置为0,即直接不等待退出
  • final BlockingQueue workQueue

    • 用于放置未执行任务的队列
    • 它一般分为直接提交队列、有界任务队列、无界任务队列、优先任务队列几种
    • 队列的实现有多种,如下:
      • SynchronousQueue/直接提交队列
        • 是一个特殊的BlockingQueue,它没有容量,每执行一个插入操作就会阻塞,需要再执行一个删除操作才会被唤醒,反之每一个删除操作也都要等待对应的插入操作。
        • 提交的任务不会被保存,总是会马上提交执行。如果用于执行任务的线程数量小于maximumPoolSize,则尝试创建新的进程,如果达到maximumPoolSize设置的最大值,则根据你设置的handler执行拒绝策略。
        • 这种方式你提交的任务不会被缓存起来,而是会被马上执行,在这种情况下,你需要对你程序的并发量有个准确的评估,才能设置合适的maximumPoolSize数量,否则很容易就会执行拒绝策略
      • ArrayBlockingQueue/有界的任务队列
        • 使用ArrayBlockingQueue有界任务队列,若有新的任务需要执行时,线程池会创建新的线程,直到创建的线程数量达到corePoolSize时,则会将新的任务加入到等待队列中。若等待队列已满,即超过ArrayBlockingQueue初始化的容量,则继续创建线程,直到线程数量达到maximumPoolSize设置的最大线程数量,若大于maximumPoolSize,则执行拒绝策略。
        • 在这种情况下,线程数量的上限与有界任务队列的状态有直接关系,如果有界队列初始容量较大或者没有达到超负荷的状态,线程数将一直维持在corePoolSize以下,反之当任务队列已满时,则会以maximumPoolSize为最大线程数上限。
      • LinkedBlockingQueue/无界的任务队列
        • 使用无界任务队列,线程池的任务队列可以无限制的添加新的任务,而线程池创建的最大线程数量就是你corePoolSize设置的数量,也就是说在这种情况下maximumPoolSize这个参数是无效的,哪怕你的任务队列中缓存了很多未执行的任务,当线程池的线程数达到corePoolSize后,就不会再增加了;若后续有新的任务加入,则直接进入队列等待,当使用这种任务队列模式时,一定要注意你任务提交与处理之间的协调与控制,不然会出现队列中的任务由于无法及时处理导致一直增长,直到最后资源耗尽的问题。
      • PriorityBlockingQueue/优先任务队列
        • PriorityBlockingQueue它其实是一个特殊的无界队列,它其中无论添加了多少个任务,线程池创建的线程数也不会超过corePoolSize的数量,只不过其他队列一般是按照先进先出的规则处理任务,而PriorityBlockingQueue队列可以自定义规则根据任务的优先级顺序先后执行。
      • BlockingDeque
      • DelayQueue
      • LinkedBlockingDeque
      • LinkedTransferQueue
      • TransferQueue
  • volatile ThreadFactory threadFactory

    • 用于生产线程的工厂
  • volatile RejectedExecutionHandler handler

    • 拒绝策略
    • 创建线程池时,为防止资源被耗尽,任务队列一般都会选择创建有界任务队列,但种模式下如果出现任务队列已满且线程池创建的线程数达到你设置的最大线程数时,这时就需要你指定RejectedExecutionHandler参数即合理的拒绝策略,来处理线程池"超载"的情况
    • 默认的拒绝策略如下:
      • AbortPolicy策略:该策略会直接抛出异常,阻止系统正常工作
      • CallerRunsPolicy策略:如果线程池的线程数量达到上限,该策略会把任务队列中的任务放在调用者线程当中运行
      • DiscardOledestPolicy策略:该策略会丢弃任务队列中最老的一个任务,也就是当前任务队列中最先被添加进去的,马上要被执行的那个任务,并尝试再次提交
      • DiscardPolicy策略:该策略会默默丢弃无法处理的任务,不予任何处理。如果使用此策略,业务场景中需允许任务的丢失
  • final ReentrantLock mainLock

    • 线程池的主锁,是可重入锁,当要操作workers set这个保持线程的HashSet时,需要先获取mainLock,还有当要处理largestPoolSize、completedTaskCount这类统计数据时需要先获取mainLock
  • final HashSet workers

    • 是Worker里的thread属性是具体维护的线程,每个Worker对象一个,这个就是用来执行任务用的线程,也就是说,Worker对象的数量也就代表了线程池中活动线程的数量。
  • final Condition termination

    • 配合来关闭线程池的属性,
  • int largestPoolSize

    • 变量记录了线程池在整个生命周期中曾经出现的最大线程个数。在线程池创建之后,可以调用setMaximumPoolSize()改变运行的最大线程的数目。
  • long completedTaskCount

    • 完成任务的数量
  • volatile boolean allowCoreThreadTimeOut

    • 核心线程在allowCoreThreadTimeout被设置为true时会超时退出,默认情况下不会退出
  • static final RejectedExecutionHandler defaultHandler

    • 默认拒绝策略,实现是AbortPolicy。
  • static final RuntimePermission shutdownPerm

    • 运行时权限处理

执行过程

  1. 核心线程数为2,最大线程数为5,超时时间为60,未执行队列容量为4

thread-pool-executor-001.png

  1. 三个任务进入线程池

thread-pool-executor-002.png

  1. t1,t2进入线程执行,t3进入队列等待

thread-pool-executor-003.png

  1. 此时t4,t5,t6三个任务进入线程池

thread-pool-executor-004.png

  1. t4,t5,t6也进入队列等待

thread-pool-executor-005.png

  1. t7,t8,t9三个任务进入线程池

thread-pool-executor-006.png

  1. 无线程可用,此时创建三个线程,t3,t4,t5进入线程执行任务,t7,t8,t9进入队列等待

thread-pool-executor-007.png

  1. t10准备进入线程池,但是已经超出最大限制,则执行拒绝策略。

thread-pool-executor-008.png 9. 经过一段时间,队列里的任务都获得到了线程,并执行。

thread-pool-executor-009.png

  1. 经过一段时间,等待时间超过了60秒,非核心线程以外的线程被回收。

thread-pool-executor-010.png

  1. 无任务执行,恢复初始状态。

thread-pool-executor-001.png

构造方法

	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.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
复制代码

execute()方法

 	public void execute(Runnable command) {
        //为空则抛出异常
        if (command == null)
            throw new NullPointerException();
        //获取ctl
        int c = ctl.get();
        //如果工作线程数小于核心线程数
        if (workerCountOf(c) < corePoolSize) {
            //执行addworker,创建一个核心线程
            if (addWorker(command, true))
                return;
            //创建失败重新获取ctl
            c = ctl.get();
        }
        //如果线程池处于运行状态并且工作线程数大于核心线程数
        if (isRunning(c) && workQueue.offer(command)) {
            //再次获取ctl,用于双重验证
            int recheck = ctl.get();
            //如果重新检查的线程池不处于运行状态,就从队列中移除任务
            if (!isRunning(recheck) && remove(command))
                //移除成功后会根据传入的拒绝策略处理拒绝,因为线程池已经不可用了。
                reject(command);
            //判断工作线程是否为0
            else if (workerCountOf(recheck) == 0)
                //创建非核心线程
                addWorker(null, false);
        }
        //如果线程池被回收或者大于最大线程数
        else if (!addWorker(command, false))
            //执行拒绝策略
            reject(command);
    }
复制代码

ThreadPoolExecutor.execute().png

CTL

ctl位操作变量

  • ThreadPoolExecutor有一个AtomicInteger变量,叫ctl(control的简写),一共32位
  • 高3位为线程池的状态runstatus(5中:Running,Shutdown,Stop,Tidying,Terminate),
  • 低29位存当前有效线程数workerCount
    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    private static final int COUNT_BITS = Integer.SIZE - 3;//COUNT_BITS=32
    private static final int CAPACITY   = (1 << COUNT_BITS) - 1;//高三位000,低29位全为1
    private static final int RUNNING    = -1 << COUNT_BITS;//高三位111,低29位全为0
    private static final int SHUTDOWN   =  0 << COUNT_BITS;//高三位000,低29位全为0
    private static final int STOP       =  1 << COUNT_BITS;//高三位001,低29位全为0
    private static final int TIDYING    =  2 << COUNT_BITS;//高三位010,低29位全为0
    private static final int TERMINATED =  3 << COUNT_BITS;//高三位011,低29位全为0
复制代码

线程交互

wait

wait是Object的方法,意味所有的Java类都可以调用

流程如下:

  1. wait是在当前线程持有wait锁的情况下,暂时放弃锁,并让出CPU资源

  2. 积极等待其他线程调用同一对象的notify或者notifyAll方法后,等待线程被激活

  3. 直到其他线程释放锁之后,等待线程才会继续执行

    注意:wait方法需要释放锁,前提条件是它已经持有锁。所以wait和notify(或者notifyAll)方法都必须被包裹在synchronized语句块中,并且synchronized后锁的对象应该与调用wait方法的对象一样。否则抛出IllegalMonitorStateException


sleep

sleep是Thread的静态方法

流程如下:

  1. sleep方法告诉操作系统至少指定时间内不需为线程调度器为该线程分配执行时间片

  2. 并不释放锁(如果当前已经持有锁),实际上,调用sleep方法时并不要求持有任何锁

    注意:sleep方法并不需要持有任何形式的锁,也就不需要包裹在synchronized中


多线程策略

原子性

  • 一个操作(有可能包含有多个子操作)要么全部执行(生效),要么全部都不执行(都不生效)

实现方式如下

  • 使用锁可以实现在同一时间只有一个线程可以拿到锁,也就保证了同一时间只有一个线程能执行申请锁和释放锁的方法
公平锁|非公平锁
  • 公平锁是指多个线程获取锁被阻塞的情况下,锁变为可用时,最新申请锁的线程获得锁。可通过在重入锁(RenentrantLock)的构造方法中传入true构建公平锁,如Lock lock = new RenentrantLock(true)
  • 非公平锁是指多个线程等待锁的情况下,锁变为可用状态时,哪个线程获得锁是随机的。synchonized相当于非公平锁。可通过在重入锁的构造方法中传入false或者使用无参构造方法构建非公平锁。
ReentrantLock(重用锁)
  • Java语言直接提供了synchronized关键字用于加锁,但这种锁一是很重,二是获取时必须一直等待,没有额外的尝试机制。java.util.concurrent.locks包提供的ReentrantLock用于替代synchronized加锁
  • 如果重入锁已经被其它线程持有,则当前线程的lock操作会被阻塞。除了*lock()*方法之外,重入锁(或者说锁接口)还提供了其它获取锁的方法以实现不同的效果。
    • lockInterruptibly()该方法尝试获取锁,若获取成功立即返回;若获取不成功则阻塞等待。与lock方法不同的是,在阻塞期间,如果当前线程被打断(interrupt)则该方法抛出InterruptedException。该方法提供了一种解除死锁的途径。
    • tryLock() 该方法试图获取锁,若该锁当前可用,则该方法立即获得锁并立即返回true;若锁当前不可用,则立即返回false。该方法不会阻塞,并提供给用户对于成功获利锁与获取锁失败进行不同操作的可能性。
    • tryLock(long time, TimeUnit unit) 该方法试图获得锁,若该锁当前可用,则立即获得锁并立即返回true。若锁当前不可用,则等待相应的时间(由该方法的两个参数决定):
      • 1)若该时间内锁可用,则获得锁,并返回true;
      • 2)若等待期间当前线程被打断,则抛出InterruptedException
      • 3)若等待时间结束仍未获得锁,则返回false。

重入锁可定义为公平锁或非公平锁,默认实现为非公平锁。

public class Counter {
    private int count;
    public void add(int n) {
        //synchronized代码块
        synchronized(this) {
            count += n;
        }
    }
}
public class Counter {
    //ReentrantLock实现
    private final Lock lock = new ReentrantLock();
    private int count;
    public void add(int n) {
        lock.lock();
        try {
            count += n;
        } finally {
            lock.unlock();
        }
    }
}
//获取锁时等待一秒,获取不到返回flase,而不是无限等待
if (lock.tryLock(1, TimeUnit.SECONDS)) {
    try {
        ...
    } finally {
        lock.unlock();
    }
}
复制代码

ReadWriteLock(悲观锁)
  • 允许多个线程同时读,但只要有一个线程在写,其他线程就必须等待。

  • 如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即读的过程中不允许写,这是一种悲观的读锁。

public class Counter {
    private final ReadWriteLock rwlock = new ReentrantReadWriteLock();
    private final Lock rlock = rwlock.readLock();
    private final Lock wlock = rwlock.writeLock();
    private int[] counts = new int[10];

    public void inc(int index) {
        wlock.lock(); // 加写锁
        try {
            counts[index] += 1;
        } finally {
            wlock.unlock(); // 释放写锁
        }
    }

    public int[] get() {
        rlock.lock(); // 加读锁
        try {
            return Arrays.copyOf(counts, counts.length);
        } finally {
            rlock.unlock(); // 释放读锁
        }
    }
}
复制代码
StampedLock(乐观锁)
  • 在读的过程中也允许获取写锁后写入。这样一来,我们读的数据就可能不一致,所以,需要一点额外的代码来判断读的过程中是否有写入,这种读锁是一种乐观锁。

  • 乐观锁的意思就是乐观地估计读的过程中大概率不会有写入,因此被称为乐观锁。反过来,悲观锁则是读的过程中拒绝有写入,也就是写入必须等待。显然乐观锁的并发效率更高,但一旦有小概率的写入导致读取的数据不一致,需要能检测出来,再读一遍就行。

  • StampedLock是不可重入锁,不能在一个线程中反复获取同一个锁

public class Point {
    private final StampedLock stampedLock = new StampedLock();

    private double x;
    private double y;

    public void move(double deltaX, double deltaY) {
        long stamp = stampedLock.writeLock(); // 获取写锁
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            stampedLock.unlockWrite(stamp); // 释放写锁
        }
    }

    public double distanceFromOrigin() {
        long stamp = stampedLock.tryOptimisticRead(); // 获得一个乐观读锁
        // 注意下面两行代码不是原子操作
        // 假设x,y = (100,200)
        double currentX = x;
        // 此处已读取到x=100,但x,y可能被写线程修改为(300,400)
        double currentY = y;
        // 此处已读取到y,如果没有写入,读取是正确的(100,200)
        // 如果有写入,读取是错误的(100,400)
        if (!stampedLock.validate(stamp)) { // 检查乐观读锁后是否有其他写锁发生
            stamp = stampedLock.readLock(); // 获取一个悲观读锁
            try {
                currentX = x;
                currentY = y;
            } finally {
                stampedLock.unlockRead(stamp); // 释放悲观读锁
            }
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }
}
复制代码
  • 写入过程中首先我们通过tryOptimisticRead()获取一个乐观读锁,并返回版本号。接着进行读取,读取完成后,我们通过validate()去验证版本号,如果在读取过程中没有写入,版本号不变,验证成功,我们就可以放心地继续后续操作。如果在读取过程中有写入,版本号会发生变化,验证将失败。在失败的时候,我们再通过获取悲观读锁再次读取。

同步代码块|同步方法
  • 非静态同步方法的锁是实例
  • 静态同步方法的锁是Class对象
  • 静态代码块的锁是synchronized关键字后面括号内的对象
  • 保证⽅法内部或代码块内部资源(数据)的互斥访问。即同⼀时间、由同⼀个Monitor 监视的代码,最多只能有⼀个线程在访问
  • 保证线程之间对监视资源的数据同步。即,任何线程在获取到 Monitor 后的第⼀时间,会先将共享内存中的数据复制到⾃⼰的缓存中;任何线程在释放Monitor 的第⼀时间,会先将缓存中的数据复制到共享内存中。

无论使用锁还是synchronized,本质都是一样,通过锁来实现资源的排它性,从而实际目标代码段同一时间只会被一个线程执行,进而保证了目标代码段的原子性。这是一种以牺牲性能为代价的方法。


CAS(compare and swap)
  • Java提供了原子操作类,其本质是使用了CPU的CAS指令保证原子性,由于是CPU级别的指令,其开销比需要操作系统参与的锁要小。
Atomic(原子性)

AtomicInteger为例,它提供的主要操作有:

  • 增加值并返回新值:int addAndGet(int delta)
  • 加1后返回新值:int incrementAndGet()
  • 获取当前值:int get()
  • 用CAS方式设置:int compareAndSet(int expect, int update)

Atomic类是通过无锁(lock-free)的方式实现的线程安全(thread-safe)访问。它的主要原理是利用了CAS:Compare and Set。

public int incrementAndGet(AtomicInteger var) {
    int prev, next;
    do {
        prev = var.get();
        next = prev + 1;
    } while ( ! var.compareAndSet(prev, next));
    return next;
}
复制代码

使用java.util.concurrent.atomic提供的原子操作可以简化多线程编程:

  • 原子操作实现了无锁的线程安全;

可见性

  • 当多个线程并发访问共享变量时,一个线程对共享变量的修改,其它线程能够立即看到

实现方式如下

volatile
  • 可见性又叫读写可见。即一个共享变量N,当有两个线程T1、T2同时获取了N的值,T1修改N的值,而T2读取N的值,可见性规范要求T2读取到的值必须是T1修改后的值。
    1. 将当前内核中线程工作内存中该共享变量刷新到主存;
    2. 通知其他内核里缓存的该共享变量内存地址无效;
    3. 被volatile修饰的变量,会加一个lock前缀的汇编指令。若变量被修改后,会立刻将变量由工作内存回写到主存中。那么意味了之前的操作已经执行完毕。这就是内存屏障
  • 保证加了 volatile 关键字的字段的操作具有原⼦性和可见性,其中原⼦性相当于实现了针对单⼀字段的线程间互斥访问。因此 volatile 可以看做是简化版的 synchronized。
  • volatile 只对基本类型 (byte、char、short、int、long、flfloat、double、boolean) 的赋值操作和对象的引⽤赋值操作有效。

顺序性

  • 程序执行的顺序按照代码的先后顺序执行,处理器为了提高程序整体的执行效率,可能会对代码进行优化,其中的一项优化方式就是调整代码顺序,按照更高效的顺序执行代码,但它会保证程序最终的执行结果和代码顺序执行时的结果一致
  • 除了从应用层面保证目标代码段执行的顺序性外,JVM还通过被称为happens-before原则隐式地保证顺序性。两个操作的执行顺序只要可以通过happens-before推导出来,则JVM会保证其顺序性,反之JVM对其顺序性不作任何保证,可对其进行任意必要的重新排序以获取高效率。
happens-before原则(先行发生原则)
  • 传递规则:如果操作1在操作2前面,而操作2在操作3前面,则操作1肯定会在操作3前发生。该规则说明了happens-before原则具有传递性
  • 锁定规则:一个unlock操作肯定会在后面对同一个锁的lock操作前发生。这个很好理解,锁只有被释放了才会被再次获取
  • volatile变量规则:对一个被volatile修饰的写操作先发生于后面对该变量的读操作
  • 程序次序规则:一个线程内,按照代码顺序执行
  • 线程启动规则:Thread对象的start()方法先发生于此线程的其它动作
  • 线程终结原则:线程的终止检测后发生于线程中其它的所有操作
  • 线程中断规则: 对线程interrupt()方法的调用先发生于对该中断异常的获取
  • 对象终结规则:一个对象构造先于它的finalize发生

Synchronized实现原理

对象在内存中的布局
  • 对象头的主要是由MarkWord和Klass Point(类型指针)组成
    • Klass Point是是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
    • Mark Word用于存储对象自身的运行时数据。如果对象是数组对象,那么对象头占用3个字宽(Word),如果对象是非数组对象,那么对象头占用2个字宽。(1word = 2 Byte = 16 bit)
  • 实例变量存储的是对象的属性信息,包括父类的属性信息,按照4字节对齐
  • 填充字符,因为虚拟机要求对象字节必须是8字节的整数倍,填充字符就是用于凑齐这个整数倍的

32位虚拟机中:

32位的虚拟机.png

64位虚拟机中:

64位的虚拟机.png

Synchronized在JVM中的实现原理

在虚拟机之中,锁对象由ObjectMonitor对象实现(C++),结构如下:

ObjectMonitor() {
    _count        = 0; //用来记录该对象被线程获取锁的次数
    _waiters      = 0;
    _recursions   = 0; //锁的重入次数
    _owner        = NULL; //指向持有ObjectMonitor对象的线程 
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
  }
复制代码

线程的生命周期存在5个状态,start、running、waiting、blocking、dead,获取对应锁对象的线程状态结构图如下:

线程状态.jpg

  • 当多个线程同时访问该方法,那么这些线程会先被放进_EntryList队列,此时线程处于blocking状态
  • 当一个线程获取到了实例对象的监视器(monitor)锁,那么就可以进入running状态,执行方法,此时,ObjectMonitor对象的_owner指向当前线程,_count加1表示当前对象锁被一个线程获取
  • 当running状态的线程调用wait()方法,那么当前线程释放monitor对象,进入waiting状态,ObjectMonitor对象的_owner变为null,_count减1,同时线程进入_WaitSet队列,直到有线程调用notify()方法唤醒该线程,则该线程重新获取monitor对象进入_Owner区
  • 如果当前线程执行完毕,那么也释放monitor对象,进入waiting状态,ObjectMonitor对象的_owner变为null,_count减1
Synchronized方法的具体实现
  • Synchronized方法是通过方法调用指令来读取运行时常量池中的ACC_SYNCHRONIZED标志隐式实现的,被标记的方法会尝试获取monitor
    • 获取到则执行方法代码,执行完毕释放monitor
    • 如果monitor被其他线程获取,当前线程阻塞等待
Synchronized代码块的具体实现
  • Synchronized代码块在需要同步的代码加入monitorentry指令,在结束同步位置加入monitorexit指令,并且JVM会保证指令是成对出现的,避免死锁
Synchronized锁的优化

在JAVA1.6之后,添加了锁优化机制,一共有4种锁,无锁,偏向锁,轻量锁,重量锁(从低到高,不可逆)

锁状态优点缺点适用场景
偏向锁加锁解锁无需额外消耗,和非同步方法差距仅为纳秒如果竞争的线程过多,则会有额外的资源消耗基本没有线程竞争的同步场景
轻量锁竞争的线程不会阻塞,使用自旋,提高程序响应速度如果长时间无法获取资源,会长时间消耗CPU资源少量线程竞争,持有锁时间短,快速响应的场景
重量锁竞争的线程不需要自旋,不会导致CPU消耗资源线程阻塞,状态转换消耗时间很多线程竞争,持有锁时间长,大吞吐量的场景
偏向锁

在大多数状态下,其实锁竞争状态很少,同一个线程多次获得一个锁的情况却很多,偏向锁的加入就是解决此问题

  • 当线程1访问代码块并获取锁对象时,会在java对象头和栈帧中记录偏向的锁的threadID,因为偏向锁不会主动释放锁,因此以后线程1再次获取锁的时候,需要比较当前线程的threadID和Java对象头中的threadID是否一致,如果一致(还是线程1获取锁对象),则无需使用CAS来加锁、解锁;
  • 如果不一致(其他线程,如线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID),那么需要查看Java对象头中记录的线程1是否存活
    • 如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁
    • 如果存活,那么立刻查找该线程(线程1)的栈帧信息
    • 如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁
    • 如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。

偏向锁是默认开启的,而且开始时间一般是比应用程序启动慢几秒,如果不想有这个延迟,那么可以使用-XX:BiasedLockingStartUpDelay=0;

如果不想要偏向锁,那么可以通过-XX:-UseBiasedLocking = false来设置;

轻量锁
  • 如果在不是很多线程争抢资源,并且线程持有锁的时间不长的情况下,就没有必要阻塞线程等待,因为阻塞线程的代价是很大的,需要从用户态转为内核态。此时以自旋态等待锁的释放
  • JDK1.6之后又引入了自适应的自旋锁。自适应意味着自旋的次数不再固定了,而是由前一次在同一个锁上的自旋次数及锁的拥有者的状态来决定

轻量锁在某些时机会升级为重量锁。

重量锁

轻量级锁什么时候升级为重量级锁?

  • 线程1获取轻量级锁时会先把锁对象的对象头MarkWord复制一份到线程1的栈帧中创建的用于存储锁记录的空间(称为DisplacedMarkWord),然后使用CAS把对象头中的内容替换为线程1存储的锁记录(DisplacedMarkWord)的地址;
  • 如果在线程1复制对象头的同时(在线程1CAS之前),线程2也准备获取锁,复制了对象头到线程2的锁记录空间中,但是在线程2CAS的时候,发现线程1已经把对象头换了,线程2的CAS失败,那么线程2就尝试使用自旋锁来等待线程1释放锁
  • 自旋的次数是有限制的,比如10次或者100次,如果自旋次数到了线程1还没有释放锁,或者线程1还在执行,线程2还在自旋等待,这时又有一个线程3过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。
锁粗化
  • 一系列持续加锁的情况,将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁,避免频繁的加锁解锁操作
锁消除
  • Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,经过逃逸分析,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间

引用逃逸

  • Java分配在堆上的对象都是靠引用操作的,当对象在某个方法中被定义好之后。那么就将其引用作为其他方法的参数传递出去,这就叫做对象的引用逃逸。
  • 如果原本的对象在当前方法结束后就会被垃圾回收器标记回收,但由于其引用被传递出去。被一个长期存活的对象所持有,那么对于GC Roots来说,他就是可达对象。声明周期就是持有对象的生命周期。可能造成内存泄露。
  • this逃逸是指构造函数返回之前其他对象就持有该对象的引用调用尚未构造完成的对象方法可能引起错误。
分类:
Android
标签:
收藏成功!
已添加到「」, 点击更改