并发编程三大特性
1、可见性:各线程之间对共享变量的可见性,即一个线程更改了共享变量的值,其他线程也能看到并更新到自己线程中的值。共享资源一般都放在堆空间(主内存),每个线程使用公共资源都会将公共资源拷贝一份到自己的线程中(本地缓存),当一个线程对共享资源进行更改并写回到堆空间,而其他线程不知道共享资源已经被修改了。
Volatile:使用Volatile修饰共享变量(非引用类型),当一个线程对共享变量进行更改,会让其他线程都会读到变量的更改值。使用volatile,将会强制所有线程都去堆内存中读取running的值。但volatile并不能保证多个线程共同修改共享变量时所带来的不一致问题,也就是说volatile不能替代synchronized,volatile的底层本质是使用CPU原语中的读屏障和写屏障,保障读或者写的时候只能一个线程处理,处理后同步内存中的共享变量。
synchronized:可保证可见性,可以触发本地缓存和主内存之间的数据同步。
2、有序性:为了提高执行效率,程序在编译或者运行的时候会对代码进行指令重排。在单线程中保证最终一致性,指令重排并不会产生问题,但是在多线程的情况下便可能产生不希望看到的结果。使用volatile修饰的内存,不可以重排序,对volatile修饰变量的读写访问,都不可以换顺序。
指令重排原则:保证单线程执行结果最终一致性(as-if-serial:看上去像是序列化)。指令重排的指令没有前后的依赖关系
JVM中的读写内存保障程序执行的有序性,其汇编指令中的LOCK 用于在多处理器中执行指令时对共享内存的独占使用。 它的作用是能够将当前处理器对应缓存的内容刷新到内存,并使其他处理器对应的缓存失效。 另外还提供了有序的指令无法越过这个内存屏障的作用。
this对象逸出:注意:尽量不要在构造方法中创建线程的同时启动线程,以避免指令重排导致的半初始化状态的对象,可在构造方法中创建对象,将启动对象独立封装为方法。
public class Test {
private int num = 8;
/* public Test() {
new Thread(() -> System.out.println(this.num)).start();
}*/
Thread t;
public Test() {
t = new Thread(() -> System.out.println(this.num));
}
public void start() {
t.start();
}
public static void main(String[] args) throws Exception {
// new Test();
new Test().start();
System.in.read();
}
}
3、原子性:一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。原子性就像数据库里面的事务一样,他们是一个团队,同生共死。加锁(上锁本质,将并发编程序列化)使用synchronized或者Lock悲观锁,、AtomicXXX原子操作(CAS乐观锁)。
缓存行对齐:缓存行64个字节是CPU同步的基本单位,缓存行隔离会比伪共享效率要高
何为缓存行?
程序运行是靠CPU执行内存中代码,但是CPU和内存的速度差异是非常大的,为了降低这种差距,CPU按块(缓存行)读取并使用了CPU缓存,现在的计算机中普遍使用了缓存,分为一级缓存,二级缓存,还有一些具备三级缓存。而每个缓存里面都是由缓存行组成的,缓存系统中是以缓存行(cache line)为单位存储的。缓存行的大小都是2的整数幂个连续字节,一般为32-256个字节。最常见的缓存行大小是64个字节(缓存行越大,局部性空间效率越高,但是读取慢;缓存行越小,局部空间效率越低,但是读取快,64字节取了折中)。当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。
缓存行对齐的编程技巧:
public class CacheLinePadding {
public static long COUNT = 10_0000_0000L;
private static class T {
private long p1, p2, p3, p4, p5, p6, p7;
public long x = 0L;
private long p9, p10, p11, p12, p13, p14, p15;
}
public static T[] arr = new T[2];
static {
arr[0] = new T();
arr[1] = new T();
}
public static void main(String[] args) throws Exception {
CountDownLatch latch = new CountDownLatch(2);
Thread t1 = new Thread(()->{
for (long i = 0; i < COUNT; i++) {
arr[0].x = i;
}
latch.countDown();
});
Thread t2 = new Thread(()->{
for (long i = 0; i < COUNT; i++) {
arr[1].x = i;
}
latch.countDown();
});
final long start = System.nanoTime();
t1.start();
t2.start();
latch.await();
System.out.println((System.nanoTime() - start)/100_0000);
}
}
JDK8引入了@sun.misc.Contended注解,来保证缓存行隔离效果 要使用此注解,必须去掉限制参数:-XX:-RestrictContended
//注意:运行这个小程序的时候,需要加参数:-XX:-RestrictContended
public class T05_Contended {
public static long COUNT = 10_0000_0000L;
private static class T {
@Contended //只有1.8起作用 , 保证x位于单独一行中
public long x = 0L;
}
public static T[] arr = new T[2];
static {
arr[0] = new T();
arr[1] = new T();
}
public static void main(String[] args) throws Exception {
CountDownLatch latch = new CountDownLatch(2);
Thread t1 = new Thread(()->{
for (long i = 0; i < COUNT; i++) {
arr[0].x = i;
}
latch.countDown();
});
Thread t2 = new Thread(()->{
for (long i = 0; i < COUNT; i++) {
arr[1].x = i;
}
latch.countDown();
});
final long start = System.nanoTime();
t1.start();
t2.start();
latch.await();
System.out.println((System.nanoTime() - start)/100_0000);
}
}
硬件层面的缓存一致性协议MESI——>Modified、Exclusive、Shared、Invalid,主动性监听更新数据。
Atomic类与CAS解读
由于某一些特别常见的操作,老是来回的加锁,加锁的情况特别多,所以干脆java就提供了这些常见的操作这么一些个类,这些类的内部就自动带了锁,当然这些锁的实现并不是synchronized重量级锁,而是CAS乐观锁的操作来实现的。
CAS的incrementAndGet() 调用了getAndAddInt().再调用了weakCompareAndSetInt(…),最终会调用到Usafe类中的本地方法compareAndSwapInt(),在C++写的HotSpot源码中使用了汇编指令就是cmpxchg命令(cmpxchg汇编指令也不是原子的,也就是说如果多个线程也会出问题),所以如果是多核cpu先lock锁再执行cmpxchg命令(比较并交换),而这里的lock是硬件层面的缓存锁或者是总线锁。