Java程序编译到运行须要通过将.java后缀的文件经过javac命令编译成.class文件(此时与平台无关),而后将对应的.class文件转化成机器码并执行,可是因为不一样平台的JVM会带来不一样的“翻译”,因此咱们在Java层写的各类Lock,其实最终依赖的是JVM的具体实现和CPU指令,才能帮助咱们达到线程安全的效果。java
下面介绍面试
在使用new指令建立一个对象的时候,JVM会建立一个instanceOopDesc对象,这个对象中包含了对象头以及实例数据编程
C/C++语言它们不存在内存模型的概念,它们依赖于处理器,不一样的处理器处理的结果不一样,也就没法保证并发安全。因此此时须要一个标准,让多线程的运行结果可预期。缓存
JMM是一组规范,要求JVM依照规范来实现,从而让咱们更好的开发多线程程序。若是没有了JMM规范,那么不一样的虚拟机可能会进行不一样的重排序,这样就会致使不一样的虚拟机上运行的结果不一样,这也就引法了问题。安全
JMM除了是规范仍是工具类和关键字的原理,咱们常见的
volatile
、synchronized
以及Lock
等的原理都是JMM。若是没有JMM,那就须要咱们本身指定何时须要内存栅栏(工做内存与主内存之间的拷贝同步)等,这样就很麻烦,由于有了JMM,因此咱们只须要使用关键字就能够开发并发程序了。bash
第一种执行状况多线程
/**
* 演示重排序的现象
* “直到达到某个条件才中止”,测试小几率事件
*/
public class OutOfOrderExecution {
private static int x = 0, y = 0;
private static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
Thread one = new Thread(new Runnable() {
@Override
public void run() {
a = 1;
x = b;
}
});
Thread two = new Thread(new Runnable() {
@Override
public void run() {
b = 1;
y = a;
}
});
one.start();
two.start();
one.join();
two.join();
System.out.println("x = " + x + ", y = " + y);
}
}
复制代码
第二种执行状况并发
/**
* 演示重排序的现象
* “直到达到某个条件才中止”,测试小几率事件
*/
public class OutOfOrderExecution {
private static int x = 0, y = 0;
private static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
Thread one = new Thread(new Runnable() {
@Override
public void run() {
a = 1;
x = b;
}
});
Thread two = new Thread(new Runnable() {
@Override
public void run() {
b = 1;
y = a;
}
});
two.start();
one.start();
one.join();
two.join();
System.out.println("x = " + x + ", y = " + y);
}
}
复制代码
/**
* 演示重排序的现象
* “直到达到某个条件才中止”,测试小几率事件
*/
public class OutOfOrderExecution {
private static int x = 0, y = 0;
private static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(1);
Thread one = new Thread(new Runnable() {
@Override
public void run() {
try {
latch.await(); //进行等待
} catch (InterruptedException e) {
e.printStackTrace();
}
a = 1;
x = b;
}
});
Thread two = new Thread(new Runnable() {
@Override
public void run() {
try {
latch.await(); //进行等待
} catch (InterruptedException e) {
e.printStackTrace();
}
b = 1;
y = a;
}
});
one.start();
two.start();
latch.countDown(); //统一开始执行
one.join();
two.join();
System.out.println("x = " + x + ", y = " + y);
}
}
复制代码
/**
* 演示重排序的现象
* “直到达到某个条件才中止”,测试小几率事件
*/
public class OutOfOrderExecution {
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; //计数
for (; ; ) {
i++;
x = 0; //清零操做
y = 0;
a = 0;
b = 0;
CountDownLatch latch = new CountDownLatch(1);
Thread one = new Thread(new Runnable() {
@Override
public void run() {
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
a = 1;
x = b;
}
});
Thread two = new Thread(new Runnable() {
@Override
public void run() {
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
b = 1;
y = a;
}
});
one.start();
two.start();
latch.countDown();
one.join();
two.join();
String result = "第" + i + "次 (" + x + ", " + y + ")";
if (x == 1 && y == 1) {
System.out.println(result);
return; //知足条件后退出循环
} else {
System.out.println(result);
}
}
}
}
复制代码
只需将上面的结束条件改成x == 0 && y == 0
便可 app
出现这种状况是由于重排序发生了,代码的执行顺序有可能为ide
y = a;
a = 1;
x = b;
b = 1;
复制代码
线程1中代码的执行顺序与Java代码不一致,代码的执行顺序并非按照指令执行的,它们的执行顺序被改变了,这就是重排序。
对比下图能够发现若是进行重排序能够减小关于变量a
的执行指令,若是在程序中个存在大量的相似状况,也就提升了处理速度。
好比存在变量a和b,若是将对a的操做连续执行效率更高的话,就可能发生重排序来提升执行效率。
CPU重排和编译器重排相似,就算编译器不重排CPU也会进行重排,它们都是打乱执行顺序达到优化的目的。
内存中的重排序并不是真正的重排序,由于内存中有缓存的存在,在JMM中表现为本地内存和主内存,若是线程1修改了变量a的值尚未来得及写入到主存,此时线程2因为可见性的缘由没法知道线程1对变量进行了修改,因此会使程序表现出乱序行为。
当一个线程执行写操做时,另一个线程没法看见此时被更改的值。就像下图所示当线程1从主存中读取变量x,并将x的值设置为1,可是此时线程1并无将x的值写回主存,因此线程2就没法得知x的值已经改变了。
/**
* 演示可见性带来的问题
*/
public class FieldVisibility {
int a = 1;
int b = 2;
public static void main(String[] args) {
while (true) {
FieldVisibility test = new FieldVisibility();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.change();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.print();
}
}).start();
}
}
private void print() {
System.out.println("b = " + b + "; a = " + a);
}
private void change() {
a = 3;
b = a;
}
}
复制代码
四种状况
a = 3; b = 3;
a = 1; b = 2;
a = 3; b = 2;
a = 1; b = 3; //发生可见性问题
复制代码
/**
* 演示可见性带来的问题
*/
public class FieldVisibility {
//解决可见性问题
volatile int a = 1;
volatile int b = 2;
public static void main(String[] args) {
while (true) {
FieldVisibility test = new FieldVisibility();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.change();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.print();
}
}).start();
}
}
private void print() {
System.out.println("b = " + b + "; a = " + a);
}
private void change() {
a = 3;
b = a;
}
}
复制代码
volatile怎么解决可见性问题
当线程1读取到x并将值更新为1后会刷回主存,当线程2再次读取x时就会从主存中加载,这样就不会引起可见性的问题。
Java做为高级语言,屏蔽了这些底层细节,用JMM定义了这些读写内存数据的规范,虽然咱们再也不须要关心一级缓存和二级缓存的问题,可是JMM抽象了主内存和本地内存的概念。
这里说的本地内存并非真正的为每一个线程分配一块内存,而是JMM的抽象,是对寄存器、一级缓存、二级缓存的抽象。
主内存和本地内存的关系
JMM有如下规定
总结:线程操做数据必须从主内存中读取数据,而后在本身的工做内存中进行操做,操做完成后再写回主内存,由于读写须要时间因此就会引起可见性的问题
在单线程状况下,后面的语句必定能看到前面的语句作了什么
加锁以后能看到解锁以前的所有操做
被volatile
修饰的变量只要执行了写操做,就必定会被读取到
调用start()
方法可让子线程中全部语句看到启动以前的结果
join()
后的语句能看到等待以前的全部操做
好比第一行代码运行后第二行会看到,第二行运行后第三行会看到,从中能够推断出第一行代码运行完第三行就会看到。
若是一个线程被interrupt()
时,那么isInterrupt()
或者InterruptException
必定能看到。
对象构造方法的最后一行语句happens-before于finalize()
的第一行语句
volatile
是一种同步机制,相对synchronized
和Lock
更轻量,不会带来上下文切换等重大开销。若是一个变量被volatile
修饰,那么JVM就知道这个变量可能会被并发修改。虽然volatile
的开销小,可是它的能力也小,相对于synchronized
来讲volatile
没法保证原子性。
/**
* 不适用volatile的场景
*/
public class NoVolatile implements Runnable {
volatile int a;
AtomicInteger realA = new AtomicInteger();
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
a++;
realA.incrementAndGet();
}
}
public static void main(String[] args) throws InterruptedException {
NoVolatile noVolatile = new NoVolatile();
Thread thread1 = new Thread(noVolatile);
Thread thread2 = new Thread(noVolatile);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(noVolatile.a);
System.out.println(noVolatile.realA.get());
}
}
复制代码
/**
* volatile的适用场景
*/
public class UseVolatile implements Runnable {
volatile boolean done = false;
AtomicInteger realA = new AtomicInteger();
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
setDone();
realA.incrementAndGet();
}
}
private void setDone() {
done = true;
}
public static void main(String[] args) throws InterruptedException {
UseVolatile noVolatile = new UseVolatile();
Thread thread1 = new Thread(noVolatile);
Thread thread2 = new Thread(noVolatile);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(noVolatile.done);
System.out.println(noVolatile.realA.get());
}
}
复制代码
volatile
修饰的变量进行赋值能够保证线程安全,可是若是不是直接赋值则没法保证,请看下面的例子
/**
* 不适用volatile的场景
*/
public class NoVolatile2 implements Runnable {
volatile boolean done = false;
AtomicInteger realA = new AtomicInteger();
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
flipDone();
realA.incrementAndGet();
}
}
private void flipDone() {
done = !done;
}
public static void main(String[] args) throws InterruptedException {
NoVolatile2 noVolatile = new NoVolatile2();
Thread thread1 = new Thread(noVolatile);
Thread thread2 = new Thread(noVolatile);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(noVolatile.done);
System.out.println(noVolatile.realA.get());
}
}
复制代码
false
,因此执行偶数次结果应该为false
,但是执行了20000次以后结果倒是true
,从中即可以看出volatile
在此状况下不适用
/**
* 演示可见性带来的问题
*/
public class FieldVisibility {
int a = 1;
int abc = 1;
int abcd = 1;
volatile int b = 2;
public static void main(String[] args) {
while (true) {
FieldVisibility test = new FieldVisibility();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.change();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.print();
}
}).start();
}
}
private void print() {
if (b == 0) {
System.out.println("b = " + b + "; a = " + a);
}
}
private void change() {
abc = 7;
abcd = 70;
a = 3;
b = 0;
}
}
复制代码
在这里b==0
做为触发的条件,由于在change()
方法中最后一句将b设置为0,因此依照happens-before原则在b=0之前的操做都是可见的,从而达到了触发器的做用
volatile
修饰的变量须要使本地缓存失效,而后从主存中读取新值,写一个volatile
变量后会当即刷回主存volatile
是轻量级的synchronized
,当在多线程环境下只作赋值操做时可使用volatile
代替synchronized
,由于赋值操做自身保证原子性,而使用volatile
又能保证可见性,因此能够实现线程安全。
boolean flag;
,或者做为触发器实现轻量级同步。synchronized
是由于它没法提供原子性和互斥性,由于无锁,它也不会在获取锁和释放锁上有开销,因此说它是低成本的。除了volatile能够保证可见性以外,synchronized、Lock、并发集合、Thread.join()和Thread。start()均可以保证可见性(具体看happens-before原则)。
一系列操做要么所有成功,要么所有失败,不会出现只执行一半的状况,是不可分割的。
对于64位值的写入,能够分为两个32位操做进行写入,因此可能会致使64位的值发生错乱,针对这种状况能够添加volatile进行解决。在32位的JVM上它们不是原子的,而在64位的JVM上倒是原子的。
简单的把原子操做组合在一块儿,并不能保证总体依然具备原子性,好比说去银行取两次钱,这两次取钱都是原子操做,可是中途银行卡可能会被女友借走,这样就形成了两次取钱的中断。
/**
* 饿汉式(静态常量)(可用)
*/
public class Singleton1 {
private static final Singleton1 INSTANCE = new Singleton1();
private Singleton1(){
}
public Singleton1 getInstance(){
return INSTANCE;
}
}
复制代码
/**
* 饿汉式(静态代码块) (可用)
*/
public class Singleton2 {
private static Singleton2 INSTANCE;
static {
INSTANCE = new Singleton2();
}
private Singleton2(){}
public static Singleton2 getInstance(){
return INSTANCE;
}
}
复制代码
/**
* 懒汉式(线程不安全) (不可用)
*/
public class Singleton3 {
private static Singleton3 INSTANCE;
private Singleton3(){}
public static Singleton3 getInstance(){
if (INSTANCE == null){
INSTANCE = new Singleton3();
}
return INSTANCE;
}
}
复制代码
由于这样写在多线程状况下有可能线程1进入了if (INSTANCE == null)
但还没来得及建立,此时线程2进入if (INSTANCE == null)
,这样就形成了重复的建立,破坏了单例。
/**
* 懒汉式(线程安全,同步方法) (不推荐用)
*/
public class Singleton4 {
private static Singleton4 INSTANCE;
private Singleton4(){}
public synchronized static Singleton4 getInstance(){
if (INSTANCE == null){
INSTANCE = new Singleton4();
}
return INSTANCE;
}
}
复制代码
由于添加了synchronized
关键字,因此能够保证同一时刻只有一个线程能进入方法也就保证了线程安全。可是因为添加了synchronized
也会对性能产生影响
/**
* 懒汉式(线程不安全,同步代码块) (不可用)
*/
public class Singleton5 {
private static Singleton5 INSTANCE;
private Singleton5() {
}
public static Singleton5 getInstance() {
if (INSTANCE == null) {
synchronized (Singleton5.class) {
INSTANCE = new Singleton5();
}
}
return INSTANCE;
}
}
复制代码
这样写看似可行,但是实际上却不能够。由于只要INSTANCE
为空就会进入判断,不管里面加不加同步迟早都会再次建立,因此这样会致使实例被屡次建立
/**
* 双重检查(推荐面试使用)
*/
public class Singleton6 {
private volatile static Singleton6 INSTANCE;
private Singleton6() {
}
public static Singleton6 getInstance() {
if (INSTANCE == null) {
synchronized (Singleton6.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton6();
}
}
}
return INSTANCE;
}
}
复制代码
优势:线程安全,延迟加载,效率高
为何要double-check?单check行不行?
由于若是不进行第二次检查不管添不添加同步都会对实例进行建立,这样就会建立多个实例,是线程不安全的
若是把synchronized
添加在方法上能够吗?
若是添加在方法上是能够的,可是这样会形成性能问题
为何必定要加volatile
由于新建对象不是原子操做,它须要通过建立空对象、调用构造方法、将地址分配给引用这三个步骤,这样可能会进行重排序,因此就可能出现空指针异常,针对这个问题能够添加volatile
关键字来解决
/**
* 静态内部类式,可用
*/
public class Singleton7 {
private Singleton7() {
}
private static class InnerClass{
//不会对内部静态实例进行初始化
private static Singleton7 INSTANCE = new Singleton7();
}
public static Singleton7 getInstance(){
return InnerClass.INSTANCE;
}
}
复制代码
静态内部类方式是一种“懒汉”的方式,在最初对类加载时不会加载内部类的静态实例
/**
* 枚举单例
*/
public enum Singleton8 {
INSTANCE;
}
复制代码