国内最全IT社区平台 联系我们 | 收藏本站
华晨云阿里云优惠2
您当前位置:首页 > 互联网 > 漫谈并发编程(三):共享受限资源

漫谈并发编程(三):共享受限资源

来源:程序员人生   发布时间:2014-11-07 09:02:59 阅读次数:1823次

解决同享资源竞争

1个不正确的访问资源示例
     斟酌下面的例子,其中1个任务产生偶数,而其他任务消费这些数字。这里,消费者任务的唯1工作就是检查偶数的有效性。
     我们先定义1个偶数生成器的抽象父类。
public abstract class IntGenerator { private volatile boolean canceled = false; public abstract int next( ); public void cancle( ) { canceled = true; } public boolean isCanceled( ) { return canceled; } }
     下面定义消费者任务。
public class EvenChecker implements Runnable { private IntGenerator generator; private final int id; public EvenChecker(IntGenerator g , int ident) { generator = g; id = ident; } public void run() { while( !generator.isCanceled() ) { int val = generator.next(); if(val % 2 != 0) { System.out.println(val + "not even!"); generator.cancle(); } } } public static void test(IntGenerator gp, int count) { System.out.println("Press Control -C to exit"); ExecutorService exec = Executors.newCachedThreadPool(); for(int i = 0; i < 10;i++) exec.execute(new EvenChecker(gp, i)); exec.shutdown(); } public static void test(IntGenerator gp) { test(gp, 10); } }
public class EvenGenerator extends IntGenerator { private int currentEvenValue = 0; public int next() { ++currentEvenValue; ++currentEvenValue; return currentEvenValue; } public static void main(String []args) { EvenChecker.test(new EvenGenerator()); } }
/* output:
Press Control -C to exit
15243not even!
15245not even!
     这个程序终究失败,由于各个EvenChecker任务在EvenGenerator处于"不恰当的"状态时,仍能够访问其中的信息。比以下面场景:1. A线程履行1条自加操作后放弃时间片,B线程接着履行两次自加及输出。 2. A线程在自加后return语句前放弃时间片,B线程完成1次自加,然后A又履行,在这类情况下依然会返回奇数。

解决竞争的方法
     基本上所有的并发模式在解决线程冲突问题的时候,都是采取序列化访问同享资源的方案。这意味着在给定时刻只允许1个任务访问同享资源,通常这是通过在代码前面加上1条锁语句来实现的,这就使得在1段时间内只有1个任务可以运行这段代码。由于锁语句产生了1种相互排挤的效果,所以这类机制常常被称为互斥量(mutex)
     Java以提供关键字synchronized的情势,为避免资源冲突提供了内置支持。当任务要履行被synchronized关键字保护的代码片断的时候,它将检查锁是不是可用,然后获得锁,履行代码,释放锁。
     要控制对同享资源的访问,得先把它包装进1个对象,然后把所有要访问这个资源的方法标记为synchronized,如果某个任务处于1个对标记为synchronized的方法的调用中,那末在这个线程从该方法返回之前,其他所有要调用类中任何标记为synchronized方法的线程都将被阻塞。
     下面是声明synchronized方法的方式:
synchronized void f() {/*... */}
synchronized void g(){/*....*/}
     所有对象都自动含有单1的锁(也称为监视锁)。当在对象上调用其任意synchronied方法的时候,此对象都被加锁,这时候该对象上的其他synchronized方法只有等到前1个方法调用终了并释放了锁以后才能被调用。在使用并发时,将域设置为private是非常重要的,这是1种保证,保证没有其他任务可以直接访问到该域。
     1个任务可以屡次取得对象的锁。如果1个方法在同1个对象上调用了第2个方法,后者又调用了同1个对象上的另外一个方法,就会产生这类情况。JVM负责跟踪对象被加锁的次数。如果1个对象被解锁(即锁被完全释放),其计数变成0。
     针对每一个类,也有1个锁(作为类的class对象的1部份),所以synchronized static方法可以在类的范围内避免对static数据的并发访问。
     总结来讲,在多个线程访问同1对象时,如果会出现线程竞速问题(所有线程只读则不会出现此状态),解决办法是把这个同享对象转变成线程安全对象(或使被调用的方法是线程安全的),或将所有线程对该资源的访问序列化(用锁在线程本身任务内同步)。如果对该资源的访问是复合操作,即便同享对象本身是线程安全的,也没法保证数据的1致性,例如:if( put(**))这类操作,就必须要把复合操作全部包括在锁内,对存在多个对象的同享,如果相互之间有状态的关联,这类处理方式仍然有效。

同步控制EvenGenerator     
     通过在EvenGenerator.java中加入synchronized关键字,可以避免不希望的线程访问:
public class SynchronizedEvenGenerator extends IntGenerator { private int currentEvenValue = 0; @Override public synchronized int next() { ++currentEvenValue; Thread.yield(); ++currentEvenValue; return currentEvenValue; } public static void main(String[] args) { EvenChecker.test(new SynchronizedEvenGenerator()); } }

使用显式的Lock对象
     java.util.concurrent类库中还包括有显式的互斥机制。Lock对象必须被显式的创建、锁定和释放。因此,它与内建的锁情势相比,代码缺少优雅性,但更加灵活。下面是采取Lock重写的EvenGenerator。
public class MutexEvenGenerator extends IntGenerator { public int currentEvenValue = 0; private Lock lock = new ReentrantLock(); @Override public int next() { lock.lock(); try { ++currentEvenValue; ++currentEvenValue; return currentEvenValue; } finally { lock.unlock(); } } public static void main(String []args) { EvenChecker.test( new MutexEvenGenerator( )); } }
     MutexEvenGenerator添加了1个互斥调用的锁,并使用lock()和unlock()方法在next()内部创建了临界资源。当你在使用Lock对象时,有1些原则需要被记住:你必须放置在finally子句中带有unlock()的try-finally语句,以免该锁被无穷期锁住。注意,return语句必须在try子句中出现,以确保unlock()不会过早产生,从而将数据暴露给第2个任务。
     Lock比synchronized灵活体现在:可以在同享对象中使用多个Lock来分隔操作,以提高并发度。除此以外,Lock可以支持你尝试获得锁且终究获得锁失败,或尝试着获得锁1段时间,然后放弃它。
public class MyMutexTest { private static Lock lock = new ReentrantLock(); public static void main(String args[]) { new Thread( new Runnable() { public void run() { lock.lock(); while(true); } }).start(); new Thread(new Runnable() { public void run() { if( lock.tryLock() == false ) { System.out.println("acquire lock failed 1"); } } }).start();; new Thread( new Runnable() { public void run() { try { if(lock.tryLock(2, TimeUnit.SECONDS) == false) { System.out.println("acquire lock failed 2"); }} catch (InterruptedException e) { } } }).start(); } }
/*output:
acquire lock failed 1
acquire lock failed 2

原子性与可见性

     原子性可以利用于除long和double以外的所有基本类型之上的"简单操作"。对读取和写入除long和double以外的基本类型变量这样的操作,可以保证它们会被当作不可分(原子)的操作来操作内存。但是JVM可以将64位(long和double变量)的读取和写入当作两个分离的32位操作来履行,这就产生了在1个读取和写入操作中间产生上下文切换,从而致使不同的任务可以看到不正确结果的可能性(这有时被称为字撕裂,由于你可能会看到部份被修改过的数值)。但是,当你定义long或double变量时,如果使用valatile关键字,就会取得(简单的赋值与返回操作的)原子性。
     对可见性的讨论,从下面的例子开始:
public class MyVisibilityTest implements Runnable{ private static boolean mv = false; private static int integer = 0; @Override public void run() { while(true) { if(mv == true) { System.out.println(integer); return; } } } public static void main(String []args) { new Thread(new MyVisibilityTest()).start(); integer = 34; mv = true; } }
     上面的程序运行效果,有时很久才打印出34,有时乃至匪夷所思的打印出0,这是由于对象的可见性的原因。
在多处理器系统上,相对单处理器而言,可见性问题要突兀的多。1个任务做出的修改,即便在不中断的意义上讲是原子性的,对其他任务也多是不可见的(例如,修改只是暂时性地存储在本地处理器的缓存中)。

volatile的作用
     把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是同享的,因此不会将该变量上的操作与其他内存操作1起重排序。volatile变量不会被缓存在寄存器或对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
     如果我们将上面例子中的成员变量声明为volatile类型,则程序将"正常"输出。
     下面借用《Java并发编程实战》中对volatile类型使用处景的描写。
  • 对变量的写入操作不依赖变量确当前值(如count++),或你能确保只有单个线程更新变量的值
  • 该变量不会与其他状态变量1起纳入不变性条件中
  • 在访问变量时不需要加锁(由于volatile只确保可见性)
     个人使用经验:对1个对象,不管是复杂类型还是基本类型如果在该对象上存在多个线程间的复合操作(如count++、if( ){do()}),则不应在此对象上使用volatile(而是直接使用同步机制保证线程安全性)。在满足上述条件的基础上,如果该变量是简单类型,则可使用volatile保证其可见性,由于简单类型具有原子性(double、long使用volatile后也具有),则对该变量的访问是线程安全的。如果该变量是复合类型,如果对该变量的写操作只是将援用直接修改,那末也能够可以volatile保证写操作的可见性,在此基础上,对该复合类型的操作也就是线程安全的了。
     对基本类型来讲,原子性+可见性 = 该变量的线程安全性,就算变量本身是线程安全的,对该变量的复合操作也会致使线程不安全。

原子类

     Java SE5引入了诸如AtomicInteger、AtomicLong、AtomicReference等特殊的原子性变量类。它们提供了下面情势的原子性条件更新操作:
     boolean compareAndSet(expectedValue, updateValue)
     这些类被调剂为可以操作在机器级别上的原子性。如果在只同享1个对象的条件下,它为你提供了1种将线程间的复合操作转为线程安全操作的机会。
     下面用利用AtomicInteger重写MutexEvenGenerator.java。
public class AtomicEvenGenerator extends IntGenerator { private AtomicInteger currentEvenValue = new AtomicInteger(0); public int next() { return currentEvenValue.addAndGet(2); } public static void main(String args[]) { EvenChecker.test(new AtomicEvenGenerator()); } }

临界区

     有时,你只是希望避免多个线程同时访问方法内部的部份代码而不是避免访问全部方法,通过这类方式分离出来的代码段被称为临界区(critical section),它也使用synchronized关键字建立。这里,synchronized被用来指定某个对象,此对象的锁被用来对花括号内的代码进行同步控制:
     synchronized(syncObject) {
          ....
     }
     这也被称为同步控制块;在进入此段代码前,必须得到syncObject对象的锁。如果其他线程已得到这个锁,那末就得等到锁被释放以后,才能进入临界区。
     使用临界区的用法其实和Lock用法极为类似,但Lock更加灵活。二者都得显式的利用1个对象,synchronized是使用其他对象,Lock是使用本身,相比之下,synchronized更加晦涩。Lock可以在1个函数中加锁,另外一个函数中解锁,临界区做不到,但这也给Lock带来使用风险。sychronized怎样才能不使用额外的1个对象进行加锁?办法就是对this加锁,如果多个线程履行的是同1任务,使用sychronized是不错的选择,由于它可以免你显式的定义和使用1个Lock。

线程本地贮存

     避免任务在同享变量上产生冲突的第2种方式是根除对变量的同享。线程本地贮存(TLS)是1种自动化机制,可以为使用相同变量的每一个不同的线程都创建不同的贮存。因此,如果你有5个线程都要使用变量x所表示的对象,那线程本地贮存就会生成5个用于x的不同的贮存块。主要是,它们使得你可以将状态与线程关联起来。
     线程本地贮存不是1种线程间同享资源的机制,它主要作用是作为对每一个线程本身状态的贮存,比如放在上下文环境中,因此1般使用为静态域贮存。创建和管理线程本地贮存可以由java.lang.ThreadLocal类来实现,以下:
class ContextThread { private static ThreadLocal<Integer> value = new ThreadLocal<Integer>(); public static void setInteger(Integer value){ ContextThread.value.set(value); } public static Integer getInteger() { return value.get(); } public static void increment() { value.set(value.get() + 1); } } public class ThreadLocalTest { public static void main(String []args) { for(int i = 0; i < 5; i++) { new Thread(new Runnable() { @Override public void run() { ContextThread.setInteger(0); ContextThread.increment(); System.out.println( ContextThread.getInteger() ); } }).start(); } } }
/*output 
1
1
1
1
1
     在创建ThreadLocal时,你只能通过get()和set()方法来访问该对象的内容,其中,get()方法将返回与其线程相干的对象的副本,而set()会将参数插入到为其线程贮存的对象中。


生活不易,码农辛苦
如果您觉得本网站对您的学习有所帮助,可以手机扫描二维码进行捐赠
程序员人生
------分隔线----------------------------
分享到:
------分隔线----------------------------
关闭
程序员人生