网站建设中需求分析说明书,南充市住房和城乡建设局网站,上海闸北网站建设,苏州工业园区服务外包职业学院1. Java内存模型 很多人将Java 内存结构与java 内存模型傻傻分不清#xff0c;java 内存模型是 Java Memory Model#xff08;JMM#xff09;的意思。 简单的说#xff0c;JMM 定义了一套在多线程读写共享数据时#xff08;成员变量、数组#xff09;时#xff0c;对数据…1. Java内存模型 很多人将Java 内存结构与java 内存模型傻傻分不清java 内存模型是 Java Memory ModelJMM的意思。 简单的说JMM 定义了一套在多线程读写共享数据时成员变量、数组时对数据的可见性、有序 性、和原子性的规则和保障。
2. 原子性
下面通过一个例子来说明一下原子性
1.问题提出两个线程对初始值为 0 的静态变量一个做自增一个做自减各做 5000 次结果是 0 吗
2.问题分析以上的结果可能是正数、负数、零。为什么呢因为 Java 中对静态变量的自增自减并不是原子操作。
例如对于 i 而言i 为静态变量实际会产生如下的 JVM 字节码指令
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 加法
putstatic i // 将修改后的值存入静态变量i
而对应 i-- 也是类似
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 减法
putstatic i // 将修改后的值存入静态变量i而 Java 的内存模型如下完成静态变量的自增自减需要在主存和线程内存中进行数据交换 如果是单线程以上 8 行代码是顺序执行不会交错所以没有问题
// 假设i的初始值为0
getstatic i // 线程1-获取静态变量i的值 线程内i0
iconst_1 // 线程1-准备常量1
iadd // 线程1-自增 线程内i1
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i1
getstatic i // 线程1-获取静态变量i的值 线程内i1
iconst_1 // 线程1-准备常量1
isub // 线程1-自减 线程内i0
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i0但多线程下这 8 行代码可能交错运行。为什么会交错思考一下
出现负数的情况
// 假设i的初始值为0
getstatic i // 线程1-获取静态变量i的值 线程内i0
getstatic i // 线程2-获取静态变量i的值 线程内i0
iconst_1 // 线程1-准备常量1
iadd // 线程1-自增 线程内i1
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i1
iconst_1 // 线程2-准备常量1
isub // 线程2-自减 线程内i-1
putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i-1出现正数的情况
// 假设i的初始值为0
getstatic i // 线程1-获取静态变量i的值 线程内i0
getstatic i // 线程2-获取静态变量i的值 线程内i0
iconst_1 // 线程1-准备常量1
iadd // 线程1-自增 线程内i1
iconst_1 // 线程2-准备常量1
isub // 线程2-自减 线程内i-1
putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i-1
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i13.解决方法
使用 synchronized 来解决
语法
synchronized( 对象 ) {要作为原子操作代码
}用 synchronized 解决并发问题
static int i 0;
static Object obj new Object();
public static void main(String[] args) throws InterruptedException {Thread t1 new Thread(() - {for (int j 0; j 5000; j) {synchronized (obj) {i;}}});
Thread t2 new Thread(() - {for (int j 0; j 5000; j) {synchronized (obj) {i--;}}
});t1.start();t2.start();t1.join();t2.join();System.out.println(i);
}如何理解呢
可以把 obj 想象成一个房间线程 t1t2 想象成两个人。 当线程 t1 执行到 synchronized(obj) 时就好比 t1 进入了这个房间并反手锁住了门在门内执行 count 代码。 这时候如果 t2 也运行到了 synchronized(obj) 时它发现门被锁住了只能在门外等待。 当 t1 执行完 synchronized{} 块内的代码这时候才会解开门上的锁从 obj 房间出来。t2 线程这时才 可以进入 obj 房间反锁住门执行它的 count-- 代码。 注意上例中 t1 和 t2 线程必须用 synchronized 锁住同一个 obj 对象如果 t1 锁住的是 m1 对 象t2 锁住的是 m2 对象就好比两个人分别进入了两个不同的房间没法起到同步的效果。 3. 可见性
1.退不出的循环
先来看一个现象main 线程对 run 变量的修改对于 t 线程不可见导致了 t 线程无法停止
static boolean run true;public static void main(String[] args) throws InterruptedException {Thread t new Thread(()-{while(run){// ....}});t.start();Thread.sleep(1000);run false; // 线程t不会如预想的停下来
}2.原因分析
初始状态 t 线程刚开始从主内存读取了 run 的值到工作内存。 因为 t 线程要频繁从主内存中读取 run 的值JIT 编译器会将 run 的值缓存至自己工作内存中的高 速缓存中减少对主存中 run 的访问提高效率 1 秒之后main 线程修改了 run 的值并同步至主存而 t 是从自己工作内存中的高速缓存中读 取这个变量的值结果永远是旧值 3.解决方法
可以通过volatile易变关键字来解决
它可以用来修饰成员变量和静态成员变量避免线程从自己的工作缓存中查找变量的值必须到 主存中获取它的值线程操作 volatile 变量都是直接操作主存
4.可见性理解
前面例子体现的实际就是可见性它保证的是在多个线程之间一个线程对 volatile 变量的修改对另一个线程可见 不能保证原子性仅用在一个写线程多个读线程的情况
getstatic run // 线程 t 获取 run true
getstatic run // 线程 t 获取 run true
getstatic run // 线程 t 获取 run true
getstatic run // 线程 t 获取 run true
putstatic run // 线程 main 修改 run 为 false 仅此一次
getstatic run // 线程 t 获取 run false
比较一下之前我们将线程安全时举的例子两个线程一个 i 一个 i-- 只能保证看到最新值不能解决指令交错
//假设i的初始值为0
getstatic i // 线程1-获取静态变量i的值 线程内i0
getstatic i // 线程2-获取静态变量i的值 线程内i0
iconst_1 // 线程1-准备常量1
iadd // 线程1-自增 线程内i1
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i1
iconst_1 // 线程2-准备常量1
isub // 线程2-自减 线程内i-1
putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i-1 注意 synchronized 语句块既可以保证代码块的原子性也同时保证代码块内变量的可见性。但缺点是 synchronized是属于重量级操作性能相对更低 4. 有序性
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;
}I_Result 是一个对象有一个属性 r1 用来保存结果问可能的结果有几种
一般情况下大家会这么分析
情况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 编译器在运行时的一些优化这个现象需要通过大量测试才能复现
借助 java 并发压测工具 jcstress https://wiki.openjdk.java.net/display/CodeTools/jcstress
mvn archetype:generate -DinteractiveModefalse -
DarchetypeGroupIdorg.openjdk.jcstress -DarchetypeArtifactIdjcstress-java-test-
archetype -DgroupIdorg.sample -DartifactIdtest -Dversion1.0创建 maven 项目提供如下测试类
JCStressTest
Outcome(id {1, 4}, expect Expect.ACCEPTABLE, desc ok)
Outcome(id 0, expect Expect.ACCEPTABLE_INTERESTING, desc !!!!)
State
public class ConcurrencyTest {int num 0;boolean ready false;Actorpublic void actor1(I_Result r) {if(ready) {r.r1 num num;} else {r.r1 1;}}Actorpublic void actor2(I_Result r) {num 2;ready true;}
}执行
mvn clean install
java -jar target/jcstress.jar
会输出我们感兴趣的结果摘录其中一次结果
*** INTERESTING testsSome interesting behaviors observed. This is for the plain curiosity.2 matching test results.[OK] test.ConcurrencyTest(JVM args: [-XX:-TieredCompilation])Observed state Occurrences Expectation Interpretation0 1,729 ACCEPTABLE_INTERESTING !!!!1 42,617,915 ACCEPTABLE ok4 5,146,627 ACCEPTABLE ok[OK] test.ConcurrencyTest(JVM args: [])Observed state Occurrences Expectation Interpretation0 1,652 ACCEPTABLE_INTERESTING !!!!1 46,460,657 ACCEPTABLE ok4 4,571,072 ACCEPTABLE ok
可以看到出现结果为 0 的情况有 638 次虽然次数相对很少但毕竟是出现了。
2.解决方法
也是通过 volatile 关键字去修饰变量可以禁用指令重排
JCStressTest
Outcome(id {1, 4}, expect Expect.ACCEPTABLE, desc ok)
Outcome(id 0, expect Expect.ACCEPTABLE_INTERESTING, desc !!!!)
State
public class ConcurrencyTest {int num 0;volatile boolean ready false;Actorpublic void actor1(I_Result r) {if(ready) {r.r1 num num;} else {r.r1 1;}}Actorpublic void actor2(I_Result r) {num 2;ready true;}
}结果为
*** INTERESTING testsSome interesting behaviors observed. This is for the plain curiosity.0 matching test results.
3.有序性理解
JVM 会在不影响正确性的前提下可以调整语句的执行顺序思考下面一段代码
static int i;
static int j;// 在某个线程内执行如下赋值操作
i ...; // 较为耗时的操作
j ...;可以看到至于是先执行 i 还是 先执行 j 对最终的结果不会产生影响。所以上面代码真正执行 时既可以是
i ...; // 较为耗时的操作
j ...;
也可以是
j ...;
i ...; // 较为耗时的操作这种特性称之为『指令重排』多线程下『指令重排』会影响正确性例如著名的 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 new Singleton() 对应的字节码为
0: new #2 // class cn/itcast/jvm/t4/Singleton
3: dup
4: invokespecial #3 // Method init:()V
7: putstatic #4 // Field
INSTANCE:Lcn/itcast/jvm/t4/Singleton;其中 4、7 两步的顺序不是固定的也许 jvm 会优化为先将引用地址赋值给 INSTANCE 变量后再执行构造方法如果两个线程 t1t2 按如下时间序列执行
时间1 t1 线程执行到 INSTANCE new Singleton();
时间2 t1 线程分配空间为Singleton对象生成了引用地址0 处
时间3 t1 线程将引用地址赋值给 INSTANCE这时 INSTANCE ! null7 处
时间4 t2 线程进入getInstance() 方法发现 INSTANCE ! nullsynchronized块外直接
返回 INSTANCE
时间5 t1 线程执行Singleton的构造方法4 处
这时 t1 还未完全将构造方法执行完毕如果在构造方法中要执行很多初始化操作那么 t2 拿到的是将 是一个未初始化完毕的单例
对 INSTANCE 使用 volatile 修饰即可可以禁用指令重排但要注意在 JDK 5 以上的版本的 volatile 才 会真正有效
4.happens-before
happens-before 规定了哪些写操作对其它线程的读操作可见它是可见性与有序性的一套规则总结 抛开以下 happens-before 规则JMM 并不能保证一个线程对共享变量的写对于其它线程对该共享变量的读可见。
线程解锁 m 之前对变量的写对于接下来对 m 加锁的其它线程对该变量的读可见
static int x;
static Object m new Object();new Thread(()-{synchronized(m) {x 10;}},t1).start();new Thread(()-{synchronized(m) {System.out.println(x);}
},t2).start();线程对 volatile 变量的写对接下来其它线程对该变量的读可见
volatile static int x;
new Thread(()-{x 10;
},t1).start();new Thread(()-{System.out.println(x);
},t2).start();
线程 start 前对变量的写对该线程开始后对该变量的读可见
static int x;
x 10;
new Thread(()-{System.out.println(x);
},t2).start();线程结束前对变量的写对其它线程得知它结束后的读可见比如其它线程调用 t1.isAlive() 或 t1.join()等待它结束
static int x;
Thread t1 new Thread(()-{x 10;
},t1);t1.start();
t1.join();
System.out.println(x);
线程 t1 打断 t2interrupt前对变量的写对于其他线程得知 t2 被打断后对变量的读可见通 过t2.interrupted 或 t2.isInterrupted
static int x;
public static void main(String[] args) {Thread t2 new Thread(()-{while(true) {if(Thread.currentThread().isInterrupted()) {System.out.println(x);break;}}},t2);t2.start();new Thread(()-{try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}x 10;t2.interrupt();},t1).start();while(!t2.isInterrupted()) {Thread.yield();}
System.out.println(x);
}
对变量默认值0falsenull的写对其它线程对该变量的读可见具有传递性如果 x hb- y 并且 y hb- z 那么有 x hb- z 变量都是指成员变量或静态成员变量 4. CAS 与 原子类
4.1 CAS
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 指令下面是直接使用 Unsafe 对象进 行线程安全保护的一个例子
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class TestCAS {public static void main(String[] args) throws InterruptedException {DataContainer dc new DataContainer();int count 5;Thread t1 new Thread(() - {for (int i 0; i count; i) {dc.increase();}});t1.start();t1.join();System.out.println(dc.getData());}
}class DataContainer {private volatile int data;static final Unsafe unsafe;static final long DATA_OFFSET;static {try {// Unsafe 对象不能直接调用只能通过反射获得Field theUnsafe Unsafe.class.getDeclaredField(theUnsafe);theUnsafe.setAccessible(true);unsafe (Unsafe) theUnsafe.get(null);} catch (NoSuchFieldException | IllegalAccessException e) {throw new Error(e);}try {// data 属性在 DataContainer 对象中的偏移量用于 Unsafe 直接访问该属性DATA_OFFSET unsafe.objectFieldOffset(DataContainer.class.getDeclaredField(data));} catch (NoSuchFieldException e) {throw new Error(e);}}public void increase() {int oldValue;while(true) {// 获取共享变量旧值可以在这一行加入断点修改 data 调试来加深理解oldValue data;// cas 尝试修改 data 为 旧值 1如果期间旧值被别的线程改了返回 falseif (unsafe.compareAndSwapInt(this, DATA_OFFSET, oldValue, oldValue 1)) {return;}}
}public void decrease() {int oldValue;while(true) {oldValue data;if (unsafe.compareAndSwapInt(this, DATA_OFFSET, oldValue, oldValue - 1)) {return;}}
}public int getData() {return data;
}
}
4.2 乐观锁与悲观锁
CAS 是基于乐观锁的思想最乐观的估计不怕别的线程来修改共享变量就算改了也没关系 我吃亏点再重试呗。
synchronized 是基于悲观锁的思想最悲观的估计得防着其它线程来修改共享变量我上了锁 你们都别想改我改完了解开锁你们才有机会。
4.3 原子操作类
jucjava.util.concurrent中提供了原子操作
// 创建原子整数对象
private static AtomicInteger i new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {Thread t1 new Thread(() - {for (int j 0; j 5000; j) {i.getAndIncrement(); // 获取并且自增 i// i.incrementAndGet(); // 自增并且获取 i}});Thread t2 new Thread(() - {for (int j 0; j 5000; j) {i.getAndDecrement(); // 获取并且自减 i--}});t1.start();t2.start();t1.join();t2.join();System.out.println(i);
}5. synchronized 优化
Java HotSpot 虚拟机中每个对象都有对象头包括 class 指针和 Mark Word。Mark Word 平时存储这个对象的 哈希码 、 分代年龄 当加锁时这些信息就根据情况被替换为标记位 、 线程锁记录指针 、 重量级锁指针 、 线程 ID 等内容
5.1 轻量级锁
如果一个对象虽然有多线程访问但多线程访问的时间是错开的也就是没有竞争那么可以使用轻 量级锁来优化。这就好比
学生线程 A用课本占座上了半节课出门了CPU时间到回来一看发现课本没变说明没 有竞争继续上他的课。如果这期间有其它学生线程 B来了会告知线程A有并发访问线程 A 随即升级为重量级锁 进入重量级锁的流程。而重量级锁就不是那么用课本占座那么简单了可以想象线程 A 走之前把座位用一个铁栅栏围起来
假设有两个方法同步块利用同一个对象加锁
static Object obj new Object();
public static void method1() {synchronized( obj ) {// 同步块 Amethod2();}
}
public static void method2() {synchronized( obj ) {// 同步块 B}
}
每个线程都的栈帧都会包含一个锁记录的结构内部可以存储锁定对象的 Mark Word 5.2 锁膨胀
如果在尝试加轻量级锁的过程中CAS 操作无法成功这时一种情况就是有其它线程为此对象加上了轻量级锁有竞争这时需要进行锁膨胀将轻量级锁变为重量级锁。
static Object obj new Object();
public static void method1() {synchronized( obj ) {// 同步块}
} 5.3 重量锁
重量级锁竞争的时候还可以使用自旋来进行优化如果当前线程自旋成功即这时候持锁线程已经退 出了同步块释放了锁这时当前线程就可以避免阻塞。
在 Java 6 之后自旋锁是自适应的比如对象刚刚的一次自旋操作成功过那么认为这次自旋成功的可能 性会高就多自旋几次反之就少自旋甚至不自旋总之比较智能。
自旋会占用 CPU 时间单核 CPU 自旋就是浪费多核 CPU 自旋才能发挥优势。好比等红灯时汽车是不是熄火不熄火相当于自旋等待时间短了划算熄火了相当于阻塞等 待时间长了划算Java 7 之后不能控制是否开启自旋功能
自旋重试成功的情况 自旋重试失败的情况 5.4 偏向锁 轻量级锁在没有竞争时就自己这个线程每次重入仍然需要执行 CAS 操作。Java 6 中引入了偏向锁 来做进一步优化只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头之后发现这个线程 ID 是自己的就表示没有竞争不用重新 CAS.
撤销偏向需要将持锁线程升级为轻量级锁这个过程中所有线程需要暂停STW访问对象的 hashCode 也会撤销偏向锁如果对象虽然被多个线程访问但没有竞争这时偏向了线程 T1 的对象仍有机会重新偏向 T2 重偏向会重置对象的 Thread ID撤销偏向和重偏向都是批量进行的以类为单位如果撤销偏向到达某个阈值整个类的所有对象都会变为不可偏向的可以主动使用 -XX:-UseBiasedLocking 禁用偏向锁
假设有两个方法同步块利用同一个对象加锁
static Object obj new Object();
public static void method1() {synchronized( obj ) {// 同步块 Amethod2();}
}public static void method2() {synchronized( obj ) {// 同步块 B}
}5.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