1. 什么是多线程并发编程
并发是指同一个时间段内多个任务同时(宏观上的同时,微观上是时间片划分)都在进行,并且都没有执行结束.
2. 为什么要进行多线程并发编程(废话)
多核CPU时代的到来打破了单核CPU对多线程效能的限制.对多个CPU意味着每个线程可以使用自己的CPU运行,这减少了线程上下文切换的开销,但随着对应用系统性能和吞吐量要求的提高,出现了处理海量数据和请求的要求,这些都对高并发编程有着迫切的需求
3. Java中的线程安全问题
线程安全问题是指当多个线程同时读写一个共享资源并且没有任何同步措施时,导致出现脏数据或者其他不可预见的结果的问题.
如果线程不去修改共享资源,那么就不会出现线程安全问题.
4. Java中共享变量的内存可见性问题
Java内存规定,讲所有的变量都放入主内存,线程使用变量时从主内存复制一份放入工作内存,读写时使用的是自己工作内存里的变量.
5. Java中的synchronized关键字
(1). synchronized关键字介绍
synchronized块是Java提供的一种原子性内置锁,Java中的每个对象都可以把它当做一个同步锁来使用(内部锁,监视器锁).==线程的执行代码在进入synchronized代码块前会自动获取这个内部锁,这时其他线程访问synchronized代码块时就会得不到这个内部锁,就会被阻塞挂起.要想释放这个内部锁,有三种方法:正常的退出synchronized代码块,抛出异常退出代码块或者同步代码块内调用了同步锁的wait()系列方法.==
Java中的线程是与操作系统的原生线程一一对应的,当其他线程被synchronized代码块阻塞时,就导致了上下文切换,非常耗时.
(2). synchronized的内存语义
==进入synchronized块会将代码块内用到的变量从工作内存清空,这样在块中使用变量时就会从主内存中获取.==
==退出synchronized块会将工作内存中的变量值刷新到主内存.==
这也是加锁和释放锁的语义.
6. Java中的volatile关键字
这是一种弱形式的同步,确保修改volatile变量时能对其他线程立即可见.
写入volatile变量时立即刷新到主内存,读取volatile变量时先从主内存刷新工作内存的变量再读取.
什么时候使用volatile关键字?
- 写入变量不依赖当前值(a = a+1).这样的操作实际上是3步,从内存获取a,给a加上1,将a写入内存.这三步不是原子操作,中间可能出现线程轮换.
- 没有使用锁进行同步,使用锁也不需要再使用volatile关键字了.
7. Java中的原子性操作
原子性操作,是指执行一系列操作时,这些操作要么全部执行要么全部不执行.(中间不能出现线程轮换)
最简单的方法就是使用synchronized关键字进行同步.
8. Java中的CAS操作
使用锁的可以做到原子操作,但是当一个线程没有获取到锁时会挂起.而volatile只能保证共享变量的可见性,不能保证原子性问题.
CAS即Compare and Swap,是JDK提供的非阻塞原子性操作.
boolean compareAndSwap(Object obj, long valueOffset, long expect, long update):意思是比较并交换,如果obj对象中偏移量为valueOffset的变量和expect相同,那么更新为update.
9. Unsafe类
JDK的rt.jar包中的Unsafe类提供了硬件级别的原子性操作,Unsafe类中的方法都是native方法.
10. Java指令重排序
Java内存模型允许编译期和处理器对指令重排序以提高运行性能,并且只会对不存在的数据依赖性的指令重排列.但是在多线程下就会出现问题.
单线程中,冲排列对程序的运行结果一定是没有影响的,但是JVM仅仅会考虑运行线程的重排列,并不会考虑会不会对其他并发的线程产生的影响(也做不到这一点,并发都是随机的).
对于多线程下的指令重排序,只需要在关键的变量上声明为volatile就可以避免重排序和内存可见性问题.
volatile变量将指令重排序的指令序列进行分隔,指令重排序之能在分隔之间进行.
11. 伪共享
(1). 什么是伪共享
为了解决计算机内存和CPU直接运行速度差距过大的问题,CPU和主内存之间会设置一级或者多级高速缓存存储器(Cache).Cache内部是按行进行存储的,一行通常为2的幂次数字节.当CPU访问某个变量时,先在Cache中查找,如果没有,去主内存中查找,并且==将目标对象所在内存区域的一个Cache行大小的内存全部缓存到Cache中==.
单线程状态下,这样可以减少对象的复制,可以提高运行速度.但是在==多线程状况下,多个线程可能同时访问Cache中的一行,造成了资源竞争与等待.==这样就会让性能有所下降.
(2). 为何会出现伪共享
因为多个变量被放入了一个缓存行中,并且多个线程同时去写入一个缓存行中的不同变量.
(3). 如何避免伪共享
JDK 8之前手动填充字段(使一个对象刚刚好占用一个缓存行的内存),JDK 8提供了@Contended注解,但@Contended注解默认情况下只能用于Java核心类.
12. 锁的概述
(1). 乐观锁和悲观锁
悲观锁指对数据被外界修改持保守态度,在数据被处理前先对数据进行加锁.实现往往依靠锁机制.
乐观锁认为数据一般情况下不会造成冲突,所以访问前不会加锁.在数据提交时,对数据冲突进行检测,并进行处理.客观所不会使用锁机制,一般添加标记字段(版本号).不会产生任何死锁..
(2). 公平锁和非公平锁
根据线程获取锁的抢占机制分为公平锁和非公平锁.
区别就是公平锁在获取锁的时候是根据请求的顺序(挂起)来分配锁的,而非公平锁完全随机.
公平锁会带来性能开销.
(3). 独占锁和共享锁
区别在于能否被多个线程持有.
独占锁只能有一个线程持有,是一种悲观锁.
共享锁能被多个线程持有,是一种客观锁.
(4). 可重入锁
当一个线程占有一个锁之后,如果还能再次获取同一个锁多次,那么这个锁是可重入锁.
synchronized内部锁是可重入锁(内部关联了一个计数器).
(5). 自旋锁
在获取锁时,发现被其他线程占有,并不会立即阻塞挂起,而是多次尝试获取锁,指定次数获取后还是没有获取到锁,才会被挂起.