java在并发编程中使用的工具类
-
java.util.concurrent
-
java.util.concurrent.atomic
-
java.util.concurrent.locks
二、进程/线程 并发/并行
1 进程
进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
2 线程
通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程 可以利用进程所拥有的资源,在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。
3 进程与线程的区别
区别 | 线程 | 进程 |
---|---|---|
根本区别 | 线程是处理器任务调度和执行的基本单位 | 进程是操作系统资源分配的基本单位 |
资源开销 | 同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小 | 每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销 |
包含关系 | 线程是进程的一部分 | 一个进程内有多个线程 |
内存分配 | 同一进程的线程共享本进程的地址空间和资源 | 进程之间的地址空间和资源是相互独立的 |
影响关系 | 一个线程崩溃整个进程都死掉 | 一个进程崩溃后,在保护模式下不会对其他进程产生影响 |
执行过程 | 线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制 | 每个独立的进程有程序运行的入口、顺序执行序列和程序出口 |
4 线程的五种状态
-
新建(new):新创建了一个线程对象。
-
可运行(runnable):线程对象创建后,当调用线程对象的 start()方法,该线程处于就绪状态,等待被线程调度选中,获取cpu的使用权。
-
运行(running):可运行状态(runnable)的线程获得了cpu时间片(timeslice),执行程序代码。
注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
-
阻塞(block):处于运行状态中的线程由于某种原因,暂时放弃对 CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被 CPU 调用以进入到运行状态。
阻塞的情况分三种:
(一). 等待阻塞:运行状态中的线程执行 wait()方法,JVM会把该线程放入等待队列(waitting queue)中,使本线程进入到等待阻塞状态;
(二). 同步阻塞:线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),,则JVM会把该线程放入锁池(lock pool)中,线程会进入同步阻塞状态;
(三). 其他阻塞: 通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。
5.死亡(dead):线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。
5 线程的四种创建方式
-
继承 Thread 类;
-
实现 Runnable 接口;
-
实现 Callable 接口;
-
使用 Executors 工具类创建线程池
6 并发与并行
-
并发
多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行。(同一时刻多个线程在访问同一个资源)
-
并行
单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的“同时进行”。(多项工作一起执行)
7 并发编程
优点:
-
充分利用多核CPU的计算能力:通过并发编程的形式可以将多核CPU的计算能力发挥到极致,性能得到提升
-
方便进行业务拆分,提升系统并发能力和性能
缺点:内存泄漏、上下文切换、线程安全、死锁等问题
三要素:
-
原子性:原子,即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。
-
可见性:一个线程对共享变量的修改,另一个线程能够立刻看到
-
有序性:程序执行的顺序按照代码的先后顺序执行
出现线程安全问题的原因及解决办法:
-
线程切换带来的原子性问题(JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题)
-
缓存导致的可见性问题(synchronized、volatile、LOCK,可以解决可见性问题)
-
编译优化带来的有序性问题(Happens-Before 规则可以解决有序性问题)
8 多线程
是什么:
多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务。
优点:
可以提高 CPU 的利用率。在多线程程序中,一个线程必须等待的时候,CPU 可以运行其它的线程而不是等待,这样就大大提高了程序的效率。也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。
缺点:
-
线程也是程序,所以线程需要占用内存,线程越多占用内存也越多
-
多线程需要协调和管理,所以需要 CPU 时间跟踪线程
-
线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题
三、线程池
1 什么是线程池?
线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。
2 Executors类创建四种常见线程池
(1)newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
(2)newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。如果希望在服务器上使用线程池,建议使用 newFixedThreadPool方法来创建线程池,这样能获得更好的性能。
(3) newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。
(4)newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
3 线程池有什么优点?
-
降低资源消耗:重用存在的线程,减少对象创建销毁的开销。
-
提高响应速度。可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
-
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
-
附加功能:提供定时执行、定期执行、单线程、并发数控制等功能。
4 线程池都有哪些状态?
-
RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。
-
SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。
-
STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。
-
TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。
-
TERMINATED:terminated()方法结束后,线程池的状态就会变成这个。
5 线程池之ThreadPoolExecutor
5.1 Executors和ThreaPoolExecutor创建线程池的区别
《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
Executors 各个方法的弊端:
-
newFixedThreadPool 和 newSingleThreadExecutor: 主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM。
-
newCachedThreadPool 和 newScheduledThreadPool: 主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。
ThreaPoolExecutor创建线程池方式只有一种,就是走它的构造函数,参数自己指定
5.2 你知道怎么创建线程池吗?
创建线程池的方式有多种,这里你只需要答 ThreadPoolExecutor 即可。
ThreadPoolExecutor() 是最原始的线程池创建,也是阿里巴巴 Java 开发手册中明确规范的创建线程池的方式。
5.3 ThreadPoolExecutor构造函数重要参数分析
ThreadPoolExecutor
3 个最重要的参数:
-
corePoolSize
:核心线程数,线程数定义了最小可以同时运行的线程数量。 -
maximumPoolSize
:线程池中允许存在的工作线程的最大数量 -
workQueue
:当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,任务就会被存放在队列中。
ThreadPoolExecutor
其他常见参数:
-
keepAliveTime
:线程池中的线程数量大于corePoolSize
的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime
才会被回收销毁; -
unit
:keepAliveTime
参数的时间单位。 -
threadFactory
:为线程池提供创建新线程的线程工厂 -
handler
:线程池任务队列超过 maxinumPoolSize 之后的拒绝策略
5.4 ThreadPoolExecutor饱和策略
ThreadPoolExecutor
饱和策略定义:
如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,ThreadPoolTaskExecutor
定义一些策略:
-
ThreadPoolExecutor.AbortPolicy
:抛出RejectedExecutionException
来拒绝新任务的处理。 -
ThreadPoolExecutor.CallerRunsPolicy
:调用执行自己的线程运行任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。 -
ThreadPoolExecutor.DiscardPolicy
:不处理新任务,直接丢弃掉。 -
ThreadPoolExecutor.DiscardOldestPolicy
: 此策略将丢弃最早的未处理的任务请求。
举个例子: Spring 通过 ThreadPoolTaskExecutor
或者我们直接通过 ThreadPoolExecutor
的构造函数创建线程池的时候,当我们不指定 RejectedExecutionHandler
饱和策略的话来配置线程池的时候默认使用的是 ThreadPoolExecutor.AbortPolicy
。在默认情况下,ThreadPoolExecutor
将抛出 RejectedExecutionException
来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 ThreadPoolExecutor.CallerRunsPolicy
。当最大池被填满时,此策略为我们提供可伸缩队列。
四、volatile 理解?
volatile是java虚拟机提供的轻量同步机制
(1)保证可见性
1 JMM模型的线程工作
各个线程对主内存中共享变量X的操作都是各个线程各自拷贝到自己的工作内存操作后再写回主内存中。
2 存在的问题:
如果一个线程A 修改了共享变量X的值还未写回主内存,这是另外一个线程B又对内存中的一个共享变量X进行操作,但是此时线程A工作内存中的共享变量对线程B来说事并不可见的。
这种工作内存与主内存延迟的现象就会造成了可见性的问题。
3 解决(volatile):
当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值
(2)不保证原子性
1 原子性:
不可分割、完整性,即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割,需要整体完整,要么同时成功,要么同时失败
2 解决方法:
1》加入synchronized
2》使用JUC下的Atomic*类(原子操作类)
3》使用locks包下的锁
(3)禁止指令重排
1 指令重排:
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
2 指令重排过程:
源代码 -> 编辑器优化的重排 -> 指令并行的重排 -> 内存系统的重排 ->最终执行的指令
3 内存屏障作用:
1》保证特定操作的执行顺序
2》保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)
五、CAS(比较交换)
cas(compare and swap):比较并交换
cas执行过程:线程1读取内存中的值A到自己的工作内存中,在工作内存中修改A为B,在写回内存的时候判断内存中的值是否为A,若是,则更新,
若不是,则重新读取内存中的值,做出修改,再次写回内存时做比较。
实际做的时候加version确定每一次的操作记录更好。
cas缺点:
-
只能保证对一个变量的原子性操作
当对一个共享变量执行操作的时候,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,
这个时候就可以用锁来保证原子性。
-
长时间自旋会给CPU带来压力
如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销
-
ABA问题
在一个时间差的时段内会造成数据的变化。比如说一个线程1从内存中取走A,这个时候另一个线程2也从内存中取走A,这个时候A的值为X,然后线程2将A的值改为Y,
过一会又将A的值改为X,这个时候线程1回来进行CAS操作发现内存中A的值仍然是X,因此线程1操作成功。
六 、锁及锁升级
1 锁四种状态
目前锁状态一种有四种,从级别由低到高依次是:无锁、偏向锁,轻量级锁,重量级锁,锁状态只能升级,不能降级。
2 锁对比
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块场景 |
轻量锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 始终得不到锁竞争的线程,使用自旋会消耗CPU | 追求响应速度,同步块执行速度非常快 |
重量锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量,同步块执行速度较慢 |
3 synchronized锁升级
(1)用法
synchronized可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。
synchronized有三种应用方式:
-
作用于实例方法,当前实例加锁,进入同步代码前要获得当前实例的锁;
-
作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁;
-
作用于代码块,对括号里配置的对象加锁。
(2)实现原理
synchronized用的锁存在Java对象头里,Java对象头里的Mark Word默认存储对象的HashCode、分代年龄和锁标记位。在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。32位JVM的Mark Word可能变化存储为以下5种数据:
锁升级过程:
线程1在进入同步代码块前,先检查MarkWord中的线程ID是否与当前线程ID一致,如果一致,则无需使用CAS来加锁、解锁。
如果不一致,再检查MarkWord中的锁标志位是否为偏向锁,如果不是,则线程1自旋等待锁释放。
如果是,再检查MarkWord中的线程ID标志的线程是否存在,如果不存在,则设置MarkWord中的线程ID为线程1的ID,此时依然是偏向锁。
如果还存在,则暂停MarkWord中的线程ID标志的线程,同时将锁标志位设置为00即轻量级锁。线程1自旋等待锁释放。
如果线程1自旋次数到了MarkWord中的线程ID标志的线程还没有释放锁,或者该线程还在执行,线程1还在自旋等待,这时又有一个线程2过来竞争这个锁对象,
那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。
如果MarkWord中的线程ID标志的线程释放锁,则会唤醒所有阻塞线程,重新竞争锁。