• 欢迎来到本博客,希望可以y一起学习与分享

Java多线程与并发

Java benz 3个月前 (03-17) 12次浏览 0个评论 扫描二维码
文章目录[隐藏]

Synchronized

Java锁比较全的入门整理
线程安全问题的主要诱因
1、存在共享数据(也称临界资源)
2、存在多条线程共同操作这些共享数据
解决问题的根本方法
同一时刻有且只有一个线程在操作共享数据,其它线程必须等到该线程处理完数据后再对共享数据进行操作。
互斥锁的特性
互斥性:即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程的协调机制,这样在同一时间只要一个线程对需要同步的代码块(复合操作)进行访问。互斥性也称为操作的原子性;
可见性:必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值),否则另外一个线程可能是本地缓存的某个副本上的数据操作,从而引起不一致;
synchronized锁的不是代码,而是对象;
根据获取锁的分类:获取对象锁和获取类锁
获取对象锁的两种方法:

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

获取类锁的两种用法:

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

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

Synchronized底层实现原理

实现Synchronized的基础

  • Java对象头
  • Monitor

对象在内存中的布局:

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

Java对象头

synchronized是悲观锁,在操作同步资源之前需要给同步资源先加锁,这把锁就是存在Java对象头里的

Hotspot的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。

Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。

Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

数组长度:只有当对象为数组的时候,才有这一部分数据 32bit

第一种分类是偏向锁/轻量级锁/重量级锁,这三种锁特指 synchronized 锁的状态,通过在对象头中的 mark word 来表明锁的状态。
对象头结构:

虚拟机位数 头对象结构 说明
32/64bit Mark Word 默认存储对象的hashCode, 分代年龄, 锁类型,锁标志位等信息
32/64bit Class Metadata Address 类型指针指向对象的类元数据, JVM通过这个指针确定该对象是哪个类的数据

Mark Word

Monitor锁

Monitor: 每个Java对象天生自带了一把看不见的锁;
Monitor锁的竞争、获取与释放

什么是重入

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

为什么会对synchronized 嗤之以鼻

1、早期版本,synchronized 属于重量级锁,依赖于Mutex Lock(系统提供的锁)实现。
2、线程之间的切换需要从用户态转换到核心,开销大。
Java6以后,synchronized 性能得到了很大的提升

自旋锁

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

自适应自旋锁

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

锁消除

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

锁粗化

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

synchronized 的四种状态

参考:Java并发编程:Synchronized底层优化(偏向锁、轻量级锁)
无锁、偏向锁、轻量级锁、重量级锁
锁升级的路径:无锁→偏向锁→轻量级锁→重量级锁。
偏向锁:减少同一线程获取锁的代价
大多情况下,锁不存在多线程竞争,总是由同一线程多次获得
核心思想:
如果一个线程获得了锁, 那么锁就进入偏向模式, 此时Mark Word的结构也变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作即获取锁的过程只需要检查Mark Word的锁标记位为偏向锁以及当前线程Id等于Mark Word的ThreadID即可, 这样就省去了大量有关锁申请的操作。
不适用于锁竞争比较激烈的多线程场合
轻量级锁
轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁。
适应的场景:线程交替执行同步块
若存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁情
轻量级锁的加锁过程

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

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

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

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

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

(1)通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word。

(2)如果替换成功,整个同步过程就完成了。

(3)如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程。

锁的内存语义

当线程释放锁时, Java内存模型会把该线程对应的本地内存中的共享变量刷新到主内存中;
而当线程获取锁时, Java内存模型会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。
主内存与工作内存之间的通信
两个线程之间进行通信时,必须经过下面两个步骤

线程A把本地内存A中更新过的共享变量刷新到主内存中去。

线程B到主内存中去读取线程A之前已更新过的共享变量。

这两个过程就是通过JMM内存之间的交互指令实现的。

如上图所示,本地内存A和B有主内存中共享变量x的副本。

假设初始时,这三个内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。

随后,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1。

从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。

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

优点 缺点 使用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 如果线程间存在锁竞争,会带来额外的锁撤销的消耗。 适用于只有一个线程访问同步块场景。
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度。 如果始终得不到锁竞争的线程使用自旋会消耗CPU。 追求响应时间。

同步块执行速度非常快。

重量级锁 线程竞争不使用自旋,不会消耗CPU。 线程阻塞,响应时间缓慢。 追求吞吐量。

 

同步块执行速度较长。

synchronized和ReentrantLock的区别

