作用
synchronized关键字,用来保障原子性,可见性,有序性。
为什么使用synchronized? 在多线程访问共享数据时,控制在同一时刻只能由一个线程执行方法或者代码块,即线程互斥,保证线程安全
应用
应用于方法:
public synchronized void addUser(){.....}
实例方法,锁住的该类的实例对象,
public synchronized static void addUser(){......}
静态方法,锁住的是类对象
应用于代码块:
synchronized(this){......}
同步代码块。锁住的是该类的实例对象
synchronized(A.class){......}
同步代码块,锁住的是类实例
synchronized(object){......}
同步代码块。锁住的是某类的实例对象
原理
采用互斥手段达到同步的目的,临界区,互斥量,信号量都是主要实现互斥的方式;
synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的
synchronized经过编译后,
会在同步块前后形成monitorenter以及minotorexit两个字节码指令,这两个字节码都需要一个reference类型参数来指明要锁定和解锁的对象;在执行monitorenter指令时,即尝试获取锁,获取成功计数器加1,执行monitorexit指令时,即释放锁,锁计数器减1;
会在方法中加入ACC_SYNCHRONIZED标识,JVM通过标识判断是否为同步方法,从而实现同步调用
优化
首先说一下synchronized的不足,其由互斥手段实现同步,而互斥主要的问题是进行线程阻塞与唤醒所带来的性能问题,也称阻塞同步。
理论情况下,由synchronize修饰的代码中共享数据是否存在竞争,都会进行加锁,用户态核心态转换,维护锁计数器,检查是否有阻塞的线程是否需要被唤醒等操作。
在JDK1.4.2中引入自旋锁,在JDK1.6中引入自适应自旋锁
问题 : 线程阻塞唤醒都要在内核态进行,进而带来性能损耗
优化措施:让线程自旋获取锁n次,不放弃处理器;
自旋不能代替阻塞;自旋的优点是避免了线程切换的开销,缺点是占用CPU的执行时间,因此当执行 时间短,锁自旋效果更好,否则白白消耗处理器资源。
自旋默认值是10次,用户可使用参数-XX:PreBlockSpin来更改,而自适应自旋则时间不固定,由前一次在同一个锁上的自旋时间和锁的拥有者状态;如果一定时间内自旋成功获取到锁,且持有锁的线程正在运行中,则JVM可能会允许自旋更长时间,否则可能会直接跳过自旋
在JDK1.5中引入锁消除
问题:虚拟机即时编译器在运行时对一些代码要求同步,然而这段代码中不存在需要竞争的共享数据。
优化措施:判定栈中的数据不会被其他线程访问,则在即时编译时进行锁消除
深入JVM一书中提到,在JDK1.5之前,javac编译器会对string连接做自动优化,如下段代码,会使用线程安全类StringBuffer做append操作,在JDK1.5之后即时编译器判断不存在竞争共享数据后,即用StringBuilder做append操作。
public string concatString(string s1,strinng s2,string s3)
{
return s1+s2+s3;
//经过javac编译后会变成如下代码
//StringBuffer str = new StringBuffer();
//str,append(s1);
//str,append(s2);
//str,append(s3);
//return str.tostring();
}
锁粗化
问题:连续的操作中对一个对象频繁加锁解锁;
优化措施:扩展加锁同步的范围
锁升级
在1.6前synchronized属于重量级锁,之后引入了偏向锁与轻量级锁。
实现轻量级锁和偏量锁的关键在于JAVA对象头中的第一部分,用于存储对象自身的运行时数据,称之mark word
简单了解下对象不同状态下的mark word
状态----------------------------标志位----------------------------存储内容
未锁定 ----------------------------01 ----------------------------对象哈希码,对象分代年龄
轻量级锁定 ----------------------------00----------------------------指向锁记录的指针
重量级锁定----------------------------10----------------------------指向重量级锁的指针
GC标记----------------------------11----------------------------空,不需要记录信息
可偏向----------------------------01----------------------------偏向线程ID,偏向时间戳,对象分代年龄
偏向锁
问题:在无竞争中,同步块会产生不必要的加锁解锁等操作
优化措施:无竞争中,消除同步。
当锁第一次被获取时,会设置对象头标志位为01,且CAS操作读取持有锁的线程ID并记录在对象的MARK WORD中,CAS成功后,持有锁的线程每次进入锁相关的同步块中,JVM都不再进行任何同步操作。
当有另一个线程尝试获取锁时,偏向模式取消,升级到轻量级锁。对象的MARK WORD存储内容由偏向内容变为轻量级内容
轻量级锁
问题:没有多线程竞争,重量级锁使用操作系统互斥量会产生性能消耗。
优化措施:在没有多线程竞争前提下,减少重量级锁使用操作系统互斥量产生的性能消耗。
进入代码块中,如果对象没有被锁定,虚拟机会在当前线程的栈帧中建立一个锁记录空间,拷贝一份对象的MARK WORD,JVM使用CAS操作将对象的MARK WORD更新为指向锁记录的指针。
如果更新失败了,JVM会检查对象的MARK WORD是否已经指向当前线程的锁记录,如果已经持有这个锁,则直接执行同步块,否则,自旋一段时间还未获取到锁或者有第3个线程过来时,轻量级锁升级为重量级锁。
在释放锁时,CAS替换对象的MARK WORD和栈帧中的锁记录,失败了,说明锁被其他线程访问了,升级了,则释放锁的同时,还需要唤醒被挂起的线程
在没有竞争时,使用CAS避免了互斥量开销
在有竞争时,不仅有互斥量的开销,还额外产生了CAS操作,会比传统重量级锁更慢
重量级锁
关键在于对象内部的一个监控器锁,其本质又依赖于底层的操作系统的Mutex Lock来实现,操作系统实现线程之间的切换需要从用户态转化为核心态。
对象监视器中主要存在3种组件:
wait_set 存储调用wait自行阻塞的线程;
entry_list 存储请求锁的线程
owner 当前获取到锁的线程、
wait_set和entry_list中的线程都处于阻塞状态,阻塞是由操作系统完成.