可见性

  1. 可见性:一个线程对共享变量值的修改,能够及时被其他线程看到;
  2. 共享变量:如果一个变量在多个线程的工作内存中都存在副本,那这个变量就是这几个线程的共享变量;
  3. 线程的工作内存:Java内存抽象出来的概念
  4. Java内存模型(JMM-Java Memory Model):描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取出变量这样的底层细节
  5. 所有变量都存储在主内存中;而每个线程有自己独立的工作内存,在工作内存中的保存的该线程使用的变量(此变量是主内存中变量的副本)
  6. Java内存的规定:
    线程对共享变量的所有操作都必须在自己的工作内存中进行,不可直接从主内存中读写;
    不同线程之间无法直接访问其他线程工作内存中的变量,线程间的变量值的传递需要通过主内存
  7. 共享变量可见性实现原理:
    线程1–>工作内存1中变量X–>更新到主内存中–>工作内存2中的变量X得到更新–>线程2

java语言层面实现可见性的两种方式

synchronized实现可见性

1. synchronized能够实现:

  • 原子性(同步)
  • 可见性

2. JMM关于synchronized的两条规定:

  • 线程解锁前,必须把共享变量的最新值刷新到内存中。
  • 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主存中中重新读取最新的值。(注意:加锁与解锁需要是同意把锁)

3. 线程执行互斥代码的过程:

  1. 获得互斥锁
  2. 清空工作内存
  3. 从主内存拷贝变量到最新副本到工作内存
  4. 执行代码
  5. 刷新变量到主内存
  6. 释放互斥锁

4. 重排序

代码书写的顺序与实际执行的顺序不同,指令重排序是编译器或处理器为了提高程序性能而坐的优化

  1. 编译器优化的重排序(编译器优化)
  2. 指令级并行重排序(处理器优化)
  3. 内存系统的重排序(处理器优化)

eg:
代码书写顺序:

int number = 1;
int result = 0;

执行顺序:

int result = 0;
int number = 1;

5. as-if-serial

as-if-serial:无论怎么样重排序,程序执行的结果应该与代码顺序执行的结果一直(java编译器,运行时和处理器都会保证java在单线程下遵循as-if-serial语义

6. 不可见原因

  1. 线程的交叉执行
  2. 重排序结合线程交叉执行
  3. 共享变量更新后的值没有在工作内存和主内存间及时更新

7.例子

package syn_vol;

public class SynchronizedDemo {
    //共享变量
    private boolean ready = false;
    private int result = 0;
    private int number = 1;
    //写操作
    public synchronized void write(){
        ready = true;   //1.1
        number = 2;     //1.2
    }
    //读操作
    public  synchronized void read(){
        if(ready){            //2.1
            result = number*3;//2.2
        }
        System.out.println("result的值:"+result);
    }

    //内部线程类
    private class ReadWriteThread extends Thread{
        //根据构造方法中传入的参数,确定线程执行读还是写操作
        private boolean flg;
        public ReadWriteThread(boolean flg){
            this.flg = flg;
        }
        @Override
        public void run() {
            if(flg){
                //构造方法中传入true,执行写方法
                write();
            }else {
              //构造方法中传入false,执行读方法
                read();
            }
        }
    }
    public static void main(String[] args) {
        SynchronizedDemo synDemo = new SynchronizedDemo();
        //写方法
        synDemo.new ReadWriteThread(true).start();
        //读方法
        synDemo.new ReadWriteThread(false).start();
        //执行顺序
        //1.1-->2.1-->2.2-->1.2 result:3
        //1.2-->2.1-->2.2-->1.1 result:0
    }
}

volatile实现可见性

1. volatile关键字:

  1. 能够保证volatile变量的可见性。
  2. 不能保证volatile变量复合操作的原子性。

2. 如何实现

volatile如何实现内存可见性:通过加入内存屏障和禁止重排序优化来实现的。

1.对volatile变量执行写操作时,会在写操作后加入一条store屏障指令。
2.对volatile变量执行读操作时,会在读操作前加入一条load屏障指令。

3. 线程读/写volatile变量的过程:

  • 线程写volatile变量的过程:
  1. 改变线程工作内存的中volatile变量副本的值。
  2. 讲改变后的副本的值从工作内存刷新到主内存。
  • 线程读volatile变量的过程:
  1. 从主内存中读取volatile变量的最新值到线程的工作内存中。
  2. 从工作内存中读取volatile变量的副本。

4.程序分析

package syn_vol;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class VolatileDemo {
    private volatile int number = 0;
    private Lock lock = new ReentrantLock();
    public int getNumber(){
        return this.number;
    } 

    public void increase(){
//        synchronized(this){
//            this.number++;
//        }
        lock.lock();//加锁
        try {
            this.number++;
        } finally{
            lock.unlock();//解锁
        }
    }

    public static void main(String[] args) {
        //匿名内部类访问外部类局部变量则外部类的对象必须为final属性
        final VolatileDemo volDemo = new VolatileDemo();
        for(int i=0;i<500;i++){
            new Thread(new Runnable() {
                public void run() {
                    volDemo.increase();
                }
            }).start()  ;
        }

        //如果还有子线程在运行,主线程就让出CPU资源
        //直到所有的子线程都运行完了,主线程再继续执行
        while (Thread.activeCount()>1) {
            Thread.yield();
        }
        System.out.println("number:"+volDemo.getNumber());
    }
}

说明:
1. 线程A读取number的值
2. 线程B读取number的值
3. 线程B执行加1
4. 线程B写入最新number的值6
5. 线程A执行加1
6. 线程A写入最新的number值5

两次number++操作,实际上只加了一次,导致number的值与实际不符合

5.保证原子性操作:

  1. 使用synchronized关键字
  2. 使用ReentrantLock(java.util.concurrent.locks包下)
  3. 使用AtomicInterger(java.util.concurrent.atomic包下)

synchronized和volatile比较

  • volatile不需要加锁,比synchronized更轻量级,不需要阻塞线程
  • 从内存可见性分析,volatile读操作相当于加锁,写操作相当于解锁
  • synchronized既能保持可见性,又能保持原子性,而volatile只能保持可见性不能保证原子性

扩展

long和double都是8字节64位的,当程序中有一个long或double类型的变量,而且该变量也没有任何关键字修饰,那么此时对64位的long、double变量的读写就不是原子操作,即可以分解.

因为java内存模型允许JVM将没有被volatile修饰的64位数据类型的读写操作划分为两次32位的读写操作来进行.就可能导致多线程并发访问该变量时,出现只读了一半就被抢走资源进而导致数据异常的问题.

解决方法是在
1. long、double类型的变量上加入volatile关键字.
2. 也可以直接使用synchronized修饰.

不过大部分虚拟机已经把long、double类型的变量对其进行一些保护,因此在实际编程中也不需刻意为long、double类型的数据加上volatile关键字.