【Java】乐观锁这么重要,看我们如何2步手动实现(极其重要,面试必问)

乐观锁这么重要,看我们如何2步手动实现(极其重要,面试必问)

愚公要移山发布于 今天 08:13

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问题。

【Java】乐观锁这么重要,看我们如何2步手动实现(极其重要,面试必问)

解决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();

}

}

}

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

第四步:输出结果

这个结果可以看到在读数据的时候只要发现没有变化即可,但是更新数据的时候要判断当前的版本号和预期的版本号是否一致,如果一致那就更新,如果不一致,那就说明更新失败。

【Java】乐观锁这么重要,看我们如何2步手动实现(极其重要,面试必问)

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

java程序员

阅读 30发布于 今天 08:13

本作品系原创,采用《署名-非商业性使用-禁止演绎 4.0 国际》许可协议


java小白到架构师系列

avatar

愚公要移山

48 声望

2 粉丝

0 条评论

得票时间

avatar

愚公要移山

48 声望

2 粉丝

宣传栏

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问题。

【Java】乐观锁这么重要,看我们如何2步手动实现(极其重要,面试必问)

解决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();

}

}

}

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

第四步:输出结果

这个结果可以看到在读数据的时候只要发现没有变化即可,但是更新数据的时候要判断当前的版本号和预期的版本号是否一致,如果一致那就更新,如果不一致,那就说明更新失败。

【Java】乐观锁这么重要,看我们如何2步手动实现(极其重要,面试必问)

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

以上是 【Java】乐观锁这么重要,看我们如何2步手动实现(极其重要,面试必问) 的全部内容, 来源链接: utcz.com/a/107398.html

回到顶部