Java有锁并发、无锁并发和CAS实例分析
发表于:2025-02-07 作者:千家信息网编辑
千家信息网最后更新 2025年02月07日,这篇文章主要介绍"Java有锁并发、无锁并发和CAS实例分析"的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇"Java有锁并发、无锁并发和CAS实例分析"文章能帮
千家信息网最后更新 2025年02月07日Java有锁并发、无锁并发和CAS实例分析对于大多数程序员(当然我也基本上是其中一员),并发编程几乎就等价于给相关数据结构加上一个锁(Mutex)。比如如果我们需要一个支持并发的栈,那最简单的方法就是给一个单线程的栈加上锁
这篇文章主要介绍"Java有锁并发、无锁并发和CAS实例分析"的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇"Java有锁并发、无锁并发和CAS实例分析"文章能帮助大家解决问题。
有锁并发
std::sync::Mutex
。(加上Arc是为了能让多个线程都拥有栈的所有权)
use std::sync::{Mutex, Arc};
#[derive(Clone)]
struct ConcurrentStack {
inner: Arc>>,
}
impl ConcurrentStack {
pub fn new() -> Self {
ConcurrentStack {
inner: Arc::new(Mutex::new(Vec::new())),
}
}
pub fn push(&self, data: T) {
let mut inner = self.inner.lock().unwrap();
(*inner).push(data);
}
pub fn pop(&self) -> Option {
let mut inner = self.inner.lock().unwrap();
(*inner).pop()
}
}
这样做的好处是显而易见的:代码写起来十分方便,毕竟和单线程版本的几乎一样。只需要按照单线程的版本写完,然后给数据结构加上锁,然后在必要的时候获取和释放(在Rust中基本上是自动的)锁即可。
那么问题是什么呢?首先不谈你可能会忘记获取和释放锁(这一点要感谢Rust,在Rust中几乎不可能发生),你可能会面临死锁的问题(哲学家就餐问题)。然后也不谈一些低优先级的任务可能会长期抢占高优先级任务的资源(因为锁是第一位的),当线程数量比较大的时候,大部分的时间都被用在了同步上(等待锁能被获取),性能就会变得非常差。考虑一个大量读取而偶尔写入的并发数据库,如果用锁去处理,即使数据库没有任何更新,每两个读之间也需要做一次同步,代价太大!
无锁并发
于是,大量计算机科学家和程序员就把目光转向了无锁(lock-free)并发。无锁对象:如果一个共享对象保证了无论其他线程做何种操作,总有一些线程会在有限的系统操作步骤后完成一个对其的操作 Her91 。也就是说,至少有一个线程对其操作会取得成效。使用锁的并发明显就不属于这一范畴:如果获取了锁的线程被延迟,那么这段时间里没有任何线程能够完成任何操作。极端情况下如果出现了死锁,那么没有任何线程能够完成任何操作。
CAS(compare and swap)原语
那大家可能会好奇,无锁并发要怎么实现呢?有没有例子呢?在此之前让我们先看一下一个公认的在无锁并发中非常重要的原子原语:CAS。CAS的过程是用指定值去比较一储存值,只有当他们相同时,才会修改储存值为新的指定值。CAS是个原子操作(由处理器支持,比如x86的compare and exchange (CMPXCHG)),该原子性保证了如果其他线程已经改变了储存值,那么写入就会失败。Rust标准库中的 std::sync::atomic
中的类型就提供了CAS操作,比如原子指针 std::sync::atomic::AtomicPtr
pub fn compare_and_swap(
&self,
current: *mut T,
new: *mut T,
order: Ordering
) -> *mut T
(在这里,不用纠结ordering是什么,也就是说请尽管忽略忽略 Acquire
, Release
, Relaxed
)
无锁栈(天真版)
#![feature(box_raw)]
use std::ptr::{self, null_mut};
use std::sync::atomic::AtomicPtr;
use std::sync::atomic::Ordering::{Relaxed, Release, Acquire};
pub struct Stack {
head: AtomicPtr>,
}
struct Node {
data: T,
next: *mut Node,
}
impl Stack {
pub fn new() -> Stack {
Stack {
head: AtomicPtr::new(null_mut()),
}
}
pub fn pop(&self) -> Option {
loop {
// 快照
let head = self.head.load(Acquire);
// 如果栈为空
if head == null_mut() {
return None
} else {
let next = unsafe { (*head).next };
// 如果现状较快照并没有发生改变
if self.head.compare_and_swap(head, next, Release) == head {
// 读取内容并返回
return Some(unsafe { ptr::read(&(*head).data) })
}
}
}
}
pub fn push(&self, t: T) {
// 创建node并转化为*mut指针
let n = Box::into_raw(Box::new(Node {
data: t,
next: null_mut(),
}));
loop {
// 快照
let head = self.head.load(Relaxed);
// 基于快照更新node
unsafe { (*n).next = head; }
// 如果在此期间,快照仍然没有过时
if self.head.compare_and_swap(head, n, Release) == head {
break
}
}
}
}
我们可以看到,无论是pop还是push思路都是一样的:先在快照上pop或者是push,然后尝试用CAS替换原来的数据。如果快照和数据一致,说明这一期间数据没有被写操作过,于是更新成功。如果不一致,说明在此期间有其他线程修改过数据,那么一切从头再来。这就是一个无锁的栈。似乎一切都已经大功告成了!
内存释放
确实你可能已经大功告成了,但前提是你在写Java,或者是其他有GC的语言。现在的问题在于,在Rust这种没有GC的语言中,pop中
return Some(unsafe { ptr::read(&(*head).data) })
并没有人去释放 head
,这是一个内存泄漏!哎,看来无锁并发好不容易呢。
关于"Java有锁并发、无锁并发和CAS实例分析"的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识,可以关注行业资讯频道,小编每天都会为大家更新不同的知识点。
线程
数据
快照
问题
原子
更新
实例
实例分析
分析
知识
一致
大功告成
也就是
也就是说
任务
优先级
内存
内容
原语
大功
数据库的安全要保护哪些东西
数据库安全各自的含义是什么
生产安全数据库录入
数据库的安全性及管理
数据库安全策略包含哪些
海淀数据库安全审计系统
建立农村房屋安全信息数据库
易用的数据库客户端支持安全管理
连接数据库失败ssl安全错误
数据库的锁怎样保障安全
校园网络安全的预防
三维工艺设计软件开发
网络安全认识的读后感
实验室安全管理数据库
软件开发模型增量模型本质
数据库改表字段内容语句
哈佛数据库
数据库gpnpd.bin
广州智能消防软件开发方案
无服务器bt
ftp服务器管理用户名密码
多级 数据库
个人nas服务器是什么
虚拟服务器网页用不了快捷键
幼儿园服务器数填什么
6月1日网络安全法实施
服务器主机打游戏好吗
主根服务器 国家安全
显示服务器有问题怎么办
跨文化交际数据库
黑龙江北斗授时模块服务器云空间
牌类游戏软件开发
云服务器如何开发票
服务器防御查询
酷开网络技术有限公司
安卓开发桌面软件开发
计算机服务器管理器
网络安全和国家的关系
教你免费获取服务器
sql数据库外网访问6