多线程与多任务
多任务处理在现代计算机操作系统中几乎已是一项必备的功能。
在许多场景下,让计算机同时去做几件事情,不仅是计算机的运算能力强大,还有一个重要的原因是计算机的运算速度与它的存储通信子系统的速度差距太大
,大量的时间都花费在磁盘I/O、网络通信或者数据库访问上。
如果不希望处理器在大部分时间里都处于等待其他资源的空闲状态,就必须使用一些手段去把处理器的运算能力“压榨”出来,否则就会造成很大的性能浪费,而让计算机同时处理几项任务则是最容易想到的,也被证明是非常有效的“压榨”手段。
让计算机并发执行若干个任务与更充分地利用计算机处理器的效能,并没有想象的那么简单,其中一个重要的复杂性是绝大多数的运算任务都不可能只靠处理器“计算”就能完成。处理器至少要与内存交互,如读取运算数据、存储运算结果等,这个I/O操作就是很难消除的(无法只靠寄存器来完成所有的运算任务)。
由于计算机的存储设备与处理器
的运行速度有着几个数量级的差距
,所以现代计算机都加入高速缓存
作为内存与处理器之间的缓冲:将运算需要使用的数据复制到缓存中,让 运算能快速进行,当运算结束后再从缓存同步回内存 之中,这样处理器就无需等待缓慢的内存读写了。
基于高速缓存解决很好解决了处理器与内存速度之间的矛盾,但也引入了一个新的问题:缓存一致性
,在多路处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一个主内存。当多个处理器的运算任务都涉及到同一个主内存区域时,将可能导致各自的缓存数据不一致。为了解决一致性的问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作。
除了增加告诉缓存之外,为了使处理器内部的运算任务单元尽可能被充分利用,处理器可能会对输入代码进行乱序执行优化
,处理器会在计算之后将乱序的执行的结果重组,保证该结果与顺序执行的结果是一致的,但不保证程序中的各个语句计算先后顺序与输入代码中的顺序一致,因此如果存在一个计算任务依赖另外一个计算任务的中间结果,那么其顺序并不能靠代码的先后顺序来保证。与处理器的乱序执行优化类似,Java虚拟机的即时编译器中也有指令重排序优化
。
Java内存模型
Java内存模型的主要目的是定义程序中各种变量的访问规则,即关注虚拟机把变量值
存储到内存和从内存中去除这样的底层细节。
变量
:与Java编程中所有的变量有所区别,它包含了实例字段、静态字段和构成数组对象的元素,但是不包含局部变量和方法参数,因为后置是线程私有的,不会被共享。
Java内存模型规定了所有的变量存储在住内存中,每个线程都有自己的工作内存,线程的工作内存
保存了被该线程使用变量的主内存副本
,线程对变量的所有操作都必须在工作内存中进行,而不嗯呢该直接读写主内存中的数据。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存完成。如下图
内存间交互操作
Java内存模型中定义以下8种操作
:
- lock(锁定):作用于主内存的变量,它把一个变量标识为一个线程独占的状态。
- unlock(解锁):作用于主内存的变量,它把一个处于锁定标识状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read(读取):作用于主内存的变量,它把一个变量从主内存传输到线程的工作内存中,以便随后的load操作。
- load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量放入工作内存的变量中。
- use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存中的变量,每个虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store(存储):作用于工作内存的变量,它把工作内存红一个变量的值传送到主内存中,以便随后的write操作。
- write(写入):作用于主内存的变量,它把store操作从工作内存中的带的变量的值放入到主内存的变量中。
Java内存模型规定了在上述8中基本操作是必须满足如下规则
:
- 不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者工作内存发起回写了但主内存不接受的情况出现。
- 不允许一个线程丢弃它最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
- 一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说就是一个变量实施use、store操作之前,不行先执行load和assign操作。
- 一个变量在同一时刻只允许一个线程对其进行lock操作,但lock操作可以被同一个线程重读执行多次,多次lock之后,只有执行相同次数的unlock操作,变量才会被释放。
- 如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作以初始化变量的值。
- 如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许unlock一个被其他线程锁定的变量。
- 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中。
volatile
关键字volatile可以说是Java虚拟机提供的最轻量级的同步机制。
当一个变量被定义volatile之后,它将具备以下特征:
第一:保证此变量对所有线程的可见性,当一个线程修改这个变量的值,新值对其他所有线程来说是可以立即得知的。而普通变量的值在线程间传递需要通过主内存来完成。
注意:普通变量一般是当前线程执行完成之后才会将值回写到主内存,而volatile变量是值修改完成之后立马回写到主内存,在每次use之前都会先执行read和load操作从主内存同步数据到工作内存。
第二:禁止指令重排优化。普通的变量仅会保证在该方法的执行过程中所有依赖赋值结构的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。
由于Java里面的运算符操作并非原子性的,这导致volatile变量的运算在并发下一样是不安全的。
如:在当前线程执行到use之后,其他线程进行值同步,当前线程计算的时候还是用旧值计算。在计算完成之后assign操作时, 导致计算结果是错误的。且会将错误值进行同步到其他线程。
CAS
cas执行包含key(在内存中的地址),oldValue(预期值),value(设定值)。cas执行是原子性的获取当前值,和预期值比较,如果相等则设置设定值。线程获取锁成功。
在设定cas值得时候最好采用递增值处理,否则会出现ABA问题。即一个T1线程将原值为A的值修改为B,在一个线程T2将B修改为A。
构成死锁条件
条件互斥: 可用资源数量少于线程数。
持有并等待其他: 至少持有一个资源,并且等待获取其他资源
不可剥夺: 持有的资源只能在使用完成才能释放
环路等待: 线程相互持有对方所需要的资源
锁的类型
自旋锁
因现代计算机大部分都是多核处理器,能够让两个或两个以上的线程同时并发执行。当线程执行时需要资源被锁定,这个时候让当前线程“稍等一下”,但不放弃处理器执行时间,看持有锁的线程是否很快就释放锁,为了让线程等待,只需要执行一个忙(空)循环自旋。
锁消除
虚拟机即时编译在运行时,对一些代码要求同步,但是对检查到不可能存在共享数据竞争的锁进行消除。
锁粗化
如果一系列连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体之内的,在这种情况下会把锁的范围扩大。