java中的各种锁

一、 Java中锁的分类

乐观锁

乐观锁是一种思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则重复读-比较-写的操作。

java中乐观锁基本都是通过CAS(Compare And Swap)实现的,CAS是一种更新的原子操作,比较当前的值和传入的值是否一样,一样则更新,否则失败。

悲观锁

悲观锁就是悲观思想,认为写多,遇到并发的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会先上锁。这样别人想读取数据就会直接block拿到锁,java中的悲观锁就是synchronized,AQS框架下的锁则是先尝试CAS乐观锁去获取锁,获取不到,才会转为悲观锁,如RetreenLock。

自旋锁

自旋锁的原理非常简单,如果持有锁的线程能在很短时间内释放资源,那么等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞状态,他们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换消耗。

线程自旋是需要消耗CPU的,说白了就是再让CPU做无用功,如果一直获取不到锁,那县城也不能一直占用CPU自旋锁做无用功,所以需要设定一个最大自旋等待时间。

如果持有锁的线程执行时间查过自旋等待的最大时间仍然没有释放锁,就会导致其他争用锁的线程在最大时间还是获取不到锁,这是争用线程会停止自旋进入阻塞状态。

非公平锁

JVM按照随机、就近原则分配锁的机制则称为不公平锁。非公平锁是指多个线程获取锁的顺序并不是按照申请的顺序,有可能后申请的线程比先申请的线程先获得锁。

在java中,ReentrantLock可以通过构造函数指定该锁是公平锁还是非公平锁,默认是非公平锁。非公平锁实际执行效率要远远超出公平锁,因此除非有特殊需要,否则最常用的还是非公平锁的分配机制。

对于synchronized而言,是一种非公平锁,由于其并不想ReentrantLock是通过AQS框架实现的线程调度,所有没有任何办法使其变为公平锁。

公平锁

与非公平锁相对,公平锁是按照线程申请的顺序进行锁的分配。通常先对锁获取请求的线程会先被分配到锁。由于公平锁会维护一个线程队列,因此相比非公平锁性能会下降5-10倍。

可重入锁(递归锁)

可重入锁又称递归锁,是指在一个线程的外层方法回去锁之后,在进入内层方法时会自动回去锁。在java中,ReentrantLock和Synchronized都是可重入锁。

ReadWriteLock 读写锁

为了提高性能,Java中提供了读写锁,在读的地方用读锁,在写的地方用写锁,灵活控制,在没有写锁的情况下,读是无阻塞的,在一定情况下提高了程序的运行效率。读写锁分为读锁和写锁,多个读锁不互斥,读锁和写锁互斥,这是由jvm控制的,你只要上好相应的锁即可。

Java中读写锁有个接口java.util.concurrent.locks.ReadWriteLock,也有具体的实现ReentrantReadWriteLock。

共享锁和独占锁

java并发包中提供的加锁模式分别为共享锁和独占锁。

1、独占锁

独占锁模式下,只有一个线程持有锁,ReentrantLock就是以独占锁实现的互斥锁。独占锁是一种悲观保守的加锁策略,他避免了读/读冲突,如果某个只读线程获取了锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,应为读操作并不会影响数据的一致性。

2、共享锁

共享锁允许多个线程同时获得锁,并发访问共享资源,如:ReadWriteLock。共享锁是一种乐观锁,他放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。

AQS内部类Node定义了两个常量SHARED和EXCLUSIVE,他们分别表示了AQS队列中等待线程获取锁的模式。

重量级锁

Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的MutexLock实现的,而操作系统实现线程之间的切换这就需要从用户态转换为核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此这种依赖于操作系统Mutex Lock实现的锁我们称之为“重量级锁”,JDK中对于Synchronized的优化,其核心就是为了减少这种重量级锁的使用。JDK1.6之后,为了减少锁和释放锁所带来的性能消耗,提高性能,引入了“轻量级锁”和“偏向锁”。

轻量级锁

锁的状态有四种:无锁状态、偏向锁、轻量级锁、重量级锁。

随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。但是锁升级只能是单向的,也就是说只能从低到高,不会出现锁的降级。

“轻量级锁”是相对于使用系统互斥量来实现的传统锁而言的,但是首先要强调一点的是,轻量级锁并不是用来代替重量级锁的,他的本意是在没有多线程竞争的前提下,减少传统的重量级锁产生的性能消耗。在解释轻量级锁的执行过程之前,先明白一点,轻量级锁所适应的场景是线程交替执行同步代码块的情况,如果存在同一时间访问统一锁的情况,就会导致轻量级锁膨胀为重量级锁。

偏向锁

Hotspot的作者经过大量研究发现大多数情况下锁不仅不会存在多线程竞争,而且总是由同一个线程多次获取。偏向锁的目的是在某个线程获得锁之后,消除这个线程锁重入(CAS)的开销,看起来像是让这个线程得到了偏护。引入偏向锁的目的是为了在无多线程竞争条件下尽量减少不必要的轻量级锁执行路径,应为轻量级锁的获取和释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销消耗的性能必须小于节省下来的CAS原子指令的性能消耗)。上面说过,轻量级锁是为了在线程交替执行同步块的时候提高性能,而偏向锁则是在只有一个线程执行同步快时进一步提高性能。

分段锁

分段锁是一种设计,并不是具体的一种锁。在ConcurrentHashMap中,其并发的思想就是通过分段所的形式来实现高效的并发操作。

二、 JAVA中几种锁的实现

1、Synchronized 同步锁

synchronized 可以把任意一个非NULL的对象当作锁。它属于独占锁,同时属于可重入锁。

Synchronized的作用范围

当做用于方法时,锁住的对象是对象的实例(this)。

当做用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen(JDK1.8则是metaspace),永久带是全局共享,因此静态方法锁相当于是类的一个全局锁,会锁住所有调用该方法的线程。

当作用域一个对象实例时,锁住的是所有以该对象为锁的代码块。他有多个队列,当多个线程一起访问某个对象监视器时,对象监视器会将这些线程存储在不同的容器中。

Synchronized 核心组件

Wait Set: 那些调用wait方法被阻塞的线程放置在这里
Contention List: 竞争队列,所有请求所得线程首先被放在这个竞争队列中
Entry List: Contention List中的那些有资格成为候选资源的线程被移动到Entry List中
OnDeck: 任意时刻,最多只有一个线程正在竞争锁资源,该线程被称为OnDeck
Owner: 当前已经获取到资源锁的线程被称为Owner
!Owner: 当前释放锁的线程

2、ReentrantLock

ReentrantLock继承接口Lock并实现了接口中定义的方法,也是一种可重入锁,除了能完成Synchronized所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。

ReentrantLock通过方法lock和unlock来进行加锁和解锁操作,与Synchronized会被JVM自动解锁不同,ReentrantLock加锁需要手动进行解锁。为了避免程序出现异常而无法正常解锁的情况,使用ReentrantLock必须在finally控制块中进行解锁操作。

3、Semaphore 信号量

Semaphore是一种基于计数的信号量,他可以设定一个阈值,基于此,多个线程竞争获取许可信号,做完自己的申请后归还,超过阈值后,线程申请许可信号量会被阻塞。Semaphore可以用来构建一些对象池、线程池等,比如数据库连接池。

Semaphore基本能完成ReentrantLock的所有工作,使用方法也与之类似,通过acquire()与release()方法来获取和释放资源。经实测,Semaphore.acquire()默认为可响应中断锁,与ReentrantLock.lockInterruptibly()作用效果一致,也就是说在等待临界资源的过程中可以被Thread.interrupt()中断。

此外,Semaphore也实现了可轮询的锁请求和定时锁的功能,除了方法名tryAcquire()与tryLock不同之外,其使用方法与ReentrantLock几乎一致,Semaphore也提供了公平和非公平锁的机制,也可以在构造函数中进行设定。

Semaphore的释放也必须手动进行,因此与ReentrantLock一样,为了避免程序出现异常而无法正常解锁的情况,使用ReentrantLock必须在finally控制块中进行解锁操作。

4、AtomicInteger

此处AtomicInteger是一个提供原子操作的Integer类,类似的还有AtomicBoolean、AtomicLong、AtomicReference等。他们的实现原理相同,区别在于运算类型不同,令人兴奋的是,可以通过AtomicReference 将一个对象的所有操作转换为原子操作。

我们知道,在多线程程序中,诸如i++等运算不具备原子性,是不安全的线程操作之一。通常我们会使用synchronized将该操作变为一个原子操作,但JVM为此类特意提供了一些同步类,使得使用方便,且使程序运行效率变得更高。通过相关资料显示,使用AtomicInteger的性能是ReentantLock的好几倍。

三、 锁的优化

1、较少锁持有时间

只在有线程安全的程序上加锁

2、减少锁粒度

将对象拆成小对象,大大增加并行度,降低锁竞争。降低了锁的竞争,偏向锁、轻量级锁的概率才会高,最典型的的减小锁粒度的案例就是ConcurrentHashMap。

3、锁分离

最常见的锁分离就是读写锁ReadWriteLock,根据功能分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,既保证了线程安全,有提高了性能。读写分离思想可以延伸,只要操作互不影响,所就可以分离,比如LinkedBlockingQueue从头部取出,从尾部放数据。

4、锁粗化

通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁。但是凡事都有一个度,如果对同一个锁不停的请求、同步、释放,其本身也会消耗宝贵的系统资源,反而不利于性能优化。

5、锁消除

锁消除是编译器级别的事,在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作,多数是因为程序员编码不规范引起的。