Reference:
https://www.cnblogs.com/Mainz/p/3546347.html?utm_source=tuicool&utm_medium=referral
synchronized 了解吗
解决多个线程之间访问资源的同步性,保证被它修饰的方法或者是代码块在任意时刻只有一个线程执行
JDK1.6对锁的实现引入了大量的优化,如自旋锁,适应性自旋锁,锁消除,锁粗化,偏向锁,轻量级锁等技术
平时怎么使用的 sychronized
使用的三种方式
-
修饰实例方法
-
修饰静态方法
给当前的类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员
如果一个线程A调用一个实例对象为非静态synchronized方法,而线程B需要调用这个实例对象的静态synchronized方法,是允许的,不会互斥。因为访问静态synchronized方法占用的锁是当前类的锁,而访问非静态synchronized发放占用的锁是当前实例对象锁
-
修饰代码块
总结 synchronized 加到 static静态方法和 synchronized(class) 代码块上都是给class类上锁。加到实例方法上是给对象实例上锁。尽量不要用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能
单例模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Singleton{
// volaitle 必要
private volatile static Singleton uniqueInstance;
private Singleton(){}
public static Singleton getUniqueInstance(){
if(uniqueInstance == null){
synchronized(Singlton.class){
if(uniqueInstance == null){
// 1. 为uniqueInstance 分配内存空间
// 2. 初始化uniqueInstance
// 3. 将uniqueInstance 指向分配的内存空间
// 多线程下,指令重排会导致顺序变化比如 1-3-2,线程A执行了1和3,此时线程B调用getUniqueInstance()发现不为空,返回,但此时的uniqueInstance 还未被初始化
// 使用volatile可以禁止jvm的指令重排,保证在多线程环境下可以正常运行
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
synchronized关键字的底层原理
底层属于jvm层面
-
同步语句块的情况
1 2 3 4 5 6 7
public class SynchronizedDemo{ public void method(){ synchronized(this){ System.out.println("synchronized 代码块") } } }
monitor对象存在于每个java对象的对象头中,所以java中任意对象可以作为锁的原因
当计数器为0获取成功,执行monitorexit后,设为0,锁被释放。如果获取锁失败,就要阻塞等待
-
synchronized修饰方法的情况
1 2 3 4 5
public class SynchronizedDemo2{ public synchronized void method(){ System.out.println("synchronized 方法") } }
没有使用monitorenter 和 monitorexit
而是用 ACC_SYNCHRONIZED 标识,指明了改方法是一个同步方法
JDK1.6之后对synchronized锁做了哪些优化
-
偏向锁
在无竞争的情况下会把整个同步都消除掉。会偏向于第一个获得他的线程
-
轻量级锁
在无竞争的情况下使用CAS操作去代替使用互斥量
-
自旋锁和自适应自旋
轻量级锁失败后,让一个线程继续等待,执行一个忙循环,而不是被挂起
挂起线程、恢复线程的操作都需要转入内核态中完成(用户态转到内核态会耗时)
自适应自旋锁:自旋的时间不再固定
-
锁消除
检测到那先共享数据不可能存在竞争,就执行锁消除
-
锁粗化
同步快的作用范围限制小
谈谈 Synchronized 和 ReentrantLock的区别
-
俩者都是可重入锁
“自己可以再次获取自己的内部锁”
-
sychronized依赖于jvm而ReentrantLock依赖于API
ReentrantLock需要lock() 和 unlock() 方法配合 try/finally语句来完成
-
ReentrantLock 增加了一些高级功能
-
等待可中断
一种能够中断等待锁的线程的机制 通过 lock.lockInterruptibly()
-
可实现公平锁
Synchronized只能是非公平锁。
公平锁就是先等待的线程先获得锁
-
可实现选择性通知
线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。而是用synchronized中的notyfy()/notifyAll(),被通知的线程是由JVM选择的
-
在CurrentHashMap还有Schronized都可以看到CAS,谈一下java中的CAS是怎么实现的
CAS:compare and swap,语义:”我认为V的值应该是A,如果是那么将V的值更新为B,否则不修改并告诉V的值实际是多少“。当多个线程尝试用CAS同事更新一个变量时,只有其中一个线程能更新变量的值,其他线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再试尝试
是CPU指令级的操作,只有一步原子操作,所以非常快。而且CAS避免了请求操作系统来裁定锁的问题,不用麻烦操作系统,直接在CPU内部就搞定了
- 非阻塞堆栈 ConcureentStack push方法观察当前最顶的节点,构建一个新节点放在堆栈上,然后,如果最顶端的节点在初始观察之后没有变化,就安装新节点
- 非阻塞类表
- ConcureentHashMap
说说CountDownLatch和CyclicBarrier
CountDownLatch 和 CyclicBarrier 都用于控制并发的工具类,扣可以理解成维护一个 计数器,但这来者有不同的侧重的:
- countDownLatch一般用于某个线程A等待若干个其他线程执行完任务后,它才执行,而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行。countDownLatch强调一个线程等多个线程完成某事。CyclicBarrier是多个线程互相等,等大家都完成,再携手共进
- CountDownLatch的countDown()方法,线程不会被阻塞,会继续往下执行;而调用CyclicBarrier的await()方法,会阻塞线程,知道CyclicBarrier指定的线程全部到达指定点才执行
- CountDownLatch方法比较少,操作简单。CyclicBarrier提供的方法更多,比如能通过getNumberWaiting() isBroken()这些方法获取当前多个线程的状态,而且,CyclicBarrier的构造方法可以传入barrierAction,指定当所有线程都达到时的业务功能
- CountDownLatch是不能复用的,CyclicBarrier可以
- 他们都是通过维护计数器来数显,线程执行await()方法后计数器会减一,直到计数器为0,所有调用await()方法而在等待的线程才会继续执行
- CyclicBarrier的计数器通过调用reset()方法可以循环使用,所以它才叫做循环屏障
谈谈semaphore
Semaphore 信号量,可以同时控制访问的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而release()释放一个许可,也可以通过 tyrAcqure 立即获取结果
讲一下java内存模型
线程有自己对应的工作内存,此外还有主内存
Volatile 保证变量可见性 和 防止指令重排序
synchronized 和 volatile 区别
- volatile 只能用于变量,而 sychronized 可以修饰方法以及代码块
- 多线程访问volatile不会阻塞
- volatile只能保证可见性,不能保证原子性。而synchronized都可以保证
- volatile解决多线程之间的可见性,而synchronized解决多个线程之间访问资源的同步性
ThreadLocal 了解吗
线程的专属本地变量,避免线程安全问题
threadLocal 只是为 ThreadLocalMap 的封装,传递了变量值
ThreadLocalMap的key是ThreadLocal对象,value就是ThreadLocal对象调用set方法设置的值。 key为弱引用,value为强引用,如果ThreadLocal没有被外部强引用的情况下,垃圾回收时,key会被清理掉,而value不会,就可能会产生内存泄漏。在调用set() get() remove() 方法的时候,会清理掉key为null的记录
threadLock的数据是分配在公共区域的堆内存中的
ThreadLocal 会在什么场景使用
Spring采用ThreadLocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙的管理多个事务配置之间的切换,挂起和恢复
在平常的项目中,之前我们上线后发现部分用户的日期不对了,排查下来是SimpleDataFormat的问题,在多线程下,会有问题,其实可以让每个线程都new一个SimpleDataFormat就好了,但是这样比较耗费资源。所以使用了线程池加上ThreadLocal 包装的 SimpleDataFormat 再调用 initialValue让每个线程都有一个SimpleDataFormat的副本,就解决了线程安全的问题,也提高了性能
还有一个就是,在想成中需要横跨若干方法调用,也就是上下文(Context) ,经常就是用户身份,任务信息等,就会存在过度传参的问题。给每个方法增加一个context参数可以解决,但有点麻烦,并且如果遇到第三方库就不行了,所以使用了ThreadLocal去做了改造,调用前在ThreadLocal中设置参数,其他地方get就好了。其实很多cookie session 的数据隔离就是通过ThreadLocal去实现的
ThreadLocal的底层实现原理呢
-
set()方法
获取当前线程,获取ThreadLocalMap对象,添加
每个线程都维护了自己的threadLocals变量
TheadLocalMap底层结构
和HashMap很像,他的entry是继承弱引用,也没有链表
用的是一个数组,存储的时候会给每一个对象一个HashCode,定位到table的位置,如果是空的就是放上去,如果不为空,就找下一个直到为空,如果相等就刷新
这样get的时候,也是会根据hash值,定位到table中,判断是否一致,不是就找下一个
ThreadLocal的对象是放在哪里的
是放在堆上的,只是通过一些技巧将可见性修改成了线程可见
可以共享线程的ThreadLocal数据吗
使用InheritableThreadLocal 实现多个线程访问ThreadLocal的值
在Thread初始化的时候,如果InheritableThreadLocal 不为空,并且父线程的InheritableThreadLocal 也存在,那么就把父线程的InheritableThreadLocal 赋值给当前线程
ThreadLocal的问题
内存泄漏
ThreadLocal的KEY被设计成了弱引用了,对应的当没有外部强引用时,发生GC时会被回收,value就一直得不到回收。所以用的时候最后记得用remove清空
ThreadLocal的key为什么要设计成弱引用
会造成和entry中value一样内存泄漏的场景
-
如果key使用强引用:引用的ThreadLocal对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏
-
如果key使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,及时没有手动删除,ThreadLocal也会被回收,value在一下一次ThreadLocalMap调用set get remove 的时候会被清除
由于ThreadLocalMap的生命周期和Thread一样长,如果都没有手动删除对应的key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在一下remove会被清除
线程池的作用
线程池的作用
-
降低资源消耗
重复利用已创建的线程降低线程创建和销毁造成的消耗
-
提高响应速度
当任务到达时,可以不需要等待线程创建就能立即执行
-
提高线程的可管理性
无限制的创建,消耗系统资源,使用线程池统一分配,调优和监控
Runnable接口和Callable接口的区别
Runnable 没有返回值也无法抛出异常
Callable 可以返回结果和抛出异常
execute() 和 submit() 方法的区别
execute() 提交不需要返回值的任务
submit() 提交需要返回值的任务,线程池会返回一个Future类型的对象
如何创建线程池
不允许使用 Executors去创建线程池,弊端如下
- FixedThreadPool 和 SingleThreadExecutor 可能堆积大量的请求,导致OOM
- CachedThreadPool 和 ScheduledThreadPool 创建大量线程,导致OOM
通过ThreadPoolExecutor方式去创建
三个重要的参数
-
corePoolSize 最小可以同时运行的线程数量,
-
maximumPoolSize 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数
-
workQueue 当新的任务来的时候会判断当前运行的线程数量是否达到核心线程数,如果达到的话,就会放在队列中
-
ArrayBlockingQueue
-
LinkedBlockingQueue
-
PriorityBlockingQueue
-
DelayQueue
-
SynchroniousQueue
一个不存储元素的阻塞队列
-
LinkedTransferQueue
-
LinkedBlockingDeque
-
其他常见参数
-
keepAliveTime 当线程池中线程数量大于 corePoolSize 的时候,如果没有新任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待时间超过了 keepAliveTime才会被回收销毁
-
unit keepAliveTime参数的时间单位
-
threadFactory executor 创建新线程的时候会用到
-
handler 饱和策略
当同时运行的线程数量达到最大线程数并且队列也放满,可以定义一些策略:
- ThreadPoolExecutor.AbortPolicy : 抛出 RejectedExecutionException 来拒绝新任务的处理
- ThreadPoolExecutor.CallerRunsPolicy: 调用执行自己的线程运行任务,可以增加队列容量。如果可以承受延迟并且不能丢弃任务的话可以选择
- ThreadPoolExecutor.DiscardPolicy 不处理新任务,直接丢掉
- ThreadPool Executor.DiscardOldestPolicy: 丢弃最早未处理的任务
ThreadPoolExcutor的worker线程管理
线程池为了掌握线程的状态并维护线程的生命周期,设计了线程池内的工作线程Worker
线程池挂会努力线程的生命周期,需要在线程长时间不运行的时候进行回收。worker通过继承AQS,使用AQS来实现独占锁这个功能,实现不可重入的特性去反应线程现在的执行状态。
线程回收的的过程:
首先尝试获取锁,如果获取锁成功,说明他在空闲状态,可以安全回收。线程池中线程销毁依赖JVM自动的回收,线程池根据当前状态维护线程引用,防止被JVM回收,但线程池决定哪些需要回收时,只需要将引用消除即可。
线程池在业务中的实践
线程池在实践中有没有最佳实践
- 把线程池的参数从代码中迁移到分布式配置中心上,实现线程池参数可动态配置和及时生效。线程池监听外部消息。JDK允许线程池使用方法来动态设置。
- setCorePoolSize 直接覆盖原来的值,当前值小于工作线程,会向空闲的worker线程发起中断请求以实现回收,当当前值大于打坐线程,则线程池会创建新的worker线程来执行队列任务
- 简化线程池配置,corePoolSize maximumPoolSize workQueue ,其中队列选俩种 1. 并行执行子任务,使用同步队列,没有什么任务应该被缓存,而是应该立即执行 2. 并行执行大批次任务,提升吞吐量,使用有界队列,缓冲大批量任务,队列容量必须声明
- 增加线程池监控
4核8G IO20% CPU80% 如何设置
先说说线程池的原理,在HotSpot VM 的线程模型中,java线程被一对一映射为内核线程,java在使用线程执行任务时,需要创建一个内核线程,当该java线程被终止时,这个内核线程也会被回收
核心线程数:
-
对于CPU密集型:
可以将线程数设置为N(CPU核心数)+1,比CPU核心数多出来的一个线程是为了防止线程也难怪偶发的缺页中断,或者其他原因导致的任务暂停而带来的影响
-
对于IO密集型:
系统大部分时间处理IO交互,而线程在处理IO的时候不会占用CPU来处理,可以将CPU叫出来给其他线程使用,因此可以多配置一些线程,2N
线程数 = N * (1+WT(线程等待时间)/ST(线程运行时间))
JDK的自带工具VisualVM 可以查看 wt/st
最大线程数 =(最大任务数-队列容量)/每个线程每秒处理能力
线程池中核心线程什么时候被创建,可以被收回吗
在执行任务时创建
但是可以预先创建 初始化线程池后,再调用 prestartAllThreads() 可以预先创建corePoolSize数量的核心线程。会调用getTask()方法,从blockingQueue阻塞队列中获取任务,因此线程不会被释放,创建没有任务执行的线程数量为corePoolSize。也可以使用 prestartCoreThread,只不过该方法只会创建1条线程
回收核心线程的方法
-
allowCoreThreadTimeOut = true
或者此时的工作线程大于corePoolSize时,线程调用BlockingQueue的poll方法获取任务,若超过了KeepAliveTime时间,返回null,线程的runWorker方法会退出while循环,线程接下来会被回收
Atomic原子类
基本类型
- AtomicInteger(Array)
- AtomicLong(Array)
- AtomicBoolen
- …
主要利用CAS + volatile 来保证原子操作,避免 synchronized 的高开销,执行效率大为提升
AQS(AbstractQueuedSynchronizer)
是一个用来构建锁和同步器的框架,简单高效的创建出应用广泛的大量的同步器,比如ReentrantLock等
AQS核心思想是,如果被请求的共享资源空间,则将当前被请求资源的线程设置为有效的工作线程,并且共享资源设置为锁定状态、如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS使用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中
- CLH (Craig,Landin,and Hagersten) 队列是一个虚拟的双向队列(不存在队列实例,仅存在节点之间的关联关系)AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个节点(Node)来实现锁的分配
维护了一个 volatile int state 和一个 FIFO线程等待队列
以ReentrantLock为例,state初始化为0,标识未锁定状态,A线成lock()时,会调用tryAcquire()独占该锁并将 state+1 。此后,其他线程再 tryAcquire()时就会失败,直到A线成unlock()到state=0为止,其他线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念
再以CountDownLatch为例,任务分为N个子线程去执行,state也初始化为N(要与线程个数一致)这N个子线程是并行执行的,每个子线程执行完成后countDown()一次,state会CAS减1,等到所有子线程完成后,会unpark()主调用线程,然后朱调用线程就会await()函数返回,继续后续动作
AQS对资源的共享方式
-
Exclusive
只有一个线程能执行,如ReentrantLock 又可分为公平锁和非公平锁
-
Share
多个线程可以同时执行 比如 Semaphore/CountDownLatch
什么是线程安全
其实线程安全说的不是线程,而是在公共区域堆内存的数据安全问题
- 局部变量,都放在每个线程的内存空间(栈内存)
- 对于类成员变量是放在堆内存的,这时可以通过ThreadLocal,操作属于自己空间的数据,但数据本身还是在堆内存的
- 通过final修饰,可以让数据只能读不能修改
- 通过互斥锁
- CAS乐观锁
记一次线程池引发的故障
我定义了一个线程池来执行任务,但是程序执行结束后任务没有完全执行完
业务场景:
统计业务需要,订单信息需要从主库经过业务代码写入统计库(中间需要逻辑处理不能走binlog)之前的接口是单线程的,100万条订单信息,100条需要10s ,所以需要很长时间。所以计划使用线程池,为每一个中心分配一条线程去执行业务
开了5个核心线程,使用有界阻塞队列。中间发现有个线程的状态是wait,但是相对应的日志才跑了一半。之后发现是因为业务代码有异常,所以中断了。我是用submit提交任务,但是任务中的异常被吞掉了
通过看源码,发现异常是设置到了一个变量outcome中,就是对submit方法返回的FutureTask对象执行get()方法得到的结果
但是在线程池中,并没有获取执行子线程的结果,所以异常就没有被抛出来、
所以在定义 ThreadFacotory创建线程的时候,调用 setUncaughtExceptionHandle,自定义异常处理方法。之后就在线程池,子线程的异常要专门去捕获一下