浅谈Java锁机制
1、悲观锁和乐观锁
我们可以将锁大体分为两类:
- 悲观锁
- 乐观锁
顾名思义,悲观锁总是假设最坏的情况,每次获取数据的时候都认为别的线程会修改,所以每次在拿数据的时候都会上锁,这样其它线程想要修改这个数据的时候都会被阻塞直到获取锁。比如MySQL
数据库中的表锁、行锁、读锁、写锁等,Java
中的synchronized
和ReentrantLock
等。
而乐观锁总是假设最好的情况,每次获取数据的时候都认为别的线程不会修改,所以并不会上锁,但是在修改数据的时候需要判断一下在此期间有没有别的线程修改过数据,如果没有修改过则正常修改,如果修改过则这次修改就是失败的。常见的乐观锁有版本号控制、CAS算法等。
2、悲观锁应用
案例如下:
public class LockDemo { static int count = 0; public static void main(String[] args) throws InterruptedException { List<Thread> threadList = new ArrayList<>(); for (int i = 0; i < 50; i++) { Thread thread = new Thread(() -> { for (int j = 0; j < 1000; ++j) { count++; } }); thread.start(); threadList.add(thread); } // 等待所有线程执行完毕 for (Thread thread : threadList) { thread.join(); } System.out.println(count); } }
在该程序中一共开启了50个线程,并在线程中对共享变量count
进行++操作,所以如果不发生线程安全问题,最终的结果应该是50000,但该程序中一定存在线程安全问题,运行结果为:
48634
若想解决线程安全问题,可以使用synchronized
关键字:
public class LockDemo { static int count = 0; public static void main(String[] args) throws InterruptedException { List<Thread> threadList = new ArrayList<>(); for (int i = 0; i < 50; i++) { Thread thread = new Thread(() -> { // 使用synchronized关键字解决线程安全问题 synchronized (LockDemo.class) { for (int j = 0; j < 1000; ++j) { count++; } } }); thread.start(); threadList.add(thread); } for (Thread thread : threadList) { thread.join(); } System.out.println(count); } }
将修改count
变量的操作使用synchronized
关键字包裹起来,这样当某个线程在进行++操作时,别的线程是无法同时进行++的,只能等待前一个线程执行完1000次后才能继续执行,这样便能保证最终的结果为50000。
使用ReentrantLock
也能够解决线程安全问题:
public class LockDemo { static int count = 0; public static void main(String[] args) throws InterruptedException { List<Thread> threadList = new ArrayList<>(); Lock lock = new ReentrantLock(); for (int i = 0; i < 50; i++) { Thread thread = new Thread(() -> { // 使用ReentrantLock关键字解决线程安全问题 lock.lock(); try { for (int j = 0; j < 1000; ++j) { count++; } } finally { lock.unlock(); } }); thread.start(); threadList.add(thread); } for (Thread thread : threadList) { thread.join(); } System.out.println(count); } }
这两种锁机制都是悲观锁的具体实现,不管其它线程是否会同时修改,它都直接上锁,保证了原子操作。
3、乐观锁应用
由于线程的调度是极其耗费操作系统资源的,所以,我们应该尽量避免线程在不断阻塞和唤醒中切换,由此产生了乐观锁。
在数据库表中,我们往往会设置一个version
字段,这就是乐观锁的体现,假设某个数据表的数据内容如下:
+----+------+----------+ ------- + | id | name | password | version | +----+------+----------+ ------- + | 1 | zs | 123456 | 1 | +----+------+----------+ ------- +
它是如何避免线程安全问题的呢?
假设此时有两个线程A、B想要修改这条数据,它们会执行如下的sql语句:
select version from e_user where name = 'zs'; update e_user set password = 'admin',version = version + 1 where name = 'zs' and version = 1;
首先两个线程均查询出zs用户的版本号为1,然后线程A先执行了更新操作,此时将用户的密码修改为了admin
,并将版本号加1,接着线程B执行更新操作,此时版本号已经为2了,所以更新肯定是失败的,由此,线程B就失败了,它只能重新去获取版本号再进行更新,这就是乐观锁,我们并没有对程序和数据库进行任何的加锁操作,但它仍然能够保证线程安全。
4、CAS
仍然以最开始做加法的程序为例,在Java中,我们还可以采用一种特殊的方式来实现它:
public class LockDemo { static AtomicInteger count = new AtomicInteger(0); public static void main(String[] args) throws InterruptedException { List<Thread> threadList = new ArrayList<>(); for (int i = 0; i < 50; i++) { Thread thread = new Thread(() -> { for (int j = 0; j < 1000; ++j) { // 使用AtomicInteger解决线程安全问题 count.incrementAndGet(); } }); thread.start(); threadList.add(thread); } for (Thread thread : threadList) { thread.join(); } System.out.println(count); } }
为何使用AtomicInteger
类就能够解决线程安全问题呢?
我们来查看一下源码:
public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; }
当count
调用incrementAndGet()
方法时,实际上调用的是UnSafe
类的getAndAddInt()
方法:
public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }
getAndAddInt()
方法中有一个循环,关键的代码就在这里,我们假设线程A此时进入了该方法,此时var1
即为AtomicInteger
对象(初始值为0),var2
的值为12(这是一个内存偏移量,我们可以不用关心),var4的值为1(准备对count进行加1操作)。
首先通过AtomicInteger
对象和内存偏移量即可得到主存中的数据值:
var5 = this.getIntVolatile(var1, var2);
获取到var5的值为0,然后程序会进行判断:
!this.compareAndSwapInt(var1, var2, var5, var5 + var4)
compareAndSwapInt()
是一个本地方法,它的作用是比较并交换,即:判断var1的值与主存中取出的var5的值是否相同,此时肯定是相同的,所以会将var5+var4
的值赋值给var1,并返回true
,对true
取反为false
,所以循环就结束了,最终方法返回1。
这是一切正常的运行流程,然而当发生并发时,处理情况就不太一样了,假设此时线程A执行到了getAndAddInt()
方法:
public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }
线程A此时获取到var1的值为0(var1即为共享变量AtomicInteger
),当线程A正准备执行下去时,线程B抢先执行了,线程B此时获取到var1的值为0,var5的值为0,比较成功,此时var1的值就变为1;这时候轮到线程A执行了,它获取var5的值为1,此时var1的值不等于var5的值,此次加1操作就会失败,并重新进入循环,此时var1的值已经发生了变化,此时重新获取var5
的值也为1,比较成功,所以将var1的值加1变为2,若是在获取var5之前别的线程又修改了主存中var1的值,则本次操作又会失败,程序重新进入循环。
这就是利用自旋的方式来实现一个乐观锁,因为它没有加锁,所以省下了线程调度的资源,但也要避免程序一直自旋的情况发生。
5、手写一个自旋锁
public class LockDemo { private AtomicReference<Thread> atomicReference = new AtomicReference<>(); public void lock() { // 获取当前线程对象 Thread thread = Thread.currentThread(); // 自旋等待 while (!atomicReference.compareAndSet(null, thread)) { } } public void unlock() { // 获取当前线程对象 Thread thread = Thread.currentThread(); atomicReference.compareAndSet(thread, null); } static int count = 0; public static void main(String[] args) throws InterruptedException { LockDemo lockDemo = new LockDemo(); List<Thread> threadList = new ArrayList<>(); for (int i = 0; i < 50; i++) { Thread thread = new Thread(() -> { lockDemo.lock(); for (int j = 0; j < 1000; j++) { count++; } lockDemo.unlock(); }); thread.start(); threadList.add(thread); } // 等待线程执行完毕 for (Thread thread : threadList) { thread.join(); } System.out.println(count); } }
使用CAS的原理可以轻松地实现一个自旋锁,首先,AtomicReference
中的初始值一定为null
,所以第一个线程在调用lock()方法后会成功将当前线程的对象放入AtomicReference
,此时若是别的线程调用lock()
方法,会因为该线程对象与AtomicReference
中的对象不同而陷入循环的等待中,直到第一个线程执行完++操作,调用了unlock()
方法,该线程才会将AtomicReference
值置为null
,此时别的线程就可以跳出循环了。
通过CAS机制,我们能够在不添加锁的情况下模拟出加锁的效果,但它的缺点也是显而易见的:
- 循环等待占用CPU资源
- 只能保证一个变量的原子操作
- 会产生ABA问题
到此这篇关于浅谈Java锁机制的文章就介绍到这了,更多相关Java锁机制内容请搜索猪先飞以前的文章或继续浏览下面的相关文章希望大家以后多多支持猪先飞!
相关文章
- 这篇文章主要介绍了如何利用java语言实现经典《复杂迷宫》游戏,文中采用了swing技术进行了界面化处理,感兴趣的小伙伴可以动手试一试...2022-02-01
java 运行报错has been compiled by a more recent version of the Java Runtime
java 运行报错has been compiled by a more recent version of the Java Runtime (class file version 54.0)...2021-04-01- 这篇文章主要介绍了在java中获取List集合中最大的日期时间操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-08-15
- 这篇文章主要介绍了教你怎么用Java获取国家法定节假日,文中有非常详细的代码示例,对正在学习java的小伙伴们有非常好的帮助,需要的朋友可以参考下...2021-04-23
- 这篇文章主要介绍了Java如何发起http请求的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-03-31
- 说起C#和Java这两门语言(语法,数据类型 等),个人以为,大概有90%以上的相似,甚至可以认为几乎一样。但是在工作中,我也发现了一些细微的差别...2020-06-25
- 这篇文章主要介绍了解决Java处理HTTP请求超时的问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-03-29
- 这篇文章主要介绍了java 判断两个时间段是否重叠的案例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-08-15
- 这篇文章主要介绍了超简洁java实现双色球若干注随机号码生成(实例代码),本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2021-04-02
- 这篇文章主要介绍了Java生成随机姓名、性别和年龄的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-10-01
java 画pdf用itext调整表格宽度、自定义各个列宽的方法
这篇文章主要介绍了java 画pdf用itext调整表格宽度、自定义各个列宽的方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-01-31- 这篇文章主要介绍了java正则表达式判断前端参数修改表中另一个字段的值,需要的朋友可以参考下...2021-05-07
Java使用ScriptEngine动态执行代码(附Java几种动态执行代码比较)
这篇文章主要介绍了Java使用ScriptEngine动态执行代码,并且分享Java几种动态执行代码比较,需要的朋友可以参考下...2021-04-15- 这篇文章主要介绍了Java开发实现人机猜拳游戏,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2020-08-03
- 这篇文章主要介绍了Java List集合返回值去掉中括号('[ ]')的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-08-29
Java中lombok的@Builder注解的解析与简单使用详解
这篇文章主要介绍了Java中lombok的@Builder注解的解析与简单使用,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2021-01-06- 下面小编就为大家带来一篇java中String类型变量的赋值问题介绍。小编觉得挺不错的。现在分享给大家,给大家一个参考。...2016-03-28
Java 8 Stream 的终极技巧——Collectors 功能与操作方法详解
这篇文章主要介绍了Java 8 Stream Collectors 功能与操作方法,结合实例形式详细分析了Java 8 Stream Collectors 功能、操作方法及相关注意事项,需要的朋友可以参考下...2020-05-20- 这篇文章主要介绍了Java线程池中的各个参数如何合理设置操作,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教...2021-06-19
- 在Java中,我们可以利用多线程来最大化地压榨CPU多核计算的能力,下面这篇文章主要给大家介绍了关于java中多线程与线程池基本使用的相关资料,需要的朋友可以参考下...2021-09-13