概述
锁的概念,来自于操作系统。
在操作系统中,进程是内存独立,实现进程间的通信需要通过一些其他的手段;但是线程之间是内存共享的,当有多个线程对某一临界资源(有可能被多个线程访问,存在线程安全的数据)进行读写访问时,就会出现线程安全的问题。
锁概念
根据锁对访问限制规则的不同,可以将锁进行分类。下面介绍一下在Java中主要的锁。
乐观锁
悲观锁
自旋锁
适应性自旋锁
公平锁
非公平锁
可重入锁
不可重入锁
共享锁
排他锁
Java锁实现
在Java中,除了使用关键字synchronized进行加锁,在Java5以后,还引入了一个juc包,其中增加了许多适用于Java的线程安全的实现,比如一系列Lock类。
实现Lock的锁类型,可以通过lock.lock()或lock.unLock()进行灵活的加锁和释放锁。其底层实现原理其实是通过Java代码调用native层,再调系统接口实现的。而synchronized则主要是JVM层实现。这里主要讲一下synchronized的运作机制。
实现方式
可以看到,经过编译以后,synchronized关键字的代码块,会被编译为两个虚拟机指令,分别对应着加锁和释放锁操作。
这里我想说一下自己对加锁的理解。Java中的加锁操作,其实是把某个对象当做锁(monitor),也就是每一个对象都是一个锁,对象头中的MaskWord中,记录着该对象的锁状态及拥有的线程信息。
当代码运行进入到同步代码块中,执行monitorenter指令,jvm会去尝试获取monitor的所有权。如果monitor的进入数为0,则线程可以拥有该monitor,并且进入数置1。synchronized是一种可重入锁,因此如果获取所有权时发现已有线程拥有,但是同一个线程,则可以直接进入,并且进入数+1。
当同步代码块运行结束,执行monitorexit,jvm会先去判断,执行该指令的必须是monitor的拥有者,然后进入数-1。如果进入数为0,则该线程不再是该monitor拥有者。
两种同步方式本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。
实现机制
在Java1.6以前,synchronized的性能是比较糟糕的,比不上Lock。但是在Java1.6以后,对synchronized做了性能优化,和Lock其实是并驾齐驱的,可以根据具体场景进行选择。
在1.6中,synchronized增加了一个锁升级的过程。
首先,synchronized默认是乐观锁,可以认为是无锁,即假设没有线程竞争,通过cas操作实现。
如果锁一直只被一个线程持有,那么乐观锁转变为偏向锁。直接检测对象头里是否存在该线程id,而不是用cas操作,进而提高了性能。并且不会主动释放偏向锁。
当有其他线程访问同步代码块时,锁会升级为轻量锁,区别在于等待线程会进行自旋
自旋超过一定次数 或者有第三个线程竞争,则升级为重量锁,等待的线程直接挂起