Java的多线程编程模型

原文地址:http://blog.csdn.net/sunnydogzhou/article/details/6425686

 

Java的多线程编程模型1

Java多线程的类库封装在java.util.concurrent.*中,java1.4到1.5的变化就是引入了这个支持并发编程的类库。首先得感谢下大名鼎鼎人类库作者Doug Lea,牛人总是让人膜拜的。

1 什么是线程安全
A class is thread-safe if it behaves correctly when accessed from multiple threads, regardless of the scheduling or interleaving of the execution of those threads by the runtime environment, and with no additional synchronization or other coordination on the part of the calling code.

译成中文意思就是
多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替运行,并且不需要额外的同步及在调用方代码不必做其他的协调,这个类的行为仍然是正确的,那么这个类就是线程安全的。 

从上面的解释可以看出
无状态对象永远是线程安全的 

2 什么是原子性
多个线程执行一个操作时,其中任何一个线程要么完全执行完此操作,要么没有执行此操作的任何步骤,那么这个操作就是原子的

3 为什么线程不安全
看一个线程不安全的经典例子:
package zl.study.concurrency;

public class ReorderingDemo {
    static int x = 0, y = 0, a = 0, b = 0;

    public static void main(String[] args) throws Exception {

        for (int i = 0; i < 100; i++) {
            x=y=a=b=0;
            Thread one = new Thread() {
                public void run() {
                    a = 1;
                    x = b;
                }
            };
            Thread two = new Thread() {
                public void run() {
                    b = 1;
                    y = a;
                }
            };
            one.start();
            two.start();
            one.join();
            two.join();
            System.out.println(x + " " + y);
        }
    } 

}
在这个例子中,如果你在一个单CPU的java测试环境中做测试,你可能只会得到一种结果(0,1),然而真的只有这样一种情况么?显然不是。JVM并不能保证线程的执行顺序,即使看起来你无数次测试都是(0,1),然而切换到别的环境中,出现其它结果(1,0)仍然是无法避免的。而在多CPU的结构中,就更不能保证了。线程可能在不同的CPU上执行,从而(0,0),(1,1)都是可能的。

4 为什么会出现这种情况
导致出现这些情况的原因有很多
Java内存分配
    寄存器:我们在程序中无法控制
    栈:存放基本类型的数据和对象的引用,但对象本身不存放在栈中,而是存放在堆中
    堆:存放用new产生的数据
    静态域:存放在对象中用static定义的静态成员    
    常量池:存放常量,比如.class信息,String

编译器优化
    调整语句执行顺序
    变量值存于寄存器而不是内存中
CPU自身的优化
    并行或者按其他顺序执行
    CPU本身的Cache会延迟变量的值刷新到内存的时间

 

 

 

Java的多线程编程模型2--怎样才线程安全

 

在Java多线程编程模型1里面讲到了为什么线程不安全,那怎样才能做到线程安全了?

 

先来看线程工作是跟内存是怎么打交道的。

在并发的线程中,分为主内存和工作内存,主内存就是程序分配的内存,工作内存就是线程所占的内存。线程可能在工作内存中存储了某些主内存对象的副本。当线程操作某个主内存的对象时,先从主内存中将变量的值拷贝到工作内存中,然后在工作内存中改变这个值,最后将这个值刷到主内存中。

 

在<<java concurrency in pratise>>中提出了线程安全的思路

1) 不要在线程间共享变量

2) 如果不行,就要final变量

3) 还是不行,就用volatile或其它并发控制。

其实基本的思路是尽量减少共享变量,如果实在要用,则需要并发控制。

 

那非要用的时候怎么办了,这个时候就需要拿出happens-before规则来检查多线程的程序了。

 

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

 

如果你的程序能够满足上面的发则,那么恭喜你,不会在出现并发的问题了。

 


Java的多线程编程模型3 -- 在1.5之前怎么并发

 

在java1.5之前,java在并发上面的建树不多,只提供了为数不多的方式来提供提高并发的效率。

其中synchronized关键字是使用最多的,这个看似简单的锁方式,效率奇差,所以那会,java程序员对于c++程序员的在java并发上的诟病总是无力回击。

 

