深入理解synchronized关键字

深入理解synchronized关键字 synchronized是并发编程中重要的使用工具之一,我们必须学会使用并且掌握它的原理。 概念及作用 JVM自带的关键字,可在需要线程安全的业务场景中使用,来保证线程安全。 用法 按照锁的对象区分可以分为对象锁和类锁 按照在代码中的位置区分可以分为方法形式和代码块形式 对象锁 锁对象为当前this或者说是当前类的实例对象 复制代码 public void synchronized method(){ System.out.println("我是普通方法形式的对象锁"); } public void method(){ synchronized(this){   System.out.println("我是代码块形式的对象锁");    } } 复制代码 类锁 锁的是当前类或者指定类的Class对象。一个类可能有多个实例对象,但它只可能有一个Class对象。 复制代码 public static void synchronized method(){   System.out.println("我是静态方法形式的类锁"); } public void method(){   synchronized(*.class){     System.out.println("我是代码块形式的类锁");   } } 复制代码 SimpleExample [参考] https://www.imooc.com/learn/1086 【慕课网,Java高并发之魂:synchronized深度解析】 最基本的用法在上一个标题用法中已将伪代码列出,这里列举在以上基础上稍微变化一些的用法 1.多个实例,对当前实例加锁,同步执行,对当前类Class对象加锁,异步执行 复制代码 public class SimpleExample implements Runnable{   static SimpleExample instance1 = new SimpleExample();   static SimpleExample instance2 = new SimpleExample();   @Override   public void run(){     method1();     method2();     method3();     method4();   }   public synchronized void method1(){     common();   }   public static synchronized void method2(){     commonStatic();   }   public void method3(){     synchronized(this){       common();     }   }   public void method4(){     synchronized(MultiInstance.class){       common();     }   }   public void method5(){     common();   }   public void method6(){     commonStatic();   }   public void common(){     System.out.println("线程 "+Thread.currentThread().getName()+" 正在执行");     try{       Thread.sleep(1000);     }catch(InterruptedException e){       e.printStackTrace();     }     System.out.println("线程 "+Thread.currentThread().getName()+" 执行完毕");   }   public static void commonStatic(){     System.out.println("线程 "+Thread.currentThread().getName()+" 正在执行");     try{       Thread.sleep(1000);     }catch(InterruptedException e){       e.printStackTrace();     }     System.out.println("线程 "+Thread.currentThread().getName()+" 执行完毕");   }   public static void main(String[] args)throws InterruptedException{     Thread t1 = new Thread(instance1);     Thread t2 = new Thread(instance2);     t1.start();     t2.start();     t1.join();     t2.join();     System.out.println("finished");   } } method1()、method3()结果为: 线程Thread-0正在执行 线程Thread-1正在执行 线程Thread-0执行完毕 线程Thread-1执行完毕 finished method2()、method4()执行结果为: 线程Thread-0正在执行 线程Thread-0执行完毕 线程Thread-1正在执行 线程Thread-1执行完毕 finished 复制代码 2.对象锁和类锁,锁的对象不一样,互不影响,所以异步执行 复制代码 // 将run方法改为 @Override public void run(){   if("Thread-0".equals(Thread.currentThread().getName())){     method1();   }else{     method2(); } } // 将main方法改为 public static void main(String[] args) throws InterruptedException{ Thread t1 = new Thread(instance1); Thread t2 = new Thread(instance1); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("finished"); } 结果为: 线程Thread-0正在执行 线程Thread-1正在执行 线程Thread-1执行完毕 线程Thread-0执行完毕 finished 复制代码 3.对象锁和无锁得普通方法,普通方法不需要持有锁,所以异步执行 复制代码 // 将run方法改为 @Override public void run(){   if("Thread-0".equals(Thread.currentThread().getName())){     method1();   }else{     method5();   } } // main方法同 2 结果为: 线程Thread-0正在执行 线程Thread-1正在执行 线程Thread-0执行完毕 线程Thread-1执行完毕 finished 复制代码 4.类锁和无锁静态方法,异步执行 复制代码 // 将run方法改为 @Override public void run(){   if("Thread-0".equals(Thread.currentThread().getName())){     method1();   }else{     method6();   } } // main方法同 2 结果为: 线程Thread-0正在执行 线程Thread-1正在执行 线程Thread-0执行完毕 线程Thread-1执行完毕 finished 复制代码 5.方法抛出异常,synchronized锁自动释放 复制代码 // run方法改为 @Override public void run(){   if("Thread-0".equals(Thread.currentThread().getName())){     method7();   }else{     method8();   } } public synchronized void method7(){   try{     ...     throw new Exception();   }catch(Exception e){     e.printStackTrace();   } } public synchronized void method8(){   common(); } public static void main(String[] args) throws InterruptedException{   // 同 2 } 结果为: 线程Thread-0正在执行 java.lang.Exception at com.marksman.theory2practicehighconcurrency.synchronizedtest.blog.SynchronizedException.method7(SynchronizedException.java:26) at com.marksman.theory2practicehighconcurrency.synchronizedtest.blog.SynchronizedException.run(SynchronizedException.java:15) at java.lang.Thread.run(Thread.java:748) 线程Thread-0执行结束 线程Thread-1正在执行 线程Thread-1执行结束 finished // 这说明抛出异常后持有对象锁的method7()方法释放了锁,这样method8()才能获取到锁并执行。 复制代码 6.可重入特性 复制代码 public class SynchronizedRecursion{   int a =0;   int b =0;   private void method1(){     System.out.println("method1正在执行,a = "+ a);     if(a ==0){       a ++;       method1();     }     System.out.println("method1执行结束,a = "+ a);   }   private synchronized void method2(){     System.out.println("method2正在执行,b = "+ b);     if(b == 0){       b ++;       method2();     }     System.out.println("method2执行结束,b = "+ b);   }   public static void main(String[] args){     SynchronizedRecursion synchronizedRecursion = new SynchronizedRecursion();     synchronizedRecursion.method1();     synchronizedRecursion.method2();   } } 结果为: method1正在执行,a =0 method1正在执行,a =1 method1执行结束,a =1 method1执行结束,a =1 method2正在执行,b =0 method2正在执行,b =1 method2执行结束,b =1 method2执行结束,b =1 // 可以看到method1()与method2()的执行结果一样的,method2()在获取到对象锁以后,在递归调用时不需要等上一次调用先释放后再获取,而是直接进入,这说明了synchronized的可重入性. // 当然,除了递归调用,调用同类的其它同步方法,调用父类同步方法,都是可重入的,前提是同一对象去调用,这里就不一一列举了. 复制代码 总结一下 一把锁只能同时被一个线程获取,没有拿到锁的线程必须等待; 每个实例都对应有自己的一把锁,不同实例之间互不影响; 锁对象是*.class以及synchronized修饰的static方法时,所有对象共用一把类锁; 无论是方法正常执行完毕或者方法抛出异常,都会释放锁; 使用synchronized修饰的方法都是可重入的。 synchronized的实现原理 monitorenter和monitorexit 将下面两段代码分别用 javac *.java编译成.class文件,再反编译 javap -verbose *.class文件 复制代码 public class SynchronizedThis{   public void method(){     synchronized(this){}   } } // 反编译结果 public void method(); descriptor:()V flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=1 0: aload_0 1: dup 2: astore_1 3: monitorenter 4: aload_1 5: monitorexit 6:goto14 9: astore_2 10: aload_1 11: monitorexit 12: aload_2 13: athrow 14:return public class SynchronizedMethod{   public synchronized void method(){} } // 反编译结果 public synchronized void method(); descriptor:()V flags: ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=0, locals=1, args_size=1 0:return LineNumberTable: line 2:0 复制代码 可以看到: synchronized加在代码块上,JVM是通过monitorenter和monitorexit来控制锁的获取的释放的; synchronized加在方法上,JVM是通过ACC_SYNCHRONIZED来控制的,但本质上也是通过monitorenter和monitorexit指令控制的。 对象头 [参考] https://www.jianshu.com/p/3d38cba67f8b 【简书,Java对象头详解】 上面我们提到monitor,这是什么鬼? 其实,对象在内存是这样存储的,包括对象头、实例数据和对齐填充Padding,其中对象头包括 Mark Word和类型指针。 Mark Word Mark Word用于存储对象自身的运行时数据,如哈希码(identity_hashcode)、GC分代年龄(age)、锁状态标志(lock)、线程持有的锁、偏向线程ID(thread)、偏向时间戳(epoch)等等,占用内存大小与虚拟机位长一致。 Mark Word (32 bits) State 锁状态 identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 Normal 无锁 thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 Biased 偏向锁 ptr_to_lock_record:30 | lock:2 Lightweight Locked 轻量级锁 ptr_to_heavyweight_monitor:30 | lock:2 Heavyweight Locked 重量级锁 | lock:2 Marked for GC GC标记 Mark Word (64 bits) State 锁状态 unused:25|identity_hashcode:31|unused:1|age:4|biased_lock:1|lock:2 Normal 无锁 thread:54 |epoch:2|unused:1|age:4|biased_lock:1|lock:2 Biased 偏向锁 ptr_to_lock_record:62 | lock:2 Lightweight Locked 轻量级锁 ptr_to_heavyweight_monitor:62 | lock:2 Heavyweight Locked 重量级锁 | lock:2 Marked for GC GC标记 可以看到,monitor就存在Mark Word中。 类型指针 类型指针指向对象的类元数据metadata,虚拟机通过这个指针确定该对象是哪个类的实例。 锁状态 biased_lock lock 状态 0 01 无锁 1 01 偏向锁 0 00 轻量级锁 0 10 重量级锁 0 11 GC标记 JDK对synchronized的优化 jdk1.6之前synchronized是很重的,所以并不被开发者偏爱,随着后续版本jdk对synchronized的优化使其越来越轻量,它还是很好用的,甚至ConcurrentHashMap在jdk的put方法都在jdk1.8时从ReetrantLock.tryLock()改为用synchronized来实现同步。 并且还引入了偏向锁,轻量级锁等概念,下面是偏向锁和轻量级锁的获取流程 ![偏向锁和轻量级锁的获取流程] https://www.processon.com/diagraming/5c25db87e4b016324f447c95 【ProncessOn 公开克隆】 [参考] 链接:https://pan.baidu.com/s/1gA_URintNDiq_SuAbJQE2Q 提取码:s6vx 【咕泡学院公开课】 偏向锁 baised_lock 如果一个线程获取了偏向锁,那么如果在接下来的一段时间里,如果没有其他线程来抢占锁,那么获取锁的线程在下一次进入方法时不需要重新获取锁。 synchronized与ReentrantLock的区别 [参考] https://time.geekbang.org/column/article/8799 【极客时间,Java核心技术36讲专栏】 区别 synchronized ReentrantLock 灵活性 代码简单,自动获取、释放锁 相对繁琐,需要手动获取、释放锁 是否可重入 是 是 作用位置 可作用在方法和代码块 只能用在代码块 获取、释放锁的方式 monitorenter、monitorexit、ACC_SYNCHRONIZED 尝试非阻塞获取锁tryLock()、超时获取锁tryLock(long timeout,TimeUnit unit)、unlock() 获取锁的结果 不知道 可知,tryLock()返回boolean 使用注意事项 1、锁对象不能为空(锁保存在对象头中,null没有对象头) 2、作用域不宜过大 1、切记要在finally中unlock(),否则会形成死锁 2、不要将获取锁的过程写在try块内,因为如果在获取锁时发生了异常,异常抛出的同时,也会导致锁无故被释放。 https://www.cnblogs.com/-Marksman/p/10232382.html
50000+
5万行代码练就真实本领
17年
创办于2008年老牌培训机构
1000+
合作企业
98%
就业率

联系我们

电话咨询

0532-85025005

扫码添加微信