1.Java中有哪些锁?
这个要看如何分类
1.1.按照是否要加锁分为:悲观锁和乐观锁
悲观锁和乐观锁是按照加锁机制进行分类的,是一种设计理念,并不是具体的某把锁:
-
悲观锁是要加锁的,如Synchronization ,ReentrantLock 都是悲观锁 , 他的设计理念:就是很悲观,每次去拿数据的时候都认为别人会修改。所以每次在拿数据的时候都会上锁。这样别人想拿数据就被挡住,直到悲观锁被释放。
乐观锁是不加锁的:他的设计理念就是很乐观,每次去拿数据的时候都认为别人不会修改。所以不会上锁,不会上锁!但是如果 想要更新数据,则会在更新前检查在读取至更新这段时间别人有没有修改过这个数据。如果修改过, 则重新读取,再次尝试更新,循环上述步骤直到更新成功(当然也允许更新失败的线程放弃操作)。 说到乐观锁,就必须提到一个概念:CAS,什么是CAS呢?Compare-and-Swap,即比较并替换,也有叫做Compare-and-Set的,比较并设置。
乐观锁的使用场景如:Mysql 或 Elasticsearch 通过version来控制并发修改 ,也可以通过时间戳,UUID等来实现乐观锁。 比如:AtomicInteger 原子类使用到CAS思想也是乐观锁
-
他们的区别在于:悲观锁是在业务一开始就加锁,业务处理完成之后,释放锁,拿Mysql来说单执行加了 for update 的查询语句时就加锁,业务处理完成,事务提交就释放锁。而乐观锁是不加锁,只是最后做数据同步的时候,判断该条数据时候被别的线程修改过,来决定要不要执行当前业操作。 所以一个是加锁,一个是不加锁的。乐观锁的性能是更高的。但是从安全性上来说悲观锁是更安全的。
1.2.按照是否要阻塞分为:阻塞锁或者自旋锁
-
阻塞锁:阻塞锁如其名,就是当拿不到这个数据的锁,当前线程就会阻塞,直到被唤醒,相当于暂停这个线程的工作,让这个线程不会占用CPU时间,但缺点是线程恢复速度要比自旋锁慢,如Synchronization
-
自旋锁(不阻塞):当一个线程尝试去获取某一把锁的时候,如果这个锁此时已经被别人获取(占用),那么此线程就无法获取到这把锁,该线程将会自旋,间隔一段时间后会再次尝试获取。这种采用循环加锁 -> 等待的机制被称为自旋锁(spinlock),要注意:长时间自旋会空耗CPU(忙等)
-
适应性自旋锁(不阻塞):在自旋锁的基础上自旋,尝试一定的次数还是获取不到锁就放弃获取锁,这种模式叫适应性自旋。
-
总结:线程竞争激烈的时候用阻塞锁,不激烈的时候用自旋锁。
1.3.按照是否要排队加锁分为:公平和非公平锁
-
公平锁(排队加锁):多个线程都在竞锁时是否要按照先后顺序排队加锁,如果是那就是公平锁
-
非公平锁(不排队加锁):多个线程都在竞锁时不需要排队加锁,是为非公平锁
1.4.按照是否可重入分为:重入锁和非重入锁
-
可重入锁:允许同一个线程多次获取同一把锁,是为可重入锁:比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁(因为这个原因可重入锁也叫做递归锁)。Java里只要以Reentrant开头命名的锁都是可重入锁,Synchronization 也是可重入的
-
非可重入锁:一个线程在多个流程中不可用获取到同一把锁,是为非重入锁
1.5.按照锁的互斥特性分为:共享锁 和 排他锁
-
共享锁:多个线程可以共享一把锁,如多个线程同时读,一般是可共享读锁 :如读锁
-
排他锁:多个线程不可同时获取到一把锁,比如:lock ,synchronized锁
2.Synchronized 原理
synchronized是基于JVM内置锁实现,通过内部对象监视器锁(Monitor)实 现,监视器锁的实现依赖 底层操作系统的Mutex lock(互斥锁)实现,它是一个重量级锁性能较低。当 然,JVM内置锁在1.5之后版本做了重大的优化、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等 技术来减少锁操作的开销,内置锁的并发性能已经基本与Lock持平。Synchronized关键字在编译的字节码中加入了两条指令来进行代码的同步。
简单理解:Synchronized基于JVM内置锁实现,通过内部对象Monitor(监视器锁)实现,通过进入与退出对象的Monitor来实现方法与代码块同步。对象的JDK1.5之后 ,Synchronized会从无锁升级为偏向锁,再升级为轻量级锁,最后升级为重量级锁。
synchronized锁的优化(升级原理)
我们都知道,计算机系统可以分为用户态和内核态
-
内核态 : CPU可以访问内存所有数据,包括外围设备,例如硬盘,网卡。CPU也可以将自己从一个程序切换到另一个程序
-
用户态 :只能受限的访问内存,切不允许访问外围设备占用CPU的能力被剥夺,CPU资源可以被其他程序获取。之所以会有这样的却分是为了防止用户进程获取别的程序的内存数据,或者获取外围设备的数据。
Synchronized原本是依赖操作系统实现的,因此在使用synchronized同步锁的时候需要进行用户态到内核态的切换,也就是说JVM依赖于底层的操作系统的Mutex Lock来实现的,但是由于使用Mutex Lock需要将当前线程挂起并从用户态切换到内核态来执行,这种切换的代价是非常昂贵的。
jdk 1.6及其之后的优化
我们知道,每个对象都拥有对象头,对象头由Mark World ,指向类的指针,以及数组长度三部分组成,锁升级主要依赖Mark Word中的锁标志位和释放偏向锁标识位。
-
偏向锁(无锁)
大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁的目的是在某个线程 获得锁之后(线程的id会记录在对象的Mark Word锁标志位中),消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护。(第二次还是这个线程进来就不需要重复加锁,基本无开销),如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。
-
轻量级锁(CAS):
轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁自旋锁);没有抢到锁的线程将自旋,获取锁的操作。轻量级锁的意图是在没有多线程竞争的情况下,通过CAS操作尝试将MarkWord锁标志位更新为指向LockRecord的指针,减少了使用重量级锁的系统互斥量产生的性能消耗。
长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)
-
重量级锁:
如果锁竞争情况严重,某个达到最大自旋次数(10次默认)的线程,会将轻量级锁升级为重量级锁,重量级锁则直接将自己挂起,在JDK1.6之前,synchronized直接加重量级锁,很明显现在得到了很好的优化。
虚拟机使用CAS操作尝试将MarkWord更新为指向LockRecord的指针,如果更新成功表示线程就拥有该对象的锁;如果失败,会检查MarkWord是否指向当前线程的栈帧,如果是,表示当前线程已经拥有这个锁;如果不是,说明这个锁被其他线程抢占,此时膨胀为重量级锁。