Java中的synchronied关键字
学习完本文,你将能回答如下问题:
(1) synchronied修饰静态方法和对象有什么不同?(碧桂园2面)
(2)说说对于 synchronized 关键字的了解?synchronied关键字可以修饰什么?(YY一面)
(3) JDK 1.6 时锁做了哪些优化(锁升级)?
synchronied关键字介绍
《Java并发编程之美》中介绍
synchronized块是Java提供的一种原子性内置锁,Java中的每个对象都可以把它当做一个同步锁来使用,这些Java内置的使用者看不到的锁被称为内置锁,也成为了监视器锁。线程的执行代码在进入synchronized代码块前会自动获取内部锁,这时候其他线程访问该同步代码块时会被阻塞挂起。拿到内部锁的线程会在正常退出同步代码块或者抛出异常后或者在同步块内调用了该内置锁资源的wait系列方法时释放该内置锁。内置锁是排它锁,也就是当一个线程获取这个锁后,其他线程必须等待该线程释放锁后才能获取资源。
另外,由于Java中的线程是与操作系统的原生线程一一对应的,所以当阻塞一个线程时,需要从用户态切换到内核态执行阻塞操作,这是很耗时的操作,而synchronized的使用就会导致上下文切换。
《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》
在Java里面,最基本的互斥同步手段就是synchronized关键字,这是一种块结构(Block Structured)的同步语法。synchronized关键字经过Javac编译之后,会在同步块的前后分别形成 monitorenter和monitorexit这两个字节码指令。这两个字节码指令都需要一个reference类型的参数来指明 要锁定和解锁的对象。如果Java源码中的synchronized明确指定了对象参数,那就以这个对象的引用作 为reference;如果没有明确指定,那将根据synchronized修饰的方法类型(如实例方法或类方法),来决定是取代码所在的对象实例还是取类型对应的Class对象来作为线程要持有的锁。
根据《Java虚拟机规范》的要求,在执行monitorenter指令时,首先要去尝试获取对象的锁。如果这个对象没被锁定,或者当前线程已经持有了那个对象的锁,就把锁的计数器的值增加一,而在执行 monitorexit指令时会将锁计数器的值减一。一旦计数器的值为零,锁随即就被释放了。如果获取对象锁失败,那当前线程就应当被阻塞等待,直到请求锁定的对象被持有它的线程释放为止。
从功能上看,根据以上《Java虚拟机规范》对monitorenter和monitorexit的行为描述,我们可以得出 两个关于synchronized的直接推论,这是使用它时需特别注意的:
(1)被synchronized修饰的同步块对同一条线程来说是可重入的。这意味着同一线程反复进入同步块也不会出现自己把自己锁死的情况。
(2)被synchronized修饰的同步块在持有锁的线程执行完毕并释放锁之前,会无条件地阻塞后面其他线程的进入。这意味着无法像处理某些数据库中的锁那样,强制已获取锁的线程释放锁;也无法强制正在等待锁的线程中断等待或超时退出。
synchronized的内存语义
等同于Synchronized怎么解决共享变量内存可见性问题?
进入synchronized块的内存语义是把在synchronized块内是使用到的变量从线程的工作内存中清除,这样在synchronized块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取。退出synchronized块的内存语义是把在synchronized块内对共享变量的修改刷新到主内存。
其实这也是加锁和释放锁的语义,当获取锁后会清空锁块内本地内存中讲会被用到的共享变量,在使用这些共享变量时从主内存进行加载,在释放锁时讲本地内存中修改的共享变量刷新到主内存。
实现原理
在 Java 中每个对象都隐式包含一个 monitor(监视器)对象,加锁的过程其实就是竞争 monitor 对象的过程,被synchronized修饰的代码块,在执行之前先使用monitorenter指令加锁,然后在执行结束之后再使用monitorexit指令释放锁资源,在整个执行期间此代码都是锁定的状态,这就是典型悲观锁的实现流程。
synchronized关键字的使用方式
Synchronized关键字主要有三种使用方式:修饰非静态(实例)方法、修饰静态(类方法)方法、修饰同步代码块。
修饰非静态(实例)方法
修饰非静态(实例)方法时,是取代码所在的对象实例作为线程要持有的锁。
通过使用Synchronized关键字,解决多个线程操作i++
线程不安全问题。
1 /** 2 * Synchronized关键字修饰非静态(实例)方法 3 * @author JustJavaIt 4 */ 5 public class SyncTest implements Runnable{ 6 7 /** 8 * 共享资源 9 */ 10 private static int i = 0; 11 12 /** 13 * 修饰非静态(实例)方法时,是取代码所在的对象实例作为线程要持有的锁。 14 */ 15 private synchronized void add() { 16 i++; 17 } 18 19 @Override 20 public void run() { 21 for (int j = 0; j < 10000; j++) { 22 add(); 23 } 24 } 25 26 public static void main(String[] args) throws Exception { 27 syncTest syncTest = new syncTest(); 28 Thread t1 = new Thread(syncTest); 29 Thread t2 = new Thread(syncTest); 30 t1.start(); 31 t2.start(); 32 t1.join(); 33 t2.join(); 34 System.out.println(i); 35 } 36 }
运行结果:
如果将上面实例代码main()的前三行进行修改后,结果会变成怎么样
运行结果:
add()
方法虽然也使用synchronized
关键字修饰了,但是因为两次new syncTest()
操作建立的是两个不同的对象,也就是说存在两个不同的对象锁,线程t1和t2使用的是不同的对象锁,所以不能保证线程安全。那这种情况应该如何解决呢?因为每次创建的实例对象都是不同的,而类对象却只有一个,如果synchronized关键字作用于类对象,即用synchronized
修饰静态方法,问题则迎刃而解。
修饰静态(类方法)方法
当synchronized作用于静态方法(synchronized修饰的方法类型是类方法),则是将class对象作为线程要持有的锁。只需要将上面示例的add()
方法前用static修饰即可,即当synchronized
作用于静态方法,锁就是当前的class对象。
1 /** 2 * 在add()方法前用static修饰即可,即当synchronized作用于静态方法,锁就是当前的class对象。 3 * @author JustJavaIt 4 */ 5 public class SyncTest3 implements Runnable { 6 /** 7 * 共享资源 8 */ 9 private static int i = 0; 10 11 private static synchronized void add() { 12 i++; 13 } 14 15 @Override 16 public void run() { 17 for (int j = 0; j < 10000; j++) { 18 add(); 19 } 20 } 21 22 public static void main(String[] args) throws Exception { 23 Thread t1 = new Thread(new syncTest3()); 24 Thread t2 = new Thread(new syncTest3()); 25 t1.start(); 26 t2.start(); 27 t1.join(); 28 t2.join(); 29 System.out.println(i); 30 } 31 }
运行结果:
修饰同步代码块
如果某些情况下,整个方法体比较大,需要同步的代码只是一小部分,如果直接对整个方法体进行同步,会使得代码性能变差,这时只需要对一小部分代码进行同步即可。
1 public class SyncTest4 implements Runnable { 2 /** 3 * 共享资源 4 */ 5 static int i = 0; 6 @Override 7 public void run() { 8 //其他操作....... 9 /** 10 * this表示当前对象实例,这里还可以使用syncTest4.class,表示class对象锁 11 */ 12 synchronized (this){ 13 for (int j = 0; j < 10000; j++) { 14 i++; 15 } 16 } 17 } 18 public static void main(String[] args) throws Exception { 19 SyncTest4 syncTest = new SyncTest4(); 20 Thread t1 = new Thread(syncTest); 21 Thread t2 = new Thread(syncTest); 22 t1.start(); 23 t2.start(); 24 t1.join(); 25 t2.join(); 26 System.out.println(i); 27 } 28 }
运行结果:
可以实现什么类型的锁?
(1)悲观锁:synchronized关键字实现的是悲观锁,每次访问共享资源时都会上锁。
(2)非公平锁:synchronized关键字实现的是非公平锁,即线程获取锁的顺序并不一定是按照线程申请锁的顺序。
(3)可重入锁:synchronized关键字实现的是可重入锁,即已经获取锁的线程,能在不释放这把锁的情况下,再次获取这把锁。
(4)独占锁(排他锁):synchronized关键字实现的是独占锁,即该锁只能被一个线程所持有,其他线程均被阻塞。
JDK 1.6 锁优化
详细介绍请查看我的另一篇博文———JDK1.6锁优化