Java线程之并发

1.线程同步

多线程的复杂度很大程度上都是来源于并发。并发必然涉及到状态共享,所以并发代码除了要实现业务逻辑,还要实现状态安全。状态安全包括三个方面:

  • 原子性:就是说当一组(一个或多个)状态被某个线程访问(通俗讲就是CRUD)时,这组状态不该被其他线程访问;
  • 可见性:当一个线程访问一组状态完成后,状态变化要立即对其他线程可见(每个线程都有自己的内存空间,如果没有同步,会先把修改后的状态缓存在线程自己的内存中,并不会马上冲刷到共享内存);
  • 有序性:当一个线程访问一组状态时,对各个状态的改变顺序应该和代码一致(如果没有同步代码,JVM可能会基于性能考虑对状态的赋值顺序作调整)。

当这三个条件得到满足,我们就说状态是安全的。在synchronized空间下的一组状态就可以满足这三个条件。被volatile修饰的单个状态也可以满足这三个条件(long和double的赋值默认都不是原子性的,这两种类型的变量如果被volatile修饰了就会变成具有赋值原子性)。volatile的性能要比synchronized好,但是适用场景少。在适合使用volatile的情况下(比如当多种线程的共享状态只有一个的时候),尽量使用volatile。

讲完状态安全,其实就讲完了多线程的同步。下面再讲多线程的协作。

2.线程协作

多种线程利用同一组状态进行通信,实现特定的业务逻辑,这就是协作。每个synchronized空间都持有一把锁。这把锁可以是任何一个普通对象(Object)。每个锁对象都有一组用来协作的方法(扩展自Object类):wait()/wait(long timeout)/wait(long timeout, int nanos)/notify()/notifyAll()。wait让current thread进入锁对象的等待队列,notify唤醒锁对象的等待队列里面的线程。

3.如何识别和设计并发系统

现在做一个题目,来理解不同种类的线程怎样进行协作:

实现一把读写锁:当有线程在读的时候,允许读线程访问,但是不允许写线程访问;当有线程在写的时候,其他线程不可访问;当读线程释放锁后,如果同时有写线程和读线程在等待,优先执行写线程。

 读写锁实现:

class ReadWriteLock {
	
	private int waitWritings;
	private int readings;
	private int writings;
	private boolean preferWriter;
	
	public synchronized boolean tryReadLock() {
		if(writings>0||(waitWritings>0&&preferWriter)) {
			return false;
		}
		readings++;
		return true;
	}
	
	public synchronized boolean tryWriteLock() {
		if(writings>0||readings>0) {
			return false;
		}
		writings++;
		return true;
	}
	
	public synchronized void readLock() {
		while(writings>0||(waitWritings>0&&preferWriter)) {
			try {
				wait();
			} catch (InterruptedException e) {

			}
		}
		readings++;
	}
	
	public synchronized void writeLock() {
		while(writings>0||readings>0) {
			try {
                waitWritings++;
				wait();
                waitWritings--;
			} catch (InterruptedException e) {

			}
		}
		writings++;
	}
	
    public synchronized void unReadLock() {
    	readings--;
    	preferWriter = true;
    	notifyAll();
   	}
    
    public synchronized void unWriteLock() {
    	writings--;
    	preferWriter = false;
    	notifyAll();
   	}

}

再补充一个数据类Couple,一个写线程类,一个读线程类。营造一种场景:写线程不停地修改Couple对象里面的夫妻信息,读线程不停读取Couple对象里面的夫妻信息。使用上面的读写锁进行状态的同步和协作:

class Couple {
	private ReadWriteLock lock = new ReadWriteLock();
	private String husband;
	private String wife;
	
	public void read() {
		try {
			lock.readLock();
			System.out.println("husband="+husband+";wife="+wife);
		} finally {
			lock.unReadLock();
		}
	}
	
	public void write(String husband,String wife) {
		try {
			lock.writeLock();
			this.husband = husband;
			this.wife = wife;
		} finally {
			lock.unWriteLock();
		}
	}
}

class ReadThread implements Runnable {
	
	private Couple couple;
	
	public ReadThread(Couple couple) {
		this.couple = couple;
	}

	@Override
	public void run() {
		while(true) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {

			}
			couple.read();
		}
	}
	
}

class WriteThread implements Runnable {
	
	private Couple couple;
	private String husband;
	private String wife;
	
	
	public WriteThread(Couple couple,String husband,String wife) {
		this.couple = couple;
		this.husband = husband;
		this.wife = wife;
	}

	@Override
	public void run() {
		couple.write(husband, wife);
	}
	
}

最后补上测试代码和结果:

Couple couple = new Couple();
Thread rt = new Thread(new ReadThread(couple));
rt.start();
while(true) {
	Thread wt1 = new Thread(new WriteThread(couple,"萧峰","阿朱"));
	Thread wt2 = new Thread(new WriteThread(couple,"郭靖","黄蓉"));
	Thread wt3 = new Thread(new WriteThread(couple,"杨过","小龙女"));
	Thread wt4 = new Thread(new WriteThread(couple,"张无忌","赵敏"));
	Thread wt5 = new Thread(new WriteThread(couple,"令狐冲","任盈盈"));
	wt1.start();
	wt2.start();
	wt3.start();
	wt4.start();
	wt5.start();
}

husband=杨过;wife=小龙女
husband=萧峰;wife=阿朱
husband=张无忌;wife=赵敏
husband=张无忌;wife=赵敏
husband=令狐冲;wife=任盈盈
husband=杨过;wife=小龙女
husband=张无忌;wife=赵敏
husband=张无忌;wife=赵敏
husband=杨过;wife=小龙女
husband=郭靖;wife=黄蓉

...

夫妻信息在多线程环境下,没有发生紊乱。说明上面的读写锁实现生效了。但是,这个测试场景没有全覆盖读写锁的功能。节省篇幅,不做全功能测试。下面借助这个例子总结一下怎么识别和设计并发程序:

  1. 识别依据:当遇到多种线程利用同一组状态(一个或多个状态)进行通信,实现特定的业务逻辑的场景,就要考虑设计成并发程序;
  2. 抽象出共享状态:有没有写线程在访问代码(抽象为有多少个线程在写:writings)、有多少读线程访问代码(抽象为有多少个线程在读:readings)、有多少写线程在等待访问代码(抽象为:waitWritings)、是不是写优先(抽象为:preferWriter)。这些状态都要被同步起来,确保状态安全。
  3. 抽象出线程种类:写线程和读线程。当然,同一个线程可以同时为写线程和读线程,一个线程分饰两种角色。
  4. 抽象出每种线程的行为:写线程的行为有:阻塞获取写锁、非阻塞获取写锁和释放写锁;读线程的行为有:阻塞获取读锁、非阻塞获取读锁和释放读锁。
  5. 实现线程间通信规则:根据状态选择不同的行为。对于读线程获取读锁的时候,如果有线程在写或有线程在等待写并且写优先的情况下,不能获取读锁,否则可以获取;对于读线程释放锁的时候,要notify其他线程;对于写线程……

根据上面的五个步骤,就可以设计出一个符合安全要求和业务要求的并发程序。

4.Java并发包的锁

下面再学习一下Java并发包里面的锁,下图是类图结构:

Java并发包抽象出了锁对象(Lock)、条件对象(Condition)等,让同步和协作等操作可以转化为对普通对象的操作。一方面让代码看起来很直观,另一方面让操作更加精细化。为了证明这一点,下面用ReentrantLock重新实现一下上面的题目,并且加一些附加条件:

实现一把读写锁:当有线程在读的时候,允许读线程访问,但是不允许写线程访问;当有线程在写的时候,其他线程不可访问

附加条件:①对外护短模式:当写线程释放写锁的时候,如果有其它写线程在等待,只唤醒写线程,如果没有写线程,才唤醒读线程;当读线程释放读锁的时候,如果有其它读线程在等待,只唤醒读线程,如果没有读线程,才唤醒写线程。【这个附加条件主要是为了展示Java并发包的精细化的特点,可能会导致其中一种线程总是拿不到锁,慎用!】②对内公平模式:让等待时间更久的线程优先拿到锁。

下面是实现代码,这里就不提供测试代码了:

class IntricatelyReadWriteLock {
	
	private int waitWritings;
	private int waitReadings;
	private int readings;
	private int writings;
	
	//公平的可重入锁
	private final ReentrantLock lock = new ReentrantLock(true);
    //写优先条件
	private final Condition writeFirst = lock.newCondition();
    //读优先条件
	private final Condition readFirst = lock.newCondition();
	
	public boolean tryReadLock() {
		try {
			lock.lock();
			
			if(writings>0) {
				return false;
			}
			readings++;
			return true;
		} finally {
			lock.unlock();
		}
	}
	
	public boolean tryWriteLock() {
		try {
			lock.lock();
			
			if(writings>0||readings>0) {
				return false;
			}
			writings++;
			return true;
		} finally {
			lock.unlock();
		}
	}
	
	public void readLock() {
		try {
			lock.lock();
			
			while(writings>0) {
				try {
					waitReadings++;
					readFirst.await();
					waitReadings--;
				} catch (InterruptedException e) {

				}
			}
			readings++;
		} finally {
			lock.unlock();
		}
	}
	
	public void writeLock() {
		try {
			lock.lock();
			
			while(writings>0||readings>0) {
				try {
					waitWritings++;
					writeFirst.await();
					waitWritings--;
				} catch (InterruptedException e) {

				}
			}
			
			writings++;
		} finally {
			lock.unlock();
		}
		
	}
	
    public void unReadLock() {
    	try {
			lock.lock();
			
	    	readings--;
	    	if(waitReadings>0) {
	    		readFirst.signalAll();
	    	}else {
	    		writeFirst.signalAll();
	    	}
    	} finally {
			lock.unlock();
		}
   	}
    
    public void unWriteLock() {
    	try {
			lock.lock();
			
	    	writings--;
	        if(waitWritings>0) {
	        	writeFirst.signalAll();
	    	}else {
	    		readFirst.signalAll();
	    	}
	    } finally {
			lock.unlock();
		}
   	}

}

对比这两种实现,你会发现Java并发包里面的锁更加直观,而且协作更加精细化,对锁的各种管理控制也更强(比如设置公平性,获取等待线程队列……)。

5.Java并发包的同步器

 最后,再介绍一下Java并发包里面提供的同步器。

  • CyclicBarrier 障栅。每个线程到一个执行点都会暂停,等到所有线程都到达,所有线程才会继续执行……可设置多个这样的执行点。
  • CountDownLath 倒计时门栓。等待所有线程执行完,再继续执行。
  • Exchanger 交换器。两个线程互相交换状态。
  • Semphore 信号量。限制访问资源的线程总数。
  • SynchronousQueue 同步队列。生产者和消费者总是同步执行,size永远是0。

这些同步器在很多特定的并发场景下非常有用。具体怎么使用本文不做介绍,可以自行学习。但是,记住这些同步器的功能,对你设计并发系统非常有用。

猜你喜欢

转载自my.oschina.net/leaforbook/blog/1826027