一、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不具备原子性
二、原子性
- 理解原子性
/** * @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,然后又同时自增,同时写入主存,结果还是会出现重复数据。
- 原子变量
JDK 1.5之后,Java提供了原子变量,在java.util.concurrent.atomic包下。原子变量具备如下特点:
- 有volatile保证内存可见性。
- 用CAS算法保证原子性。
- 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。
- 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; } }