在1.5之前,java提供的并发容器Vector,我们来看下具体的实现java.util.Vector

 

[java]   view plain copy
  1. public   class  Vector<E>  
  2.     extends  AbstractList<E>  
  3.     implements  List<E>, RandomAccess, Cloneable, java.io.Serializable  
  4. {  
  5.     public   synchronized   void  copyInto(Object[] anArray) {  
  6.     System.arraycopy(elementData, 0 , anArray,  0 , elementCount);  
  7.     }  
  8.     public   synchronized   void  trimToSize() {  
  9.     modCount++;  
  10.     int  oldCapacity = elementData.length;  
  11.     if  (elementCount < oldCapacity) {  
  12.             elementData = Arrays.copyOf(elementData, elementCount);  
  13.     }  
  14.     }  
  15.     public   synchronized   void  ensureCapacity( int  minCapacity) {  
  16.     modCount++;  
  17.     ensureCapacityHelper(minCapacity);  
  18.     }  
  19. ... ...  
  20. }  

 

从中可以看出,Vector是把所有的方法前面的加上了synchronized关键字

 

在来看另外的一类静态方法,这类容器可以把List在包装一层,让后就可以作为并发的容器

[java]   view plain copy
  1. List list = Collections.synchronizedList( new  ArrayList());  
  2.     ...  
  3. synchronized (list) {  
  4.     Iterator i = list.iterator(); // Must be in synchronized block   
  5.     while  (i.hasNext())  
  6.         foo(i.next());  
  7. }  

 

仔细分析会发现

[java]   view plain copy
  1. public   static  <T> List<T> synchronizedList(List<T> list) {  
  2. urn (list instanceof  RandomAccess ?  
  3.             new  SynchronizedRandomAccessList<T>(list) :  
  4.             new  SynchronizedList<T>(list));  
  5. }  

原来Collections.synchronizedList(List<T> list))这个方法最终会新建一个SynchronizedList<E>,它是继承自SynchronizedCollection<E>,来看SynchronizedList<E>的构造函数,

[java]   view plain copy
  1. SynchronizedList(List<E> list) {  
  2.     super (list);  
  3.     this .list = list;  
  4. }  

看看父类的详细的构成

 

[java]   view plain copy
  1.     static   class  SynchronizedCollection<E>  implements  Collection<E>, Serializable {  
  2.     // use serialVersionUID from JDK 1.2.2 for interoperability   
  3.     private   static   final   long  serialVersionUID = 3053995032091335093L;  
  4.     final  Collection<E> c;   // Backing Collection   
  5.     final  Object mutex;      // Object on which to synchronize   
  6.     SynchronizedCollection(Collection<E> c) {  
  7.             if  (c== null )  
  8.                 throw   new  NullPointerException();  
  9.         this .c = c;  
  10.             mutex = this ;  
  11.         }  
  12.     SynchronizedCollection(Collection<E> c, Object mutex) {  
  13.         this .c = c;  
  14.             this .mutex = mutex;  
  15.         }  
  16.     public   int  size() {  
  17.         synchronized (mutex) { return  c.size();}  
  18.         }  
  19.     public   boolean  isEmpty() {  
  20.         synchronized (mutex) { return  c.isEmpty();}  
  21.         }  
  22.     public   boolean  contains(Object o) {  
  23.         synchronized (mutex) { return  c.contains(o);}  
  24.         }  
  25.     public  Object[] toArray() {  
  26.         synchronized (mutex) { return  c.toArray();}  
  27.         }  
  28.     public  <T> T[] toArray(T[] a) {  
  29.         synchronized (mutex) { return  c.toArray(a);}  
  30.         }  
  31. ... ...  
  32. }  

 

怎样,是不是有一种恍然大悟的感觉,原来在构造函数里面弄了一个 Object mutx = this,让后所有的方法在调用的时候都synchronized(mutex)

 

