多线程(4):Java线程同步机制中的锁优化以及线程池技术

一、锁优化 

这里的锁优化讲的是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 框架的使用示意图

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: 

/**
     * 用给定的初始参数创建一个新的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.RunnableCallable(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并发编程艺术》

  参考资料:https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/Multithread/JavaConcurrencyAdvancedCommonInterviewQuestions.md

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值