一、什么是多线程并发编程
- 并发:同一时间段内多个任务同时执行
- 并行:单位时间内多个任务同时执行
单CPU单位时间内只能执行一个任务,多CPU并行不会有切换上下文开销
二、Java中的线程安全问题
- 线程安全问题:当多个线程同时读写一个共享资源并且没有任何同步措施时,导致出现脏数据或者其他不可预见的结果
- 内存可见性问题:Java内存模型规定,将所有的变量都存放在主内存中,当线程使用变量时,会把主内存里的变量复制到自己的工作空间或者叫做工作内存,线程读写变量时操作的是自己工作内存中的变量
三、关键字
- synchronized(笨重):一种原子性内置锁(排他锁),所有对象都可以作为同步锁(称为内部锁或监视器锁)。线程的执行代码在进入synchronized代码块之前会获取内部锁,此时其他线程访问该同步代码块时会被阻塞挂起。释放锁资源:1、正常退出,2、抛出异常,3、调用内部锁的wait方法
- volatile(弱形式的同步):被修饰的变量读写都直接操作主内存。非阻塞
TIP:
- synchronized的使用会导致上下文的切换(笨重)
- synchronized内存语义:1、进入:synchronized块内使用到的变量从线程的工作内存中清除(所以会从主内存中取);2、退出:对共享变量的修改刷新主内存
- synchronized可以解决共享变量内存可见性问题以及实现原子性操作
- volatile可以解决内存可见性问题,但不保证原子性
四、Java中的原子性操作
1.synchronized:多线程读同一个变量也会阻塞
2.Atomic系列类、Unsafe类:非阻塞CAS(Compare and Swap)算法实现的原子性操作类,
- CAS操作经典ABA问题:看不懂T_T
五、Unsafe类重要方法
- long objFieldOffset(Field field):返回指定的变量所属类中的内存偏移地址
- int arrayBassOffset(Class clzz):获取数组中第一个元素的地址
- int arrayIndexScale(Class clzz):获取数组中第一个元素占用的字节
- boolean compareAndSwapLong(Object obj,long offset,long expect,long update):比较obj中偏移量为offset的变量值是否与expect相等,相等则使用update更新并返回true,否则返回false;
- public native long getLongvolatile(Object obj, long offset):获取obj中偏移量为offset的变量对应volatile语义的值
- void putLongvolatile(Object obj, long offset,long value):赋值
- void putOrderedLong(Object obj, long offset,long value):有延迟的putLongvolatile方法,不保证值的修改对其他线程立刻可见。只有变量使用volatile修饰并且预计会被意外修改时才使用该方法
- void park(boolean isAbsolute, long time):阻塞当前线程,time后唤醒,isAbsolute表示time是否为绝对值。其他唤醒方式:1、被interrupt;2、其他线程调用upPark并且将当前线程作为参数
- void unPark(Object thread):唤醒调用park后阻塞的线程
- long getAndSetLong(Object obj,long offset,long update):JDK8新增,获取比那辆volatile语义的值,并设置变脸volatile语义的值为update
- long getAndAddLong(Object obj,long offset,long addValue):JDK8新增,获取并+addValue
六、使用Unsafe类(jdk1.8)
import sun.misc.Unsafe;
public class Main {
static final Unsafe unsafe = Unsafe.getUnsafe();
static final long offset;
private volatile long state = 0;
static {
try {
offset = unsafe.objectFieldOffset(Main.class.getDeclaredField("state"));
} catch (NoSuchFieldException e) {
e.printStackTrace();
throw new Error(e);
}
}
public static void main(String[] args) throws Exception {
Main test = new Main();
boolean f = unsafe.compareAndSwapLong(test, offset, 0, 1);
System.out.println(f);
}
}
java.lang.ExceptionInInitializerError
Caused by: java.lang.SecurityException: Unsafe
at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
at Main.<clinit>(Main.java:4)
Exception in thread "main"
原因:Unsafe.getUnsafe方法会判断是不是Bootstrap加载器加载了Main.class。由于Main.class是用AppClassLoader加载的,因此抛出了异常
Unsafe类可以直接操作内存,这是不安全的,所以JDK开发组特意做了这个限制。可以通过反射获取Unsafe实例:
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class Main {
static final Unsafe unsafe;
static final long offset;
private volatile long state = 0;
static {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
offset = unsafe.objectFieldOffset(Main.class.getDeclaredField("state"));
} catch (Exception e) {
e.printStackTrace();
throw new Error(e);
}
}
public static void main(String[] args) throws Exception {
Main test = new Main();
boolean f = unsafe.compareAndSwapLong(test, offset, 0, 1);
System.out.println(f);
}
}
true
七、Java指令重排序
- 指令重排序:Java内存模型允许编译器和处理器对指令重排序以提高运行性能,并且只会对不存在数据依赖的指令重排序。单线程下重排序可以保证最终执行的结果与程序顺序执行的结果一直,但多线程下就会存在问题。
- volatile:可以避免指令重排序。1、写volatile变量时,可以确保volatile写之前的操作不会被编译器重排序到volatile写之后;2、读volatile变量时,可以确保volatile读之后的操作不会被编译器重排序到volatile读之前。
八、伪共享
- 为了解决计算机系统中主内存与CPU之间运行速度差问题,会在CPU与主内存之间添加一级或多级告诉缓冲存储器(Cache),Cache内部是按行存储的,每一行称为一个Cache行。Cache行是Cache与主内存进行数据交换的单位。
- CPU访问变量时,先从CPU Cache中看是否存在,有就用,没有就去主存将该变量所在主存区域的一个Cache行大小的内存复制到Cache中(可能把多个变量存放到一个Cache中)。
- 当多个线程同时修改一个缓存行里面的多个变量时,由于同时只能有一个线程操作缓存行,所以相比将每个变量存放到一个缓存行,性能会有所下降,这就是伪共享。
事实上,单线程下多个变量被放入同一个缓存行对性能是有利的:
public class Main {
static final int COLUM_NUM = 2048;
static final int LINE_NUM = 2048;
public static void main(String[] args) throws Exception {
long[][] arrays = new long[COLUM_NUM][LINE_NUM];
long start = System.currentTimeMillis();
for (int i = 0; i < COLUM_NUM; i++) {
for (int j = 0; j < LINE_NUM; j++) {
// 顺序访问数组,平均耗时5ms
arrays[i][j] = 1;
// arrays[j][i] = 1; 跳跃访问数组,平均耗时30ms
}
}
long end = System.currentTimeMillis();
System.out.println(end - start);
}
}
很明显的,顺序访问数组的性能是比跳跃访问快的;因为缓存有容量限制的,跳跃访问数组的情况下,缓存行中的数据还没被读取就被替换了,再次读取又得从主存中获取,造成重复访问主存
九、避免伪共享
- 如下,JDK8之前,程序员一般都是通过字节填充的方式来避免该问题。假如缓存行为64字节,那么我们再Main类里面填充了6个long类型的变量。每个long占8字节,加上类对象的字节码的对象头占8字节,总共64字节,正好可以放入一个缓存行:
public class Main {
public volatile long value = 0L;
public long p1, p2, p3, p4, p5, p6;
}
- JDK8提供了一个sun.misc.Contended注解,用来解决伪共享问题,将上面代码修改为如下:
@sun.misc.Contended
public class Main {
public volatile long value = 0L;
}
@Contended注解可以用来修饰类和变量。默认情况下,只用于Java核心类,如rt包下的类。若用户类路劲下的类需要使用该注解,则需要添加JVM参数:-XX:-RestrictContended。填充的宽度默认为128,要自定义宽度则可以设置-XX:ContendedPaddingWidth
例子:不打开注释(1)(2)的情况下会有伪共享,性能明显比单独打开(1)或(2)后差
import sun.misc.Contended;
public class FalseSharing {
static VolatileLong[] longs = new VolatileLong[8];
static {
for (int i = 0; i < longs.length; i++) {
longs[i] = new VolatileLong();
}
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[8];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(new FalseSharing(i));
}
long start = System.currentTimeMillis();
for (int i = 0; i < threads.length; i++) {
threads[i].start();
}
for (int i = 0; i < threads.length; i++) {
threads[i].join();
}
long end = System.currentTimeMillis();
System.out.println(end - start);
}
static class FalseSharing implements Runnable {
int index;
public FalseSharing(int i) {
this.index = i;
}
@Override
public void run() {
for (int i = 0; i < 1 << 27; i++) {
longs[index].value = i;
}
}
}
// (1)使用注解,运行时需要添加JVM参数-XX:-RestrictContended
// @Contended
public static final class VolatileLong {
public volatile long value;
// (2)使用字节填充
// public long p1, p2, p3, p4, p5, p6;
}
}
十、锁的概述
- 乐观锁与悲观锁:数据库中引入的名词。悲观锁指对数据被外界修改持保守添堵,认为数据很容易就会被其他线程修改,所以在数据被处理前对数据进行加锁,并在整个数据处理过程中,使数据处于锁定状态(往往依赖数据库提供的锁机制实现);乐观锁是相对悲观锁来说的,它认为数据再一般情况下不会造成冲突,所以在访问记录前不会加排他锁,而是在进行数据提交更新时,才会正式对数据冲突进行检测。
- 公平锁与非公平锁:根据线程获取锁的抢占机制划分。公平锁:先申请先得;非公平锁:运行时闯入,先申请不一定先得。ReentranLock提供了公平和非公平锁的实现。
- 独占锁与共享锁:根据线程是否能被多个线程共同持有划分。ReentranLock就是独占锁;ReadWriteLock是共享锁
- 可重入锁:线程再次获取它自己已经持有的锁时不会阻塞。
- 自旋锁:线程在获取锁时发现锁已经被其它线程占有,不马上阻塞自己,在不放弃CPU使用权的情况下,多次尝试获取(默认10次,可以使用-XX:PreBlockSpinsh参数设置改值),很有可能在后面几次尝试中其它线程已经释放了锁。尝试次数后仍没有获取到才会被阻塞挂起。自旋锁是使用CPU时间换取线程阻塞与调度的开销(如果在尝试次数内获取到了锁可以避免上下文切换的开销),但是很有可能这些CPU时间白白浪费了
JAVA的锁实现:
- 公平锁:new ReentranLock(true)
- 非公平锁:new ReentranLock(false)
- 独占锁:ReentranLock
- 共享锁:ReadWriteLock
- 可重入锁:synchronized内部锁。锁内部维护一个线程标示,用来标示该锁目前被那个线程占用,然后关联一个计数器。重入时计数器会+1,释放锁时-1;为0时线程标示被重置为null。