Java多线程高并发

      什么是进程:进程是正在运行的应用程序,进程是线程的集合

      什么是线程,线程就是一条执行路径,一个独立的执行单元

      什么是多线程,为了提高效率

      实现多线程的方式:

      1.继承Thread类,重写run方法

      2.实现runnable接口

      3.使用匿名内部类

      4.callable

      5.使用线程池创建线程

Thread和Runnable

            如果只是使用run方法,不修改类的其他行为,应该使用Runnable而不是使用Thread

            

在调用wait方法時,线程必须要持有被调用对象的锁,当调用wait方法后,线程就会释放掉该对象的锁

在调用Thread类的sleep方法时,线程不会释放掉对象的锁

关于wait与notify和notifyAll方法的总结:

  1. 在调用wait时,首先要确保调用了wait方法的线程已经持有了对象的锁

  2. 当调用wait后,该线程就会释放掉这个对象的锁,然后进入到等待状态(wait set)

  3. 当线程调用了wait后进入到等待状态时,它就可以等待其他线程调用相同对象的notify或notifyAll方法来使自己唤醒

  4. 一旦这个线程被其他线程唤醒之后,该线程就会与其他线程一同竞争这个对象的锁(公平竞争);只有当该线程获得这个对象的锁之后,线程才会继续往下执行

  5. 调用wait方法的代码片段需要放在一个synchronized块或是synchronized方法中,这样才可以确保线程在调用wait方法前已经获取到了对象的锁

  6. 当调用对象的notify方法时,它会随机唤醒该对象等待集合(wait set)中的任意一个线程,当某个线程被唤醒后,它就会与其他线程一同竞争该对象的锁

  7. 当调用对象的notifyAll方法时,它会唤醒该对象等待集合(wait set)中的所有线程,这些线程被唤醒后,又会开始竞争对象的锁

  8. 在某一时刻,只有唯一一个线程拥有对象的锁

如果某个对象有若干个synchronized方法,那么在某一时刻只能有一个线程进入其中一个synchronized方法,当前对象只有一个锁;当一个线程去访问对象的synchronized static方法的时候,实际上访问的是当前对象所对应的class的锁

当使用synchronized关键字来修饰代码块时,字节码层面上是通过monitorenter与monitorexit指令来实现的锁的获取与释放动作;

monitorenter与monitorexit的关系可能是1对1或1对多,1对多是为了在异常情况也能正常退出。

当线程进入monitorenter指令后,线程将会持有Monitor对象,退出monitorenter指令后,线程将会释放Monitor对象。

对于synchronized关键字修饰方法来说,并没有出现monitorenter与monitorexit指令,而是出现了一个ACC_SYNCHRONIZED标志。原因是JVM使用了ACC_SYNCHRONIZED

访问标志类区分一个方法是否是同步方法;当方法被调用时,调用指令会检查该方法是否拥有ACC_SYNCHRONIZED标志,如果有,那么执行线程会先持有方法所在对象的Monitor对象,然后再去执行方法体;在该方法执行期间,其他任何线程均无法再获取到这个Monitor对象,当线程执行完该方法后,它会释放掉这个Monitor对象。即使该方法发生异常也会释放该对象。

static方法没有传入this参数,所以不能在static方法使用this。

JVM中的同步是基于进入与退出监视器对象(管程对象)(Monitor)来实现的,每个对象实例都会有一个Monitor对象,Monitor对象会和java对象一同创建并销毁。monitor对象是由c++来实现的。

当多个线程同时访问一段同步代码时,这些线程会被放到一个EntryList集合中,处于阻塞状态的线程都会被放到该列表中。接下来,当线程获取到对象的monitor时,monitor是依赖于底层操作系统的mutex lock来实现互斥的,线程获取mutex成功,则会持有该mutex,这时其他线程无法再获取到该mutex。

如果线程调用了wait方法,那么该线程就会释放掉所持有的mutex,并且该线程就会进入到WaitSet集合(等待集合)中,等待下一次被其他线程调用notify/notifyAll唤醒。如果当前线程顺利执行完毕方法,那么它也会释放掉所持有的mutex。

总结一下:同步锁在这种实现方式当中,因为Monitor是依赖于底层的操作系统实现,这样就存在用户态于内核态之间的切换,所以会增加性能开销。

