小强(我的一个朋友)以前在是一公司快乐的crud boy,直到有一天,小强最近新交了妹子,感觉自己工资有点少,不够花,于是他选择了跳槽,但是发现,由于以前自己都是躺过来的,在面试的过程每次这个高并发都被问住,小强人麻了!!!
**
走在万家灯火的的路上,小强渴望着加薪升职!终于小强决定把并发重新系统深度的学习一遍!!雄起
1.并发和并行
并行
指同一时刻,有多条指令在多个处理器上同时执行。无论从微观还是从宏观来看,二者都是一起执行的,是并行的。
并发
并发(concurrency) :从微观上讲,对于同一时刻,只能由一条指令执行,但多个进程指令被快速的轮换执行,在宏观上看是一起执行的
2.java内存模型
1.JMM定义
2.如何体现在内存硬件
关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来完成:
- lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
- unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
- use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
- write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行read和load操作, 如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。但Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。
并发的本质到底是什么!!! 请继续往下看
3.并发三大特性
并发编程Bug的源头:可见性、原子性和有序性问题
可见性和有序性
当共享变量的值被一个线程修改时,其他线程可以看到被修改的值,java内存模型是通过在变量修改后将新值同步回主内存,同时让其他的线程可以读取到最新内存的值。
如何保证可见性和有序性
- volatile 关键字
- 内存屏障
- synchronized
- Lock锁
此外final也可以满足可见性!!!
原子性
原子性(atomicity):指一个操作是不可分割的、完整的,要么全部执行成功,要么全部不执行,不存在执行一半的情况
在 Java 中,对基本数据类型的变量的读取和赋值操作是原子性操作(64位处理器)。不采取任何的原子性保障措施的自增操作并不是原子性的。
如何保证原子性
- synchronized
- Lock
因为被 synchronized 修饰某段代码 后,无论是单核 CPU 还是多核 CPU,只有一个线程能够执行该代码,所以一 定能保证原子操作.
- CAS+volatile
通过自旋锁和可见性,该算法的核心是硬件对于并发操作的支持。
咱讲了这么多原理,开始搞事情了,那我们先从volatile搞起,兄弟们,跟我冲!!!
4.再次深度理解volatile
1.volatile可见性底层原理
1.特性
volatile修饰的变量的read、load、use操作和assign、store、write必须是连续的,即修改后必须立即刷新主内存,使用时必须从主内存中重新获取,由此保证volatile变量操作对多线程的可见性。
2.硬件层面
通过lock前缀指令,会锁定变量缓存行区域并写回主内存,这个操作称为"缓存锁定",假设一个处理器的缓存被修改,会回写到内存并导致其他处理器的缓存无效,其他线程在读取数据时,智能从内存中获取。
对Volatile关键字修饰的变量,执行写操作的时候,Jvm会发送一条lock前缀指令给CPU,lock前缀指令会等待它之前所有的指令完成,并且把工作变量的内存数据写回到主内存之后才开始执行,此时,会满足MESI缓存一致性协议,是基于写失效的,其实就是在刷新主内存的时候导致其他的工作线程的副本内存中的数据失效。
2.指令重排序
1.什么是指令重排序
Java语言规范规定JVM线程内部维持顺序化语义。即只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。
指令重排序的意义:JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。
2.指令重排序demo
java
package multiThread;
public class ReOrderingDemo {
//定义四个静态变量
private static int x=0,y=0;
private static int a=0,b=0;
public static void main(String[] args) throws InterruptedException {
int i=0;
while (true){
i++;
x=0;y=0;a=0;b=0;
//开两个线程,第一个线程执行a=1;x=b;第二个线程执行b=1;y=a
Thread thread1=new Thread(new Runnable() {
@Override
public void run() {
a=1;
x=b;
}
});
Thread thread2=new Thread(new Runnable() {
@Override
public void run() {
b=1;
y=a;
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
//等两个线程都执行完毕后拼接结果
String result="第"+i+"次执行x="+x+"y="+y;
//如果x=0且y=0,则跳出循环
if (x==0&&y==0){
System.out.println(result);
break;
}else{
System.out.println(result);
}
}
}
}
这段代码首先定义四个静态变量x,y,a,b,每次循环时让他们都等于0,接着用两个线程,第一个线程执行a=1;x=b;第二个线程执行b=1;y=a。
这段程序有几个结果呢?从逻辑上来讲,应该有3种运行情况:
当第一个线程执行到a=1的时候,第二个线程执行到了b=1,最后x=1,y=1;
当第一个线程执行完,第二个线程才刚开始,最后x=0,y=1;
当第二个线程执行完,第一个线程才开始,最后x=1,y=0;
理论上是不可能出现x=0,y=0的结果,但是实际还是发生了。
第26962次执行x=0y=1
第26963次执行x=0y=1
第26964次执行x=0y=1
第26965次执行x=0y=1
第26966次执行x=0y=0
其实这种情况正式应为发生了重排序。导致都先运行x=a,y=b
3.volatile的重排序规则
volatile禁止重排序场景:
-
第二个操作是volatile写,不管第一个操作是什么都不会重排序
-
第一个操作是volatile读,不管第二个操作是什么都不会重排序
-
第一个操作是volatile写,第二个操作是volatile读,也不会发生重排序
JMM内存屏障插入策略
-
在每个volatile写操作的前面插入一个StoreStore屏障
-
在每个volatile写操作的后面插入一个StoreLoad屏障
-
在每个volatile读操作的后面插入一个LoadLoad屏障
-
在每个volatile读操作的后面插入一个LoadStore屏障
4.volatile模式在单例中的应用
在创建单例模式的方法有多种,我们通常用的是下面这一种
java
public class SingleTon {
private static SingleTon instance = null;
private SingleTon() {
}
public static SingleTon getInstance() {
if (instance == null) {//第一次判断是否为null
synchronized (SingleTon.class) {//使用synchronized对class加锁
if (instance == null) {//再次判断是否为空
instance = new SingleTon();
}
}
}
return instance;
}
}
上述代码,在执行时,可能会有个问题,就是重排序的问题,其实就是一种报null的异常。
上面的代码其实在下面分成3步:
1.分配地址空间
2.初始化SingleTon对象
3.将SingleTon对象的地址赋值给instance,此时instance不为null
最重要的是,JVM为了提高性能,编译器和处理器会对指令进行重排序,在上面的3步中,2和3是没有数据依赖的,可以重排序,所以可能执行的顺序是1/3/2。所以有一种情况,当第一个线程执行1/3/2创建单例时,正好执行到了3步骤,只是将地址空间赋值给instance,并没有初始化,那么第二个线程进来判断instance不为null,直接就调用单例内部其他方法的话可能就会报null。
所以此处就可以用volatile修饰,在后面加入storeLoad屏障,防止指令重排序。
我本来以为已经讲完了,小强拉着我"海哥,还有一个伪共享问题呢,再讲讲,有次面试就是被这个问死的"
我为了我兄弟的爱情,拼了,把最后一个搞完。
5.伪共享问题
如果多核线程在操作同一缓存行的不同数据时,会出现频繁的缓存失效,即使是从代码层面看两个线程操作的数据没有任务的关系,这种问题就是伪共享问题。
1.怎么查看缓存行
2.避免伪共享问题的方式
1.缓存行填充
arduino
class ShareObject {
// 避免伪共享: @Contended + jvm参数:-XX:-RestrictContended jdk8支持
//@Contended
volatile long x;
long p1, p2, p3, p4, p5, p6, p7;
volatile long y;
}
2.使用注解+jvm配置
使用 @sun.misc.Contended 注解(java8)
此外注意需要配置jvm参数:-XX:-RestrictContended
java
class ShareObject {
// 避免伪共享: @Contended + jvm参数:-XX:-RestrictContended jdk8支持
//@Contended
volatile long x;
long p1, p2, p3, p4, p5, p6, p7;
volatile long y;
}
经典案例:
java
public class FalseShareTest {
public static void main(String[] args) throws InterruptedException {
testShareObject(new ShareObject());
}
private static void testShareObject(ShareObject shareObject) throws InterruptedException {
long start = System.currentTimeMillis();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 300000000; i++) {
shareObject.x++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 300000000; i++) {
shareObject.y++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(shareObject.x + "," + shareObject.y);
System.out.println(System.currentTimeMillis() - start);
}
}
class ShareObject {
// 避免伪共享: @Contended + jvm参数:-XX:-RestrictContended jdk8支持
//@Contended
volatile long x;
//long p1, p2, p3, p4, p5, p6, p7;
volatile long y;
}
当没有增加缓存行long p1, p2, p3, p4, p5, p6, p7;时,时间为9s,当加上缓存行long p1, p2, p3, p4, p5, p6, p7是,时间缩减为1s。
小强开心的笑了:"海哥,你这说的很有道理,你这个文档我可以当做技术笔记了"
我:"是你对你对象的爱感动了我"
小强:"海哥,并发还有CAS,AQS,锁等,啥时候给我再讲讲"
我:"下次必须给你安排"!!!!