说起synchronized相信大家都很熟悉,就这个东西叫做互斥锁,平时呢可以帮助我们实现譬如线程安全的问题。那么今天咱们就来深入底层,好好的谈一下synchronized的原理和应用
一.谈一下对于synchronized的了解
synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被他修饰的方法或者代码块在任意时刻只有一个线程执行。但是这个关键字虽然从功能上看还不错,但是在JDK6之前synchronized是不被人喜欢的,因为它是重量级锁,效率非常低。这是因为监视器锁monitor是依赖于底层的操作系统的Mutex lock来实现的,Java的线程是映射到操作系统的原生线程之上的,如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的转化需要从用户态转到内核态,这个状态之间的转化需要相对比较长的时间,时间成本相对较高。但是jdk6之后,Java官方从JVM层面对synchronized进行了较大的优化,所以现在的效率还算可以,至于优化方式,就包括了自旋锁,适应性自旋锁,锁销除,锁粗化,偏向锁等来减少锁系统的开销。
二.谈一下如何使用synchronized关键字?
synchronized主要有三种用法:
1)修饰实例方法:作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁。
2)修饰静态方法: 给当前的类加锁,作用域所有类的对象实例,因为静态成员属于类成员。所以如果一个线程A调用了一个实例对象的非静态synchronized方法,而线程B需要调用这个实例对象所属类的静态synchronized方法是允许的,这样不会发生互斥现象,因为访问静态synchronized方法占用的锁是当前类的锁,而访问非静态synchrinized方法占用的是当前实例对象锁。
3)修饰代码块:制定加锁对象,对给定的对象加锁,进入同步代码块之前要获得给定对象的锁。
所以修饰静态方法和代码块都是给类上锁,修饰实例方法是给实例对象上锁。尽量不要使用synchronized(String a)因为JVM中字符串常量池具有缓存功能。
3.谈一下单例模式和双重检验锁方式实现单例模式的原理
public class Singleton{ private volatile static Singleton uniqueInstance; private Singleton(){ } public static Singleton getUniqueInstance(){ //先判断对象是否已经实例过,没有实例过才进入加锁代码 if (uniqueInstance==null){ synchronized (Singleton.class){ if(uniqueInstance==null){ uniqueInstance =new Singleton(); } } } return uniqueInstance; } }
需要注意uniqueInstance采用volatile修饰也是非常重要的。uniqueInstance=new Singleton();这段代码其实分为了三步执行:
1.为uniqueInstance分配内存空间
2.初始化uniqueInstance
3.将uniqueInstance指向分配的内存地址。
但是由于JVM指令重排的特性,执行顺序可能变成了1,3,2。这样在多线程环境下会导致一个线程获得还没有初始化的实例。例如线程1执行1和3,此时线程2调用getUniqueInstance()后发现uniqueInstance不为空,因此返回了uniqueInstance但此时UniqueInstance还没有初始化,所以运行就不会正常,但是加上volatile可以禁止JVM指令重排。保证在多线程的条件下也可以正常运行。
三.讲一下synchronized关键字的底层原理。
synchronized底层原理属于jvm层面
1)synchronized同步代码块
public class SynchronizedDemo{ public void method(){ synchronized(this){ System.out.println("synchronized 代码块"); } } }
通过JDK自带的javap命令可以查看SynchronizedDemo类相关的字节码文件。首先切换到类的对应目录执行javac Syschronzied.java的命令生成编译后的.class文件,然后执行javap -c -s -v -l Syschronized.class。可以看到synchronized使用的monitorenter和monitorexit命令实现的,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令指向的是代码块结束的位置。也就是说,线程试图获取锁也就相当于获取monitor(monitor对象存在于每个Java对象的对象头中,synchronized就是通过这种方式获取锁的,也是为什么java中任意对象可以作为锁的原因)的持有权,当计数器为0时,则可以获取成功,之后将计数器加1,在执行monitorexit的命令之后,计数器就设为0,表明了锁已经被重新释放,如果获取失败就需要当前线程阻塞等待,知道锁被另外一个线程释放为止。
2)synchronize修饰方法的时候。
在修饰方法的时候,底层代码相对简单,就是加入了ACC_SYNCHRONIZED标识,这个标识指明了该方法是一个同步方法,JVM通过该访问标识来确定是不是一个同步方法从而执行相应的调用。
四.谈一谈JDK6之后synchronized关键字底层的优化
JDK1.6对锁的实现引入了大量的优化,如偏向锁,轻量级锁,自旋和适应性自旋等等来减少锁的开销。锁主要有四种形态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。锁可以升级不可以降级,这种策略是为了可以提高获得锁和释放锁的效率。这个想要理解深入的化,建议去看一下《深入了解Java虚拟机第二版》,一定会受益匪浅。
五.谈谈synchronized和reentrantlock的区别
1)两者都是可重入锁。
可重入锁的概念:自己可以再次获取自己的内部锁,比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获得这个对象的锁的时候还是可以获取的,如果不可锁冲入的话,就会造成死锁。每一个线程每次获得锁,锁的计数器都会自增1,所以想要等到锁的计数器下降为0时才能释放锁。
2)synchronized依赖于JVM而reentrantLock依赖于API
前面已经说了太多synchronized在JVM当中的实现和优化了,那么为什么说reentranLock是在API层面呢,就是JDK层面,因为它需要lock()和unlock()方法配合try/finally语句块来完成。所以我们可以通过他的源代码来看他是如何实现的。
3)Reentrantlock比synchronized增加了很多高级的功能。
主要说来有三点,1.等待可中断2.可实现公平锁3.可实现选择性通知(锁可以绑定多个条件)
Reentrantlock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制,也就是说正在等待的线程可以选择放弃等待,改为处理其他的事情。
Reentrantlock可以指定是公平锁还是非公平锁,也就是先等待的线程先获得。这是通过ReetranLock(boolean fair)来制定的。
synchronized中的wait()和notify()/notifyAll()方法相结合可以实现等待通知机制,但是Reentrantlock需要记住Condition接口和newCondition()方法。这个接口是JDK1.5之后才有的,具有很好的灵活性。比如可以实现一个Lock对象中可以创建多个Condition实例,即对象监视器。线程对象可以注册在制定的Condition中,从而有选择的进行线程通知,在调度线程上更加灵活,在使用notify()方法进行通知时,被通知的线程是JVM选择的,而ReetrantLock可以和Condition实例结合实现选择性通知。而synchronized只有一个Condition实例,所以执行signAll()方法的话就会通知所有等待的线程,很费事。
原文地址:https://www.cnblogs.com/ffdsj/p/12396199.html