目录
1.多线程的出现是要解决什么问题的?本质什么?
2.Java是怎么解决并发问题的?
3.线程安全有哪些实现思路?
4.如何理解并发和并行的区别?
5.线程有哪几种状态?分别说明从一种状态到另一种状态转变有哪些方式?
CPU,内存,I/O设备的速度是有极大差异的,为了合理利用CPU的高性能,平衡这三者的速度差异,计算机体系结构,操作系统,编译程序都做出了共享,主要体现为:
CPU增加了缓存,以均衡与内存的速度差异;//导致可见性问题
操作系统增加了进程,线程,以分时复用CPU,进而均衡CPU与I/O设备的速度差异;//导致原子性问题
编译程序优化指令执行次序,使得缓存能够得到更加合理的利用。//导致有序性问题
Java内存模型是个很复杂的规范,具体看Java内存模型详解。
理解的第一个维度:核心知识点
JMM本质上可以理解为,Java内存模型规范了JVM如何提供按需禁用缓存和编译优化的方法。
具体来说,这些方法包括:
volatile,sychronized和final三个关键字
Happens-Before规则
理解的第二个维度:可见性,有序性,原子性
原子性
在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。请分析以下哪些操作是原子性操作:
上面4个只有语句1的操作具备原子性。
也就是说,只有简单的读取,赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。
从上面可以看出,Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任意时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。
可见性
Java提供了volatile关键字来保证可见性。
当一个共享变量被volatile修饰时,他会保证修改的值会立刻被更新到主存,当有其他线程需要读取时,他会去内存中读取新值。
而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
另外,通过synchronized和Lock也能够保证可见性,sychronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将变量的修改刷新到主存当中。因此可以保证可见性。
有序性
在Java里面,可以通过volatile关键字来保证一定的“有序性"。另外可以通过sychronized和Lock来保证有序性。很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。当然JMM是通过Happens-Before规则来保证有序性的。
1.互斥同步
sychronized和ReentrantLock
2.非阻塞同步
互斥同步最主要的问题就是线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。
互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,那就肯定会出现问题。无论共享数据是否真的出现竞争,他都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁),用户态转换,维护锁计数器和检查是否有被阻塞的线程唤醒等操作。
CAS
随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略:先进行操作,如果没有其他线程争用共享数据,那操作就成功了,否则采取补偿措施(不断地重试,直到成功为止)。这种乐观的并发策略的许多实现都不需要将线程阻塞,因此这种同步操作称为非阻塞同步。
乐观锁需要操作和冲突检测这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。硬件支持的原子性操作最典型的是:比较并交换(Compare-and-Swap,CAS)。CAS指令需要有3个操作数,分别是内存地址V,旧的预期值A和新值B。当执行操作时,只有当V的值等于A,才将V的值更新为B。
AtomicInteger
J.U.C包里面的整数原子类AtomicInteger,其中的compareAndSet()和getAndIncrement()等方法都使用了Unsafe类的CAS操作。
3.无同步方案
要保证线程安全,并不是一定就要进行同步。如果一个方法本来就不涉及共享数据,那他自然就无须任何同步措施去保证正确性。
栈封闭
多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有的。
线程本地存储(Thread Local Storage)
如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码能否保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。
并发是指一个处理器 同时处理多个任务。
并行是指多个处理器或者多核的处理器同时处理多个不同的任务。
新建(New)
创建后尚未启动。
可运行(Runnable)
可能正在运行,也可能正在等待CPU时间片。
包含了操作系统线程状态中的Runnuing和Ready。
阻塞(Blocking)
等待获取一个排他锁,如果其线程释放了锁就会结束此状态。
无限期等待(Waiting)
等待其他线程显式地唤醒,否则不会分配CPU时间片。
限期等待(Timed Waiting)
无需等待其他线程显式地唤醒,在一定时间之后会被系统自动唤醒。
调用Thread.sleep()方法使线程进入限期等待状态时,常常用”使一个线程睡眠“进行描述。
调用Object.wait()方法进入限期等待或者无限期等待时,常常用”挂起一个线程“进行描述。
睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态。
阻塞和等待的区别在于,阻塞是被动的,他是等待获取一个排他锁。而等待是主动的,通过调用Thread.sleep()和Object.wait()等方法进入。
死亡(Terminated)
可以是结束任务之后自己结束,或者产生了异常而结束。