性质
可重入
什么是可重入:指的是同一线程的外层函数获得锁之后,内层函数可以直接再次获取该锁
好处:避免死锁、提升封装性
粒度:线程而非调用
情况1:证明同一个方法是可重入的
package com.gwh.lock.test;
/**
* 可重入粒度测试:递归调用本方法
*/
public class SynchronizedRecursion10 {
int a=0;
public static void main(String[] args) {
SynchronizedRecursion10 synchronizedRecursion10=new SynchronizedRecursion10();
synchronizedRecursion10.method1();
}
private synchronized void method1(){
System.out.println("这是method1,a="+a);
if(a==0){
a++;
method1();
}
}
}
情况2:证明可重入不要求是同一个方法
package com.gwh.lock.test;
/**
* 证明可重入不要求是同一个方法
* 可重入粒度测试:调用类内部另外的方法
*/
public class SynchronizedOtherMethod11 {
public synchronized void method1(){
System.out.println("我是method1");
method2();
}
public synchronized void method2(){
System.out.println("我是method2");
}
public static void main(String[] args) {
SynchronizedOtherMethod11 otherMethod11=new SynchronizedOtherMethod11();
otherMethod11.method1();
}
}
情况3:证明可重入不要求是同一个类中的
package com.gwh.lock.test;
/**
* 可重入粒度测试,调用父类方法
*/
public class SynchronizedSuperClass12 {
public synchronized void doSomething(){
System.out.println("我是父类方法");
}
}
class TestClass extends SynchronizedSuperClass12{
public synchronized void doSomething(){
System.out.println("我是子类方法");
super.doSomething();
}
public static void main(String[] args) {
TestClass testClass=new TestClass();
testClass.doSomething();
}
}
不可中断
一旦这个锁,已经被被人获得了,如果我还想获得,我只能选择等待或者阻塞,直到别的线程释放这个锁,如果别人永远不释放锁,那么我只能永远的等下去
相比于lock类,可以拥有中断的能力,第一点,如果我觉的我等的时间太久了,有权中断现在已经获取到的锁的线程的执行;第二点,如果我觉的我等待的时间太久了不想再等了,也可以退出
原理
加锁和释放锁的原理:现象、时机、深入JVM看字节码
现象
每一个类的实例都对应一把锁,而每一个Synchronized的方法都必须的先获得调用该方法的类的实例的锁,方能执行,否则这个线程会阻塞,而这个方法一旦执行了他就独占了这把锁,直到这个方法返回或者抛出异常,才会释放锁;一旦释放之后,剩下那些被阻塞的线程才能获得这把锁,重新进入到可执行的状态,这就意味着,当一个对象中有Synchronized修饰的方法或者代码块的时候,要想执行这个方法,就必须的先获取这个方法的对象锁,如果此对象的对象锁,已经被其他对象所占用,就必须等待到他被释放,所有JAVA对象都含有一把互斥锁这个锁对象有JVM自动获取,和释放,我们只用指定这把锁的对象就可以了,至于锁的获取和释放不需要我们自己操作。
获取和释放锁的时机:内置锁
等价方法
package com.gwh.lock.test;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SynchronizedToLock13 {
Lock lock=new ReentrantLock();
public synchronized void method1(){
System.out.println("我是synchronized形式的锁");
}
public void method2(){
lock.lock();
try {
System.out.println("我是lock形式的锁");
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
SynchronizedToLock13 toLock13=new SynchronizedToLock13();
toLock13.method1();
toLock13.method2() ;
}
}
反编译类
package com.gwh.lock.test;
/**
* 反编译字节码
*/
public class Decompilation14 {
private Object object=new Object();
public void insert(){
synchronized (object){
}
}
}
Monditorenter和Moditorexit指令
Monditorenter:每一个对象都与一个Monditorenter相关联,而一个Monditorenter的lock锁只能被一个线程在同一时间所获得,而一个线程再尝试获得这个Monditorenter的所有权的时候,只会发生以下3种情况
1.Monditorenter计数器为0,这意味着目前还没有获得,然后这个线程会立刻获得Monditorenter对象,然后把Monditorenter计数器加一,当其他线程再进来的时候,看到这个加一,就知道Monditorenter对象已经被别的线程所获取,就说明这个线程是Monditorenter对象的所有者。
2.如果说Monditorenter已经拿到这个锁的所有权,又重入了,这样会导致计数器累积
3.如果Monditorenter已经被其他线程所持有了,而其他线程去获取他的时候,只能得到获取不了的信号,然后就会进入到阻塞状态直到这个Monditorenter计数器为0的时候才能获取锁
Moditorexit:作用是释放Monditorenter对象的所有权
前提是已经拥有了Monditorenter锁的所有权,才能释放,释放的过程就是将Monditorenter对象的计数器减一,如果Monditorenter计数器为0的时候,就说明这个线程已经没有Monditorenter的所有权了。如果减完之后不为0,那就说明刚才是可重入进来的,他就还获得这把锁,如果说,减到0之后,还未释放锁,那就说明,刚才其他受阻塞的线程,还在尝试获取这把锁的所有权。
可重入原理:加锁次数计数器
JVM负责跟踪对象被加锁的次数
线程第一次给对象加锁的时候,计数变为1.每当这个线程的线程在此对象上再次获得锁时,计数会递增
当任务离开的时候,计数递减,当计数为0的时候,锁被完全释放
保证可见性的原理:内存模型
比如a=0,a++,循环执行,当他在被Synchronized所修饰的方法中执行时,先获取到a的值,然后把a写入到主内存,然后执行另一个线程,另一个线程也会从主内存中获取到被第一次执行所写入的值,保证线程的安全
Synchronized的缺陷
1.效率低:锁的释放情况少、试图获取锁时不能设定超时、不能中断一个正在试图获得锁的线程
锁的释放情况少:我们都知道,当一个线程获取到线程锁的时候,其他线程就会被阻塞,如果其他线程想获取到这把线程锁的时候,他就只能等待到当前这个线程去释放,而释放情况少,就体现在只有两种情况下线程才会释放锁,第一种情况是:当我们的程序执行完这段代码的时候,我们线程就会释放这把锁,第二种情况,就是执行到一半发生异常了,我们的JVM才会自动把线程锁给释放掉,除了这两种情况下,我们的Synchronized都不会释放锁,所以如果当我们的程序在执行大的操作的时候,其他线程只能再等待,这样就影响程序的效率
试图获取锁时不能设定超时:刚才说了,程序不释放锁的时候,其他线程只能用来等待,没法设定超时的时间
不能中断一个正在试图获得锁的线程:不能中断
2.不够灵活:加锁和释放锁的时机单一,每个锁仅有单一的条件,可能是不够的
3.无法知道是否成功获取到锁
常见面试问题
Synchronized的使用注意点?
1.使用注意点:锁对象不能为空,作用域不宜过大,避免死锁
锁对象不能为空:就是我们如果指定了一个对象为我们的锁对象,那么他就必须是被实例化过的,就是被new过的,不是一个空对象,因为我们的Synchronized是放在我们的对象头中修饰的,如果这个对象为空,更没有对象头,那么这个锁是没有办法工作的
作用域不宜过大:作用域是指被Synchronized代码块所包括的范围,如果我们尽量多的代码都被Synchronized所修饰,那么我们代码就会达到安全的目的,但是对于效率就会降低,我们多线程编程,目的是为了提高效率,再不需要安全的情况下,我们并行去执行,是可以提高运行的效率,所以如果我们把Synchronized作用域设置的过大,就会影响我们程序的执行效率的。
2.如何选择Lock和Synchronized关键字?
建议:1.如果可以的话,尽量就不使用lock也不要使用Synchronized关键字而是使用java.Util.common中的各种包中的类
2.如果Synchronized在程序中适用,那么我们就优先选择Synchronized,因为这样可以减少我们所需要编写的代码,可以减少出错率
3.如果特别需要用的lock的时候,我们再使用lock
3.多线程访问同步方法的各种具体情况
4.多个线程等待同一个Synchronized锁的时候,JVM如何选择下一个获取锁的线程?
一个持有锁的线程,在程序运行结束或者出现异常的时候,就会释放这把锁。这个时候除了刚才已经在等待中的线程之外,其他线程也会来竞争这把锁,而竞争结果是由JVM来操作的,获取最终的结果是不公平的(随机的),有可能是等待时间最长的线程获取这把锁,也有可能是刚生成的线程去获取这把锁
5.Synchronized使得同时只有一个线程可以执行,性能较差,由什么办法可以提升性能呢?
1.优化使用范围
2.使用其他的lock锁
6.一句话介绍Synchronized
JVM会自动通过使用monitor来加锁和解锁,保证了同时只有一个线程可以执行指定代码,从而保证了线程安全,同时具有可重入和不可中断的性质