并发相关

2020-05-12  本文已影响0人  万福来

并发相关

JAVA高性能内存队列-disruptor

JAVA内置队列

image.png

高性能内存队列-disruptor

image.png

disruptor为啥这么快

无锁设计

内部采用CAS方式获取下一个任务序列号,没有锁竞争,不需要线程上下文切换

伪共享问题解决

当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能。


image.png

如何解决?

数组实现

java内存模型是什么

java内存模型和jvm内存结构不是一回事,JMM是为了解决java并发问题提供的一种解决方案。
它定义了线程和主内存之间的抽象关系。线程之间的共享变量存储在主内存中,每个线程都有自己的工作内存,工作内存中存储了该线程读写共享变量的副本。

happens-before关系

在JMM中,如果一个操作的执行结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系,这个的两个操作既可以在同一个线程,也可以在不同的两个线程中。
与程序员密切相关的happens-before规则如下:
1、程序顺序规则:一个线程中的每个操作,happens-before于该线程中任意的后续操作。
2、监视器锁规则:对一个锁的解锁操作,happens-before于随后对这个锁的加锁操作。
3、volatile域规则:对一个volatile域的写操作,happens-before于任意线程后续对这个volatile域的读。
4、传递性规则:如果 A happens-before B,且 B happens-before C,那么A happens-before C。

重排序

在执行程序时,为了提高性能,编译器和处理器会对指令做重排序。所以当初开发者编写的代码顺序和处理器实际运行的代码程序顺序可能发生了变化,这就是重排序,重排序就可能会导致出现内存可见性问题。但是处理器可以通过插入特定类型的内存屏障来禁止指令重排序,从而解决内存可见性问题。

重排序的两个原则

volatile 和 final

synchronized

synchronized是jvm提供的一种用来进行并发控制的多线程锁;synchronized是基于进入和退出monitor对象来实现方法同步和代码块同步的,每一个java对象都可以作为synchronized的锁对象。

synchronized三种用法

synchronized的实现。

JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。代码块同步是使用monitorenter和monitorexit指令实现的,monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。
根据虚拟机规范的要求,在执行monitorenter指令时,首先要去尝试获取对象的锁,如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1;相应地,在执行monitorexit指令时会将锁计数器减1,当计数器被减到0时,锁就释放了。如果获取对象锁失败了,那当前线程就要阻塞等待,直到对象锁被另一个线程释放为止。

synchronized的优化状态

锁状态 优点 缺点 适用场景
偏向锁 加锁状态不需要其他消耗 如果存在锁竞争,会带来额外锁撤销 适用于只有一个线程
轻量级锁 竞争线程不会阻塞,提高程序响应 始终得不到锁的线程,会自旋消耗CPU 适用低延时
重量级锁 线程竞争不会自旋消耗CPU 线程阻塞,响应时间慢 适用高吞吐量

AQS 实现原理

AQS:AbstractQueuedSynchronizer,即队列同步器。它是构建锁或者其他同步组件的基础框架(如ReentrantLock、ReentrantReadWriteLock、Semaphore等),它是JUC并发包中的核心基础组件。
AQS的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态。作者推荐采用静态内部实现类继承AQS抽象类,并通过组合模式来实现锁的功能。AQS其实就是模板模式的实现。
AQS主要提供了两类方法来实现同步器功能

//独享式的尝试获取
protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
}
//独享式的尝试释放
protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
}
// 共享式的尝试获取
protected int tryAcquireShared(int arg) {
        throw new UnsupportedOperationException();
}
// 共享式的尝试释放
protected boolean tryReleaseShared(int arg) {
        throw new UnsupportedOperationException();
}
// 判断是否为独占式
protected boolean isHeldExclusively() {
        throw new UnsupportedOperationException();
}

AQS使用一个int类型的成员变量state来表示同步状态,当state>0时表示已经获取了锁,当state = 0时表示释放了锁。它提供了三个方法(getState()、setState(int newState)、compareAndSetState(int expect,int update))来对同步状态state进行操作,当然AQS可以确保对state的操作是安全的。
AQS通过内置的FIFO同步队列来完成资源获取线程的排队工作,如果当前线程获取同步状态失败(锁)时,AQS则会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态。

CountDownLatch VS CyclicBarrier

实现方式不同

实现原理

Callable实现原理

Callable的实现类只能配合线程池执行

在线程池中有一个submit方法,可以提交Callable类型的实现类;
但是在submit方法内部,其实是用一个FutureTask对象对Callable对象进行封装以后,在通过execute方法进行执行,execute方法只能执行Runnable类型的实现类,所以FutureTask其实也是一个Runnable接口的实现类;FutureTask 内部维护了一个volatile修饰的state状态,新创建的FutureTask实例state状态为新建状态,该状态可以在run方法中进行修改,run方法内部调用了Callable接口的call方法,其实就是使用了组合模式,execute方法执行FutureTask的run方法,其实就是执行Callable接口call方法,在call方法执行完成后,将方法返回值设置到FutureTask对象的成员变量中,然后修改执行状态为完成,最后在唤醒等待队列中的等待获取方法执行结果的线程,所以如果一个线程调用FutureTask对象的get方法,内部会先根据状态值判断方法十分执行完成,如果没有执行完成则加入阻塞队列中进行等待。

读写锁

ReentrantReadWriteLock可重入读写锁(实现ReadWriteLock接口)

使用:ReentrantReadWriteLock是ReadWriteLock接口的实现类。ReadWriteLock接口的核心方法是readLock(),writeLock()。实现了并发读、互斥写。但读锁会阻塞写锁,是悲观锁的策略。
ReentrantReadWriteLock有5个静态方法:

性能和建议:适用于读多写少的情况。性能较高。

StampedLock戳锁 一个高性能的读写锁

使用:
StampedLock控制锁有三种模式(排它写,悲观读,乐观读),一个StampedLock状态是由版本和模式两个部分组成,锁获取方法返回一个数字作为票据stamp,它用相应的锁状态表示并控制访问。
原理:
StampedLockd的内部实现是基于CLH(自旋锁队列)锁的,一种自旋锁,保证没有饥饿且FIFO。
CLH锁原理:锁维护着一个等待线程队列,所有申请锁且失败的线程都记录在队列。一个节点代表一个线程,保存着一个标记位locked,用以判断当前线程是否已经释放锁。当一个线程试图获取锁时,从队列尾节点作为前序节点,循环判断所有的前序节点是否已经成功释放锁。
StampedLockd内部维护了一个StampedLockd,源码中的WNote就是等待链表队列,每一个WNode标识一个等待线程,whead为CLH队列头,wtail为CLH队列尾,state为锁的状态。long型即64位,倒数第八位标识写锁状态,如果为1,标识写锁占用!
首先是常量标识:
WBIT=1000 0000(即-128)
RBIT =0111 1111(即127)
SBIT =1000 0000(后7位表示当前正在读取的线程数量,清0)

  1. 乐观读:
    tryOptimisticRead():如果当前没有写锁占用,返回state(后7位清0,即清0读线程数),如果有写锁,返回0,即失败。
  2. 校验stamp:
    校验这个戳是否有效validate():比较当前stamp和发生乐观锁得到的stamp比较,不一致则失败。
  3. 悲观读:
    乐观锁失败后锁升级为readLock():尝试state+1,用于统计读线程的数量,如果失败,进入acquireRead()进行自旋,通过CAS获取锁。如果自旋失败,入CLH队列,然后再自旋,如果成功获得读锁,激活cowait队列中的读线程Unsafe.unpark(),最终依然失败,Unsafe().park()挂起当前线程。
  4. 排它写:
    writeLock():典型的cas操作,如果STATE等于s,设置写锁位为1(s+WBIT)。acquireWrite跟acquireRead逻辑类似,先自旋尝试、加入等待队列、直至最终Unsafe.park()挂起线程。
  5. 释放锁
    unlockWrite():释放锁与加锁动作相反。将写标记位清零,如果state溢出,则退回到初始值;

自旋锁(SPIN LOCK)

自旋锁是指当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态。
自旋锁适用于锁保护的临界区很小的情况,临界区很小的话,锁占用的时间就很短。
缺点

排队自旋锁(TICKET LOCK)

Ticket Lock 是为了解决上面的公平性问题,类似于现实中银行柜台的排队叫号:锁拥有一个服务号,表示正在服务的线程,还有一个排队号;每个线程尝试获取锁之前先拿一个排队号,然后不断轮询锁的当前服务号是否是自己的排队号,如果是,则表示自己拥有了锁,不是则继续轮询。
缺点
Ticket Lock 虽然解决了公平性的问题,但是多处理器系统上,每个进程/线程占用的处理器都在读写同一个变量serviceNum ,每次读写操作都必须在多个处理器缓存之间进行缓存同步,这会导致繁重的系统总线和内存的流量,大大降低系统整体的性能。

MCS锁 && CLH锁

上一篇 下一篇

猜你喜欢

热点阅读