Java编程思想 之 共享受限资源

并发程序可以同时做多件事情,但是, 两个或多个线程彼此互相干涉的问题也就出现了。以下例子出自<< java编程思想第4版>>一书的并发部分.

1.不正确的访问资源

考虑以下例子,其中的一个任务产生偶数,而其他任务消费这些数字.这里,消费者任务的唯一工作就是检查偶数的有效性.

//产生偶数的抽象类
public abstract class IntGenerator {
  private volatile boolean canceled = false;
  public abstract int next();
  // 退出
  public void cancel() { canceled = true; }
  //判断是否不为偶数
  public boolean isCanceled() { return canceled; }
}

为了保持可视性canceled 用volatile 修饰.

//消费者任务
public class EvenChecker implements Runnable {
  private IntGenerator generator;
  //线程的ID
  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.cancel(); // Cancels all EvenCheckers
      }
    }
  }
  // 执行线程
  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 < count; i++)
      exec.execute(new EvenChecker(gp, i));
    exec.shutdown();
  }
  // 默认10个线程在运行
  public static void test(IntGenerator gp) {
    test(gp, 10);
  }
}

定义完任务后,测试类如下:

//产生偶数的类
public class EvenGenerator extends IntGenerator {
  private int currentEvenValue = 0;
  public int next() {
    ++currentEvenValue; // Danger point here!
    ++currentEvenValue;
    return currentEvenValue;
  }
  public static void main(String[] args) {
    EvenChecker.test(new EvenGenerator());
  }
}
//output
Press Control-C to exit
2363 not even!
2365 not even!
2361 not even!

结果可以看出,next方法是线程不安全的.当一个任务在执行第一个++currentEvenValue的递增操作之后,但没有执行第二个操作之前,另一个任务执行了next()方法,使该值处于”不恰当”的状态.
这里递增操作很重要,它自身也需要多个操作,并且在递增过程中任务可能会被线程机制挂起.以此,在Java中递增不是原子性的操作.

2.解决共享资源竞争

2.1 synchronized关键字

基本上所有的并发模型在解决线程冲突问题的时候,都是采用序列化访问共享资源的方案.在java中,使用synchronized关键字来防止资源冲突,当任务执行到被synchronized关键字保护的代码片段的时候,它将检查锁是否可用,然后获取锁,执行代码,释放锁.

改进EvenGenerator类,加入synchronized关键字.

public class
SynchronizedEvenGenerator extends IntGenerator {
  private int currentEvenValue = 0;
  public synchronized int next() {
    ++currentEvenValue;
    Thread.yield(); // Cause failure faster
    ++currentEvenValue;
    return currentEvenValue;
  }
  public static void main(String[] args) {
    EvenChecker.test(new SynchronizedEvenGenerator());
  }
}
//output
Press Control-C to exit

其中的Thread.yield()是切换其他的线程,在这里是增加出错的几率.结果看出,程序一直在运行,永不终止.通过这种方式,任何时刻只有一个任务可以通过由互斥量看护的代码.

2.2 Lock对象

Lock对象必须被显示的创建,锁定和释放.因此,它与synchronized相比,代码缺乏优雅性.
但在性能方面,Lock通常会比使用synchronized要高效一些.

修改上述的EvenGenerator,如下:

public class MutexEvenGenerator extends IntGenerator {
  private int currentEvenValue = 0;
  private Lock lock = new ReentrantLock();
  public int next() {
    lock.lock();
    try {
      ++currentEvenValue;
      Thread.yield(); // Cause failure faster
      ++currentEvenValue;
      return currentEvenValue;
    } finally {
      lock.unlock();
    }
  }
  public static void main(String[] args) {
    EvenChecker.test(new MutexEvenGenerator());
  }
}

这里需要注意的是,lock.unlock()必须放置在finally子句中,而且return语句必须在try子句中出现,以确保unlock()不会过早发生,从而将数据暴露给了第二个任务.

3.原子性与可视性

3.1 原子性

原子性可以应用于除了long和double之外的所有基本类型(不可分的操作)之上的”简单操作”.但是JVM可以将64位(long和double变量)的读取和写入当做两个分离的32位操作来执行,这就产生了在一个读取和写入操作中间发生上下文切换,从而导致不同任务可以看到不正确结果的可能性.当定义long和double变量时,如果使用volatile关键字,就会获得原子性.

3.2 可视性

一个任务做出的修改,即使在不中断的意义上讲是原子性的,对其他任务也可能是不可视的(例如,修改只是暂时性的存储在本地处理器的缓存中).
volatile关键字确保了应用中的可视性,如果一个域声明是volatile的,那么只要对这个域产生了写操作,那么所有读操作都将看到这个修改,因为,volatile域会立即被写入到主存中,而读取操作就发生在主存中.如果一个域完全由synchronized方法或语句防护,那就不必将其设置为是volatile的了.
如果将一个域定义为volatile的,那么就会告诉编译器不要执行任何移除读取和写入操作的优化,这些操作的目的是用线程中的局部变量维护队这个域的精确同步.但是,volatile并不能对递增操作产生影响.

4.临界区

临界区也称为同步控制块.如下类:

class PairManager2 extends PairManager {
  public void increment() {
    Pair temp;
    synchronized(this) {
      p.incrementX();
      p.incrementY();
      temp = getPair();
    }
    store(temp);
  }
}

这里的this表示使用该对象表示一把锁,如果使用PairManager2.class的话,就是使用该类的字节码表示一把锁.

也可以使用Lock对象创建临界区:

class ExplicitPairManager2 extends PairManager {
  private Lock lock = new ReentrantLock();
  public void increment() {
    Pair temp;
    lock.lock();
    try {
      p.incrementX();
      p.incrementY();
      temp = getPair();
    } finally {
      lock.unlock();
    }
    store(temp);
  }
}

5.ThreadLocal

防止任务在共享资源上产生冲突的第二种方式是根除对变量的共享.线程本地存储是一种自动化机制,可以为使用相同变量的每个不同的线程都创建不用的存储.
示例如下:

//任务
class Accessor implements Runnable {
  //线程ID
  private final int id;
  public Accessor(int idn) { id = idn; }
  public void run() {
    while(!Thread.currentThread().isInterrupted()) {
      ThreadLocalVariableHolder.increment();
      System.out.println(this);
      //线程切换
      Thread.yield();
    }
  }
  public String toString() {
    return "#" + id + ": " +
      ThreadLocalVariableHolder.get();
  }
}


public class ThreadLocalVariableHolder {
  //本地线程类
  private static ThreadLocal<Integer> value =
    new ThreadLocal<Integer>() {
      private Random rand = new Random(47);
      protected synchronized Integer initialValue() {
        return rand.nextInt(10000);
      }
    };
   //在ThreadLocal保存值
  public static void increment() {
    value.set(value.get() + 1);
  }
  //在ThreadLocal取值
  public static int get() { return value.get(); }

  public static void main(String[] args) throws Exception {
    ExecutorService exec = Executors.newCachedThreadPool();
    for(int i = 0; i < 5; i++)
      exec.execute(new Accessor(i));

    TimeUnit.SECONDS.sleep(3);  // Run for a while
    exec.shutdownNow();         // All Accessors will quit
  }
}
//output
#3: 57560
#1: 56584
#1: 56585
#1: 56586
#3: 57561
#3: 57562
#0: 63881
#0: 63882
#0: 63883
...
...

ThreadLocal对象通常当作静态域存储,其中的incremeng()和get()方法都不是synchronizedde,因为ThreadLocal保证不会出现竞争条件.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值