synchronized关键字阐述
个人博客:kana.chat:90
什么是synchronized?
讲到Sync,其实不可避免要讲述JMM模型。笔者简单的概述一下JMM。
CPU缓存
对于现代的计算机,因为处理器和存储设备相差速度过大,所以在两者之间会加一层高速缓存。将运算需要的数据复制到缓存中,让计算可以快速运行,运算结束后从缓存同步回内存,处理器都无需等待缓慢的内存读写了。
但是这样的同时也引出了新的矛盾:缓存一致性
在多处理器中,每个处理器都有自己的高速缓存,而他们又共享一块主内存,那么同步到主内存以谁的数据为准呢?这时每个处理器访问缓存时都要遵循一定的协议,读写时根据协议操作。
java虚拟机中内存模型中定义的内存访问操作与硬件的缓存访问操作是具有可比性的。
java内存模型(java memory model 即JMM):
java内存模型规定所有的变量都存储在主内存中,而每条线程都有自己的工作线程,工作线程中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值)都在工作线程内,而不能直接读写主内存中的变量(volatile变量仍然有工作内存的拷贝,但是由于它的特殊操作性,看起来像在主内存读写一般)。不同线程之间也无法互相直接访问,要通过主内存来进行访问。
关系图如下(类似CPU缓存模型):
java中定义了8种操作实现主内存和工作内存间的交互
(虚拟机实现时必须是原子性的):
lock(锁定)unlock(解锁)read(读取)load(载入)use(使用)
assign(赋值) store(存储) write(写入)
这八种操作的具体限制和使用就不再过多阐述,可以自学搜索,
资料来源自《深入理解java虚拟机 周志明》
java针对线程安全有三类操作:
1.互斥同步,使用synchronized或者ReentrantLock实现同步。因为jdk1.6后sync大幅度优化,所以二者性能方面差距不是很大,而后者的主要差距在于:等待可中断(一个线程长期持有锁,另外等待的线程可暂时放弃等待处理其他事)、公平锁(按照申请锁的顺序获取锁)、锁绑定多个条件(多个sync才能绑定多个条件,而ReentrantLock只要多次调用new Condition()即可)。
2.非阻塞同步,这依赖于硬件指令集的发展,硬件保证一个语义上看起来需要多次操作的行为只需要一次处理器指令就能完成。
CAS指令需要三个操作数,内存位置(可以理解为变量内存地址,V)、旧的预期值(A)、新值(B)。CAS指令执行时,当且仅当V符合旧预期值A的时候,处理器用新值B更新V,否则不执行更新,不过无论是否更新都会返回V的旧值,上述是一个原子操作。
3.无同步方案:
(1).可重入代码(不依赖堆上数据和公用的系统资源,不调用非可重入代码,状态量都由参数传入等)。
(2).线程本地存储,在线程本地保存数据,共享数据的范围缩小到线程内,如ThreadLocal类内部使用了ThreadLocalMap来维护数据。
而Sync其实就是在多线程条件下,防止数据并发操作问题的一个互斥同步方案
java对象头如何查看
synchronize与使用aqs原理开发的ReentrantLock不同,后者是java层面的,而前者则是jvm层面的关键字,只能用查看字节码的方式来看源码。
一个java对象由他的实例数据(属性、变量)、填充数据以及对象头组成。
我们通过代码查看
jdk中的unsafe类可以查看,但是里面的方法基本是C++代码(native方法 ,如CAS), 所以这里使用引入工具类来查看。
前三项是对象头的长度,第四项是实例数据的大小,而最后一项是给下一个对象地址补齐(补成16位,作为8的倍数0在64为jvm中必须是8的倍数,例如:8 16 24等)
这一点添加一个实例变量就能看出
添加了一个四字节int变量,赋值为0。
回到上图。 因为一个byte是8个bit(8位),所以12位对象头有96位
关于对象头:
markword里面包含了锁的信息、hashcode、分代年龄(用于GC)等信息,而第二个则指向对象的元数据。
截取一段hotspot源码中的注释。
第一行就写明,前25位unused,再往后31位是hashcode,再加没用的一位,之后分别是 分代年龄(四位)、偏向锁的标识(一位)、锁的状态(两位)。
hashcode:
(hashcode是个native方法,实质是计算内存地址)
如果不调用就不会去计算。
示例:
10进制转16进制更易发现两者相等:
使用小端模式存储,高字节存于低地址。
这个字节其实是保留字段 属于unused
其他部分:
GC分代年龄:
偏向标识:
能否成为偏向锁
锁的状态(轻量 重量 偏向):
00偏向锁 01轻量锁 10重量锁 11gc状态 结合偏向标识 101 无锁可偏向 001无锁不可偏向
偏向锁可以不与操作系统交互,通过自旋方式完成同步,而轻量锁不行。
klass point(指向方法区中class对象(以字节码方式存储)的首地址的指针):
klass point不一定是32位,因为不是所有jvm都开启了指针压缩。
自定义线程对象
普通无竞争加锁后是偏向锁
当对象拥有hashcode后,这个对象就不能再成为偏向锁了,因为偏向锁存线程id的位置被hashcode字节码占去。
出现线程竞争后升级为重量锁