在多线程并发编程中synchronized一直是元老级角色,很多人都会称呼它为重量级锁。但是,随着Java SE 1.6对 synchronized进行了各种优化之后,有些情况下它就并不那么重了,Java SE 1.6中为了减少获得锁和释放锁带来的 性能消耗而引入的偏向锁和轻量级锁,以及锁的存储结构和升级过程。我们仍然沿用前面使用的案例,然后通过 synchronized关键字来修饰在inc的方法上。再看看执行结果。
public class SynchronizedDemo2 {
private static int count = 0;
public static void inc() {
synchronized (SynchronizedDemo2.class) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
new Thread(() -> SynchronizedDemo2.inc()).start();
}
Thread.sleep(3000);
System.out.println("运行结果" + count);
}
}
字节码
//这里只贴出了方法inc那部分
public static void inc();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=0
0: ldc #2 // class com/gh/test/concurrent/synchronizedDemo/SynchronizedDemo2
2: dup
3: astore_0
4: monitorenter //这里添加了monitorenter锁指令
5: lconst_1
6: invokestatic #3 // Method java/lang/Thread.sleep:(J)V
9: goto 17
12: astore_1
13: aload_1
14: invokevirtual #5 // Method java/lang/InterruptedException.printStackTrace:()V
17: getstatic #6 // Field count:I
20: iconst_1
21: iadd
22: putstatic #6 // Field count:I
25: aload_0
26: monitorexit //释放锁
27: goto 35 //方法inc 正常结束跳到35
30: astore_2
31: aload_0
32: monitorexit //释放锁
33: aload_2 //从这里开始是对没有catch的异常的处理
34: athrow //抛出异常
35: return
synchronized的应用方式
上面示例为修饰代码块,下面我们来看下修饰方法时有何区别
public synchronized static void inc1() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
//===============================字节码=====================================================
public static synchronized void inc1();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=0
0: lconst_1
1: invokestatic #3 // Method java/lang/Thread.sleep:(J)V
4: goto 12
7: astore_0
8: aload_0
9: invokevirtual #5 // Method java/lang/InterruptedException.printStackTrace:()V
12: getstatic #6 // Field count:I
15: iconst_1
16: iadd
17: putstatic #6 // Field count:I
20: return
通过查看字节码可以发现:
synchronized 修饰的同步代码块,jvm采用的是 monitorenter ,monitorexit 指令。
synchronized 修饰的同步方法,jvm采用的是 ACC_SYNCHRONIZED 标记符。
同步方法,官方介绍
Locks In Synchronized Methods
When a thread invokes a synchronized method, it automatically acquires the intrinsic lock for that method's object and releases it when the method returns. The lock release occurs even if the return was caused by an uncaught exception.
You might wonder what happens when a static synchronized method is invoked, since a static method is associated with a class, not an object. In this case, the thread acquires the intrinsic lock for the Class object associated with the class. Thus access to class's static fields is controlled by a lock that's distinct from the lock for any instance of the class.
谷歌翻译如下:
当线程调用synchronized方法时,它会自动获取该方法对象的内部锁,并在方法返回时释放它。即使返回是由未捕获的异常引起的,也会发生锁定释放。
您可能想知道在调用静态同步方法时会发生什么,因为静态方法与类相关联,而不是与对象相关联。在这种情况下,线程获取Class与类关联的对象的内部锁。因此,对类的静态字段的访问由与该类的任何实例的锁不同的锁控制。
可重入同步
回想一下,线程无法获取另一个线程拥有的锁。但是,一个线程可以获取它已经拥有的锁。允许线程多次获取相同的锁可启用重入同步。这描述了一种情况,其中同步代码直接或间接地调用也包含同步代码的方法,并且两组代码使用相同的锁。在没有可重入同步的情况下,同步代码必须采取许多额外的预防措施,以避免线程导致自身阻塞。
synchronized括号后面的对象
synchronized后面的对象是一把锁,在java中任意一个对象都可以成为锁,简单来说,我们把object比喻是一 个key,拥有这个key的线程才能执行这个方法,拿到这个key以后在执行方法过程中,这个key是随身携带的,并且 只有一把。如果后续的线程想访问当前方法,因为没有key所以不能访问只能在门口等着,等之前的线程把key放回 去。所以,synchronized锁定的对象必须是同一个,如果是不同对象,就意味着是不同的房间的钥匙,对于访问者 来说是没有任何影响的 synchronized的字节码指令
对于同步块的实现使用了monitorenter和monitorexit指令,他们隐式的执行了Lock和UnLock操作,用于提供原子性保证。 monitorenter指令插入到同步代码块开始的位置、monitorexit指令插入到同步代码块结束位置,jvm需要保证每 个monitorenter都有一个monitorexit对应。 这两个指令,本质上都是对一个对象的监视器(monitor)进行获取,这个过程是排他的,也就是说同一时刻只能有 一个线程获取到由synchronized所保护对象的监视器,线程执行到monitorenter指令时,会尝试获取对象所对应的monitor所有权,也就是尝试获取对象的锁;而执行 monitorexit(或抛出异常)就是释放monitor的所有权 。
总结:
同步方法通过ACC_SYNCHRONIZED
关键字隐式的对方法进行加锁。当线程要执行的方法被标注上ACC_SYNCHRONIZED
时,需要先获得锁才能执行该方法。
同步代码块通过monitorenter
和monitorexit
执行来进行加锁。当线程执行到monitorenter
的时候要先获得所锁,才能执行后面的方法。当线程执行到monitorexit
的时候则要释放锁。
每个对象自身维护这一个被加锁次数的计数器,当计数器数字为0时表示可以被任意线程获得锁。当计数器不为0时,只有获得锁的线程才能再次获得锁。即可重入锁。
相关文章:
Synchronized的实现原理(一)