1.1、 synchronized的主要作用有如下三方面
1、原子性:确保线程互斥的访问代码块;2、可见性:保证共享变量的修改能够及时可见,其实是通过Java内存模型中的“对一个变量unlock操作之前,必须要同步到主内存中;如果对一个变量进行lock操作,则将会清空工作内存中此变量的值,在执行引擎使用此变量钱,需要重新从主内存中load操作或者assign操作初始化变量”来保证;3、有序性:有效解决重排序问题,即“一个unlock操作先行发生(happen-before)于后面对同一个锁的lock操作”
1.2、synchronized 的三种使用方式:
1、作用在实例方法上,此时锁是当前实例对象。
2、作用在静态方法上,此时锁是当前类的Class对象。
3、作用在代码块上, 此时锁是Synchronized括号中指定的对象。
synchronized 是在软件层面依赖JVM。当一个线程访问同步方法或者同步代码块时,首先需要得到锁才能执行同步代码,当退出或者抛出异常时都必须释放锁。但synchronized作用在方法或者代码的实现是不同的,具体实现机制如下:
同步代码块是在编译时把指令 monitorenter和monitorext指令分别加入到同步代码块的开始位置和结束位置。
1)、monitorenter:每个对象都是一个监视器(monitor)。当monitor 被占用是就会处于锁定状态,线程 执行monitorenter指令尝试获取monitor的所有权,过程如下:
1、如果monitor的进入数为0,则该线程进入 monitor,然后将进入数设置为1,该线程即为 monitor的所有者;
2、如果当前线程已经占有了改monitor,只是重新进入,则进入monitor的进入数加 1;
3、如果monitor已被其他线程所占有,则该线程进入阻塞状态,只到占有monitor的线程释放,并且monitor的进入数为0,再重新获取monitor的所有权。
2)、monitorexit: 执行monitorexit的线程必须是objectref所对应的monitor所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那边线程对出monitor,不再占有此monitor。其他被这个monitor阻塞的线程会尝试去获取这个monitor的所有权。
方法同步在编译时没有插入 monitorenter 和monitorexit 指令来完成,只是相对于其他方法,其常量池中多了 ACC_SYNCHRONIZED标识符。JVM是根据改标识符来实现方法同步的:
当方法调用时,调用指令会检查方法的ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功后才执行方法体,方法执行完成后释放monitor。在方法执行其他,其他任何线程都无法在获取同一个monitor对象。
在JVM中,对象在内存中的分布分为三个区域:对象头、实例数据和对齐填充。
1、实例数据:存放类的属性数据信息,包括父类的属性信息。
2、对齐填充:由于虚拟机要求对象起始地址必须为8字节的整数倍。填充数据不是必须的,仅仅是为了字节对齐;
3、对象头:Java对象头默认在2个字宽,如果是数组则为3个字宽,第三个用了存储数组的长度。
Synchronized用的锁就是存储在Java对象头里的, 虚拟机的对象头主要包括两部分:Mark Word(标记字段)、Class Pointer(类型指针)。
Mark Word 主要用于存储对象自身运行时数据,如:哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
1、无锁状态下的结构
锁状态 |
25bit |
4bit |
1bit是否是偏向锁 |
2bit所标志位 |
无锁状态 |
对象的hashCode |
对象分代年龄 |
0 |
01 |
2、Mark Word 的状态变化
在线程进入同步代码块的时候,如果此同步对象没有被锁定,即它的所标志位是“01”,则虚拟机首先在当前线程的栈中创建一个称之为 ”锁记录(Lock Record)“的空间,用于存储所对象的Mark Word的拷贝。
Lock Record 是线程私有的数据结构,每个线程都有一个可用的Lock Record列表,同时还有一个全局的可用列表。每个被锁住的对象Mark Word都会和一个Lock Record 关联(对象头的Mark Word中的 Lock word 指向Lock Record的起始地址),同时Lock Record中有一个Owner字段存放当前线程所拥有的锁的地址。
从JDK6开始,就对synchronized的实现机制进行了较大的调整,加入了自旋锁、自适应自旋锁、锁消除、锁粗化、偏向锁、轻量级锁这些优化策略,这些技术都是为了在线程之间更高效的共享数据及解决竞争问题,从而提高程序的执行效率。
锁主要存在四种状态,它们依次为:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。它们是上一次升级的,锁状态只能是从低到高依次升级,而不能降级。
在互斥同步执行代码时,对性能影响最大的就是阻塞的实现,挂起线程和恢复线程的操作都需要有用户态转到内核态来完成,这些操作给Java虚拟机的并发性能带来了很大的压力。同时,虚拟机的开发团队注意到在许多应用上,共享数据的锁状态只会持续很短的一段时间,为了这段时间而去挂起和恢复线程并不值得。所以我们可以让后面请求锁的那个线程“稍等一会”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需要让线程执行一个空循环(即自旋),这项技术就是所谓的自旋锁。
自旋锁在JDK6中是默认开启了。自旋等待不能代替阻塞,且不说对处理器的要求,自旋等待本身虽然避免了线程切换的开销,但它是占用了处理器时间,所以如果锁被占用的时间很短,自旋等待的效果就会非常好,反之如果锁被占用的时间很长,那么自旋的线程只能白白消耗处理器资源,而不会做任何有价值的工作,就会带来性能的浪费。因此自旋等待的时间必须有一定的限度,如果自旋超过了限定次数或时间仍然没有成功获取锁,就应当使用传统的挂起线程。自旋次数默认值为10次,可以通过参数 -XX:PreBlockSpin来自行更改。
在JDK6中对自旋锁的优化,引入了自适应自旋。自适应意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间及锁的拥有这的状态来决定的。如果在同一个所对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而允许自旋等待持续相对更长的时间。另一方面,如果对于某个锁,自旋很少成功获得锁,那在以后要获取这个锁时就可能省掉了自旋过程,直接阻塞,以避免浪费处理器资源。
有了自适应自旋,随着程序运行时间的增长及性能监控信息的不断完善,虚拟机对程序锁的状况就会越来越精准,虚拟机也就会变得越来越聪明。