您的位置:首页 > 编程语言 > Java开发

JAVA编程思想学习总结:第21章第3节共享受限资源

2015-04-21 16:55 585 查看

(1)解决共享资源竞争

1、利用synchronized关键字

当任务要执行被synchronized关键字保护的代码片段的时候,它将检查锁是否可用,然后获取锁,执行代码,释放锁。

所有对象都自动含有单一的锁(也称为监视器)。当在对象上调用其任意synchronized方法的时候,此对象都被加锁,这时该对象上的其他synchronized方法只有等到前一个方法调用完毕并释放了锁之后才能被调用。

注意,在使用并发时,将域设置为private是非常重要的。否则,synchronized关键字就不能防止其他任务直接访问域,这样就会产生冲突。

JVM负责跟踪对象被加锁的次数。如果一个对象被解锁(即锁被完全释放),其计数变为0。在任务第一次给对象加锁的时候,计数的任务才能允许继续获取多个锁。每当任务离开一个synchronized方法,计数递减,当计数为零的时候,锁被完全释放,此时别的任务就可以使用此资源。

Brian同步规则:如果你正在写一个变量,它可能接下来将被另一个线程读取,或者正在读取一个上一次已经被一个线程写过的变量,那么你必须使用同步,并且,读写线程都必须用相同的监视器锁同步。

为了让临界共享资源正确地工作,每个访问临界共享资源的方法必须被同步。