相同的方法有

    public static <T> Collection<T> synchronizedCollection(Collection<T> c)

    public static <T> List<T> synchronizedList(List<T> list)

    public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)

    public static <T> Set<T> synchronizedSet(Set<T> s)

    public static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m)

    public static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s)

 

 

Java的多线程编程模型4--synchronized

 

在Java1.5之前,synchronized应该是最常用的java支持并发手段。那synchronized是怎么做到的了,从java1.0开始,java中的每个对象就一个内部锁。如果一个类的方法被synchronized关键字所修饰,那么这个对象的锁将保护整个方法。

举例来说:

public synchronized void method(){

    method body

}

等价于

public void method(){

    this.intrinsicLock.lock();

    try{

        method body;

    }finally(){

        this.intrinsicLock.unlock();

    }

}

 

从上面的代码示例可以看出,synchronized的使用方式是比较简单的。这也导致了大量的初学者在碰到java编程的时候落入陷阱里,认为既然synhronized可以搞定一切,那么不管三七二十一,只要有并发可能性的地方,就加上synchronized的关键字,这显然是不对的。在java对象中,这个java对象只有这一个内部锁,其中一个synchronized方法获取到了这个锁,另外一个synchronized方法的调用将被阻塞。

class sync{

    public synchronized void methodA(){};

    public synchronized void methodB(){};

    ... ...

 

}

methodA 和methodB在初始就是互斥的,如果methodA和methodB进入互相等待,就很容易出现死锁的情况。那如果碰到这种情况,应该怎么做了?常用的方式是在方法内部新建一个无意义的对象,然后对这个无意义的对象加锅。

[java]   view plain copy
  1. package  zl.study.concurrency.synchronize;  
  2. public   class  Sync {  
  3.     private   int  i;  
  4.       
  5.     public   void  plus(){  
  6.         Object dummy = new  Object();  
  7.         synchronized (dummy){  
  8.             i++;  
  9.         }  
  10.     }  
  11.       
  12.     public   void  minus(){  
  13.         Object dummy = new  Object();  
  14.         synchronized (dummy){  
  15.             i--;  
  16.         }         
  17.     }  
  18. }  

 

另外需要注意的是将静态类声明为synchronized方法也是合法的。举例来说,如果Sync有一个static synchronized方法,那么这个方法被调用时,bank.class这个类对象本身在jvm中将被锁住。

 

 

Java的多线程编程模型5--从AtomicInteger开始

 

AtomicInteger,一个提供原子操作的Integer的类。在Java语言中,++i和i++操作并不是线程安全的,在使用的时候,不可避免的会用到synchronized关键字。而AtomicInteger则通过一种线程安全的加减操作接口。

来看AtomicInteger提供的接口。

//获取当前的值

public final int get()

//取当前的值,并设置新的值

 public final int getAndSet(int newValue)

//获取当前的值,并自增

 public final int getAndIncrement()

//获取当前的值,并自减

public final int getAndDecrement()

//获取当前的值,并加上预期的值

public final int getAndAdd(int delta)

... ...

我们在上一节提到的CAS主要是这两个方法

    public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

    public final boolean weakCompareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

这两个方法是名称不同,但是做的事是一样的,可能在后续的java版本里面会显示出区别来。

详细查看会发现,这两个接口都是调用一个unsafe的类来操作,这个是通过JNI实现的本地方法,细节就不考虑了。

 

下面是一个对比测试,我们写一个synchronized的方法和一个AtomicInteger的方法来进行测试,直观的感受下性能上的差异

