带着任务学习
- synchronized的作用对象?对象锁和类锁的区别?
- synchronized本质上是通过什么保证线程安全的?
- 加锁、释放锁的原理
- 可重入原理
- 保证可见性原理
- synchronized的缺陷有什么?Lock是怎样弥补这些缺陷的?
- synchronized和Lock的对比情况,如何选择?
- synchronized在使用时的注意事项
- synchronized修饰的方法在抛出异常时会释放锁吗?
- 多线程同时争抢一个锁时,JVM会如何选择哪个线程先获取锁?
- synchronized使得同一时刻只有一个线程可执行,并发性能较差,如何去提升性能?
- 什么是锁的升级和降级?JVM里的偏向锁、轻量级锁、重量级锁是什么?
synchronized使用注意事项
- 一把锁只能同时被一个线程获取,没有获得锁的线程只能等待
- 每个实例都有自己的一把锁,不同实例之间互不影响。但如果锁的对象是类、静态方法时,所有对象公用一把锁
- synchronized修饰的方法,无论方法是否正常执行完毕或是抛出异常,都会释放锁
synchronized原理分析
加锁、释放锁的原理
对象获得锁:monitorEnter指令
对象释放锁:monitorExit指令
一个对象在尝试获得monitor锁时会发生三种情况:
- monitor计数器为0,表示未被其他线程持有,请求线程会让计数器+1,使得其他线程不能获取锁
- 如果当前已经持有了monitor锁,并且又重入了这把锁时,计数器会随着重入次数累加
- 锁已经被其他线程持有,等待锁的释放
monitorexit指令:释放对于monitor的所有权,释放过程很简单,就是讲monitor的计数器减1,如果减完以后,计数器不是0,则代表刚才是重入进来的,当前线程还继续持有这把锁的所有权,如果计数器变成0,则代表当前线程不再拥有该monitor的所有权,即释放锁。
对象-监视器-同步队列关系
任意线程对Object的访问,首先需要获得Object的监视器,如果获取失败说明其他线程正在持有锁,这是未获得锁的线程需要进入同步队列SynchronizedQueue中并且线程状态变为BLOCKED,当Object的锁被释放后,在同步队列中的线程就会有机会重新获取该锁
可重入原理:monitor计数器
线程每次成功获取锁后会让monitor计数器+1,如果线程中有其他方法调用以及持有锁的对象,那么就不需要再次获取锁,只是让计数器+1即可
JVM中锁的优化
由于JVM中的monitor指令依赖于底层操作系统的Mutex Lock来实现,并且使用Mutex Lock时需要将当前线程挂起并从用户态切换到内核态来执行,这种切换的资源耗费是非常大的。而添加了Synchronized关键字并不一定会遇到并发问题,所以为了节省单线程下使用Synchronized导致的不必要的消耗JDK1.6对锁的实现做了大量优化:锁粗化、锁销除、轻量级锁、偏向锁、适用性自旋等技术来减少锁操作的开销。
- 锁粗化:将不必要的、连在一起的Lock、UnLock操作进行合并,扩展为一个范围更大的锁。
- 锁销除:通过运行时JIT编译器的逃逸分析来消除一些在当前同步块以内并被其他线程共享的数据的锁保护。
- 轻量级锁:当前同步代码出于无锁竞争的状态,在获取到锁后会依靠CAS原子指令完成锁的获取和释放;当存在锁竞争的情况下,执行CAS指令失败的线程会调用操作系统互斥锁进入到阻塞状态,当锁被释放是会唤醒。
- 偏向锁:是为了在无锁竞争的情况下避免在锁获取过程中执行不必要的CAS原子指令
- 适用性自旋:当线程在获取轻量级锁的过程中执行CAS操作失败时,在进入与monitor相关联的操作系统重量级锁(mutex semaphore)前会进入忙等待(Spinning)然后再次尝试,当尝试一定的次数后如果仍然没有成功则调用与该monitor关联的semaphore(即互斥锁)进入到阻塞状态。
锁的类型
Synchronized的四种类型:无锁 ——》 偏向锁 ——》 轻量级锁 ——》 重量级锁
锁的升级是不可逆的,会随着竞争情况逐渐升级。
自旋锁和自适应自旋锁
自旋锁
由于共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和回复阻塞线程并不值得。在如今多处理器环境下,完全可以让另一个没有获取到锁的线程在门外等待一会(自旋),但不放弃CPU的执行时间。等待持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需要让线程执行一个忙循环(自旋),这便是自旋锁由来的原因。
自适应自旋锁
在JDK 1.6中引入了自适应自旋锁。这就意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋 时间及锁的拥有者的状态来决定的。如果在同一个锁对象上,自旋等待刚刚成功获取过锁,并且持有锁的线程正在运行中,那么JVM会认为该锁自旋获取到锁的可能性很大,会自动增加等待时间。
锁销除
锁消除是指虚拟机即时编译器再运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持。意思就是:JVM会判断再一段程序中的同步明显不会逃逸出去从而被其他线程访问到,那JVM就把它们当作栈上数据对待,认为这些数据时线程独有的,不需要加同步。此时就会进行锁消除。
锁粗化
原则上,我们都知道在加同步锁时,尽可能的将同步块的作用范围限制到尽量小的范围(只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小。在存在锁同步竞争中,也可以使得等待锁的线程尽早的拿到锁)。
大部分上述情况是完美正确的,但是如果存在连串的一系列操作都对同一个对象反复加锁和解锁,甚至加锁操作时出现在循环体中的,那即使没有线程竞争,频繁的进行互斥同步操作也会导致不必要的性能操作。
轻量级锁
在JDK 1.6之后引入的轻量级锁,需要注意的是轻量级锁并不是替代重量级锁的,而是对在大多数情况下同步块并不会有竞争出现提出的一种优化。它可以减少重量级锁对线程的阻塞带来的线程开销。从而提高并发性能。
如果要理解轻量级锁,那么必须先要了解HotSpot虚拟机中对象头的内存布局。上面介绍Java对象头也详细介绍过。在对象头中(Object Header)存在两部分。第一部分用于存储对象自身的运行时数据,HashCode、GC Age、锁标记位、是否为偏向锁。等。一般为32位或者64位(视操作系统位数定)。官方称之为Mark Word,它是实现轻量级锁和偏向锁的关键。 另外一部分存储的是指向方法区对象类型数据的指针(Klass Point),如果对象是数组的话,还会有一个额外的部分用于存储数据的长度。
偏向锁
由于大多数情况下,锁并不只是与其他线程存在竞争,还可能会由同一线程下的其他方法来多次获取资源,如果在同一线程下反复获取锁,就会多出现获取锁和释放锁的操作这是一种不必要的性能开销和上下文切换。
偏向锁的概念是:当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和推出同步块时不需要进行CAS操作来加锁和解锁。只需要简单的测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果成功,表示线程已经获取到了锁。
偏向锁的撤销
偏向锁使用了一种等待竞争出现才会释放锁的机制。所以当其他线程尝试获取偏向锁时,持有偏向锁的线程才会释放锁。但是偏向锁的撤销需要等到全局安全点(就是当前线程没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,让你后检查持有偏向锁的线程是否活着。如果线程不处于活动状态,直接将对象头设置为无锁状态。如果线程活着,JVM会遍历栈帧中的锁记录,栈帧中的锁记录和对象头要么偏向于其他线程,要么恢复到无锁状态或者标记对象不适合作为偏向锁。<br />
锁的优缺点对比
Synchronized与Lock
Synchronized的缺陷
- 效率低:在锁竞争的情况下,锁的释放情况少,只有等持有锁的线程执行完对应方法或异常结束才会释放锁;试图获取锁的时候不能设置超时时间,不能中断一个正在使用锁的线程(Lock可以中断和设置超时时间)
- 不够灵活:加锁和释放锁的时机单一,每个锁仅有一个单一的条件(读写锁更灵活)
- 无法知道是否成功获得锁:(Lock可以拿到锁的状态信息,并做出对应的操作)
Lock解决相应的问题
- lock()加锁
- unlock():解锁
- tryLock():尝试获取锁,返回boolean
- tryLock(long, TimeUtil):尝试获取锁,并可设置超时时间
Synchronized只有锁只与一个条件(是否获取锁)相关联,不灵活,后来Condition与Lock的结合解决了这个问题。
多线程竞争一个锁时,其余未得到锁的线程只能不停的尝试获得锁,而不能中断。高并发的情况下会导致性能下降。ReentrantLock的lockInterruptibly()方法可以优先考虑响应中断。 一个线程等待时间过长,它可以中断自己,然后ReentrantLock响应这个中断,不再让这个线程继续等待。有了这个机制,使用ReentrantLock时就不会像synchronized那样产生死锁了。
再次深入理解
Synchronized的其他知识点:
- Synchronized是通过JVM实现的
- Synchronized锁住的对象不能为空,因为锁的信息保存在对象头里
- Synchronized作用域不能过大,容易影响性能和产生死锁
- 避免死锁
- 在能选择的情况下,既不要用Lock也不要用synchronized关键字,用java.util.concurrent包中的各种各样的类,如果不用该包下的类,在满足业务的情况下,可以使用synchronized关键字,以避免出错
- Synchronized是非公平锁,因为新的尝试获取锁的线程可能立刻获取到锁,而在等待队列中等待已久的线程可能再次等待(这样设计的原因是有利于提高性能,但也可能导致饥饿现象,即线程一直不能获取到锁)