内存模型
内存模型是在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象。其主要目标是定义程序中各个变量的访问规则。
主内存和工作内存
所有的变量都存储在主内存中,每条线程还有自己的工作内存,其工作内存中是被线程使用到的变量的主内存副本拷贝,线程对变量的读取、赋值等操作都必须在工作内存中进行,而不能直接读取主内存中的变量。
内存间交互操作
从主内存拷贝到工作内存:顺序地执行read和load操作。
工作内存同步到主内存:store和write操作。
volatile的特性
Volatile的作用和synchronized相同,但是和synchronized相比,更轻量。其特性主要有如下两点:
保证此变量对所有线程的可见性
啥意思呢?指当一个线程修改了这个变量的值,新值对于其他线程来说是立即可知的。而普通变量做不到这一点,普通变量的值在线程间传递均需要通过主内存来完成,比如线程A修改了一个普通变量的值,然后向主内存进行回写,另外一条线程B在线程A回写完成了之后再从主内存进行读取操作,新变量值才会对线程B可见。
禁止指令重排序优化
因为指令重排序会干扰程序的并发执行。
多线程
为什么需要多线程?
计算机的运算速度与它的存储和通信子系统速度的差距太大,大量的时间都花费在磁盘I/O、网络通信、数据库访问上了。使用多线程能更好地利用cpu。
有哪些并发应用场景?
充分利用计算机处理器
一个服务端同时对多个客户端提供服务
如何使处理器内部的运算单元被充分利用?
加入一层高速缓存
将运算需要使用到的数据复制到缓存中,让运算能快速进行。当运算结束后再从缓存同步回内存中,这样处理器就无须等待缓慢的内存读写了。不过这个要考虑一个问题:怎么保证缓存的一致性。
对输入代码进行乱序执行优化
线程的实现方式
使用内核线程实现
内核线程就是直接由操作系统内核支持的线程。
使用用户线程实现
用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助,内核也感知不到线程存在的实现。这种实现方式使用较少。
使用用户线程加轻量级进行混合实现
合并到一起
线程调度
线程调度是指系统为线程分配处理器使用权的过程。主要分为两种:协同式和抢占式。
协同式
线程的执行时间由线程本身来控制,线程把自己的工作执行完了,会主动通知系统切换到另外一个线程上。
其优点是实现简单,而且没有线程同步的问题。缺点是如果一个线程编写有问题,一直不告诉系统进行线程切换,那程序就会一直阻塞在那里,容易导致系统崩溃。
抢占式
线程将由系统来分配执行时间,线程切换不由本身来决定。java使用的线程调度方式就是这种。
线程安全
当多个线程访问一个对象时,如果不考虑这个线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其它的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是安全的。
共享数据的分类
不可变
不可变的共享数据是用final修饰的数据,其一定是线程安全的。如果共享数据是一个基本类型变量,那么只要在定义的时候使用final关键字即可。
如果共享数据是一个对象,那就需要对象的行为不会对其状态产生影响,可以将对象中带有状态的变量都声明为final。比如String类就是一个不可变类
绝对线程安全
在Java API中标注自己是线程安全的类,大多数都不是绝对的线程安全。比如Vector是一个线程安全的集合,它的所有的方法都被修饰成同步,但是在多线程的环境中,它依旧不是同步的。
相对线程安全
相对线程安全就是我们通常意义上所说的线程安全,它只能保证对这个对象单独操作是线程安全的。但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。
大部分的线程安全类都属于这种类型。
线程兼容
对象本身不是线性安全的,但可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全的使用。大部分的不是线程安全的类,都属于这种类型。
线程对立
无论怎样,都不能在多线程环境中并发使用,如System.setIn()、System.SetOut()。一个对输入进行修改,一个对输出进行修改,两者是不能“交替”进行的。
实现方法
方式一:互斥同步——悲观并发策略
(1)synchronized
其原理是:这个关键字在经过编译后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令。当执行monitorenter指令时,程序会尝试获取对象的锁,如果能获取到,则把锁的计数器+1,相应的,在执行monitorexit时,会将锁计数器-1。当计数器为0时,锁就被释放。
其特点是:对同一条线程来说是可重入的;同步块在已进入的线程执行完之前,会阻塞后面的其他线程进入。
其选用场景是:在确实必要的情况下才使用此,因为其是重量级的。
(2)ReentrantLock
此重入锁是java.util.concurrent(JUC)包下的类。其高级特性有:等待可中断、可实现公平锁、锁可以绑定多个条件。
方式二:非阻塞同步——乐观并发策略
先进行操作,如果没有其它线程争用共享数据,那操作就是成功了;如果共享数据有争用,产生了冲突,那就再采取其它的补偿措施。
方式三:无同步方案
如果一个方法本来就不涉及共享数据,那就没有必要进行同步措施。比如可重复代码和线程本地存储。
(1)可重入代码
如果一个方法,它的返回结果是可预测的,只要输入了相同的数据,就都能返回相同的结果,那它就满足可重入的要求。
(2)线程本地存储
如果一段代码中所需要的数据必须与其它代码共享,而且这些共享数据的代码在同一个线程中执行,如此,我们可以把共享数据的可见范围限制在一个线程中,这样,就不用同步也能保证线程之间不出现数据争用问题。
锁优化
适应性自旋
因为阻塞或者唤醒一个JAVA的线程需要操作系统切换CPU状态来完成,这种状态的转换需要耗费处理器时间。如果同步代码块中的内容过于简单,很可能导致状态转换消耗的时间比用户代码执行的时间还要长。
为了解决这个问题,我们可以让后面请求锁的线程“稍等一下”,执行一个忙循环,进行自旋。此时没有放弃处理器的执行时间。如果自旋超过了限定的次数,仍然没有成功获得锁,那就会使用传统的方式去挂起线程了。
那什么叫做适应性自旋呢?
就是在同一个锁对象上,如果自旋等待刚刚成功获得过锁,那虚拟机就会认为这次自旋获得锁的概率挺大,就会允许其自旋等待持续相对更长的时间。相反,如果自旋很少成功获得过锁,则可能省略掉自旋过程。
锁消除
指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。
锁粗化
如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。
如果虚拟机探测到一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围粗化到整个操作序列的外部,这样只需要加锁一次就够了。
轻量级锁
在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能损耗。
适用场景:无实际竞争,多个线程交替使用锁;允许短时间的锁竞争。
偏向锁
偏向锁用于减少无竞争且只有一个线程使用锁的情况下,使用轻量级锁产生的性能消耗。轻量级锁每次申请、释放锁都至少需要一次CAS,但偏向锁只有初始化时需要一次CAS。
适用场景:无实际竞争,且将来只有第一个申请锁的线程会使用锁。
以上是关于JAVA虚拟机中高效并发的详细介绍,