一、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;
}
}
京公网安备 11010502036488号