1.并行与并发区别
并发指同一时间段多个任务同时都在进行,并且都没有执行结束,而并行是说在单位时间内多个任务在同时运行。
并发任务强调在一个时间段内同时进行,而一个时间段有多个单位i时间构成,所以说并发的多个任务在单位时间内不一定同时在执行。
一个CPU同时只能执行一个任务,所以单CPU时代多个任务都是并发执行的。
注:在多线程时间中,线程的个数往往多于CPU个数,所以即使存在并行任务,一般还是称为多线程并发编程而非多线程并行编程。
2.Java中的线程安全问题
线程安全问题是指当多个线程同时读写一个共享资源并且没有任何同步措施时,导致出现脏数据或者其他不可预见的结果的问题
3.Java中共享变量的内存可见性问题
Java内存模型规定将所有的变量都存放在主内存中,当线程使用变量时,会把主内存里面的变量复制到自己的工作空间或者叫作工作内存,线程读写变量时操作的是自己工作内存中的变量。
当一线程操作共享变量时,它首先从主内存复制共享变量到自己的工作内存,然后工作内里的变量进行处,处理完后将变量值更新到主内存。那么假如线程A和线程B同时处理一个共享变量,会出现什么情况?我们使用上图所示CPU架构,假设线程A和线程B使用不同CPU执行,并且当前两级Cache都为空,那么这时候由于Cache的存在,将会导致内存不可见问题,具体看下面的分析。·线程A首先取共享变量X的值,由于两级Cache都没有命中,所以加载主内存中X的值,假如为0。然后把X=O的值缓存到两级缓存,线程A修改X的值为1,然后将其写入两级Cache,并且刷新到主内存。线程A操作完毕后,线程A所在的CPU的两级Cache内和主内存里面的X的值都是l。·线程B获X的值,首先一级缓存没有命中,然后看二级缓存,二级缓存命中了,所以返X=1;到这里一切都是正常的,因为这时候主内存中也X=l。然后线程B改X的值为2,并将其存放到线程2所在的一级Cache和共享二级Cache中,最后更新主内存中X的值为:到这里一切都是好的。·线程A这次又需要修改X的值,获取时一级缓存命中,并且X=l,到这里问题就出现了,明明线程已经把X的值修改为了2,为何线程A获取的还是l呢?这就是共享变量的内存不可见问题,也就是线程B写入的值对线程A不可见。
4.synchronized关键字介绍
synchronized块Java提供的一种原子性内置锁,Java中的每个对象都可以把它当作一个同步锁来使用,这些Java内置的使用者看不到的锁被称为内部锁,也叫作监视器锁。线程的执行代码在进入synchronized代码块前会自动获取内部锁,这时候其他线程访问该同步代码块时会被阻塞挂起。拿到内部锁的线程会在正常退出同步代码块或者抛出异后或者在同步块内调用了该内置锁资源的wait系列方法时释放该内置锁。内置锁是排它锁,也就是当一个线程获取这个锁后,其他线程必须等待该线程释放锁后才能获取该锁。
另外,由于Java中的线程是与操作系统的原生线程一一对应的,所以当阻塞一个线程时伞,需要从用户态切换到内核态执行阻塞操作,这是很耗时的操作,而synchronized的使用就会导致上下文切换。
5.synchronized的内存语义
这个内存语义就可以解决共享变量内存可见性问题。进入synchronized块的内存语义是把在synchronized块内使用到的变量从线程的工作内存中清除,这样在synchronized块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取。退出synchronized块的内存语义是把在synchronized块内对共享变量的修改刷新到主内存。
其实这也是加锁和释放锁的语义,当获取锁后会清空锁块内本地内存中将会被用到的共享变量,在使用这些共享变量时从主内存进行加载,在释放锁时将本地内存中修改的共享变量刷新到主内存。除可以解决共享变量内存可见性问题外,synchronized经常被用来实现原子性操作。另外请注意,synchronized关键字会引起线程上下文切换并带来线程调度开销。
6.volatile关键字
用volatile关键字。该关键字可以确保对一个变量的更新对其他线程马上可见当一个变量被声明为volatile时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。当其他线程读取该共享变量时,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。volatile的内存语义和synchronized有相似之处,具体来说就是,当线程写入了volatile变量值时就等价于线程退出synchronized同步块(把写入工作内存的变量值同步到主内存),读取volatie变量值时就相当于进入同步块(先清空本地内存变量值,再从主内存获取最新值)。
volatile虽然提供了可见性保证,但并不保证操作的原子性。那么一般在什么时候才使用volatile关键字呢?
(1)写入变量值不依赖变量的当前值时。因为如果依赖当前值,将是获取一计算一写入三步操作,这三步操作不是原子性的,而volatile不保证原子性。
(2)读写变量值时没有加锁。因为加锁本身已经保证了内存可见性,这时候不需要把变量声明为volatile的。
7.原子性操作
所谓原子性操作,是指执行一系列操作时,这些操作要么全部执行,要么全部不执行,不存在只执行其中一部分的情况。在设计计数器时一般都先读取当前值,然后+l,再更新。这个过程是改写的过程,如果不能保证这个过程是原子性的,那么就会出现线程安全问题。
8.CAS操作
CAS即CompareandSwap,其是JDK提供的非阻塞原子性操作,它通过硬件保证了比较更新操作的原子。JDK里面的Unsafe类提供了一系列的compareAndSwap*方法,下面以compareAndSwapLong方法为例进行简单介绍。
booleancompareAndSwapLong(Objectobj,longvalueOffset,longexpect,longupdate)方法:其中compareAndSwap的意思是比较并交换。
CAS有四个操作数,分别为:对象内存位置、对象中的变量的偏移量、变量预期值和新的值。
其操作含义是,如果对象obj中内存偏移量为valueOffset的变量值为expect,则使用新的值update替换旧的值expect。这是处理器提供的一个原子性指令。
关于CAS操作有个经典的ABA问题,具体如下:
假线程I使用CAS修改初始值为A的变量X,那么线程I会首先去获取当前变量X的值(为A),然后使用CAS操作尝试修改X的值为B,如果使用CAS操作成功了,那么程序运行一定是正确的吗?其实未必,这是因为有可能在线程I获取变量X的值后,在执行CAS前,线程II使用CAS修改了变量X的值为B,然后又使用CAS修改了变量X的值为A。所以虽然线程I执行CAS时X的值是A,但是这A己经不是线程I获取时的A了。这就是ABA问。ABA题的产生是因为变量的状态值产生了环形转换,就是变量的值可以从A到B,然后再从B到A。如果变量的值只能朝着一个方向转换,比如A到,B到C不构成环形,就不会存在问题。
JDK中的AtomicStampedReference类给每个变量的状态值都配备了一个时间戳,从而避免了ABA问题的产生。
9.Unsafe类
9.1Unsafe类中的重要方法
JDK的rt.jar 包中的UnSafe类提供了硬件级别的原子性操作,Unsafe类中的方法都是native方法,使用JNI的方式访问本地C++实现库。
我们来了解一下Unsafe类提供的几个主要的方法以及编程时如何使用Usafe类做一些事情:
long objectFieldOffset(Field field)方法;返回指定变量所属类中的内存偏移地址,该偏移地址仅仅在使用该Unsafe函数中访问指定字段时使用。如下代码使用Unsafe获取变量value在AtomicLong对象中的内存偏移。
static{
try{
valueOffset=unsafe.objectFieldOffset(AtomicLong.class.getDeclaredField("value"));
}catch(Exceptione){
thrownewError(r);
}
}
int arrayBaseOffset(Class arrayClass)方法:获取数组中第一个元素的地址
int arrayIndexScale(Class arrayClass)方法:获取数组中一个元素的占用的字节
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)方法:设置obj对象中offset偏移地址对用的long类field值为value,支持volatile语义。
void putOrderedLong(Object obj,long offset,long value)方法:设置obj对象中offset偏移地址对应的long类field值为value,这是一个有延迟的putLongvolatile方法,并且不保证值修改对其他线程立刻可见。只有变量使用volatile修饰并且预计会被意外修改时才使用该方法。
void park(boolean isAbsoulte,long time)方法:阻塞当前程,其中参数isAbsolute等于false且time等于0表示一直阻塞。time大于0表示等待指定的time后阻塞线程会被唤醒,这个time是个相对值,是个增量值,也就是相对当前时间累加time后当前线程就会被唤醒。如果isAbsolute等于true,并且time大于0,则表示阻塞的线程到指定的间点后会被唤醒,这里time是个绝对间,是将某个时间点换算为ms后的值。另外,当其他线程调用了当前阻塞线程的interrupt方法而中断了当前线程时,当前线程也会返回,而当其他线程调用了unPark方法并且把当前线程作为参数时当前线程也会返回
void unpark(Object thread)方法:唤醒调用park后阻塞的线程
long getAndSetLong(Object obj,long offset,long update)方法:获取对象obj中偏移量为offset的变量volatile予以的当前值,并设置变量volatile语义的值为update;
public final long getAndSetLong(Object obj, long offset, long update) {
long l;
do {
l = this.getLongVolatile(obj, offset); //1
} while (!this.compareAndSwapLong(obj, offset, l, update));
return l;
}
由以上代码可知,首先1处的getLongvolatile获取当前变量的值,然后使用CAS原子操作设置新值。这里使用while循环是考虑到,在多个线程同时调用的情况CAS失败时需要重试。
longgetAndAddLong(Object obj,long offset,long addValue)方法:取对象。同中偏移量为offset的变量volatilei吾义的当前值,并设置变量值为原始值+addValue
public final long getAndAddLong(Object obj, long offset, long addValue) {
long l;
do {
l = this.getLongVolatile(obj, offset);
} while (!this.compareAndSwapLong(obj, offset, l, l + addValue));
return l;
}
类似getAndSetLong实现,只是这里进行CAS操作时使用了原始值+传递的增量参数addValue的值。
9.2 如何使用Unsafe类
请看示例:
import sun.misc.Unsafe;
public class TestUnSafe {
//获取 Unsafe 的实例 1
static final Unsafe unsafe= Unsafe.getUnsafe();
//记录变量state在类TestUnsafe中偏移值2
static final long stateOffset;
//变量3
private volatile long state;
static {
//获取 state 变量在类 TestUnSafe 中的偏移值4
try {
stateOffset = unsafe.objectFieldOffset(TestUnSafe.class.getDeclaredField("state"));
} catch (NoSuchFieldException | SecurityException e) {
System.out.println(e.getLocalizedMessage());
throw new Error(e);
}
}
public static void main(String[] args) {
//创建实例5
TestUnSafe test = new TestUnSafe();
Boolean success = unsafe.compareAndSwapInt(test,stateOffset,0,1);
System.out.println(success);
}
}
如上代码中,代码1获取了Unsafe的一个实例,代码3创建了一个变量state并初始化为0。代码4使用unsafe.objectFieldOffset获取TestUnSafe类里面的state变量,在TestUnSafe对象里面的内存偏移量地址并将其保存到stateOffset变量中。代码6调用创建的unsafe实例的compareAndSwapint方法,设置test对象的state变量的值。具体意思是,如果test对象中内存偏移量为stateOffset的state变量值为0,则更新该值为10运行上面的代码,我们期望输出true,然而执行后会输出如下结果:
@CallerSensitive
public static Unsafe getUnsafe() {
7
Class loadclass = Reflection.getCallerClass();
8
if (!VM.isSystemDomainLoader(loadclass.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
判断paramClassLoader是否是Bootstrap类加载器
public static boolean isSystemDomainLoader(ClassLoader paramClassLoader) {
return paramClassLoader == null;
}
代码7获取调用getUnsafe这个方法的对象的Class对象,这里是TestUnSafe.class。
代码8判断是不是Bootstrap类加载器加载的localClass,在这里是看是不是Bootstrap加载器加载了TestUnSafe.class很明显由于TestUnSafe.class是使用AppClassLoader加载的,所以这里直接抛出了异常。思考一下,这里为何要有这个判断?我们知道Unsafe类是rt.jar包提供的,rt.jar包里面的类是使用Bootstrap类加载器加载的,而我们的启动main函数所在类是使用AppC!assLoader加载的,所以在main函数里面加载Unsafe类时,根据委托机制,会委托给Bootstrap去加载Unsafe类。
如果没有代码8的限制,那么我们的应用程序就可以随意使用Unsafe做事情了,而Unsafe类可以直接操作内存,这是不安全的,所以JDK开发组特意做了这个限制,让开发人员在正规渠道使用Unsafe类,而是在rt.jar包里面的核心类中使用nsafe功能。
如果开发人员真的想要实例化Unsafe类,那该如何做?方法有多种,既然从正规渠访问不了,那么就玩点黑技,使用万能的反射来获取Unsafe实例方法。
import sun.misc.Unsafe;
public class TestUnSafe2 {
static final Unsafe unsafe;
static final long stateOffset;
private volatile long state=0;
static {
try {
//使用反射获取UnSafe的成员变量theUnsafe
Field field = Unsafe.class.getDeclaredField("theUnsafe");
//设置为可存取
field.setAccessible(true);
//获取该变量的值
unsafe = (Unsafe) field.get(null);
//获取state在TestUnSafe2中的偏移量
stateOffset = unsafe.objectFieldOffset(TestUnSafe2.class.getDeclaredField("state"));
} catch (Exception ex) {
System.out.println(ex.getLocalizedMessage()) ;
throw new Error(ex);
}
}
public static void main(String[] args) {
TestUnSafe2 test=new TestUnSafe2();
boolean success = unsafe.compareAndSwapInt(test, stateOffset, 0, 1);
System.out.println(success);
}
2.10 指令重排序
Java内存模型允许编译器和处理器对指令重排以提高行性能,并且只会对不存在数据依赖性的指令重排序。在单线程下重排序可以保证最终执行的结果与程序顺序执行的结果一致,是在多线程下就存在问题。
下面看个例子:
int a =1;(1)
int b=2;(2)
int c=a+b;(3)
在如上代码中,变量c的值依赖a和b的值,所以重排序能够保证(3)的操作在(2)(1)之后,但是(1)(2)谁先执行就不一定了,这在单线程下不会存在问题,因为并不影响最终结果。
下面看一个多线程的例子:
public class ThreadTest{
private static int num = 0;
private static boolean ready = false;
public static class ReadThread extends Thread {
public void run (){
while(!Thread.currentThread().isInterrupted()){
if(ready){//1
System.out.println(num+num);//2
}
System.out.println("read thread...");
}
}
}
public static class WriteThread extends Thread{
public void run(){
num = 2;//3
ready = true;//4
System.out.println("writeThread set over...");
}
}
public static void main(String[] args) throws InterruptedException {
ReadThread rt = new ReadThread();
rt.start();
WriteThread wt = new WriteThread();
wt.start();
Thread.sleep(10);
rt.interrupt();
System.out.println("main exit");
}
}
public class TestForContent2 {//2
static final int LINE_NUM = 1024;
static final int COLUM_NUM = 1024;
public static void main(String[] args) {
long[][] array = new long[LINE_NUM][COLUM_NUM];
long startTime = System.currentTimeMillis();
for (int i = 0; i < COLUM_NUM ; i++) {
for (int j = 0; j < LINE_NUM ; j++) {
array[j][i] = i*2+j;
}
}
long endTime = System.currentTimeMillis();
long cacheTime = endTime - startTime;
System.out.println("cache time :" + cacheTime + "ms");
}
}
首先这段代码里面的变量没有被声明为volatile的,也没有使用任何同步措施,所以在多线程下存在共享变量内存可见性问题。
这里先不谈内存可见性问题,因为通过把变量声为volatile的本身就可以避免指令重排序问题。这先看看指令重排序会造成什么影响,如上代码在不考虑、内存可见性问题的情况下一定会输出4答案是不一定,由于代码1234之不存在依赖关系,所以写线程的代码34可能被重排序为先执行4再执行3。那么执行4后,读线程可能已经执行了1操作,并且在3执行前开始执行2操作,这时候输出结果为0而不是4。
重排序在多线程下会导致非预期程序执行结果,而使用volatile修饰ready就可以免重排序和内存可见性问题。写volatile变量时,以确保volatile写之前的操作不会被编译器重排序到volatile写之后。读volatile变量时,可以确保volatile读之后的操作不会被编译器重排序到volatile读之前。
2.11 伪共享、
11.1
缓存系统中是以缓存行(cache line)为单位存储的,当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。
11.2为何会出现伪共享
共享的产生是因为多个变量被放入了一个缓存行中,并且多个线程同时去写入缓存行中不同的变量。那么为何多个变量会被放入一个缓存行呢?其实是因为缓存与内存交换数据的单位就是缓存行,当CPU要访问的变量没有在缓存中找到时,根据程序运行的局部性原理,会把该变量所在内存中大小为缓存行的内存放入缓存行。
longa;
lonqb;
longc;
longd;
如上代码声明了四个long变量,假设缓存行的大小为32节,那么当CPU问变量a时,发现该变量没有在缓存中,就会去主内存把变量a以及内存地址附近的b、c、d放入缓存行。也就是地址连续的多个变量才有可能会被放到一个缓存行中。当创建数组时,数组里面的多个元素就会被放入同一个缓存行。那么在单线程下多个变量被放入同一个缓存行对性能有影响吗?其实在正常情况下单线程访问时将数组元素放入一个或者多个缓存行对代码执行是有利的,因为数据都在缓存中,代码执行会更快,请对比下面代码的执行。
public class TestForContent { //1
static final int LINE_NUM = 1024;
static final int COLUM_NUM = 1024;
public static void main(String[] args) {
long[][] array = new long[LINE_NUM][COLUM_NUM];
long startTime = System.currentTimeMillis();
for (int i = 0; i < LINE_NUM ; i++) {
for (int j = 0; j < COLUM_NUM; j++) {
array[i][j] = i*2+j;
}
}
long endTime = System.currentTimeMillis();
long cacheTime = endTime - startTime;
System.out.println("cache time:" + cacheTime + "ms");
}
}
在笔者的mac电脑上执行代码1多次,耗时均在l0ms以下,执行代码2多次,耗时均在10ms以上。显然代码1比代码2执行得快,这是因为数组内数组元素的内存地址是连续的,访问数组第一个元素时,会把第一个元素后的若干元素一块放入缓存行,这样顺序访问数组元素时会在缓存中直接命中,因而就不会去主内存读取了,后续访问也是这样。也就是说,当顺序访问数组里面元素时,如果当前元素在缓存没有命中,那么会从主内存一下子读取后续若干个元素到缓存,也就是一次内存访问可以让后面多次访问直接在缓存中命中。而代码2是跳跃式访问数组元素的,不是顺序的,这破坏了程序访问的局部性原则,并且缓存是有容量控制的,当缓存满了时会根据一定淘汰算法替换缓存行,这会导致从内存置换过来的缓存行的元素还没等到被读取就被替换掉了。所以在单个线程下顺序修改一个缓存行中的多个变量,会充分利用程序运行的局部性原则,从而加速了程序的运行。而在多线程下并发修改一个缓存行中的多个变量时就会竞争缓存行,从而降低程序运行性能。
11.3 如何避免伪共享
JDK8之前一般都是通过字节填充的方式来避免该问题,也就是创建个变量时使用填充字段填充该变量所在的缓存行,这样就避免了将多个变量存放在同一个缓存行中,例如如下代码:
public final static class FilledLong{
public volatile long value=0L;
public long p1,p2,p3,p4,p5;
}
假如缓存行为64宇节,那么我们在FilledLong类里面填充了6个long类型的变量,每个long类型变量占用8字节,加上value变量的8字节总共56字节。另外这里FilledLong是一个类对象,而类对象的字节码的对象头占用8字节,所以一个FilledLong对象实际会占用64字节的内存,这正好可以放入一个缓存行。JDK8提供了一个sun.misc.Contended注解,用来解决伪共享问题。将上面代码修改为如下。
@sun.misc.Contended
public final static class FilledLong{
public volatile long value=0L;
public long p1,p2,p3,p4,p5;
}
在这里注解用来修饰类,当然也可以修饰变量,比如在Thread类中。
/** The current seed for a ThreadLocalRandom */
@sun.misc.Contended("tlr")
long threadLocalRandomSeed;
/** Probe hash value; nonzero if threadLocalRandomSeed initialized */
@sun.misc.Contended("tlr")
int threadLocalRandomProbe;
/** Secondary seed isolated from public ThreadLocalRandom sequence */
@sun.misc.Contended("tlr")
int threadLocalRandomSecondarySeed;
Thread类里面这三个变量默被初始化为,这三个变量会在ThreadLocalRandom类中使用,后面章节会专门讲解ThreadLocalRandom的实现原理。
需要注意的是,在默认情况下,@Contended注解只用于Java核心类,比如此包下的类。如果用户类路径下的类需要使用这个注解,则需要添加JVM参数:-XX:-RestrictContended。填充的宽度默认为128,要自定义宽度则可以设置-XX:ConnddPaddingWidth参数。