--本文多总结于Java并发编程的艺术一书中的内容,深深感谢此书作者,使我从中受益。
先知道一点,不是多线程下程序的执行速度就是最快的。
 
了解一下什么是时间片
时间片是CPU分配给各个线程的时间,获得了时间片的线程才会得到执行的机会,时间片一般为几十毫秒。
 
什么是多线程?
CPU通过给每个线程分配时间片来实现多线程,每个线程都需要获取处理器的时间片,来获得执行的机会。只是时间片较短,在线程切换的时候给我们制造了一个假象,让我们以为多个线程是并发执行的,实则不然。cpu在分配时间片给下一个线程的时候,当前线程会保存自己当前结束时的状态,以便下次获得时间片的时候可以延续上次任务的状态继续执行,所以任务从保存到再加载的过程就是一次上下文的切换,上下文的切换会占用时间。
 
刚才说到的,不是多线程的执行速度就是最快的,当并发量达到一定程度的时候,速度才会比单线程的串行执行速度快,因为单线程不会存在由上下文切换带来的时间上的损耗,而多线程是一定存在的。
 
有什么办法是可以减少上下文切换的呢?答案是一定有,可以采用无锁并发编程(volatile),CAS算法(原子操作),使用最少线程(减少大量等待线程-减少工作线程数),和使用协程(在单线程中实现多任务的调度)。
 
现在探讨一下Java并发机制的底层实现原理
 
首先,介绍一下Java文件从创建到最终被执行的过程,说点JVM的类加载机制。
我们创建的.java文件首先会被编译器(例如idea)编译成.class文件,然后这个.class文件会被JVM的类加载器所加载,并被转译为一系列的字节码,JVM会执行这些字节码,最终变为可以在处理器CPU上执行的一系列汇编指令,这些汇编指令才是真正操作的指令。
 
现在先来简单探讨一下volatile和synchronized
volatile可以被称为轻量级的synchronized,但是和synchronized完全不同,它在多处理器开发中保证了共享变量的可见性。
可见性的意思是说,当一个线程修改一个共享变量的时候,另外一个线程可以读到这个修改的值,它不会引起上下文的切换。
Java线程内存模型JSR-133确保所有线程看到的volatile变量是一致的。当volatile变量被执行到汇编指令lock一词时,会引发两件事情:
1. 将当前处理器缓存行的数据写回到系统内存。
2. 这个写回操作会使其他CPU里缓存了该内存地址的数据无效。
每个处理器通过嗅探在总线上的传播数据来检查自己缓存的值是否是过期了,当发现自己缓存行中的对应的内存地址被修改了,将会把缓存行的数据设置为无效状态,当处理器对这个数据进行修改的时候会再向主内存中取。
 
解释一下缓存行,每个处理器都会执行多个线程,一个服务器可能会有多个处理器。在Java内存模型中,线程不会直接操作主内存中的数据,而是把主内存中的数据拷贝一份当作副本存到自己线程的线程栈中和处理器的缓存行(L1、L2、L3)中,缓存行会存储这一副本。当某一个正在执行的线程想对volatile变量进行操作的时候,会看这个volatile是否被修改过了,如果修改过了会将线程栈中的变量副本置为无效状态,然后再去主内存中拿新的副本,然后会将修改后的值刷新到主内存中。
volatile使用优化:追加字节的方式。
 
Java中的每一个对象都可以作为锁,synchronized实现同步的基础:
对于synchronized修饰的普通方法,锁是当前对象。
对于静态同步方法,锁是当前类的Class对象。
对于同步方法块,锁是synchronized括号里配置的对象,当一个线程试图访问同步代码时,他首先必须得到锁。
 
synchronized的底层语意时使用monitor来实现的,每一个对象(锁)都有一个monitor:
1. monitor计数器为0,则等待的线程可以进入,进入成功后,monitor的计数器+1.
2. 如果线程已经占有当前的monitor则直接进入,计数器+1.
3. 如果对象的monitor已被其他线程占有,那么当前竞争的线程将会被阻塞,直到monitor的计数器变为0,则会重新尝试获取monitor的所有权。
代码块同步使用的是monitorenter指令和monitorexit指令实现的。monitorenter是在编译后插入到同步代码块的开始位置,monitorexit是插入到方法结束或异常处。当执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获取对象的锁。
 
