private static int count;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
private static void increment() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
什么是CAS?
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
count.incrementAndGet();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
count.incrementAndGet();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
public static void main(String[] args) throws InterruptedException {
AtomicStampedReference<String> reference = new AtomicStampedReference<>("A", 1);
String oldValue = reference.getReference();
int oldVersion = reference.getStamp();
boolean result = reference.compareAndSet(oldValue, "B", oldVersion, oldVersion + 1);
System.out.println("修改1版本的:" + result);
result = reference.compareAndSet("B", "C", 1, 2);
System.out.println("修改2版本的:" + result);
}
private static int count;
private static ReentrantLock lock = new ReentrantLock();
public static void increment() {
lock.lock();
try {
count++;
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
ThreadLocal 保证原子性的方式,是不让多线程去操作临界资源,而是让每个线程去操作属于自己的数据。
static ThreadLocal tl1 = new ThreadLocal();
static ThreadLocal tl2 = new ThreadLocal();
public static void main(String[] args) throws InterruptedException {
tl1.set("123");
tl2.set("456");
Thread t1 = new Thread(() -> {
System.out.println("t1:" + tl1.get()); // null
System.out.println("t1:" + tl2.get()); // null
});
t1.start();
System.out.println("main:" + tl1.get()); // 123
System.out.println("main:" + tl2.get()); // 456
}
ThreadLocal实现原理:
ThreadLocal 内存泄漏问题:
可见性问题是基于 CPU 位置出现的,CPU 处理速度非常快,相对CPU而言,去主内存获取数据这个事情太慢了,CPU提供了 L1、L2、L3 三级缓存,每次去主内存拿完数据后,就会存储到 CPU 的三级缓存,每次去三级缓存拿数据,效率肯定会提升。
但这就带来问题,现在CPU都是多核,每个线程的工作内存(CPU三级缓存)都是独立的, 会告知每个线程中做修改时,只改自己的工作内存,没有及时的同步到主内存中,导致数据不一致的问题。
private static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (flag) {
// ...
}
System.out.println("t1线程结束");
});
t1.start();
Thread.sleep(10);
flag = false;
System.out.println("主线程将flag改为false");
}
volatile 是一个关键字,用来修饰成员变量。如果属性被 volatile 修改,相当于会告诉CPU,对当前属性的操作,不允许使用CPU缓存,必须去主内存操作。
volatile的内存语义:
其实加了volatile就是告诉CPU,对当前属性的读写操作,不允许使用 CPU 缓存,加了 volatile 修饰的属性,会在转为汇编指令后,追加一个 lock 的前缀,CPU执行这个指令时,对于带有 lock 前缀的会做两个事情:
总结:volatile就是让 CPU 每次操作这个数据时,必须立即同步到主内存,以及从主内存读取数据。
private volatile static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (flag) {
// ...
}
System.out.println("t1线程结束");
});
t1.start();
Thread.sleep(10);
flag = false;
System.out.println("主线程将flag改为false");
}
synchronized也可以解决可见性问题,synchronized的内存语义:
private static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (flag) {
synchronized (MyTest.class) {
// ...
}
// System.out.println(); // 打印方法内部也使用了synchronized
}
System.out.println("t1线程结束");
});
t1.start();
Thread.sleep(10);
flag = false;
System.out.println("主线程将flag改为false");
}
Lock锁保证可见性的方式和 synchronized完全不同,synchronized基于它的内存语义,在获取锁和释放锁时,对CPU缓存做了一个同步到主内存的操作。
Lock锁时基于volatile实现的,Lock锁内部再进行加锁和释放锁时,会对一个由volatile修饰的state属性进行加减操作。
如果对volatile修饰的属性进行写操作,CPU会执行带有lock前缀的指令,CPU会降修改后的数据,从CPU缓存立即同步到主内存,同时也会将其他属性也立即同步到主内存中。还会降其他CPU缓存行中的这个数据设置为无效,必须重新去主内存中拉取。
private static boolean flag = true;
private static Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (flag) {
lock.lock();
try {
// ...
} finally {
lock.unlock();
}
}
System.out.println("t1线程结束");
});
t1.start();
Thread.sleep(10);
flag = false;
System.out.println("主线程将flag改为false");
}
final修饰的属性,在运行期间是不允许修改的,这样一来,就间接保证了可见性,所有多线程读取 final 属性,值肯定是一样的。
final并不是说每次取数据从主内存读取,也没这个必要,而且final和volatile不允许同时修饰一个变量。
final修饰的内容已经不允许再次别写了,而 volatile 是保证每次读写数据去主内存读取,并且volatile会影响一定的性能,就不需要同时修饰。
在Java中,.java 文件中的内容会被编译,在执行前需要再次转为 CPU 可以识别的指令,CPU在执行这些指令时,为了提升执行效率,在不影响最终结果的前提下(满足一下要求),会对指令进行重排。
指令乱序执行的原因,是为了尽可能的发挥 CPU 的性能。Java 中的程序是乱序执行的,验证乱序执行效果:
static int a, b, x, y;
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < Integer.MAX_VALUE; i++) {
a = 0;
b = 0;
x = 0;
y = 0;
Thread t1 = new Thread(() -> {
a = 1;
x = b;
});
Thread t2 = new Thread(() -> {
b = 1;
y = a;
});
t1.start();
t2.start();
t1.join();
t2.join();
if (x == 0 && y == 0) {
System.out.println("第" + i + "次,x = " + x + ", y = " + y);
}
}
}
DCL 单例模式由于指令重排序可能出现问题:线程可能会拿到没有初始化的对象,导致在使用时,由于内部属性为默认值,导致出现一些不必要的问题:
public class MyTest {
private static volatile MyTest test;
private MyTest() {}
public static MyTest getInstance() {
// B
if (test == null) {
synchronized (MyTest.class) {
if (test == null) {
// A: 开辟空间、test指向地址、初始化
test = new MyTest();
}
}
}
return test;
}
}
as-if-serial语义:不论指令如何重排序,需要保证单线程的程序执行结果是不变的。而且如果存在依赖关系,那么也不可以做指令重排。
// 这种情况肯定不能做指令重排
int i = 0;
i++;
// 这种情况肯定不能做指令重排序
int j = 200;
j * 100;
j + 100;
// 这里即便出现了指令重排,也不可以影响最终的结果,20100
具体规则:
**JMM 只有不出现上述 8 种情况时,才不会触发指令重排效果。**不需要过分的关注happens-before原则,只需要可以写出线程安全的代码就可以了。
如果需要让程序对某一个属性的操作不出现指令重排,除了满足 happens-before 原则之外,还可以基于 volatile 修饰属性,从而对这个属性的操作,就不会出现指令重排问题。
volatile如何实现的禁止指令重排?