通过对象互斥锁的概念来保证共享数据操作的完整性。每个对象都对应于一个可称为【互斥锁】的标记,这个标记用于保证在任何时刻,只有一个线程访问该对象。

那些处于EntryList与WaitSet中的线程均处于阻塞状态,阻塞操作是由操作系统完成,在linux下是通过pthread_mutex_lock函数实现的。线程被阻塞后便会进入到内核调度状态,这会导致系统在用户态与内核态之间来回切换,严重影响锁的性能。EntryList与WaitSet都是Monitor的对象,用C++实现。调用wait方法会进入WaitSet队列,notify/notifyAll唤醒后进入EntryList。

解决上述问题的办法便是自旋(Spin)。其原理是:当发生对monitor的争用时,若owner能够在很短时间内释放掉锁,则那些正在争用的线程就可以稍微等待一下(即所谓的自旋-做while循环),在owner线程释放锁之后,争用线程可能会立刻获取到锁,从而避免系统阻塞。不过,当owner运行的时间超过了临界值后,争用线程自旋一段时间后依然无法获取到锁,这时争用线程会停止自旋而进入阻塞状态。所以总体思想是:先自旋,不成功再进行阻塞,尽量降低阻塞的可能性,这对那些执行时间很短的代码块来说有极大的性能提升。显然,自旋在多处理器(多核心)上才有意义。

互斥锁的属性:

  1. PTHREAD_MUTEX_TIMED_NP:这是缺省值,也就是普通锁。当一个线程加锁后,其余请求锁的线程就会形成一个等待队列,并且在解锁后按照优先级获取到锁。这种策略可以确保资源分配的公平性。

  2. PTHREAD_MUTEX_RECURSIVE_NP:嵌套锁。允许一个线程对同一个锁成功获取多次,并通过unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。

  3. PTHREAD_MUTEX_ERRORCHECK_NP: 检错锁。如果一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同,这样就保证了当不允许多次加锁时不会出现最简单情况下的死锁。

  4. PTHREAD_MUTEX_ADAPTIVE_NP:适应锁。动作最简单的锁类型,仅仅等待解锁后重新竞争。

查看openjdk源码查看Monitor如何实现。

参考:http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/f5e5d3ac66a0/src/share/vm/runtime/objectMonitor.cpp

在JDK1.5之前,我们若想实现线程同步,只能通过synchronized关键字这一种方式来实现;底层,java也是通过synchronized关键字来做到数据的原子性维护的;synchronized关键字是JVM实现的一种内置锁,从底层角度来说,这种锁的获取与释放都是由jvm帮助我们隐式实现的。

从jdk1.5开始,并发包引入了Lock锁,Lock同步锁是基于java来实现的,因此锁的获取与释放都是通过java代码来实现与控制的;然而,synchronized是基于底层操作系统的Mutex Lock来实现的,每次对锁的获取与释放动作都会带来用户态与内核态之间的切换,这种切换会极大地增加系统的负担;在并发量较高时,也就是说锁的竞争比较激烈时,synchronized锁在性能上的表现就会非常差。

从jdk1.6开始,synchronized锁的实现发生了很大的变化;jvm引入了相应的优化手段来提升synchronized锁的性能,这种提升涉及到偏向锁、轻量级锁、以及重量级锁等,从而减少锁的竞争 所带来的用户态与内核态之间的切换;这种锁的优化实际上是通过java对象头中的一些标志位来实现的;对于锁的访问与改变,实际上都与java对象头息息相关。

从jdk1.6开始,对象实例在堆当中会被划分为三个组成部分:对象头、实例数据与对齐填充。

对象头主要也是由3块内容来构成:

  1. Mark Word

  2. 指向类的指针

  3. 数组长度

其中Mark Word(它记录了对象、锁及垃圾回收相关的信息,在64位的jvm中,其长度也是64bit)的位信息包含了如下组成部分:

  1. 无锁标记

  2. 偏向锁标记

  3. 轻量级锁标记

  4. 重量级锁标记

  5. GC标记

对于 synchronized锁来说,锁的升级主要都是通过Mark Word中的锁标记位与是否是偏向锁标志位来达成的;synchronized关键字所对应的锁都是先从偏向锁开始,随着锁竞争的不断升级,逐步演化至轻量级锁,最后变成重量级锁。

