【Java】并发编程三大特性
目录
一、可见性(visibility)
1.用 volatile 保障可见性
2.某些语句会触发可见性机制
二、有序性(ordering)
1.语句乱序执行的现象
2.this 对象逸出的现象
3.阻止乱序的方式:内存屏障
三、原子性(atomicity)
1.什么是原子性操作?
2.如何保证操作的原子性?
(1)悲观锁
(2)乐观锁
(3)总结
本文为个人学习笔记,一切内容仅供参考。
一、可见性(visibility)
1.用 volatile 保障可见性
可见性:
被 volatile 修饰的内存,对所有线程可见。
线程每一次使用被 volatile 修饰的变量时,都需要去内存中重新读取一遍该变量的值。
代码:
public class Main {
private static volatile boolean running = true;
private static void m() {
System.out.println("m start!");
while (running) {}
System.out.println("m end!");
}
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
Main.m();
}, "t1").start();
TimeUnit.SECONDS.sleep(1);
running = false;
}
}
在上例中,如果 running 不用 volatile 修饰,则在 main 线程中修改其值后,并不会影响 t1 线程中 copy 到的 running 的值,线程会一直循环运行。
volatile 修饰引用类型(包括数组)只能保证引用本身的可见性,不能保证内部字段(成员变量)的可见性。
2.某些语句会触发可见性机制
某些语句内部带有 synchronized,可以触发可见性,如:println()。
这些语句会触发内存缓存同步刷新:可以将线程本地的缓存和主内存之间的数据进行刷新与同步。
二、有序性(ordering)
1.语句乱序执行的现象
举例:
提高 CPU 的执行效率:CPU 的处理速度远大于寄存器,当指令 1 去读取数据时,需要等待很长的时间,这个时候 CPU 就可能会先去执行指令 2 做计算。
前提:前后两条语句没有依赖关系,如:x = 1; x++; 就不会发生乱序。
as-if-serial:不论在计算机底层实际是谁先执行,都不会影响单线程的最终一致性。
如果在多线程环境下发生乱序,则可能会造成一些不好的影响。
代码:
public class Main {
private static boolean ready = false;
private static int number;
private static class MyThread extends Thread {
@Override
public void run() {
while (!ready) {
Thread.yield();
}
System.out.println(number);
}
}
public static void main(String[] args) {
Thread t = new MyThread();
t.start();
// 下面这两条语句没有依赖关系,可能会发生乱序执行
number = 42;
ready = true;
}
}
分析:
在类初始化时,会先给静态变量赋初值,即 number = 0。
若发生了语句的乱序执行,即若 ready = true 先执行,则此时的 number 还没被初始化就会被打印。
最后打印的 number 可能是 42 或 0。
2.this 对象逸出的现象
代码:
public class Main {
private int num = 8;
public Main() {
new Thread(() -> {
System.out.println(num);
}).start();
}
public static void main(String[] args) {
new Main();
}
}
分析:
在对象初始化的过程中,可能会先将对象(this)与内存空间建立关联,再调用构造方法对成员变量进行初始化。
在构造方法还未执行完时,可能会先启动线程,然后打印输出还未初始化的变量值 num = 0。
对象的创建过程详见:。
解决方法:不要在构造方法中启动线程(但是可以创建线程),可以单独写一个方法用于启动线程。
3.阻止乱序的方式:内存屏障
内存屏障的含义:是一种特殊的指令,其前面的指令必须执行完,后面的才能执行。
JVM 内存屏障:
volatile 底层使用内存屏障实现:
volatile 读 = 读数据 + LoadLoadBarrier + LoadStoreBarrier
volatile 写 = LoadStoreBarrier + 写数据 + StoreLoadBarrier
volatile:
使用 volatile 可以保证可见性。
使用 volatile 可以保证有序性。
三、原子性(atomicity)
1.什么是原子性操作?
原子性操作:不能被其它线程打断,不能与其它线程一起并发执行的操作。
代码:
public class Main {
private static long n = 0L;
public static void main(String[] args) throws InterruptedException {
Thread[] t = new Thread[100];
CountDownLatch latch = new CountDownLatch(t.length);
for (int i = 0; i < t.length; i++) {
t[i] = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
n++;
}
latch.countDown();
});
}
for (int k = 0; k < t.length; k++) {
t[k].start();
}
// 阻塞,等待所有线程执行完毕后再打印n的值
latch.await();
System.out.println(n);
}
}
理论结果为:1 000 000。
实际结果为:122 507。
原因:多个线程访问同一个共享数据时会产生竞争,可能会造成数据的不一致(并发访问之下产生的不期望出现的结果)。
如何解决这个问题?
用 synchronized 上锁保证原子性:
可以将 n++ 这条语句用 synchronized(Main.class) 上锁来实现线程同步(保障数据一致性)。
上锁的本质就是把并发编程序列化。
用 synchronized 也可以保证可见性,上例中每次 n++ 完以后,一定会与主内存做同步。
用 synchronized 不能保证有序性,其内部的语句仍然可能出现乱序重排的现象。
2.如何保证操作的原子性?
(1)悲观锁
含义:悲观地认为这个操作会被别的线程打断。
(2)乐观锁
含义:乐观地认为这个操作不会被别的线程打断,又叫做无锁、自旋锁(使用 CAS 实现)。
CAS(compare and swap / exchange / set)
CAS 原理:
当这里的值 E、V 为普通的数据类型时,不用考虑 ABA 问题;但如果是引用类型,则需要加 version 来解决 ABA 问题。
version 可以由带时间戳、数字或布尔类型变量等方式来实现。
CAS 自身必须保证原子性。
代码:
public class Main {
// 设置初始值为0
AtomicInteger count = new AtomicInteger(0);
static CountDownLatch latch = new CountDownLatch(100);
void run() {
for (int i = 0; i < 10000; i++) {
// count++
count.incrementAndGet();
}
latch.countDown();
System.out.println(Thread.currentThread().getName() + ":" + count);
}
public static void main(String[] args) throws InterruptedException {
Main m = new Main();
List<Thread> t = new ArrayList<>();
for (int i = 0; i < 100; i++) {
t.add(new Thread(m::run, "thread-" + i));
}
// 启动所有线程
t.forEach((o) -> o.start());
latch.await();
System.out.println(m.count); // 1000000
}
}
分析:
AtomicInteger 可以保证原子性,用于对 int 类型的数据进行原子性的访问,此时的访问是加了锁的。
AtomicXxx:通过 CAS 实现,一般会比直接用 synchronized 上锁效率更高。
incrementAndGet():实现线程安全的自增(原子性操作)。
其他保证原子性的类:AtomicLong、LongAdder(效率:LongAdder > AtomicLong > Sync)。
CAS 底层实现:
由 CPU 原语(指令级别)支持。
底层还是通过上锁实现的(汇编指令:lock cmpxchg),不允许其他 CPU(线程)打断当前对某块内存的 CAS 操作(单核 CPU 没必要上锁)。
(3)总结
基本概念补充:
critical section(临界区,即被锁住的代码):
若临界区的执行时间长,语句多,就叫锁的粒度比较粗。
若临界区的执行时间短,语句少,就叫锁的粒度比较细。可以使线程争用时间变短,从而提高效率。
悲观锁与乐观锁的效率对比:
悲观锁:
有一个等待队列,等待中的线程不消耗 CPU 的资源。
适用场景:临界区执行时间长,且等待线程很多。
乐观锁:
所有线程会一直循环地去访问数据,这些线程都是活着的,需要线程间的调度,会消耗 CPU 的资源。
适用场景:临界区执行时间短,且等待线程数少。
原文地址: https://blog.csdn.net/sss18982488580/article/details/123499423
本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
相关文章