并发编程的其他基础知识
一、多线程并发编程
多个线程在多个CPU上运行称为多线程并发编程。
二、多线程并发编程的好处
多个CPU意味着每个线程可以使用自己的CPU运行,减少了上下文切换的开销。
三、Java中的线程安全问题
多个线程同时读写一个共享资源并且没有任何同步措施时,导致出现脏数据或者其他不可预见的结果的问题。
四、Java中共享变量的内存可见性问题
主内存:Java规定所有的变量存放的地方叫做主内存。
工作内存:线程使用变量时,会把主内存的变量复制到自己的工作空间,也叫工作内存。
当处于两个cpu的两个线程同时处理一个共享变量时,因为Cache的存在,导致内存不可见问题。
五、Java中的synchronized关键字
synchronized是Java提供的一种原子性内置锁,也叫内部锁、监视器锁、同步锁。
syschronized的内存语义是:当获取锁后,会清理锁块内本地内存中将会用到的共享变量,在使用这些共享变量的时候,直接从主内存进行加载。在释放锁的时候将本地内存修改的共享变量刷回到主内存。
六、Java中的volatile关键字
当一个变量被声明为volatile时,线程在写入变量的时候不会把值缓存在寄存器或者其他地方,而是把值刷回到主内存。线程读取变量的时候,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。因此volatile能保证内存可见性。但是不保证原子性。
七、Java中的原子操作
使用sychronized能够保证原子性。
八、Java中的CAS操作
CAS是JDK提供的非阻塞原子性操作。很类似于乐观锁的操作。
CAS有四个操作数
- 对象内存位置
- 对象中的变量的偏移量
- 变量预期值
- 新的值
ABA问题:A-B,B-A,虽然此时的值还是和原来一样,但是某种意义上来讲已经不是原来那个值了。
九、Unsafe类
unsafe类提供了硬件级别的原子性操作,并且Unsafe类中的方法都是native,使用JNI的方式访问本地C++实现库。
十、Java指令重排序
Java内存模型允许编译器和处理器对指令重排序以提供运行性能,并且只对不存在数据依赖性的指令重排序,在单线程下重排序可以保证最终的结果与程序执行的结果一致,但是多线程则不然。声明为volatile的变量可以避免指令重排序问题。
十一、伪共享
CPU存在一级缓存,在Cache内部是按行存储的,其中每一行称为一个cache行,当CPU访问某个变量时,会先看CPU Cache里面是否存在,如果存在直接获取,否则去主内存中获取该变量,由于Cache行里面放的是内存块,而不是单个变量,所以一个cache行中会存在多个变量,当多个线程同时修改一个缓存行里面的多个变量时,由于只能有一个线程操作缓存行,所以性能会下降,这就是伪共享。
出现伪共享的原因是缓存与内存交换的单位是缓存行,CPU发现变量没有在缓存行中,会把变量周围的变量一并放入缓存行。单线程下是有利的,因为把更多的变量加入到了缓存中,一定程序上可以提供命中率,多线程则不然。
避免伪共享:
- 字节填充
- @sun.misc,Contended:该注解只用于Java核心类,用户使用需要添加JVM参数:-XX:RestrictContended,填充的宽度默认是128,自定义宽度 -XX:ContendedPaddingWidth参数。
十二、锁的概述
- 乐观锁:只有到数据提交更新的时候,再处理冲突
- 悲观锁:在处理数据前加锁
- 公平锁:按照先来先到获取锁,有性能开销,不建议使用
- 非公平锁:不按照先来先到,都有可能获取到锁
- 独占锁:只能被单个线程持有,ReentrantLock是独占锁
- 共享锁:可以被多个线程持有,ReadWriteLock读写锁是共享锁
- 可重入锁:运行一个线程持有锁时,再次获取自己持有的锁,synchronized是可重入锁
- 自旋锁:多次尝试获取锁,使用CPU时间换取线程阻塞与调度的开销,尝试次数 -XX:PreBlockSpinsh,默认是10次 ,目的减少内核态到用户态的切换开销。