Synchronized实现同步的基础
在JAVA中,synchronized的使用具体表现为三种形式:
- 修饰普通的实例方法,这时候的锁是当前实例对象;
- 修饰静态方法,这时候的锁是当前类的Class对象;
- 修饰方法块,这时候锁是synchronized参数传递的对象;
Synchronized实现同步的原理(简单描述)
Synchronized实现原理涉及到底层计算机硬件的支持,这里从JVM层面了解一下Synchrinized实现原理,因此我们看一下JVM规范中是怎么说的:
Java虚拟机中的同步Synchronized是用monitor的进入和退出实现的,无论是显式同步(有明确的monitorenter指令和monitorexit指令)还是隐式同步(依赖方法调用和返回指令实现)。
但是使用synchronized来同步方法和同步代码块的实现是不一样的,同步代码块使用的是monitorenter和monitorexit指令。而方法同步不是使用monitorenter和monitorexit指令,而是由方法调用指令读取运行时常量池中方法的ACC_SYNCHRONIZED标志来隐式实现的。
JVM要保证每个monitorenter指令都要有与之对应的monitorexit指令,不管方法是否正常结束,因此编译器会自动生成一个异常处理器用于处理所有的异常,并在其中执行monitorexit指令。
任何对象都有一个monitor与之对应,当一个monitor被持有后,对象即处于锁定状态,其他线程执行到monitorenter指令时,会尝试获取对象所对应的monitor所有权,也即尝试获得对象的锁。
synchronized同步方法的特点
- synchronized取得的锁都是对象锁;
- A线程持有对象的锁时,B线程可以异步调用同一个对象的非synchronized类型的方法;
- synchronized拥有锁重入的功能,A线程在获得对象锁后,可以再次请求该对象的锁,也就是说在synchronized方法/块内部请求同一个对象的其他synchronized方法/块时,永远可以得到锁。
- 在父子继承的环境中,synchronized同样具备锁的可重入性:在子类的synchronized方法中,可以调用父类的synchronized方法。
synchronized使用示例
使用synchronized修饰访问对象实例变量的方法,确保线程安全(synchronized基本用法):
package com.demo;public class TestSync { private int num = 0; synchronized public void setNumber(String threadName){ try { if(threadName.equals("A")){ num = 100; System.out.println("Thread A set number complete!"); Thread.sleep(1000); }else{ num = 200; System.out.println("Other thread set number complete!"); } System.out.println("Thread name: " + Thread.currentThread().getName() + ", num = " + num); }catch (InterruptedException e){ e.printStackTrace(); } } public static void main(String[] args) { TestSync testSync = new TestSync(); Thread threadA = new Thread(new Runnable() { @Override public void run() { testSync.setNumber("A"); } }); threadA.setName("A"); threadA.start(); Thread threadB = new Thread(new Runnable() { @Override public void run() { testSync.setNumber("B"); } }); threadB.setName("B"); threadB.start(); }}
上述代码中的setNumber方法会修改实例中的num变量,如果是线程A调用的该方法,则将num值置为100,过1秒后再输出设置的num值。如果不是A线程执行该方法,则置num为200.接下来的代码中开启了两个线程,名字分别设置为A和B,运行结果如下:
Thread A set number complete!
Thread name: A, num = 100
Other thread set number complete!
Thread name: B, num = 200
Process finished with exit code 0
上面的代码,setNumber方法如果省去了synchronized关键字,变为一个非同步方法,就会因为两个线程前后修改实例变量导致A线程修改的值100被覆盖,输出的结果如下:
Thread A set number complete!
Other thread set number complete!
Thread name: B, num = 200
Thread name: A, num = 200
从打印的顺序也可以看出来,非同步方法,多个线程可以任意进入方法中,线程间是交叉乱序执行的。
接下来验证“A线程持有对象的锁时,B线程可以异步调用同一个对象的非synchronized类型的方法;”这个结论。将上面的代码稍作修改,增加一个非同步的方法notSynMethod,该方法将num改为666然后打印当前线程名字和修改后的num值。
package com.demo;public class TestSync { private int num = 0; synchronized public void setNumber(String threadName){ try { if(threadName.equals("A")){ num = 100; System.out.println("Thread A set number complete!"); Thread.sleep(1000); }else{ num = 200; System.out.println("Other thread set number complete!"); } System.out.println("Thread name: " + Thread.currentThread().getName() + ", num = " + num); }catch (InterruptedException e){ e.printStackTrace(); } } public void notSynMethod(){ num = 666; System.out.println("Thread name: " + Thread.currentThread().getName() + " has set num to " + num); } public static void main(String[] args) { TestSync testSync = new TestSync(); Thread threadA = new Thread(new Runnable() { @Override public void run() { testSync.setNumber("A"); } }); threadA.setName("A"); threadA.start(); Thread threadB = new Thread(new Runnable() { @Override public void run() { testSync.notSynMethod(); } }); threadB.setName("B"); threadB.start(); }}
输出结果:
Thread A set number complete!
Thread name: B has set num to 666
Thread name: A, num = 666
Process finished with exit code 0
从输出结果可以看出,线程A在将num设置为100后开始sleep1秒钟(并未释放testSync对象锁),这期间B线程启动并修改num值为666,结果A线程sleep结束并打印出来num的值是修改后的666。这说明“A线程持有testSync对象的锁时,B线程可以异步调用同一个对象的非synchronized类型的notSynMethod方法;”
最后验证synchronized对象锁的可重入性。再次回忆一下,“可重入”指的是线程在得到对象锁之后,再次请求该对象锁是可以再次得到该对象的锁的。因此测试代码构造两个同步方法,在第一个同步方法synMethod1中调用第二个同步方法synMethod2,如果能成功调用,说明对象锁是可重入的。
package com.demo;public class TestSync { synchronized void synMethod1() throws Exception{ System.out.println("in synMethod1..."); this.synMethod2(); Thread.sleep(500); System.out.println("synMethod1 completed!"); } synchronized void synMethod2(){ System.out.println("in synMethod2..."); } public static void main(String[] args) { TestSync testSync = new TestSync(); Thread thread = new Thread(new Runnable() { @Override public void run() { try { testSync.synMethod1(); } catch (Exception e) { e.printStackTrace(); } } }); thread.start(); }}
程序输出:
in synMethod1...
in synMethod2...
synMethod1 completed!
Process finished with exit code 0
分清线程执行不同的synchronized同步方法时所持有的锁
synchronized同步方法是对当前对象加锁,而synchronized代码块是对某个对象加锁,这个对象称为“对象监视器”。在使用synchronized关键字时,时刻都要分辨清楚被同步的方法/代码块对应的“对象监视器”是什么,如果使用了错误的对象监视器,会得到与预期不一致的结果。
在文章开始就说过了synchronized对于修饰实例方法、静态方法、代码块时使用的是不同的锁,前面的例子中已经验证了,线程进入synchronized修饰的实例方法时,得到的是当前对象锁。接下来验证对于静态方法,对应的锁是当前Java Class类。
package com.demo;public class TestSync { synchronized static void staticMethodA(){ System.out.println(Thread.currentThread().getName() + " enter staticMethodA()..."); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " exit staticMethodA()..."); } synchronized static void staticMethodB(){ System.out.println(Thread.currentThread().getName() + " in staticMethodB()..."); } synchronized void synMethod(){ System.out.println(Thread.currentThread().getName() + " in synMethod()"); } public static void main(String[] args) { TestSync testSync = new TestSync(); Thread threadA = new Thread(new Runnable() { @Override public void run(){ TestSync.staticMethodA(); } }); threadA.setName("A"); Thread threadB = new Thread(new Runnable() { @Override public void run() { TestSync.staticMethodB(); } }); threadB.setName("B"); Thread threadC = new Thread(new Runnable() { @Override public void run() { testSync.synMethod(); } }); threadC.setName("C"); threadA.start(); threadB.start(); threadC.start(); }}
输出结果:
A enter staticMethodA()...
C in synMethod()
A exit staticMethodA()...
B in staticMethodB()...
Process finished with exit code 0
上面的代码中,TestSync类有两个静态方法,一个实例方法,当开启三个线程分别执行三个方法时,可以看到线程A进入了静态方法staticMethodA()尚未退出之前,线程B是无法进入静态方法staticMethodB()的,但是线程C可以执行synchronized方法synMethod()。这说明线程A所持有的锁和线程B请求的锁是同一个,都是当前类。线程C可以无阻碍的进入方法synMethod(),说明线程C请求的是testSync对象的锁,它没有被其他线程持有,因此可以进入该方法。
总结成一句话就是:“多个线程在访问某个方法前,如果请求的是同一把锁,那么这些线程必须排队等待(同步执行),否则线程之间就是异步执行”。
String类型的常量池特性
如果使用String类型的对象作为对象监视器时要注意,下面这样的代码,变量a和b引用的是同一个对象:
String a = "111";String b = "111";
如果将这两个对象作为监视器同步代码块时,A线程获取并持有a对象锁时,B线程是无法b对象的锁的,因为a和b指向的是同一个对象。下面举例验证一下,顺便演示synchronized代码块的语法:
package com.demo;public class TestSync { private String obj; TestSync(String lockObject){ this.obj = lockObject; } public void synBlock(){ synchronized (obj){ System.out.println(Thread.currentThread().getName() + " Holding lock: "+ obj +" enter synBlock()..."); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " Release lock: "+ obj +" leaving synBlock()..."); } } public static void main(String[] args) { String a = "111"; String b = "111"; TestSync testSyncA = new TestSync(a); TestSync testSyncB = new TestSync(b); Thread threadA = new Thread(new Runnable() { @Override public void run() { testSyncA.synBlock(); } }); threadA.setName("Thread A"); Thread threadB = new Thread(new Runnable() { @Override public void run() { testSyncB.synBlock(); } }); threadB.setName("Thread B"); threadA.start(); threadB.start(); }}
执行结果:
Thread A Holding lock: 111 enter synBlock()...
Thread A Release lock: 111 leaving synBlock()...
Thread B Holding lock: 111 enter synBlock()...
Thread B Release lock: 111 leaving synBlock()...
Process finished with exit code 0
由于a和b引用的是同一个字符串对象,导致原本不希望同步的两个线程被同步执行了。
关于synchronzed关键字的用法,暂时就介绍这么多了,其他用法(例如内部类中使用synchronized方法)大同小异,可以自行分析。
这里是NPE,共同学习,向JAVA架构师迈进,欢迎关注、评论和转发,谢谢!