并发三大特性
原子性
一个或多个操作,要么全部执行且在执行过程中不被任何因素打断,要么全部不执行。在 Java 中,对基本数据类型的变量的读取和赋值操作是原子性操作(64位处理器)。不采取任何的原子性保障措施的自增操作并不是原子性的,比如i++操作。
如何保证原子性
- 通过 synchronized 关键字保证原子性
- 通过 Lock锁保证原子性
- 通过 CAS保证原子性
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class AtomicDemo {
private static int counter = 0;
private static AtomicInteger counter2 = new AtomicInteger();
private static int counter3 = 0;
private static int counter4 = 0;
private final static Lock lock = new ReentrantLock();
static synchronized void increment() {
counter3++;
}
static void increment2() {
lock.lock();
try {
counter4++;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread t = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
counter++;
}
for (int j = 0; j < 10000; j++) {
counter2.incrementAndGet();
}
for (int j = 0; j < 10000; j++) {
increment();
}
for (int j = 0; j < 10000; j++) {
increment2();
}
});
t.start();
// try {
// t.join();
// } catch (InterruptedException e) {
// throw new RuntimeException(e);
// }
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(counter);
System.out.println(counter2.get());
System.out.println(counter3);
System.out.println(counter4);
}
}
可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
如何保证可见性
- 通过 volatile 关键字保证可见性
- 通过内存屏障保证可见性
- Thread.sleep() 使用了内存屏障,保证了可见性
- 通过 synchronized 关键字保证可见性
- 通过 Lock锁保证可见性
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class VisibilityDemo {
private boolean flag = true;
private volatile boolean flag2 = true;
private boolean flag3 = true;
private boolean flag4 = true;
private final static Lock lock = new ReentrantLock();
public void refresh() {
// 希望结束数据加载工作
flag = false;
flag2 = false;
flag3 = false;
flag4 = false;
System.out.println(Thread.currentThread().getName() + "修改flag:"+flag);
System.out.println(Thread.currentThread().getName() + "修改flag2:"+flag2);
System.out.println(Thread.currentThread().getName() + "修改flag3:"+flag3);
System.out.println(Thread.currentThread().getName() + "修改flag4:"+flag4);
}
public void load() {
System.out.println(Thread.currentThread().getName() + "开始执行.....");
while (flag) {
//TODO 业务逻辑:加载数据
}
System.out.println(Thread.currentThread().getName() + "数据加载完成,跳出循环");
}
public void load2() {
System.out.println(Thread.currentThread().getName() + "开始执行.....");
while (flag) {
//TODO 业务逻辑:加载数据
try {
Thread.sleep(1);
// Thread.sleep(0);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(Thread.currentThread().getName() + "数据加载完成,跳出循环");
}
public void load3() {
System.out.println(Thread.currentThread().getName() + "开始执行.....");
while (flag) {
//TODO 业务逻辑:加载数据
synchronized (this) {
}
}
System.out.println(Thread.currentThread().getName() + "数据加载完成,跳出循环");
}
public void load4() {
System.out.println(Thread.currentThread().getName() + "开始执行.....");
while (flag) {
lock.lock();
try {
} finally {
lock.unlock();
}
}
System.out.println(Thread.currentThread().getName() + "数据加载完成,跳出循环");
}
public static void main(String[] args) {
var d = new VisibilityDemo();
// 线程threadA模拟数据加载场景
Thread t1 = new Thread(() -> d.load(), "threadA1");
t1.start();
Thread t2 = new Thread(() -> d.load2(), "threadA2");
t2.start();
Thread t3 = new Thread(() -> d.load3(), "threadA3");
t3.start();
Thread t4 = new Thread(() -> d.load4(), "threadA4");
t4.start();
// 让threadA先执行一会儿后再启动线程B
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 线程threadB通过修改flag控制threadA的执行时间,数据加载可以结束了
Thread threadB = new Thread(() -> d.refresh(), "threadB");
threadB.start();
}
}
有序性
即程序执行的顺序按照代码的先后顺序执行。为了提升性能,编译器和处理器常常会对指令做重排序,所以存在有序性问题。
如何保证有序性
-
通过 volatile 关键字保证有序性
-
通过 内存屏障保证有序性
-
通过 synchronized关键字保证有序性
-
通过Lock锁保证有序性
java线程的内存交互模型
主内存与工作内存交互协议
Java内存模型定义了以下八种原子操作来完成
- lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
- unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
- use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
- write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
可见性案例深入分析
Java中可见性底层有两种实现:
- 内存屏障 (synchronized Threed.sleep(10) volatile)
- cpu上下文切换 (Threed.yield() Threed.sleep(0) )
锁的内存语义
锁获取和释放的内存语义:
- 当线程获取锁时,JMM会把该线程对应的本地内存置为无效。
- 当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。
volatile内存语义
volatile写的内存语义:
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
volatile读的内存语义:
当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量。
正确的用法应该是使用volatile修饰singleton
原因就在于singleton = new Singleton()这行代码,创建了一个对象。这行代码可以分解为三行伪代码
memory = allocate(); //1. 分配对象内存空间
ctorInstance(memory); //2.初始化对象
instance = memory; //3.设置instance指向刚刚分配的内存地址
上面2和3之间可能会被重排序
happens-before
JSR-133使用happens-before的概念来指定两个操作之间的执行顺序。由于这两个操作可以在一个线程之内,也可以在不同的线程之内。因此,JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证。
JMM遵循一个基本原则:只要不改变程序的执行结果,编译器和处理器怎么优化都行。
总结
stance = memory; //3.设置instance指向刚刚分配的内存地址
上面2和3之间可能会被重排序
### happens-before
JSR-133使用happens-before的概念来指定两个操作之间的执行顺序。由于这两个操作可以在一个线程之内,也可以在不同的线程之内。因此,JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证。
> JMM遵循一个基本原则:只要不改变程序的执行结果,编译器和处理器怎么优化都行。
### 总结
**Java中的volatile关键字可以保证多线程操作共享变量的可见性以及禁止指令重排序,synchronized关键字不仅保证可见性,同时也保证了原子性(互斥性)。在更底层,JMM通过内存屏障来实现内存的可见性以及禁止重排序。**为了程序员的方便理解,提出了happens-before,它更加的简单易懂,从而避免了程序员为了理解内存可见性而去学习复杂的重排序规则以及这些规则的具体实现方法。