极客时间已完结课程限时免费阅读

02 | 代码加锁:不要让“锁”事成为烦心事

02 | 代码加锁:不要让“锁”事成为烦心事-极客时间

02 | 代码加锁:不要让“锁”事成为烦心事

讲述:王少泽

时长17:20大小15.88M

你好,我是朱晔。
在上一讲中,我与你介绍了使用并发容器等工具解决线程安全的误区。今天,我们来看看解决线程安全问题的另一种重要手段——锁,在使用上比较容易犯哪些错。
我先和你分享一个有趣的案例吧。有一天,一位同学在群里说“见鬼了,疑似遇到了一个 JVM 的 Bug”,我们都很好奇是什么 Bug。
于是,他贴出了这样一段代码:在一个类里有两个 int 类型的字段 a 和 b,有一个 add 方法循环 1 万次对 a 和 b 进行 ++ 操作,有另一个 compare 方法,同样循环 1 万次判断 a 是否小于 b,条件成立就打印 a 和 b 的值,并判断 a>b 是否成立。
@Slf4j
public class Interesting {
volatile int a = 1;
volatile int b = 1;
public void add() {
log.info("add start");
for (int i = 0; i < 10000; i++) {
a++;
b++;
}
log.info("add done");
}
public void compare() {
log.info("compare start");
for (int i = 0; i < 10000; i++) {
//a始终等于b吗?
if (a < b) {
log.info("a:{},b:{},{}", a, b, a > b);
//最后的a>b应该始终是false吗?
}
}
log.info("compare done");
}
}
他起了两个线程来分别执行 add 和 compare 方法:
Interesting interesting = new Interesting();
new Thread(() -> interesting.add()).start();
new Thread(() -> interesting.compare()).start();
按道理,a 和 b 同样进行累加操作,应该始终相等,compare 中的第一次判断应该始终不会成立,不会输出任何日志。但,执行代码后发现不但输出了日志,而且更诡异的是,compare 方法在判断 a<b 成立的情况下还输出了 a>b 也成立:
群里一位同学看到这个问题笑了,说:“这哪是 JVM 的 Bug,分明是线程安全问题嘛。很明显,你这是在操作两个字段 a 和 b,有线程安全问题,应该为 add 方法加上锁,确保 a 和 b 的 ++ 是原子性的,就不会错乱了。”随后,他为 add 方法加上了锁:
public synchronized void add()
但,加锁后问题并没有解决。
我们来仔细想一下,为什么锁可以解决线程安全问题呢。因为只有一个线程可以拿到锁,所以加锁后的代码中的资源操作是线程安全的。但是,这个案例中的 add 方法始终只有一个线程在操作,显然只为 add 方法加锁是没用的
之所以出现这种错乱,是因为两个线程是交错执行 add 和 compare 方法中的业务逻辑,而且这些业务逻辑不是原子性的:a++ 和 b++ 操作中可以穿插在 compare 方法的比较代码中;更需要注意的是,a<b 这种比较操作在字节码层面是加载 a、加载 b 和比较三步,代码虽然是一行但也不是原子性的。
所以,正确的做法应该是,为 add 和 compare 都加上方法锁,确保 add 方法执行时,compare 无法读取 a 和 b:
public synchronized void add()
public synchronized void compare()
所以,使用锁解决问题之前一定要理清楚,我们要保护的是什么逻辑,多线程执行的情况又是怎样的。

加锁前要清楚锁和被保护的对象是不是一个层面的

除了没有分析清线程、业务逻辑和锁三者之间的关系随意添加无效的方法锁外,还有一种比较常见的错误是,没有理清楚锁和要保护的对象是否是一个层面的。
我们知道静态字段属于类,类级别的锁才能保护;而非静态字段属于类实例,实例级别的锁就可以保护。
先看看这段代码有什么问题:在类 Data 中定义了一个静态的 int 字段 counter 和一个非静态的 wrong 方法,实现 counter 字段的累加操作。
class Data {
@Getter
private static int counter = 0;
public static int reset() {
counter = 0;
return counter;
}
public synchronized void wrong() {
counter++;
}
}
写一段代码测试下:
@GetMapping("wrong")
public int wrong(@RequestParam(value = "count", defaultValue = "1000000") int count) {
Data.reset();
//多线程循环一定次数调用Data类不同实例的wrong方法
IntStream.rangeClosed(1, count).parallel().forEach(i -> new Data().wrong());
return Data.getCounter();
}
因为默认运行 100 万次,所以执行后应该输出 100 万,但页面输出的是 639242:
我们来分析下为什么会出现这个问题吧。
在非静态的 wrong 方法上加锁,只能确保多个线程无法执行同一个实例的 wrong 方法,却不能保证不会执行不同实例的 wrong 方法。而静态的 counter 在多个实例中共享,所以必然会出现线程安全问题。
理清思路后,修正方法就很清晰了:同样在类中定义一个 Object 类型的静态字段,在操作 counter 之前对这个字段加锁。
class Data {
@Getter
private static int counter = 0;
private static Object locker = new Object();
public void right() {
synchronized (locker) {
counter++;
}
}
}
你可能要问了,把 wrong 方法定义为静态不就可以了,这个时候锁是类级别的。可以是可以,但我们不可能为了解决线程安全问题改变代码结构,把实例方法改为静态方法。
感兴趣的同学还可以从字节码以及 JVM 的层面继续探索一下,代码块级别的 synchronized 和方法上标记 synchronized 关键字,在实现上有什么区别。

加锁要考虑锁的粒度和场景问题

在方法上加 synchronized 关键字实现加锁确实简单,也因此我曾看到一些业务代码中几乎所有方法都加了 synchronized,但这种滥用 synchronized 的做法:
一是,没必要。通常情况下 60% 的业务代码是三层架构,数据经过无状态的 Controller、Service、Repository 流转到数据库,没必要使用 synchronized 来保护什么数据。
二是,可能会极大地降低性能。使用 Spring 框架时,默认情况下 Controller、Service、Repository 是单例的,加上 synchronized 会导致整个程序几乎就只能支持单线程,造成极大的性能问题。
即使我们确实有一些共享资源需要保护,也要尽可能降低锁的粒度,仅对必要的代码块甚至是需要保护的资源本身加锁。
比如,在业务代码中,有一个 ArrayList 因为会被多个线程操作而需要保护,又有一段比较耗时的操作(代码中的 slow 方法)不涉及线程安全问题,应该如何加锁呢?
错误的做法是,给整段业务逻辑加锁,把 slow 方法和操作 ArrayList 的代码同时纳入 synchronized 代码块;更合适的做法是,把加锁的粒度降到最低,只在操作 ArrayList 的时候给这个 ArrayList 加锁。
private List<Integer> data = new ArrayList<>();
//不涉及共享资源的慢方法
private void slow() {
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
}
}
//错误的加锁方法
@GetMapping("wrong")
public int wrong() {
long begin = System.currentTimeMillis();
IntStream.rangeClosed(1, 1000).parallel().forEach(i -> {
//加锁粒度太粗了
synchronized (this) {
slow();
data.add(i);
}
});
log.info("took:{}", System.currentTimeMillis() - begin);
return data.size();
}
//正确的加锁方法
@GetMapping("right")
public int right() {
long begin = System.currentTimeMillis();
IntStream.rangeClosed(1, 1000).parallel().forEach(i -> {
slow();
//只对List加锁
synchronized (data) {
data.add(i);
}
});
log.info("took:{}", System.currentTimeMillis() - begin);
return data.size();
}
执行这段代码,同样是 1000 次业务操作,正确加锁的版本耗时 1.4 秒,而对整个业务逻辑加锁的话耗时 11 秒。
如果精细化考虑了锁应用范围后,性能还无法满足需求的话,我们就要考虑另一个维度的粒度问题了,即:区分读写场景以及资源的访问冲突,考虑使用悲观方式的锁还是乐观方式的锁。
一般业务代码中,很少需要进一步考虑这两种更细粒度的锁,所以我只和你分享几个大概的结论,你可以根据自己的需求来考虑是否有必要进一步优化:
对于读写比例差异明显的场景,考虑使用 ReentrantReadWriteLock 细化区分读写锁,来提高性能。
如果你的 JDK 版本高于 1.8、共享资源的冲突概率也没那么大的话,考虑使用 StampedLock 的乐观读的特性,进一步提高性能。
JDK 里 ReentrantLock 和 ReentrantReadWriteLock 都提供了公平锁的版本,在没有明确需求的情况下不要轻易开启公平锁特性,在任务很轻的情况下开启公平锁可能会让性能下降上百倍。

多把锁要小心死锁问题

刚才我们聊到锁的粒度够用就好,这就意味着我们的程序逻辑中有时会存在一些细粒度的锁。但一个业务逻辑如果涉及多把锁,容易产生死锁问题。
之前我遇到过这样一个案例:下单操作需要锁定订单中多个商品的库存,拿到所有商品的锁之后进行下单扣减库存操作,全部操作完成之后释放所有的锁。代码上线后发现,下单失败概率很高,失败后需要用户重新下单,极大影响了用户体验,还影响到了销量。
经排查发现是死锁引起的问题,背后原因是扣减库存的顺序不同,导致并发的情况下多个线程可能相互持有部分商品的锁,又等待其他线程释放另一部分商品的锁,于是出现了死锁问题。
接下来,我们剖析一下核心的业务代码。
首先,定义一个商品类型,包含商品名、库存剩余和商品的库存锁三个属性,每一种商品默认库存 1000 个;然后,初始化 10 个这样的商品对象来模拟商品清单:
@Data
@RequiredArgsConstructor
static class Item {
final String name; //商品名
int remaining = 1000; //库存剩余
@ToString.Exclude //ToString不包含这个字段
ReentrantLock lock = new ReentrantLock();
}
随后,写一个方法模拟在购物车进行商品选购,每次从商品清单(items 字段)中随机选购三个商品(为了逻辑简单,我们不考虑每次选购多个同类商品的逻辑,购物车中不体现商品数量):
private List<Item> createCart() {
return IntStream.rangeClosed(1, 3)
.mapToObj(i -> "item" + ThreadLocalRandom.current().nextInt(items.size()))
.map(name -> items.get(name)).collect(Collectors.toList());
}
下单代码如下:先声明一个 List 来保存所有获得的锁,然后遍历购物车中的商品依次尝试获得商品的锁,最长等待 10 秒,获得全部锁之后再扣减库存;如果有无法获得锁的情况则解锁之前获得的所有锁,返回 false 下单失败。
private boolean createOrder(List<Item> order) {
//存放所有获得的锁
List<ReentrantLock> locks = new ArrayList<>();
for (Item item : order) {
try {
//获得锁10秒超时
if (item.lock.tryLock(10, TimeUnit.SECONDS)) {
locks.add(item.lock);
} else {
locks.forEach(ReentrantLock::unlock);
return false;
}
} catch (InterruptedException e) {
}
}
//锁全部拿到之后执行扣减库存业务逻辑
try {
order.forEach(item -> item.remaining--);
} finally {
locks.forEach(ReentrantLock::unlock);
}
return true;
}
我们写一段代码测试这个下单操作。模拟在多线程情况下进行 100 次创建购物车和下单操作,最后通过日志输出成功的下单次数、总剩余的商品个数、100 次下单耗时,以及下单完成后的商品库存明细:
@GetMapping("wrong")
public long wrong() {
long begin = System.currentTimeMillis();
//并发进行100次下单操作,统计成功次数
long success = IntStream.rangeClosed(1, 100).parallel()
.mapToObj(i -> {
List<Item> cart = createCart();
return createOrder(cart);
})
.filter(result -> result)
.count();
log.info("success:{} totalRemaining:{} took:{}ms items:{}",
success,
items.entrySet().stream().map(item -> item.getValue().remaining).reduce(0, Integer::sum),
System.currentTimeMillis() - begin, items);
return success;
}
运行程序,输出如下日志:
可以看到,100 次下单操作成功了 65 次,10 种商品总计 10000 件,库存总计为 9805,消耗了 195 件符合预期(65 次下单成功,每次下单包含三件商品),总耗时 50 秒。
为什么会这样呢?
使用 JDK 自带的 VisualVM 工具来跟踪一下,重新执行方法后不久就可以看到,线程 Tab 中提示了死锁问题,根据提示点击右侧线程 Dump 按钮进行线程抓取操作:
查看抓取出的线程栈,在页面中部可以看到如下日志:
显然,是出现了死锁,线程 4 在等待的一个锁被线程 3 持有,线程 3 在等待的另一把锁被线程 4 持有
那为什么会有死锁问题呢?
我们仔细回忆一下购物车添加商品的逻辑,随机添加了三种商品,假设一个购物车中的商品是 item1 和 item2,另一个购物车中的商品是 item2 和 item1,一个线程先获取到了 item1 的锁,同时另一个线程获取到了 item2 的锁,然后两个线程接下来要分别获取 item2 和 item1 的锁,这个时候锁已经被对方获取了,只能相互等待一直到 10 秒超时。
其实,避免死锁的方案很简单,为购物车中的商品排一下序,让所有的线程一定是先获取 item1 的锁然后获取 item2 的锁,就不会有问题了。所以,我只需要修改一行代码,对 createCart 获得的购物车按照商品名进行排序即可:
@GetMapping("right")
public long right() {
...
.
long success = IntStream.rangeClosed(1, 100).parallel()
.mapToObj(i -> {
List<Item> cart = createCart().stream()
.sorted(Comparator.comparing(Item::getName))
.collect(Collectors.toList());
return createOrder(cart);
})
.filter(result -> result)
.count();
...
return success;
}
测试一下 right 方法,不管执行多少次都是 100 次成功下单,而且性能相当高,达到了 3000 以上的 TPS:
这个案例中,虽然产生了死锁问题,但因为尝试获取锁的操作并不是无限阻塞的,所以没有造成永久死锁,之后的改进就是避免循环等待,通过对购物车的商品进行排序来实现有顺序的加锁,避免循环等待。

重点回顾

我们一起总结回顾下,使用锁来解决多线程情况下线程安全问题的坑吧。
第一,使用 synchronized 加锁虽然简单,但我们首先要弄清楚共享资源是类还是实例级别的、会被哪些线程操作,synchronized 关联的锁对象或方法又是什么范围的。
第二,加锁尽可能要考虑粒度和场景,锁保护的代码意味着无法进行多线程操作。对于 Web 类型的天然多线程项目,对方法进行大范围加锁会显著降级并发能力,要考虑尽可能地只为必要的代码块加锁,降低锁的粒度;而对于要求超高性能的业务,还要细化考虑锁的读写场景,以及悲观优先还是乐观优先,尽可能针对明确场景精细化加锁方案,可以在适当的场景下考虑使用 ReentrantReadWriteLock、StampedLock 等高级的锁工具类。
第三,业务逻辑中有多把锁时要考虑死锁问题,通常的规避方案是,避免无限等待和循环等待。
此外,如果业务逻辑中锁的实现比较复杂的话,要仔细看看加锁和释放是否配对,是否有遗漏释放或重复释放的可能性;并且对于分布式锁要考虑锁自动超时释放了,而业务逻辑却还在进行的情况下,如果别的线线程或进程拿到了相同的锁,可能会导致重复执行
为演示方便,今天的案例是在 Controller 的逻辑中开新的线程或使用线程池进行并发模拟,我们当然可以意识到哪些对象是并发操作的。但对于 Web 应用程序的天然多线程场景,你可能更容易忽略这点,并且也可能因为误用锁降低应用整体的吞吐量。如果你的业务代码涉及复杂的锁操作,强烈建议 Mock 相关外部接口或数据库操作后对应用代码进行压测,通过压测排除锁误用带来的性能问题和死锁问题
今天用到的代码,我都放在了 GitHub 上,你可以点击这个链接查看。

思考与讨论

本文开头的例子里,变量 a、b 都使用了 volatile 关键字,你知道原因吗?我之前遇到过这样一个坑:我们开启了一个线程无限循环来跑一些任务,有一个 bool 类型的变量来控制循环的退出,默认为 true 代表执行,一段时间后主线程将这个变量设置为了 false。如果这个变量不是 volatile 修饰的,子线程可以退出吗?你能否解释其中的原因呢?
文末我们又提了两个坑,一是加锁和释放没有配对的问题,二是锁自动释放导致的重复逻辑执行的问题。你有什么方法来发现和解决这两种问题吗?
在使用锁的过程中,你还遇到过其他坑吗?我是朱晔,欢迎在评论区与我留言分享你的想法,也欢迎你把这篇文章分享给你的朋友或同事,一起交流。
分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 49

提建议

上一篇
01 | 使用了并发工具类库,线程安全就高枕无忧了吗?
下一篇
03 | 线程池:业务代码最常用也最容易犯错的组件
unpreview
 写留言

精选留言(57)

  • Darren
    置顶
    2020-03-10
    思考与讨论: volatile的问题:可见性问题和禁止指令重排序优化。 可见性问题:本质上是cpu缓存失效,必须从主内存读取数据; 禁止指令重排序优化:x86处理器仅下,只实现了volatile的读写内存屏障,也就是store load,也就是写读,本质上也就是读写可见性,happen-before原则。 实现原理是通过寄存器esp实现的。 当然也不会退出循环,因为cpu缓存到主内存的同步不是实时的。 锁释放和重复执行问题:锁建议使用synchronized,在JDK1.6后,synchronized与Lock性能上差距很小了(优化了很多,自旋锁,自适应自旋锁、偏向锁,轻量级锁等),synchronized也不用程序获取和释放锁,同步代码块是通过monitorenter monitorexit实现的,同步方法是方法头中有ACC_SYNCHRONIZED标志;在分布式场景下,可以考虑etcd,etcd支持锁的自动续期等; 重复执行:首先在锁的使用场景下做好处理,尽量避免重复执行,但业务层面一定要做好幂等。
    展开

    作者回复: 👍🏻

    86
  • Seven.Lin澤耿
    2020-03-10
    1.加群解锁没有配对可以用一些代码质量工具协助排插,如Sonar,集成到ide和代码仓库,在编码阶段发现,加上超时自动释放,避免长期占有锁 2.锁超时自动释放导致重复执行的话,可以用锁续期,如redisson的watchdog;或者保证业务的幂等性,重复执行也没问题。

    作者回复: 这个回答太赞了!

    共 3 条评论
    56
  • 睿睿睿睿睿睿、
    2020-03-11
    老师我有个意见代码能否不要大量使用Lambda表达式,并不是每个读者都是老司机

    作者回复: 其实Java8出来已经挺久了,使用Lambda和Stream可以显著改善代码可读性,确保代码简洁性,因此专栏是大量使用Java8的一些新特性的。给你几个建议: 1、可以进一步订阅极客时间专门的学习java的专栏系统学习Lambda语法,比如https://time.geekbang.org/course/detail/181-107395,然后自己对着练习一下 2、买一本《Java实战第二版》系统学习Java8的方方面面 3、关注一下本专栏的加餐,之后我们会通过加餐介绍下Java8 4、遇到实在看不懂的代码,下载源码后,在IDEA中点击lambda或stream API的地方,停留一下,左侧可以看到有提示 replace stream API with loop或replace lambda with anonymous class选项,翻译为非stream和lambda的语法,帮助你理解

    共 9 条评论
    49
  • 黄海峰
    2020-03-10
    超时自动释放锁后怎么避免重复逻辑好难,面试曾被卡,求解。。。

    作者回复: 有两个方面:1. 避免超时,单独开一个线程给锁延长有效期。比如设置锁有效期30s,有个线程每隔10s重新设置下锁的有效期。 2. 避免重复,业务上增加一个标记是否被处理的字段。或者开一张新表,保存已经处理过的流水号。

    共 7 条评论
    38
  • 编程界的小学生
    2020-03-09
    1.不能退出。必须加volatile,因为volatile保证了可见性。改完后会强制让工作内存失效。去主存拿。如果不加volatile的话那么在while true里面添加输出语句也是OK的。因为println源码加锁了,sync会让当前线程的工作内存失效。 解释的对吗?献丑了。

    作者回复: 嗯必须加volatile或者使用AtomicBoolean/AtomicReference等也行,后者相比volatile除了确保可见性还提供了CAS方法保证原子性

    共 4 条评论
    24
  • 汤杰
    2020-03-11
    对着代码看锁过期蒙了半天,还以为trylock的时间不是等待锁的时间,以为我一直理解的是错误的。最好加上特定的条件。本地锁哪有锁过期呢。原来有些分布式锁为了防止调用方挂了不释放锁加了超时。看到有说用客户端续期的,业务保证的,业务的确一定要保证的,用分布式锁可以解决业务数据库幂等在高并发冲突强烈下性能降低。

    作者回复: 抱歉,因为本文删除了原来有的分布式锁的例子,所以最后总结这边的描述谈到的『锁自动超时释放问题』有点唐突,我们改一下。你理解的没错,锁过期是指分布式锁的过期,本地锁是只有等待锁超时

    13
  • insight
    2020-03-11
    看老师使用Lambda表达式感觉学到了非常多,非常支持老师这样做,毕竟程序员就是要不断走出舒适区,学习新东西的。就是老师的Lambda加餐能不能早一点来,对照起来看的更舒服一些

    作者回复: 应该快了,大概是下周

    12
  • 郑思雨
    2020-08-07
    一、加锁和释放没有配对: lock 与 unlock 通常结对使用,使用时,一般将unlock放在finally代码块中。但是释放锁时最好增加判断: if (lock.isHeldByCurrentThread()) lock.unlock(); 这样避免锁持有超时后释放引发IllegalMonitorStateException异常。 如果怕忘记释放锁,可以将锁封装成一个代理模式,如下: public class AutoUnlockProxy implements Closeable { private Lock lock; public AutoUnlockProxy(Lock lock){ this.lock = lock; } public void lock(){ lock.lock(); } public boolean tryLock(){ return lock.tryLock(); } @Override public void close() throws IOException { lock.unlock(); } } 使用时,通过try-with-resource 的方式使用,可以达到自动释放锁的目的: try(AutoUnlockProxy proxy = new AutoUnlockProxy(new ReentrantLock())){ proxy.lock(); }catch (Exception e){ e.printStackTrace(); } 二、锁自动释放导致的重复逻辑执行(补充的细节点) 1、代码层面:对请求进行验重; 2、数据库层面:如果有插入操作,建议设置唯一索引,在数据库层面能增加一层安全保障;
    展开

    作者回复: 赞

    共 2 条评论
    10
  • 看不到de颜色
    2020-03-25
    关于锁过期问题。以前做redis分布式锁的时候一直在思考这个问题。当时觉得就是尽量让锁过期时间比程序执行之间略长一些,以保证加锁区域代码能尽量执行完成。看到老师给其他同学评论说可以用另外一个线程去不断重置锁时间,这里有我理解是针对像redis这种利用setnx实现的分布式锁可以这么解决。那还有其他场景吗?

    作者回复: 就是锁续期解决 可以看一下redisson实现

    共 3 条评论
    6
  • pedro
    2020-03-10
    volatile 老生长谈的问题了,关于锁过期,如果开启一个线程续期,但是有最大重试次数,比如 5 次,那么 5 次以后如何保证其它线程拿到锁而不会重复执行业务了?

    作者回复: 可以无限续期,比如redisson的RedissonLock,锁续期是每次续一段时间,比如30秒,然后10秒执行一次续期,虽然是无限次续期,即使客户端崩溃了也没关系无法自动续期后自然会超时

    共 3 条评论
    6
  • 木槿花开
    2020-03-17
    老师好: //不涉及共享资源的慢方法 TimeUnit.MILLISECONDS.sleep(10) 这个方法本质调用的是Thread的 public static void sleep(long millis, int nanos) 这是一个static的类方法,在加锁粒度太粗的wrong方法中,是不是因为线程都去抢Thread的类锁才导致耗时较长,Thread类不就成公共资源了?

    作者回复: 不是这个原因,主要是演示锁包裹了并不需要加锁的慢方法

    3
  • better
    2020-03-14
    实践了一遍add()方法和compare()方法这个例子,结合synchronized的底层原理,不知道理解得正不正确。在这个例子中的理解是,由于synchronized底层是基于moniter指令和对象锁实现的,所以当为add()方法和compare()方法同时加锁后,interesting这个对象就作为对象锁被锁住了,而每次拿到这个对象锁的线程只能有一个,所以执行add()方法的线程和执行compare()方法的两个线程在同一时刻有且只能有一个线程拿到了interesting这个对象锁,所以两个方法就变成串行化执行了,线程安全问题也就得到了解决。 不知道老师结合synchronized底层原理,在这个示例中有没有更好的理解方式
    展开

    作者回复: 没错,主要就是为了防止多个线程交错执行两个方法,synchronized是比较简单清理的解决方式。

    3
  • 小胡子
    2020-08-09
    真实的业务场景中商品成千上万存储在db没办法对商品排序操作,那怎么避免死锁了呢

    作者回复: 排序的是要加锁的对象不是数据库所有数据

    2
  • z小俊、Arno
    2020-03-26
    .filter(result -> result) 老师 这个是什么作用啊? 去掉,测试代码错误的示例结果也是对的了。

    作者回复: 你可能搞混了,比较下下面三段代码的输出: System.out.println(IntStream.rangeClosed(1,10).mapToObj(i->i%2==0).filter(result->result==true).collect(Collectors.toList())); System.out.println(IntStream.rangeClosed(1,10).mapToObj(i->i%2==0).filter(result->result).collect(Collectors.toList())); System.out.println(IntStream.rangeClosed(1,10).mapToObj(i->i%2==0).map(result->result).collect(Collectors.toList()));

    共 2 条评论
    2
  • 何岸康
    2020-03-17
    Volatile保证了多线程操作时变量的可见性。java中还有两个关键字可以实现可见性,即synchronized和final。 http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html中有比较详细的描述;英语不好我就不翻译了,直接贴出来。 Volatile: “Volatile fields are special fields which are used for communicating state between threads. Each read of a volatile will see the last write to that volatile by any thread; in effect, they are designated by the programmer as fields for which it is never acceptable to see a "stale" value as a result of caching or reordering. The compiler and runtime are prohibited from allocating them in registers. They must also ensure that after they are written, they are flushed out of the cache to main memory, so they can immediately become visible to other threads. Similarly, before a volatile field is read, the cache must be invalidated so that the value in main memory, not the local processor cache, is the one seen. There are also additional restrictions on reordering accesses to volatile variables.” synchronized: “This means that any memory operations which were visible to a thread before exiting a synchronized block are visible to any thread after it enters a synchronized block protected by the same monitor, since all the memory operations happen before the release, and the release happens before the acquire.” final: “The values for an object's final fields are set in its constructor. Assuming the object is constructed "correctly", once an object is constructed, the values assigned to the final fields in the constructor will be visible to all other threads without synchronization. In addition, the visible values for any other object or array referenced by those final fields will be at least as up-to-date as the final fields.”
    展开

    作者回复: 不错

    2
  • mgs2002
    2020-03-16
    老师,我知道volatile是保证线程间变量可见性和防止指令重排序的,不过开头例子我把a和b的volatile修饰去掉执行right方法,结果也是正确的呢

    作者回复: synchronized也可以确保可见性,本例的right实现中volatile不是必须的,我这里是一个例子就没有去掉了,而且主要还为了引出思考题

    共 2 条评论
    2
  • 请叫我和尚
    2020-03-15
    老师的 Synchronized Demo2 给counter++,right 的方法是给 static 变量加Synchronized,但是上面又说了是多线程去执行++方法,static 是属于类的,你给static 加同步,是否可以理解为你给类加了锁,多线程下去执行这个方法,是不是就变为了不是多线程去并行执行,而是串行去执行了 同理是不是可以直接写 Synchronized(Data.class){ TODO }, 一个实例,比如 right 方法要处理100 个添加操作,本来是多线程去并行做 100N 个添加操作,但是去给 right方法 里加了类锁,就变成了串行去执行,
    展开

    作者回复: 可以给class加锁,对本例效果一样。正如你说的,加锁让让并行变为了串行,所以尽量要锁范围小一点,本例只是demo,真实情况对于++这种操作,加锁就没必要了,用Atomic类更好。

    2
  • aoe
    2020-03-09
    老师,是用Data.class加锁,结果也是正确的。 请教一下:private static Object locker = new Object(); 与Data.class在加锁时,有什么优势吗? 因为我觉得:Data.class相对于新创建的locker对象更节省内存。 public void right1() { synchronized (Data.class) { counter++; } } 测试代码 public static void main(String[] args) throws InterruptedException { LongAdder adder = new LongAdder(); for (int i = 0; i < 10; i++) { Thread thread = new Thread(new Runnable() { @Override public void run() { Data data = new Data(); for (int j = 0; j < 10000; j++) { // data.wrong(); data.right1(); } adder.increment(); } }); thread.start(); } while (adder.longValue() < 10){ Thread.sleep(1000L); } System.out.println(counter); }
    展开

    作者回复: 结果是一样的。但是如果不同方法需要不同的锁,那么就只能用多个锁字段了。

    共 2 条评论
    2
  • 西街恶人
    2020-03-09
    看老师使用了Lombok
    2
  • Sam.张朝
    2020-06-05
    这里都是大神啊,除了我这个小兵
    1