一、锁优化
这里的锁优化讲的是JVM对synchronized 的优化。
1.1、自旋锁
原理(思想):
互斥同步进入阻塞态的开销很大。自旋锁的思想是 让一个线程在请求一个共享数据的锁时,执行忙循环(自旋)一段时间,如果在这段时间内能够获得锁,就可以避免进入阻塞状态。
适用场景:
虽然能避免进入阻塞从而减少开销,但它需要进行忙循环操作占用CPU时间。它只适合用于共享数据的锁定状态很短的场景。
JDK1.6后引入自适应的自旋锁,自旋的次数不再固定了而是由 前一次在同一个锁上的自旋次数和锁得的拥有者的状态决定。
自旋锁与互斥锁的区别:
- 互斥锁:线程中有上下文切换,CPU的抢占,信号的发送等开销
- 自旋锁:线程一直running,死循环检查锁的标志,机制不复杂,不过要注意死循环的问题
1.2、锁消除
原理(思想):
对于被监测出不可能存在竞争的共享数据的锁进行消除。通过逃逸分析解决。如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就是可以将它们的锁消除。
1.3、锁粗化
如果一系列连续操作都对同一个对象反复加锁和解锁,频繁加锁操作会导致性能损耗,如果JVM探测到将会把加锁的范围扩展(粗化)到整个操作序列的外部,这样只需要加锁一次就可以了。
1.4、轻量级锁
原理(思想):
JDK1.6引入偏向锁和轻量级锁,让锁有了4种状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。
轻量级锁相较于传统的重量级锁而言 它使用了CAS操作 来避免重量级锁使用的互斥开销。对于大部分锁同步周期内很少竞争,因此不需要互斥量进行同步,可以先采用CAS操作进行同步,如果CAS失败了再改用互斥量进行同步。
1.5、偏向锁
原理(思想):
偏向于让第一个获取锁的对象的线程,这个线程在之后获取该锁就不需要进行同步操作或者CAS操作了。偏爱第一个获取锁线程。当锁对象第一次被线程获得时进入 偏向状态,标记为101。同时使用CAS操作将线程ID记录到MarkWord中,如果CAS操作成功这个线程以后每次进入这个锁相关的同步块 就不需要进行任何同步操作了。当别的线程尝试获取这个锁对象时,偏向状态就结束,此时撤销偏向后 恢复未锁定状态或者轻量级状态。
二、锁的深入化
1.读写锁(ReentrantReadWriteLock)
读读共享、写写互斥、读写和写读互斥
static Map<String, Object> map = new HashMap<String, Object>();
static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
static Lock r = rwl.readLock();
static Lock w = rwl.writeLock();
原理:
......
三、多线程优化 / 多线程开发良好的实践
(1)给线程起个有意义名字。
(2)缩小同步范围,较少锁争用,例如使用synchronized应尽量使用同步块而不是同步方法。
(3)多用同步工具少用wait()和notify()等方法。比如CountDownLatch, CyclicBarrier, Semaphore 和 Exchanger 这些同步类简化和优化了编码操作。
(4)使用BlockingQueue(阻塞队列,基于ReentrantLock)来实现 生产者消费者问题。
(5)多用并发集合少用同步集合,例如应该使用ConcurrentHashMap 而不是Hashtable。
(6)使用 本地变量和不可变类 来保证线程安全。
(7)创建线程时使用 线程池 而不是直接创建
四、Java线程池
4.1、为什么要用线程池 / 使用线程池的好处
来自《java并发编程艺术》:
(1)减少每次获取资源的消耗,提高资源利用率——通过重复利用已创建的线程降低线程创建和销毁的消耗。
(2)提高响应速度:——任务到达时,任务可以不用等线程创建就能立即执行。
(3)提高线程可管理性——线程是稀缺资源,使用线程池可以统一分配、调优】监控
4.2、Executor框架介绍
1.简介
JDK1.5后引入,引入后通过Executor来启动线程比使用Thread的start()方法更好,好处:更容易管理、效率提高(用线程池技术)、可避免this逃逸问题。它不仅包括了线程池的管理,还提供了线程工厂、队列以及拒绝策略。
(什么是this逃逸:指在构造函数返回之前其他线程就持有该对象的引用,调用尚未构造完全的对象方法会错误)
2.Executor框架的结构(3部分组成)
(1)任务(Runnable
/ Callable
)
执行任务需要实现Runnable
接口 或 Callable
接口,这两个接口的实现类都可以被ThreadPoolExecutor
或 ScheduledThreadPoolExecutor
执行。
(2)任务的执行(Executor
)
包括任务执行机制核心接口Executor,以及集成该接口的ExecutorService
接口。ThreadPoolExecutor(常用)
和ScheduledThreadPoolExecutor
实现了ExecutorService
接口。如下图:
(3)异步计算的结果(Future)
Future
接口以及 Future
接口的实现类 FutureTask
类都可以代表异步计算的结果。
当把Runnable
接口 或 Callable
接口 的实现类提交给ThreadPoolExecutor
或 ScheduledThreadPoolExecutor
执行,调用submit()方法时会返回一个FutureTask
对象。
3.Executor 框架的使用示意图
(1)主线程首先要创建实现Runnable或者Callable接口的任务对象。
(2)把创建完的接口交给ExecutorService
执行。
(3)如果执行 ExecutorService.submit(…)
,ExecutorService
将返回一个实现Future
接口的对象。
(4)最后主线程可以执行Futuretask.get()方法来等待任务执行完成。线程也可以执行 FutureTask.cancel()
来取消此任务的执行。
4.3、如何去创建线程池
注意:《阿里java开发手册》规定,为了避免资源耗尽风险,不允许Executors去创建,而是通过ThreadPoolExecutor方式。Executors 返回线程池对象欧如下弊端:
- FixedThreadPool 和 SingleThreadExecutor ——允许请求的队列长度为Integer.MAX_VALUE ,可能堆积大量请求,从而导致OOM(内存耗尽)。
- CachedThreadPool 和 ScheduledThreadPool ——允许创建的线程数为Integer.MAX_VALUE ,可能会创建大量线程,导致OOM
常见的创建线程池方式有以下几种:
Executors.newCachedThreadPool()
:无限线程池。Executors.newFixedThreadPool(nThreads)
:创建固定大小的线程池。Executors.newSingleThreadExecutor()
:创建单个线程的线程池。
1.通过Executor框架的工具类Executors实现(可创建3种类型的ThreadPoolExecutor 线程池)
三种常见线程池介绍:
(1)FixedThreadPool:
该方法返回一个 固定线程数量的线程池,并且线程数量始终不变。当有一个新的任务提交时,线程池若有空闲线程,则立即执行。若没有,任务会被暂存在一个 任务队列 中,待有线程空闲时,便处理在任务队列中的任务。
(2)SingleThreadExecutor:
该方法返回一个 只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个 任务队列 中,待线程空闲,按先进先出(FIFO)顺序执行队列中的任务。
(3)CachedThreadPool:
该方法返回一个 可根据实际情况调整线程数量的线程池。线程池线程数量不确定,但若有空闲线程可以进行复用会有限选择可服用的线程。若所有现场均在工作,又有新的任务提交,则会创建新的线程处理任务。当所有线程在当前任务执行完后,将返回线程池进行复用。
2.通过构造方法实现
比如使用ThreadPoolExecutor的构造方法(4个):
用给定的初始参数创建一个新的ThreadPoolExecutor:
/**
* 用给定的初始参数创建一个新的ThreadPoolExecutor。
*/
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;
}
4.4、ThreadPoolExecutor
构造函数具体分析
1.参数分析(7个)
- corePoolSize:核心线程数 。线程数定义了最小可以同时运行的线程数量。
- maximumPoolSize:最大线程数。当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数
- workQueue:当新任务来的时候,会先判断当前运行的线程数量是否达到核心线程数,如果达到线程就会被存放在队列中。
- keepAliveTime:存活时间。当线程池中线程数量大于
corePoolSize
时,如果没有新的任务提交,核心线程外的线程不会立刻销毁,而是等待,直到等待的时间超过keepAliveTime才会被销毁。 - unit:keepAliveTime参数的时间单位。
- threadFactory:executor 创建新线程的时候会用到。
- handler:饱和策略。
线程池原理图:
2.ThreadPoolExecutor
饱和策略
(1)饱和策略的定义
如果当前同时运行的线程数量达到最大线程数量,并且队列也已经被放满了任务ThreadPoolTaskExecutor
定义了一些策略:
ThreadPoolExecutor.AbortPolicy
:抛出RejectedExecutionException
来拒绝新任务的处理ThreadPoolExecutor.CallerRunsPolicy
:调用执行自己的线程运行任务ThreadPoolExecutor.DiscardPolicy
: 不处理新任务,直接丢弃掉ThreadPoolExecutor.DiscardOldestPolicy
: 此策略将丢弃最早的未处理的任务请求
4.5、几个常见的对比
1.Runnable
和Callable(JDK1.5后才有,用来补充Runnable)
Runnable接口不会返回结果或者异常但是Callable接口可以;工具类 Executors
可以实现 Runnable
对象和 Callable
对象之间的相互转换
2.execute()
和 submit()
execute()方法用于提交不需要返回值的任务,无法判断任务是否被线程池执行成功与否;
submit()用于提交需要返回值的任务,线程池会返回一个Future类型的对象,通过这个对象可以判断任务是否执行成功,通过该对象的get()方法可以获取返回值,get()方法会阻塞当前线程直到任务完成
3.shutdown()和shutdownNow()
shutdown():关闭线程池。
线程池的状态变为SHUTDOWN,线程不再接收新任务,但队列里的任务得执行完毕;
shutdownNow():关闭线程池。线程状态变为STOP,线程池会终止当前正在运行的任务,并停止处理排队的任务,然后返回正在等待执行的List。
4.isTerminated()
VS isShutdown()
isShutDown
当调用shutdown()
方法后返回为 true。isTerminated
当调用shutdown()
方法后,并且所有提交的任务完成后返回为 true
4.6、如何合理配置线程池
判断是CPU密集型还是 IO密集型。
什么是CPU密集型:意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。
怎么配置:任务可以少配置线程数,大概和机器的cpu核数相当,这样可以使得每个线程都在执行任务。
什么是IO密集型:该任务需要大量的IO,即大量的阻塞。在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。
怎么配置:大部分线程都阻塞,故需要多配置线程数,2*cpu核数。
### 若对你有帮助的话,欢迎点赞!评论!转发!谢谢!
上一篇:同步机制高级应用
下一篇:并发容器
参考资料:《java并发编程艺术》