java学习笔记:并发编程实战

虽然在这里看起来,结果偏离了一些可以接受,但是如果这个计数器的值被用来生成数值序列或唯一的对象标识符,那么在多次调用中返回相同的值将导致严重的数据完整性问题。

在并发编程中,像这种由于不恰当的执行时序而出现不正确的结果是一种非常重要的情况,这种情况叫做“竞态条件(Race Condition)”。

竞态条件

当某个计算的正确性取决于多个线程的交替执行时序的时候,那么就会发生竞态条件。常见的竞态条件类型是“先检查后执行”操作,既通过一个可能失效的观测结果来决定下一步的动作。

举个栗子:你和朋友约好一起去网吧开黑,你当了网吧的时候,发现你朋友不在,此时你可能选择呆在网吧里等他,也可能去他家找他,如果你去找他,那么当你出了网吧以后,你在网吧的观测结果(朋友不在)就可能失效了,因为他可能在你去他家找他的路上已经到了网吧,而你却去找他了。

这个栗子中,正确的结果是(你们在网吧会面),但是这个结果取决于事件发生的时序(既谁先到网吧并且等待对方的时长)。这种观察结果的失效就是大多数竞态条件的本质——基于一种可能失效的观测结果来做出判断或者执行某个计算。

再举个栗子,假设有两个线程A、B,A、B线程都用来判断某个文件夹是否存在,不存在就创建它,假如当A线程发现文件夹不存在时,正打算创建文件夹,但是此时B线程已经完成了文件夹的创建,那么此时A线程观测的结果就已经失效了,但是A线程依旧根据这个已失效的观测结果在进行下一步动作,这就可能会导致各种问题。

使用“先检查后执行”的一种常见的情况就是延迟初始化。就比如在单例模式中有一种写法如下:

public class LazyInitRace {

private static LazyInitRace instance = null;

public LazyInitRace getInstance(){

if(instance == null){

instance = new LazyInitRace();

}

return instance;

}

}

这就是典型的延迟初始化,在单线程中这样写没毛病,但是在多线程环境中,如果有A、B线程同时执行getInstance()方法,那么结果可能符合预期,也可能会得到两个不一样的对象。因为在A线程发现instace为null时,B线程可能也同时发现instace为null。

与大多数并发错误一样,竞态条件并不总是会产生错误,还需要某种不恰当的执行时序,但是如果发生问题,那么可能导致很严重的问题。

在上面的示例中都包含了一组需要以原子方式执行(或者说不可分割)的操作。要避免竞态条件问题,就必须在某个线程修改变量时,通过某种方式防止其他线程使用这个变量,从而确保其他线程只能在修改操作完成之前或之后读取和修改状态,而不是在修改过程中。

在上面统计已处理请求数量的示例中,我们可以使用AtomicLong对象来替换long,因为AtmoicLong类是线程安全类,所以可以保证示例也是示例安全的,但是在添加一个状态变量时,是否还可以通过使用线程安全的对象来管理而类的状态以维护其线程安全性呢?如下所示:

public class UnsafeCachingFactorizer implements Servlet {

private final AtomicReference lastNumber = new AtomicReference<>();

private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<>();

public void service(ServletRequest req, ServletResponse resp) {

BigInteger i = extractFromRequest(req);

if (i.equals(lastNumber.get())) {

encodeIntoResponse(resp, lastFactors.get());

} else {

BigInteger[] factors = factor(i);

lastNumber.set(i);

lastFactors.set(factors);

encodeIntoResponse(resp, factors);

}

}

}

在上述例子中,虽然两个变量都是线程安全的,但是在service方法中依然存在竞态条件,因为在上述例子中,类的不变性条件已经被破坏了,只有确保了这个不变性条件不被破坏,才是正确的。当不变性条件中涉及到了多个变量时,各个变量之间并不是彼此独立的,而是某个变量的值会对其他变量的值产生约束。因此,当更新某一个变量时,需要在同一个原子操作中对其他变量同时进行更新。

在上述例子中,虽然set方法是原子操作,但是在set方法无法同时更新lastNumber和lastFactors。如果当一个线程执行了lastNumber.set()方法还没执行下一个set方法时,如果此时有一个线程访问service方法,那么得到的结果就与我们所预期的不一致了。

所以,要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量。

三、加锁机制

======

3.1内置锁

在Java中提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized Block)。同步代码块包含两部分:一个是作为锁的对象引用,一个作为由这个锁保护的代码块。以关键字synchronized来修饰的方法是一种横跨整个方法体的同步代码块,其中该同步代码块的锁就是方法调用所在的对象(this).静态的synchronized方法以Class对象为作为锁。

每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁(Intrinsic Lock)或是监视器锁(Monitor Lock)。线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁。

Java的内置锁相当于一种互斥锁,最多只有一个线程能持有这种锁。当线程A尝试获取线程B持有的锁时,线程A必须等待或阻塞,知道线程B释放了该锁。如果线程B不释放锁,则线程A也将永远等下去。任何一个执行同步代码块的线程,都不可能看到有其他线程正在执行由同一个锁保护的同步代码块。

下面时应用了内置锁的示例:

public class SynchronizedFactorizer implements Servlet {

private BigInteger lastNumber;

private BigInteger[] lastFactors;

public synchronized void service(ServletRequest req, ServletResponse resp) {

BigInteger i = extractFromRequest(req);

if (i.equals(lastNumber)) {

encodeIntoResponse(resp, lastFactors.get());

} else {

BigInteger[] factors = factor(i);

lastNumber = i;

lastFactors = factors;

encodeIntoResponse(resp, factors);

}

}

}

虽然使用synchrnoized关键字保证了结果的正确性,但是在同一时刻只有一个线程可以执行service方法,这就导致了服务的响应性非常低,并发性非常的糟糕,变成了一个性能问题,而不是线程安全问题。

3.2 重入

当某个线程请求一个由其他线程持有的锁是,发出请求的线程就会被阻塞,但是,由于内置锁是可重入的,即如果某个线程试图获得一个已经由它自己持有的锁时,那么这个请求就会成功。"重入"意味着获取锁的操作粒度是“线程”而不是“调用”。重入的一种实现方法就是,为每一个锁关联一个计数值和一个所有者线程。当计数值为0时,就认为这个锁是没有被任何线程持有。当线程请求一个未被持有的锁时,JVM记下锁的持有者,并且将获取计数值置为1。如果同一个线程再次获取这个锁,计数值将递增,而当线程退出同步代码块时,计数值会相应的递减。当计数值为0时,这个锁将被释放。

下面是一个重入的例子:

public class Widget{
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

学习视频:

大厂面试真题:

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!
es/e5c14a7895254671a72faed303032d36.jpg" alt=“img” style=“zoom: 33%;” />

最后

学习视频:

[外链图片转存中…(img-MpLwzpAf-1712124117077)]

大厂面试真题:

[外链图片转存中…(img-wRHppSNg-1712124117077)]

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!

  • 16
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值