Executor框架、线程池、ThreadLocal、乐观锁、悲观锁、无锁CAS 原理

一、Executor框架:

1、什么是Executor框架:

Executor就是一个任务的执行和调度框架,将任务的提交过程与执行过程解耦,用户只需定义好任务提交,具体如何执行,什么时候执行不需要关心;其中,最顶层是Executor接口,它的定义很简单,只有一个用于执行任务的execute方法。Executor框架主要由3大部分组成:

(1)任务:实现Callable接口或Runnable接口的类,其实例就可以成为一个任务提交给ExecutorService去执行:其中Callable任务可以返回执行结果,Runnable任务无返回结果。

(2)任务的执行:包括任务执行机制的核心接口Executor,以及继承自Executor的ExecutorService接口。Executor框架有两个关键类实现了ExecutorService接口:ThreadPoolExecutor 和 ScheduledThreadPoolExecutor;

(3)任务的异步计算结果:包括Future接口和实现Future接口的FutureTask类、ForkJoinTask类。

使用步骤:把任务,如Runnable接口或Callable接口的实现类提交(submit)给线程池执行,如ExecutorService、ThreadPoolExecutor、ScheduledThreadPoolExecutor等。线程池会返回一个异步计算结果,即Future对象,然后调用Future的get方法等待执行结果即可。

另外,还有一个Executors类,它是一个工具类(有点类似集合框架的Collections类),用于创建ExecutorService、ScheduledExecutorService、ThreadFactory 和 Callable对象。

2、Java线程池中submit()和execute()方法有什么区别?

两个方法都可以向线程池提交任务,execute()方法的返回类型是void,它定义在Executor接口中, 而submit()方法可以返回持有计算结果的Future对象,它定义在ExecutorService接口中,它扩展了Executor接口,其它线程池类像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有这些方法。

 

 

二、线程池:

1、为什么使用线程池:

影响一个多线程应用程序的响应时间的几个主要因素之一是,为每个任务创建一个线程的时间。多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。

线程池是指在初始化一个多线程应用程序过程中创建一个线程集合,然后在需要执行新的任务时重用这些线程而不是每次都新建一个线程。线程池中的每个线程都有被分配一个任务,一旦任务已经完成了,线程回到线程池中并等待下一次分配任务。

2、常用的线程池:

(1)newSingleThreadExecutor:

创建一个单线程的线程池。这个线程池只有一个线程在工作,串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

适用场景:适用于需要保证顺序地执行各个任务;并且在任意时间点,不会有多个线程活动的应用场景。

(2)newFixedThreadPool:

创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大容量。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

适用场景:为了满足资源管理的需求,而需要限制当前线程数量的应用场景,它适用于负载比较重的服务器。

(3)newCachedThreadPool:

