赞
踩
本文快速回顾了常考的的知识点,用作面试复习,事半功倍。
知乎阅读体验不好,请移步Csdn:Java并发知识点快速复习手册(下) - Rude3Knife的博客 - CSDN博客
已发布知识点复习手册
本文内容参考自CyC2018的Github仓库:CS-Notes
https://github.com/CyC2018/CS-Notes/
有删减,修改,补充额外增加内容
本作品采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可。
如果多个线程对同一个共享数据进行访问而不采取同步操作的话,那么操作的结果是不一致的。
以下代码演示了 1000 个线程同时对 cnt 执行自增操作,操作结束之后它的值有可能小于 1000。
public
Java 内存模型试图屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。
处理器上的寄存器的读写的速度比内存快几个数量级,为了解决这种速度矛盾,在它们之间加入了高速缓存。
加入高速缓存带来了一个新的问题:缓存一致性。如果多个缓存共享同一块主内存区域,那么多个缓存的数据可能会不一致,需要一些协议来解决这个问题。
所有的变量都存储在主内存中,每个线程还有自己的工作内存,工作内存存储在高速缓存或者寄存器中,保存了该线程使用的变量的主内存副本拷贝。
线程只能直接操作工作内存中的变量,不同线程之间的变量值传递需要通过主内存来完成。
Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互操作。
Java 内存模型保证了 read、load、use、assign、store、write、lock 和 unlock 操作具有原子性
Java 内存模型保证了 read、load、use、assign、store、write、lock 和 unlock 操作具有原子性,例如对一个 int 类型的变量执行 assign 赋值操作,这个操作就是原子性的。但是 Java 内存模型允许虚拟机将没有被 volatile 修饰的 64 位数据(long,double)的读写操作划分为两次 32 位的操作来进行,即 load、store、read 和 write 操作可以不具备原子性。
有一个错误认识就是,int 等原子性的类型在多线程环境中不会出现线程安全问题。前面的线程不安全示例代码中,cnt 属于 int 类型变量,1000 个线程对它进行自增操作之后,得到的值为 997 而不是 1000。
使用 AtomicInteger 重写之前线程不安全的代码之后得到以下线程安全实现:
public
除了使用原子类之外,也可以使用 synchronized 互斥锁来保证操作的原子性。它对应的内存间交互操作为:lock 和 unlock,在虚拟机实现上对应的字节码指令为 monitorenter 和 monitorexit。
public
可见性指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。
Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的。
主要有有三种实现可见性的方式:
有序性是指:在本线程内观察,所有操作都是有序的。在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序。
在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
https://blog.csdn.net/qq_30137611/article/details/78146864
happens-before是判断数据是否存在竞争、线程是否安全的重要依据
定义:
如果操作A happens-before 于 操作B,那么就可以确定,操作B执行完之后,j 的值一定为 1;因为happens-before关系可以向程序员保证:在操作B执行之前,操作A的执行后的影响[或者说结果](修改 i 的值)操作B是可以观察到的[或者说可见的]
这里列举几个常见的Java“天然的”happens-before关系
① 程序顺序规则: 一个线程中的每个操作,happens-before于该线程中的任意后续操作(也就是说你写的操作,如果是单线程执行,那么前面的操作[程序逻辑上的前]就会happens-before于后面的操作) 这里的影响指修改了 i 变量的值
② 监视器锁规则: 对一个锁的解锁,happens-before 于随后对这个锁的加锁
③ volatile变量规则: 对一个 volatile域的写,happens-before于任意后续对这个volatile域的读
④ 传递性:如果 A happens-before B,且 B happens-before C,那么A happens-before C
上面提到了可以用 volatile 和 synchronized 来保证有序性。除此之外,JVM 还规定了先行发生原则,让一个操作无需控制就能先于另一个操作完成。
主要有以下这些原则:
Single Thread rule
在一个线程内,在程序前面的操作先行发生于后面的操作。
Monitor Lock Rule
一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。
Volatile Variable Rule
对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。
Thread Start Rule
Thread 对象的 start() 方法调用先行发生于此线程的每一个动作。
Thread Join Rule
Thread 对象的结束先行发生于 join() 方法返回。
Thread Interruption Rule
对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 interrupted() 方法检测到是否有中断发生。
Finalizer Rule
一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。
Transitivity
如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C。
https://blog.csdn.net/u010687392/article/details/50549236
采用Lock-Free算法替代锁,加上原子操作指令实现并发情况下资源的安全、完整、一致性
而关于Lock-Free算法,则是一种新的策略替代锁来保证资源在并发时的完整性的,Lock-Free的实现有三步:
- 1、循环(for(;;)、while)
- 2、CAS(CompareAndSet)
- 3、回退(return、break)
https://www.jianshu.com/p/195ae7c77afe
通过关键字sychronize可以防止多个线程进入同一段代码,在某些特定场景中,volatile相当于一个轻量级的sychronize,因为不会引起线程的上下文切换
内存屏障,又称内存栅栏,是一个CPU指令。在程序运行时,为了提高执行性能,编译器和处理器会对指令进行重排序
JMM为了保证在不同的编译器和CPU上有相同的结果,通过插入特定类型的内存屏障来禁止特定类型的编译器重排序和处理器重排序,插入一条内存屏障会告诉编译器和CPU:不管什么指令都不能和这条Memory Barrier指令重排序。
一般来说,volatile大多用于标志位上(判断操作),
但是由于操作上的优势,只需要简单的声明一下即可,而且被它声明的代码块也是具有操作的原子性。
ThreadLocal提供了线程的局部变量,每个线程都可以通过set()和get()来对这个局部变量进行操作,但不会和其他线程的局部变量进行冲突,实现了线程的数据隔离。
而ThreadLocal的设计,并不是解决资源共享的问题,而是用来提供线程内的局部变量,这样每个线程都自己管理自己的局部变量,别的线程操作的数据不会对我产生影响,相当于封装在Thread内部了,供线程自己管理。
它有三个暴露的方法,set、get、remove。
如果ThreadLocal不设为static的,由于Thread的生命周期不可预知,这就导致了当系统gc时将会回收它,而ThreadLocal对象被回收了,此时它对应key必定为null,这就导致了该key对应得value拿不出来了,而value之前被Thread所引用,所以就存在key为null、value存在强引用导致这个Entry回收不了,从而导致内存泄露。
避免内存泄露的方法:
threadLocal能够实现当前线程的操作都是用同一个Connection,保证了事务!
关于Volatile关键字具有可见性,但不具有操作的原子性,而synchronized比volatile对资源的消耗稍微大点,但可以保证变量操作的原子性,保证变量的一致性,最佳实践则是二者结合一起使用。
1、synchronized:解决多线程资源共享的问题,同步机制采用了“以时间换空间”的方式:访问串行化,对象共享化。同步机制是提供一份变量,让所有线程都可以访问。
2、对于Atomic的出现,是通过原子操作指令+Lock-Free完成,从而实现非阻塞式的并发问题。
3、对于Volatile,为多线程资源共享问题解决了部分需求,在非依赖自身的操作的情况下,对变量的改变将对任何线程可见。
4、对于ThreadLocal的出现,并不是解决多线程资源共享的问题,而是用来提供线程内的局部变量,省去参数传递这个不必要的麻烦,ThreadLocal采用了“以空间换时间”的方式:访问并行化,对象独享化。ThreadLocal是为每一个线程都提供了一份独有的变量,各个线程互不影响。
等待IO的方式:阻塞,非阻塞
获得通知的方式:异步,非异步
多个线程不管以何种方式访问某个类,并且在主调代码中不需要进行同步,都能表现正确的行为。
线程安全有以下几种实现方式:
不可变(Immutable)的对象一定是线程安全的,不需要再采取任何的线程安全保障措施。只要一个不可变的对象被正确地构建出来,永远也不会看到它在多个线程之中处于不一致的状态。多线程环境下,应当尽量使对象成为不可变,来满足线程安全。
不可变的类型:
对于集合类型,可以使用 Collections.unmodifiableXXX() 方法来获取一个不可变的集合。
public
Collections.unmodifiableXXX() 先对原始的集合进行拷贝,需要对集合进行修改的方法都直接抛出异常。
public
synchronized 和 ReentrantLock。
互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步(Blocking Synchronization)。
从处理问题的方式上说,互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施(例如加锁),那就肯定会出现问题。
随着硬件指令集的发展,我们有了另外一个选择:基于冲突检测的乐观并发策略,通俗地说,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施(最常见的补偿措施就是不断地重试,直到成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步(Non-Blocking Synchronization)。
乐观锁需要操作和冲突检测这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。
硬件支持的原子性操作最典型的是:比较并交换(Compare-and-Swap,CAS)。
CAS 指令需要有 3 个操作数,分别是: - 内存位置(在 Java 中可以简单理解为变量的内存地址,用 V 表示) - 旧的预期值(用 A 表示) - 新值(用 B 表示)。
当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
但是无论是否更新了 V 的值,都会返回 V 的旧值,上述的处理过程是一个原子操作。
当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值(A和内存值V相同时,将内存值V修改为B),而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试(否则什么都不做)
J.U.C 包里面的整数原子类 AtomicInteger,其中的 compareAndSet() 和 getAndIncrement() 等方法都使用了 Unsafe 类的 CAS 操作。
J.U.C 包里面的整数原子类 AtomicInteger 的方法调用了 Unsafe 类的 CAS 操作。
以下代码使用了 AtomicInteger 执行了自增的操作。
private
以下代码是 incrementAndGet() 的源码,它调用了 Unsafe 的 getAndAddInt() 。
public
以下代码是 getAndAddInt() 源码,var1 指示对象内存地址,var2 指示该字段相对对象内存地址的偏移,var4 指示操作需要加的数值,这里为 1。通过 getIntVolatile(var1, var2) 得到旧的预期值,通过调用 compareAndSwapInt() 来进行 CAS 比较,如果该字段内存地址中的值等于 var5,那么就更新内存地址为 var1+var2 的变量为 var5+var4。
可以看到 getAndAddInt() 在一个循环中进行,发生冲突的做法是不断的进行重试。
public
如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。
J.U.C 包提供了一个带有标记的原子引用类“AtomicStampedReference”来解决这个问题,它可以通过控制变量值的版本来保证 CAS 的正确性。大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。
要保证线程安全,并不是一定就要进行同步。如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性。
多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有的。
public
如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。
符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽量在一个线程中消费完。其中最重要的一个应用实例就是经典 Web 交互模型中的“一个请求对应一个服务器线程”(Thread-per-Request)的处理方式,这种处理方式的广泛应用使得很多 Web 服务端应用都可以使用线程本地存储来解决线程安全问题。
可以使用 java.lang.ThreadLocal 类来实现线程本地存储功能。
对于以下代码,thread1 中设置 threadLocal 为 1,而 thread2 设置 threadLocal 为 2。过了一段时间之后,thread1 读取 threadLocal 依然是 1,不受 thread2 的影响。
public
为了理解 ThreadLocal,先看以下代码:
public
每个 Thread 都有一个 ThreadLocal.ThreadLocalMap 对象。
/* ThreadLocal values pertaining to this thread. This map is maintained
当调用一个 ThreadLocal 的 set(T value) 方法时,先得到当前线程的 ThreadLocalMap 对象,然后将 ThreadLocal->value 键值对插入到该 Map 中。
public
get() 方法类似。
public
ThreadLocal 从理论上讲并不是用来解决多线程并发问题的,因为根本不存在多线程竞争。
在一些场景 (尤其是使用线程池) 下,由于 ThreadLocal.ThreadLocalMap 的底层数据结构导致 ThreadLocal 有内存泄漏的情况,应该尽可能在每次使用 ThreadLocal 后手动调用 remove(),以避免出现 ThreadLocal 经典的内存泄漏甚至是造成自身业务混乱的风险。
这种代码也叫做纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误。
可重入代码有一些共同的特征,例如不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法等。
这里的锁优化主要是指虚拟机对synchronized的优化。
锁竞争是kernal mode下的,会经过user mode(用户态)到kernal mode(内核态) 的切换,是比较花时间的。
自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态。
它只适用于共享数据的锁定状态很短的场景。
自旋次数的默认值是 10 次,用户可以使用虚拟机参数 -XX:PreBlockSpin 来更改。
在 JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的次数不再固定了,而是由前一次在同一个锁上的自旋次数及锁的拥有者的状态来决定。
锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除。检测到某段代码是线程安全的(言外之意:无锁也是安全的),JVM会安全地原有的锁消除掉!
逃逸分析:如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们上的锁进行消除。
对于一些看起来没有加锁的代码,其实隐式的加了很多锁。例如下面的字符串拼接代码就隐式加了锁:
public
String 是一个不可变的类,Javac 编译器会对 String 的拼接自动优化。在 JDK 1.5 之前,会转化为 StringBuffer 对象的连续 append() 操作,在 JDK 1.5 及以后的版本中,会转化为 StringBuilder 对象的连续 append() 操作,即上面的代码可能会变成下面的样子:
public
虚拟机观察变量 sb,很快就会发现它的动态作用域被限制在 concatString() 方法内部。也就是说,sb 的所有引用永远不会“逃逸”到 concatString() 方法之外,其他线程无法访问到它。因此,虽然这里有锁,但是可以被安全地消除掉。
如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁的加锁操作就会导致性能损耗。
上一节的示例代码中连续的 append() 方法就属于这类情况。如果虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部。对于上一节的示例代码就是扩展到第一个 append() 操作之前直至最后一个 append() 操作之后,这样只需要加锁一次就可以了。
但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,频繁地进行互斥同步操作也会导致不必要的性能损耗。
总结:在无竞争环境下,把整个同步都消除,CAS也不做。
偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程在之后获取该锁就不再需要进行同步操作,甚至连 CAS 操作也不再需要。
可以使用 -XX:+UseBiasedLocking=true 开启偏向锁,不过在 JDK 1.6 中它是默认开启的。
当锁对象第一次被线程获得的时候,进入偏向状态,标记为 1 01。同时使用 CAS 操作将线程 ID 记录到 Mark Word 中,如果 CAS 操作成功,这个线程以后每次进入这个锁相关的同步块就不需要再进行任何同步操作。
当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定状态或者轻量级锁状态。
轻量级锁是相对于传统的重量级锁而言,它使用 CAS 操作来避免重量级锁使用互斥量的开销。对于绝大部分的锁,在整个同步周期内都是不存在竞争的,因此也就不需要都使用互斥量进行同步,可以先采用 CAS 操作进行同步,如果 CAS 失败了再改用互斥量进行同步。(乐观锁)
JDK 1.6 引入了偏向锁和轻量级锁,从而让锁拥有了四个状态:无锁状态(unlocked)、偏向锁状态(biasble)、轻量级锁状态(lightweight locked)和重量级锁状态(inflated)。
如果 CAS 操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的虚拟机栈,如果是的话说明当前线程已经拥有了这个锁对象,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程线程抢占了。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁。
但如果存在锁竞争,除了互斥量的开销外,还额外发生了CAS操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢。
简单来说:如果发现同步周期内都是不存在竞争,JVM会使用CAS操作来替代操作系统互斥量。这个优化就被叫做轻量级锁。
https://www.cnblogs.com/suneryong/p/6726413.html
不管是同步集合还是并发集合他们都支持线程安全,他们之间主要的区别体现在性能和可扩展性,还有他们如何实现的线程安全。同步HashMap, Hashtable, HashSet, Vector, ArrayList 相比他们并发的实现(比如:ConcurrentHashMap, CopyOnWriteArrayList, CopyOnWriteHashSet)会慢得多。造成如此慢的主要原因是锁, 同步集合会把整个Map或List锁起来,而并发集合不会。并发集合实现线程安全是通过使用先进的和成熟的技术像锁剥离。比如ConcurrentHashMap 会把整个Map 划分成几个片段,只对相关的几个片段上锁,同时允许多线程访问其他未上锁的片段。
java.util.concurrent包中包含的并发集合类如下:
- ConcurrentHashMap
-
- CopyOnWriteArrayList
-
- CopyOnWriteArraySet
常见逸出的有下面几种方式:
具体解释见:https://segmentfault.com/a/1190000014546223#articleHeader4
安全发布对象有几种常见的方式:
https://segmentfault.com/a/1190000014747667#articleHeader5
上面transferMoney()发生死锁的原因是因为加锁顺序不一致而出现的~
例子中,改造为得到对应的hash值来固定加锁的顺序,这样我们就不会发生死锁的问题了!
如果在调用某个方法时不需要持有锁,那么这种调用被称为开放调用!
使用显式Lock锁,在获取锁时使用tryLock()方法。当等待超过时限的时候,tryLock()不会一直等待,而是返回错误信息。
我是蛮三刀把刀,目前为后台开发工程师。主要关注后台开发,网络安全,Python爬虫等技术。
来微信和我聊聊:yangzd1102
Github:https://github.com/qqxx6661
同步更新以下博客
1. Csdn
http://blog.csdn.net/qqxx6661
拥有专栏:Leetcode题解(Java/Python)、Python爬虫开发
2. 知乎
https://www.zhihu.com/people/yang-zhen-dong-1/
拥有专栏:码农面试助攻手册
3. 掘金
https://juejin.im/user/5b48015ce51d45191462ba55
4. 简书
https://www.jianshu.com/u/b5f225ca2376
本人长期维护的个人项目,完全免费,请大家多多支持。
实现功能
网站地址
https://pricemonitor.online/
如果文章对你有帮助,不妨收藏起来并转发给您的朋友们~
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。