多线程并发编程:
并发是指同一个时间段内多个任务同时都在执行,并且都没有执行结束,并发任务强调在一个时间段内同时执行,而一个时间段由多个单位时间累积而成,所以说并发的多个任务在单位时间内不一定同时在执行。在单CPU的时代多个任务都是并发执行的,这是因为单个CPU同时只能执行一个任务。在单CPU时代多任务是共享一个CPU的,当一个任务占用CPU运行时,其他任务就会被挂起,当占用CPU的任务时间片用完后,会把CPU让给其他任务来使用,所以在单CPU时代多线程编程是没有太大意义的,并且线程间频繁的上下文切换还会带来额外开销。在多线程编程实践中,线程的个数往往多于CPU的个数,所以一般都称多线程并发编程而不是多线程并行编程。
线程安全问题:
当多个线程同时读写一个共享资源并且没有任何同步措施时,导致出现脏数据或者其他不可预见的结果的问题
共享变量的内存不可见:
● 线程A首先获取共享变量X的值,由于两级Cache都没有命中,所以加载主内存中X的值,假如为0。然后把X=0的值缓存到两级缓存,线程A修改X的值为1,然后将其写入两级Cache,并且刷新到主内存。线程A操作完毕后,线程A所在的CPU的两级Cache内和主内存里面的X的值都是1。● 线程B获取X的值,首先一级缓存没有命中,然后看二级缓存,二级缓存命中了,所以返回X= 1;到这里一切都是正常的,因为这时候主内存中也是X=1。然后线程B修改X的值为2,并将其存放到线程2所在的一级Cache和共享二级Cache中,最后更新主内存中X的值为2;到这里一切都是好的。● 线程A这次又需要修改X的值,获取时一级缓存命中,并且X=1,到这里问题就出现了,明明线程B已经把X的值修改为了2,为何线程A获取的还是1呢?这就是共享变量的内存不可见问题,也就是线程B写入的值对线程A不可见。【线程2修改值并刷新到主内存后,并没有告诉线程1,而线程1的工作内存又保存了工享变量的值】
Synchronized关键字:
共享变量内存可见性问题主要是由于线程的工作内存导致的。
使用synchronized关键字可以解决工享变量内存可见性的问题:进入synchonized块的语义是把在synchonized块内对共享变量的修改刷新到主内存。 这个和加锁和释放锁的语义是一致的。当获取锁后会清空锁内存块本地内存中将会被用到的共享变量,在使用这些共享变量时从主内存加载。在释放锁时将本地内存中修改的共享变量刷新到主内存。
除可以解决共享变量内存可见性问题外,synchonized经常被用来实现原子操作。但是这样会引起线程上下文切换并带来线程调度开销。
Volatile关键字:
使用Volatile关键字可以解决工享变量内存可见性的问题:该关键字可以确保对一个变量的更新对其他线程马上可见。当一个变量被声明为volatile时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。当其他线程读取该共享变量时,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。
写Volatile变量时,可以确保volatile写之前的操作不会被编译器重排序到Volatile写之后(避免被覆盖)。 读Volatile变量时,可以确保Volatile读之后的操作不会被编译到Volatile读之前(避免误读)。
使用Volatile关键字的场景:
1 写变量值不依赖变量的当前,因为如果依赖当前值,将是获取—计算—写入三步操作,这三个操作不是原子性的,而Volatile不保证原子性。 (正因如此,所以常常会用到CAS算法,从硬件级别保证操作的原子性)
2 读写变量时没有加锁,因为加锁本身已经保证了内存的可见性,这时候无需把变量声明为volatile。
原子性操作:
原子性操作:执行一系列操作时,这些操作要么全部执行,要么全部不执行,不存在只执行其中一部分的情况。
CAS操作:
在Java中,锁在并发处理中占据了一席之地,但是使用锁有一个不好的地方,就是当一个线程没有获取到锁时会被阻塞挂起,这会导致线程上下文的切换和重新调度开销。Java提供了非阻塞的volatile关键字来解决共享变量的可见性问题,这在一定程度上弥补了锁带来的开销问题,但是volatile只能保证共享变量的可见性,不能解决读—改—写等的原子性问题。CAS即Compare and Swap,其是JDK提供的非阻塞原子性操作,它通过硬件保证了比较—更新操作的原子性。
CAS有四个操作数,分别为:对象内存位置、对象中的变量的偏移量、变量预期值和新的值。其操作含义是,如果对象obj中内存偏移量为valueOffset的变量值为expect,则使用新的值update替换旧的值expect。这是处理器提供的一个原子性指令。
Java指令重排序:
Java内存模型允许编译器和处理器对指令重排序以提高运行性能,并且只会对不存在数据依赖性的指令重排序。在单线程下重排序可以保证最终执行的结果与程序顺序执行的结果一致,但是在多线程下就会存在问题。
使用Volatile关键字可以解决指令重排序的问题,使用场景如下:
1 写变量值不依赖变量的当前,因为如果依赖当前值,将是获取—计算—写入散步操作,这三个操作不是原子性的,而Volatile不保证原子性。 (正因如此,所以常常会用到CAS算法,从硬件级别保证操作的原子性)
2 读写变量时没有加锁,因为加锁本身已经保证了内存的可见性,这时候无需把变量声明为volatile。
伪共享:
当CPU访问某个变量时,首先会去看CPU Cache内是否有该变量,如果有则直接从中获取,否则就去主内存里面获取该变量,然后把该变量所在内存区域的一个Cache行大小的内存复制到Cache中。由于存放到Cache行的是内存块而不是单个变量,所以可能会把多个变量存放到一个Cache行中。当多个线程同时修改一个缓存行里面的多个变量时,由于同时只能有一个线程操作缓存行,所以相比将每个变量放到一个缓存行,性能会有所下降,这就是伪共享。
伪共享的产生是因为多个变量被放入了一个缓存行中,并且多个线程同时去写入缓存行中不同的变量。那么为何多个变量会被放入一个缓存行呢?其实是因为缓存与内存交换数据的单位就是缓存行,当CPU要访问的变量没有在缓存中找到时,根据程序运行的局部性原理,会把该变量所在内存中大小为缓存行的内存放入缓存行。【把主内存的数据拉到CPU Cache时,一个缓存行存放了多个变量。当多个线程想访问这些变量时却不能共同操作该缓存行】
解决伪共享的办法:
1 在JDK 8之前一般都是通过字节填充的方式来避免该问题,也就是创建一个变量时使用填充字段填充该变量所在的缓存行,这样就避免了将多个变量存放在同一个缓存行中。
2 JDK 8提供了一个sun.misc.Contended注解,用来解决伪共享问题。