并发指同一时间段多个任务同时都在进行,并且都没有执行结束,而并行是说在单位时间内多个任务在同时运行。
并发任务强调在一个时间段内同时进行,而一个时间段有多个单位i时间构成,所以说并发的多个任务在单位时间内不一定同时在执行。
一个CPU同时只能执行一个任务,所以单CPU时代多个任务都是并发执行的。
注:在多线程时间中,线程的个数往往多于CPU个数,所以即使存在并行任务,一般还是称为多线程并发编程而非多线程并行编程。
示例:计数器问题
t1 | t2 | t3 | t4 | |
---|---|---|---|---|
线程A | 从主内存读取count值到本线程 | 递增本地线程count的值 | 写回主内存 | |
线程B | 从主内存读取count值到本线程 | 递增本地线程count的值 | 写回主内存 |
假设count初始值为0,线程A在t1和t2时间读取了主内存中的count并在本地将其递增为1,t2时线程B从主内存中读取了count的值0并于t3时将其递增为1,t3时线程A将count的新值1更新到主内存,t4时线程B进行了同样的操作,最终主内存中count的值为1而非我们想要的2。
如图是一个双核CPU模型,每个核都有自己的一级缓存,有些架构里还有一个所有CPU共享的二级缓存。
现假设线程A和线程B同时处理一个共享变量,由于Cache的存在,将会出现内存不可见问题,原因如下:
synchronized关键字是一种原子性内置锁,线程进入synchronized代码块前会自动获取监视器锁,这时其他线程再访问该同步代码块是会被阻塞挂起。
前面的共享变量内存可见性问题主要是线程的工作内存导致的,而synchronized的内存语义可以解决此问题。
synchronized的内存语义:
public class ThreadSafeInteger {
private int value;
public synchronized int get() {
return value;
}
public synchronized void set(int value) {
this.value = value;
}
}
注1:get()方法虽然只是读操作,但仍要加上synchronized来实现value的内存可见性。
注2:使用synchronized虽然解决了共享变量value的内存可见性问题,但由于synchronized是独占锁,同时只能有一个线程调用get()方法,其他调用线程则会被阻塞,同时存在线程切换、调度的开销,效率并不高。
当一个变量声明为volatile时,线程在写入变量时就不会把值缓存,而是直接把值刷新到主存中;当其他线程读取该变量时,会从主存中重新获得最新值,而不是使用当前工作内存中的值。
public class ThreadSafeInteger {
private volatile int value;
public int get() {
return value;
}
public void set(int value) {
this.value = value;
}
}
注:volatile虽然保证了可见性,但并不保证操作的原子性。
public class Test {
private static volatile long _longVal = 0;
public static void main(String[] args) {
Thread t1 = new Thread(new LoopVolatile1());
t1.start();
Thread t2 = new Thread(new LoopVolatile2());
t2.start();
try{
t1.join();
t2.join();
}catch(Exception e){
e.printStackTrace();
}
System.out.println("final val is: " + _longVal);
}
private static class LoopVolatile1 implements Runnable {
public void run() {
long val = 0;
while (val < 100000) {
_longVal++;
val++;
}
}
}
private static class LoopVolatile2 implements Runnable {
public void run() {
long val = 0;
while (val < 100000) {
_longVal++;
val++;
}
}
}
}
运行上述代码,发现每次输出不同。
CAS即Compare And Swap,JDK中Unsafe类提供了一系列的compareAndSwap*方法。
下面以compareAndSwapLong(Object obj, long valueOffset, long expect, long update)方法为例进行介绍
boolean compareAndSwapLong(Object obj, long valueOffset, long expect, long update)方法:如果obj对象中内存偏移值为valueOffset的变量值为expect,则使用新的值update替换之。这是处理器提供的一个原子性指令。
假如线程A要去通过CAS修改变量X,要先判断X当前值是否改变过,如果“未改变”,则更新之。但这并不能保证X没有被改变过:假如A修改X前,线程B修改了X的值,然后又修改回来,A的CAS操作仍能成功,但X实际上发生过改变。
JDK中的AtomicStampedReference类给每个变量都配备了一个时间戳,从而避免了ABA问题的产生。
JDK的rt.jar 包中的UnSafe类提供了硬件级别的原子性操作,Unsafe类中的方法都是native方法,使用JNI的方式访问本地C++实现库。
Unsafe类可以直接操作内存,这是不安全的,如果是普通的调用的话,它会抛出一个SecurityException异常;只有由主类加载器加载的类才能调用这个方法。
见下:
public static Unsafe getUnsafe() {
Class localClass = Reflection.getCallerClass();
if(!VM.isSystemDomainLoader(localClass.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
但通过万能的反射,还是可以使用到Unsafe类的:
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
Java内存模型允许编译器和处理器对不存在数据依赖性的指令进行重排序以提高性能。
如下:
int a = 1; //(1)
int b = 2; //(2)
int c = a + b; //(3)
如果有必要,JVM完全可以将(2)放在(1)前执行,但这并不会影响最终结果。
单线程下这样做是ok的,但多线程下就会出现问题:
private static int num = 0;
private static boolean ready = false;
public static void main(String[] args){
Thread t1 = new Thread(new ReadTask());
Thread t2 = new Thread(new WriteTask());
t1.start();
t2.start();
try{
Thread.sleep(100);
}catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println("main exit!");
}
public static class ReadTask implements Runnable {
@Override
public void run() {
while(true) {
if(ready) {
System.out.println(num);
return;
}
}
}
}
public static class WriteTask implements Runnable {
@Override
public void run() {
num = 1; //(1)
ready = true; //(2)
}
}
理论上这段代码并不一定输出1,因为进行指令重排序后,WriteTask中的(2)语句有可能会先于(1)执行,导致输出为0。
注:在没有公平性需求的前提下尽量使用非公平锁,因为公平锁会带来额外性能开销。
一个线程要再次获取自己已经获取了的锁时如果不被阻塞,则该锁为可重入锁。
public class Test{
public synchronized void f1() {
System.out.println("f1...");
}
public synchronized void f2() {
System.out.println("f2...");
f1();
}
}
上述代码中,调用f2()并不会造成阻塞,说明synchronized内部锁是可重入锁。
当前线程获取锁时,如果发现锁已经被其他线程占有,并不会马上阻塞自己,而是在不放弃CPU使用权的情况下,多次尝试获取,到一定次数后才放弃。目的是使用CPU时间换取线程阻塞与调度的开销。