浅谈Java锁机制
作者:随身电源 发布时间:2022-01-09 00:58:56
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问题
来源:https://juejin.cn/post/7013272218679541773


猜你喜欢
- 介绍Java中的享元模式(Flyweight Pattern)是一种结构型设计模式,旨在通过共享尽可能多的对象来减少内存占用和提高性能.Ja
- 前言App安全非常重要,尤其是数据安全。但是我们知道通过Charles等工具可以对App的网络请求进行抓包,如果我们的数据没有进行加密,这样
- Spring Cloud Zuul 集成Swagger1.准备服务注册中心eureka-server2.创建微服务swagger-servi
- 首先 下载 jedis.jar包然后再 工程设置里面找到Libraries,点击+。添加下载好的jedis.jar包。点击OK退出即可创建J
- 1、字符数组的定义与初始化字符数组的初始化,最容易理解的方式就是逐个字符赋给数组中各元素。char str[10]={ 'I'
- 当把窗体的FormBorderStyle属性设为None后会导致边框没了,结果窗体无法用鼠标拖动、最大、最下化和关闭……下面解决窗体可拖动问
- IntroC# 9 中进一步增强了模式匹配的用法,使得模式匹配更为强大,我们一起来了解一下吧SampleC# 9 中增强了模式匹配的用法,增
- 最近在学习DataBinding的使用,中间遇到了不少的坑,记录以下,帮助以后学习DataBinding的朋友。有一个 ViewModel
- 在程序设计中,进行异常处理是非常关键和重要的一部分。一个程序的异常处理框架的好坏直接影响到整个项目的代码质量以及后期维护成本和难度。试想一下
- 本文实例讲述了C#实现绑定Combobox的方法。分享给大家供大家参考。具体实现方法如下:public class StaticVariab
- MainActivity如下: package cn.testnbackpressed; import android.os.Bundle;
- 前面文章介绍了Android利用麦克风采集并显示模拟信号的实现方法,这种采集手段适用于无IO控制、单纯读取信号的情况。如果传感器本身需要包含
- 本文实例讲述了Java实现的并发任务处理方法。分享给大家供大家参考,具体如下:public void init() { super.init
- 在Web的应用方面有js的插件实现自动完成(或叫智能提示)功能,但在WinForm窗体应用方面就没那么好了。TextBox控件本身是提供了一
- 首先给大家介绍一文件的上传 实体类import java.sql.Timestamp; /** * * @Decription 文件上传实体
- RocketMq消息处理RocketMq消息处理整个流程如下:本系列RocketMQ4.8注释github地址,希望对大家有所帮助,要是觉得
- 1,带Tomcat的打包方式1.1, 在pom.xml文件添加以下配置(目的:自定main入口和跳过Junit代码)<build>
- 1.首先解释一下什么是方法重载?方法重载是指在同一个类中方法同名,参数不同,调用时根据实参的形式,选择与他匹配的方法执行操作的一种技术。这里
- 觉得好有点帮助就顶一下啦。socke编程,支持多客户端,多线程操作避免界面卡死。开启socketprivate void button1_C
- 本文为大家分享了类似微信朋友圈,点击+号图片,可以加图片功能,供大家参考,具体内容如下xml:<?xml version="