并发
并发(concurrency)是指CPU在某个时间段内交替处理多任务的能力。每个CPU不可能只顾着执行某个进程,而让其他进程一直等待被执行。所以,CPU把可执行时间均分成若干份,每个进程执行一份或多份时间后,记录当前的工作状态,释放相关资源并进入等待状态,让其他进程抢占CPU等资源。
在并发环境下,由于程序的封闭性被打破,出现了以下特点:
1.并发程序之间有相互制约的关系。直接制约体现在一个程序需要另一个程序的计算结果;间接制约体现在多个进程竞争共享资源。
2.并发程序的执行过程是断断续续的。程序需要保留现场,记忆现场指令及执行点。
3.当并发数设置合理并且CPU拥有足够的处理能力时,并发会提高程序的运行效率。
在Java编程中,并发主要与线程有关。
线程
线程是CPU调度和分派的基本单位,为了更充分地利用CPU资源,一般都会使用多线程进行处理。多线程的作用是提高任务的平均执行速度,但是会导致程序可解性变差,编程难度加大。所以,合适的线程数才能让CPU资源被充分利用。
每一个线程都有自己的操作栈、程序计数器、局部变量表等资源。同一进程内的所有线程都可以共享该进程的所有资源。
Java提供了两种形式定义线程类:
1.实现Runnable接口并重写其中的run()方法。
1 class Consumer implements Runnable { 2 3 private Store store; 4 5 public Consumer(Store store) { 6 this.store = store; 7 } 8 9 @Override 10 public void run() { 11 for (int i = 0; i < 1000; i++) { 12 store.getValue(); 13 } 14 } 15 16 }
2.继承Thread类并重写其中的run()方法。
1 class Producer extends Thread { 2 3 private Store store; 4 5 public Producer(Store store) { 6 this.store = store; 7 } 8 9 @Override 10 public void run() { 11 for (int i = 0; i < 1000; i++) { 12 store.setValue((int) (Math.random() * 100)); 13 } 14 } 15 16 }
里氏代换原则对继承的一个约束是子类不重写父类的非抽象方法,而Thread类的run()方法不是一个抽象方法,所以继承Thread类并重写其中的run()方法就不符合里氏代换原则,该方式不推荐使用。相比之下,实现Runnable接口可以使编程更加灵活,对外暴露的细节也比较少,让使用者专注于实现线程的run()方法。
线程状态
线程的生命周期分为以下5种状态:
新建状态
新建状态是线程被创建且未启动的状态。也就是说,初始化一个线程对象时,该对象进入新建状态。
线程对象的初始化分为2种:
1.如果是继承Thread类的线程类,则该类线程对象可以直接通过new运算进行初始化。
2.如果是实现Runnable接口的线程类,则该类线程对象通过new运算进行初始化后需要包装为一个Thread对象。
就绪状态
就绪状态是线程启动后运行之前的状态。即启动了的线程在准备执行run()方法时的状态。
线程的启动是指线程对象调用Thread的start()方法。
运行状态
运行状态是线程运行时的状态,即启动了的线程在执行run()方法时的状态。
阻塞状态
阻塞状态分以下3种情况:
同步阻塞:缺少资源无法继续运行。抢占到资源后会退出该状态。
主动阻塞:主动让出CPU执行权,即线程执行Thread的sleep()方法之后的状态。调用sleep()方法时会传入一个long类型的参数,表示睡眠的时间,单位为毫秒,时间结束时会退出该状态。
等待阻塞:进入睡眠,即线程执行Object的wait()方法之后的状态。其他线程执行Object的notify()方法或notifyAll()方法之后会退出该状态。
终止状态
终止状态是线程执行结束或因异常退出后的状态。
线程同步
线程同步机制的主要任务是,对多个相关线程在执行次序上进行协调,使并发执行的每个线程之间能按照一定的时序共享资源,并能很好地相互合作,从而使程序的执行具有可再现性。
资源的共享分为两种方式:
互斥共享方式:某些资源例如打印机、磁带机等,一次只能给一个线程使用,当一个线程申请该资源时,如果该资源有其他线程在使用,则该线程需要等待,直到资源被释放之后才能申请。
同时访问方式:某些资源例如磁盘设备等,一次可以给多个线程“同时”访问,这种“同时”是宏观上的,实际上还是多个线程交替访问。
临界资源指的是一段时间内只能由一个线程访问的资源,而临界区指的是每个线程中访问临界资源的那部分代码。显然,若能保证每个线程互斥地进入自己的临界区,便可以实现每个线程对临界资源的互斥访问。为此,需要在每个线程进入临界区前需要对访问的临界资源进行检查,如果它是空闲的,则进入临界区;否则等待,直到临界资源空闲。具体流程如下:
进入区:检查临界资源的状态,如果空闲,则将其状态改为被访问,并进入临界区;如果被访问,则循环等待,直到其状态变为空闲。
临界区:访问临界资源。
退出区:将临界资源的状态改为空闲,并释放临界资源。
Java提供synchronized关键字标识方法或代码块,被标识的方法称为同步方法,被标识的代码块称为同步代码块。每个对象都有一个监视器与之关联。当线程通过该对象执行同步方法或同步代码块时,它首先试图获取监视器,如果获取到监视器,则锁定该对象,防止其他线程通过该对象执行同步方法或同步代码块,执行结束后,解锁该对象并释放监视器;如果获取不到监视器,表示有其他线程通过该对象执行同步方法或同步代码块,则会进入等待。所以,监视器的作用就相当于进入区和退出区的作用。
例如:定义两种线程——生产者(Producer)和消费者(Consumer),生产者每次会产生一个数,消费者每次会取出一个数。Producer和Consumer线程对象通过同一个Store对象来调用Store的同步方法。
1 class Store { 2 3 private int value; 4 5 public synchronized int getValue() { 6 System.out.println("-取出" + value); 7 return value; 8 } 9 10 public synchronized void setValue(int value) { 11 this.value = value; 12 System.out.println("放入" + value); 13 } 14 15 }
1 @Test 2 void test() { 3 Store store = new Store(); 4 Thread producer = new Producer(store); // 继承Thread类的线程类对象的初始化 5 Thread consumer = new Thread(new Consumer(store)); // 实现Runnable接口的线程类对象的初始化 6 producer.start(); 7 consumer.start(); 8 }
部分输出结果:
当Consumer线程对象调用getValue()方法时,会获取监视器,锁定Store对象,直到方法返回后解锁Store对象,释放监视器;当Producer线程对象调用setValue()方法时也是如此。所以在创建Producer和Consumer线程对象时需要传入同一个Store对象。如果传入不同的Store对象,每一个Store对象都有一个监视器,则起不到锁定的效果。
根据输出结果可以发现:取出多次数后才放入一次数,放入多次数后才取出一次数。要实现放入一个数后取出一个数的效果,则需要添加一个标识量。
改进:在Store类中添加一个mutex标识量,当mutex为true时,表示Store内存了一个数,等待Consumer来取;为false时,表示Store内没有数,等待Producer生产数。
1 class Store { 2 3 private int value; 4 private boolean mutex; // mutex初始值为false,表示没有数 5 6 public synchronized int getValue() { 7 while (! mutex) { // mutex为false时进入等待 8 try { 9 wait(); 10 } catch (InterruptedException e) { 11 e.printStackTrace(); 12 } 13 } 14 System.out.println("-取出" + value); 15 mutex = false; // 取出数后将mutex置为false 16 notify(); 17 return value; 18 } 19 20 public synchronized void setValue(int value) { 21 while (mutex) { // mutex为true时进入等待 22 try { 23 wait(); 24 } catch (InterruptedException e) { 25 e.printStackTrace(); 26 } 27 } 28 this.value = value; 29 System.out.println("放入" + value); 30 mutex = true; // 放入数后将mutex置为true 31 notify(); 32 } 33 34 }
部分输出结果: