我们在学习java多线程的过程中,总是听到别人说什么乐观锁和悲观锁,那到底什么是乐观锁?什么是悲观锁呢?我们在多线程的并发编程中会遇到对共享资源的操作,而多个线程并发的对共享资源操作会发生线程安全问题,也就是使得共享资源产生混乱,最终得到的结果值与预期值是不一致的,为了解决这种问题,就提供了两种思想来解决,一种是悲观锁,另一种是乐观锁,所以它们只是解决对共享资源操作时产生不一致问题的两种解决思想而已。下面将介绍乐观锁和悲观锁的概率,以及具体在java中的实现。
悲观锁由名字我们就能探究一二,“悲观”表示的它就是一个悲观主义者,它对共享资源的使用是持悲观的态度,它觉得在它使用共享资源的过程中一定会被其他线程更改,所以为了防止线程安全问题的发生,在它使用共享资源的时候,会对共享资源加锁,不允许其他的线程进行使用,待它自己使用完以后,释放掉锁,其他线程才能去使用该共享资源,这就是悲观锁的思想。
在java中通过使用synchronized、ReentrantLock等可独占锁来实现悲观锁这种思想,它是通过对共享资源加琐的形式来实现的,具体对于synchronized、ReentrantLock的使用以及原理可以参考我以往的文章:
java多线程(一)—— 基础使用篇
java多线程(二)—— synchronized锁原理
悲观锁及其实现会有如下的两个缺点:
悲观锁的思想使得不管线程对共享资源的操作是读或是写,都要对资源进行加锁,此时其他线程只能等着线程释放完锁以后才能去使用,试想一下,如果多个线程对于共享资源的操作仅仅是读,那其实这种情况并不会造成线程安全问题,使用悲观锁的思想,就得让一个线程等待上一个线程使用完以后自己才能够使用,这样会使得运行效率低下。所以如果对共享资源的操作方式是多读,使用这种思想会降低程序的运行速度,而在多写的情况下使用这种思想就会比较划算,相对于乐观锁的方式对性能的损耗就不会太大。
在悲观锁的实现synchronized的使用过程中,synchronized是通过一个monitor对象来对共享资源进行加锁,当多个线程竞争一个共享资源,monitor会将没有竞争到共享资源的线程置于一个阻塞队列,等待获得共享资源的使用权;而竞争到共享资源的线程由于缺少某些运行条件,monitor就会将该线程置于一个等待队列,待唤醒以后就会进入到阻塞队列;这一系列的线程状态的转化,需要操作系统从用户态转换为内核态,这个过程是比较耗费cpu资源,所以也就使得synchronized的效率较低。
乐观锁这种思想表达的也就是一个乐观主义的思想,它觉得在自己读取共享资源的过程中,该资源是不会被其他线程所修改,所以它会放心大胆的使用,不会对该资源进行加锁,而在自己将要对该资源进行修改也即写操作时,会检查该资源和自己读取时的值是否一致,如果一致自己就直接进行写操作,否则,得要去重新读取该资源,进行一系列处理,直至自己进行写操作时,该值和读取时的值一致才真正的进行写操作;这种思想也很好理解,读取时大胆的读取,修改时小心翼翼,如果自己修改的时候该值和刚读取时的值是一样的,证明在自己使用这个资源的过程中,并没有被其他线程所修改过,所以自己就可以进行写操作了,这样就能够保证线程安全了。因此,不管是悲观锁还是乐观锁都能够解决线程安全问题。
乐观锁的优点:
乐观锁的缺点:
乐观锁的实现方式有两种:版本号控制和cas算法来进行实现,java中对这两种方式的实现都有,但其中最重要的方法是cas算法,接下来我会介绍一下在java中的具体实现。
接下来介绍一下到底什么是CAS算法,它在java中又是如何实现的呢?
CAS的全称为compare and swap,即比较和替换,也叫做compare and set,即交换和设置,它的算法流程大概如下图所示,通过这个流程图就可以明白cas算法到底在干什么了。
大概的伪代码为如下:
while(true){
int oldR = r;//读取共享资源r
/*
做一些操作
*/
int result = oldR + 1;//准备将result写入到r当中,即准备将r值设置为result
/*
做一些操作
*/
if(compareAndSwap(oldR,result)){//compareAndSwap这个方法就是上面流程中标注的那三个步骤,
//重新读取r的值,再与oldR比较,如果相等就将r的值修改为result返回true,跳出循环
//否则不能修改r的值,返回false,继续循环。
//注意compareAndSwap这个方法的执行过程必须满足原子性,
//一般是通过操作系统的原子操作来实现
break;
}
}
以上就是cas的算法流程了,通过以上的流程我们来分析分析为什么乐观锁适合于多读少写的情况,如果在我们的程序中对共享资源的写比较频繁,那当我们比较共享资源的旧值和旧值,也就是比较oldR和currentR是否相等,因为对于共享资源的修改的次数比较读,所以在比较时很大概率上是不相等的,那此时又要重新循环做一遍重复的操作,循环开销就变大了,也就是如果写的次数比较多,那循环的次数也就会增加,使得浪费cpu资源,造成执行效率低下,所以乐观锁比较适合于多读少写的情况。
原子变量类可以分为 4 组:
我们就以AtomicInteger为例,来介绍原子类的使用以及它的实现原理。我们实现上面流程图的实例,将共享资源进行r进行加1操作。
方法一:自己写循环的逻辑
public static void main(String[] args) {
AtomicInteger r = new AtomicInteger(4);
System.out.println("pre: " + r.get());
while (true){
int oldR = r.get();
int result = oldR + 1;
if(r.compareAndSet(oldR,result)){
break;
}
}
System.out.println("after: " + r.get());
}
方法二:使用原理类提供的一些计算函数
public static void main(String[] args) {
AtomicInteger r = new AtomicInteger(4);
System.out.println("pre: " + r.get());
int v = r.incrementAndGet();//++r
// r.getAndIncrement();//r++
System.out.println("after: " + v);
}
以下是AtomicInteger类常用的一些方法:
public final int get() // 获取当前值
public final int getAndSet(int newValue) // 获取当前值,并设置新值,满足原子性
public final int getAndIncrement()// 获取当前值,并自增,满足原子性
public final int getAndDecrement() // 获取当前值,并自减,满足原子性
public final int getAndAdd(int delta) // 获取当前值,并加上预期值,满足原子性
boolean compareAndSet(int expect, int update) // 如果输入值(update)等于预期值,将该值设置为输入值,满足原子性
public final void lazySet(int newValue) // 最终设置为 newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
我们可以看到AtomicInteger类中的真实值是存储在属性value中,该属性前增加了volatile关键字,也就代表了对该属性值的操作能够保证可见性和有序性,那它的原子性将有什么来实现呢?很显然,就得考unsafe这个属性去实现对value值操作的原子性,那我们来看看它的原子性具体是如何实现的。
compareAndSet(int expect,int update)方法:
我们来看看AtomicInteger中compareAndSet是如何实现的,他是通过调用unsafe中的compareAndSwapInt方法来实现,valueOffset表示的是在AtomicInteger对象中内存的偏移量,把一个AtomicInteger对象存储在内存中,然后通过valueOffset来表示value值在该对象内存中的位置,就可以得到value的值,所以expect表示的是oldR旧值,而通过传入valueOffset来获取当value的当前值。
unsafe实现compareAndSwapInt(this,valueOffset,expect,update)方法直接是通过一个本地方法,也就是c语言实现了该方法。这也能够理解,因为我们的cas算法中,要使得value的旧值与当前值的比较、更改value的值的过程具备原子性,这必须得通过操作系统的原子操作才能够实现,所以就只能使用本地方法实现了。
incrementAndGet()方法:
incrementAndGet也就是实现++i的操作,也是通过unsafe兑现来实现。它的一个实现流程如下:首先通过unsafe的getAndAddInt()方法将AtomicInteger对象中的value值进行加1,但是返回的是加1之前的value值,所以最终返回的值应该要在getAndAddInt()后加上1。