JAVA内存模型和线程安全

一.JAVA内存模型(JMM,JAVA Memory Model):

    运行时涉及到两种内存,主内存和工作区内存,其中工作区内存通常为CPU的高速缓存区用来加快内存数据读取操作的(各线程独立).所有的变量内容都存在主内存中,当需要对内存数据进行操作时,数据将会从主存中load到工作区缓存并由CPU计算和赋值操作,然后再由工作区内存write到主存中,读取时如果工作区内存中已经有(loaded)则直接使用;工作区内存保存了线程使用的变量的副本,线程不可以直接操作主内存,只能操作工作区内存,对于需要变更的变量,需要通过一系列回写指令集同步到主内存中.且工作区内存是线程独占的,主内存是线程共享的.如下为操作集:

  1. lock:对主内存中的变量"加锁",标记为一个线程持有.如果一个变量已经被lock,其他线程尝试lock将被阻塞,同一个线程可以多次lock,不过锁的引用次数将会+1,需要unlock同样次数,才能解锁.对一个变量lock将会导致清空工作区内存中此变量的副本,即当其他线程再次使用此变量时需要重新获取.
  2. unlock:对主内存中的变量"释放锁",释放锁的线程和持有锁的线程必须是同一个线程,无法对没有加锁的变量执行unlock.
  3. read:由工作区内存向主内存发出"read"操作,随后必须执行load操作.
  4. load:工作区内存中加载"read"操作指定的变量,并放入副本中.此指令需要和read保持顺序
  5. use:变量交付给执行引擎做计算,当JVM需要使用变量时,将会使用此操作.
  6. assign:在工作区内存中,将变量更新为某个值.一个被assign操作的变量,必须被write到主内存,如果没有被assign的变量不能被write到主内存.
  7. store:工作区内存向主内存发出"同步"操作.
  8. write:工作区内存将store操作指定的变量同步到主内存中.此操作需要和store保持顺序.

其中read->load,store->write指令必须按照顺序执行,即不能load一个没有被read操作指定的变量,也不能write一个没有被store操作指定的变量,不过这read + load/store + write不一定必须是连续的,其中间仍然可以有其他指令.(volatile有特例)

    volatile是java提供的轻量级变量同步机制,它确保了变量可见性,即任何一个线程修改了volatile修饰的变量,其他线程将立即可见.对于普通变量因为存在主内存和工作区内存的复制和同步,所以无法具备此特性.volatile变量存储在主内中,任何线程需要使用此变量时,必须再次read,因为其他线程对此变量的更改,将会被其他线程在使用此变量时立即获得新值.

    volatile只是保证了可见性,但是它并非线程安全,因为如果线程一旦read到此值然后进行计算,在尚未write到主内存时其他线程也做了同样的操作,那么volatile变量的最终结果将无法达到预期..如果期望volatile变量线程安全,必须同步或者CAS.volatile变量操作时,read->load->use三个操作是连续的,assign->store->write三个操作是连续的.

    通常volatile对“值”类型的对象是有效的,对引用类型是没有意义的。

 

二.线程安全

    线程是执行任务的最小调度单元,内核线程是OS创建和管理的线程,它将有内核完成线程的切换以及调度(CPU调度).任何一个java线程都对应一个内核线程,即java线程的所有特性都基于内核并受制于内核.在linux和windows系统中,一个java线程就是底层的一个内核线程.java对线程的调度基于内核,在主流的系统中,广泛采用了"抢占式"调度机制,即线程都以"争抢CPU资源"的姿态来运行,最终被运行的线程将有内核的调度算法来决定,如果线程没有获得运行资源,那么线程将被"暂停".."协同式"调度已经不适合多线程(进程)的系统,它表现为线程之间互相"谦让",如果一个线程获得运行资源,那么它将一直运行下去直到结束,如果一个线程是"长时间"的,那么极有可能这个线程将独占一个CPU,而其他线程无法获得资源..

   线程状态:

  1. NEW:新创建线程,尚未开启.
  2. RUNNABLE:当前线程已经被启动或者正在被运行,处于此状态的线程标明即将或者已经得到了运行资源.
  3. WAITING:如果线程因为wait()/join()/LockSupport.park(this)/sleep()导致当前线程无法继续执行或者获得资源.
  4. BLOCKED:如果当前线程因为对象锁获取时,被"阻塞",那么线程将处于BLOCKED状态,此状态下,线程不会释放资源.
  5. TERMINATED:线程执行结束,资源即将被回收.

    在JAVA中(甚至任何语言或者平台中)确保线程安全的方式,无外乎"同步锁"和"CAS","同步锁"是一种粗暴而严格的同步手段,它强制对资源的访问必须队列化,一个资源在任何时候只能有一个线程可访问.在java中"synchronized"修饰词可以用来同步方法的调用,synchronized可以指定需要同步的对象,如果 没有指定,默认为当前对象,如果是static方法,则表示对Class同步.synchronized关键词在编译之后,最终会生成2个指令:monitorenter和monitorexit,执行引擎如果遇到monitorenter指令,将会尝试获取对象锁,如果获取成功,则锁计数器+1,同时工作区中的对象值将视为无效,重新从主存中load;monitorexit将导致锁计数器-1,即释放锁,此时将会把对象值从工作区缓存中write到主存中;如果计数器为0,则表示此对象没有被任何线程加锁.如果获取锁失败,当前线程阻塞.此外synchronized本身具有"重入性"语义,如果此对象上的monitor是当前线程,那么锁获取操作将直接成功.

   我们不再争论synchronized锁和ReentrantLock API锁谁更优秀,这一把双刃剑,性能方面两者在普通情况下(即无复杂递深的lock调用或者多层synchronized)性能几乎差不多,synchronized稍微优秀一些.但是ReentrantLock提供了多样化的控制以及Condition机制,可以帮助我们有效的控制并发环境中,让线程遵循条件的阻塞和唤醒;例如BlockingQueue的实现机制.

    CAS(Compare and swap),设计方式上更像一种"乐观锁",通过"比较"-"更新"这种无阻塞的手段实现数据在多线程下的"安全性".在JAVA中CAS操作遍布Atomic包下的API中,底层使用一个闭源的Unsafe.compareAndSwapInt(Object,valueOffset,expect,update),其中需要告知对象的内存地址.CAS会出现一个有趣的问题,就是ABA,即A变量被更改为B之后,再次被更改为A,此时对于持有A数据的线程尝试更改值是可以成功了,就像B值从来就没有出现过一样..其实吧,这个问题不是问题,既然有线程把数据更改为A,那么后续的线程操作就应该遵守现在的结果,而无需关注过去的过程.

你可能感兴趣的:(JAVA,java,操作系统)