这是我参与更文挑战的第N天,活动详情查看更文挑战
要理解并发编程,首先要知道CPU多线程是如何实现的。
上下文切换机制:CPU是通过给每个线程分配CPU时间片(时间片一般是几十毫秒)来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片比较短,所以CPU通过不停的切换线程执行,让我们感觉多个线程是同时执行的。CPU通过时间片分配算法来循环执行任务(例如,时间边轮转算法,优先级算法),当前任务执行一个时间片后会切换到下一个任务,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以在加载这个任务的状态。所以任务从保存到在加载的过程就是一次上下文切换。
并发机制的底层实现原理
Java代码在编译之后会变成字节码文件,字节码文件被类加载器加载到JVM中,JVM执行字节码,最终需要转化为汇编指令在CPU上执行,Java中所使用的的并发机制依赖于JVM的实现和CPU的指令。
Synchronized 和 Volatile
在Java中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。
在多线程并发编程中Synchronized一直是元老级角色,喝多人都会称呼它为重量级锁。
利用Synchronized实现同步的基础:Java中的每一个对象都可以作为锁。具体表现为以下3中形式:
- 对于普通同步方法,锁是当前实例对象。
- 对于静态同步方法,锁是当前类的Class对象
- 对于同步方法块,锁是Synchronized括号里配置的对象。
当一个线程试图访问同步代码开始,它必须首先得到锁,退出或者抛出异常时必须释放锁。
Synchronized在JVM里的实现原理
什么是Monitor?
Monitor其实是一种同步工具,也可以说是一种同步机制,它通常被描述为一个对象,主要特点是:
- 对象的所有方法都被“互斥”的执行。好比一个Monitor只有一个运行“许可”,任一个线程进入任何一个方法都需要获得这个“许可”,离开时把许可归还。
- 通常提供singal机制:允许正持有“许可”的线程暂时放弃“许可”,等待某个谓词成真(条件变量),而条件成立后,当前进程可以“通知”正在等待这个条件变量的线程,让他可以重新去获得运行许可。
JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。具体就是通过monitorenter和monitorexit指令来实现的。
monitorenter指令是编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,他将会处于锁定状态。线程执行到monitorenter指令是,将会尝试获取对象所对应的Monitor的所有权,即尝试获得对象的锁。
Volatile的定义和原理
Volatile是轻量级的Synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能够读到这个修改的值。如果Volatile变量修饰符使用恰当的话,它会比Synchronized的使用和执行成本更低,因为他不会引起线程上下文的切换和调度。
在汇编代码中查看volatile
当我们把含有volatile的代码编译成汇编代码是会发现,被volatile修饰的变量代码中有Lock前缀指令。Lock前缀的指令在多核处理器下会引发两件事情:
- 将当前处理器缓存行的数据写回到系统缓存。
- 这个写会内存的操作会使其他CPU里缓存了该内存地址的数据无效。
首先我们要明白,为了提高处理速度,处理器不会直接和内存进行通信,而是先将系统内存的数据读到内部缓存后在进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,讲这个变量所在缓存行的数据写会到系统内存。并且,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设为无效状态,当处理器对这个数据进行操作时,会重新从系统内存中把数据读到处理器缓存里。