对于锁的演化来说,它会经历如下阶段:

无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁

偏向锁:

    针对于一个线程来说的,它的主要作用就是优化同一个线程多次获取一个锁的情况;如果一个synchronized方法被一个线程访问,那么这个方法所在的对象就会在其Mark Word中的偏向锁进制标记,同时还会有一个字段来存储该线程的ID;当这个线程再次访问同一个synchronized方法时,它会检查这个对象的Mark Word的偏向锁标记以及是否指向了其线程ID,如果是的话,那么该线程就无须再去进入管程(Monitor)了,而是直接进入到该方法体中。

如果是另外一个线程访问这个synchronized方法,那么实际情况会如何呢?

       偏向锁会被取消。

在当前的多内核多线程的环境下同一个线程下一次继续获取到锁的概率是很低的,关闭偏向锁会提升性能

轻量级锁:

若第一个线程已经获取到了当前对象的锁,这时第二个线程又开始尝试争抢该对象的锁,由于该对象的锁已经被第一个线程获取到,因此它是偏向型锁,而第二个线程在争抢时,会发现该对象头中的Mark Word已经是偏向锁,但里面存储的线程ID并不是自己(是第一个线程),那么它会进行CAS(Compare and Swap),从而获取到锁,这里面存在两种情况:

  1. 获取锁成功:那么它会直接将Mark Word中的线程ID由第一个线程变成自己(偏向锁笔记为保存不变),这样该对象依然会保存偏向锁的状态

  2. 获取锁失败:则表示这时可能会有多个线程同时在尝试争抢该对象的锁, 那么这时偏向锁就会进行升级,升级为轻量级锁

自旋锁:

若自旋失败(依然无法获取到锁),那么锁就会转化为重量级锁,在这种情况下,无法获取到锁的线程都会进入Monitor(即内核态)

自旋最大的一个特点就是避免了线程从用户态进入到内核态。自旋锁是轻量级锁的一种实现方式。

有一种自适应自旋锁,每次自旋的次数是不固定的,JVM根据以前的自旋情况来动态调整自旋次数。

重量级锁:

线程最终从用户态进入到了内核态。

编译器对于锁的优化措施:

锁消除技术:

JIT编译器(Just In Time编译器)可以在动态编译同步代码时,使用一种叫逃逸分析的技术,来通过该技术判别程序中所使用的的对象是否只被一个线程所使用,而没有散布到其他线程当中;如果情况就是这样的话,那么JIT编译器在编译这个同步代码时就不会生成Synchronized关键字所标识的锁的申请与释放机器码,从而消除锁的使用流程。

锁粗化:

JIT编译器在执行动态编译时,若发现前后相邻的synchronized块使用的是同一个锁对象,那么它就会把这几个synchronized块合并为一个较大的同步块,这样做的好处在于线程在执行这些代码时,就无需平凡申请与释放锁了,从而达到申请与释放锁一次,就可以执行完全部的同步代码块,从而提升了性能。

死锁:线程1等待线程2互斥持有的资源,而线程2也在等待线程1互斥持有的资源,两个线程都无法继续执行。

活锁:线程持续重试一个总是失败的操作,导致无法继续执行。

饿死:线程一直被调度器延迟访问其赖以执行的资源,也许是调度器先于低优先级的线程而执行高优先级的线程,同时总是会有一个高优先级的线程可以执行,饿死也叫做无限延迟。

查看死锁的一些工具:

jps -l

jstack xxx(线程号,jps命令得到)

jvisualvm

jmc

关于lock与synchronized关键字在锁的处理上的重要差别

  1. 锁的获取方式:前者是通过程序代码的暗示由开发者手工获取,后者是通过JVM来获取(无需开发者干预)

  2. 具体实现方式:前者是通过java代码的方式来实现的,后者是通过jvm底层来实现的(无需开发者关注)

  3. 锁的释放方式:前者务必通过unlock()方法在finally块中手工释放,后者是通过jvm来释放(无需开发者关注)

  4. 锁的具体类型:前者提供了多种,如公平锁,非公平锁,后者与前者均提供了可重入锁