[java]   view plain copy
  1. package  zl.study.concurrency;  
  2. import  java.util.concurrent.atomic.AtomicInteger;  
  3. public   class  AtomicIntegerCompareTest {  
  4.     private   int  value;  
  5.       
  6.     public  AtomicIntegerCompareTest( int  value){  
  7.         this .value = value;  
  8.     }  
  9.       
  10.     public   synchronized   int  increase(){  
  11.         return  value++;  
  12.     }  
  13.       
  14.     public   static   void  main(String args[]){  
  15.         long  start = System.currentTimeMillis();  
  16.           
  17.         AtomicIntegerCompareTest test = new  AtomicIntegerCompareTest( 0 );  
  18.         for int  i= 0 ;i<  1000000 ;i++){  
  19.             test.increase();  
  20.         }  
  21.         long  end = System.currentTimeMillis();  
  22.         System.out.println("time elapse:" +(end -start));  
  23.           
  24.         long  start1 = System.currentTimeMillis();  
  25.           
  26.         AtomicInteger atomic = new  AtomicInteger( 0 );  
  27.           
  28.         for int  i= 0 ;i<  1000000 ;i++){  
  29.             atomic.incrementAndGet();  
  30.         }  
  31.         long  end1 = System.currentTimeMillis();  
  32.         System.out.println("time elapse:" +(end1 -start1) );  
  33.           
  34.           
  35.     }  
  36. }  

结果

time elapse:31
time elapse:16
由此不难看出,通过JNI本地的CAS性能远超synchronized关键字

 

 

Java的多线程编程模型5--Java中的CAS理论

 

CAS,compare and swap的缩写,中文翻译成比较并交换。

我们都知道,在java语言之前,并发就已经广泛存在并在服务器领域得到了大量的应用。所以硬件厂商老早就在芯片中加入了大量直至并发操作的原语,从而在硬件层面提升效率。在intel的CPU中,使用cmpxchg指令。

在Java发展初期,java语言是不能够利用硬件提供的这些便利来提升系统的性能的。而随着java不断的发展,Java本地方法(JNI)的出现,使得java程序越过JVM直接调用本地方法提供了一种便捷的方式,因而java在并发的手段上也多了起来。而在Doug Lea提供的cucurenct包中,CAS理论是它实现整个java包的基石。

1. CAS:

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该 位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前 值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”

通常将 CAS 用于同步的方式是从地址 V 读取值 A,执行多步计算来获得新 值 B,然后使用 CAS 将 V 的值从 A 改为 B。如果 V 处的值尚未同时更改,则 CAS 操作成功。

类似于 CAS 的指令允许算法执行读-修改-写操作,而无需害怕其他线程同时 修改变量,因为如果其他线程修改变量,那么 CAS 会检测它(并失败),算法 可以对该操作重新计算。

2.非阻塞算法

如果每个线程在其他线程任意延迟(或甚至失败)时都将持续进行操作,就可以说该算法是无等待的。与此形成对比的是,无锁定算法要求仅 某个线程 总是执行操作。(无等待的另一种定义是保证每个线程在其有限的步骤中正确计 算自己的操作,而不管其他线程的操作、计时、交叉或速度。

3.用CAS来实现非阻塞算法

[java]   view plain copy
  1. package  zl.study.concurrency;  
  2. /**  
  3.  * AtomicInteger的模拟类,主要是用来测试CAS和非阻塞方式加锁  
  4.  * @author peter  
  5.  *  
  6.  */   
  7. public   class  SimulatedAtomicInteger {  
  8.     private   int  value=  0 ;  
  9.       
  10.     private   int  get(){  
  11.         return   this .value;  
  12.     }  
  13.     /**  
  14.      * 模拟CAS  
  15.      * @param current  
  16.      * @param next  
  17.      * @return  
  18.      */   
  19.     private   synchronized   boolean  compareAndSet( int  current, int  next){  
  20.         return  current == next? true : false ;  
  21.     }  
  22.     /**  
  23.      * 模拟非阻塞算法  
  24.      * @return  
  25.      */   
  26.     public   final   int  incrementAndGet() {  
  27.         for  (;;) {  
  28.             int  current = get();  
  29.             int  next = current +  1 ;  
  30.             if  (compareAndSet(current, next))   
  31.                 return  next;  
  32.         }  
  33.     }  
  34. }  

需要注意的是这个方法中的CAS是在jav代码实现的,这个并没有包含内存位置。在concurrent包中,是JNI的方式,内存位置也作为参数传入这个JNI方法中,在后面碰到了在做详细的介绍

在后面介绍java 5提供的并发工具时,我们还能经常看到类似于SimulatedAtomicInteger得写法,大家可以好好体会!


你可能感兴趣的:(java)