先了解一下Java对象头,一会儿会用到
synchronized用的锁是存在Java对象头中里的,虚拟机用2个字节宽来存储Java对象头,如果是数组的话用3个字节宽来存储,在32位虚拟机中,一个字节宽等于4个字节,即32位。Java对象头中的Mark Word中默认存储对象的hashcode、分代年龄和锁标记位。
Java中锁一共有四种状态:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个锁的状态会随着锁竞争的情况自主地逐渐升级,且不会因竞争状态下降而下降,也就是锁的状态只会升不会降。
偏向锁:当一个线程访问同步快获取锁时,会将锁对象的对象头中和线程栈栈帧中的锁记录里存放偏向锁(该锁)所偏向的线程的线程id,如果在偏向锁中存在某一线程的线程id,表示该线程获取了这个锁,也就是获得了这个锁所在的这个对象,如果不存在,线程想获取该锁的话,会尝试使用CAS算法将对象头中的偏向锁里存放指向当前线程的线程id。同时,当有竞争出现时,也就是有其他线程竞争该锁时,持有偏向锁的线程会主动去释放这个偏向锁,让偏向锁变为自由的可获取的状态。然后,拥有偏向锁的对象会根据条件变为无锁状态、偏向于其他线程、标记对象不适合作为偏向锁(升级为轻量级锁)等三个状态。可以通过JVM参数来设置开启或关闭是否默认启用偏向锁,关闭偏向锁后,程序会默认进入轻量级锁状态。
轻量级锁:线程执行同步块之前,JVM会现在当前线程的栈帧中创建一块用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针,如果成功,则线程获得锁,如果不成功,表示有其他线程也在竞争锁,当前线程就会尝试使用自旋来获取锁,但自旋会消耗CPU。自旋锁是一种非阻塞锁,为了降低因自旋所消耗的cpu,可以采用自适应自旋锁,JVM会自动调节自旋次数上限,超出上限后再进入排队队列。同时为了避免无用的自旋,轻量级锁在一定情况下会自动升级成重量级锁,当锁处于重量级的状态下,其他线程获取锁时都会被阻塞住,知道当前线程释放锁后其他线程才有竞争这个锁的机会,重量级锁线程不会使用自旋去竞争。
 
原子操作的实现原理
处理器有两种方式实现原子操作:
1 使用总线锁保证原子性
2 使用缓存锁保证原子性
 
Java有两种方式实现原子操作:
1 锁:锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。
2 循环CAS(ABA问题的解决方法是,在变量前追加版本号)
 
 
 
 
 
 
 
 
 
 
 
 
 
--本文多总结于Java并发编程的艺术一书中的内容,深深感谢此书作者,使我从中受益。
先知道一点,不是多线程下程序的执行速度就是最快的。
 
了解一下什么是时间片
时间片是CPU分配给各个线程的时间,获得了时间片的线程才会得到执行的机会,时间片一般为几十毫秒。
 
什么是多线程?
CPU通过给每个线程分配时间片来实现多线程,每个线程都需要获取处理器的时间片,来获得执行的机会。只是时间片较短,在线程切换的时候给我们制造了一个假象,让我们以为多个线程是并发执行的,实则不然。cpu在分配时间片给下一个线程的时候,当前线程会保存自己当前结束时的状态,以便下次获得时间片的时候可以延续上次任务的状态继续执行,所以任务从保存到再加载的过程就是一次上下文的切换,上下文的切换会占用时间。
 
刚才说到的,不是多线程的执行速度就是最快的,当并发量达到一定程度的时候,速度才会比单线程的串行执行速度快,因为单线程不会存在由上下文切换带来的时间上的损耗,而多线程是一定存在的。
 
有什么办法是可以减少上下文切换的呢?答案是一定有,可以采用无锁并发编程(volatile),CAS算法(原子操作),使用最少线程(减少大量等待线程-减少工作线程数),和使用协程(在单线程中实现多任务的调度)。
 
现在探讨一下Java并发机制的底层实现原理
 
首先,介绍一下Java文件从创建到最终被执行的过程,说点JVM的类加载机制。
我们创建的.java文件首先会被编译器(例如idea)编译成.class文件,然后这个.class文件会被JVM的类加载器所加载,并被转译为一系列的字节码,JVM会执行这些字节码,最终变为可以在处理器CPU上执行的一系列汇编指令,这些汇编指令才是真正操作的指令。
 
现在先来简单探讨一下volatile和synchronized
volatile可以被称为轻量级的synchronized,但是和synchronized完全不同,它在多处理器开发中保证了共享变量的可见性。
可见性的意思是说,当一个线程修改一个共享变量的时候,另外一个线程可以读到这个修改的值,它不会引起上下文的切换。
Java线程内存模型JSR-133确保所有线程看到的volatile变量是一致的。当volatile变量被执行到汇编指令lock一词时,会引发两件事情:
1. 将当前处理器缓存行的数据写回到系统内存。
2. 这个写回操作会使其他CPU里缓存了该内存地址的数据无效。
每个处理器通过嗅探在总线上的传播数据来检查自己缓存的值是否是过期了,当发现自己缓存行中的对应的内存地址被修改了,将会把缓存行的数据设置为无效状态,当处理器对这个数据进行修改的时候会再向主内存中取。
 
