@lemonguge
2017-09-13T14:48:45.000000Z
字数 6199
阅读 363
Concurrency
有关Java SE5线程新特征的内容全部在java.util.concurrent、java.util.concurrent.atomic、java.util.concurrent.locks三个包下面,里面包含数目众多的接口和类,熟悉这部分API特征是一项艰难的学习过程。
在这一篇文章,我看了《Java并发编程实践》、《Java Programming》以及很多的博客,所以难免文中会有一些参考。在写这篇文章的过程中,可能会有一些认知不正确的地方,但这将是一起进步的过程。新特征对写出多线程程序没有必须的关系,在Java SE5之前就可以写出很优秀的多线程程序,只是代价不一样而已。为了编写高效稳定可靠的多线程程序,线程部分的新增内容显得尤为重要。
在讲解之前,首先回顾之前文章提到的两个概念:
volatile关键字可以保证直接从主存中读取一个变量,如果这个变量被修改后,总是会被立即写回到主存中去。原子操作是不能被CPU运行切换所中断的,一旦操作开始,那么它一定可以执行完毕。其中部分的Java语句可以认定为具有原子性的:
long和double之外的基本类型变量的读取和写入操作。volatile的变量的读和写,包括long和double类型以及引用类型。通常对域中的值做赋值和返回(get 和set 方法)操作都是原子性的,int i = 1;这个语句就是原子性的;i++;不具有原子性,结合之前讲解的Java内存模型中将很好理解,假如有两个线程同时运行在一个CPU上,那么这两个线程将以时间片的形式来体现出多任务。它们将共用同一个CPU缓存,一个线程A执行的是i++;语句,另一个线程B执行i=3;语句。可以想象出一种情况,当线程A执行完读i=1时,这时候CPU进行了运行切换去执行线程B,那么i将会变成3,再执行i+1,那么i的结果将会等于4!这是一个“读-改-写”的过程。
互斥和原子起到的效果是一样的,也不能说表达的是同一个意思。我曾经看到一句话,非常好的描述了这两者的区别。
互斥是这个资源只有我用,你不许用;原子是我一口气把活干完,不间断。
在有关Java线程的讨论中,一个常不正确的知识是“原子操作是不需要进行同步控制”。当把多个原子操作整合成一个复合操作时,还是需要同步的。
class CompositeOper implements Runnable {// 保证从主存中读取,而不是CPU缓存private volatile AtomOper atom;public CompositeOper(AtomOper atom) { this.atom = atom; }@Overridepublic void run() {atom.num = 10; // 原子操作try {Thread.sleep(1000);} catch (InterruptedException e) {}System.out.println(Thread.currentThread().getName() + ":" + atom.num); // 原子操作}}public class AtomOper {public int num;public static void main(String[] args) throws InterruptedException {AtomOper atom = new AtomOper();new Thread(new CompositeOper(atom)).start();Thread.sleep(500);atom.num = 12;}} /* Output:Thread-0:12*///:~
可以发现以上示例并没有输出10,而是输出12。为了防止主线程中途修改,保证使得线程Thread-0输出10,只需要给主线程和线程Thread-0都加上同样的隐式锁atom即可。代码如下所示:
class CompositeOper implements Runnable {private volatile AtomOper atom;public CompositeOper(AtomOper atom) { this.atom = atom; }@Overridepublic void run() {synchronized (atom) { // 加上同步代码块atom.num = 10;try {Thread.sleep(1000);} catch (InterruptedException e) {}System.out.println(Thread.currentThread().getName() + ":" + atom.num);}}}public class AtomOper {public int num;public static void main(String[] args) throws InterruptedException {AtomOper atom = new AtomOper();new Thread(new CompositeOper(atom)).start();Thread.sleep(500);synchronized (atom) { // 和线程1同一把锁atom.num = 12;}}} /* Output:Thread-0:10*///:~
正如上面代码所演示的,使复合操作在完整的运行期间占有锁,以确保其行为是原子的。
由于volatile变量与锁相比是更轻量的同步机制,它们不会引起上下文的切换和线程调度。尽管提供了相似的可见性保证,但是它们不能用于构建原子化的复合操作。
首先看一个之前提到过的银行存钱示例:
// 银行class Bank {private int sum; // 银行库存public void add(int num) { // 每次存钱,库存增加sum += num;}public int getSum() { return sum; }}// 储户class Customer implements Runnable {private Bank bank;public Customer(Bank bank) { this.bank = bank; }@Overridepublic void run() {for (int i = 0; i < 5000; i++)// 存5000次,每次存100bank.add(100);}}public class BankDemo {public static void main(String[] args) throws InterruptedException {Bank bank = new Bank();// 指定a和b储户存钱的银行为bankCustomer a = new Customer(bank);Customer b = new Customer(bank);// 开启两个线程可以使a和b同时存钱new Thread(a, "a").start(); // 指定线程名为anew Thread(b, "b").start(); // 指定线程名为bThread.sleep(5000);System.out.println(bank.getSum());}} /* Output: 输出结果并不确定762900*///:~
可以发现上述示例中没有输出我们所期望的数值1000000,以前为了解决这个问题可以给add()方法加上synchronized关键字,但是如果我们使用可见性的原子变量同样可以解决这个问题,代码如下:
import java.util.concurrent.atomic.AtomicInteger;// 银行class Bank {private volatile AtomicInteger sum = new AtomicInteger(); // 银行库存public void add(int num) { // 每次存钱,库存增加int temp = sum.addAndGet(num);}public int getSum() {return sum.get();}}// 储户class Customer implements Runnable {private Bank bank;public Customer(Bank bank) {this.bank = bank;}@Overridepublic void run() {for (int i = 0; i < 3; i++) // 存三次,每次存100bank.add(100);}}public class BankDemo {public static void main(String[] args) throws InterruptedException {Bank bank = new Bank();// 指定a和b储户存钱的银行为bankCustomer a = new Customer(bank);Customer b = new Customer(bank);// 开启两个线程可以使a和b同时存钱new Thread(a, "a").start(); // 指定线程名为anew Thread(b, "b").start(); // 指定线程名为bThread.sleep(1000);System.out.println(bank.getSum());}}
在同步块中,锁也是监视器对象,在Java SE5的java.util.concurrent.locks包中定义了显式的锁和监视器。Lock对象必须被显式的创建、锁定和释放,因此它与同步块中的隐式锁相比,会缺乏优雅性。但对于解决某些问题来说,它更加灵活。
在临界区中使用适当Java同步块就可以避免竞态条件,对于同任务的多线程,只要一个线程获得了锁,所有其他等待进入该同步块的线程将被阻塞。这就是隐式锁体现出来的互斥性,在java.util.concurrent.locks包有一个ReentrantLock类,这是一个互斥锁。这个类实现了Lock接口,我们应该先去了解一下Lock接口具有的功能。
void lock()方法:获取锁void unlock()方法:释放锁Condition newCondition()方法:获取监听器Condition类是显式的监听器,之前介绍过了通过监听器可以改变线程的状态。通过查看API可以发现:
void await()方法使当前监听的线程处于冻结状态,相当于隐式锁的wait()方法。void signal()方法随机唤醒一个处于冻结状态的线程,相当于隐式锁的notify()方法。void signalAll()方法唤醒所有处于冻结状态的线程,相当于隐式锁的notifyAll()方法。通过这几个方法,我们可以对之前的while和notifyAll进行改进,因为可以主动控制监听器来唤醒对方线程。
import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.ReentrantLock;import java.util.concurrent.locks.Lock;//资源代表烤鸭class Resource{boolean flag; // 通过标志位判断烤鸭是否做好了Lock lock = new ReentrantLock(); // 创建互斥锁// 有两个任务的多线程,所以需要两个监视器,每个监视器监视运行一个任务的多线程Condition prodCond = lock.newCondition();Condition ConsCond = lock.newCondition();}//生产者代表餐厅class Producer implements Runnable{private Resource res;public Producer(Resource res) {this.res = res;}@Overridepublic void run() {while (true) {res.lock.lock(); // 临界区while (res.flag)try { res.prodCond.await(); } catch (InterruptedException e) { }System.out.println(Thread.currentThread().getName() + "@Producer run..");res.flag = true;res.ConsCond.signal(); // 唤醒消费者,烤鸭做好啦res.lock.unlock();}}}//消费者class Consumer implements Runnable{private Resource res;public Consumer(Resource res) {this.res = res;}@Overridepublic void run() {while (true){res.lock.lock(); // 临界区while (!res.flag)try { res.ConsCond.await(); } catch (Exception e) { }System.out.println(Thread.currentThread().getName() + "@Consumer eat..");res.flag = false;res.prodCond.signal(); // 唤醒烤鸭店,烤鸭吃完啦res.lock.unlock();}}}public class Cond {public static void main(String[] args) {Resource res = new Resource();Producer pro1 = new Producer(res);Producer pro2 = new Producer(res);Consumer con1 = new Consumer(res);Thread t1 = new Thread(pro1,"生产者1");Thread t2 = new Thread(pro2,"生产者2");Thread t3 = new Thread(con1,"消费者");t1.start();t2.start();t3.start();}} ///:OK~
笔者往往会为一个任务创建一个监听器,监视器调用await()方法使运行该任务的当前线程处于冻结状态,一个线程任务可以被多个线程共享,这些处于冻结状态的线程们也只能被冻结它们的那个监视器所唤醒。
线程池就是Java SE5的新特征之一,在之前的文章讲到过线程的代价,创建一个线程是需要占用系统资源的,尤其是反复创建线程对象所带来的性能开销更大。为了节省这种开销,就需要用到线程池技术。
线程池的基本思想是一种对象池的思想,开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理。当有线程任务时,从池中取一个,执行完成后线程对象归池。在Java SE5之前,要实现一个线程池是相当有难度的,现在Java SE5为我们做好了一切,我们只需要按照提供的API来使用,即可享受线程池带来的极大便利。
java.util.concurrent.Executors类提供大量创建连接池的静态方法。
// TODO