Java分布式应用学习笔记03JVM对多线程的资源同步和交互机制

1.  前言

既然是分布式系统,就离不开对于多线程程序的开发,面对客户端大并发的访问,如何控制程序的多线程资源?我们都知道在程序中使用关键字synchronized,对对象级别的加锁也好,对类级别的加锁也罢。JVM在底层是如何运行的,这个属于JVM处理多线程的原理了,当然了,JVM最终当然还是需要操作系统和CPU一起完成真正的多线程并发的问题。只是咱们这次放慢时间,看看JVM这一层对于多线程并发机制是如何做处理的。

不同的线程之间一般出现的交互关系有:竞争,也可以称为互斥;交互,也可以称为共享协作。

2.  旧话重提——何为线程不安全

这其实是一个老生常谈的问题了。还是那句老话,线程不安全发生在单例或者多例的情况下,如果每次访问服务代码都new一个新的对象,此新对象所有非静态的东东都是指向内存中不同的地址段。换句话说就是你干你的,我干我的。井水不犯河水,你走你的独木桥,我有我的阳关道,何来线程安全问题。下面是一个单例模式下的代码片段:

Java代码    收藏代码
  1. int i=0;  
  2. public void add(){  
  3.     this.i ++ ;  
  4. }  
 如果在同一个时刻,客户端有多个人同时调用了此代码块的add方法。会出现i的值可能会出现与预期结果不符的现象。咱们将时间放慢,就用显微镜看看JVM是如何处理this.i ++ ;这简简单单的一行代码的。

1.  首先在JVM的堆区(main memory)区域分配给i一个内存存储场所,并存储其值为初始值0。

2.  一个客户端发起调用,线程启动后分配了一个区域操作数栈区域(working memory)当线程执行到了this.i++时候,JVM细节上在working memory做着5个步骤

3.  装载i,线程发起一个请求,让JVM执行引擎像堆区发一个读取的指令

4.  读取i,读取指令开始执行,从堆开始读取i,i从堆复制到working memory区

5.  进行i++操作,线程完成相加指令

6.  存储i,将i++的结果赋值给i变量,之后在存储到working memory区域

7.  写入i,将i的结果值写回到刚刚的堆区(main memory)

在3~7这几步骤中虽然时间极其短,但是高并发下,一定概率还是能发生线程不安全的问题的。

在JVM堆区(main memory)通常的操作有:read、write、lock、unlock。

read:从堆读取变量的值。

write:将working memory的值写回到堆中的变量值。

lock:由线程发起,同步操作堆区,将堆中的对象上锁。

unlock:也由线程发起,去除对象上的锁。前提是线程掌握了该对象的锁。

在working memory区域,其实也是真正的指令工作区域,一般有以下一些操作:use、use、assign、load、store、lock、unlock。lock与unlock上面已经说过了,我们看看其他的指令是什么意思。

use:由线程发起,将working memory区域的变量值复制到JVM执行引擎中

assign:由线程发起,将变量值复制到working memory区域。a=i,相当于线程发起了一个assign指令。

load:将堆中read到的值复制到working memory中。

store:负责将working memory区域的值返回复制给堆区。

这些指令就是三大区域:堆、指令工作区(就是上面一直称之为working memory的东东)、JVM执行引擎区交互的指令,利用这些指令,我们程序中的变量值才能发生变化。

如果这个代码块处在一个每次请求都new一个对象出来的情况下有线程安全问题吗,答案当然是:“没有”。i变量属于局部变量每次new一个对象出来,对象指针指向内存新的地址区域,对象内局部变量也是指向新的内存位置。

3.  线程资源竞争机制

既然上面的程序有了线程安全问题,那么我们怎么解决呢?

有多种解决方案:加同步关键字、使用ThreadLocal进行副本操作、使用new…………。

一般集中在前两者,前者是用时间换取空间,加锁,阻塞。后者是以空间换时间,使用变量副本。我们在此只讨论加锁、互斥、阻塞的synchronized。
Java代码    收藏代码
  1. public void add() {  
  2.     synchronized (this) {  
  3.         this.i++;  
  4.     }  
  5. }  

 这个几乎是家喻户晓了,在该线程的对象上加锁,拿到传国玉玺,挟天子以令诸侯,别的诸侯谁也别想下圣旨,皇帝在我(当前执行线程)手里呢。执行i++后该线程释放对此对象的持有锁,交出玉玺,也该让别人过过类似曹操的隐了吧。这就是所谓的互斥,保证了在同一个时间段,对同步对象进行加锁后,别人就在执行队列中等待着,再来一个君主,看到曹操还没爽够呢,得了,和刘备一起在执行队列中等着吧,等曹操爽够了,皇帝、玉玺没用了,交出锁,释放对象锁。根据队列的先进先出原则,按道理是该刘备抢到玉玺,也过把隐!

还有lock和unlock方法和synchronized功能类似。一般情况下使用的概率较少,因为得成对出现。

Java代码    收藏代码
  1. private Lock lock = new ReentrantLock();  
  2. public void add() {  
  3.     lock.lock();  
  4.     this.i++;  
  5.     lock.unlock();  
  6. }  

 volatile修饰变量,虽然减少了线程不安全的概率,但是呢不能从根本上完全解除。

