介绍 synchronized是java关键字。JVM规范中,synchronized关键字用于在线程并发执行时,保证同一时刻,只有一个线程可以执行某个代码块或方法;同时还保证了代码在执行完后所修改的数据对其它线程是可见的。总结来说:synchronized解决了并发编程安全问题的原子性,可见性,有序性。
Java中每一个对象都可以作为锁(monitor),这是synchronized实现同步的基础。synchronized作用于每一个对象时,要求占有当前对象的锁要么没有任何线程占用,或者是当前线程占用,这样该线程才能获得该对象的访问权,一旦某个线程获得对象的访问权,其它线程就会因为无法获得访问权限而进入阻塞状态。
synchronized可用于3种情况:
普通同步方法(实例方法),锁是当前实例对象 ,进入同步代码前要获得当前实例的锁
静态同步方法,锁是当前类的Class对象 ,进入同步代码前要获得当前类的Class对象的锁
同步方法块,锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
基本使用 没有同步的情况 代码段1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 public class SynchronizedTest01 { public void method1 () { System.out.println("Method 1 start" ); try { System.out.println("Method 1 execute" ); Thread.sleep(3000 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Method 1 end" ); } public void method2 () { System.out.println("Method 2 start" ); try { System.out.println("Method 2 execute" ); Thread.sleep(1000 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Method 2 end" ); } public static void main (String[] args) { final SynchronizedTest01 test = new SynchronizedTest01(); new Thread(new Runnable() { @Override public void run () { test.method1(); } }).start(); new Thread(new Runnable() { @Override public void run () { test.method2(); } }).start(); } }
执行结果如下,线程1和线程2同时进入执行状态,线程2执行速度比线程1快,所以线程2先执行完成,这个过程中线程1和线程2是同时执行的。
1 2 3 4 5 6 Method 1 start Method 1 execute Method 2 start Method 2 execute Method 2 end Method 1 end
对普通方法同步 代码段2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 public class SynchronizedTest02 { public synchronized void method1 () { System.out.println("Method 1 start" ); try { System.out.println("Method 1 execute" ); Thread.sleep(3000 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Method 1 end" ); } public synchronized void method2 () { System.out.println("Method 2 start" ); try { System.out.println("Method 2 execute" ); Thread.sleep(1000 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Method 2 end" ); } public static void main (String[] args) { final SynchronizedTest02 test = new SynchronizedTest02(); new Thread(new Runnable() { @Override public void run () { test.method1(); } }).start(); new Thread(new Runnable() { @Override public void run () { test.method2(); } }).start(); } }
执行结果如下,跟代码段一比较,可以很明显的看出,线程2需要等待线程1的method1执行完成才能开始执行method2方法。
1 2 3 4 5 6 Method 1 start Method 1 execute Method 1 end Method 2 start Method 2 execute Method 2 end
对静态方法(类)同步 代码段3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 public class SynchronizedTest03 { public static synchronized void method1 () { System.out.println("Method 1 start" ); try { System.out.println("Method 1 execute" ); Thread.sleep(3000 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Method 1 end" ); } public static synchronized void method2 () { System.out.println("Method 2 start" ); try { System.out.println("Method 2 execute" ); Thread.sleep(1000 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Method 2 end" ); } public static void main (String[] args) { final SynchronizedTest03 test01 = new SynchronizedTest03(); final SynchronizedTest03 test02 = new SynchronizedTest03(); new Thread(new Runnable() { @Override public void run () { test01.method1(); } }).start(); new Thread(new Runnable() { @Override public void run () { test02.method2(); } }).start(); } }
执行结果如下,对静态方法的同步本质上是对类的同步(静态方法本质上是属于类的方法,而不是对象上的方法),所以即使test01和test2属于不同的对象,但是它们都属于SynchronizedTest03类的实例,所以也只能顺序的执行method1和method2,不能并发执行。
1 2 3 4 5 6 Method 1 start Method 1 execute Method 1 end Method 2 start Method 2 execute Method 2 end
使用代码块同步 代码段4
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 public class SynchronizedTest04 { public void method1 () { System.out.println("Method 1 start" ); try { synchronized (this ) { System.out.println("Method 1 execute" ); Thread.sleep(3000 ); } } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Method 1 end" ); } public void method2 () { System.out.println("Method 2 start" ); try { synchronized (this ) { System.out.println("Method 2 execute" ); Thread.sleep(1000 ); } } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Method 2 end" ); } public static void main (String[] args) { final SynchronizedTest04 test = new SynchronizedTest04(); new Thread(new Runnable() { @Override public void run () { test.method1(); } }).start(); new Thread(new Runnable() { @Override public void run () { test.method2(); } }).start(); } }
执行结果如下,虽然线程1和线程2都进入了对应的方法开始执行,但是线程2在进入同步块之前,需要等待线程1中同步块执行完成。
1 2 3 4 5 6 Method 1 start Method 1 execute Method 2 start Method 1 end Method 2 execute Method 2 end
原理 反编译字节码分析synchronized
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class TestSynchronized { public synchronized static void test1 () { System.out.println("test1" ); } public synchronized void test2 () { System.out.println("test2" ); } public void test3 () { synchronized (this ) { System.out.println("test3" ); } } }
用 javap 命令反编译字节码TestSynchornize.class
代码块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public void test3 () ; descriptor: ()V flags: ACC_PUBLIC Code: stack=2 , locals=3 , args_size=1 0 : aload_0 1 : dup 2 : astore_1 3 : monitorenter 4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 7: ldc #6 // String test3 9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 12 : aload_1 13 : monitorexit 14 : goto 22 17 : astore_2 18 : aload_1 19 : monitorexit 20 : aload_2 21 : athrow 22 : return Exception table: from to target type 4 14 17 any 17 20 17 any LineNumberTable: line 19 : 0 line 20 : 4 line 21 : 12 line 22 : 22 StackMapTable: number_of_entries = 2 frame_type = 255 offset_delta = 17 locals = [ class TestSynchronized, class java/lang/Object ] stack = [ class java/lang/Throwable ] frame_type = 250 offset_delta = 4
monitorenter :
每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
monitorexit:
执行monitorexit的线程必须是objectref所对应的monitor的所有者。
指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
静态方法
1 2 3 4 5 6 7 8 9 10 11 12 public static synchronized void test1 () ; descriptor: ()V flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED Code: stack=2 , locals=0 , args_size=0 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String test1 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8 : return LineNumberTable: line 9 : 0 line 10 : 8
实例方法
1 2 3 4 5 6 7 8 9 10 11 12 public synchronized void test2 () ; descriptor: ()V flags: ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=2 , locals=1 , args_size=1 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #5 // String test2 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8 : return LineNumberTable: line 14 : 0 line 15 : 8
从反编译的结果来看,方法的同步并没有通过指令monitorenter和monitorexit来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。
运行结果解释 1、代码段2结果:
虽然method1和method2是不同的方法,但是这两个方法都进行了同步,并且是通过同一个对象去调用的,所以调用之前都需要先去竞争同一个对象上的锁(monitor),也就只能互斥的获取到锁,因此,method1和method2只能顺序的执行。
2、代码段3结果:
虽然test01和test02属于不同对象,但是test01和test02属于同一个类的不同实例,由于method1和method2都属于静态同步方法,所以调用的时候需要获取同一个类上monitor(每个类只对应一个class对象),所以也只能顺序的执行。
3、代码段4结果:
对于代码块的同步实质上需要获取Synchronized关键字后面括号中对象的monitor,由于这段代码中括号的内容都是this,而method1和method2又是通过同一的对象去调用的,所以进入同步块之前需要去竞争同一个对象上的锁,因此只能顺序执行同步块。
总结 Synchronized是Java并发编程中最常用的用于保证线程安全的方式,其使用相对也比较简单。但是如果能够深入了解其原理,对监视器锁等底层知识有所了解,一方面可以帮助我们正确的使用Synchronized关键字,另一方面也能够帮助我们更好的理解并发编程机制,有助我们在不同的情况下选择更优的并发策略来完成任务。对平时遇到的各种并发问题,也能够从容的应对。
参考