前言
上篇博客我们总结了线程安全性问题。造成线程安全性问题的其中一个诱因就是多线程环境下存在共享数据;
而解决这个诱因的方法就是利用Synchronized的互斥性;
原理就是当存在多个线程操作共享数据时,需要保证同一时刻有且只有一个线程在操作共享数据,其他线程必须等待该线程执行完后才能再执行。
应用方式
1、修饰普通实例方法,锁住的是该类的实例对象
/**
* synchronized 放在普通方法上,内置锁就是当前类的实例
* @return
*/
public synchronized int getNext() {
return value ++;
}
内置锁指的是获得锁和释放锁是隐式的,进入synchronized修饰的代码就获得锁,走出相应的代码就释放锁。
相对应与内置锁的是显示锁,ReentrantLock实现的是显示锁,加锁和释放锁通过lock和unlock来实现。
2、修饰静态方法,锁住的是类对象
/**
* 修饰静态方法,内置锁是当前的Class字节码对象
* 由于静态成员不专属于任何一个实例对象,是类成员,因此通过class对象锁可以控制静态 成员的并发操作。
* Sequence.class
* @return
*/
public static synchronized int getPrevious() {
return value --;
}
3、修饰代码块,指定加锁对象,对指定对象加锁。同步代码块主要有三种方式:synchronized(this)、synchronized(obj)、synchronized(object.class),分别是指锁住本对象、锁住指定的实例对象对象、锁住类对象
//this,当前实例对象锁
synchronized(this){
for(int j=0;j<1000000;j++){
i++;
}
}
//锁住类对象
synchronized(AccountingSync.class){
for(int j=0;j<1000000;j++){
i++;
}
}
性能
1、synchronized,未获取到锁的线程采取的是无线等待策略,一旦开始等待,就既不能中断也不能取消,容易产生饥饿和死锁的问题。
2、在线程调用notify方法时,随机唤醒一个等待队列中的线程,无法满足公平性;
3、synchronized在jdk1.5之前,属于重量级锁,上篇博客也提到了,重量级锁在时间片轮换时,会发生上下文切换问题,影响性能问题;
jdk1.6以后,修改了管理内置锁的算法,引入了偏向锁和轻量级锁。随着竞争情况的升级,锁会也从偏向锁、轻量级锁、重量级锁逐渐升级。
偏向锁
由来
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,而每次的锁的获得和释放都会消耗大量性能,为了让线程获得锁的代价更低而引入了偏向锁。
适用情况
偏向锁适用于只有一个线程访问同步代码块的情况。
如何升级为轻量级锁
而偏向锁的获取和释放和jvm的底层实现原理以及对象头有关,底层的东西还需继续学习。
偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁;如果线程一持有偏向锁,线程二竞争时,会先判断线程一是否存在,如果线程一存在,偏向锁升级为轻量级锁,如果线程一不存在,线程二获得偏向锁。
参考博客:偏向锁升级轻量级锁
轻量级锁
适用情况
轻量级锁可以多个线程同时使用。
自旋
另外轻量级锁的特点是自旋:竞争的线程在竞争锁时,不会发生阻塞,提高了程序的响应速度正式由于自旋的缘故。
自旋就是空转cpu,是相当耗性能的。当一个线程获得锁后,另一个线程自旋等待,当第一个线程释放锁后,等待的线程就可以继续。
如何升级为重量级锁
当自旋获取锁仍然失败时,表示存在其他线程竞争锁(两条或两条以上的线程竞争同一个锁),则轻量级锁膨胀成重量级锁。
参考博客:轻量级锁升级为重量级锁