Java并发编程的三大特性:原子性、可见性、有序性
并发是什么呢?
并发指的是Java中有多线程并发运行。
为什么会有并发?
因为一个线程在执行的过程中不仅会用到cpu资源,还会用到IO,也就是输入输出。IO的速度远远比不上CPU的运算速度。如果线程在IO的时候,不放弃CPU资源,就会导致CPU空闲,以至于CPU的利用率十分低下。所以就引入了多线程,当一个线程要请求IO的时候,会放弃cpu资源。这个时候,其他线程可以使用cpu。这就提高了cpu的利用率。
虽然线程之间的切换有额外的资源消耗,但是带来的回报更大。所以多线程是主流。
Java并发编程有三大特性:
1、可见性
因为线程所需的数据都存在了内存中,当cpu需要数据的时候,如果每次都去内存中去读取的话,是不合适的。因为CPU的运算是十分快的,是到内存中读取数据的速度的指数级倍数,二者是不匹配。所以这就会出现CPU利用率不高的问题。所以CPU内增加了一个高速缓存,高速缓存读取数据的效率是很高的,是可以与CPU匹配的,所以线程会将内存中的数据拷贝到CPU高速缓存中,读取和改写的是CPU高速缓存中的数据,然后再运算结束后,再将数据写入到内存中。
我们都知道现在的CPU都是多核的,所以说一个线程对应了一个高速缓存,如果多个线程同时读取一个变量的时候,会在每个高速缓存中都拷贝一份数据,彼此之间是不可见的。
不可见的话,出导致并发的一些问题,接下来我们举个例子
假如有A、B两个线程,
1、A首先执行依次X++操作,因为此时CPU缓存1中不存在X值,所以会从内存中将x=1调入,然后进行X++操作,然后将X=2写入到CPU缓存1和内存中。
2、B接着执行X++操作,因为CPU缓存2中不存在X值,所以将X=2调入CPU缓存2中,进行X++操作,然后将X=3写入到CPU缓存2和内存中。
3、这时A再次执行X++操作,因为此时cpu缓存1中已经存在X=2了,所以直接使用,执行X++,将X=3将写入到CPU缓存1和内存中。
正确结果应该是4,现在却变成了3,都是因为内存不可见的问题。
对于可见性问题,Java提供了volatile关键字来修饰变量。当变量被volatile修饰后,CPU读取的时候,将会禁用缓存,每次都去内存中读取内存。
2、原子性
原子操作是Java中的最小操作过程。原子的意思就是操作不可以被中断。
还拿之前的例子来说明。
x++其实并不是一个原子性操作,它被分为三部分:
1、读取x的值
2、x+1
3、将x+1写入到内存中
当按照以下步骤处理时,会出现线程不安全
1、当A读取先完成了1、2步骤后,然后被中断
2、B开始执行,执行力1、2、3步骤,x变为了2
3、A执行3步骤,x变为2
还是出现了内存丢失。
对于原子性,Java中引入了锁来解决,当A进行X++操作时,对加锁,在代码执行完成后,才会释放锁。在A持有锁的期间,如果B想要执行X++操作,则会被阻塞。
加锁的时候,其他线程会被切换,当再次被运行的时候线程会重新从内存中读取值,所以枷锁本身已经保证了可见性。
随着计算机硬件的发展,Java又引入原子类来解决这一问题。这个具体的实现我们以后单独说。
3、有序性
由于编译会对代码进行优化,所以有的时候会调整代码的执行顺序。有的时候也会导致意想不到的bug。
public void test1(){//线程A执行
obj = new Object();
init = true;
}
public void test2(){//线程B执行
if(init){
obj.toString();
}
}
比如上述代码,从test1方法看来,obj = new Object()和init = true 看起来时没啥关系的,有可能编译的时候,会将init = true编译到前面。
假如当A执行完init = true后,线程A停止,然后切换到了线程B,这个时候init = true,会进入 obj.toString()。但是此时obj 还未初始化,会报异常!
将变量用volatile修饰的时候,会避免指令重排。
当变量被volatile修饰时,
1、如果是写volatile,那么写volatile之前的语句不会被安排到写volatile之后
2、如果是读volatile,那么读volatile之后的语句不会被安排到读volatile之前。