Java并发编程之Java内存模型

1. 底层原理

1.1 JVM内存结构 VS JMM内存模型

  1. JVM内存结构和JVM的运行区域有关,包括堆、方法区、虚拟机栈、本地方法栈、程序计数器
    1. 堆:线程共享,new出来的实例对象;
    2. 虚拟机栈:线程私有,基本数据类型以及对象的引用地址;
    3. 方法区:线程共享,static静态变量,类信息(方法代码,变量名,方法名,访问权限,返回值),常量,永久引用(static修饰的类);
    4. 本地方法栈:native方法;
    5. 程序计数器:程序的位置,行号数;
  2. JMM内存模型
    1. JMM是一种规范,防止在不同的虚拟机上运行结果不一样,可以更方便地开发出多线程程序
    2. volatile、synchronized、lock等的原理都是JMM,如果没有JMM,必须手动指定什么时候同步

2. 重排序

public class ReOrder {

    private static int x = 0, y = 0;
    private static int a = 0, b = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                a = 1;
                x = b;
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                b = 1;
                y = a;
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}

如上代码所示,正常情况下,x,y有以下三种结果:

  1. t1执行完t2再执行:x=0,y=1
  2. t2执行完t1再执行:x=1,y=0
  3. t1和t2各执行一半:x=1,y=1

那么会不会出现x=0,y=0的结果呢?只有x=b在a=1之前,或者y=a在b=1之前执行才会发生这种情况,这种情况一旦发生,就说明发生了指令重排序。

  • 重排序的定义:当指令的执行顺序和Java代码的执行顺序不一样,就说明发生了指令重排序。
  • 重排序的好处:提升处理速度。
  • 重排序发生的2种情况:
    1. 编译器优化(JVM优化),尤其发生数据没有依赖关系的情况,更有可能会发生指令重排序;
    2. CPU指令重排序:就算编译器不重排序,CPU也可能会发生指令重排序;

2. 可见性

2.1 可见性问题演示

代码演示:

public class Visibility {

    private static int a = 1, b = 2;

    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                a = 3;
                b = a;
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(b+ "," + a);
            }
        });
        t1.start();
        t2.start();
    }
}

正常情况下,会发生一下三种情况:

  1. t2先执行:a = 1, b = 2
  2. t1先执行:a = 3, b = 3
  3. t1执行一半给t2执行:a = 3, b = 2

但是由于内存可见性问题,也可能出现第四种情况:a = 1, b = 3,为什么会发生这种情况?
由于t1线程执行a=3,b=a后,b被写回到主内存,而a还没来得及写回到主内存,此时,t2已经在主内存中读取了a和b的值, 就造成了a=1,b=3的情况。t2线程没“看完整”t1线程的操作,只看到了b的赋值情况,而没看到a的赋值情况。

当使用volatile关键字之后,a的值改变,立刻刷回到主内存,t2读取到的一定是改变的值。

2.1 为什么发生可见性问题?

在这里插入图片描述

  • 如图所示,数据从主内存到CPU过程中有多层缓存,分别是L3、L2、L1、寄存器。由于多层缓存的存在,可以大幅提升CPU的处理效率;
  • 每个核心将自己需要的数据读到私有的缓存中,然后将修改后的值写回到缓存中,最后等待刷到内存中,由于这个等待的过程,当核心1更新某共享数据后,核心2还没有等到核心1将缓存刷回主内存就读取数据了,导致脏数据;

2.2 JMM如何解决可见性问题?

  • JMM定义了一套读写规范,我们不用关心寄存器、一级缓存、二级缓存等,JMM抽象出主内存和本地内存的概念。

  • 本地内存包括寄存器、一级缓存、二级缓存;

  • 主内存包括三级缓存和内存;

  • 主内存和本地内存的关系:

    • 所有的变量都存储在主内存中,同时每个线程都有自己的工作内存,工作内存中的变量是主内存中的拷贝;
    • 线程不能直接读写主内存中的变量,而是只能操作自己工作内存中的变量,然后同步到主内存中;
    • 主内存是多个线程共享的,但是线程不共享工作内存,如果线程之间需要通信,必须借助于主内存中转完成
    • 正是因为需要主内存来交换才导致了可见性问题;

