赞
踩
目录
synchronized是Java中的关键字,synchronized可以保证方法或者代码块在运行时,同一时刻只有一个线程可以进入到被修饰的代码块中,同时它还可以保证共享变量的内存可见性,Java中每一个对象都可以作为锁,这是synchronized实现同步的基础。
它修饰的对象有以下几种:
注意:对象锁是可以有很多把的但是类锁只有一把,因为对象可以创建多个但是类的字节码文件只有一份。
synchronized
关键字可以保证只有一个线程拿到锁,访问共享资源。synchronized
时,会对应执行 lock
、unlock
原子操作,保证可见性。- static final Object lock = new Object();
- static int counter = 0;
- public static void main(String[] args) {
- synchronized (lock) {
- counter++;
- }
- }
- public static void main(java.lang.String[]);
- descriptor: ([Ljava/lang/String;)V
- flags: ACC_PUBLIC, ACC_STATIC
- Code:
- stack=2, locals=3, args_size=1
- 0: getstatic #2 // <- lock引用 (synchronized开始)
- 3: dup
- 4: astore_1 // lock引用 -> slot 1
- 5: monitorenter // 将 lock对象 MarkWord 置为 Monitor 指针
- 6: getstatic #3 // <- i
- 9: iconst_1 // 准备常数 1
- 10: iadd // +1
- 11: putstatic #3 // -> i
- 14: aload_1 // <- lock引用
- 15: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList
- 16: goto 24
- 19: astore_2 // e -> slot 2
- 20: aload_1 // <- lock引用
- 21: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList
- 22: aload_2 // <- slot 2 (e)
- 23: athrow // throw e
- 24: return
- Exception table:
- from to target type
- 6 16 19 any
- 19 22 19 any
- LineNumberTable:
- line 8: 0
- line 9: 6
- line 10: 14
- line 11: 24
- LocalVariableTable:
- Start Length Slot Name Signature
- 0 25 0 args [Ljava/lang/String;
- StackMapTable: number_of_entries = 2
- frame_type = 255 /* full_frame */
- offset_delta = 19
- locals = [ class "[Ljava/lang/String;", class java/lang/Object ]
- stack = [ class java/lang/Throwable ]
- frame_type = 250 /* chop */
- offset_delta = 4

从反编译的字节码文件中我们可以看到加锁时使用了monitorenter和monitorexit。线程在执行monitorenter指令的时候,会对Monitor进行加锁,加锁后其他线程无法获得锁,除非线程主动解锁。即使在执行过程中,由于某种原因,比如CPU时间片用完,线程放弃了CPU,但是它并没有进行解锁。而由于synchronized的锁是可重入的,下一个时间片还是只能被他自己获取到,还是会继续执行代码。直到所有代码执行完,这就保证了原子性。
synchronized
在修改了本地内存中的变量后,解锁前会将本地内存修改的内容刷新到主内存中,确保了共享变量的值是最新的,也就保证了可见性。
在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序可以提高程序执行的性能,但是代码的执行顺序改变,可能会导致多线程程序出现可见性问题和有序性问题。
但是synchronized提供了有序性保证,这其实和as-if-serial语义有关。as-if-serial语义是指不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果都不能被改变。编译器和处理器无论如何优化,都必须遵守as-if-serial语义。只要编译器和处理器都遵守了这个语义,那么就可以认为单线程程序是按照顺序执行的,由于synchronized修饰的代码,同一时间只能被同一线程访问。那么可以认为是单线程执行的。所以可以保证其有序性。
1)以32位的虚拟机为例,Java中一个普通对象的对象头包括MarkWord和KlassWord两部分。synchronized锁对象时就是修改MarkWord中的信息来实现把对象锁住。
Mark Word中存储的数据会随着锁标志位的变化而变化,在32位虚拟机中,不同状态下的组成如下
Normal:无锁状态(01),此时的MarkWord存储的是对象的hash值、分代年龄、偏向锁的状态和当前锁状态。
Biased:偏向锁(01),此时的MarkWord存储的是对象的偏向线程ID、重偏向计数器、分代年龄、偏向锁的状态和当前锁状态。适用于多线程访问对象但不是同时访问的情况。
Lightweight Locked:轻量级锁(00),当多个线程同时竞争同一个对象,此时就不适合用偏向锁了,锁会升级为轻量级锁。在多线程交替执行同步代码块时(轻度竞争),避免使用重量级锁带来的性能消耗。
Heavyweight Locked:重量级锁(10)
引入偏向锁的目的:减少只有一个线程执行同步代码块时的性能消耗,即在没有其他线程竞争的情况下,一个线程获得了锁。从偏向锁的MarkWord结构中可以看出,如果对象执行了hashCode()方法,则此对象无法进入偏向锁状态。因为MarkWord中没有地方存储ThreadID
偏向锁的获取流程:
批量重偏向:如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID;当撤销偏向锁达到阈值 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至t2。因为前19次是轻量,释放之后为无锁不可偏向,但是20次后面的是偏向t2,释放之后依然是偏向t2。
批量撤销:当一个偏向锁如果撤销次数到达40的时候就认为这个对象设计的有问题;那么JVM会把这个对象所对应的类所有的对象都撤销偏向锁;并且新实例化的对象也是不可偏向的。
引入轻量级锁的目的:在多线程交替执行同步代码块时(轻度竞争),避免使用重量级带来的性能消耗。但多个线程同时进入临界区(发生竞争)则会使得轻量级锁膨胀为重量级锁。
轻量级锁的获取流程:
Java锁的几种状态并不包括自旋锁,当轻量级锁的竞争就是采用的自旋锁机制。
什么是自旋锁:当线程A已经获得锁时,线程B再来竞争锁,线程B不会直接被阻塞,而是在原地循环 等待,当线程A释放锁后,线程B可以马上获得锁。
引入自旋锁的原因:因为阻塞和唤起线程都会引起操作系统用户态和核心态的转变,对系统性能影响较大,而自旋等待可以避免线程切换的开销。
自旋锁的缺点:自旋等待虽然可以避免线程切花的开销,但它也会占用处理器的时间。如果持有锁的线程在较短的时间内释放了锁,自旋锁的效果就比较好,如果持有锁的线程很长时间都不释放锁,自旋的线程就会白白浪费资源,所以一般线程自旋的次数必须有一个限制,该次数可以通过参数-XX:PreBlockSpin调整,一般默认为10。
自适应自旋锁:JDK1.6引入了自适应自旋锁,自适应自旋锁的自旋次数不在固定,而是由上一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的。如果对于某个锁对象,刚刚有线程自旋等待成功获取到锁,那么虚拟机将认为这次自旋等待的成功率也很高,会允许线程自旋等待的时间更长一些。如果对于某个锁对象,线程自旋等待很少成功获取到锁,那么虚拟机将会减少线程自旋等待的时间。
当轻量级锁自旋过后还无法获取锁时,对象的锁会膨胀为重量级锁。
重量级锁的获取流程:
1、检查对象头中的MarkWord是否为无锁状态,如果是则通过CAS将Owner指向当前线程
2、如果是有锁状态,则向系统申请一个Monitor对象并与对象进行绑定,完成后进入EntryList中等待。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。