由于内存和处理之间的速度差异,所以说在内存和处理之间加入一层高速缓存。将需要运算的数据复制到缓存中,使运算快速执行,处理器执行后,再将缓存中的结果同步到内存。
引入缓存解决了内存和处理器速度不匹配问题,但是引入了缓存不一致性问题。在多处理器系统中,每一个处理器都有自己的缓存,而它们共享主内存。当多个处理器处理主内存中同一块区域的数据时,将可能导致数据不一致问题。
为了使得处理器的运算单元能够充分利用,处理器会把输入的代码打乱顺序执行。处理器计算之后将计算结果进行重组,使得重组后的结果与代码顺序执行的结果相同,但是并不保证各个语句的计算结果的先后顺序与代码中的顺序相同,因此如果一个任务以来另一个任务的中间结果,那么其顺序性不能靠代码的顺序来保证。与处理器的乱序优化相似,java虚拟机使用了重排序优化。
Java内存模型的目标是:定义程序中变量的访问规则。这里的变量不是指方法中的局部变量和参数(以内这些变量存储在Java虚拟机栈中,是线程私有的,不是共享变量,不存在线程竞争),指的是实例的字段,静态变量,构成数组对象的元素。
Java内存模型可以分为主内存和工作内存。所有的变量都存储在主内中,每一个线程都有自己的一个工作内存。
线程的工作内存中使用的变量是主内存中变量的一个副本。对变量的所有操作都只能在工作内存进行,不能直接对主内存中的变量进行访问。不同线程之间,不能够直接的访问对方工作空间中的变量,只能够通过主内存间接的传递变量。
Java内存模型中定义了8种操作:
lock(锁定):用于主内存中的变量,把一个变量标识为线程独占状态。
unlock(释放):用于主内存中的变量,把一个处于锁定状态的变量释放,释放后的变量才可以被其它线程锁定。
Read(读取):用于主内存中的变量,把主内存中变量的值读到工作内存中。
Load(载入):用于工作内存中的变量,将read操作从主内存中读取的变量的值放入到工作内存的变量的副本中。
Use(使用):用于工作内存中的变量,将工作内存中变量的值传给执行引擎,每当虚拟机遇到一个需要变量值的字节码指令时都会执行该操作。
Assign(赋值):用于工作内存的变量,将执行引擎返回值赋值给工作内存中的变量,当虚拟机遇到给变量赋值的指令时就会执行该操作。
Store(存储):用于工作内存的变量,将工作内存中变量的值传到主内存中
Write(写入):用于主内存中的变量,将store从工作内存中出入到主内存中的值放入到变量中。
保证变量对所有线程是可见的,即当一个线程修改了变量的值,其他线程可以立即知道新值。
对于volatile修饰的变量,线程每次使用前都要从主内存刷新,因此可以保证线程的可见性。
synchronized关键字,synchronized关键字在编译时,会在同步代码块的前后分别添加monitorenter和monitorexit指令。在执行monitorenter指令时,首先要去获取对象的锁,如果获取成功,或者当前线程已经拥有该对象锁,则将锁计数器加1。在执行monitorexit指令时,将锁计数器减1,如果锁计数器为0,则释放锁,其他在该对象锁上竞争等待的线程重新开始竞争锁。
synchronized是可重入的,如果一个线程持有锁对象,再重新请求该所对象时不会阻塞。在锁对象被线程持有时,其他线程获取锁对象会阻塞等待,直到持有该锁对象的线程释放锁,才会重新竞争。
Java.until.Concurrent包下的ReentrantLock也可以实现同步,ReentrantLock使用lock和unlock来加锁和释放锁。ReentrantLock有以下特性:
(1) 可设置中断:如果等待持有锁的线程释放锁的事件太长,可以中断等待区执行其他任务。
(2) 可设置公平锁
(3) 可以绑定若干个条件。Condition类。
阻塞同步会带来系统的性能问题,线程的阻塞和唤醒都会带来性能问题。这是一种悲观策略。对于同步问题不一定都要进行加锁,还有一种乐观的策略,非阻塞同步。
CAS算法有三个操作数,变量的内存地址V,期望的旧变量值A,新的值B。如果V中的值与期望的旧值A相同,则用新值A来替换V的值。否则就不执行更新。CAS算法有一个ABA问题。
阻塞同步,对于线程的挂起和唤醒会给操作系统的并发性能增加很多压了。实际上共享数据的锁定状态只持续很短的一段时间,为了这点时间去执行挂起和唤醒线程操作不值得。而自旋锁,如果具有一个以上的处理器,允许两个或以上线程同时执行,如果一个锁对象已经被一个线程持有,则另一个线程要稍等一会,但是不会放弃CPU的执行权,看看线程是否很快就会释放锁,只要让线程进入忙循环状态(自旋)。
对于自旋锁,如果线程长时间不释放锁,则处于自旋状态的线程就会浪费处理器的资源。所以引入了自适应自旋,自适应意味着自旋时间不固定,而是根据上一次在同一个锁上的自旋时间和锁的拥有者状态决定的。如果在同一个锁上,自旋刚刚获得锁,并且持有锁的线程正在执行,则将自旋等待的时间设置长一点。如果在该对象锁上,很少通过自旋来获得锁,则省略使用自旋获取锁,直接阻塞等待。
根据经验对于绝大部分的锁在整个同步期间是不存在竞争的。所以引入了轻量级锁。在执行monitorenter时,需要去获取锁对象。判断对象头的Mark Word中的标志位,如果标志位为01(没有锁定),则在线程栈帧中存储一个名为锁记录的空间,将Mark Word复制到锁记录空间。然后使用CAS操作尝试将Mark Word替换为指向当前线程的指针。如果替换成功则表示获取锁成功,并将Mark Word标志位置为00(轻量级锁)。如果操作失败则检查Mark Word中指针是否指向当前线程,如果指向则表示当前线程已经拥有锁,可以直接进入同步代码块执行。否则,说明这个对象的锁已经被其他线程拥有。
如果存在两个以上的线程竞争锁,则轻量级锁不再有效,膨胀为重量级锁,将锁标志位置为10。Mark Word中存储的是重量级锁的指针,后面等待锁的线程进入阻塞状态。
释放锁也是使用CAS操作,如果Mark Word指向当前线程,则使用CAS操作将当前锁记录中的数据替换回Mark Word中,如果替换成功,则锁释放,如果没有替换成功则表示有线程尝试竞争锁,则在释放锁的同时唤醒等待的线程。
根据经验,锁对象大多数情况下往往只被某一个线程获取,所以引入了偏向锁。在开启偏向锁模式下,锁对象第一次被线程获取的时候,会将Mark Word中的锁标志位置为01(偏向锁),并使用CAS操作将获取该偏向锁的线程ID记录在MarkWord中,如果操作成功,则该线程再进入该锁对象相关的同步代码块中,就不需要进行同步操作。
如果有其他线程尝试获取锁对象,则偏向模式结束。