解释一下缓存行,每个处理器都会执行多个线程,一个服务器可能会有多个处理器。在Java内存模型中,线程不会直接操作主内存中的数据,而是把主内存中的数据拷贝一份当作副本存到自己线程的线程栈中和处理器的缓存行(L1、L2、L3)中,缓存行会存储这一副本。当某一个正在执行的线程想对volatile变量进行操作的时候,会看这个volatile是否被修改过了,如果修改过了会将线程栈中的变量副本置为无效状态,然后再去主内存中拿新的副本,然后会将修改后的值刷新到主内存中。
volatile使用优化:追加字节的方式。
 
Java中的每一个对象都可以作为锁,synchronized实现同步的基础:
对于synchronized修饰的普通方法,锁是当前对象。
对于静态同步方法,锁是当前类的Class对象。
对于同步方法块,锁是synchronized括号里配置的对象,当一个线程试图访问同步代码时,他首先必须得到锁。
 
synchronized的底层语意时使用monitor来实现的,每一个对象(锁)都有一个monitor:
1. monitor计数器为0,则等待的线程可以进入,进入成功后,monitor的计数器+1.
2. 如果线程已经占有当前的monitor则直接进入,计数器+1.
3. 如果对象的monitor已被其他线程占有,那么当前竞争的线程将会被阻塞,直到monitor的计数器变为0,则会重新尝试获取monitor的所有权。
代码块同步使用的是monitorenter指令和monitorexit指令实现的。monitorenter是在编译后插入到同步代码块的开始位置,monitorexit是插入到方法结束或异常处。当执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获取对象的锁。
 
先了解一下Java对象头,一会儿会用到
synchronized用的锁是存在Java对象头中里的,虚拟机用2个字节宽来存储Java对象头,如果是数组的话用3个字节宽来存储,在32位虚拟机中,一个字节宽等于4个字节,即32位。Java对象头中的Mark Word中默认存储对象的hashcode、分代年龄和锁标记位。
Java中锁一共有四种状态:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个锁的状态会随着锁竞争的情况自主地逐渐升级,且不会因竞争状态下降而下降,也就是锁的状态只会升不会降。
偏向锁:当一个线程访问同步快获取锁时,会将锁对象的对象头中和线程栈栈帧中的锁记录里存放偏向锁(该锁)所偏向的线程的线程id,如果在偏向锁中存在某一线程的线程id,表示该线程获取了这个锁,也就是获得了这个锁所在的这个对象,如果不存在,线程想获取该锁的话,会尝试使用CAS算法将对象头中的偏向锁里存放指向当前线程的线程id。同时,当有竞争出现时,也就是有其他线程竞争该锁时,持有偏向锁的线程会主动去释放这个偏向锁,让偏向锁变为自由的可获取的状态。然后,拥有偏向锁的对象会根据条件变为无锁状态、偏向于其他线程、标记对象不适合作为偏向锁(升级为轻量级锁)等三个状态。可以通过JVM参数来设置开启或关闭是否默认启用偏向锁,关闭偏向锁后,程序会默认进入轻量级锁状态。
轻量级锁:线程执行同步块之前,JVM会现在当前线程的栈帧中创建一块用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针,如果成功,则线程获得锁,如果不成功,表示有其他线程也在竞争锁,当前线程就会尝试使用自旋来获取锁,但自旋会消耗CPU。自旋锁是一种非阻塞锁,为了降低因自旋所消耗的cpu,可以采用自适应自旋锁,JVM会自动调节自旋次数上限,超出上限后再进入排队队列。同时为了避免无用的自旋,轻量级锁在一定情况下会自动升级成重量级锁,当锁处于重量级的状态下,其他线程获取锁时都会被阻塞住,知道当前线程释放锁后其他线程才有竞争这个锁的机会,重量级锁线程不会使用自旋去竞争。
 
原子操作的实现原理
处理器有两种方式实现原子操作:
1 使用总线锁保证原子性
2 使用缓存锁保证原子性
 
Java有两种方式实现原子操作:
1 锁:锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。
2 循环CAS(ABA问题的解决方法是,在变量前追加版本号)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 posted on 2019-12-12 20:35  冗热  阅读(170)  评论(0编辑  收藏  举报