一、 synchronized的三种应用方式
1.修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁。(锁的是当前对象)
2.修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁。(锁的是当前Class对象)
3.修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象。(锁的是{}中的对象)
二、synchronized的字节码指令
synchronized同步块使用了monitor.enter和monitor.exit指令实现同步,这两个指令,本质上都是对一个对象的监视器(monitor)进行获取,这个过程是排他的,也就是说同一时刻只能有一个线程获取到由synchronized所保护对象的监视器。
线程执行到monitor.enter指令时,会尝试获取对象所对应的monitor所有权,也就是尝试获取对象的锁,而执行monitor.exit,就是释放monitor的所有权。
三、synchronized的锁的原理
两个重要的概念:一个是对象头,另一个是monitor。
- Java对象头:Java对象头是实现synchronized的锁对象的基础。一般而言,synchronized使用的锁对象是存储在Java对象头里。它是轻量级锁和偏向锁的关键。
①Mark Word用于存储对象自身的运行时数据
②Class Metadata Address类型指针,即是对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
③Array length如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据。 - Monitor:Monitor是一个同步工具,它内置于每一个Object对象中,相当于一个许可证。拿到许可证即可以进行操作,没有拿到则需要阻塞等待。
四、synchronized锁的优化 - 自旋锁与自适应自旋
线程的挂起和恢复会极大的影响开销。并且jdk官方人员发现,很多线程在等待锁的时候,在很短的一段时间就获得了锁,所以它们在线程等待的时候,并不需要把线程挂起,而是让他无目的的循环,一般设置10次。这样就避免了线程切换的开销,极大的提升了性能。
而适应性自旋,是赋予了自旋一种学习能力,它并不固定自旋10次一下。他可以根据它前面线程的自旋情况,从而调整它的自旋,甚至是不经过自旋而直接挂起。 - 锁消除
对不会存在线程安全的锁进行消除。 - 锁粗化
如果jvm检测到有一串零碎的操作都对同一个对象加锁,将会把锁粗化到整个操作外部,如循环体。 - 偏向锁
多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让其获得锁的代价更低而引入了偏向锁。
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。
如果测试成功,表示线程已经获得了锁。
如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成01(表示当前是偏向锁)。
如果没有设置,则使用CAS竞争锁。
如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
5、轻量级锁
引入轻量级锁的主要目的是在多线程竞争不激烈的情况下,通过CAS竞争锁,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
6、重量级锁
重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。
7、锁升级
偏向锁升级轻量级锁:当一个对象持有偏向锁,一旦第二个线程访问这个对象,如果产生竞争,偏向锁升级为轻量级锁。轻量级锁升级重量级锁:一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。
五、wait和notify的原理
调用wait方法,首先会获取监视器锁,获得成功以后,会让当前线程进入等待状态进入等待队列并且释放锁。
当其他线程调用notify后,会选择从等待队列中唤醒任意一个线程,而执行完notify方法以后,并不会立马唤醒线程,原因是当前的线程仍然持有这把锁,处于等待状态的线程无法获得锁。必须要等到当前的线程执行完按monitor.exit指令以后,也就是锁被释放以后,处于等待队列中的线程就可以开始竞争锁了。
六、wait和notify为什么需要在synchronized里面
wait方法的语义有两个,一个是释放当前的对象锁、另一个是使得当前线程进入阻塞队列,而这些操作都和监视器是相关的,所以wait必须要获得一个监视器锁。
而对于notify来说也是一样,它是唤醒一个线程,既然要去唤醒,首先得知道它在哪里,所以就必须要找到这个对象获取到这个对象的锁,然后到这个对象的等待队列中去唤醒一个线程。
七、java.util.concurrent.Locks
比起标准的同步块来说,lock是一个更加灵活、更加精密的线程同步机制。Lock是从java1.5开始推出的,它定义在java.util.concurrent.Lock包里,它提供了更广泛的锁操作。
相同点:Lock能完成synchronized所实现的所有功能
不同点:1.synchronized是jvm层面上的,是一个关键字,Lock是一个类;
2.Lock有比synchronized更精确的线程语义和更好的性能,而且不强制性的要求一定要获得所; - synchronized同步数据少量的话,性能比Lock好,二数据大量同步,Lock性能要好;
- synchronized会自动释放锁,而Lock一定要求程序员手动释放,并且最好在finally块中释放(这是释放外部资源的最好的地方)
- synchronized只能包含在一个方法内----而lock()和unlock()操作却可以在跨越多个不同的方法使用。
- synchronized不支持公平性,任一个线程都能获取已经被释放的锁,不能指定优先权。但我们却可以使用Lock API指定公平属性从而实现公平性。它能确保等待时间最长的线程优先获取锁。
- 当一个线程不能访问synchronized时,它会被阻塞住。而 Lock API提供的有 tryLock()方法,使用该方法,只有在锁不被其他线程持有且可用时,才会真正获取锁。这将极大地降低阻塞时间
- 那些获取访问synchronized的等待线程不能被中断,Lock API提供了一个 lockInterruptbly()方法,当线程正在等待锁时,该方法可以用于中断该线程。
八、Lock API
void lock() - 如果锁可用就获取锁。如果锁不可用就阻塞住,直到锁被释放void lockInterruptibly() - 这个方法和lock()方法很类似,但是它允许阻塞的线程被中断,并且通过抛出一个java.lang.InterruptedException可以重新运行。
boolean tryLock() - 这是lock()方法的非阻塞版本;它会立即试图获取锁,如果锁定成功的话,就返回true。
boolean tryLock(long timeout,TimeUnit timeUnit) -这和tryLock()方法很像,只不过该方法,会在放弃获取锁之前,等待一段指定时间。
void unlock() -解锁该锁实例。
一个锁实例应该永远处于解锁状态,这样才能避免死锁条件。使用lock的推荐方式是:代码块中应该包含try/catch 以及finally 块。
Lock lock = …;
lock.lock();
try{
// access to the shared resource
} finally{
lock.unlock();
}
除了Lock接口之外,我们还有一个读写锁ReadWriteLock接口,此接口包含了一对锁,一个用于只读操作,一个用于写操作。只要没有写操作,读锁可以同时被多个线程持有。
ReadWriteLock 所声明的用于获取读、写锁的方法:
.Lock readLock() - 返回用于读操作的锁。
.Lock writeLock() - 返回用于写操作的锁。