辛苦牛,掌握主流技术栈,包括前端后端,已经7年时间,曾在税务机关从事开发工作,目前在国企任职。希望通过自己的不断分享,可以帮助各位想或者已经走在这条路上的朋友一定的帮助
❤️金九银十马上就要来啦,各位小伙伴们有计划跳槽的要开始准备了,博主接下来一段时间会给大家持续更新面试题目,大家持续关注一下,感谢
今天是并发的相关面试题,欢迎指正
之前的面试文章链接也给到大家
金九银十面试题之Mysql
金九银十面试题之设计模式
金九银十面试题之数据结构和算法
金九银十面试题之Mybatis
金九银十面试题之《Spring Data JPA、Spring MVC、AOP》
金九银十面试题之《Spring IOC》
金九银十面试题之JUC
Java 线程的通信由 JMM 控制,JMM 的主要目的是定义程序中各种变量的访问规则。变量包括实例字段、 静态字段,但不包括局部变量与方法参数,因为它们是线程私有的,不存在多线程竞争。JMM 遵循 一个基本原则:只要不改变程序执行结果,编译器和处理器怎么优化都行。例如编译器分析某个锁只会 单线程访问就消除锁,某个 volatile 变量只会单线程访问就把它当作普通变量。
JMM 规定所有变量都存储在主内存,每条线程有自己的工作内存,工作内存中保存被该线程使用的变量 的主内存副本,线程对变量的所有操作都必须在工作空间进行,不能直接读写主内存数据。不同线程间
无法直接访问对方工作内存中的变量,线程通信必须经过主内存。
关于主内存与工作内存的交互,即变量如何从主内存拷⻉到工作内存、从工作内存同步回主内存,JMM 定义了 8 种原子操作:
操作 | 作用变量范围 | 作用| |
---|---|---|
lock | 主内存 | 把变量标识为线程独占状态 |
unlock | 主内存 | 释放处于锁定状态的变量 |
read | 主内存 | 把变量值从主内存传到工作内存 |
load | 工作内存 | 把 read 得到的值放入工作内存的变量副本 |
user | 工作内存 | 把工作内存中的变量值传给执行引擎 |
assign | 工作内存 | 把从执行引擎接收的值赋给工作内存变量 |
store | 工作内存 | 把工作内存的变量值传到主内存 |
write | 主内存 | 把 store 取到的变量值放入主内存变量中 |
不管怎么重排序,单线程程序的执行结果不能改变,编译器和处理器必须遵循 as-if-serial 语义。
为了遵循 as-if-serial,编译器和处理器不会对存在数据依赖关系的操作重排序,因为这种重排序会改变执 行结果。但是如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。
as-if-serial 把单线程程序保护起来,给程序员一种幻觉:单线程程序是按程序的顺序执行的。
先行发生原则,JMM 定义的两项操作间的偏序关系,是判断数据是否存在竞争的重要手段。
JMM 将 happens-before 要求禁止的重排序按是否会改变程序执行结果分为两类。对于会改变结果的重
排序 JMM 要求编译器和处理器必须禁止,对于不会改变结果的重排序,JMM 不做要求。
JMM 存在一些天然的 happens-before 关系,无需任何同步器协助就已经存在。如果两个操作的关系不
在此列,并且无法从这些规则推导出来,它们就没有顺序性保障,虚拟机可以对它们随意进行重排序。
程序次序规则:一个线程内写在前面的操作先行发生于后面的。
管程锁定规则: unlock 操作先行发生于后面对同一个锁的 lock 操作。
volatile 规则:对 volatile 变量的写操作先行发生于后面的读操作。
线程启动规则:线程的 start 方法先行发生于线程的每个动作。
线程终止规则:线程中所有操作先行发生于对线程的终止检测。
对象终结规则:对象的初始化先行发生于 finalize 方法。
传递性:如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C。
as-if-serial 保证单线程程序的执行结果不变,happens-before 保证正确同步的多线程程序的执行结果 不变。
这两种语义的目的都是为了在不改变程序执行结果的前提下尽可能提高程序执行并行度。
为了提高性能,编译器和处理器通常会对指令进行重排序,重排序指从源代码到指令序列的重排序,分为三种:
JMM 为 volatile 定义了一些特殊访问规则,当变量被定义为 volatile 后具备两种特性:
静态变量 i 执行多线程 i++ 的不安全问题
自增语句由 4 条字节码指令构成的,依次为 getstatic 、 iconst_1 、 iadd 、 putstatic ,当把 getstatic i的值取到操作栈顶时,volatile保证了i值在此刻正确,但在执行iconst_1、iadd时,其他线程可能已经改变了 i 值,操作栈顶的值就变成了过期数据,所以putstatic 执行后就可能 把较小的 i 值同步回了主内存。
适用场景
1 运算结果并不依赖变量的当前值。2 一写多读,只有单一的线程修改变量值。
内存语义
写一个 volatile 变量时,把该线程工作内存中的值刷新到主内存。
读一个 volatile 变量时,把该线程工作内存值置为无效,从主内存读取。
指令重排序特点
第二个操作是 volatile 写,不管第一个操作是什么都不能重排序,确保写之前的操作不会被重排序到写之 后。
第一个操作是 volatile 读,不管第二个操作是什么都不能重排序,确保读之后的操作不会被重排序到读之 前。
第一个操作是 volatile 写,第二个操作是 volatile 读不能重排序。
JSR-133 增强 volatile 语义的原因
在旧的内存模型中,虽然不允许 volatile 变量间重排序,但允许 volatile 变量与普通变量重排序,可能 导致内存不可⻅问题。JSR-133 严格限制编译器和处理器对 volatile 变量与普通变量的重排序,确保 volatile 的写-读和锁的释放-获取具有相同的内存语义。
final 可以保证可⻅性,被 final 修饰的字段在构造方法中一旦被初始化完成,并且构造方法没有把 this引用传递出去,在其他线程中就能看⻅ final 字段值。
在旧的 JMM 中,一个严重缺陷是线程可能看到 final 值改变。比如一个线程看到一个 int 类型 final 值为 0,此时该值是未初始化前的零值,一段时间后该值被某线程初始化,再去读这个 final 值会发现值变为 1。
为修复该漏洞,JSR-133 为 final 域增加重排序规则:只要对象是正确构造的(被构造对象的引用在构造 方法中没有逸出),那么不需要使用同步就可以保证任意线程都能看到这个 final 域初始化后的值。
写 final 域重排序规则
禁止把 final 域的写重排序到构造方法之外,编译器会在 final 域的写后,构造方法的 return 前,插入一个 Store Store 屏障。确保在对象引用为任意线程可⻅之前,对象的 final 域已经初始化过。
读 final 域重排序规则
在一个线程中,初次读对象引用和初次读该对象包含的 final 域,JMM 禁止处理器重排序这两个操作。 编译器在读 final 域操作的前面插入一个 Load Load 屏障,确保在读一个对象的 final 域前一定会先读 包含这个 final 域的对象引用。
每个 Java 对象都有一个关联的 monitor,使用 synchronized 时 JVM 会根据使用环境找到对象的 monitor,根据 monitor 的状态进行加解锁的判断。如果成功加锁就成为该 monitor 的唯一持有者, monitor 在被释放前不能再被其他线程获取。
同步代码块使用 monitorenter 和 monitorexit 这两个字节码指令获取和释放 monitor。这两个字节码 指令都需要一个引用类型的参数指明要锁定和解锁的对象,对于同步普通方法,锁是当前实例对象;对于静态同步方法,锁是当前类的 Class 对象;对于同步方法块,锁是 synchronized 括号里的对象。
执行 monitorenter 指令时,首先尝试获取对象锁。如果这个对象没有被锁定,或当前线程已经持有 锁,就把锁的计数器加 1,执行 monitorexit 指令时会将锁计数器减 1。一旦计数器为 0 锁随即就被释放。
例如有两个线程 A、B 竞争 monitor,当 A 竞争到锁时会将 monitor 中的 owner 设置为 A,把 B 阻塞 并放到等待资源的 ContentionList 队列。ContentionList 中的部分线程会进入 EntryList,EntryList 中 的线程会被指定为 OnDeck 竞争候选者,如果获得了锁资源将进入 Owner 状态,释放锁后进入!Owner 状态。被阻塞的线程会进入 WaitSet。
被 synchronized 修饰的同步块对一条线程来说是可重入的,并且同步块在持有锁的线程释放锁前会阻 塞其他线程进入。从执行成本的⻆度看,持有锁是一个重量级的操作。Java 线程是映射到操作系统的内 核线程上的,如果要阻塞或唤醒一条线程,需要操作系统帮忙完成,不可避免用户态到核心态的转换。
不公平的原因
所有收到锁请求的线程首先自旋,如果通过自旋也没有获取锁将被放入 ContentionList,该做法对于已 经进入队列的线程不公平。
为了防止 ContentionList 尾部的元素被大量线程进行 CAS 访问影响性能,Owner 线程会在释放锁时将ContentionList 的部分线程移动到 EntryList 并指定某个线程为 OnDeck 线程,该行为叫做竞争切换, 牺牲了公平性但提高了性能。
JDK 6 对 synchronized 做了很多优化,引入了自适应自旋、锁消除、锁粗化、偏向锁和轻量级锁等提 高锁的效率,锁一共有 4 个状态,级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁,状态会 随竞争情况升级。锁可以升级但不能降级,这种只能升级不能降级的锁策略是为了提高锁获得和释放的效率。
同步对性能最大的影响是阻塞,挂起和恢复线程的操作都需要转入内核态完成。许多应用上共享数据的 锁定只会持续很短的时间,为了这段时间去挂起和恢复线程并不值得。如果机器有多个处理器核心,我 们可以让后面请求锁的线程稍等一会,但不放弃处理器的执行时间,看看持有锁的线程是否很快会释放 锁。为了让线程等待只需让线程执行一个忙循环,这项技术就是自旋锁。
自旋锁在 JDK1.4 就已引入,默认关闭,在 JDK6 中改为默认开启。自旋不能代替阻塞,虽然避免了线程 切换开销,但要占用处理器时间,如果锁被占用的时间很短,自旋的效果就会非常好,反之只会白白消耗处理器资源。如果自旋超过了限定的次数仍然没有成功获得锁,就应挂起线程,自旋默认限定次数是10。
JDK6 对自旋锁进行了优化,自旋时间不再固定,而是由前一次的自旋时间及锁拥有者的状态决定。
如果在同一个锁上,自旋刚刚成功获得过锁且持有锁的线程正在运行,虚拟机会认为这次自旋也很可能 成功,进而允许自旋持续更久。如果自旋很少成功,以后获取锁时将可能直接省略掉自旋,避免浪费处 理器资源。
有了自适应自旋,随着程序运行时间的增⻓,虚拟机对程序锁的状况预测就会越来越精准。
锁消除指即时编译器对检测到不可能存在共享数据竞争的锁进行消除。
主要判定依据来源于逃逸分析,如果判断一段代码中堆上的所有数据都只被一个线程访问,就可以当作 栈上的数据对待,认为它们是线程私有的而无须同步。
原则需要将同步块的作用范围限制得尽量小,只在共享数据的实际作用域中进行同步,这是为了使等待
锁的线程尽快拿到锁。
但如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体之外的,即使 没有线程竞争也会导致不必要的性能消耗。因此如果虚拟机探测到有一串零碎的操作都对同一个对象加 锁,将会把同步的范围扩展到整个操作序列的外部。
偏向锁是为了在没有竞争的情况下减少锁开销,锁会偏向于第一个获得它的线程,如果在执行过程中锁
一直没有被其他线程获取,则持有偏向锁的线程将不需要进行同步。
当锁对象第一次被线程获取时,虚拟机会将对象头中的偏向模式设为 1,同时使用 CAS 把获取到锁的线 程 ID 记录在对象的 Mark Word 中。如果 CAS 成功,持有偏向锁的线程以后每次进入锁相关的同步块 都不再进行任何同步操作。
一旦有其他线程尝试获取锁,偏向模式立即结束,根据锁对象是否处于锁定状态决定是否撤销偏向,后 续同步按照轻量级锁那样执行。
轻量级锁是为了在没有竞争的前提下减少重量级锁使用操作系统互斥量产生的性能消耗。
在代码即将进入同步块时,如果同步对象没有被锁定,虚拟机将在当前线程的栈帧中建立一个锁记录空间,存储锁对象目前 Mark Word 的拷⻉。然后虚拟机使用 CAS 尝试把对象的 Mark Word 更新为指向 锁记录的指针,如果更新成功即代表该线程拥有了锁,锁标志位将转变为 00,表示处于轻量级锁定状态。
如果更新失败就意味着至少存在一条线程与当前线程竞争。虚拟机检查对象的 Mark Word 是否指向当 前线程的栈帧,如果是则说明当前线程已经拥有了锁,直接进入同步块继续执行,否则说明锁对象已经 被其他线程抢占。如果出现两条以上线程争用同一个锁,轻量级锁就不再有效,将膨胀为重量级锁,锁标志状态变为 10,此时Mark Word 存储的就是指向重量级锁的指针,后面等待锁的线程也必须阻塞。
解锁同样通过 CAS 进行,如果对象 Mark Word 仍然指向线程的锁记录,就用 CAS 把对象当前的 Mark Word 和线程复制的 Mark Word 替换回来。假如替换成功同步过程就顺利完成了,如果失败则说明有其他线程尝试过获取该锁,就要在释放锁的同时唤醒被挂起的线程。
偏向锁的优点是加解锁不需要额外消耗,和执行非同步方法比仅存在纳秒级差距,缺点是如果存在锁竞争会带来额外锁撤销的消耗,适用只有一个线程访问同步代码块的场景。
轻量级锁的优点是竞争线程不阻塞,程序响应速度快,缺点是如果线程始终得不到锁会自旋消耗 CPU, 适用追求响应时间、同步代码块执行快的场景。
重量级锁的优点是线程竞争不使用自旋不消耗CPU,缺点是线程会阻塞,响应时间慢,适应追求吞吐 量、同步代码块执行慢的场景。
Lock 接是 juc 包的顶层接口,基于Lock 接口,用户能够以非块结构来实现互斥同步,摆脱了语言特性 束缚,在类库层面实现同步。Lock 并未用到 synchronized,而是利用了 volatile 的可⻅性。
重入锁 ReentrantLock 是 Lock 最常⻅的实现,与 synchronized 一样可重入,不过它增加了一些高级 功能:
一般优先考虑使用 synchronized:
以非公平锁为例,通过 nonfairTryAcquire 方法获取锁,该方法增加了再次获取同步状态的处理逻 辑:判断当前线程是否为获取锁的线程来决定获取是否成功,如果是获取锁的线程再次请求则将同步状 态值增加并返回 true,表示获取同步状态成功。
成功获取锁的线程再次获取锁将增加同步状态值,释放同步状态时将减少同步状态值。如果锁被获取了 n次,那么前n-1次 tryRelease 方法必须都返回fasle,只有同步状态完全释放才能返回true,该方 法将同步状态是否为 0 作为最终释放条件,释放时将占有线程设置为null 并返回 true。
对于非公平锁只要 CAS 设置同步状态成功则表示当前线程获取了锁,而公平锁则不同。公平锁使用 tryAcquire 方法,该方法与 nonfairTryAcquire 的唯一区别就是判断条件中多了对同步队列中当 前节点是否有前驱节点的判断,如果该方法返回 true 表示有线程比当前线程更早请求锁,因此需要等待前驱线程获取并释放锁后才能获取锁。
ReentrantLock 是排他锁,同一时刻只允许一个线程访问,读写锁在同一时刻允许多个读线程访问,在 写线程访问时,所有的读写线程均阻塞。读写锁维护了一个读锁和一个写锁,通过分离读写锁使并发性 相比排他锁有了很大提升。
读写锁依赖 AQS 来实现同步功能,读写状态就是其同步器的同步状态。读写锁的自定义同步器需要在 同步状态,即一个 int变量上维护多个读线程和一个写线程的状态。读写锁将变量切分成了两个部分,
高 16 位表示读,低 16 位表示写。
写锁是可重入排他锁,如果当前线程已经获得了写锁则增加写状态,如果当前线程在获取写锁时,读锁 已经被获取或者该线程不是已经获得写锁的线程则进入等待。写锁的释放与 ReentrantLock 的释放类 似,每次释放减少写状态,当写状态为 0 时表示写锁已被释放。
读锁是可重入共享锁,能够被多个线程同时获取,在没有其他写线程访问时,读锁总会被成功获取。如 果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已被其他线程获取则进
入等待。读锁每次释放会减少读状态,减少的值是(1<<16),读锁的释放是线程安全的。
锁降级指把持住当前拥有的写锁,再获取读锁,随后释放先前拥有的写锁。
锁降级中读锁的获取是必要的,这是为了保证数据可⻅性,如果当前线程不获取读锁而直接释放写锁, 假设此刻另一个线程 A 获取写锁修改了数据,当前线程无法感知线程 A 的数据更新。如果当前线程获取 读锁,遵循锁降级的步骤,A 将被阻塞,直到当前线程使用数据并释放读锁之后,线程 A 才能获取写锁 进行数据更新。
AQS 队列同步器是用来构建锁或其他同步组件的基础框架,它使用一个 volatile int state 变量作为共享 资源,如果线程获取资源失败,则进入同步队列等待;如果获取成功就执行临界区代码,释放资源时会 通知同步队列中的等待线程。
同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,对同步状态进行更改需要使用同步器提供的 3个方法 getState 、 setState 和 compareAndSetState ,它们保 证状态改变是安全的。子类推荐被定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步 接口,它仅仅定义若干同步状态获取和释放的方法,同步器既支持独占式也支持共享式。
同步器是实现锁的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。锁面向使用者,定义了 使用者与锁交互的接口,隐藏实现细节;同步器面向锁的实现者,简化了锁的实现方式,屏蔽了同步状 态管理、线程排队、等待与唤醒等底层操作。
每当有新线程请求资源时都会进入一个等待队列,只有当持有锁的线程释放锁资源后该线程才能持有资 源。等待队列通过双向链表实现,线程被封装在链表的 Node 节点中,Node 的等待状态包括: CANCELLED(线程已取消)、SIGNAL(线程需要唤醒)、CONDITION (线程正在等待)、 PROPAGATE(后继节点会传播唤醒操作,只在共享模式下起作用)。
独占模式表示锁只会被一个线程占用,其他线程必须等到持有锁的线程释放锁后才能获取锁,同一时间
只能有一个线程获取到锁。
共享模式表示多个线程获取同一个锁有可能成功,ReadLock 就采用共享模式。
独占模式通过 acquire 和 release 方法获取和释放锁,共享模式通过 acquireShared 和 releaseShared 方法获取和释放锁。
获取同步状态时,调用 acquire 方法,维护一个同步队列,使用 tryAcquire 方法安全地获取线程 同步状态,获取失败的线程会被构造同步节点并通过addWaiter 方法加入到同步队列的尾部,在队列中自旋。之后调用acquireQueued 方法使得该节点以死循环的方式获取同步状态,如果获取不到则阻 塞,被阻塞线程的唤醒主要依靠前驱节点的出队或被中断实现,移出队列或停止自旋的条件是前驱节点 是头结点且成功获取了同步状态。
释放同步状态时,同步器调用 tryRelease 方法释放同步状态,然后调用 unparkSuccessor 方法唤 醒头节点的后继节点,使后继节点重新尝试获取同步状态。
头节点是成功获取到同步状态的节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点。
目的是维护同步队列的 FIFO 原则,节点和节点在循环检查的过程中基本不通信,而是简单判断自己的 前驱是否为头节点,这样就使节点的释放规则符合 FIFO,并且也便于对过早通知的处理,过早通知指前驱 节点不是头节点的线程由于中断被唤醒。
获取同步状态时,调用 acquireShared 方法,该方法调用 tryAcquireShared 方法尝试获取同步状 态,返回值为 int 类型,返回值不小于于 0 表示能获取同步状态。因此在共享式获取锁的自旋过程中, 成功获取同步状态并退出自旋的条件就是该方法的返回值不小于0。
释放同步状态时,调用 releaseShared方法,释放后会唤醒后续处于等待状态的节点。它和独占式的区别在于 tryReleaseShared 方法必须确保同步状态安全释放,通过循环 CAS 保证,因为释放同步状 态的操作会同时来自多个线程。
NEW:新建状态,线程被创建且未启动,此时还未调用 start 方法。
RUNNABLE:Java 将操作系统中的就绪和运行两种状态统称为 RUNNABLE,此时线程有可能在等待时 间片,也有可能在执行。
BLOCKED:阻塞状态,可能由于锁被其他线程占用、调用了 sleep 或 join 方法、执行了 wait 方法 等。
WAITING:等待状态,该状态线程不会被分配 CPU 时间片,需要其他线程通知或中断。可能由于调用 了无参的 wait 和 join 方法。
TIME_WAITING:限期等待状态,可以在指定时间内自行返回。导可能由于调用了带参的 wait 和 join 方法。
TERMINATED:终止状态,表示当前线程已执行完毕或异常退出。
守护线程是一种支持型线程,可以通过 setDaemon(true) 将线程设置为守护线程,但必须在线程启动前设置。
守护线程被用于完成支持性工作,但在 JVM 退出时守护线程中的 finally 块不一定执行,因为 JVM 中没 有非守护线程时需要立即退出,所有守护线程都将立即终止,不能靠在守护线程使用 finally 确保关闭资 源。
命令式编程中线程的通信机制有两种,共享内存和消息传递。在共享内存的并发模型里线程间共享程序 的公共状态,通过写-读内存中的公共状态进行隐式通信。在消息传递的并发模型里线程间没有公共状 态,必须通过发送消息来显式通信。Java 并发采用共享内存模型,线程之间的通信总是隐式进行,整个 通信过程对程序员完全透明。
volatile 告知程序任何对变量的读需要从主内存中获取,写必须同步刷新回主内存,保证所有线程对变 量访问的可⻅性。
synchronized 确保多个线程在同一时刻只能有一个处于方法或同步块中,保证线程对变量访问的原子 性、可⻅性和有序性。
等待通知机制指一个线程 A 调用了对象的 wait 方法进入等待状态,另一线程 B 调用了对象的 notify/notifyAll 方法,线程 A 收到通知后结束阻塞并执行后序操作。对象上的 wait 和 notify/notifyAll 如同开关信号,完成等待方和通知方的交互。
如果一个线程执行了某个线程的 join 方法,这个线程就会阻塞等待执行了 join 方法的线程终止, 这里涉及等待/通知机制。 join 底层通过 wait 实现,线程终止时会调用自身的 notifyAll 方法,通知所有等待在该线程对象上的线程。
管道 IO 流用于线程间数据传输,媒介为内存。PipedOutputStream 和 PipedWriter 是输出流,相当于 生产者,PipedInputStream 和 PipedReader 是输入流,相当于消费者。管道流使用一个默认大小为
1KB 的循环缓冲数组。输入流从缓冲数组读数据,输出流往缓冲数组中写数据。当数组已满时,输出流 所在线程阻塞;当数组首次为空时,输入流所在线程阻塞。
ThreadLocal 是线程共享变量,但它可以为每个线程创建单独的副本,副本值是线程私有的,互相之间 不影响。
降低资源消耗,复用已创建的线程,降低开销、控制最大并发数。 隔离线程环境,可以配置独立线程池,将较慢的线程与较快的隔离开,避免相互影响。 实现任务线程队列缓冲策略和拒绝机制。实现某些与时间相关的功能,如定时执行、周期执行等。
可以通过 Executors 的静态工厂方法创建线程池:
可以调用 shutdown 或 shutdownNow 方法关闭线程池,原理是遍历线程池中的工作线程,然后逐个调用线程的 interrupt 方法中断线程,无法响应中断的任务可能永远无法终止。
区别是 shutdownNow 首先将线程池的状态设为 STOP,然后尝试停止正在执行或暂停任务的线程,并返回等待执行任务的列表。而 shutdown 只是将线程池的状态设为 SHUTDOWN,然后中断没有正在 执行任务的线程。
通常调用 shutdown 来关闭线程池,如果任务不一定要执行完可调用 shutdownNow 。
可以从以下⻆度分析:1任务性质:CPU 密集型、IO 密集型和混合型。2任务优先级。3任务执行时 间。4任务依赖性:是否依赖其他资源,如数据库连接。
性质不同的任务可用不同规模的线程池处理,CPU 密集型任务应配置尽可能小的线程,如配置 Ncpu+1 个线程的线程池。由于 IO 密集型任务线程并不是一直在执行任务,应配置尽可能多的线程,如2*Ncpu。 混合型的任务,如果可以拆分,将其拆分为一个 CPU 密集型任务和一个 IO 密集型任务,只要两个任务 执行的时间相差不大那么分解后的吞吐量将高于串行执行的吞吐量,如果相差太大则没必要 分解。
优先级不同的任务可以使用优先级队列 PriorityBlockingQueue 处理。 执行时间不同的任务可以交给不同规模的线程池处理,或者使用优先级队列让执行时间短的任务先执行。
依赖数据库连接池的任务,由于线程提交 SQL 后需要等待数据库返回的结果,等待的时间越⻓ CPU 空闲的时间就越⻓,因此线程数应该尽可能地设置大一些,提高 CPU 的利用率。 建议使用有界队列,能增加系统的稳定性和预警能力,可以根据需要设置的稍微大一些。
阻塞队列支持阻塞插入和移除,当队列满时,阻塞插入元素的线程直到队列不满。当队列为空时,获取 元素的线程会被阻塞直到队列非空。阻塞队列常用于生产者和消费者的场景,阻塞队列就是生产者用来 存放元素,消费者用来获取元素的容器。
Java 中的阻塞队列
ArrayBlockingQueue,由数组组成的有界阻塞队列,默认情况下不保证线程公平,有可能先阻塞的线程最后才访问队列。
LinkedBlockingQueue,由链表结构组成的有界阻塞队列,队列的默认和最大⻓度为 Integer 最大值。
PriorityBlockingQueue,支持优先级的无界阻塞队列,默认情况下元素按照升序排序。可自定义 compareTo 方法指定排序规则,或者初始化时指定 Comparator 排序,不能保证同优先级元素的顺序。
DelayQueue,支持延时获取元素的无界阻塞队列,使用优先级队列实现。创建元素时可以指定多久才 能从队列中获取当前元素,只有延迟期满时才能从队列中获取元素,适用于缓存和定时调度。
SynchronousQueue,不存储元素的阻塞队列,每一个 put 必须等待一个 take。默认使用非公平策 略,也支持公平策略,适用于传递性场景,吞吐量高。
LinkedTransferQueue,链表组成的无界阻塞队列,相对于其他阻塞队列多了 tryTransfer 和 transfer 方法。transfer方法:如果当前有消费者正等待接收元素,可以把生产者传入的元素立刻 传输给消费者,否则会将元素放在队列的尾节点并等到该元素被消费者消费才返回。 tryTransfer 方法用来试探生产者传入的元素能否直接传给消费者,如果没有消费者等待接收元素则返回 false,和 transfer 的区别是无论消费者是否消费都会立即返回。
LinkedBlockingDeque,链表组成的双向阻塞队列,可从队列的两端插入和移出元素,多线程同时入队 时减少了竞争。
实现原理
使用通知模式实现,生产者往满的队列里添加元素时会阻塞,当消费者消费后,会通知生产者当前队列 可用。当往队列里插入一个元素,如果队列不可用,阻塞生产者主要通过 LockSupport 的 park 方法 实现,不同操作系统中实现方式不同,在Linux下使用的是系统方法pthread_cond_wait 实现。
ThreadLoacl 是线程共享变量,主要用于一个线程内跨类、方法传递数据。ThreadLoacl 有一个静态内 部类 ThreadLocalMap,其 Key 是 ThreadLocal 对象,值是 Entry 对象,Entry 中只有一个 Object 类 的 vaule 值。ThreadLocal 是线程共享的,但 ThreadLocalMap 是每个线程私有的。ThreadLocal 主要 有 set、get 和 remove 三个方法。
set 方法
首先获取当前线程,然后再获取当前线程对应的 ThreadLocalMap 类型的对象 map。如果 map 存在就
直接设置值,key 是当前的 ThreadLocal 对象,value 是传入的参数。
如果 map 不存在就通过 createMap 方法为当前线程创建一个 ThreadLocalMap 对象再设置值。
get 方法
首先获取当前线程,然后再获取当前线程对应的 ThreadLocalMap 类型的对象 map。如果 map 存在就 以当前 ThreadLocal 对象作为 key 获取 Entry 类型的对象 e,如果 e 存在就返回它的 value 属性。
如果 e 不存在或者 map 不存在,就调用 setInitialValue 方法先为当前线程创建一个 ThreadLocalMap 对象然后返回默认的初始值 null。
remove 方法
首先通过当前线程获取其对应的 ThreadLocalMap 类型的对象 m,如果 m 不为空,就解除
ThreadLocal 这个 key 及其对应的 value 值的联系。
存在的问题
线程复用会产生脏数据,由于线程池会重用 Thread 对象,因此与 Thread 绑定的 ThreadLocal 也会被 重用。如果没有调用 remove 清理与线程相关的 ThreadLocal 信息,那么假如下一个线程没有调用 set 设置初始值就可能 get 到重用的线程信息。
ThreadLocal 还存在内存泄漏的问题,由于 ThreadLocal 是弱引用,但 Entry 的 value 是强引用,因此 当 ThreadLocal 被垃圾回收后,value 依旧不会被释放。因此需要及时调用 remove 方法进行清理操作。
希望博主收集的内容能帮到大家,祝大家能找到一个好的工作,过好的生活,如有错误欢迎指正。