2.3 happens-before原则

  • 什么是happens-before:该原则是用来解决可见性问题的,在时间上,动作A发生在动作B之前,B保证能看见A;
  • 另一种解释:如果一个操作happens-before另一个操作,那么我们说第一个操作对于第二个操作是可见的;
  • 什么不是happens-before:两个线程没有相互配合的机制,所以代码A和B的执行结果不能保证总是被对象看到的,这就不具备happens-before;
  • 只要符合了happens-before原则,就不会产生可见性问题;
  • 符合happens-before原则的常见场景:
    1. 单线程原则:在一个线程之内,后面的语句一定能看到前面的语句做了什么 ,因为每个线程都有自己的工作内存,自己工作内存的变量都是可见的(如果数据之间没有依赖,单线程下会发生指令重排序,但是不影响结果,所以单线程原则不影响重排序);
    2. 锁操作(synchronized和lock):如果t1线程对a对象解锁了,紧接着t2线程对a对象加锁了,那么t2线程能看到t1线程的所有操作,无论t1做了什么修改,做了什么逻辑,t2都可以看到,不会发生脏数据的情况;
    3. volatile:volatile修饰的变量发生的读写操作,当t1线程发生写操作,t2线程进行读操作时一定能看到这个写操作;
    4. 线程启动:子线程一定能看到主线程在执行start()之前的操作;
    5. 线程join:主线程join()后面的语句一定能看到子线程运行的所有的语句;
    6. 传递性:如果a happens-before b, b happens-before c,那么a 一定happens-before c;
    7. 中断:一个线程被其他线程中断时,那么检测中断的线程一定能看到并抛出异常;
    8. 符合happens-before原则的工具类:ConcurrentHashMap、CountDownLatch、线程池、FutureTask、CyclicBarrier;
  • 轻量级同步:给b加了volatile,不仅b被影响,还可以实现轻量级的同步,b = a 之前的代码对读取打印b后的代码可见,所以在写入线程里对a的赋值,一定会对读取线程可见,所以这里的a即使不加volatile,只要b读取到是3,就可以保证a读取到的都是3而不可能是1,所以只给b加volatile,b赋值操作执行之前的其他变量的赋值操作也具有可见性。

2.4 volatile关键字详解

  • 定义:volatile是一种同步机制,比synchronized或Lock锁等更轻量,因为volatile仅仅是控制把缓存中的数据立刻刷回到主内存中,不会被线程缓存,不会给对象上锁,所以不会发生上下文切换等开销很大的行为;
  • 如果一个变量被volatile修饰,那么JVM就知道这个变量有并发可能,就会禁止重排序;
  • volatile无法保证原子性,且只能作用于属性,读写操作都是无锁的,不能替代synchronized,场景有限;
  • 不适用场景:a++
  • 适用场景1:boolean flag(作为一个标记位),如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作(修改,取反,对比),就可以用volatile来代替synchronized,因为赋值本身是有原子性的,而volatile又保证了可见性,所以就足以保证线程安全。
  • 适用场景2:作为刷新之前变量的触发器,只要volatile变量被赋值,那么在其执行之前的赋值操作都可见;
  • volatile的两点作用:
    1. 可见性:读一个volatile变量之前,需要先使相应的本地缓存失效,这样就必须到主内存读取最新值,写一个volatile属性会立即刷入到主内存。
    2. 禁止指令重排序优化:解决单例双重锁乱序问题
  • 保证可见性的措施:synchronized、Lock、并发集合、join、start都保证可见性;
  • synchronized可见性:不仅保证了原子性,还保证了可见性;凡是被synchronized修饰的代码,上一个线程的操作可以被下一个线程所看到;
  • synchronized近朱者赤:在单个线程中synchronized修饰的代码块,在其之前的赋值操作也是对另一个线程可见的;

3. 原子性

3.1 什么是原子性?

  • 定义:一系列的操作要么全部都成功,要么全部都不成功,是不可分割的。
  • i++不是原子性的。
  • 用synchronized实现原子性:保证同一时刻只有一个线程执行这段代码
  • 原子操作 + 原子操作 != 原子操作。
  • Java中的原子操作有哪些:
    1. 除了long和double外的基本数据类型的赋值操作,在32位的JVM上,long和double的操作不是原子性的,在64位上是原子性的,在商用JVM中不会出现这种问题;
    2. 所有引用的赋值操作;
    3. Atomic包中的所有类的原子操作;

3.2 synchronized关键字详解

