Synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。Synchronized的作用主要有三个:(1)确保线程互斥的访问同步代码(2)保证共享变量的修改能够及时可见(3)有效解决重排序问题。从语法上讲,Synchronized总共有三种用法:
(1)修饰普通方法
(2)修饰静态方法
(3)修饰代码块
首先来看一下没有使用同步的情况
public class SynchronizedTest { public void method1(){ System.out.println("method1 start"); try{ System.out.println("method1 excute"); Thread.sleep(3000); }catch (Exception e){ e.printStackTrace(); } System.out.println("method1 end"); } public void method2(){ System.out.println("method2 start"); try{ System.out.println("method2 excute"); Thread.sleep(1000); }catch (Exception e){ e.printStackTrace(); } System.out.println("method2 end"); } public static void main(String[] args){ final SynchronizedTest test=new SynchronizedTest(); 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是同时执行的。)
method1 start
method1 excute
method2 start
method2 excute
method2 end
method1 end
使用synchronized修饰普通方法,其他保持不变,输出结果为
method1 start
method1 excute
method1 end
method2 start
method2 excute
method2 end
可以很明显的看出,线程2需要等待线程1的method1执行完成才能开始执行method2方法。为什么? 因为使用synchronized关键字修饰方法,则锁为this,即调用method的类的实例test。
如果在main方法中增加一个
final SynchronizedTest test1=new SynchronizedTest();
并且使用test1调用method2,则输出结果如下:
method1 start
method1 excute
method2 start
method2 excute
method2 end
method1 end
为什么? 因为虽然使用了关键字synchronized,但是调用synchronized的对象不一致,也就是synchronized锁的不是同一个对象,所以同步并未生效。
使用synchronize修饰static方法(即将方法修改为static synchronized,其他不变),输出结果为
method1 start
method1 excute
method1 end
method2 start
method2 excute
method2 end
可以看出为顺序执行
如果在main方法中增加一个
final SynchronizedTest test1=new SynchronizedTest();
并且使用test1调用method2,则输出结果也是一致的,为什么呢?
对静态方法的同步本质上是对类的同步(静态方法本质上是属于类的方法,而不是对象上的方法),所以即使test和test2属于不同的对象,但是它们都属于SynchronizedTest类的实例,所以也只能顺序的执行method1和method2,不能并发执行。
使用synchronized修饰代码块
public class SynchronizedTest { public void method1(){ System.out.println("method1 start"); try{ synchronized(this){ System.out.println("method1 excute"); Thread.sleep(3000); } }catch (Exception e){ e.printStackTrace(); } System.out.println("method1 end"); } public void method2(){ System.out.println("method2 start"); try{ synchronized(this){ System.out.println("method2 excute"); Thread.sleep(1000); } }catch (Exception e){ e.printStackTrace(); } System.out.println("method2 end"); } public static void main(String[] args){ final SynchronizedTest test=new SynchronizedTest(); // final SynchronizedTest test1=new SynchronizedTest(); new Thread(new Runnable() { @Override public void run() { test.method1(); } }).start(); new Thread(new Runnable() { @Override public void run() { test.method2(); } }).start(); } }
输出结果为:
method1 start
method1 excute
method2 start
method1 end
method2 excute
method2 end
可以看出结果为非顺序执行,为什么? 其实是顺序的,看后面原理就明白了, 只不过同步关键字加在代码库里面,method2的start 打印会先输出,后面同步竞争时还是顺序的。
这就涉及到synchronized关键字的原理了
先看synchronized代码块,首先对代码进行javac编程,再进行javap -c 反编译得出结果如下:
关于这两条指令的作用,我们直接参考JVM规范中描述:
monitorenter :
Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
• If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
• If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
• If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.
这段话的大概意思为:
每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
monitorexit:
The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.
这段话的大概意思为:
执行monitorexit的线程必须是objectref所对应的monitor的所有者。
指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
再看下synchronized同步方法:
synchronized 方法控制对类成员变量的访问:每个类实例对应一把锁,每个 synchronized 方法都必须获得调用该方法的类实例的锁方能
执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行
状态。这种机制确保了同一时刻对于每一个类实例,其所有声明为 synchronized 的成员函数中至多只有一个处于可执行状态(因为至多只有
一个能够获得该类实例对应的锁),从而有效避免了类成员变量的访问冲突(只要所有可能访问类成员变量的方法均被声明为 synchronized)
。
在 Java 中,不光是类实例,每一个类也对应一把锁,这样我们也可将类的静态成员函数声明为 synchronized ,以控制其对类的静态成
员变量的访问。
synchronized方法实际上等同于用一个synchronized块包住方法中的所有语句,然后在synchronized块的括号中传入this关键字。当然如果是静态方法,需要锁定的则是class对象。
可能一个方法中只有几行代码涉及到线程同步的问题,所以synchronized块比synchronized方法更近细粒度的控制了多个线程的访问,只有synchronized块中的内容不能同时被多个线程访问,方法中的其他语句仍然可以同时被多个线程所访问(包括synchronized块之前和之后的)。
运行结果解释
有了对Synchronized原理的认识,再来看上面的程序就可以迎刃而解了。
1、同步普通方法结果:
虽然method1和method2是不同的方法,但是这两个方法都进行了同步,并且是通过同一个对象去调用的,所以调用之前都需要先去竞争同一个对象上的锁(monitor),也就只能互斥的获取到锁,因此,method1和method2只能顺序的执行。
2、同步静态方法结果:
虽然test和test2属于不同对象,但是test和test2属于同一个类的不同实例,由于method1和method2都属于静态同步方法,所以调用的时候需要获取同一个类上monitor(每个类只对应一个class对象),所以也只能顺序的执行。
3、代码块同步结果:
对于代码块的同步实质上需要获取Synchronized关键字后面括号中对象的monitor,由于这段代码中括号的内容都是this,而method1和method2又是通过同一的对象去调用的,所以进入同步块之前需要去竞争同一个对象上的锁,因此只能顺序执行同步块。