关于java:乐观锁这么重要看我们如何2步手动实现极其重要面试必问

5次阅读

共计 4085 个字符,预计需要花费 11 分钟才能阅读完成。

java 多线程中的锁分类多种多样,其中有一种次要的分类形式就是乐观和乐观进行划分的。这篇文章次要介绍如何本人手写一个乐观锁代码。不过文章为了保障完整性,会从根底开始介绍。

一、乐观锁概念

说是写乐观锁的概念,然而通常乐观锁和乐观锁的概念都要一块写。比照着来才更有意义。

1、乐观锁概念

乐观锁:总是假如最坏的状况,每次去拿数据的时候都认为他人会批改,所以每次在拿数据的时候都会上锁,这样他人想拿这个数据就会阻塞,直到它拿到锁。

比方 synchronized 就是一个 乐观锁 ,当一个办法应用了 synchronized 润饰时,其余的线程想要拿到这个办法就须要 等到别的线程开释

数据库外面也用到了这种乐观锁的机制。比方行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。这样其余的线程就不能同步操作,必须要等到他开释才能够。

2、乐观锁概念

乐观锁:总是假如最好的状况,每次去拿数据的时候都认为他人不会批改,所以不会上锁,只在更新的时候会判断一下 在此期间 他人有没有去更新这个数据。

留神 “在此期间” 的含意是 拿到数据到更新数据 的这段时间。因为没有加锁,所以别的线程可能会更改。还有一点那就是乐观锁其实是不加锁的。

理解了概念之后,再看个例子:java 中的 Atomic 包下的类就是应用了乐观锁机制。咱们挑出来一个看看官网是如何实现的,而后依照这样的实现机制咱们本人就能够实现。

3、乐观锁实现案例

java 并发机制中次要有三个个性须要咱们去思考,原子性、可见性和有序性。AtomicInteger 的作用就是为了保障原子性。就是用这个演示:

public class Test {
    // 一个变量 a
    private static volatile int a = 0;
    public static void main(String[] args) {Test test = new Test();
        Thread[] threads = new Thread[5];
        // 定义 5 个线程,每个线程加 10
        for (int i = 0; i < 5; i++) {threads[i] = new Thread(() -> {
                try {for (int j = 0; j < 10; j++) {System.out.println(a++);
                        Thread.sleep(500);
                    }
                } catch (Exception e) {e.printStackTrace();
                }
            });
            threads[i].start();}
    }
}

这个例子很简略:咱们定义了一个 变量 a,初始值是 0 ,而后应用 5 个线程去减少 每个线程减少 10,按情理来说 5 个线程一共减少了 50,运行一下不到 50,起因就在于外面那个加一操作:a++;

对于 a ++ 的操作,其实能够合成为 3 个步骤。

**(1)从主存中读取 a 的值 **

**(2)对 a 进行加 1 操作 **

**(3)把 a 从新刷新到主存 **

线程 1 把 a 进行了加 1 操作 ,然而还 没来得及重刷入到主存 线程 2 就读取了 ,此时线程 2 读取的必定是 没来及刷入内存的旧值。这才造成了谬误。解决办法就能够应用AtomicInteger

public class Test3 {
    // 应用 AtomicInteger 定义 a
    static AtomicInteger a = new AtomicInteger();
    public static void main(String[] args) {Test3 test = new Test3();
        Thread[] threads = new Thread[5];
        for (int i = 0; i < 5; i++) {threads[i] = new Thread(() -> {
                try {for (int j = 0; j < 10; j++) {
                        // 应用 getAndIncrement 函数进行自增操作
                        System.out.println(a.incrementAndGet());        
                        Thread.sleep(500);
                    }
                } catch (Exception e) {e.printStackTrace();
                }
            });
            threads[i].start();}
    }
}

当初咱们应用 AtomicInteger 定义 a,而后应用 incrementAndGet 进行自增操作,最初的后果就总是 50 了。咱们来剖析一下:

4、乐观锁案例剖析

想要找进去答案咱们还要从 AtomicInteger 的 incrementAndGet 办法说起。因为这个办法实现了锁一样的性能。这里应用的是 jdk1.8 的版本,不同的版本会有出入。

/**
* Atomically increments by one the current value.
* @return the updated value
*/
public final int incrementAndGet() {return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

这里咱们能够看到自增操作次要是应用了 unsafegetAndAddInt办法。因为不是专门介绍 AtomicInteger,所以不会对源码进行具体的剖析。

  • Unsafe:Unsafe 是位于 sun.misc 包下的一个类,Unsafe 类使 Java 语言领有了相似 C 语言指针一样操作内存空间的能力。也就是说咱们间接操作了内存空间进行了加 1 操作。
  • unsafe.getAndAddInt:其外部又调用了 Unsafe.compareAndSwapInt 办法。这个机制叫做 CAS 机制,

CAS 即比拟并替换,实现并发算法时罕用到的一种技术。CAS 操作蕴含三个操作数——内存地位、预期原值及新值。执行 CAS 操作的时候,将内存地位的值与预期原值比拟,如果相匹配,那么处理器会主动将该地位值更新为新值,否则,处理器不做任何操作。

咱们应用一个例子来解释置信你会更加的分明。

意思是,你老爸想让你娶张三,等到真正结婚的那天,如果你老爸料想的新娘子 (张三) 和你实在获得新娘子一样,就给你办婚礼,否则不办婚礼。

然而这样的 CAS 机制会带来一个比拟常见的问题。那就是 ABA 问题,你在桌子上放了 100 元,回来还是 100,然而在你走的那段时间,他人曾经拿走了 100 块,起初又还回来了。这就是 ABA 问题。

ABA 问题看似没问题,其实在金融行业会造成隐患,你会容忍他人拿走你的 100 万再还回来,而你却毫不知情嘛?

解决 ABA 问题的思路就是给数据加上版本号。

5、乐观锁思维

OK,下面说了这么多,其实就是想说一句话那就是乐观锁能够由 CAS 机制 + 版本机制来实现。

  • CAS 机制:当多个线程尝试应用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败。CAS 无效地阐明了“我认为地位 V 应该蕴含值 A;如果蕴含该值,则将 B 放到这个地位;否则,不要更改该地位,只通知我这个地位当初的值即可“。
  • 版本机制:CAS 机制保障了在更新数据的时候没有被批改为其余数据的同步机制,版本机制就保障了没有被批改过的同步机制(意思是下面的 ABA 问题)。

基于这个思维咱们就能够实现一个乐观锁。上面咱们写一下代码。这个代码在我本人电脑上亲测通过。

二、实现一个乐观锁

第一步:定义咱们要操作的数据

public class Data {
    // 数据版本号
    static int version = 1;
    // 实在数据
    static String data = "java 的架构师技术栈";
    public static int getVersion(){return version;}
    public static void updateVersion(){version = version + 1;}
}

第二步:定义一个乐观锁

public class OptimThread extends Thread { 
    public int version;
    public String data;
    // 构造方法和 getter、setter 办法
    public void run() {
        // 1. 读数据
        String text = Data.data;
        println("线程"+ getName() + ",取得的数据版本号为:" + Data.getVersion());
        println("线程"+ getName() + ",预期的数据版本号为:" + getVersion());
        System.out.println("线程"+ getName()+"读数据实现 =========data =" + text);
        // 2. 写数据:预期的版本号和数据版本号统一,那就更新
        if(Data.getVersion() == getVersion()){println("线程" + getName() + ",版本号为:" + version + ",正在操作数据");
            synchronized(OptimThread.class){if(Data.getVersion() == this.version){
                    Data.data = this.data;
                    Data.updateVersion();
                    System.out.println("线程" + getName() + "写数据实现 =========data =" + this.data);
                    return ;
                }
            }
        }else{
             // 3. 版本号不正确的线程,须要从新读取,从新执行
            println("线程"+ getName() + ",取得的数据版本号为:" + Data.getVersion());
            println("线程"+ getName() + ",预期的版本号为:" + getVersion());
            System.err.println("线程"+ getName() + ",须要从新执行。==============");
        }  
    }
}

第三步:测试

public class Test {public static void main(String[] args) {for (int i = 1; i <= 2; i++) {new OptimThread(String.valueOf(i), 1, "fdd").start();}
    }
}

定义了两个线程,而后进行读写操作

第四步:输入后果

这个后果能够看到在读数据的时候只有发现没有变动即可,然而更新数据的时候要判断以后的版本号和预期的版本号是否统一,如果统一那就更新,如果不统一,那就阐明更新失败。

OK,明天的文章先写到这。如果问题还请批评指正。

正文完
 0