Java代码    收藏代码
  1. volatile int i = 0;  

 因为用volatile修饰的变量,是直接在堆区进行操作,根本就不复制到操作工作栈。节省了每条代码的中间过程。

详细请查看http://www.ibm.com/developerworks/cn/java/j-jtp06197.html

4.  线程资源交互机制

线程之间除了互斥关系外,还有协作交互的关系。打个比喻,曹操假意汉献帝诏书,下诏让刘备去征讨袁术,刘备说:“曹公,没有汉献帝的玉玺,百姓不信是皇帝下的诏书,除非您将皇帝的玉玺给我,带到淮南,让百姓们也见见咱们是真正的奉诏讨贼!”。曹操说:“善”,之后将玉玺交出,刘备带着玉玺去征讨袁术这个伪皇帝,造成一种现象就是在刘备征讨成功前,曹操没有玉玺用,下不了诏书,刘备回来后归还玉玺给曹操,曹操才能继续挟天子以令诸侯。线程的协作交互机制也是大家耳熟能详的wait方法和notify方法。如下代码

Java代码    收藏代码
  1. package thread;  
  2.   
  3. /** 
  4.  *  
  5.  * @author liuyan 
  6.  */  
  7. public class TestNotify {  
  8.     public static void main(String[] args) {  
  9.   
  10.         TestNotify testNotify = new TestNotify();  
  11.   
  12.         ThreadB b = testNotify.new ThreadB();  
  13.           
  14.         System.out.println("b is start");  
  15.   
  16.         b.start();  
  17.           
  18. /* 
  19.         try { 
  20.             Thread.currentThread().sleep(5); 
  21.         } catch (InterruptedException e1) { 
  22.             // TODO Auto-generated catch block 
  23.             e1.printStackTrace(); 
  24.         } 
  25. */  
  26.           
  27.         synchronized (b) {  
  28.   
  29.             try {  
  30.   
  31.                 System.out.println("Waiting for b to complete");  
  32.                   
  33.                 // 暂时放弃对象锁,让主线程暂停,让ThreadB开始执行  
  34.                 b.wait();   
  35.   
  36.             } catch (InterruptedException e) {  
  37.   
  38.                 e.printStackTrace();  
  39.             }  
  40.   
  41.             System.out.println("Final Total is:" + b.total);  
  42.   
  43.         }  
  44.   
  45.     }  
  46.   
  47.     class ThreadB extends Thread {  
  48.   
  49.         int total;  
  50.   
  51.         public void run() {  
  52.   
  53.             synchronized (this) {  
  54.   
  55.                 System.out.println("ThreadB is running");  
  56.   
  57.                 for (int i = 0; i < 100; i++) {  
  58.                     total += i;  
  59.                 }  
  60.                   
  61.                 // 执行完毕,唤醒被暂停的线程  
  62.                 notify();   
  63.             }  
  64.         }  
  65.     }  
  66. }  

 在主线程启动新线程b,主线程与新线程同时run,继续往下走。主线程相当于曹操,线程b相当于刘备。主线程使用wait方法交出b对象的持有锁,等待线程b使用完成后释放对象锁,主线程才能继续拥有对象b的对象锁,继续往下走自己的路。

从严格意义上来讲,可能会出现线程b抢在主线程前抢到b的锁,执行,线程b用完后将锁交出去,问题是此时等待集合wait set并没有任何线程元素。之后主线程一直执行到wait操作等着那个永远不会唤醒它的那个“人”。各位可以将注释那段等待的代码释放,扩大此事件的发生概率,主线程一定会发生永远睡眠、永远等待被唤醒的郁闷状态,等待永远是痛苦的,尤其是这种没有结果的等待。因为主线程运行的优先级和资源抢占比新启动的线程要高,所以可以说不加主线程睡眠的代码片段,99.9999999%的几率不会出现以上那种郁闷现象。notify是随机在等待集合中跳出一个线程将其唤醒。notifyAll方法是将等待集合中所有的线程都唤醒。

5.  线程的运行状态

线程运行状态分为几个阶段:

新生阶段:尚未启动,还在酝酿准备启动的阶段,也就是还未start的阶段。

可运行状态:这是代表调用了start方法或者是线程被唤醒,线程就进入可运行状态,线程对象进入到可运行池中。

运行状态:线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一一种方式。

等待/阻塞/睡眠状态:这是线程有资格运行时它所处的状态。实际上这个三状态组合为一种,其共同点是:线程仍旧是活的,但是当前没有条件运行。换句话说,它是可运行的,但是如果某件事件出现,他可能返回到可运行状态。

死亡态:当线程的run()方法完成时就认为它死去。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

 

6.  总结

这次相当于又复习了一下J2SE方面的线程的运行原理和互斥、交互机制。无论在Java的哪个领域,多线程永远是个活跃的话题。在分布式Java系统应用也不例外,甚至对多线程研发人员的要求要苛刻得多。线程安全和多线程的调试也是十分热门的话题。写程序时刻有并发下该程序还能否正常运行、性能是否会很差的疑问思维总是很好的。

PS:写完后才意识到一件事,袁术称帝的时候是真玉玺!刘备就算那玉玺去征讨袁术,也是假玉玺。呵呵,各位不必认真,打个比方罢了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值