Java 并发编程_Synchronized

硬件的效率和一致性:

由于计算机的运算速度和它的存储和通讯子系统的速度差距巨大,大部分时间都花在IO,网络和数据库上。为了压榨CPU的运算能力,需要并发。

由于运算速度的差距,CPU和存储设备间加入多层的cache。同时也引入了缓存一致性的问题。解决缓存一致性有多种读写协议,(MSI,MESI,MOSI,Synapse,Firefly和Dragon Protocol等。




JAVA内存模型:

主内存和工作内存
1.所有的变量都存储在主内存中
2.每个线程都还有自己的工作内存,拥有主内存的对象的拷贝
3.线程只能操作自己的工作内存,线程间的交互只能通过主内存通讯





内存间的交互操作:

8种操作的作用场景:

1.read 和load ,store和write 必须一对操作
2.不允许线程丢弃assign操作,变量在工作内存中改变后必须把变法同步回主内存
3.没有assign操作,不允许变量从工作内存同步回主内存
4.新变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化的变量(load或assign)对一个变量实时use和store之前必须先执行过assign和load操作
5.一个变量同一时刻值允许一条线程lock操作,但lock操作可以多次,执行相同数量的unlock,变量才会解锁
6.lock操作会清空工作内存副本,执行引擎使用前,需要执行load或者assign操作初始化变量的值
7.没有lock操作,就不允许unlock操作。不允许unlock另一个线程变量。
8.unlock操作前必须先store,write操作


锁是 java 并发编程中最重要的同步机制

Java 中的每一个对象都可以作为锁。

    对于普通同步方法,锁是当前实例对象。
    对于静态同步方法,锁是当前类的 Class 对象。
    对于同步方法块,锁是 Synchonized 括号里配置的对象。

当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。

public class Account {

   private int balance;

   public Account(int balance) {
     this.balance = balance;
   }

   public int getBalance() {
     return balance;
   }

   public synchronized void add(int num) {
     balance = balance + num;
   }

   public synchronized void withdraw(int num) {
     balance = balance - num;
   }

}


每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列,就绪队列存储了将要获得锁的线程,阻塞队列存储了被阻塞的线程,当一个被线程被唤醒(notify)后,才会进入到就绪队列,等待cpu的调度。当一开始线程a第一次执行account.add方法时,jvm会检查锁对象account的就绪队列是否已经有线程在等待,如果有则表明account的锁已经被占用了,由于是第一次运行,account的就绪队列为空,所以线程a获得了锁,执行account.add方法。如果恰好在这个时候,线程b要执行account.withdraw方法,因为线程a已经获得了锁还没有释放,所以线程b要进入account的就绪队列,等到得到锁后才可以执行。

一个线程执行临界区代码过程如下:
1 获得同步锁
2 清空工作内存
3 从主存拷贝变量副本到工作内存
4 对这些变量计算
5 将变量从工作内存写回到主存
6 释放锁

可见,synchronized既保证了多线程的并发有序性,又保证了多线程的内存可见性。

同步的原理:

JVM 规范规定 JVM 基于进入和退出 Monitor 对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用 monitorenter 和 monitorexit 指令实现,而方法同步是使用另外一种方式实现的,细节在 JVM 规范里并没有详细说明,但是方法的同步同样可以使用这两个指令来实现。monitorenter 指令是在编译后插入到同步代码块的开始位置,而 monitorexit 是插入到方法结束处和异常处, JVM 要保证每个monitorenter 必须有对应的 monitorexit 与之配对。任何对象都有一个 monitor 与之关联,当且一个monitor 被持有后,它将处于锁定状态。线程执行到 monitorenter指令时,将会尝试获取对象所对应的 monitor 的所有权,即尝试获得对象的锁。


Java 对象头:

Java 对象头里的 Mark Word 里默认存储对象的 HashCode,分代年龄和锁标记位。





32 位 JVM的 Mark Word 的默认存储结构:





原子操作:

处理器实现原子操作:

32 位 IA-32 处理器使用 基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。

第一个机制是通过总线锁保证原子性,总线锁就是使用处理器提供的一个 LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占使用共享内存。

第二个机制是通过缓存锁定保证原子性,在同一时刻我们只需保证对某个内存地址的操作是原子性即可,但总线锁定把 CPU 和内存之间通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,最近的处理器在某些场合下使用缓存锁定代替总线锁定来进行优化。频繁使用的内存会缓存在处理器的 L1,L2 和 L3 高速缓存里,那么原子操作就可以直接在处理器内部缓存中进行,并不需要声明总线锁,缓存锁定就是如果缓存在处理器缓存行中内存区域在 LOCK 操作期间被锁定,当它执行锁操作回写内存时,处理器不在总线上声言 LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时会起缓存行无效。

Java 实现原子操作:

Java 中可以通过 锁和环循环 CAS 的方式来实现原子操作。



猜你喜欢

转载自maosheng.iteye.com/blog/2264477