java并发编程这个领域中synchronized关键字一直都是元老级的角色,在java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的,java的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。在JDK1.6之后java官方对从JVM层面对synchronized 较大优化,所以现在的synchronized锁效率也优化得很不错。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
通过反编译下面的代码来看看Synchronized是如何实现对代码块进行同步的,切换到类的对应目录执行javac SynchronizedTest.java命令生成编译后的.class文件,然后执行javap -v SynchronizedTest.class。
1)synchronized同步代码块
public class SynchronizedTest { public static void main(String[] args) { new SynchronizedTest().method(); } public void method() { synchronized (this) { System.out.println("synchronized 代码块"); } } }
从上面我们可以看出:synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权.当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
2)synchronized修饰方法
public class SynchronizedTest { public static void main(String[] args) { new SynchronizedTest().method(); } public synchronized void method() { System.out.println("Hello World!"); } }
synchronized 修饰的方法的同步并没有 monitorenter 指令和 monitorexit 指令完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。
小结:对于同步块的实现使用了monitorenter和monitorexit指令,而同步方法则是依靠方法修饰符上的ACC_SYNCHRONIZED来完成的。无论采用哪种方法,其本质是对一个对象的监视器(monitor)进行获取,而这个获取过程是排他的,也就是同一个时刻只能有一个线程获取到由synchronized所保护对象的监视器。
synchronized实现同步的基础是:Java中每个对象都可以作为锁,具体表现为以下三种形式:
1.对于普通同步方法,锁是当前实例对象;
2.对于静态同步方法,锁是当前类的class对象;
3.对于同步方法块,锁是synchronized括号里配置的对象
1)多个线程访问的是多个对象
public class HasSelfPrivateNum { private int num = 0; public static void main(String[] args) { HasSelfPrivateNum numRef1 = new HasSelfPrivateNum(); HasSelfPrivateNum numRef2 = new HasSelfPrivateNum(); ThreadTestA thread1 = new ThreadTestA(numRef1); thread1.start(); ThreadTestB thread2 = new ThreadTestB(numRef2); thread2.start(); } synchronized public void add(String username) { try { if (username.equals("a")) { num = 100; System.out.println("a set over!"); Thread.sleep(2000); } else { num = 200; System.out.println("b set over!"); } System.out.println(username + " num=" + num); } catch (InterruptedException e) { e.printStackTrace(); } } static public class ThreadTestA extends Thread { private HasSelfPrivateNum numRef; public ThreadTestA(HasSelfPrivateNum numRef) { this.numRef = numRef; } @Override public void run() { numRef.add("a"); } } static public class ThreadTestB extends Thread { private HasSelfPrivateNum numRef; public ThreadTestB(HasSelfPrivateNum numRef) { this.numRef = numRef; } @Override public void run() { numRef.add("b"); } } }
两个线程ThreadTestA和ThreadTestB分别访问同一个类的不同实例的相同名称的同步方法,但是效果确实异步执行,因为synchronized取得的锁都是对象锁,而不是把一段代码或方法当做锁。所以在上面的实例中,哪个线程先执行带synchronized关键字的方法,则哪个线程就持有该方法所属对象的锁Lock,那么其他线程只能呈等待状态。当前创建了两个HasSelfPrivateNum类对象,所以就产生了两个锁。当ThreadTestA的引用执行到add方法run中的Thread.sleep(2000)语句时,ThreadB就会“乘机执行”。
2)多个线程访问的是同一个对象
public static void main(String[] args) { HasSelfPrivateNum numRef = new HasSelfPrivateNum(); ThreadTestA thread1 = new ThreadTestA(numRef); thread1.start(); ThreadTestB thread2 = new ThreadTestB(numRef); thread2.start(); }
多个线程访问的是同一个对象,哪个线程先执行带synchronized关键字的方法,则哪个线程就持有该方法,那么其他线程只能呈等待状态。如果多个线程访问的是多个对象则不一定,因为多个对象会产生多个锁。
3)脏读
发生脏读的情况实在读取实例变量时,此值已经被其他线程更改过。
public class PublicVar { public String username = "A"; public String password = "AA"; synchronized public void setValue(String username, String password) { try { this.username = username; Thread.sleep(3000); this.password = password; System.out.println("setValue method thread name=" + Thread.currentThread().getName() + " username=" + username + " password=" + password); } catch (InterruptedException e) { e.printStackTrace(); } } //该方法前加上synchronized关键字就同步了 public void getValue() { System.out.println("getValue method thread name=" + Thread.currentThread().getName() + " username=" + username + " password=" + password); } public static void main(String[] args) { try { PublicVar publicVarRef = new PublicVar(); ThreadC thread = new ThreadC(publicVarRef); thread.start(); Thread.sleep(500);//打印结果受此值大小影响 publicVarRef.getValue(); } catch (InterruptedException e) { e.printStackTrace(); } } static class ThreadC extends Thread { private PublicVar publicVar; public ThreadC(PublicVar publicVar) { this.publicVar = publicVar; } @Override public void run() { publicVar.setValue("B", "BB"); } } }