一、volatile关键字与内存可见性

1. 内存可见性

代码展示

/**
 * @Description 关于volatile关键字内存可见性问题
 * @Author Meng
 * @Versions
 * @Date 2021-08-04-9:28
 */
public class TestVolatile {
    public static void main(String[] args) {
        ThreadDome threadDome = new ThreadDome();
        new Thread(() -> {
            threadDome.modify();
        },"AA").start();

        new Thread(() ->{
            synchronized (threadDome){
                while (true) {
                    if (threadDome.isFlag()) {
                        System.out.println(Thread.currentThread().getName() + "->" + threadDome.isFlag());
                        break;
                    }
                }
            }

        },"BB").start();
    }
}

class ThreadDome {
    private boolean flag = false;


    public void modify() {
        synchronized (this) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            setFlag(true);
            System.out.println(Thread.currentThread().getName() + "->" + isFlag());
        }
    }

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}

这段代码先创建了一个资源类ThreadDome,它有一个成员变量flag初始值为false,还有一个方法isFlag() 用于改变成员变量flag = true,同时还有一条输出语句。main方法里面创建了一个线程用于调用资源类中的方法,还有主线程中while语句去调用flag,若flag=true则结束while循环,并输出flag的值。

图片说明

从图中可以看到,该程序并没有结束,也就是死循环。说明主线程读取到的flag还是false,可是另一个线程明明将flag改为true了,而且打印出来了,这是什么原因呢?这就是内存可见性问题。

  • 内存可见性问题:当多个线程同时操作共享变量时,线程彼此不可见。就是A 线程 看不到 B线程对共享变量的修改
    图片说明

要解决这个问题,可以加锁。如下:

while (true){
        synchronized (threadDemo){
            if (threadDemo.isFlag()){
                System.out.println("主线程读取到的flag = " + threadDemo.isFlag());
                break;
            }
        }
 }
  • 给while 下面的代码加上synchronize(threadDome) 这样就只有拿到锁才能指行下面的代码。假如BB线程先拿到锁这个while就会一直循环下去,直到AA线程拿到锁后修改flag = true ,然后将修改好的值同步到主存,执行完后释放锁。BB线程拿到锁后读取主存的flag = true 才会输出语句结束循环,然后释放锁。结束
  • 但是加上synchronize后,每次只拿拿到锁的那一个线程才能访问,其他的就会阻塞,效率就会非常低,不想加锁,又要解决内存可见性问题,那么就可以使用volatile关键字。

2. volatile关键字

用法:

package com.meng.volatileDemo;

/**
 * @Description 关于volatile关键字内存可见性问题
 * @Author Meng
 * @Versions
 * @Date 2021-08-04-9:28
 */
public class TestVolatile {
    public static void main(String[] args) {
        ThreadDome threadDome = new ThreadDome();
        new Thread(() -> {
            threadDome.modify();
        },"AA").start();

        new Thread(() ->{
//            synchronized (threadDome){
                while (true) {
                    if (threadDome.isFlag()) {
                        System.out.println(Thread.currentThread().getName() + "->" + threadDome.isFlag());
                        break;
                    }
                }
//            }

        },"BB").start();
    }
}

class ThreadDome {
    private volatile boolean flag = false;


    public void modify() {
//        synchronized (this) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            setFlag(true);
            System.out.println(Thread.currentThread().getName() + "->" + isFlag());
//        }
    }

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}

这样就可以解决内存可见性问题

  • volatile和synchronize的区别
    volatile不具备互斥性(互斥性就是一个线程拿到锁后,其他线程进不来)
    volatile不具备原子性