传统上,我们可以通过synchronized关键字+wait+notify/notifyAll 来实现多个线程之间的协调与通信,整个过程都是由jvm来帮助我们实现的;开发者无需(也是无法)了解底层的实现细节。

从jdk1.5开始,并发包提供了Lock,Condition(await 与signal/signalAll)来实现多个线程之间的协调与通信,整个过程都是由开发者来控制的,而且相比于传统方式,更加灵活,功能也更加强大。

Thread.sleep与await(或是Objectde的wait方法)的本质区别:sleep本质上不会释放锁,而await会释放锁,并且在signal后,还需要重新获取锁才能继续执行(该行为与object的wait方法完全一致)

volatile关键字(不稳定的意思)

private volatile int count;

volatile关键字主要有三方面的作用:

  1. 实现long/double类型变量的原子操作

  2. 防止指令重排序

  3. 实现变量的可见性

double a = 1.0 不是原子性操作,先赋值低32位然后再赋值高32位,可以在前面加上volatile关键字解决这个问题

当使用volatile修饰变量时,应用就不会从CPU寄存器中获取该变量的值,而是从内存(高速缓存)中获取

volatile与锁类似的地方有2点:

  1. 确保变量的内存可见性

  2. 防止指令重排序

volatile可以确保对变量写操作的原子性,但不具备排他性,

另外一点:使用锁可能会导致线程的上下文切换(内核态与用户态之间的切换),但使用volatile并不会出现这种情况

volatile int a = b + 2; 这种情况下无法保证对a操作的原子性,b可能会被多个线程使用,哪怕b也是用volatile修饰的也是不行的

volatile int a = a++;   这种情况也不行

适用于下面这些情况:赋值前不用去读取其他变量的值,更符合volatile的底层语义

volatile int count = 1;

volatile boolean flag = false;

如果要实现volatile写操作的原子性,那么在等号右侧的赋值变量中就不能出现被多线程所共享的变量,哪怕这个变量也是个volatile也不可以

voltile Date date = new Date(); 这种方式也是不可以的,因为new Date()操作会先在堆构建一个Date数据类型,然后返回其引用给date,整个过程不是原子性的

防止指令重排序与实现变量的可见性都是通过一种手段来实现的:内存屏障(memory barrier)

int a = 1;

String b = "hello";

内存屏障(Release Barrier, 释放屏障)

volatile boolean v = false;  //写入操作

内存屏障(Store Barrier, 存储屏障)

Release Barrier:防止下面的volatile与上面的所有操作的指令重排序

Store Barrier :重要作用是刷新处理器缓存,结果是可以确保存储屏障之前一切的操作所生成的结果对于其他处理器来说都立刻可见。

内存屏障(Load Barrier, 加载屏障)

boolean v1 = v;  //读取操作

内存屏障(Acquire Barrier, 获取屏障)

int a = 1;

String s = "hello";

Load Barrier:可以刷新处理器缓存,同步其他处理器对该volatile遍历的修改结果。

Acquire Barrier:可以防止上面的volatile读取操作与下面的所有操作语句的指令重排序。

对于volatile关键字变量的读写操作,本质上都是通过内存屏障来执行的。

内存屏障兼具了两方面能力:1.防止指令重排序,2.实现变量内存的可见性

  1. 对于读取操作来说,volatile可以确保该操作与其后续的所有读写操作都不会进行指令重排序。

  2. 对于修改操作来说,volatile可以确保该操作与其前面的所有读写操作都不会进行指令重排序。

volatile只适用于原生数据类型(原子性),对于引用类型是不适用的(非原子性)。

volatile与锁的一些比较:

锁同样具备变量内存可见性与防止指令重排序的功能,锁同样是使用内存屏障实现的。

synchronized的实现方式:

monitorenter

内存屏障(Acquire Barrier,获取屏障)

......

内存屏障(Release Barrier,释放屏障)

monitorexit

使用了volatile后会有性能上的损失,只能从内存或高速缓存取变量,不能从CPU寄存器取变量。

java内存模型(java memory model JMM)以及happen-before

  1. 变量的原子性问题

  2. 变量的可见性问题

  3. 变量修改的时序性问题

happen-before重要规则:

  1. 顺序性规则(限定在单个线程上的):该线程的每个动作都happen-before它的后面的动作。

  2. 隐式锁(monitor)规则:unlock happen-before lock, 之前的线程对于同步代码块的所有执行结果对于后序获取锁的线程来说都是可见的。

  3. volatile读写规则:对于一个volatile变量的写操作一定会happen-before后序对该对象的读操作。

  4. 多线程启动规则:thread对象的start方法happen-before该线程run方法中的任何一个动作,包括在其中启动的任何子线程。

  5. 多线程的终止规则:一个线程启动了一个子线程,并且调用了子线程的join方法等待其结束,那么当子线程结束后,父线程的接下来的所有操作都可以看到子线程run方法中的执行结果。

  6. 线程的中断规则:可以调用interrupt方法来中断进程,这个调用happen-before对该线程中断的检查(isInterrupted)。

        

CountDownLatch: 可以实现多个子线程执行完之后才执行主线程,在最后一个子线程到达一个点之后唤醒主线程继续执行,底层使用AQS来实现。

关于CyclicBarrier的底层执行流程:

  1. 初始化CyclicBarrier中的各种成员变量,包括parties、count、以及Runnable(可选)

  2. 当调用await方法时,底层会先检查计数器是否已经归零,如果是的话,那么就首先执行可选的Runnable,接下来开始下一个generation;

  3. 在下一个分代中,将会重置count值为parties,并且创建新的generation实例;

  4. 同时会调用Condition的signallAll方法,唤醒所有在屏障前面等待的线程,让其开始继续执行;

  5. 如果计数器没有归零,那么当前的调用线程将会通过Condition的wait方法,在屏障前进行等待;

  6. 以上所有执行流程均在lock锁的控制范围内,不会出现并发情况。

CAS(Compare And Swap)

比较与交换:这是一个不断循环的过程,一直到变量值被修改成功为止。CAS本身是由硬件指令来提供支持的,换句话说,硬件中是通过一个原子指令来实现比较与交换的;因此,CAS可以确保变量操作的原子性的。

  1. synchronized关键字与Lock等锁机制都是悲观锁:无论做何种操作,首先都需要先上锁,接下来再去执行后续操作,从而确保了接下来的所有操作都是由当前这个线程来执行的。

  2. 乐观锁:线程在操作之前不会做任何预先的处理,而是直接去执行;当在最后执行变量更新的时候,当前线程需要由一种机制来确保当前被操作的变量时没有被其他线程修改的;CAS是乐观锁的一种极为重要的实现方式。

对于CAS来说,其操作数主要涉及到如下三个:

  1. 需要被操作的内存值V

  2. 需要进行比较的值A

  3. 需要进行写入的值B

只有当V==A的时候,CAS才会通过原子操作的手段来将V的值更新为B

AtomicInteger

关于CAS的限制或是问题:

  1. 循环开销问题:并发量大的情况下会导致线程一直自旋。

  2. 只能保证一个变量的原子性操作,可以通过AtomicReference来实现对多个变量的原子操作

  3. ABA问题:1 -> 3 -> 1。可以通过添加一个版本号或时间戳来解决

Future

FutureTask

CompletableFuture:可以保证任务执行的异步性,也能保证获取结果的异步性

ThreadLocal:本质上,ThreadLocal是通过空间来换取时间,从而实现每个线程当中都会有一个变量的副本,这样每个线程就都会操作该副本,从而完全规避多线程的并发问题。使用ThreadLocal需要注意内存泄漏的问题,需要在try finally语句执行ThreadLocal的remove方法,把不再使用的对象释放掉

java中存在四种类型的引用:

  1. 强引用(strong):垃圾回收不会进行回收

  2. 软引用(soft):内存空间明显不够的时候GC会把软引用的对象回收

  3. 弱引用(weak):下一次垃圾回收的时候把弱引用回收掉,前提是弱引用对象没有强引用对象指向它

  4. 虚引用(phantom):本身不表示一个对象,只是在GC的时候会收到一个通知,进行后续的操作

除了强引用外其他引用都继承Reference抽象类

AbstractQueuedSynchronizer(AQS)