ReentrantLock(再入锁):

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

ReentrantLock公平锁的设置

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

Reentrant Lock将锁对象化

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

总结

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

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

Java内存模型JMM

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

JMM中的主内存

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

JMM中的工作内存

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

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

  • JMM描述的是一组规则, 围绕原子性, 有序性、可见性展开
  • 相似点:存在共享区域和私有区域

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

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

JMM如何解决可见性问题


把数据从内存加载到缓存寄存器,运算结束,写回主内存.

但是当线程共享变量的时候,情况就变得非常复杂了,如果处理器对某个变量进行了修改,可能只是体现在该内核的缓存里,而运行在其它内核上的线程可能加载的是旧状态,这很可能导致一致性的问题,从理论上来说,多线程共享引入了复杂的数据依赖性问题,不管处理器,编译器怎么做重排序都必须尊重数据依赖型的要求,否则就打破了数据的正确性.这就是jmm所要解决的问题.

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

在执行程序的时候,为了提高性能,处理器和编译器常常会对指令进行重排序,需要满足以下条件:

  • 在单线程环境下不能改变程序运行的结果
  • 存在数据依赖关系的不允许重排序

以上两点可以归结为一点:
无法通过happens-before原则推导出来的, 才能进行指令的重排序
jmm内部的实现通常是依赖于所谓的内存屏障,通过禁止某些重排序的方式提供内存可见性保证,也就是实现了各种happens-before的规则,更多的复杂度在于,需要尽量确保各种编译器,各种体系结构的处理器,能够提供一致的行为。

在jmm中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作必须满足happens-before的关系。

happens-before原则

happens-before原则非常重要,它是判断数据是否存在竞争,线程是否安全的主要依据,依靠此原则我们变能解决在并发环境下两个操作存在冲突的问题。

我们分析以下代码,假设线程A happens-before线程,即线程A先于线程B发生,可以确定线程B操作后j是1是确定的,如果它们不存在happens-before原则,那么j=1就不一定能够成立。

happens-before的八大原则:

1.程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
2.锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作;
3.volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
4.传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
5.线程启动规则:Thread对象的start() 方法先行发生于此线程的每一个动作;
6.线程中断规则:对线程interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
7.线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join() 方法结束、Thread.is Alive() 的返回值手段检测到线程已经终止执行;
8.对象终结规则:一个对象的初始化完成先行发生于他的finalize() 方法的开始;
happens-before的概念
如果两个操作不满足上述任意一个happens-before规则, 那么这两个操作就没有顺序的保障, JVM可以对这两个操作进行重排序;
如果操作A happens-before操作B, 那么操作A在内存上所做的操作对操作B都是可见的。

我们约定线程A执行write操作,线程B执行read操作,且线程A优先于线程B去执行,那么线程B获得的结果是什么呢?

可以看到5,6,7,8这四个规则是可以被忽略的,因为与这段代码毫无关系。

两个线程,规则1不适用,没有锁,规则2不适用,规则3肯定也不适用,没使用volatile,规则4也不适合。

所以无法通过happens-before原则推导出A happens-before B,不知道B什么时候执行。

所以这段代码不是线程安全的。
我们只需满足2,3规则中的一个即可保证线程安全,即加同步锁或者volatile。

volatile

volatile:JVM提供的轻量级同步机制

volatile的作用:

  • 保证被volatile修饰的共享变量对所有线程总是可见的
  • 禁止指令重排序优化

volatile的可见性:
我们必须意识到volatile修饰的变量总是对所有线程立即可见的,对volatile的所有写操作总是能立即反映到其它线程中,但是对于volatile运算操作在多线程环境中并不保证安全性。
volatile变量为何立即可见?

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

volatile如何禁止重排优化?
内存屏障(Memory Barrier)
1.保证特定操作的执行顺序
2.保证某些变量的内存可见性

通过插入内存屏障指令禁止在内存屏障前后的指令执行重排序优化
强制刷出各种CPU的缓存数据, 因此任何CPU上的线程都能读取到这些数据的最新版本
volatile正是通过内存屏障实现其在内存中的语义即可见性和禁止重排优化。

volatile和synchronized的区别

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

例一

value变量的任何改变都会反映到线程中,但是若有多条线程同时访问increase方法,就会出现线程安全问题,毕竟value++操作并不具备原子性。

