Synchronizer关键字是java实现内存一致性的一个关键字
官方解释:
同步方法采用一种简单的方法来防止线程之间的干扰和内存一致性错误:当一个对象对多个线程可见时,对这个对象的所有变量的读取和写入都通过同步方法来实现。
简单来说:
当使用这个关键字以后,一次只有一个线程进入这个同步代码块,或者类中的同步方法。
示例
通过反编译
public static void demo2(){ System.out.println("进入demo2方法"); synchronized (Demo.class){ try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("执行sync完毕demo2方法"); }
1 public static demo2()V 2 TRYCATCHBLOCK L0 L1 L2 java/lang/InterruptedException 3 TRYCATCHBLOCK L0 L3 L4 null 4 TRYCATCHBLOCK L4 L5 L4 null 5 L6 6 LINENUMBER 28 L6 7 GETSTATIC java/lang/System.out : Ljava/io/PrintStream; 8 LDC "u8fdbu5165demo2u65b9u6cd5" 9 INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V 10 L7 11 LINENUMBER 29 L7 12 LDC Lutils/Demo;.class 13 DUP 14 ASTORE 0 15 MONITORENTER------------------------------------------进入锁 16 L0 17 LINENUMBER 31 L0 18 LDC 10 19 INVOKESTATIC java/lang/Thread.sleep (J)V 20 L1 21 LINENUMBER 34 L1 22 GOTO L8 23 L2 24 LINENUMBER 32 L2 25 FRAME FULL [java/lang/Object] [java/lang/InterruptedException] 26 ASTORE 1 27 L9 28 LINENUMBER 33 L9 29 ALOAD 1 30 INVOKEVIRTUAL java/lang/InterruptedException.printStackTrace ()V 31 L8 32 LINENUMBER 35 L8 33 FRAME SAME 34 ALOAD 0 35 MONITOREXIT----------------------------------------解除锁 36 L3 37 GOTO L10 38 L4 39 FRAME SAME1 java/lang/Throwable 40 ASTORE 2 41 ALOAD 0 42 MONITOREXIT----------------------------------------解除锁 43 L5 44 ALOAD 2 45 ATHROW 46 L10 47 LINENUMBER 36 L10 48 FRAME CHOP 1 49 GETSTATIC java/lang/System.out : Ljava/io/PrintStream; 50 LDC "u6267u884csyncu5b8cu6bd5demo2u65b9u6cd5" 51 INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V 52 L11 53 LINENUMBER 37 L11 54 RETURN 55 LOCALVARIABLE e Ljava/lang/InterruptedException; L9 L8 1 56 MAXSTACK = 2 57 MAXLOCALS = 3
发现synchronized是使用MonitorEnter和MonitorExit来获取锁和释放锁的。同时用Synchronized来修饰方法,是为方法添加一个ACC_SYNCHRONIZED标识.
关键字的使用
synchronized是取对象来当作锁,而不是把代码块或者方法当作锁。
1.静态方法,锁Class类
2.非静态方法,锁实例
3.synchronized(obj),锁实例
4.synchronized(obj.class),锁class类
5.synchronized(this), 锁实例
synchronized锁具有重入功能,及当一个线程拥有这个锁的时候,他能够重复的获取这个锁.同时synchtonized没法响应中断,当代码块中出现异常的时候会自动释放自己持有的锁.
关键字的底层实现
先来了解一下对象头
计算机位数 | 头对象结构 | 说明 |
32bit/64bit | MarkWord | 存储对象的hashCode,锁信息,分代年龄和gc标志 |
32bit/64bit | class MateData Address | 类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例。 |
由上可知锁信息主要是存在与MarkWord中的,再来看一下MarkWord
MarkWord中标识了当前锁的等级.同时当前说的Synchronized锁对应的就是重量级锁,指向得就是一个Monitor对象.(MarkWord中的LockWord指向的就是Monitor)
同时什么是Monitor呢
Monitor可以理解为是一个同步工具,也可以描述为一种同步机制,通常Monitor描述成一个对象.
所有对象都有成为Monitor的潜质,Monitor和对象之间是一一对应的,Monitor跟随对象一起创建或者销毁.也有可能会在上锁的时候创建Monitor.
Monitor是线程私有的数据结构,Monitor是用ObjectMonitor来实现的.其主要的数据结构如下
ObjectMonitor() { _header = NULL; _count = 0; //记录个数 _waiters = 0, _recursions = 0; //重入次数 _object = NULL; _owner = NULL;//锁拥有者 _WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; }
由以上的Count和recursions就能看到Synchronized是可重入锁,
WaitSet和EntrySet区别
获取Monitor的线程会同时进入Entryset中等待notify来唤醒,线程1获取锁会将owner指向线程自己.同时count++,当线程1调用wait方法时.Owner=null,count--,recursions--,线程1进入WaitSet,同时对象调用nitifyAll方法来启动其他线程获取当前的Monitor.
锁升级和优化
锁的四种情况:无锁状态,偏向锁,轻量级锁,重量级锁.
偏向锁:实际发现线程之间竞争锁的现象只是少数,大多数还是同一个线程来再次获取这个锁,所以当线程持有当前这个锁的时候会将自己的线程Id存入Monitor的偏向锁指向.当重入进入的时候就会添加Monitor的recursions字段.只有当重入字段变成0才能释放锁.当存在多个线程竞争一个锁的时候,偏向锁升级成轻量级锁.
轻量级锁:多个线程竞争一个锁,当线程发现当前锁已经被持有开始CAS自旋来获取锁.同时CAS会消耗CPU,当时当线程持有锁的时间很短的时候会大幅度提高响应速率,避免了线程的阻塞.当线程CAS次数超过额定次数,或者多个线程同时CAS来获取这个锁的时候,锁会升级成为重量级锁,
重量级锁:重量级锁会阻塞所有的等待线程防止CPU空转,
锁是可以升级的,当时不可以退化,也就是偏向锁可以变成轻量级锁,轻量级锁不能变成偏向锁.重量级锁同理.
锁消除
jvm会自动判断锁的必要性,同时消除一些重复的锁来节约时间和空间.
StringBuffer sb = new StringBuffer(); for(int i = 0; i < 10; i++){ sb.append("111"); }
锁粗化
使用同步的要求是让同步的范围越小越好,这样的目的是为了让线程等待的时间变短,但是当你重复获取锁和释放锁的时候,锁就会自动将自己的同步范围变大,让你获取锁和释放锁的次数减小.
上面的代码,jvm会自动将锁上在for循环的外面.
问题:
1.waitSet和EntrySet的区别
2.synchronized和lock锁的区别
使用方式不同,lock可以响应中断,sync默认是非公平锁但是lock可以自己选择
3.重量级锁为什么需要优化
重量级锁的开销比较大,因为重量级锁会将其他等待的线程进行阻塞,是用notify和notifyAll来唤醒线程,这就需要操作系统来帮忙,从用户态变成了内核态,状态转换会消耗大量的时间.