在并发编程中,多线程的共享资源的修改往往会造成严重的线程安全问题,解决这种问题简单暴力的方式就是加锁,加锁的方式使用简单易理解,但常常会因为阻塞导致性能问题
有没有可能做到无锁还保证线程安全呐?这得看具体情况。得益于CAS技术,有很多情况下我们可以做到不使用锁也能保证线程的安全
比如今天我最近遇到的场景如下(由于场景比较复杂,用一个模拟简化一下)
假设有一个商店,背后有一个工厂可以生产商品,商店也可以有用户来购买商品,为了简化,假设工厂只能生产一个商品、而用户也只能买一个商品
需求如下:
简简单单的一个需求,在多线程环境下就会出现隐患
先不考虑多线程情况,这个代码很好写,我们用一个ready变量标识是否生产完成,用一个unSupply变量标识是否有欠用户一个商品,代码如下
public class SerialShop {
private volatile boolean ready; // 商品生产完成
private volatile boolean unSupply; // 是否欠用户一个商品
public volatile boolean done; // 交易完成
public void send() { // 发货
System.out.println("send to user");
done = true;
}
public void buy() {
if (ready) { // 商品生产完成
send(); // 直接发货
return;
}
this.unSupply = true; // 没有准备好则填写一个欠货单
}
public void ready() {
this.ready = true; // 标识商品准备完成
if (this.unSupply) { // 如果发现有欠货单
send(); // 给用户发货
}
}
}
这时,我们简单跑一下
@Test
public void buyBeforeReady() {
buy();
ready();
}
@Test
public void buyAfterReady() {
ready();
buy();
}
结果无论先购买再生产完,还是生产完再购买,最终都会走到send方法,完成交易
上面的代码虽然简单,但在多线程下就会出现问题,用实际的情形描述一下
画个时序图描述一下这个情景

因为多线程无法保证有序性,所以这种情况出现的概率很大,而一旦出现就是严重问题
用代码模拟一下这个场景:
public class UnsafeShop {
private volatile boolean ready; // 商品生产完成
private volatile boolean unSupply; // 欠用户
public volatile boolean done; // 交易完成
public void send() {
System.out.println("send to user");
done = true;
}
public void buy() throws InterruptedException {
if (ready) { // 准备好了
send(); // 直接发货
return;
}
Thread.sleep(100); // 这里手动降低线程速度,为了重现场景
this.unSupply = true; // 没有准备好则填写一个欠货单
}
public void ready() throws InterruptedException {
this.ready = true; // 标识商品准备完成
if (this.unSupply) { // 如果发现有欠货单
send(); // 给用户发货
}
}
@Test
public void unsafe() throws InterruptedException {
// 用户购买
new Thread(() -> {
try {
buy();
} catch (InterruptedException e) {
}
}).start();
Thread.sleep(50);
// 工厂生产
new Thread(() -> {
try {
ready();
} catch (InterruptedException e) {
}
}).start();
while (true) ;
}
}
执行结果:并没有走到send方法(上面的代码通过sleep来降低线程的执行速度,是为了100%呈现错误,实际中就算不写sleep也有可能出现这种情况)
那么如何避免上面的问题呐,最简单暴力的方式就是加锁
上面的问题之所以出现,是因为用户查看是否商品已准备和标识欠货的两步操作没有原子性,导致中间的过程可能被工厂的线程快速完成所有动作和判断
实际情形下我们可以这么解决问题:在接纳用户的时候,如果工厂来人送货,让工厂的人在外面等着,等用户把该做的都做了,工厂的人再进来标识准备完毕并送货
用代码模拟一下这个解决方案
public class BlockShop {
private volatile boolean ready; // 商品生产完成
private volatile boolean unSupply; // 欠用户
public volatile boolean done; // 交易完成
public void send() {
System.out.println("send to user");
done = true;
}
public void buy() throws InterruptedException {
synchronized (this) { // 接纳用户时不让工厂人进入
if (ready) { // 准备好了
send(); // 直接发货
return;
}
Thread.sleep(100);
this.unSupply = true; // 没有准备好则填写一个欠货单
}
}
public void ready() throws InterruptedException {
synchronized (this) { // 接纳用户时不让工厂人进入
this.ready = true; // 标识商品准备完成
}
if (this.unSupply) { // 如果发现有欠货单
send(); // 给用户发货
}
}
@Test
public void block() throws InterruptedException {
// 用户购买
new Thread(() -> {
try {
buy();
} catch (InterruptedException e) {
}
}).start();
Thread.sleep(50);
// 工厂生产
new Thread(() -> {
try {
ready();
} catch (InterruptedException e) {
}
}).start();
while (true) ;
}
}
这时,不会在出现上述问题,彻底的解决了线程安全
而从解决问题的实际场景来看,这种解决问题的方法在现实中简直是弱智,工厂的人就在外面傻等,这就是阻塞,会降低代码的执行速度
当然以上场景的阻塞实际其实很小,但个人认为锁这东西能不用尽量不用,在场景复杂的时候阻塞的弱点会更加凸显出来
在这种场景下,能不能不使用锁来达到线程安全的效果呐?
我想了很多办法,比如buy时先标识已欠货,再去判断是否已准备,或者标识完已欠货再去看一眼是否已准备好,但都行不通,原因就是无法保证原子性,也无法保证多线程的有序性
冥思苦想后,想到一个解决方案:工厂人员上门后第一件事就是把欠货单撕了!
此时欠货单有三个状态:初始状态/被撕了/填写完,我们用商品的库存标识为:0/1/-1(欠用户一台)
private volatile int stock = 0;
而stock==1也说明货已到,所以不需要ready变量了
最终代码如下
public class NoBlockShop {
private volatile int stock = 0; // 库存量 -1代表亏欠用户一台
public volatile boolean done; // 交易完成
final AtomicIntegerFieldUpdater<NoBlockShop> STATUS_UPDATER =
AtomicIntegerFieldUpdater.newUpdater(NoBlockShop.class, "stock");
public void send() {
done = true;
}
public void buy() throws InterruptedException {
for (;;) {
if (stock ==1) { // 有货
send(); // 直接发货
return;
}
if (STATUS_UPDATER.compareAndSet(this, 0, -1)) {// 标识欠货,如果失败说明库存有变动,再回头查看一下
return;
}
}
}
public void ready() throws InterruptedException {
if (!STATUS_UPDATER.compareAndSet(this, 0, 1)) { // 标识有库存
send(); // 如果失败代表用户来过了,直接发货
}
}
}
不仅解决了线程安全,还无锁(也可以称作乐观锁),并且代码还简洁了,CAS是真香
测试一下线程安全,代码如下
ExecutorService executorService = Executors.newFixedThreadPool(20);
List<NoBlockShop> shops = new ArrayList<>();
for (int i=0;i<100000;i++) {
NoBlockShop shop = new NoBlockShop();
shops.add(shop);
executorService.execute(()->{
try {
shop.buy();
} catch (InterruptedException e) {}
});
executorService.execute(()->{
try {
shop.ready();
} catch (InterruptedException e) {}
});
}
Thread.sleep(500);
System.out.println(shops.stream().filter(v->!v.done).count());
初始化10万个shop,然后用不同线程分别buy和ready,最终输出没交易的shop个数
UnsafeShop(初版),一般结果都不是0,且每次执行都不一样,说明有的shop对象出现线程安全问题BlockShop(锁版),结果是0,说明线程安全NoBlockShop(CAS版),结果是0,说明也实现了线程安全根据这个可以继续改造一下让商店,让工厂可以不断生产商品,用户也能不断购买,依然使用stock,为正代表有n个库存,为负代表欠用户n个商品,并且可以一次性购买/生产多个,不再是一次性买卖了,代码如下
public class NoBlockSupermarket {
private volatile int stock = 0; // 当前库存数量,为负代表欠货
public AtomicInteger deals = new AtomicInteger(0); // 交易量,测试用
final AtomicIntegerFieldUpdater<NoBlockSupermarket> STOCK_UPDATER =
AtomicIntegerFieldUpdater.newUpdater(NoBlockSupermarket.class, "stock");
public void send() {
deals.incrementAndGet(); // 增加成交数,测试用
}
public void buy(int n) {
int e = 0; // 已买数量
while (e != n) {
int stock = this.stock;
if (!STOCK_UPDATER.compareAndSet(this, stock, stock - 1)) { // 库存-1
continue;
}
if (stock > 0) { // 有货
send();
}
e++;
}
}
public void supply(int n) {
int e = 0; // 已处理数量
while (e != n) {
int stock = this.stock;
if (!STOCK_UPDATER.compareAndSet(this, stock, stock + 1)) {// 库存+1
continue;
}
if (stock < 0) { // 欠货
send();
}
e++;
}
}
}
使用CAS可以避免多线程情况下的阻塞,但也并不是所有场景都适用,在冲突严重的情况下乐观锁性能可能反而不如悲观锁
我所举例的场景其实就是一个典型的发布订阅模式的场景,冲突不高的情况下用乐观锁的方式替换悲观锁,会达到性能上质的飞跃
我正在使用Ruby2.1.1和Rails4.1.0.rc1。当执行railsc时,它被锁定了。使用Ctrl-C停止,我得到以下错误日志:~/.rvm/gems/ruby-2.1.1/gems/spring-1.1.2/lib/spring/client/run.rb:47:in`gets':Interruptfrom~/.rvm/gems/ruby-2.1.1/gems/spring-1.1.2/lib/spring/client/run.rb:47:in`verify_server_version'from~/.rvm/gems/ruby-2.1.1/gems/spring-1.1.
有这样的事吗?我想在Ruby程序中使用它。 最佳答案 试试这个http://csl.sublevel3.org/jp2a/此外,Imagemagick可能还有一些东西 关于ruby-是否有将图像文件转换为ASCII艺术的命令行程序或库?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/6510445/
在我的路线文件中我有:match'graphs/(:id(/:action))'=>'graphs#(:action)'如果是GET请求(工作)或POST请求(不工作),我想匹配它我知道我可以使用以下方法在资源中声明POST请求:post'/'=>:show,:on=>:member但是我怎样才能为比赛做到这一点呢?谢谢。 最佳答案 如果你同时想要POST和GETmatch'graphs/(:id(/:action))'=>'graphs#(:action)',:via=>[:get,:post]编辑默认值可以设置如下match'g
我一直在研究ruby的并行/异步处理能力,并阅读了许多文章和博客文章。我查看了EventMachine、Fibers、Revactor、Reia等。不幸的是,我无法为这个非常简单的用例找到简单、有效(且非IO阻塞)的解决方案:File.open('somelogfile.txt')do|file|whileline=file.gets#(R)ReadfromIOline=process_line(line)#(P)Processthelinewrite_to_db(line)#(W)WritetheoutputtosomeIO(DBorfile)endend你看到了吗,我的小脚本正
我有生产服务器(Nginx+Passenger)。当我尝试从另一台计算机ab-n3-c3myhost.ru/时,我在我的nginxerror.log中收到此错误日志:[pid=21160thr=139775297914624file=ext/nginx/HelperAgent.cpp:584time=2011-08-3115:25:49.22]:UncaughtexceptioninPassengerServerclientthread:exception:Cannotreadresponsefrombackendprocess:Connectionresetbypeer(104)ba
“架设一个亿级高并发系统,是多数程序员、架构师的工作目标。许多的技术从业人员甚至有时会降薪去寻找这样的机会。但并不是所有人都有机会主导,甚至参与这样一个系统。今天我们用12306火车票购票这样一个业务场景来做DDD领域建模。”开篇要实现软件设计、软件开发在一个统一的思想、统一的节奏下进行,就应该有一个轻量级的框架对开发过程与代码编写做一定的约束。虽然DDD是一个软件开发的方法,而不是具体的技术或框架,但拥有一个轻量级的框架仍然是必要的,为了开发一个支持DDD的框架,首先需要理解DDD的基本概念和核心的组件。一.什么是领域驱动设计(DDD)首先要知道DDD是一种开发理念,核心是维护一个反应领域概
在执行所有promise后,我正在尝试进行一些计算。但是proc从不调用:cbr_promise=Concurrent::Promise.execute{CbrRatesService.call}bitfinex_promise=Concurrent::Promise.execute{BitfinexService.call}proc=Proc.newdoputs10endConcurrent::Promise.all?([cbr_promise,bitfinex_promise]).then{proc}使用concurrent-ruby制作gem。例如,我是否应该创建一个每100毫秒
如何处理并发ruby线程池中的异常(http://ruby-concurrency.github.io/concurrent-ruby/file.thread_pools.html)?例子:pool=Concurrent::FixedThreadPool.new(5)pool.postdoraise'somethinggoeswrong'end#howtorescuethisexceptionhere更新:这是我的代码的简化版本:defprocesspool=Concurrent::FixedThreadPool.new(5)products.eachdo|product|new_
在Ruby1.9.x中,不允许我的Ruby脚本再次运行或等待前一个实例完成的简单方法是什么?**我希望避免困惑的文件锁定或进程表检查。有没有类似globalmutex的东西或信号量已经在核心?我研究了原生Mutex但这似乎只适用于一个Ruby进程内的线程,而不适用于不同进程。**稍后我可能会添加超时功能,或者限制为N个实例,或者希望使用多个全局锁(每个系统范围的资源一个,最多只能有一个实例)。 最佳答案 这段非常短的代码将卡住在原地,直到/tmp中以您的脚本命名的锁定文件被独占锁定:File.open("/tmp/#{File.ba
以下是我的赛璐珞代码。client1.rb2个客户之一。(我将其命名为客户端1)client2.rb2个客户中的第2个。(命名为客户端2)备注:上述2个客户端之间的唯一区别是传递给服务器的文本。即('client-1'和'client-2'分别)针对以下2个服务器(一次一个)测试这2个客户端(通过并排运行)。我发现非常奇怪的结果。server1.rb(取自celluloid-zmq的README.md的基本示例)将其用作上述2个客户端的示例服务器导致任务的并行执行。输出rubyserver1.rbReceivedat04:59:39PMandmessageisclient-1Going