二、原子性

  1. 理解原子性
    /**
    * @Description 原子性:就是一个操作不能再分
    * @Author Meng
    * @Versions
    * @Date 2021-08-04-11:00
    */
    public class TestIcon {
     public static void main(String[] args) {
         AtomicDemo atomicDemo = new AtomicDemo();
         for (int i = 0; i < 10; i++) {
             new Thread(() -> {
                 try {
                     Thread.sleep(100);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
                 System.out.println("线程:" + Thread.currentThread().getName() + "->" + atomicDemo.getI());
             },String.valueOf(i)).start();
         }
     }
    }
    class AtomicDemo {
     volatile int i;
     public int getI(){
         return i++;
     }
    }
    图片说明

可以发现,出现了重复数据。明显产生了多线程安全问题,或者说原子性问题。所谓原子性就是操作不可再细分,而i++操作分为读改写三步,如下:

int temp = i;
i = i+1;
i = temp;

图片说明

  • 看到这里,好像和上面的内存可见性问题一样。是不是加个volatile关键字就可以了呢?其实不是的,因为加了volatile,只是相当于所有线程都是在主存中操作数据而已,但是不具备互斥性。比如两个线程同时读取主存中的0,然后又同时自增,同时写入主存,结果还是会出现重复数据。
  1. 原子变量
    JDK 1.5之后,Java提供了原子变量,在java.util.concurrent.atomic包下。原子变量具备如下特点:
  • 有volatile保证内存可见性。
  • 用CAS算法保证原子性。
  1. CAS算法:
    CAS算法是计算机硬件对并发操作共享数据的支持,CAS包含3个操作数:
  • 内存值V
  • 预估值A
  • 更新值B
    当且仅当V== A 时,才会把B的值赋给V,即 V = B,否则不做任何操作。就上面的i++问题,CAS算法是这样处理的:首先V是主存中的值0,然后预估值A也是0,因为此时还没有任何操作,这是V = B,所有进行自增,同时把主存中的值变为1.如果第二个线程读取到主存中的还是0也没关系,因为此时预估值子已经变成1 V != A 所有不进行任何操作。
    /**
    * @Description 原子性:就是一个操作不能再分
    * @Author Meng
    * @Versions
    * @Date 2021-08-04-11:00
    */
    public class TestIcon {
      public static void main(String[] args) {
          AtomicDemo atomicDemo = new AtomicDemo();
          for (int i = 0; i < 10; i++) {
              new Thread(() -> {
                  try {
                      Thread.sleep(100);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  System.out.println("线程:" + Thread.currentThread().getName() + "->" + atomicDemo.getI());
              },String.valueOf(i)).start();
          }
      }
    }
    class AtomicDemo {
    //    volatile int i;
      // 创建一个初始值为0的新 原子整数
      AtomicInteger i =  new AtomicInteger();
      public int getI(){
          // 原子地递增当前值,具有VarHandle.getAndAdd指定的记忆效应。
          // 等效于getAndAdd(1) 。
          return i.getAndIncrement();
      }
    }

    三、锁的分段机制

    JDK 1.5之后,在java.util.concurrent包中提供了多种并发容器类来改进同步容器类的性能。其中最主要的就是ConcurrentHashMap。
  1. ConcurrentHashMap:
    ConcurrentHashMap就是一个线程安全的hash表。我们知道HashMap是线程不安全的,HashTable加了锁,是线程安全的,因此它效率低。HashTable加锁就是将整个hash表锁起来,当有多个线程访问是,同一时间只能有一个线程访问,并行变成了串行,因此效率低。ConcurrentHashMap,它采用了锁分段机制
    图片说明

如上图所示,ConcurrentHashMap默认分成了16个segment,每个Segment都对应一个Hash表,且都有独立的锁。所以这样就可以每个线程访问一个Segment,就可以并行访问了,从而提高了效率。这就是锁分段。但是,java 8 又更新了,不再采用锁分段机制,也采用CAS算法了。

四、创建线程的方式---实现Callable接口

public class TestCallable {
    public static void main(String[] args){
        CallableDemo callableDemo = new CallableDemo();
        //执行callable方式,需要FutureTask实现类的支持,用来接收运算结果
        FutureTask<Integer> result = new FutureTask<>(callableDemo);
        new Thread(result).start();
        //接收线程运算结果
        try {
            Integer sum = result.get();//当上面的线程执行完后,才会打印结果。跟闭锁一样。所有futureTask也可以用于闭锁
            System.out.println(sum);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class CallableDemo implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
       int sum = 0;
       for (int i = 0;i<=100;i++){
           sum += i;
       }
       return sum;
    }
}