乐观锁悲观锁

Posted zqlmianshi

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了乐观锁悲观锁相关的知识,希望对你有一定的参考价值。

java中的乐观锁和悲观锁是常用的并发控制机制,用于并发访问共享数据时保证数据的一致性。它们的区别在于对于共享数据的访问策略不同。

  1. 悲观锁

悲观锁认为在并发访问中,数据很容易被其他线程修改,因此在访问共享数据时,会采用“独占”的方式,即在访问数据之前,先将其锁定,确保其他线程无法修改该数据,待访问完成后再释放锁。

Java中的synchronized关键字就是一种悲观锁的实现方式。它会对代码块或方法进行加锁,确保同一时刻只有一个线程可以执行该代码块或方法,从而保证数据的一致性。但synchronized关键字在并发量较高时,由于会造成线程阻塞,导致程序性能下降。

  1. 乐观锁

乐观锁认为数据在并发访问时,不容易被其他线程修改,因此在访问共享数据时,不对数据进行加锁,而是在更新数据时,先检查数据是否被其他线程修改过,如果没有被修改,则直接更新数据,否则放弃更新并返回错误信息。

Java中的CAS(Compare And Swap)操作就是一种乐观锁的实现方式。CAS操作是通过比较当前值与旧值是否一致来判断是否被修改过,如果一致,则使用新值更新,否则返回错误信息。Java中的AtomicInteger和AtomicReference等类都是基于CAS操作实现的。

总之,悲观锁适用于写操作较多的场景,适合保证数据的强一致性;而乐观锁适用于读操作较多的场景,适合保证数据的最终一致性。但乐观锁需要解决ABA问题,即在两次检查之间,数据可能被其他线程修改多次,导致检查失效。因此,应根据具体场景选择适合的锁机制

乐观锁与悲观锁

2018年10月24日 周三 19:40

 

乐观锁与悲观锁.rtf

 

2018年7月29日 周日 18:55

 

乐观锁与悲观锁

 

概念:

这里抛开数据库来谈乐观锁和悲观锁,扯上数据库总会觉得和Java离得很远.

悲观锁:一段执行逻辑加上悲观锁,不同线程同时执行时,只能有一个线程执行,其他的线程在入口处等待,直到锁被释放.

乐观锁:一段执行逻辑加上乐观锁,不同线程同时执行时,可以同时进入执行,在最后更新数据的时候要检查这些数据是否被其他线程修改了(版本和执行初是否相同),没有修改则进行更新,否则放弃本次操作.

从解释上可以看出,悲观锁具有很强的独占性,也是最安全的.而乐观锁很开放,效率高,安全性比悲观锁低,因为在乐观锁检查数据版本一致性时也可能被其他线程修改数据.

 

从下面的例子中可以看出来这里说的安全差别.

乐观锁例子:

 

/**

 * 乐观锁

 

 *

 

 * 场景:有一个对象value,需要被两个线程调用,由于是共享数据,存在脏数据的问题

 

 * 悲观锁可以利用synchronized实现,这里不提.

 

 * 现在用乐观锁来解决这个脏数据问题

 

 *

 

 * @author lxz

 

 *

 

 */

public class OptimisticLock {

 

    public static int value = 0; // 多线程同时调用的操作对象

 

    /**

     * A线程要执行的方法

 

     */

    public static void invoke(int Avalue, String i)

 

            throws InterruptedException {

 

        Thread.sleep(1000L);//延长执行时间

        if (Avalue != value) {//判断value版本

            System.out.println(Avalue + ":" + value + "A版本不一致,不执行");

 

            value--;

 

        } else {

 

            Avalue++;//对数据操作

            value = Avalue;;//对数据操作

            System.out.println(i + ":" + value);

 

        }

 

    }

 

 

 

    /**

     * B线程要执行的方法

 

     */

    public static void invoke2(int Bvalue, String i)

 

            throws InterruptedException {

 

        Thread.sleep(1000L);//延长执行时间

        if (Bvalue != value) {//判断value版本

            System.out.println(Bvalue + ":" + value + "B版本不一致,不执行");

 

        } else {

 

            System.out.println("B:利用value运算,value="+Bvalue);

 

        }

 

    }

 

 

 

    /**

     * 测试,期待结果:B线程执行的时候value数据总是当前最新的

 

     */

