前言:
只要涉及到Java并发那么我们就会考虑线程安全,实际上能够实现线程安全的方法很多,今天先介绍一下synchronized关键字,主要从使用,原理介绍
一、synchronized的使用方法
1、修饰代码块:大括号括起来的代码,作用于调用的对象
2、修饰方法:整个方法,作用于调用的对象
下面来演示代码,并且理解这两个使用的方法
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class SynchronizedExample { //使用synchronized修饰代码块 public void test1(int j) { synchronized(this){ for(int i=0;i<10;i++) { System.out.println("test->"+j+"->"+i); } } } //使用synchronized修饰方法 public synchronized void test2(int j) { for(int i=0;i<10;i++) { System.out.println("test->"+j+"->"+i); } } public static void main(String args[]) { SynchronizedExample example1=new SynchronizedExample(); SynchronizedExample example2=new SynchronizedExample(); ExecutorService e=Executors.newCachedThreadPool(); e.execute(()->{ example1.test1(1); }); e.execute(()->{ example2.test1(2); }); } }
上面的代码演示了如何修饰代码块和方法。这里主要说“作用于调用的对象”如何理解。上面两个线程分别执行两个对象中的test1方法,结果是两个线程会结合在一起执行,这是因为调用的对象不同,一个是example1对象,另一个是example2对象。如果同时调用example1对象中的test1和test2方法,那么会先执行test1再执行test2。就是这时候只在调用的对象相同的时候才会实现同步操作。结果如下图
3、修饰静态方法:整个静态方法,作用于所有对象
4、修饰类:括号起来的部分,作用于所有对象
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class SynchronizedExample { //使用synchronized修饰类 public void test1(int j) { synchronized(SynchronizeExample.class){ for(int i=0;i<10;i++) { System.out.println("test->"+j+"->"+i); } } } //使用synchronized修饰静态方法 public static synchronized void test2(int j) { for(int i=0;i<10;i++) { System.out.println("test->"+j+"->"+i); } } public static void main(String args[]) { SynchronizedExample example1=new SynchronizedExample(); SynchronizedExample example2=new SynchronizedExample(); ExecutorService e=Executors.newCachedThreadPool(); e.execute(()->{ example1.test1(1); }); e.execute(()->{ example2.test1(2); }); } }
当理解了上面两条,下面的结果就很显然了。这里有点不同就是使用synchronized修饰类和修饰方法块时,后面的一个为当前类,一个为this,this可以理解为当前类的实例化对象,所以只对当前调用的对象有加锁。而锁一个类则是锁了当前类的所有实例化对象。即作用范围不一样。
二、synchronized原理
上面我们只是介绍了一下synchronized关键字的使用方法。接下来就是底层实现原理的介绍。
1、执行synchronized代码块的三个步骤:
(1)、线程尝试获得锁,如果获得就进入第二步,否则就加入等待队列,阻塞并等待唤醒
(2)、执行synchronized修饰部分的代码块
(3)、线程释放锁。如果等待队列上有等待的线程,从中取一个并唤醒,但是不一定是按顺序的,也就是不保证公平性
2、JVM中的synchronized底层实现
JVM是基于进入和退出monitor对象来实现方法同步和代码块同步。对于代码块同步:任何对象都有一个monitor与之关联,当一个monitor被持有后就会处于锁定状态,当线程执行到monitorenter指令时,就会尝试获得对象的monitor所有权。而反映到上面步骤即尝试获得锁。而后执行代码,当遇到monitorexit指令后就会释放锁。所以monitorexit指令一般放在方法结束处和异常处。上述这些指令都是JVM中编译后自动放置的。平常写代码的时候并不需要。
3、为什么synchronized作用的是对象
在上面的使用方法中基本上都说了synchronized作用于对象,无论是调用的对象还是所有对象。为什么是作用于对象而不是代码块呢?是因为synchronized用的锁是存在Java对象头里的
synchronized代表重量级锁(其实JDK有对synchronized进行各种优化,所以性能上有所提升。并不是说重量级锁就不使用了。)所以synchronized锁的是对象而不是代码块。
4、synchronized的可重入性
synchronized是可重入的。可重入是通过记录锁的持有线程和持有数量来实现。当被调用synchronized保护的代码时,检查对象是否被锁,如果是再检查当前线程锁定,如果还是,那么增加持有数量。也就是上面例子中当一个线程拿到example1的锁之后,在没有释放锁的时候再次请求example1的锁是可以一定可以拿到的。这时候在JVM中关联的计数器就会加一,就相当于该线程持有锁的数量加一。而后每次执行一次monitorexit指令就会数量减一。当计数器为0的时候才会使得该线程释放对象锁(这里一定要弄清是对象而不是代码块,线程持有的是一个对象)。
5、synchronized的内存可见性
synchronized是保证内存可见性的。在释放锁的时候,所有写入都会写回内存,获得锁后,就会从内存中读最新数据。但是若只为了保证内存可见性,简易使用volatile这个更加轻量级的锁。
四、总结
其实重点强调的就是synchronized虽然是包裹着一个代码块,但是作用的一定是一个对象。这里并不是说线程拿到这个对象,这个对象就会被锁住,里面的其他代码都不可执行。这里意思是两个或多个线程尝试获得synchronized修饰的同一个代码块或方法,才会引起锁竞争。但是由于有时候作用的只是当前的调用对象,所以其他对象访问的时候也会也不会引起锁竞争,具体看使用方法的第一个例子。更重要还是去理解JVM中加入的monitorenter指令和monitorexit指令,会对理解synchronized有非常重要的帮助。同时说一下,当子类继承父类的时候,没有重写父类的synchronized方法,那么使用的时候是直接使用父类的方法,所以会同步。但是如果重写了方法却没有加上synchronized修饰。那么就不会有同步操作。所以子类是不会继承父类的synchronized锁。