Coin163

首页 > java多线程系列----------- 共享受限资源(二)

java多线程系列----------- 共享受限资源(二)

2021腾讯云限时秒杀,爆款1核2G云服务器298元/3年!(领取2860元代金券),
地址https://cloud.tencent.com/act/cps/redirect?redirect=1062

2021阿里云最低价产品入口+领取代金券(老用户3折起),
入口地址https://www.aliyun.com/minisite/goods

能有此文十分感谢《Java编程思想》一书及其作者Bruce Eckel!

五、临界区

        有时,只是希望防止多个线程同时访问方法内部的部分代码而不是防止访问整个方法。通过这种方式分离出来的代码段被称为临界区(critical section),它也使用synchronized关键字建立。这里,synchronized被用来指定某个对象。此对象的锁被用来对花括号内的代码进行同步控制:
	synchronized(syncObject){
		//this code can be accessed by only one task at a time
	}
        这也被称为同步控制块;在进入此段代码前,必须得到syncObject对象的锁。如果其他线程已经得到这个锁,那么就得等到锁被释放以后才能进入临界区。         通过使用同步控制块,而不是对整个方法进行同步控制,可以使多个任务访问对象的时间性能得到显著提高,下面的例子比较了这两种同步控制方法。此外,也演示了如何把一个非保护类型的类,在其他类的保护和控制之下,应用于多线程环境:
// Synchronizing blocks instead of entire methods. Also
// demonstrates protection of a non-thread-safe class
// with a thread-safe one.
package concurrency;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.*;

class Pair { // Not thread-safe
  private int x, y;
  public Pair(int x, int y) {
    this.x = x;
    this.y = y;
  }
  public Pair() { this(0, 0); }
  public int getX() { return x; }
  public int getY() { return y; }
  public void incrementX() { x++; }
  public void incrementY() { y++; }
  public String toString() {
    return "x: " + x + ", y: " + y;
  }
  public class PairValuesNotEqualException
  extends RuntimeException {
    public PairValuesNotEqualException() {
      super("Pair values not equal: " + Pair.this);
    }
  }
  // Arbitrary invariant -- both variables must be equal:
  public void checkState() {
    if(x != y)
      throw new PairValuesNotEqualException();
  }
}

// Protect a Pair inside a thread-safe class:
abstract class PairManager {
  AtomicInteger checkCounter = new AtomicInteger(0);
  protected Pair p = new Pair();
  private List<Pair> storage =
    Collections.synchronizedList(new ArrayList<Pair>());
  public synchronized Pair getPair() {
    // Make a copy to keep the original safe:
    return new Pair(p.getX(), p.getY());
  }
  // Assume this is a time consuming operation
  protected void store(Pair p) {
    storage.add(p);
    try {
      TimeUnit.MILLISECONDS.sleep(50);
    } catch(InterruptedException ignore) {}
  }
  public abstract void increment();
}

// Synchronize the entire method:
class PairManager1 extends PairManager {
  public synchronized void increment() {
    p.incrementX();
    p.incrementY();
    store(getPair());
  }
}

// Use a critical section:
class PairManager2 extends PairManager {
  public void increment() {
    Pair temp;
    synchronized(this) {
      p.incrementX();
      p.incrementY();
      temp = getPair();
    }
    store(temp);
  }
}

class PairManipulator implements Runnable {
  private PairManager pm;
  public PairManipulator(PairManager pm) {
    this.pm = pm;
  }
  public void run() {
    while(true)
      pm.increment();
  }
  public String toString() {
    return "Pair: " + pm.getPair() +
      " checkCounter = " + pm.checkCounter.get();
  }
}

class PairChecker implements Runnable {
  private PairManager pm;
  public PairChecker(PairManager pm) {
    this.pm = pm;
  }
  public void run() {
    while(true) {
      pm.checkCounter.incrementAndGet();
      pm.getPair().checkState();
    }
  }
}

