多线程和Synchronized在其中的使用
一、多线程
1.适用场景
需要提高任务执行效率,有多个任务且任务量大,或者多个任务中有会阻塞的情况
2.线程状态
1. 新建状态(New): 线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。
2. 就绪状态(Runnable): 也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。
3. 运行状态(Running): 线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
4. 阻塞状态(Blocked): 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
5. 死亡状态(Dead): 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
3.线程安全问题
原因:
多个线程对共享资源进行操作
根源:不满足原子性 可见性 有序性
要做到保证安全的前提下,提高效率
具体方式:没有安全问题的代码,不需要加锁
读写分离:
读 :valatile 保证可见性就够了
写:加锁
4.java创建多线程的方式
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
二、Synchronized
1. synchronized实现原理
synchronized(Object o){
…
}
本质上是线程执行到共享资源的synchronized代码块时,对o对象进行上锁,此时再有其他线程来时,发现o对象已经上锁,所以被阻塞。
对象锁机制
private static Object obj = new Object();
public static void main(String[] args) {
synchronized (obj) {
System.out.println("hello world");
}
}
以上代码进行javap -c 反编译后
可以看到第5行,第15行和第21行的monitorenter和monitorexit指令。
也就是说执行synchronized包裹的同步代码块的底层是:首先执行monitorenter指令,执行完代码后退出时在实行monitorexit指令。
其关键就是必须对对象的monitor监视器进行获取,当线程获取monitor后才能继续往下执行,否则只能等待,而这个获取的过程是互斥的,同一时间只能有一个线程能获取到monitor。
以上有两个monitorexit是因为要确保异常时也能解锁。
2. synchronized优化
使用monitor监视器的操作,确实能保证线程安全,但效率低下,我们在解决线程安全问题时,要在安全的前提下,尽可能的保证效率。
1. 前置知识
1. CAS
CAS(compare and swap),是java对于乐观锁的一种具体实现,基于Unsafe来实现呢,本质上是基于CPU提供的接口保证线程安全修改变量。
乐观锁:线程直接尝试对共享变量进行操作。
悲观锁:线程先加锁,然后再对共享变量进行操作。
操作过程:简单理解为CAS(V,O,N) 保存的三个值分别为:V:内存中存放是实际值;O:预期的旧值;N:更新的新值。 当V和O相同时,表明该值没有被其他线程改过,就可以将新值N赋给V;反之,V和O不相同,表示O已经不是最新的旧值了,所以不能将N赋给V。其结果就是当多线程操作时,一个会成功,其他的会失败。
自旋锁:在多线程情况下,某一线程CAS操作失败,使用自旋方式,去不停的进行CAS访问。
ABA问题: CAS在检查旧值有没有变化的时候,如果这个旧值A被其他线程修改成了B,又被修改成了A,这样发现旧值没有变化,实际上已被其他线程修改过。 解决的方案就是采用数据库中常用的乐观锁方式,添加一个版本号就可以解决,
公平性:自旋锁的问题是处于自旋状态的线程,更有可能优先得到这把锁。
2. java对象头
Synchronized获取到的对象的锁实际上是对象头上的一个标志,存储在对象头中的Mark Word里。
2. 锁升级过程
- **起初是无锁状态,当某一个线程访问同步代码块并获取锁时,升级为偏向锁。**会在对象头和栈帧中的锁记录里存储偏向的线程ID,之后如果是同个线程再次进入同步代码块就不再需要进行CAS操作来加锁。
- 当有其他线程尝试竞争偏向锁时,就会升级为轻量级锁,JVM首先会在当前线程的栈帧中创建用于存储锁记录的缓存空间,并将对象头的Mark Word复制到锁记录中,然后当各个线程尝试对 对象头中的Mark Word进行CAS操作,如果成功,该线程获得锁,如果失败,代表其他线程也在竞争,该线程就会自旋来不停的获取锁。轻量级锁只要为了在 多线程在不同的时间段请求同一把锁这种情况下提高效率,来避免线程的阻塞以及唤醒。
- **当多线程同时竞争锁资源时,就会升级为重量级锁。**JVM虚拟机会阻塞加锁失败的线程,并且在目标锁 。Java线程的阻塞以及唤醒,都是依靠操作系统来完成的,需要进行用户态和内核态的切换,所以非常影响性能。而升级为重量级锁之后,再也无法退回轻量级,所以为了尽量避免这种多余的阻塞,唤醒操作。在线程进入阻塞状态之前,以及被唤醒之后竞争不到 锁的情况下,进入自旋状态,在处理器上空跑并且轮询锁是否被释放。如果此时锁恰好被释放了,那么当前线程便无须进入阻塞状态,而是直接获得这把锁。
3. 其他的优化
-
锁粗化
当连接在一起的多次加锁,解锁过程存在时,会将其合并为一个范围更大的锁。
-
锁消除
当检测到一段代码中,堆上的数据不会逃逸出 当前线程,那么可以认为这段代码是线程安全的,不必要加锁。
死锁问题
-
锁粗化
当连接在一起的多次加锁,解锁过程存在时,会将其合并为一个范围更大的锁。
-
锁消除
当检测到一段代码中,堆上的数据不会逃逸出 当前线程,那么可以认为这段代码是线程安全的,不必要加锁。
死锁问题
描述:不同的线程分别占用对方需要的同步资源,都在等待对方放弃自己需要的同步资源,就会出现死锁。