3.2.1 synchronized基本用法
  • 定义:如果一个对象对多个线程可见,synchronized能够保证在同一时刻最多只有一个线程操作这个对象,以达到保证并发安全的效果。
  • 作用:保证可见性和原子性,可以避免线程安全问题:运行结果错误
  • 两种使用方法:
    1. 对象锁:

      1. 方法锁,默认锁对象为this当前实例对象

        public class ObjectLock3 implements Runnable {
            @Override
            public void run() {
                method();
            }
        
            public synchronized void method() {
                System.out.println(Thread.currentThread().getName() + "进入同步方法");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        
            public static void main(String[] args) {
                ObjectLock3 objectLock3 = new ObjectLock3();
                Thread t1 = new Thread(objectLock3);
                Thread t2 = new Thread(objectLock3);
                t1.start();
                t2.start();
            }
        }
        
      2. 同步代码块锁,自己指定锁对象

        public class ObjectLock1 implements Runnable {
        
            @Override
            public void run() {
                synchronized (this) {
                    System.out.println(Thread.currentThread().getName() + "进入同步代码块");
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "退出同步代码块");
                }
            }
        
            public static void main(String[] args) {
                ObjectLock1 objectLock1 = new ObjectLock1();
                new Thread(objectLock1).start();
                new Thread(objectLock1).start();
            }
        }
        Thread-0进入同步代码块
        Thread-0退出同步代码块
        Thread-1进入同步代码块
        Thread-1退出同步代码块
        
        public class ObjectLock2 implements Runnable {
        
            private static final Object lock1 = new Object();
            private static final Object lock2 = new Object();
        
            @Override
            public void run() {
                synchronized (lock1) {
                    System.out.println(Thread.currentThread().getName() + "进入同步代码块1");
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "退出同步代码块1");
                }
                synchronized (lock2) {
                    System.out.println(Thread.currentThread().getName() + "进入同步代码块2");
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "退出同步代码块2");
                }
            }
            public static void main(String[] args) {
                ObjectLock2 objectLock2 = new ObjectLock2();
                new Thread(objectLock2).start();
                new Thread(objectLock2).start();
            }
        }
        Thread-0进入同步代码块1
        Thread-0退出同步代码块1
        Thread-0进入同步代码块2
        Thread-1进入同步代码块1
        Thread-1退出同步代码块1
        Thread-0退出同步代码块2
        Thread-1进入同步代码块2
        Thread-1退出同步代码块2
        
    2. 类锁:

      1. 静态方法锁,synchronized加在static方法上,锁对象为当前类

        public class ObjectStaticLock1 implements Runnable {
        
            @Override
            public void run() {
                method();
            }
        
            public static synchronized void method() {
                System.out.println(Thread.currentThread().getName() + "进入到同步静态方法中");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "退出同步静态方法");
            }
        
            public static void main(String[] args) {
                ObjectStaticLock1 objectStaticLock1 = new ObjectStaticLock1();
                ObjectStaticLock1 objectStaticLock2 = new ObjectStaticLock1();
                Thread t1 = new Thread(objectStaticLock1);
                Thread t2 = new Thread(objectStaticLock2);
                t1.start();
                t2.start();
            }
        }
        
      2. 同步代码块锁,synchronized(*.class)代码块,指定锁对象为class对象,所谓的类锁,不过是Class对象的锁而已

        public class ObjectStaticLock2 implements Runnable {
            @Override
            public void run() {
                synchronized (ObjectStaticLock2.class) {
                    System.out.println(Thread.currentThread().getName() + "进入到同步代码块");
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "退出同步代码块");
                }
            }
        
            public static void main(String[] args) {
                ObjectStaticLock2 objectStaticLock1 = new ObjectStaticLock2();
                ObjectStaticLock2 objectStaticLock2 = new ObjectStaticLock2();
                Thread t1 = new Thread(objectStaticLock1);
                Thread t2 = new Thread(objectStaticLock2);
                t1.start();
                t2.start();
            }
        }
        
