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=黄蓉...
夫妻信息在多线程环境下,没有发生紊乱。说明上面的读写锁实现生效了。但是,这个测试场景没有全覆盖读写锁的功能。节省篇幅,不做全功能测试。下面借助这个例子总结一下怎么识别和设计并发程序:
- 识别依据:当遇到多种线程利用同一组状态(一个或多个状态)进行通信,实现特定的业务逻辑的场景,就要考虑设计成并发程序;
- 抽象出共享状态:有没有写线程在访问代码(抽象为有多少个线程在写:writings)、有多少读线程访问代码(抽象为有多少个线程在读:readings)、有多少写线程在等待访问代码(抽象为:waitWritings)、是不是写优先(抽象为:preferWriter)。这些状态都要被同步起来,确保状态安全。
- 抽象出线程种类:写线程和读线程。当然,同一个线程可以同时为写线程和读线程,一个线程分饰两种角色。
- 抽象出每种线程的行为:写线程的行为有:阻塞获取写锁、非阻塞获取写锁和释放写锁;读线程的行为有:阻塞获取读锁、非阻塞获取读锁和释放读锁。
- 实现线程间通信规则:根据状态选择不同的行为。对于读线程获取读锁的时候,如果有线程在写或有线程在等待写并且写优先的情况下,不能获取读锁,否则可以获取;对于读线程释放锁的时候,要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。
这些同步器在很多特定的并发场景下非常有用。具体怎么使用本文不做介绍,可以自行学习。但是,记住这些同步器的功能,对你设计并发系统非常有用。