`

Thread详解(互斥和协作)

阅读更多
 

Thread详解二(互斥和协作)

分类: 读书笔记之java 330人阅读 评论(0) 收藏 举报

Java Monitor 从两个方面来支持线程之间的同步,即:互斥执行与协作。 Java 使用对象锁 ( 使用 synchronized 获得对象锁 ) 保证工作在共享的数据集上的线程互斥执行 , 使用 notify/notifyAll/wait 方法来协同不同线程之间的工作。这些方法在 Object 类上被定义,会被所有的 Java 对象自动继承。
实质上,Java 的 Object 类本身就是监视者对象,Java 语言对于这样一个典型并发设计模式做了内建的支持。不过,在 Java 里,我们已经看不到了我们在 C++ 一节所讨论的区域锁与条件变量的概念。下图很好地描述了 Java Monitor 的工作机理。

线程如果获得监视锁成功,将成为该监视者对象的拥有者。在任一时刻内,监视者对象只属于一个活动线程 (Owner) 。拥有者线程可以调用 wait 方法自动释放监视锁,进入等待状态。

 

线程间的监视锁机制是遵循java所谓的Happens-before原则的,其实内容也都很好理解,A一定要发生在B前面。

happens-before完整规则:
(1)同一个线程中的每个Action都happens-before于出现在其后的任何一个Action。
(2)对一个监视器的解锁happens-before于每一个后续对同一个监视器的加锁。
(3)对volatile字段的写入操作happens-before于每一个后续的同一个字段的读操作。
(4)Thread.start()的调用会happens-before于启动线程里面的动作。
(5)Thread中的所有动作都happens-before于其他线程检查到此线程结束或者Thread.join()中返回或者Thread.isAlive()==false。
(6)一个线程A调用另一个另一个线程B的interrupt()都happens-before于线程A发现B被A中断(B抛出异常或者A检测到B的isInterrupted()或者interrupted())。
(7)一个对象构造函数的结束happens-before与该对象的finalizer的开始
(8)如果A动作happens-before于B动作,而B动作happens-before与C动作,那么A动作happens-before于C动作。

 

上面提到了volatile字段,

volatile相当于synchronized的弱实现,也就是说volatile实现了类似synchronized的语义,却又没有锁机制。它确保对volatile字段的更新以可预见的方式告知其他的线程。
volatile包含以下语义:
(1)Java 存储模型不会对valatile指令的操作进行重排序:这个保证对volatile变量的操作时按照指令的出现顺序执行的。
(2)volatile变量不会被缓存在寄存器中(只有拥有线程可见)或者其他对CPU不可见的地方,每次总是从主存中读取volatile变量的结果。也就是说对于volatile变量的修改,其它线程总是可见的,并且不是使用自己线程栈内部的变量。也就是在happens-before法则中,对一个valatile变量的写操作后,其后的任何读操作理解可见此写操作的结果。
尽管volatile变量的特性不错,但是volatile并不能保证线程安全的,也就是说volatile字段的操作不是原子性的,volatile变量只能保证可见性(一个线程修改后其它线程能够理解看到此变化后的结果),要想保证原子性,目前为止只能加锁!

volatile通常在下面的场景(停止线程的标志位):

 

[java] view plaincopy
 
  1. volatile boolean done = false;  
  2.   
  3. …  
  4.   
  5.     while( ! done ){  
  6.         dosomething();  
  7.     }  


 

还有一个定义,原子操作,具体描述,多个线程执行一个操作时,其中任何一个线程要么完全执行完此操作,要么没有执行此操作的任何步骤,那么这个操作就是原子的。

所以原子操作是不需要进行同步的(synchronized)。

不是一行代码就表示他是原子操作,比如C++这个自增就不是,下面是原子操作的例子,

Reads and writes are atomic for reference variables and for most primitive variables (all types except long and double).读写基本变量,除了long和double
Reads and writes are atomic for all variables declared volatile (including long and double variables).加上volatile字段的读写基本变量。

 

 

1,synchronized关键字

在实际编程中,我们有两种方式实现同步,分别是同步方法(synchronized methods)或同步块(synchronized block/synchronized statement)。
 
同步方法是在方法前加synchronized, 如果该方法是static的,则认为锁是相对于Class的,其他线程操作该类的任何对象时,遇到static同步方法或者方法内同步该Class时,需要等待;若该方法不是static的,则认为锁是相对于自身对象(this)的,其他线程操作此对象时,遇到同步方法(非static),需要等待。

注意点

1. 可以认为锁是属于引用类型的, 同步的操作需要获取锁之后才进行,否则一直等待。编程时需注意锁(synchronized)的对象 
2. 线程在wait后会释放持有的锁。
3. 各线程同步时遵守先触发,先得锁原则(happens-before relationship)。 
4. 构造函数无法被synchronized。

 

2,wait(),notify(),notifyAll()。

如果条件不满足,则等待。当条件满足时,等待该条件的线程将被唤醒。在Java中,这个机制的实现依赖于wait/notify。等待机制与锁机制是密切关联的。

 

[java] view plaincopy
 
  1. synchronized(obj) {  
  2.     while(!condition) {  
  3.         obj.wait();  
  4.     }  
  5.     obj.doSomething();  
  6. }  

当线程A获得了obj锁后,发现条件condition不满足,无法继续下一处理,于是线程A就wait()。
在另一线程B中,如果B更改了某些条件,使得线程A的condition条件满足了,就可以唤醒线程A:

 

 

[java] view plaincopy
 
  1. synchronized(obj) {  
  2.     condition = true;  
  3.     obj.notify();  
  4. }  


 

需要注意的点

1. wait, notify, notifyAll必须在synchronized代码内。即该线程持有了某引用的锁时,wait, notify, notifyAll才可以被执行,否则,会报IllegalMonitorStateException

 

[java] view plaincopy
 
  1. // 会抛IllegalMonitorStateException异常, 因为没持有this的锁。  
  2. this.wait();  
  3.   
  4. synchronized(this) {  
  5.   
  6.     // 正确写法  
  7.     this.wait();  
  8.   
  9.     // 会抛IllegalMonitorStateException异常, 因为没有持有a对象的锁。  
  10.     this.a.wait();  
  11. }  

2. 执行wait()后会释放锁持有的锁,其他等待同步中的线程这时会持有该锁,并执行。 
3. wait()执行后,线程状态会变为disabled, 想继续执行除非以下事件中的一个发生: 
    a)其他线程在此同步的引用上执行了notify()或者notifyAll(),注意是和wait相同引用上执行的notify()。
    b)线程被其他线程中断,会报InterruptedException。
    c)wait可以指定timeout, 当timeout时间过去时。
 
4. wait继续执行需要重新获得该引用的锁,若有其他线程占有着此锁,则仍然无法恢复。 
5. notify是随机唤醒一个wait中的线程,notifyAll是把所有wait中的线程全部唤醒。notify的对象仍旧是其持有的锁的引用。 
6. 如果没有线程wait, notify将会被忽略。

 

 

3,wait和sleep的区别

sleep与wait的不同点是:sleep并不释放锁,并且sleep的暂停和wait暂停是不一样的。obj.wait会使线程进入obj对象的等待集合中并等待唤醒。但是wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException。
      如果线程A希望立即结束线程B,则可以对线程B对应的Thread实例调用interrupt方法。如果此刻线程B正在wait/sleep/join,则线程B会立刻抛出InterruptedException,在catch() {} 中直接return即可安全地结束线程。

 

4,并发程序可能会出现的问题。

死锁,多个线程之间互相等待。著名 的哲学家拿叉子事件。

还有两个不是很长见的问题。饥饿和活锁,饥饿指的是要等一个锁很长很长时间,像当前占有锁的同步方法中执行了很复杂的操作,导致其他要等待太久。活锁就更有趣一些,像两个线程之间互相响应,谦让。像两个人相向而行,都想互相避开,但是A向左,B向右;A向右,B向左,还是碰到一起。

超时的等待和死锁对于用户来说就没有什么太大的区别了。

 

 

参考:http://wenjuema.iteye.com/blog/660705

http://blog.csdn.net/aming2006/article/details/4463979

http://www.blogjava.net/xylz/archive/2010/07/03/325168.html

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics