(2.1.27)Java并发编程,2.1.27java并发编程
(2.1.27)Java并发编程,2.1.27java并发编程
- (2.1.27)Java并发编程
- (2.1.27.1)Java并发编程:并发
- (2.1.27.2)Java并发编程:JAVA的内存模型
- (2.1.27.3)Java并发编程:CAS操作
- (2.1.27.4)Java并发编程:原子类Atomic
- (2.1.27.5)Java并发编程:Volatile
- (2.1.27.6)Java并发编程:synchronized
- (2.1.27.7)Java并发编程:Object.wait/notify
- (2.1.27.8)Java并发编程:Lock显示锁
- (2.1.27.9)Java并发编程:Lock之AQS
- (2.1.27.10)Java并发编程:Lock之ReentrantLock独享式重入锁
- (2.1.27.11)Java并发编程:Lock之ReentrantReadWriteLock 读写分离独享式重入锁
- (2.1.27.12)Java并发编程:Lock之Semaphore共享式不可重入锁
- (2.1.27.13)Java并发编程:Lock之CountDownLatch计数式独享锁
- (2.1.27.14)Java并发编程:Lock之Condition等待通知
- (2.1.27.15)Java并发编程:Lock之CyclicBarrier公共屏障
- (2.1.27.16)Java并发编程:Lock之ConcurrentHashMap
文章目录
- 一、Java 内存模型中的可见性、原子性和有序性
- 1.1 可见性
- 1.2 原子性
- 1.3 有序性
- 二、线程安全
- 2.1 不安全线程的示例
- 2.2 线程安全的实现方法
- 2.2.1 互斥同步(悲观锁)
- 2.2.2 非阻塞式同步(乐观锁)
- 三、乐观锁与悲观锁
- 3.1 乐观锁的缺点
- 3.1.1 ABA 问题
- 3.1.2 循环时间长开销大
- 3.1.3 只能保证一个共享变量的原子操作
- 四、公平锁和非公平锁
- 五、可重入锁
- 六、可中断锁
- 参考文献
- 物理计算机的缓存不一致问题
- 总线锁
- 缓存不一致协议
- 缓存锁
- 物理计算机的单线程中顺序程序的乱序执行
- 数据依赖
- 重排序规则(as-if-serial)
- Java中的工作内存和主内存不一致问题
- 八种原子操作规则
- Java中单线程顺序代码乱序执行在多线程中引发的问题
- Happens-Before 原则
- CAS操作:其实就是一次赋值过程,只不过这个赋值过程前需要校验正确性,不正确则直接关闭。
- 当需要更新时,判断当前内存值与之前取到的值是否相等,若相等,则用新值更新,若失败则重试,一般情况下是一个自旋操作,即外部包裹一个无限循环,不断的重试。
- 自旋操作:由于线程的阻塞和唤醒都是需要用户态和核心态的转化, 耗费较多的CPU资源,因此一些轻量级锁会通过自旋操作去执行命令(通过一个无限循环,内部包裹并触发CAS操作),而不进行用户态的转变。
- 乐观锁与悲观锁
一、Java 内存模型中的可见性、原子性和有序性
1.1 可见性
可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。
也就是一个线程修改的结果,另一个线程马上就能看到。比如:
用volatile修饰的变量,就会具有可见性。volatile修饰的变量不允许线程内部缓存和重排序,即直接修改内存。所以对其他线程是可见的。但是这里需要注意一个问题,volatile只能让被他修饰内容具有可见性,但不能保证它具有原子性。比如 volatile int a = 0;之后有一个操作 a++;这个变量a具有可见性,但是a++ 依然是一个非原子操作,也就是这个操作同样存在线程安全问题。
在 Java 中 volatile、synchronized 和 final 实现可见性。
1.2 原子性
原子是世界上的最小单位,具有不可分割性。
- 比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作时原子操作。
- 再比如:a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。
非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。
java的concurrent包下提供了一些原子类,我们可以通过阅读API来了解这些原子类的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。
在 Java 中 synchronized 和在 lock、unlock 中操作保证原子性。
1.3 有序性
Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性
- volatile 是因为其本身包含“禁止指令重排序”的语义
- synchronized 是由“一个变量在同一个时刻只允许一条线程对其进行 lock 操作”这条规则获得的,此规则决定了持有同一个对象锁的两个同步块只能串行执行。
二、线程安全
线程安全的定义:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么这个类就是线程安全的。
2.1 不安全线程的示例
- 我们创建Count类,在该类中有一个count()方法,计算从1一直加到10的和,在计算完后输出当前线程的名称与计算的结果
- 我们开启多个线程,这些线程共享一个Count实例,并调用其count()方法
- 我们期望线程输出的结果是首项为55且等差为55的等差数列
class ThreadNotSafeDemo {
private static class Count {
private int num;
private void count() {
for (int i = 1; i <= 10; i++) {
num += i;
}
System.out.println(Thread.currentThread().getName() + "-" + num);
}
}
public static void main(String[] args) {
Runnable runnable = new Runnable() {
Count count = new Count();
public void run() {
count.count();
}
};
//创建10个线程,
for (int i = 0; i < 10; i++) {
new Thread(runnable).start();
}
}
}
但是结果并不是我们期望的。具体结果如下图所示:
【线程安全结果图】
我们可以看见,线程并没有按照我们之间想的那样,线程按照从Thread-0到Thread-9依次排列,并且Thread-0与Thread-1线程输出的结果是错误的。
之所以会出现这样的情况,是CPU在调度的时候线程是可以交替执行的
具体来讲是因为:
2.2 线程安全的实现方法
上面我们了解了之所以会出现线程安全的问题,主要原因就是因为存在多条线程共同操作共享数据,同时CPU的调度的时候线程是可以交替执行的,导致了程序的语义发生改变,所以会出现与我们预期的结果违背的情况。
2.2.1 互斥同步(悲观锁)
互斥同步(Mutual Exclusion&Synchronization)是常见的一种并发正确性保障手段。互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步(Blocking Synchronization)
同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个(或者是一些,使用信号量的时候)线程使用。而互斥是实现同步的一种手段,临界区(CriticalSection)、互斥量(Mutex)和信号量(Semaphore)都是主要的互斥实现方式。因此,在这4个字里面,互斥是因,同步是果;互斥是方法,同步是目的。
2.2.2 非阻塞式同步(乐观锁)
从处理问题的方式上说,互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施(例如加锁),那就肯定会出现问题,无论共享数据是否真的会出现竞争,它都要进行“加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作”。
随着硬件指令集的发展,我们有了另外一个选择:基于冲突检测的乐观并发策略
通俗地说,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施(最常见的补偿措施就是不断地重试,直到成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步(Non-Blocking Synchronization)。
为什么笔者说使用乐观并发策略需要“硬件指令集的发展”才能进行呢?
因为我们需要操作和冲突检测这两个步骤具备原子性,靠什么来保证呢? 如果这里再使用互斥同步来保证就失去意义了,所以我们只能靠硬件来完成这件事情,硬件保证一个从语义上看起来需要多次操作的行为只通过一条处理器指令就能完成,
三、乐观锁与悲观锁
针对共享数据的多线程并发控制,就有了锁的概念
-
悲观锁
-
乐观锁
从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种。
悲观锁往往引起进程挂起和恢复执行,这将导致很大的开销。举个例子,如果一个线程需要某个资源,但是这个资源的占用时间很短,当线程第一次抢占这个资源时,可能这个资源被占用,如果此时挂起这个线程,可能立刻就发现资源可用,然后又需要花费很长的时间重新抢占锁,时间代价就会非常的高。相比之下,乐观锁:每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。在上面的例子中,某个线程可以不让出cpu,而是一直while循环,如果失败就重试,直到成功为止。所以,当数据争用不严重时,乐观锁效果更好。
同样的,乐观锁适用于Write写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
3.1 乐观锁的缺点
ABA 问题是乐观锁一个常见的问题
3.1.1 ABA 问题
如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?
很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。
这个问题被称为CAS操作的 “ABA”问题。其实际影响可以用下文解释:
假设有一个遵循CAS原理的提款机,小灰有100元存款,要用这个提款机来提款50元。
由于提款机硬件出了点小问题,小灰的提款操作被同时提交两次,开启了两个线程,两个线程都是获取当前值100元,要更新成50元。
理想情况下,应该一个线程更新成功,另一个线程更新失败,小灰的存款只被扣一次。
线程1首先执行成功,把余额从100改成50。线程2因为某种原因阻塞了。这时候,小灰的妈妈刚好给小灰汇款50元。
线程2仍然是阻塞状态,线程3执行成功,把余额从50改成100。
线程2恢复运行,由于阻塞之前已经获得了“当前值”100,并且经过compare检测,此时存款实际值也是100,所以成功把变量值100更新成了50。
ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。
JDK 1.5 以后的 AtomicStampedReference
类就提供了此种能力,更新一个“对象-引用”二元组,通过在引用上加上“版本号”,从而避免ABA问题。其中的 compareAndSet 方法就是首先检查 (当前引用是否等于预期引用,并且当前标志是否等于预期标志),如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
3.1.2 循环时间长开销大
自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会一直占用CPU的资源,会给CPU带来非常大的执行开销,譬如AtomicInteger#incrementAndGet中的循环
如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
3.1.3 只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,自旋CAS就无法保证操作的原子性,这个时候就可以用锁。
还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。
从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。
四、公平锁和非公平锁
CPU在调度线程的时候是在等待队列里随机挑选一个线程,由于这种随机性所以是无法保证线程先到先得的(synchronized控制的锁就是这种非公平锁)。
但这样就会产生饥饿现象,即有些线程(优先级较低的线程)可能永远也无法获取CPU的执行权,优先级高的线程会不断的强制它的资源。
那么如何解决饥饿问题呢?
这就需要公平锁了。公平锁可以保证线程按照时间的先后顺序执行,避免饥饿现象的产生。但公平锁的效率比较低,因为要实现顺序执行,需要维护一个有序队列。
//ReentrantLock便是一种公平锁,通过在构造方法中传入true就是公平锁,传入false,就是非公平锁。
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
以下是使用公平锁实现的效果:
public class LockFairTest implements Runnable{
//创建公平锁
private static ReentrantLock lock=new ReentrantLock(true);
public void run() {
while(true){
lock.lock();
try{
System.out.println(Thread.currentThread().getName()+"获得锁");
}finally{
lock.unlock();
}
}
}
public static void main(String[] args) {
LockFairTest lft=new LockFairTest();
Thread th1=new Thread(lft);
Thread th2=new Thread(lft);
th1.start();
th2.start();
}
}
输出结果:
Thread-1获得锁
Thread-0获得锁
Thread-1获得锁
Thread-0获得锁
Thread-1获得锁
Thread-0获得锁
Thread-1获得锁
Thread-0获得锁
Thread-1获得锁
Thread-0获得锁
Thread-1获得锁
Thread-0获得锁
Thread-1获得锁
Thread-0获得锁
Thread-1获得锁
Thread-0获得锁
这是截取的部分执行结果,分析结果可看出两个线程是交替执行的,几乎不会出现同一个线程连续执行多次。
五、可重入锁
当一个线程得到一个对象后,再次请求该对象锁时是可以再次得到该对象的锁的。像synchronized和ReentrantLock都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。
具体概念就是:自己可以再次获取自己的内部锁。
public class SynchronizedTest {
public void method1() {
synchronized (SynchronizedTest.class) {
System.out.println("方法1获得ReentrantTest的锁运行了");
method2();
}
}
public void method2() {
synchronized (SynchronizedTest.class) {
System.out.println("方法1里面调用的方法2重入锁,也正常运行了");
}
}
public static void main(String[] args) {
new SynchronizedTest().method1();
}
}
上面便是synchronized的重入锁特性,即调用method1()方法时,已经获得了锁,此时内部调用method2()方法时,由于本身已经具有该锁,所以可以再次获取。
public class ReentrantLockTest {
private Lock lock = new ReentrantLock();
public void method1() {
lock.lock();
try {
System.out.println("方法1获得ReentrantLock锁运行了");
method2();
} finally {
lock.unlock();
}
}
public void method2() {
lock.lock();
try {
System.out.println("方法1里面调用的方法2重入ReentrantLock锁,也正常运行了");
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
new ReentrantLockTest().method1();
}
}
上面便是ReentrantLock的重入锁特性,即调用method1()方法时,已经获得了锁,此时内部调用method2()方法时,由于本身已经具有该锁,所以可以再次获取。
六、可中断锁
在Java中,synchronized就不是可中断锁,而Lock是可中断锁。
如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。
lockInterruptibly()的用法体现了Lock的可中断性。
参考文献
- 概念
- 最通俗易懂的乐观锁与悲观锁原理及实现
- 面试必备之乐观锁与悲观锁
- Java高效并发之乐观锁悲观锁、(互斥同步、非互斥同步)
- Java并发编程
- Java并发编程之Java内存模型
- Java并发编程之Java CAS操作
- Java并发编程之Volatile
- Java并发编程之synchronized
- Java并发编程之原子类
- Java并发编程之锁机制之引导篇
- Java并发编程之锁机制之Lock接口
- Java并发编程之锁机制之AQS
- java多线程–我是家宝
- Java显式锁学习总结之一:概论
- Java显式锁学习总结之二:使用AbstractQueuedSynchronizer构建同步组件
- Java显式锁学习总结之三:AbstractQueuedSynchronizer的实现原理
- Java显式锁学习总结之四:ReentrantLock源码分析
- Java显式锁学习总结之五:ReentrantReadWriteLock源码分析
- Java显式锁学习总结之六:Condition源码分析
- Volatile
- Java中Volatile关键字详解
- Object对象中的wait,notify
- Java Object对象中的wait,notify,notifyAll通俗理解
- JVM源码分析之Object.wait/notify实现
- JVM源码分析之Object.wait/notify(All)完全解读
- Lock与AQS
- Java显式锁学习总结之一:概论
- Java显式锁学习总结之三:AbstractQueuedSynchronizer的实现原理
- 深入浅出Java并发包—锁机制(二)
- AbstractQueuedSynchronizer(二)——acquire/acquireQueued方法
- AQS源码
- Java多线程(七)之同步器基础:AQS框架深入分析
- ReentrantLock
- Java并发之ReentrantLock详解
- 轻松学习java可重入锁(ReentrantLock)的实现原理
- Semaphore
- 深入理解Semaphore
- synchronized
- 深入研究 Java Synchronize 和 Lock 的区别与用法
- synchronized和lock比较浅析
- 关于synchronized和ReentrantLock之多线程同步详解
- 读写锁ReentrantReadWriteLock
- java并发编程(五)–Java中的锁(读写锁ReentrantReadWriteLock)
- 可重入读写锁ReentrantReadWriteLock基本原理分析
- Java并发编程–ReentrantReadWriteLock
- 并发锁之二:ReentrantReadWriteLock读写锁
- 并发编程(三):从AQS到CountDownLatch与ReentrantLock
- Condition
- Java显式锁学习总结之六:Condition源码分析
- java并发编程之Condition
- CountDownLatch 与 CyclicBarrier
- Java 线程同步组件 CountDownLatch 与 CyclicBarrier 原理分析
- Java并发Concurrent包的锁(五)——CyclicBarrier源码分析及使用
- Java多线程系列(八)—CyclicBarrier源码分析
- HashMap
- Java并发Concurrent包——ConcurrentHashMap原理分析
相关文章
- 暂无相关文章
用户点评