对于ReentrantLock来说,其执行逻辑如下所示:

  1. 尝试获取对象的锁,如果获取不到(意味着已经有其他线程持有了锁,并且尚未释放),那么它就会进入到AQS的阻塞队列当中。

  2. 如果获取到,那么根据锁是公平苏还是非公平锁来进行不同的处理

            2.1  如果是公平锁,那么线程会直接放置到AQS阻塞队列的末尾

            2.2  如果是非公平锁,那么线程会首先尝试进行CAS计算,如果成功,则直接获取到锁;

                   如果失败,则与公平锁的处理方式一样,被放到阻塞队列末尾。

       3. 当锁被释放时(调用了unlock方法),那么底层会调用release方法对state成员变量值进行减一操作,如果减一后,state值不为0,那么release操作就执行完毕;

           如果减一操作后,state值为0,则调用LockSupport的unpark方法唤醒该线程后的等待队列中的第一个后继线程(pthread_mutex_unlock),将其唤醒,使之能够获取到对象的锁(release时,对于公平锁与非公平锁的处理逻辑是一致的);之所以调用release方法后state值可能不为零,原因在于ReentrantLock是可重入锁,表示线程可以多次调用lock方法,导致每调用一次,state值都会加一。

            

对于ReentrantLock来说,所谓的上锁,本质上就是对AQS中的state成员变量的操作:对该成员变量+1,表示上锁;对该成员变量-1,表示释放锁。

            

关于ReentrantReadWriteLock的操作逻辑:

底层仍然分为公平锁和非公平锁

state低16位为写锁个数,高16位为读锁个数,这个设计师非常精妙的

读锁:

  1. 在获取读锁时,会尝试判断当前对象是否拥有了写锁,如果已经拥有,则直接失败。

  2. 如果没有写锁,就表示当前对象没有排它锁,则当前线程会尝试给对象上锁。

  3. 如果当前线程已经持有了该对象的锁,那么直接将读锁数量+1。

写锁:

  1. 在获取写锁,会尝试判断当前对象是否拥有了锁(读锁与写锁),如果已经拥有且持有的线程非当前线程,直接失败。

  2. 如果当前对象没有被加锁,那么写锁就会为当前对象上锁,并且将写锁的个数加一。

  3. 将当前对象的排它锁线程持有者设为自己。

关于AQS与synchronized关键字之间的关系:

synchronized:

  1. synchronized关键字在底层的C++实现中,存在两个重要的数据结构(集合):WaitSet、EntryList

  2. WaitSet中存放的是调用了Object的wait方法的线程对象(被封装成了C++的Node对象)

  3. EntryList中存放的是陷入到阻塞状态、需要获取monitor的那些线程对象

  4. 当一个线程被notify后,它就会从WsitSet中移动到EntryList中

  5. 进入到EntryList后,该线程依然需要与其他线程争抢monitor对象

  6. 如果争抢到,就表示该线程获取到了对象的锁,它就可以以排他方式执行对应的同步代码

AQS:

  1. AQS中存在两种队列,分别是Condition对象上的条件队列,以及AQS本身的阻塞队列(FIFO)

  2. 这两个队列中的每一个对象都是Node实例(里面封装了线程对象)

  3. 当位于Condition条件队列中的线程被其他线程signal后,该线程就会从条件队列中移动到AQS的阻塞队列中

  4. 位于AQS阻塞队列中的Node对象本质上都是由一个双向链表来构成的

  5. 在获取AQS锁时,这些进入到阻塞队列中的线程会按照在队列中的排序先后尝试获取

  6. 当AQS阻塞队列中的线程获取到锁后,就表示该线程已经可以正常执行了

  7. 陷入到阻塞状态的线程,依然需要进入到操作系统的内核态,进入阻塞(park方法实现)

线程池:

ThreadPoolExecutor的参数的含义:

int corePoolSize, 线程池当中所一直维护的线程数量,如果线程池处于任务空闲期间,那么该线程也并不会被回收

int maximumPoolSize, 线程池中所维护的线程数的最大数量

long keepAliveTime, 超过corePoolSize的线程在经过keepAliveTime时间后如果一直处于空闲状态,那么超过的这部分线程将会被回收掉

TimeUnit unit, 指的是keepAliveTime的时间单位

BlockingQueue<Runnable> workQueue, 想线程池所提交的任务位于的阻塞队列,它的实现有多种方式

ThreadFactory threadFactory, 线程工厂,用于创建新的线程并被线程池所管理,默认线程工厂所创建的线程都是用户线程(不是守护线程)且优先级为正常优先级

RejectedExecutionHandler handler:表示当线程池中的线程都在忙于执行任务且阻塞队列也已经满了的情况下,新到来任务该如何对待和处理。

它有4中策略:

  1. AbortPolicy: 直接抛出一个运行期异常

  2. DiscardPolicy: 默默地丢弃掉任务,什么都不做并且不抛出任何异常

  3. DiscardOldestPolicy: 丢弃掉阻塞队列存放时间最久的任务(队头元素),并且为当前所提交的任务留出一个队列中的空闲空间,一遍将其放进队列中;但是未必能放入队列中,因为可能刚刚丢弃队头的元素然后就有别的线程把任务放入队尾。

  4. CallerRunsPolicy: 直接由提交任务的线程来运行这个提交的任务。

在线程池中,最好将偏向锁的标记关闭,这样会提升性能。

对于线程池来说,其提供了execute与submit两种方式来向线程池提交任务

总体来说,submit方法是可以取代execute方法的,因为它既可以接收Callable(有返回值)任务,也可以接收Runnable(没有返回值)任务

线程窃取可以提高执行效率,让执行速度快的线程帮忙执行剩余的任务

线程池的总体执行策略:

  1. 如果线程池中正在执行的线程数 < corePoolSize,那么线程池就会优先选择创建新的线程而非将提交的任务加到阻塞队列中。

  2. 如果线程池中正在执行的线程数 >= corePoolSize,那么线程池就会优先选择对提交任务进行阻塞排队而非创建新的线程。

  3. 如果提交的任务无法加入阻塞队列当中,那么线程池就会创建新的线程;如果创建的线程数超过了maximumPoolSize,那么拒绝策略就会起作用。

关于线程池任务的提交:

  1. 两种提交方式:submit和execute。

  2. submit有三种方式,无论那种方式,最终都是将传递进来的任务转换为一个Callable对象进行处理。

  3. 当Callable对象构造完毕后,最终都会调用Executor接口中声明的execute方法进行统一的处理。

对于线程池来说,存在两个状态需要维护:

  1. 线程池本身的状态:ctl的高3位表示

  2. 线程池中所运行着的线程的数量:ctl的其余29位来表示

线程池一共存在5种状态:

  1. RUNNING:线程池可以接收新的任务提交,并且还可以正常处理阻塞队列中的任务。

  1. SHUTDOWN:不在接收新的任务提交,不过线程池可以继续处理阻塞队列中的任务。

  2. STOP:不再接收新的任务,同时还会丢弃阻塞队列中的既有任务;此外,它还会中断正在处理中的任务。

  3. TIDYING(清理):所有任务都执行完毕后(同时也涵盖了阻塞队列中的任务),当前线程池中的活动线程数量将为0,将会调用terminated方法。

  4. TERMINATED:线程池的终止状态,当terminated方法执行完毕后,线程池将会处于该状态下。

线程池状态转换:

RUNNING -> SHUTDOWN:当调用了线程池的shutdown方法时,货值当finalize方法被隐式调用后(该方法内部会调用shutdown方法)

RUNNING,SHUTDOWN -> STOP:当调用了线程池的shutdownNow方法时

SHUTDOWN -> TIDYING:当线程池和阻塞队列均为空时

STOP -> TIDYING:在线程池变为空时(阻塞队列在变为STOP状态时就清空了)

TIDYING -> TERMINATED:在terminated方法被执行完毕时

ForkJoinPool:分割任务,本别执行,然后把结果组合起来

CompoetionService:把线程池中的任务的结果按执行完成时间按先后顺序排列放在一个BlockingQueue中,可以结合ThreadPoolExecutor使用

random:生成随机数,在并发环境下如果多个线程获取到相同的种子,用老的种子获取新的种子的过程会使用CAS函数规避产生相同的随机数,多线程可能会有很多自旋,性能会受限。

ThreadLocalRandom:可以解决在多线程高并发环境下产生随机数的性能问题

对于一个随机数生成器来说,有两个要是需要考量:

  1. 随机数生成器的种子

  2. 具体的随机数生成算法(函数)

对于ThreadLocalRandom来说,其随机数生成器的种子是存放在每个线程的ThreadLocal中的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值