package concurrent;
/*
* P674-678
* 该段代码测试了在不对临界数据上锁时,各线程对临界数据抢占造成的影响
* 同时测试了对临界资源实现互斥机制后,临界数据在运行过程中的变化
* 互斥机制通过两种方式实现:1,Synchronized.2,lock对象
*/
import java.util.concurrent.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
abstract class IntGenerator{
private volatile boolean canceled = false;
public abstract int next();
public void cancel(){
canceled =true;
}
public boolean isCanceled(){
return canceled;
}
}
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();
//System.out.println(this+":"+val);
if(val% 2!=0){//当返回一个奇数时,输出该奇数
System.out.println(val+" now even!");
generator.cancel();
}
}
}
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();
}
public static void test (IntGenerator gp)throws Exception{
test(gp,10);
}
}
public class EvenGenerator extends IntGenerator{
private int currentEvenValue =0;
private Lock lock =new ReentrantLock();//3,利用lock实现同步
//以下几行代码可以通过删除代码前的注释符分别查看运行效果,标号1表示不采用同步机制,标号2表示采用synchronized,标号3表示采用lock
//public int next(){//1,该函数并没有采用同步机制,所以不同的任务返回的currentEvenValue可能是奇数,也可能是偶数
//public synchronized int next(){//2,利用synchronized实现任务同步
public int next(){
lock.lock();//3,利用lock实现同步
try{//3,利用lock实现同步
++currentEvenValue;
try {//由于进程调度的原因,我的环境下必须让进程睡眠一段时间,才能看到不同进程在对currentEvenValue操作时的抢占
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
++currentEvenValue;
return currentEvenValue;
}finally{//3,利用lock实现同步
lock.unlock();//3,利用lock实现同步
}//3,利用lock实现同步
}
public static void main(String[] args)throws Exception{
EvenChecker.test(new EvenGenerator());
}
}


2、Lock对象

Lock对象必须被显式地创建,锁定,和释放。因此它与内建的锁形式相比,代码缺乏优雅性。但是对于某些类型的问题它更加灵活。

在使用Lock对象时,将这里所示的惯用法内部化是很重要的:紧接着的对对lock()的调用,必须放置在finally子句中带有unlock()的try_finally语句中。注意,return语句必须在try子句中出现,以确保unlock()不会过早发生(为什么可以确保unlock不会过早发生)。

对于显式的Lock对象,如果抛出一个异常,可以通过finally子句将系统维护在正确的状态。

lock往往用于任务尝试获取锁但可能一直获取不到锁的情况,或者尝试着获取锁一段时间,如果一直获取不到,则放弃它。

package concurrent;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class AttemptLocking {
private ReentrantLock lock =new ReentrantLock();
public void untimed(){
boolean captured =lock.tryLock();
try{
System.out.println("tryLock(): "+captured);
}catch(Exception e){
e.printStackTrace();
}finally{
if(captured) lock.unlock();
}
}
public void timed(){
boolean captured = false;
try{
captured =lock.tryLock(2,TimeUnit.SECONDS);
}catch(InterruptedException e){
throw new RuntimeException(e);
}
try{
System.out.println("tryLock(2,TimeUnit.SECONDS);"+captured);
}finally{
if(captured)
lock.unlock();
}
}
public static void main(String[] args){
final AttemptLocking al= new AttemptLocking();
al.untimed();
al.timed();
//now create a separate task to grab the lock;
new Thread(){
{setDaemon(true);}//后台进程
public void run(){
al.lock.lock();
System.out.println("acquired");
}
}.start();//为什么Thread可以这样被创建?PS:这是创建匿名内部类的方式,继承其父类并可以在<span style="font-family: Arial, Helvetica, sans-serif;">在创建的过程中重载其父类方法</span>

Thread.yield();//Give the 2nd task a chance;
al.untimed();
al.timed();
}
}
运行结果:

tryLock(): true

tryLock(2,TimeUnit.SECONDS);true

acquired

tryLock(): false

tryLock(2,TimeUnit.SECONDS);false

(2)原子性

原子操作不需要进行同步控制,一旦操作开始,原子操作一定会执行完毕。

原子性可以应用于在除long和double之外的所有基本类型上的简单操作,比如读和写。JVM可以将64位的读取和写入当作丙个分离的32位操作来执行,这就产生了在一个读取和写入操作中间发生上下文切换,从而导致不同的任务可以看到不正确结果的可能性(有时这被日期为字撕裂,因为你可能会看到部分被修改过的数值)。但是,当你定义long或double变量时,使用volatile关键字,就会获得(简单的赋值与返回操作的)原子性。

volatile关键字还确保了应用中的可视性,如果将一个域 声明为volatile的,那么只要对这个域产生了写操作,那么所有的读操作就都可以看到这个修改。即使使用了本地缓存,情况也确实如此,volatile域会立即被写入到主存中,而读取操作就发生在主存中。

使用volatile而不是synchronized唯一安全的情况是类中只有一个可变的域。再次提醒,你的第一选择应该是使用synchronized关键字,这是最安全的方式。

如果一个域可能会被多个任务同时访问,或者这些任务中至少有一个是写入任务,那么就应该将这个域设置为volatile。如果你将一个域定义为volatile,那么它就会告诉编译器不要执行任何移除读取和写入操作的优化,这些操作的目的是用线程中的局部变量维护对这个域的精确同步。但是,volatile并不能对递增不是原子性操作这一事实产生影响。

package concurrent;
/*
* P683
* Operations that may seem safe are not.
* when threads are present.
*/
import java.util.concurrent.*;
class SerialNumberGenerator{
private static volatile int serialNumber =0;
public static int nextSerialNumber(){
return serialNumber++;
}
}
class CircularSet{
private int[] array;
private int len;
private int index=0;
public CircularSet(int size){
array=new int[size];
len =size;
//Initialize to a value not produced
// by the SerialNumberGenerator;
for(int i=0;i<size;i++){
array[i]=-1;
}
}
public synchronized void add(int i){
array[index]=i;
//Wrap index and write over old elements;
index=++index%len;
}
public synchronized boolean contains(int val){
for(int i=0;i<len;i++)
if(array[i]==val)return true;
return false;
}
}
public class SerialNumberChecker {
private static final int SIZE=10;
private static CircularSet serials=new CircularSet(1000);
private static ExecutorService exec=Executors.newCachedThreadPool();
static class SerialChecker implements Runnable{
public void run(){
while(true){
int serial=SerialNumberGenerator.nextSerialNumber();
if(serials.contains(serial)){//当任务生成了其它任务已经生成的重数字时退出
System.out.println("Duplicate: "+serial);
System.exit(0);
}
serials.add(serial);
}
}
}
public static void main(String[] args)throws Exception{
for(int i=0;i<SIZE;i++){
exec.execute(new SerialChecker());
}
//stop after n second if there's an argument;
if(args.length>0){
TimeUnit.SECONDS.sleep(new Integer(args[0]));
System.out.println("NO duplicates detected");
System.exit(0);
}
}
}


(3)临界区

为防止多个纯种同时访问方法内部的部分代码而不是防止访问整个方法,可以使用synchronized关键字将代码分离出来,这样的代码段叫临界区。

这也被称为同步控制块;在进入此段代码前,必须得到需要锁定对象的锁。如果其它纯种已经得到这个锁,那么就得等到锁被释放以后,才能进入临界区。

package concurrent;
/*
* P686-688
* Synchronizing blocks instead of entire methods.Also
* demonstrates protection of a non-thread-safe class
* with a thread-safe one
* 对于PairManager类的结构,它的一些功能在基类中实现并且其一个或多个 抽象方法在派生类中定义,这种结构在设计模式中称为模板方法。
* 该例同样可以使用lock对象实现。
*/
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.*;
class Pair{
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 aPair 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 methos;
class PairManager1 extends PairManager{
public synchronized void increment(){
p.incrementX();
p.incrementY();
store(getPair());
}
}
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()+" chechCounter = "+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();//对象的值加1
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);
}
}
synchronized块必须给定一个在其上进行同步的对象,并且最合理的方式是,使用其方法正在被调用的当前对象:synchronized(this),这正是上段代码中PairManager2所使用的方式。在这种方式中,如果获得了synchronized块上的锁,那么该对象其他的synchronized方法和临界区就不能被调用了。因此,如果在this上同步,临界区的效果就会直接缩小在同步的范围内。

有时必须在另一个对象上同步,但是如果这么做,就必须确保所有相关的任务都是在同一个对象上同步的。

package concurrent;
//Synchronizing on another object.
/*
* P689-690
*/
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();
}
}
运行结果:两段程序g()和f()各自运行,互不影响。

g()

f()

g()

g()

g()

g()

f()

f()

f()

f()

(4)本地存储

我并不能理解为什么每个线程都分配了自己的存储。
书中的这段话有助于我理解:ThreadLocal对象通常当作静态域存储。在创建ThreadLocal时,你只能通过get()和set()方法来访问该对象的内容,其中,get()方法将返回与其线程相关联的对象的副本,而set()方法将参数插入到其纯种存储的对象中。
当运行这个程序时,每个单独的纯种都被分配了自己的存储,即使只有一个ThreadLocalVariableHolder对象。
package concurrent;

import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/*
* Automatically giving each thread its own storage.
* P690-691
*/
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);
exec.shutdown();
}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: