Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit4c1a665

Browse files
committed
更新并发编程系列文章
1 parent6e80708 commit4c1a665

File tree

32 files changed

+1110
-294
lines changed

32 files changed

+1110
-294
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#CAS有很多优点,但它的缺点呢?
2+
3+
>本文我们一起学习探讨CAS的缺点。
4+
5+
在面试中,面试官常问是了解CAS吗?它有什么优点?你可能一听,这多简单,我会!于是你就开始噼里啪啦说可以避免加互斥锁,可以提高程序的运行效率等。
6+
7+
但是CAS 的缺点,你知道吗?因为对于开发人员来说,对于任何一门技术或者某个知识点,我们都应该熟悉它的优缺点及适合的应用场景,这样才能写出更优雅且高效的代码了。
8+
9+
首先,我们就来看一下 CAS 有哪几个主要的缺点。
10+
11+
##ABA 问题
12+
**首先,CAS 最大的缺点就是 ABA 问题。**
13+
14+
决定 CAS 是否进行 swap 的判断标准是“当前的值和预期的值是否一致”,如果一致,就认为在此期间这个数值没有发生过变动,这在大多数情况下是没有问题的。
15+
16+
但是在有的业务场景下,我们想确切知道从上一次看到这个值以来到现在,这个值是否发生过变化。例如,**这个值假设从 A 变成了 B,再由 B 变回了 A,此时,我们不仅认为它发生了变化,并且会认为它变化了两次。**
17+
18+
在这种场景下,我们使用 CAS,就看不到这两次的变化,因为仅判断“当前的值和预期的值是否一致”就是不够的了。CAS 检查的并不是值有没有发生过变化,而是去比较这当前的值和预期值是不是相等,如果变量的值从旧值 A 变成了新值 B 再变回旧值 A,由于最开始的值 A 和现在的值 A 是相等的,所以 CAS 会认为变量的值在此期间没有发生过变化。所以,CAS 并不能检测出在此期间值是不是被修改过,它只能检查出现在的值和最初的值是不是一样。
19+
20+
**那么如何解决这个问题呢?添加一个版本号就可以解决。**
21+
22+
我们在变量值自身之外,再添加一个版本号,那么这个值的变化路径就从 A→B→A 变成了 1A→2B→3A,这样一来,就可以通过对比版本号来判断值是否变化过,这比我们直接去对比两个值是否一致要更靠谱,所以通过这样的思路就可以解决 ABA 的问题了。
23+
24+
##自旋时间过长
25+
26+
**CAS 的第二个缺点就是自旋时间过长。**
27+
28+
由于单次 CAS 不一定能执行成功,所以 CAS 往往是配合着循环来实现的,有的时候甚至是死循环,不停地进行重试,直到线程竞争不激烈的时候,才能修改成功。
29+
30+
可是如果我们的应用场景本身就是高并发的场景,就有可能导致 CAS 一直都操作不成功,这样的话,循环时间就会越来越长。而且在此期间,CPU 资源也是一直在被消耗的,这会对性能产生很大的影响。所以这就要求我们,要根据实际情况来选择是否使用 CAS,在高并发的场景下,通常 CAS 的效率是不高的。
31+
32+
##范围不能灵活控制
33+
**CAS 的第三个缺点就是不能灵活控制线程安全的范围。**
34+
35+
通常我们去执行 CAS 的时候,是针对某一个,而不是多个共享变量的,这个变量可能是 Integer 类型,也有可能是 Long 类型、对象类型等等,但是我们不能针对多个共享变量同时进行 CAS 操作,因为这多个变量之间是独立的,简单的把原子操作组合到一起,并不具备原子性。因此如果我们想对多个对象同时进行 CAS 操作并想保证线程安全的话,是比较困难的。
36+
37+
有一个解决方案,那就是利用一个新的类,来整合刚才这一组共享变量,这个新的类中的多个成员变量就是刚才的那多个共享变量,然后再利用 atomic 包中的 AtomicReference 来把这个新对象整体进行 CAS 操作,这样就可以保证线程安全。
38+
39+
相比之下,如果我们使用其他的线程安全技术,那么调整线程安全的范围就可能变得非常容易,比如我们用 synchronized 关键字时,如果想把更多的代码加锁,那么只需要把更多的代码放到同步代码块里面就可以了。
40+
41+
##总结
42+
本文中我们学习了 CAS 的三个缺点,分别是**ABA 问题、自旋时间过长以及线程安全的范围不能灵活控制**
43+
44+
##干货分享
45+
**小伙伴们**关注【**码上Java**】微信公众号,回复关键字“**面试宝典**”,领取一份**嘟嘟**平时收集的一些优质资源,包含我们代码侠必备的优质简历模板、面试题库、电子书等资源大礼包一份,助力**小伙伴们**早日收获**心仪offer**,遇见更好的自己~
46+
47+
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
2+
#Callable和Runnable的不同?
3+
4+
>本文我们一起学习 Callable 和 Runnable 的不同。
5+
6+
##Runnable接口
7+
首先,我们先来看看Runnable有什么缺点?
8+
###1.不能返回一个返回值
9+
第一个缺陷,对于 Runnable 而言,它不能返回一个返回值,虽然可以利用其他的一些办法,比如在 Runnable 方法中写入日志文件或者修改某个共享的对象的办法,来达到保存线程执行结果的目的,但这种解决问题的行为千曲百折,属于曲线救国,效率着实不高。
10+
11+
实际上,在很多情况下执行一个子线程时,我们都希望能得到执行的任务的结果,也就是说,我们是需要得到返回值的,比如请求网络、查询数据库等。可是 Runnable 不能返回一个返回值,这是它第一个非常严重的缺陷。
12+
13+
###2. 不能抛出 checked Exception
14+
第二个缺陷就是不能抛出 checked Exception,如下面这段代码所示:
15+
16+
```java
17+
public class RunThrowException {
18+
   /**
19+
    * 普通方法内可以 throw 异常,并在方法签名上声明 throws
20+
    */
21+
   public void normalMethod() throws Exception {
22+
       throw new IOException();
23+
   }
24+
   Runnable runnable = new Runnable() {
25+
       /**
26+
        *  run方法上无法声明 throws 异常,且run方法内无法 throw 出 checked Exception,除非使用try catch进行处理
27+
        */
28+
       @Override
29+
       public void run() {
30+
           try {
31+
               throw new IOException();
32+
           } catch (IOException e) {
33+
               e.printStackTrace();
34+
           }
35+
       }
36+
   }
37+
}
38+
```
39+
在这段代码中,有两个方法,第一个方法是一个普通的方法,叫作 normalMethod,可以看到,在它的方法签名中有 throws Exception,并且在它的方法内也 throw 了一个 new IOException()。
40+
41+
然后在下面的的代码中,我们新建了一个 Runnable 对象,同时重写了它的 run 方法,我们没有办法在这个 run 方法的方法签名上声明 throws 一个异常出来。同时,在这个 run 方法里面也没办法 throw 一个 checked Exception,除非如代码所示,用 try catch 包裹起来,但是如果不用 try catch 是做不到的。
42+
43+
这就是对于 Runnable 而言的两个重大缺陷。
44+
45+
##Callable 接口
46+
Callable 是一个类似于 Runnable 的接口,实现 Callable 接口的类和实现 Runnable 接口的类都是可以被其他线程执行的任务。 我们看一下 Callable 的源码:
47+
48+
```java
49+
publicinterfaceCallable<V> {
50+
Vcall()throwsException;
51+
}
52+
```
53+
54+
可以看出它也是一个 interface,并且它的 call 方法中已经声明了 throws Exception,前面还有一个 V 泛型的返回值,这就和之前的 Runnable 有很大的区别。实现 Callable 接口,就要实现 call 方法,这个方法的返回值是泛型 V,如果把 call 中计算得到的结果放到这个对象中,就可以利用 call 方法的返回值来获得子线程的执行结果了。
55+
56+
##Callable 和 Runnable 的区别
57+
1. 方法名,Callable 规定的执行方法是 call(),而 Runnable 规定的执行方法是 run();
58+
2. 返回值,Callable 的任务执行后有返回值,而 Runnable 的任务执行后是没有返回值的;
59+
3. 抛出异常,call() 方法可抛出异常,而 run() 方法是不能抛出受检查异常的;
60+
和 Callable 配合的有一个 Future 类,通过 Future 可以了解任务执行情况,或者取消任务的执行,还可获取任务执行的结果,这些功能都是 Runnable 做不到的,Callable 的功能要比 Runnable 强大。
61+
62+
##总结
63+
本文中我们学习了 Runnable 的两个缺陷,第一个是没有返回值,第二个是不能抛出受检查异常;接下来分析了 Callable 接口,并且把 Callable 接口和 Runnable 接口的区别进行了对比和总结。
64+
65+
66+
67+
68+
69+
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#Java中哪几种锁?分别有什么特点?
2+
>本文中我们一起学习Java中锁的分类及其特点。
3+
4+
锁是用来控制多线程访问共享资源的方式,一般的讲,一个锁能够防止多个线程同时访问共享资源。
5+
6+
对于 Java 中的锁而言,根据分类标准我们把锁分为以下 7 大类别,分别是:
7+
8+
1. 偏向锁/轻量级锁/重量级锁;
9+
10+
2. 可重入锁/非可重入锁;
11+
12+
3. 共享锁/独占锁;
13+
14+
4. 公平锁/非公平锁;
15+
16+
5. 悲观锁/乐观锁;
17+
18+
6. 自旋锁/非自旋锁;
19+
20+
7. 可中断锁/不可中断锁。
21+
22+
##1. 偏向锁/轻量级锁/重量级锁
23+
24+
第一种分类是偏向锁/轻量级锁/重量级锁,这三种锁特指 synchronized 锁的状态,通过在对象头中的 mark word 来表明锁的状态。
25+
26+
- 偏向锁
27+
28+
如果自始至终,对于这把锁都不存在竞争,那么其实就没必要上锁,只需要打个标记就行了,这就是偏向锁的思想。一个对象被初始化后,还没有任何线程来获取它的锁时,那么它就是可偏向的,当有第一个线程来访问它并尝试获取锁的时候,它就将这个线程记录下来,以后如果尝试获取锁的线程正是偏向锁的拥有者,就可以直接获得锁,开销很小,性能最好。
29+
30+
- 轻量级锁
31+
32+
JVM 开发者发现在很多情况下,synchronized 中的代码是被多个线程交替执行的,而不是同时执行的,也就是说并不存在实际的竞争,或者是只有短时间的锁竞争,用 CAS 就可以解决,这种情况下,用完全互斥的重量级锁是没必要的。轻量级锁是指当锁原来是偏向锁的时候,被另一个线程访问,说明存在竞争,那么偏向锁就会升级为轻量级锁,线程会通过自旋的形式尝试获取锁,而不会陷入阻塞。
33+
34+
- 重量级锁
35+
36+
重量级锁是互斥锁,它是利用操作系统的同步机制实现的,所以开销相对比较大。当多个线程直接有实际竞争,且锁竞争时间长的时候,轻量级锁不能满足需求,锁就会膨胀为重量级锁。重量级锁会让其他申请却拿不到锁的线程进入阻塞状态。
37+
38+
![](https://imgkr2.cn-bj.ufileos.com/d98be439-2529-423d-a72d-0a090d4a1714.png?UCloudPublicKey=TOKEN_8d8b72be-579a-4e83-bfd0-5f6ce1546f13&Signature=1PJteW%252FObFcinppVV1QA5uOAByc%253D&Expires=1599744613)
39+
40+
你可以发现锁升级的路径:无锁→偏向锁→轻量级锁→重量级锁。
41+
42+
综上所述,偏向锁性能最好,可以避免执行 CAS 操作。而轻量级锁利用自旋和 CAS 避免了重量级锁带来的线程阻塞和唤醒,性能中等。重量级锁则会把获取不到锁的线程阻塞,性能最差。
43+
44+
##2. 可重入锁/非可重入锁
45+
46+
第 2 个分类是可重入锁和非可重入锁。可重入锁指的是线程当前已经持有这把锁了,能在不释放这把锁的情况下,再次获取这把锁。同理,不可重入锁指的是虽然线程当前持有了这把锁,但是如果想再次获取这把锁,也必须要先释放锁后才能再次尝试获取。
47+
48+
对于可重入锁而言,最典型的就是 ReentrantLock 了,正如它的名字一样,reentrant 的意思就是可重入,它也是 Lock 接口最主要的一个实现类。
49+
50+
##3.共享锁/独占锁
51+
52+
第 3 种分类标准是共享锁和独占锁。共享锁指的是我们同一把锁可以被多个线程同时获得,而独占锁指的就是,这把锁只能同时被一个线程获得。我们的读写锁,就最好地诠释了共享锁和独占锁的理念。读写锁中的读锁,是共享锁,而写锁是独占锁。读锁可以被同时读,可以同时被多个线程持有,而写锁最多只能同时被一个线程持有。
53+
54+
##4.公平锁/非公平锁
55+
56+
第 4 种分类是公平锁和非公平锁。公平锁的公平的含义在于如果线程现在拿不到这把锁,那么线程就都会进入等待,开始排队,在等待队列里等待时间长的线程会优先拿到这把锁,有先来先得的意思。而非公平锁就不那么“完美”了,它会在一定情况下,忽略掉已经在排队的线程,发生插队现象。
57+
58+
##5.悲观锁/乐观锁
59+
60+
第 5 种分类是悲观锁,以及与它对应的乐观锁。悲观锁的概念是在获取资源之前,必须先拿到锁,以便达到“独占”的状态,当前线程在操作资源的时候,其他线程由于不能拿到锁,所以其他线程不能来影响我。而乐观锁恰恰相反,它并不要求在获取资源前拿到锁,也不会锁住资源;相反,乐观锁利用 CAS 理念,在不独占资源的情况下,完成了对资源的修改。
61+
62+
##6.自旋锁/非自旋锁
63+
64+
第 6 种分类是自旋锁与非自旋锁。自旋锁的理念是如果线程现在拿不到锁,并不直接陷入阻塞或者释放 CPU 资源,而是开始利用循环,不停地尝试获取锁,这个循环过程被形象地比喻为“自旋”,就像是线程在“自我旋转”。相反,非自旋锁的理念就是没有自旋的过程,如果拿不到锁就直接放弃,或者进行其他的处理逻辑,例如去排队、陷入阻塞等。
65+
66+
##7.可中断锁/不可中断锁
67+
68+
第 7 种分类是可中断锁和不可中断锁。在 Java 中,synchronized 关键字修饰的锁代表的是不可中断锁,一旦线程申请了锁,就没有回头路了,只能等到拿到锁以后才能进行其他的逻辑处理。而我们的 ReentrantLock 是一种典型的可中断锁,例如使用 lockInterruptibly 方法在获取锁的过程中,突然不想获取了,那么也可以在中断之后去做其他的事情,不需要一直傻等到获取到锁才离开。
69+
70+
##总结
71+
本文中我们首先会对锁的分类有一个整体的概念,了解锁究竟有哪些分类标准。然后在后续的课程中,会对其中重要的锁进行详细讲解。
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
2+
#不要在混淆JVM内存结构与Java内存模型了!
3+
4+
>本文我们一起学习什么是Java内存模型。
5+
6+
在学习Java内存模型之前,我们简单了解一下什么JVM内存结构。
7+
8+
##JVM内存结构
9+
10+
Java 代码是要运行在虚拟机上的,而虚拟机在执行 Java 程序的过程中会把所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途。 JVM 运行时内存区域结构可分为以下 6 个区。
11+
12+
1. 堆区(Heap):堆是存储类实例和数组的,通常是内存中最大的一块。实例很好理解,比如 new Object() 就会生成一个实例;而数组也是保存在堆上面的,因为在 Java 中,数组也是对象。
13+
14+
2. 虚拟机栈(Java Virtual Machine Stacks):它保存局部变量和部分结果,并在方法调用和返回中起作用。
15+
16+
3. 方法区(Method Area):它存储每个类的结构,例如运行时的常量池、字段和方法数据,以及方法和构造函数的代码,包括用于类初始化以及接口初始化的特殊方法。
17+
18+
4. 本地方法栈(Native Method Stacks):与虚拟机栈基本类似,区别在于虚拟机栈为虚拟机执行的 Java 方法服务,而本地方法栈则是为 Native 方法服务。
19+
20+
5. 程序计数器(The PC Register):是最小的一块内存区域,它的作用通常是保存当前正在执行的 JVM 指令地址。
21+
22+
6. 运行时常量池(Run-Time Constant Pool):是方法区的一部分,包含多种常量,范围从编译时已知的数字到必须在运行时解析的方法和字段引用。
23+
24+
##JMM内存模型是什么
25+
26+
JMM其实是一种规范,是一种和多线程相关的一组规范,需要各个 JVM 的实现来遵守 JMM 规范,以便于开发者可以利用这些规范,更方便地开发多线程程序。这样一来,即便同一个程序在不同的虚拟机上运行,得到的程序结果也是一致的。
27+
28+
如果没有 JMM 内存模型来规范,那么很可能在经过了不同 JVM 的“翻译”之后,导致在不同的虚拟机上运行的结果不一样,那是很大的问题。这也是为什么需要JMM的一个很重要的原因。
29+
30+
因此,JMM 与处理器、缓存、并发、编译器有关。它解决了 CPU 多级缓存、处理器优化、指令重排等导致的结果不可预期的问题。
31+
##总结
32+
本文我们学习了JVM内存结构与Java内存模型这两个容易混淆的概念,理解了JMM内存模型。其实之前我们使用了各种同步工具和关键字,包括 volatile、synchronized、Lock 等,其实它们的原理都涉及 JMM。正是 JMM 的参与和帮忙,才让各个同步工具和关键字能够发挥作用,帮我们开发出并发安全的程序。
33+
34+
35+
36+

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp