做网站的编程语言,益阳建站网站制作,wordpress博客的搭建,中国最大的现货交易平台内存模型与高效并发
一、java 内存模型
【java 内存模型】是 Java Memory Model#xff08;JMM#xff09; 简单的说#xff0c;JMM 定义了一套在多线程读写共享数据时#xff08;成员变量、数组#xff09;时#xff0c;对数据的可见性、有序 性、和原子性的规则和保障…内存模型与高效并发
一、java 内存模型
【java 内存模型】是 Java Memory ModelJMM 简单的说JMM 定义了一套在多线程读写共享数据时成员变量、数组时对数据的可见性、有序 性、和原子性的规则和保障
1原子性
原子性在学习线程时讲过下面来个例子简单回顾一下 问题提出两个线程对初始值为 0 的静态变量一个做自增一个做自减各做 5000 次结果是 0 吗?
2问题分析
以上的结果可能是正数、负数、零。为什么呢因为 Java 中对静态变量的自增自减并不是原子操作。
3解决方法
使用 synchronized同步关键字
synchronized( 对象 ) {要作为原子操作代码
}注意上例中 t1 和 t2 线程必须用 synchronized 锁住同一个 obj 对象如果 t1 锁住的是 m1 对 象t2 锁住的是 m2 对象就好比两个人分别进入了两个不同的房间没法起到同步的效果。 二、可见性
1退不出的循环
一种现象main 线程对另一个线程 t 中的变量的修改不可见。 由于t 线程需要反复调用 run 变量JIT 会把 run 变量放在 工作内存中的高速缓存中不需要从总内存中读取。 所以即使更改主内存中的 run 变量也无法改变高速缓存中的 run 变量 t 线程会一直运行。
2解决方法
volatile易变关键字。一般避免使用 volatile因为不能保证线程的安全。而使用 synchronized 关键字既可以保证线程的可见性又可以保证线程的原子性。
它可以用来修饰成员变量和静态成员变量他可以避免线程从自己的工作缓存中查找变量的值必须到 主存中获取它的值线程操作 volatile 变量都是直接操作主存
3可见性
volatile 只是保证多线程之间的可见性不保证原子性即不保证多线程安全性。仅用在一个写线程多个读线程的情况 synchronized 语句块既可以保证代码块的原子性也同时保证代码块内变量的可见性。但缺点是 synchronized 是属于重量级操作性能相对更低。 如果在前面示例的死循环中加入 System.out.println() 会发现即使不加 volatile 修饰符线程 t 也能正确看到对 run 变量的修改了因为 println() 方法底层 使用了 synchronized 关键字使用 synchronized 关键字会清理缓存。 三、有序性
1诡异的结果
int num 0;
boolean ready false;
// 线程1 执行此方法
public void actor1(I_Result r) {if(ready) {r.r1 num num;} else {r.r1 1;}
}
// 线程2 执行此方法
public void actor2(I_Result r) {num 2;ready true;
}情况1线程1 先执行这时 ready false所以进入 else 分支结果为 1
情况2线程2 先执行 num 2但没来得及执行 ready true线程1 执行还是进入 else 分支结果为1
情况3线程2 执行到 ready true线程1 执行这回进入 if 分支结果为 4因为 num 已经执行过了
结果还有可能是 0。
这种情况下是线程2 执行 ready true切换到线程1进入 if 分支相加为 0再切回线程2 执行num 2。
这种现象叫做指令重排是 JIT 编译器在运行时的一些优化这个现象需要通过大量测试才能复现
2解决方法
volatile 修饰的变量可以禁用指令重排
3有序性理解
JVM 在不影响正确性的前提下可以调整语句的执行顺序。这种特性称之为【指令重排】多线程下【指令重排】会影响正确性例如著名的 double-checked locking 模式实现单例。
public final class Singleton {private Singleton() { }private static Singleton INSTANCE null;public static Singleton getInstance() {// 实例没创建才会进入内部的 synchronized代码块if (INSTANCE null) {synchronized (Singleton.class) {// 也许有其它线程已经创建实例所以再判断一次if (INSTANCE null) {INSTANCE new Singleton();}}}return INSTANCE;}
}以上的实现特点是 懒惰实例化 首次使用 getInstance() 才使用 synchronized 加锁后续使用时无需加锁
但在多线程环境下上面的代码是有问题的 如果有两个线程一个线程为 INSTANCE 分配了空间INSTANCE ! null但还未进行初始化操作另一个线程在 INSTANCE ! null时直接返回了未初始化完成的单例。
对 INSTANCE 使用 volatile 修饰即可可以禁用指令重排但要注意在 JDK 5 以上的版本的 volatile 才 会真正有效
4happens-before
happens-before 规定了哪些写操作对其它线程的读操作可见它是可见性与有序性的一套规则总结 抛开以下 happens-before 规则JMM 并不能保证一个线程对共享变量的写对于其它线程对该共享变 量的读可见
线程解锁 m 之前对变量的写对于接下来对 m 加锁的其它线程对该变量的读可见线程对 volatile 变量的写对接下来其它线程对该变量的读可见线程 start 前对变量的写对该线程开始后对该变量的读可见线程结束前对变量的写对其它线程得知它结束后的读可见比如其它线程调用 t1.isAlive() 或 t1.join()等待它结束线程 t1 打断 t2interrupt前对变量的写对于其他线程得知 t2 被打断后对变量的读可见通过t2.interrupted 或 t2.isInterrupted对变量默认值0falsenull的写对其它线程对该变量的读可见具有传递性如果 x hb- y 并且 y hb- z 那么有 x hb- z
四、CAS 与 原子类
1CAS
CAS 即 Compare and Swap, 它体现的是一种乐观锁的思想比如多个线程要对一个共享的整型变量执行 1 操作
// 需要不断尝试
while(true) {int 旧值 共享变量 ; // 比如拿到了当前值 0int 结果 旧值 1; // 在旧值 0 的基础上增加 1 正确结果是 1/*这时候如果别的线程把共享变量改成了 5本线程的正确结果 1 就作废了这时候compareAndSwap 返回 false重新尝试直到compareAndSwap 返回 true表示我本线程做修改的同时别的线程没有干扰*/if( compareAndSwap ( 旧值, 结果 )) {// 成功退出循环}
}获取共享变量时为了保证该变量的可见性需要使用 volatile 修饰。结合 CAS 和 volatile 可以实现无 锁并发适用于竞争不激烈、多核 CPU 的场景下。
因为没有使用 synchronized所以线程不会陷入阻塞这是效率提升的因素之一但如果竞争激烈可以想到重试必然频繁发生反而效率会受影响
CAS 底层依赖于一个 Unsafe 类来直接调用操作系统底层的 CAS 指令。
2乐观锁与悲观锁
CAS 是基于乐观锁的思想最乐观的估计不怕别的线程来修改共享变量就算改了也没关系我吃亏点再重试呗。synchronized 是基于悲观锁的思想最悲观的估计得防着其它线程来修改共享变量我上了锁 你们都别想改我改完了解开锁你们才有机会。
3原子操作类
jucjava.util.concurrent中提供了原子操作类可以提供线程安全的操作例如AtomicInteger、 AtomicBoolean等它们底层就是采用 CAS 技术 volatile 来实现的。
五、synchronized 优化
synchronized 是一个重量级锁如果要阻塞或唤醒一条线程则需要操作系统来帮忙完成这就不可避免的陷入用户态到内核态的转换中进行这种转换需要耗费很多的处理器时间状态转换消耗的时间甚至会比用户代码本身执行的时间还要长。
但 synchronized有非常大的优化余地JDK 6 之后synchronized synchronized与ReentrantLock的性能基本上能够持平。
Java HotSpot 虚拟机中每个对象都有对象头包括 class 指针和 Mark Word。Mark Word 平时存 储这个对象的 哈希码 、 分代年龄 当加锁时这些信息就根据情况被替换为 标记位 、 线程锁记录指 针 、 重量级锁指针 、 线程ID 等内容
1轻量级锁
如果一个对象虽然有多线程访问但多线程访问的时间是错开的也就是没有竞争那么可以使用轻量级锁来优化。 每个线程都的栈帧都会包含一个锁记录的结构内部可以存储锁定对象的 Mark Word 轻量级锁的解锁过程是通过CAS操作来进行的。 如果出现两条以上的线程争用同一个锁的情况那轻量级锁就不再有效必须要膨胀为重量级锁锁标志 的状态值变为“10”此时Mark Word中存储的就是指向重量级锁互斥量的指针后面等待锁的线程也必须进入阻塞状态。
2锁膨胀
如果在尝试加轻量级锁的过程中CAS 操作无法成功这时一种情况就是有其它线程为此对象加上了轻 量级锁有竞争这时需要进行锁膨胀将轻量级锁变为重量级锁。
3重量级锁
重量级锁竞争的时候还可以使用自旋来进行优化如果当前线程自旋成功即这时候持锁线程已经退 出了同步块释放了锁这时当前线程就可以避免阻塞。
在 Java 6 之后自旋锁是自适应的比如对象刚刚的一次自旋操作成功过那么认为这次自旋成功的可能 性会高就多自旋几次反之就少自旋甚至不自旋总之比较智能。
自旋会占用 CPU 时间单核 CPU 自旋就是浪费多核 CPU 自旋才能发挥优势。好比等红灯时汽车是不是熄火不熄火相当于自旋等待时间短了划算熄火了相当于阻塞等 待时间长了划算Java 7 之后不能控制是否开启自旋功能
4偏向锁
轻量级锁在没有竞争时就自己这个线程每次重入仍然需要执行 CAS 操作。 Java 6 中引入了偏向锁来做进一步优化只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头之后发现这个线程 ID是自己的就表示没有竞争不用重新 CAS。
偏向锁中的“偏”就是偏心的“偏”、偏袒的“偏”。它的意思是这个锁会偏向于第一个获得它的线 程如果在接下来的执行过程中该锁一直没有被其他的线程获取则持有偏向锁的线程将永远不需 要再进行同步。
偏向锁可以提高带有同步但无竞争的程序性能但它同样是一个带有效益权衡Trade Off性质的优化也就是说它并非总是对程序运行有利。如果程序中大多数的锁都总是被多个不同的线程访问那偏向模式就是多余的。在具体问题具体分析的前提下有时候使用参数-XX-UseBiasedLocking来禁止偏向锁优化反而可以提升性能。
5其他优化
1.减少上锁时间
同步代码块中尽量短
2.减少锁的粒度
将一个锁拆分为多个锁提高并发度例如
ConcurrentHashMapLongAdder 分为 base 和 cells 两部分。没有并发争用的时候或者是 cells 数组正在初始化的时候会使用 CAS 来累加值到 base有并发争用会初始化 cells 数组数组有多少个 cell就允许有多少线程并行修改最后将数组中每个 cell 累加再加上 base 就是最终的值LinkedBlockingQueue 入队和出队使用不同的锁相对于LinkedBlockingArray只有一个锁效率要高
3. 锁粗化
多次循环进入同步代码块不如同步块内多次循环 另外 JVM 可能会做如下优化把多次 append 的加锁操作粗化为一次因为都是对同一个对象加锁 没必要重入多次
new StringBuffer().append(a).append(b).append(c);4. 锁消除
JVM 会进行代码的逃逸分析例如某个加锁对象是方法内局部变量不会被其它线程所访问到这时候 就会被即时编译器忽略掉所有同步操作。
5. 读写分离
CopyOnWriteArrayList ConyOnWriteSet
参考 https://wiki.openjdk.java.net/display/HotSpot/Synchronization
http://luojinping.com/2015/07/09/java锁优化/
https://www.infoq.cn/article/java-se-16-synchronized
https://www.jianshu.com/p/9932047a89be
https://www.cnblogs.com/sheeva/p/6366782.html
https://stackoverflow.com/questions/46312817/does-java-ever-rebias-an-individual-lock