多线程学习(七)

共享受限资源

关于线程的基本特性差不多介绍了,接下来是一些关于线程安全的问题了。

不正确的访问资源

/**
 * 定义一个抽象的生成器 用于生成 int 整数
 * 
 *
 */
public abstract class IntGenerator {
	private volatile boolean canceled=false;
	public abstract int next();
	public void cancel(){
		this.canceled=true;
	}
	public boolean isCanceled(){
		return this.canceled;
	}
}

定义一个检查器,去检查生成器生成的内容

public class EvenChecker implements Runnable {
	private IntGenerator intGenerator;

	public EvenChecker(IntGenerator intGenerator) {
		super();
		this.intGenerator = intGenerator;
	}

	@Override
	public void run() {
		// TODO Auto-generated method stub
		while (!intGenerator.isCanceled()) {
			int val = intGenerator.next();
			System.out.println(val);
			if (val % 2 != 0) {
				System.out.println(val + "should not appear,current Thread:"+Thread.currentThread().getName());
				intGenerator.cancel();
			}
		}
	}

	public static void main(String[] args) {
		ExecutorService exec=Executors.newFixedThreadPool(10);
		Runnable target=new EvenChecker(new IntGenerator() {
			private int val;
			@Override
			public int next() {
				val++;
				Thread.yield();
				val++;
				return val;
			}
		});
		for(int a=0;a<10;a++){//开启10个线程
			exec.execute(target );
		}
		exec.shutdown();//任务执行完之后尽快退出
	}
}

输出#

关于上面的输出:解释一下 由于next没有保证原子性导致了线程不安全 同时 canceled 是使用volatile修饰的,它修改之后对所有的线程都是可见的 按道理说 程序只可能有一次输出 这里第一个线程输出之后就不会有其他输出了,但是因为这里没有使用同步 所以可能在线程一输出之后还没来得及改 cancel的标志 又切换其他的线程了,这时候线程看到的cancel标志任然是false,所以线程继续执行进了while 循环就有了第二次第三次。。。输出

上面的实例展示了线程的的一个基本的问题,你永远不知道一个线程什么时候执行、什么时候切换时间片 当你拿着叉子准备吃叉起最后一块食物时候,这块食物消失了,因为你的线程被挂起,其他的线程吃掉了它。这正是并发编程要解决的问题。
解决冲突的方法就是 当一个资源被一个任务所使用时候,为这个资源上锁。
基本上所有的并发模式在解决线程冲突问题的时候,都采用 序列化访问共享资源的方案。这意味着在给定的时刻只允许一个任务访问共享资源。通常在代码前面 加一条上锁语句来实现的,因为锁有一种相互排斥的效果,所以这种机制又被称为 互斥量 (mutex)
java以提供关键字 synchronized 的形式,为防止资源冲突提供支持。当任务要执行synchronized 关键字保护的代码的时候,它将检查锁是否可用。然后获取锁,执行代码,释放锁。
共享资源一般以对象的形式存在的内存片段,但也可以是文件、输入、输出端口,或者是打印机。要想控制对共享资源的访问。得先把它包装进一个对象。然后把所有对这个资源的访问的方法标记为synchronized.
如果一个线程正处于对一个对象的一个标记synchronized的方法访问之中,在方法返回之前。那么其他任何对这个对象的synchronized方法的访问的线程都将阻塞。

同步的规则:
如果你正在写一个变量,它可能接下来将别另一个线程读取,或者正在读取一个上一次以及被另一个线程写过的变量。那么你必须使用同步。并且,读写线程必须使用相同的监视器锁进行同步。
修改以前的线程不安全的代码:

public class EvenChecker implements Runnable {
	private IntGenerator intGenerator;

	public EvenChecker(IntGenerator intGenerator) {
		super();
		this.intGenerator = intGenerator;
	}

	@Override
	public void run() {
		// TODO Auto-generated method stub
		while (!intGenerator.isCanceled()) {
			int val = intGenerator.next();
			System.out.println(val);
			if (val % 2 != 0) {
				System.out.println(val + "should not appear,current Thread:"+Thread.currentThread().getName());
				intGenerator.cancel();
			}
		}
	}

	public static void main(String[] args) {
		ExecutorService exec=Executors.newFixedThreadPool(10);
		Runnable target=new EvenChecker(new IntGenerator() {
			private int val;
			@Override
			public synchronized int next() {
				val++;
				Thread.yield();
				val++;
				return val;
			}
		});
		for(int a=0;a<10;a++){//开启10个线程
			exec.execute(target );
		}
		exec.shutdown();//任务执行完之后尽快退出
	}
}

此时这个程序就不会产生奇数了。

也可以使用 Java SE5 中juc类库提供的Lock对象 代码如下:

public class EvenChecker implements Runnable {
	private IntGenerator intGenerator;

	public EvenChecker(IntGenerator intGenerator) {
		super();
		this.intGenerator = intGenerator;
	}

	@Override
	public void run() {
		// TODO Auto-generated method stub
		while (!intGenerator.isCanceled()) {
			int val = intGenerator.next();
			System.out.println(val);
			if (val % 2 != 0) {
				System.out.println(val + "should not appear,current Thread:" + Thread.currentThread().getName());
				intGenerator.cancel();
			}
		}
	}

	public static void main(String[] args) {
		ExecutorService exec = Executors.newFixedThreadPool(10);
		Runnable target = new EvenChecker(new IntGenerator() {
			private Lock lock = new ReentrantLock();
			private int val;

			@Override
			public int next() {
				lock.lock();
				try {
					val++;
					Thread.yield();
					val++;
					return val;
				} finally {
					lock.unlock();
				}
			}
		});
		for (int a = 0; a < 10; a++) {// 开启10个线程
			exec.execute(target);
		}
		exec.shutdown();// 任务执行完之后尽快退出
	}
}

尽管这里的代码try-catch比用synchronized要多得多,但是也代表了Lock关键字的优点之一,如果使用synchronized 时候,某些事物失败了,那么会抛出一个异常,但是你没有任何机会去做一些清理工作,以维护系统处于良好的状态。有了显示的Lock对象,你就可以在finally中将系统维护在正确的状态中了。

大致上使用 synchronized 代码量更少,并且出现错误的记录更低、只有在尝试解决一些特殊的问题的时候才使用 Lock.比如:尝试获取锁,最终获取失败、尝试获取锁一段时间,然后放弃它。
Lock的其他特性:

public class AttemptLocking {
	private Lock lock=new ReentrantLock();
	private void untime(){
		boolean flag=lock.tryLock();
		try{
			System.out.println("try lock:"+flag);
		}finally {
			if(flag){
				lock.unlock();
			}
		}
	}
	private void time(){
		boolean flag=false;
		try {
			flag=lock.tryLock(2, TimeUnit.SECONDS);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		try{
			System.out.println("try lock(2,TimeUnit.SECONDS):"+flag);
		}finally {
			if(flag){
				lock.unlock();
			}
		}
	}
	public static void main(String[] args) {
		AttemptLocking al=new AttemptLocking();
		al.time();
		al.untime();
		new Thread(){
			{
				setDaemon(true);
			}
			@Override
			public void run() {
				al.lock.lock();
				System.out.println("deamon lock!");
			}
			
		}.start();
		Thread.yield();
		try {
			TimeUnit.SECONDS.sleep(2);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		al.untime();
		al.time();
	}
}

使用Lock的那些属性来获取 锁,和获取锁失败

posted @ 2017-05-07 12:36  风中小蘑菇  阅读(166)  评论(0编辑  收藏  举报