3.2.2 多线程访问同步方法的7种情况
  1. 两个线程同时访问一个对象的同步方法:会发生同步,锁对象都为同一个实例对象;
  2. 两个线程同时访问两个对象的同步方法:互不影响,锁对象不同;
  3. 两个线程访问的是synchronized的静态方法:会发生同步,锁对象都为Class对象,Class对象只有一个;
  4. 同时访问同步方法和非同步方法:非同步方法不受影响,不发生同步;
  5. 访问同一个对象不同的普通同步方法:会发生同步,锁对象默认为同一个实例对象;
  6. 同时访问静态synchronized和非静态synchronized方法:互不影响,静态syn方法的锁对象为Class对象,非静态syn方法的锁对象为一个实例对象this,实例对象和Class对象不是同一个对象,实例对象在堆中,Class对象在方法区中;
  7. 方法抛出异常后,会释放锁;
    总结:
    1. 一把锁只能同时被一个线程获取,没拿到锁的线程必须等待,如1、5;
    2. 每个实例都有自己的一把锁,不同实例互不影响,当使用Class对象以及synchonized修饰的static方法的时候,所有对象共用同一把类锁,对应2、3、4、5;
    3. 遇到异常,会释放锁,对应7;
3.2.3 synchronized关键字的性质
3.2.3.1 可重入
  • 定义:一个线程已经获取到锁,想再次获取到这把锁时不需要释放,直接可以用;
  • 什么是不可重入:一个线程获取到锁之后,想再次使用这个锁,必须释放锁之后还其他线程竞争;
  • 好处:避免死锁:假如一个类有两个synchronized方法,当一个线程执行了方法1获得了默认的this对象锁,这个时候要执行方法2,如果synchronized不具备可重入性,那么这个线程就无法获取到访问方法2的锁,又无法释放锁,就造成了死锁。
  • 粒度:线程范围,在一个线程中,只要这个线程拿到了这把锁,在这个线程内部就可以一直使用
    1. 同一个方法是可重入的;
    2. 可重入不要求是同一个方法;
    3. 可重入不要求是同一个类中;
3.2.3.2 不可中断

一旦这个锁已经被别的线程获得了,如果本线程还想获得,该线程只能等待或阻塞,直到别的线程释放这个锁。如果别的线程永远不释放锁,那么本线程则永远等待下去。
相比之下,Lock类,拥有可以中断的能力:

  • 如果等的时间过长,可以中断现在已经获取的锁的线程的执行;
  • 如果等待时间过长,也可以退出。
3.2.4 synchronized原理
3.2.4.1 加锁和释放锁原理
  • 每个一个对象都有一个内置的monitor锁,这个锁存储在对象头中的,锁的获取和释放实际上需要执行两个指令:monitorenter和monitorexit,当线程执行到monitorenter的时候会尝试获取这个锁;
  • 反编译:先javac demo.java,然后javap -verbose demo.class文件;
  • monitorenter和monitorexit在执行的时候会让对象锁的计数+1或-1;
  • 获取锁的过程:首先一个线程要获取一个对象锁的时候会查看这个monitor锁的计数器如果为0,那么就给他+1,这样别的线程就进不来了,如果一个线程有了这把锁,又重入了,在计数器再+1;如果monitor被其他线程持有了,直到计数器=0,才会获取这个锁。
  • 释放锁的过程:将monitor的计数器-1,直到=0,表示不再拥有所有权了,如果不是0,说明刚才是可重入进来的
3.2.4.1 可重入原理

一个线程拿到一把锁之后,还想再次进入由这把锁所控制的方法,则可以再次进入,原理是用了monitor锁的计数器。

  • JVM负责跟踪被加锁的次数
  • 线程第一次给对象加锁的时候,计数+1.每当这个相同的线程再次获取该对象锁的时候,计数器会递增;
  • 每当任务离开的时候,计数递减,当计数为0的时候,锁被完全释放;
3.2.4.1 可见性原理

线程A和线程B通信:

  1. 本地内存A把修改后的内容放到主内存中;
  2. 本地内存B从主内存从读取修改后的内容;

synchnized修饰的代码块对对象的任何修改,在释放锁之前都要将修改的内容先写回到主内存中,所以从主内存中读取的内容都是最新的。

3.2.5 synchronized的缺陷
  • 效率低:锁的释放情况少(只有代码执行完和抛异常)、试图获得锁时候不能设定超时、不能中断一个正在试图获得锁的线程
  • 不够灵活:加锁和释放的时机单一,每个锁仅仅有单一的条件,可能是不够的。读写锁更灵活。
  • 无法知道是否成功获取到锁,没法去尝试获取,去判断。Lock是可以通过tryLock方法尝试获取,返回true代表成功加锁。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值