value++操作是先读取值,然后再写回一个新值,相当于原来的值加1分两步来完成。如果第二个线程在第一个线程读取旧值写回新值之间,读取value的值,那么第二个线程就会与第一个线程一起看到同一个值,并执行相同的加1操作,引发了线程安全问题。所以对于increase必须用synchronized修饰,以便保证线程安全,需要补充并且注意的是,synchronized关键字解决的是执行控制的问题,它会阻止其它线程获取当前对象的监控锁,这就使被synchronized保护的代码块无法被其它线程访问,也就无法并发执行。

修改线程安全:

synchronized会创建一个内存屏障指令,其保证了所有CPU结果都会直接刷到主存中,从而保证了操作的内存可见性。也保证了顺序执行。

例二

由于对boolen的修改属于原子操作,因此可以使volatile修饰该变量,使其修改对其它线程立即可见,从而达到线程安全的目的。

例三

面试时经常要写的所谓实现线程安全的单例写法,通过引入synchronized代码块试图解决多线程请求单例时重复创建单例的隐患。下面的代码在多线程环境下依然会有隐患。
单例的双重检测实现:

原因:

new singleton()创建时会有三步 :

并可以有如下的重排序优化。这样就可能导致getInstance返回null,一个线程走到了第二步,memory还是空,另一个线程判断instance不是null,直接返回memory,造成错误。

解决方法如下,使用volatile使instance禁止指令进行重排序即可,即2和3不能颠倒过来。

CAS(Compare and Swap)

像synchronized这种独占锁属于悲观锁,悲观锁始终假定,因此会屏蔽一切可能违反数据完整性的操作,除此之外,还有乐观锁,它假定不会发生并发冲突,因此只在提交操作时检查是否违反数据完整性,如果提交失败则会进行重试,而乐观锁最常见的就是CAS。

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

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

CAS思想

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

将内存位置的值与预期原值进行比较,如果相匹配则处理器会自动将内存位置的值更新为新值,否则处理器不做任何操作,这里内存位置的值V即主内存的值.

举个例子,当一个线程需要修改共享变量的值,完成这个操作先取出共享变量的值赋给A,基于A的基础进行计算得到新值B,执行完毕需要更新共享变量的值的时候,我们调用CAS方法去更新共享变量的值.

看一下之前的例子:

查看其字节码:

可以看到value++被拆分成了如下的指令,首先需要getfield拿到原始的value,也就是从我们的主内存中将value加载进当前线程的工作内存中,执行iadd进行+1的操作,之后再执行putfield把累加后的值写回我们的主内存当中。

通过volatile修饰的变量,可以保证线程之间的可见性,同时也不允许JVM对它们进行重排序。但是并不能保证这三个指令的原子执行,在多线程并发下,无法做到线程安全。

该如何解决呢?

在add前加入synchronized操作即可解决。

但是能否尽量提升性能呢?

AtomicInteger

可以使用AtomicInteger来满足需求,其位于java.util.concurrent.atomic包中,
从AtomicInteger的内部属性可以看出,它依赖于unsafe提供的一些底层能力,进行底层操作,以volatile的value字段记录数值以保证可见性。

其中的getAndIncrement方法可以解决上面value++的不安全性。此方法会利用value字段的地址偏移直接完成操作。

点进getAndIncrement,因为需要返回数值多以需要添加失败重试的逻辑。

而向返回布尔类型的,因为其返回值表现得就是成功与否,所以不需要进行重试。

Unsafe里的这些方法,如compareAndSetInt 则实现了CAS。

CAS多数情况下对开发者来说是透明的,我们更多的是使用并发包间接享受到lock-free机制在扩展性上的好处。

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

缺点

  • 若循环时间长,则开销很大
  • 只能保证一个共享变量的原子操作
  • ABA问题 (解决:AtomicStampedReference)

Java线程池

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

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

Fork/Join框架

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


Work-Stealing算法:某个线程从其它队列里窃取任务来执行。

为什么要使用线程池

  • 降低资源消耗
  • 提高线程的可管理性

Executor的框架

J.U.C的三个Executor接口

包路径:java.util.concurrent
Executor:运行新任务的简单接口,将任务提交和任务执行细节解耦。

ExecutorService:具备管理执行器和任务生命周期的方法,提交任务机制更完善。
ScheduledExecutorService:支持Future和定期执行任务。

ThreadPoolExecutor


hreadPoolExecutor的构造函数

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

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

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

线程池的状态

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

工作线程的状态

线程池的大小如何确定?

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


文章 Java多线程与并发 转载需要注明出处
喜欢 (0)

您必须 登录 才能发表评论!