    public static void main(String[] args) throws InterruptedException {

 

        new Thread(new Runnable() {//A线程

            public void run() {

 

                try {

 

                    for (int i = 0; i < 3; i++) {

 

                        int Avalue = OptimisticLock.value;//A获取的value

                        OptimisticLock.invoke(Avalue, "A");

 

                    }

 

 

 

                } catch (InterruptedException e) {

 

                    e.printStackTrace();

 

                }

 

            }

 

        }).start();

 

        new Thread(new Runnable() {//B线程

            public void run() {

 

                try {

 

                    for (int i = 0; i < 3; i++) {

 

                        int Bvalue = OptimisticLock.value;//B获取的value

                        OptimisticLock.invoke2(Bvalue, "B");

 

                    }

 

                } catch (InterruptedException e) {

 

                    e.printStackTrace();

 

                }

 

            }

 

        }).start();

 

    }

 

 

 

}

}

 

?

测试结果:

A:1

0:1B版本不一致,不执行

B:利用value运算,value=1

A:2

B:利用value运算,value=2

A:3

 

从结果中看出,B线程在执行的时候最后发现自己的value和执行前不一致,说明被A修改了,那么放弃了本次执行.

?

多运行几次发现了下面的结果:

A:1

B:利用value运算,value=0

A:2

1:2B版本不一致,不执行

A:3

B:利用value运算,value=2

 

从结果看A修改了value值,B却没有检查出来,利用错误的value值进行了操作. 为什么会这样呢?

这里就回到前面说的乐观锁是有一定的不安全性的,B在检查版本的时候A还没有修改,在B检查完版本后更新数据前(例子中的输出语句),A更改了value值,这时B执行更新数据(例子中的输出语句)就发生了与现存value不一致的现象.

?

针对这个问题,我觉得乐观锁要解决这个问题还需要在检查版本与更新数据这个操作的时候能够使用悲观锁,比如加上synchronized,让它在最后一步保证数据的一致性.这样既保证多线程都能同时执行,牺牲最后一点的性能去保证数据的一致.

?

补充

感谢评论中提出的cas方式解决乐观锁最后的安全问题,以前不知道cas(比较-交换)这个在java中的存在,找了找资料才发现java的concurrent包确实使用的cas实现乐观锁的数据同步问题.

下面是我对这两种方式的一点看法:

有两种方式来保证乐观锁最后同步数据保证它原子性的方法

1,CAS方式:Java非公开API类Unsafe实现的CAS(比较-交换),由C++编写的调用硬件操作内存,保证这个操作的原子性,concurrent包下很多乐观锁实现使用到这个类,但这个类不作为公开API使用,随时可能会被更改.我在本地测试了一下,确实不能够直接调用,源码中Unsafe是私有构造函数,只能通过getUnsafe方法获取单例,首先去掉eclipse的检查(非API的调用限制)限制以后,执行发现报 java.lang.SecurityException异常,源码中getUnsafe方法中执行访问检查,看来java不允许应用程序获取Unsafe类. 值得一提的是反射是可以得到这个类对象的.

2,加锁方式:利用Java提供的现有API来实现最后数据同步的原子性(用悲观锁).看似乐观锁最后还是用了悲观锁来保证安全,效率没有提高.实际上针对于大多数只执行不同步数据的情况,效率比悲观加锁整个方法要高.特别注意:针对一个对象的数据同步,悲观锁对这个对象加锁和乐观锁效率差不多,如果是多个需要同步数据的对象,乐观锁就比较方便.

?

扩展:利用反射获得Unsafe对象

第一步:去掉eclipse受限制的API检查:

将Windows->Preferences->Java-Complicer->Errors/Warnings->Deprecated and restricted API,中的Forbidden references(access rules)设置为Warning,Unsafe可以编译通过。

第二步:利用反射跳过安全检查获取Unsafe对象:

 

Class<Unsafe> s1  = (Class<Unsafe>) Class.forName("sun.misc.Unsafe");

 

    Field u1 = s1.getDeclaredField("theUnsafe");//获得Unsafe的theUnsafe属性

    u1.setAccessible(true);//获得private属性的可访问权限

    Unsafe unsafe1 = (Unsafe) u1.get(null);//获得Class中属性对应的值

    System.out.println(unsafe1.addressSize());//测试获取的Unsafe对象

 

    //或者

    Field u = Unsafe.class.getDeclaredField("theUnsafe");

 

    u.setAccessible(true);

 

    Unsafe unsafe = (Unsafe) u.get(null); 

 

    System.out.println(unsafe.addressSize());//测试获取的Unsafe对象

 

关于Unsafe的使用方法给个参考地址,平时用不到,我没有去深入看.

地址:Java Magic. Part 4: sun.misc.Unsafe?

?

http://www.cnblogs.com/qinggege/p/5284750.html

以上是关于乐观锁悲观锁的主要内容,如果未能解决你的问题,请参考以下文章

乐观锁与悲观锁

乐观锁--悲观锁

Java 乐观锁 悲观锁

悲观锁乐观锁

乐观锁和悲观锁

乐观锁和悲观锁