public class CriticalSection {
  // Test the two different approaches:
  static void
  testApproaches(PairManager pman1, PairManager pman2) {
    ExecutorService exec = Executors.newCachedThreadPool();
    PairManipulator
      pm1 = new PairManipulator(pman1),
      pm2 = new PairManipulator(pman2);
    PairChecker
      pcheck1 = new PairChecker(pman1),
      pcheck2 = new PairChecker(pman2);
    exec.execute(pm1);
    exec.execute(pm2);
    exec.execute(pcheck1);
    exec.execute(pcheck2);
    try {
      TimeUnit.MILLISECONDS.sleep(500);
    } catch(InterruptedException e) {
      System.out.println("Sleep interrupted");
    }
    System.out.println("pm1: " + pm1 + "\npm2: " + pm2);
    System.exit(0);
  }
  public static void main(String[] args) {
    PairManager
      pman1 = new PairManager1(),
      pman2 = new PairManager2();
    testApproaches(pman1, pman2);
  }
} /* Output: (Sample)
pm1: Pair: x: 15, y: 15 checkCounter = 272565
pm2: Pair: x: 16, y: 16 checkCounter = 3956974
*/
        正如注释中注明的,Pair不是线程安全的,因为他的约束条件(虽然是任意的)需要两个变量要维护成相同的值。此外,如前面所述,自增操作不是线程安全的,并且因为没有任何方法被标记为synchronized,所有不能保证一个Pair对象在多线程程序中不被破坏。         可以想象一下这种情况:某人交给你一个非线程安全的Pair类,而你需要在一个线程环境中使用它。通过创建PairManager类就可以实现这一点,PairManager类持有一个Pair对象并控制对它的一切访问。注意唯一的public方法是getPair(),它是synchronized的。对于抽象方法increment(),对increment()的同步控制将在实现的时候进行处理。 至于PairManager类的结构,它的一些功能在基类中实现,并且其一个或多个抽象方法在派生类中定义,这种结构在涉及模式中称为模板方法。设计模式使你得以把变化封装在代码里;在此,发生变化的部分是模板方法increment()。在PairManager1中整个increment()方法是被同步控制的,但是在PairManager2中,increment()方法使用同步控制块进行同步。注意synchronized关键字不属于方法特征签名的组成部分,所以可以在覆盖方法的时候加上去。 store()方法将一个Pair对象添加到synchronized ArrayList中,所以这个操作是线程安全的。因此该方法不必进行防护,可以放在 PairManager2的synchronized语句块的外部。 PairManipulator被创建用来测试两种不同类型的PairManager,其方法是在某个任务中调用increment(),而PairChecker则在另一个任务中执行。为了追踪可以运行测试的频度,PairChecker在每次成功时都递增checkCounter。在main()中创建了两个PairManipulator对象,并允许它们运行一段时间,之后每个PairManipulator的结果会得到展示。 尽管每次运行的结果可能会非常不同,但一般来说,对于PairChecker的检查频率,PairManager1.increment()不允许PairManager2.increment()那样多。后者采用同步控制块进行同步,所以对象不加锁的时间更长,使得其他线程能更多地访问。 还可以使用显式的Lock对象来创建临界区:
import java.util.concurrent.locks.*;

// Synchronize the entire method:
class ExplicitPairManager1 extends PairManager {
  private Lock lock = new ReentrantLock();
  public synchronized void increment() {
    lock.lock();
    try {
      p.incrementX();
      p.incrementY();
      store(getPair());
    } finally {
      lock.unlock();
    }
  }
}

// Use a critical section:
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);
  }
}

六、在其他对象上同步

synchronized块必须给定一个在其上进行同步的对象,并且最合理的方式是,使用其方法正在被调用的当前对象:synchronized(this),这正是前面PairManager2所使用的方式。在这种方式中,如果获得了synchronized块上的锁,那么该对象其他的synchronized方法和临界区就不能被调用了。 有时必须在另一个对象上同步,但是如果要这么做,就必须确保所有相关的任务都是在同一个对象上同步的。下面的示例演示了两个任务可以同时进入同一个对象,只要这个对象上的方法是在不同的锁上同步的即可:
class DualSynch {
	private Object syncObject = new Object();

	public synchronized void f() {
		for (int i = 0; i < 5; i++) {
			System.out.println("f()");
			Thread.yield();
		}
	}

	public void g() {
		synchronized (syncObject) {
			for (int i = 0; i < 5; i++) {
				System.out.println("g()");
				Thread.yield();
			}
		}
	}
}

public class SyncObject {
	public static void main(String[] args) {
		final DualSynch ds = new DualSynch();
		new Thread() {
			public void run() {
				ds.f();
			}
		}.start();
		ds.g();
	}
} /*
 * Output: (Sample) g() f() g() f() g() f() g() f() g() f()
 */
        DualSynch.f()(通过同步整个方法)在this同步,而g()有一个在syncObject上同步的synchronized块。因此,这两个同步是互相独立的。通过在main()中创建调用f()的Thread对这一点进行了演示,因为main()线程是用来调用g()的。从输出可以看到,这两个方式在同时运行,因此任何一个方法都没有因为对另一个方法的同步而被阻塞。

七、线程本地存储

        防止任务在共享资源上产生冲突的第二种方式是根除对变量的共享。 线程本地存储是一种自动化机制,可以为使用相同变量的每个不同的线程都创建不同的存储。因此,如果有5个线程都要使用变量x所表示的对象,那线程本地存储就会生成5个用于x的不同的存储块。主要是,它们使得你可以将状态与线程关联起来。 创建和管理线程本地存储可由java.lang.ThreadLocal类来实现。如下所示:
// Automatically giving each thread its own storage.
import java.util.concurrent.*;
import java.util.*;

class Accessor implements Runnable {
  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);
      }
    };
  public static void increment() {
    value.set(value.get() + 1);
  }
  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
  }
}
ThreadLocal对象通常当作静态域存储。在创建ThreadLocal时,只能通过get()和set()方法来访问该对象的内容。其中,get()方法将返回与其线程关联的对象的副本,而set()会将参数插入到为其线程存储的对象中,并返回存储中原有的对象。Increment()和get()方法在ThreadLocalVariableHolder中演示了这一点。注意,increment()和get()方法都不是synchronized的,因为ThreadLocal保证不会出现竞争条件。 当运行这个程序时,你可以看到每个单独的线程都被分配了自己的存储,因为它们每个都需要跟踪自己的计数器。



原文

能有此文十分感谢《Java编程思想》一书及其作者Bruce Eckel! 五、临界区         有时,只是希望防止多个线程同时访问方法内部的部分代码而不是防止访问整个方法。通过这种方式分离出来的代码

------分隔线----------------------------