创建一个可缓存的线程池,大小不受限。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(即60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

适用场景:适用于执行很多的短时间异步任务的小程序,或者是负载较轻的服务器。

(4)newScheduledThreadPool:

创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

3、线程池启动策略:

(1)线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。

(2)当调用execute() 方法添加一个任务时,线程池会做如下判断:

① 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;

② 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列。

③ 如果这时候队列满了,而且正在运行的线程数量小于maximumPoolSize,那么还是要创建线程运行这个任务;

④如果队列满了,而且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会根据设定的拒绝策略,做出相应的措施。

(3)当一个线程完成任务时,它会从队列中取下一个任务来执行。

(4)当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。

4、线程池拒绝策略:

(1)ThreadPoolExecutor.AbortPolic(默认):当任务添加到线程池之中被拒绝的时候,这个时候会抛出RejectedExecutionException异常;

(2)ThereadPoolExecutor.CallerRunsPolicy:当任务被拒绝的时候,会在线程池当前正在执行线程的WORKER里面处此线程;

(3)ThreadPoolExecutor.DiscardOldestPoliy:当被拒绝的时候,线程池会放弃队列之中等待最长时间的任务,并且将被拒绝的任务添加到队列之中。

(4)ThreadPoolExecutor.DiscardPolicy:当任务添加拒绝的时候,将直接丢弃此线程。

 

 

三、ThreadLocal:

1、什么是ThreadLocal:

对于多线程资源共享的问题,线程同步机制采取了时间换空间的方式,访问串行化,对象共享化;而ThreadLocal采取了空间换时间的方式,访问并行化,对象独享化。

ThreadLocal是一个线程的本地化对象。当在多线程环境中采用ThreadLocal维护变量时,ThreadLocal会为每个使用该变量的线程分配一个独立的副本。每个线程只可以独立的改变自己的副本,从而隔离了多个线程访问数据的冲突,保证了各个线程的数据互不干扰。适用于各个线程依赖不同的变量值完成操作的场景。

每个线程的内部都有一个数据结构ThreadLocalMap(有点像HashMap,但是没有实现Map接口),Map里面使用key存储线程本地对象(ThreadLocal)和线程的变量副本(value),但是一个ThreadLocal只能保存一个value,并且各个线程的数据互不干扰。当执行set方法时,其值是保存在当前线程的ThreadLocals变量中,当执行get方法中,是从当前线程的threadLocals变量获取。所以对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。

每个ThreadLocal只能保存一个变量副本,如果想要一个线程能够保存多个副本以上,就需要创建多个ThreadLocal。

2、ThreadLocal的核心方法:

get()方法用于获取当前线程的副本变量值。

set()方法用于保存当前线程的副本变量值。

initialValue()为当前线程初始副本变量值。

remove()方法移除当前前程的副本变量值

3、ThreadLocalMap解决Hash冲突的方法:

和HashMap的最大的不同在于,ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式,ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。
在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置i,过程如下:
(1)如果当前位置是null,就初始化一个Entry对象放在位置i上;
(2)如果位置i已经有Entry对象了,如果这个Entry对象的key正好是即将设置的key,那么重新设置Entry中的value;
(3)如果位置i的Entry对象和即将设置的key没关系,那么只能找下一个空位置;

4、ThreadLocal导致的内存泄露:

ThreadLocalMap的key是弱引用,而value是强引用。这就导致了一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用key会被回收,而value不会回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。

既然Key是弱引用,那么我们在调用ThreadLocal的get()、set()方法时完成后,再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。

5、ThreadLocal应用场景:
(1)Hibernate的session获取:每个线程访问数据库都应当是一个独立的session会话,如果多个线程共享同一个session会话,有可能其他线程关闭连接了,当前线程再执行提交时就会出现会话已关闭的异常,导致系统异常。使用ThreadLocal的方式能避免线程争抢session,提高并发下的安全性。

(2)使用ThreadLocal的典型场景正如上面的数据库连接管理,线程会话管理等场景;适用于无状态,副本变量独立后不影响业务逻辑的高并发场景。如果业务逻辑强依赖于副本变量,则不适合用ThreadLocal解决。

6、Spring框架如何保证线程安全:

一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中绝大多数的bean都可以声明为singleton作用域。就是因为Spring对一些非线程安全的“状态性对象”采用了ThreadLocal进行封装,让它们成为线程安全的对象,因此有状态的bean就可能以singleton的方式在多线程中正常工作了。

 

 

四、乐观锁、悲观锁、无锁CAS:

1、乐观锁:乐观锁认为竞争不总是发生,因此每次操作共享资源时都是不加锁,而是假设没有冲突,并将“比较-替换”这两个动作作为一个原子操作去修改内存中的变量,如果发生冲突失败就重试,直到成功为止。无锁就是一种乐观策略,无锁策略采用volatile+CAS的技术来保证线程执行的安全性。

2、悲观锁:悲观锁认为竞争总是会发生,因此每次对共享资源进行操作时,都会持有一个独占的锁,比如synchronized。

3、CAS:Compare And Swap:

(1)其算法核心思想:执行函数:CAS(V,E,N),其包含3个参数:内存值V,预期值E,要修改的值N。

当且仅当预期值A和内存值V相同时,才会将内存值修改为B并返回true,若V值和E值不同,则说明已经有其他线程做了更新,则当前线程不执行更新操作,但可以选择重新读取该变量再尝试再次修改该变量,也可以放弃操作。CAS一定要volatile变量配合,这样才能保证每次拿到的变量是主内存中最新的那个值,否则旧的预期值A对某条线程来说,永远是一个不会变的值A,只要某次CAS操作失败,永远都不可能成功。由于CAS无锁操作中没有锁的存在,因此不可能出现死锁的情况,也就是天生免疫死锁。

(2)CPU指令对CAS的支持:

CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。

(3)CAS的ABA问题及其解决方案:

假设这样一种场景,当第一个线程执行CAS(V,E,U)操作,在获取到当前变量V,准备修改为新值U前,另外两个线程已连续修改了两次变量V的值,使得该值又恢复为旧值,这样的话,我们就无法正确判断这个变量是否已被修改过。

解决方法:使用带版本的标志解决ABA问题。在更新数据时,只有要更新的数据和版本标识符合期望值,才允许替换。

 

五、线程控制方法:

1、线程中sleep()方法和wait方法的区别?

(1)sleep()使当前线程进入睡眠状态,让出CPU的使用,目的是不让当前线程独自霸占该进程所获的CPU资源,以留一定时间给其他线程执行的机会,但是该线程仍然占有该锁,其他线程无法访问这个对象。在sleep()休眠时间期满后,该线程不一定会立即执行,因为其它线程可能正在运行而且没有被调度为放弃执行,除非此线程具有更高的优先级。 

(2)wait()方法是Object类里的方法;当一个线程执行到wait()方法时,它就进入到一个和该对象相关的等待池中,同时释放对象锁(wait(long timeout)超时时间到后还需要返还对象锁);其他线程可以访问该对象锁;wait()可以使用notify或者notifyAll()或者指定睡眠时间来唤醒当前等待池中的线程。notify()和notifyAll()方法只是随机唤醒等待该对象锁的线程,并不决定哪个线程能够获取到锁。

2、start()方法和run()方法的区别

只有调用了start()方法,才会表现出多线程的特性,不同线程的run()方法里面的代码交替执行。如果只是调用run()方法,那么代码还是同步执行的,必须等待一个线程的run()方法里面的代码全部执行完毕之后,另外一个线程才可以执行其run()方法里面的代码。

3、thread类中的yield方法有什么作用?

yield()方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。执行yield()的线程有可能在进入到暂停状态后马上又被执行。

4、Thread.sleep(0)的作用是什么

由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。

5、如何在两个线程之间共享数据

通过在线程之间共享对象,然后通过wait/notify/notifyAll、await/signal/signalAll进行唤起和等待,比方说阻塞队列BlockingQueue就是为线程之间共享数据而设计的。

6、为什么wait()方法和notify()/notifyAll()方法要在同步块中被调用

这是JDK强制的,wait()方法和notify()/notifyAll()方法在调用前都必须先获得对象的锁

7、wait()方法和notify()/notifyAll()方法在放弃对象监视器时有什么区别

wait()方法和notify()/notifyAll()方法在放弃对象监视器的时候的区别在于:wait()方法立即释放对象监视器,notify()/notifyAll()方法则会等待线程剩余代码执行完毕才会放弃对象监视器。

8、怎么检测一个线程是否持有对象监视器

Thread类提供了一个holdsLock(Object obj)方法,当且仅当对象obj的监视器被当前线程持有的时候才会返回true。

9、怎么唤醒一个阻塞的线程

如果线程是因为调用了wait()、sleep()或者join()方法而导致的阻塞,可以中断线程,并且通过抛出InterruptedException来唤醒它;如果线程遇到了IO阻塞,无能为力,因为IO是操作系统实现的,Java代码并没有办法直接接触到操作系统。

 

 

六、其他:

1CopyOnWriteXXX可以用于什么应用场景?

多读少写的场景。读写并不是在同一个对象上。在写时会大面积复制数组,所以写的性能差,在写完成后将读的引用改为执行写的对象。CopyOnWriteXXX(免锁容器)的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出ConcurrentModificationException。在CopyOnWriteXXX中,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行。

2、单例模式的线程安全性

单例模式:某个类的实例在多线程环境下只会被创建一次出来。单例模式有很多种的写法:

(1)饿汉式单例模式的写法:线程安全

(2)懒汉式单例模式的写法:非线程安全

(3)双检锁单例模式的写法:线程安全

3、如何合理的配置Java线程池?

(1)高并发、任务执行时间短的业务,可以少配置线程数,大概和机器的cpu核数相当,可以使得每个线程都在执行任务,减少线程上下文的切换;

(2)并发不高、任务执行时间长的业务要区分开看:

①IO密集型时,因为IO操作并不占用CPU,大部分线程都阻塞,故需要多配置线程数,让CPU处理更多的业务

②假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样,把线程池中的线程数设置得少一些,减少线程上下文的切换

③并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考其他有关线程池的文章。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。

(3)有界队列和无界队列的配置需区分业务场景,一般情况下配置有界队列,在一些可能会有爆发性增长的情况下使用无界队列。

(4)任务非常多时,使用非阻塞队列使用CAS操作替代锁可以获得好的吞吐量。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值