暑假闲得无聊,便读了疯狂java讲义的第16章补充了一下多线程的知识顺便做了一下书后习题练练,把解答和感想一并发到网上与大家讨论。
习题1.写2个线程,其中一个线程打印1~52,另一个线程打印A~Z,打印顺序应该是12A34B56C……5152Z。该习题需要利用多线程通信的知识。
思路:按照题目的意思是要每隔两个数字打印一个字母,需要用到进程通信,我们这里先建立一个专门用来充当同步监听器的类PrintMonitor,里面有一个同步方法printAll用于打印数组,每隔一定的间隔便通知其他线程苏醒(notifyAll)然后自己放弃同步监听器(wait),两个打印线程我打算使用同一个线程类PrintThread,所以PrintThread类需要有属性monitor(同步监听器),arr(需要打印的数组),interval(打印间隔)三个属性方便main方法向其传参,在main方法中先让打印数字的线程开始,之后让主线程休眠1毫秒,之后再开启打印字母的线程,目的是为了确保打印数字的线程先开始,如果不休眠的话经过测试,有的时候会导致打印字母的线程先开始.
源代码如下:
以下类文件在同一包下,主函数为Main类的main()方法
public class PrintMonitor {
public synchronized void printAll(String[] arr, int interval){
for ( int i = 0; i < arr.length; i++){
System.out.print(arr[i]);
if ( (i + 1) % interval == 0 ){
this.notifyAll();
myWait();
}
}
}
//出于方便将wait方法封装了一下
private void myWait(){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class PrintThread extends Thread{
PrintMonitor monitor;
String[] arr;
int interval;
public PrintThread(PrintMonitor monitor, String[] arr, int interval) {
this.monitor = monitor;
this.arr = arr;
this.interval = interval;
}
@Override
public void run() {
monitor.printAll(arr, interval);
}
}
public class Main {
public static void main(String[] args) {
//数字数组
String arr1[] = new String[52];
//字母数组
String arr2[] = new String[26];
for (int i = 1; i < 53; i++) {
arr1[i - 1] = i + "";
}
for (char i = 65; i < 65 + arr2.length; i++){
arr2[i - 65] = i + "";
}
PrintMonitor pm = new PrintMonitor();
PrintThread t1 = new PrintThread(pm, arr1, 2);
PrintThread t2 = new PrintThread(pm, arr2, 1);
t1.start();
//为了确保t1在t2之前开始运行
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
反复运行结果:
12A34B56C78D910E1112F1314G1516H1718I1920J2122K2324L2526M2728N2930O3132P3334Q3536R3738S3940T4142U4344V4546W4748X4950Y5152Z
解题反思:
- 虽然Thread的run方法没有方法参数,但是可以通过PrintThread的成员变量在创建对象时传递变量给run方法
- 在多线程程序中可以建立专门的同步监听器类,例如本题中的PrintMonitor类,然后在PrintThread中也要有一个存放同步监听器的域,对于需要通信的若干的PrintThread对象要在该域中存放同一个PrintMonitor监听器对象
习题2.假设车库有3个车位(可以用boolean[]数组来表示车库)可以停车,写一个程序模拟多个用户开车离开、停车入库的效果。注意:车库有车时不能停车
解法一:
思路:与上一题类似,建立Park类表示停车场,作为同步监听器,属性里面有一个三元的boolean数组作为成员变量表示停车位(true表示有车,false表示无车),有两个同步方法,park()表示停车,反复检查车库是否已满,如果检测出车库已满则放弃同步监听器(wait),如果不满则停车,leave()表示驶离停车场,将车驶离后,唤醒其他所有线程争抢停车位(notifyAll).创建User继承Thread类用于表示用户,用户先进行停车,休眠两秒后驶离,反复十次,这样模拟用户的行为。为了验证程序的正确与否,在Park类中我还添加了int类型的parkTotal域和leaveTotal域,对于停车场进入停车与驶离的总数进行计数,等到所有的User线程运行结束之后(将所有的User线程join进主线程),打印这两个数据,如果程序没有错误的话,这两个域中的数字应该始终是相等的.
源代码如下:
以下类文件在同一包下,主函数为Main类的main()方法
public class Park {
boolean[] cars;
//用于统计数据
int parkTotal;
int leaveTotal;
public Park(){
cars = new boolean[3];
}
public synchronized void park(){
//此处一定要用while不能用if,因为可能需要反复争抢
while ( isFull() ){
myWait();
}
//停车
cars[getOneEmpty()] = true;
parkTotal++;
show();
}
public synchronized void leave(){
//空出车位
cars[getOneCar()] = false;
this.notifyAll();
leaveTotal++;
show();
}
private boolean isFull(){
boolean flag = true;
for ( boolean place : cars ){
if ( !place ){
flag = false;
break;
}
}
return flag;
}
private int getOne(boolean flag){
int i = 0;
for ( ; i < cars.length; i++ ){
if ( cars[i] == flag ){
return i;
}
}
return -1;
}
//得到一个空车位的索引
private int getOneEmpty(){
return getOne(false);
}
//得到一个有车的车位的索引
private int getOneCar(){
return getOne(true);
}
private void myWait() {
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException();
}
}
private void show(){
for( boolean place : cars ){
System.out.print(place + " ");
}
System.out.println();
}
public int getParkTotal() {
return parkTotal;
}
public int getLeaveTotal() {
return leaveTotal;
}
}
public class User extends Thread {
Park park;
public User(Park park) {
this.park = park;
}
@Override
public void run() {
for ( int i = 0; i < 10; i++ ){
park.park();
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
park.leave();
}
}
}
public class Main {
public static void main(String[] args) {
Park park = new Park();
User u1 = new User(park);
User u2 = new User(park);
User u3 = new User(park);
User u4 = new User(park);
User u5 = new User(park);
User u6 = new User(park);
u1.start();
u2.start();
u3.start();
u4.start();
u5.start();
u6.start();
try {
u1.join();
u2.join();
u3.join();
u4.join();
u5.join();
u6.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("停车总计:" + park.getParkTotal());
System.out.println("离开总计:" + park.getLeaveTotal());
}
}
反复运行结果(仅给出最后三行输出):
false false false
停车总计:60
离开总计:60
停车总计=离开总计,程序正确.
解题反思:
- 在Park类的第15行,我在一开始写程序的时候用的是if而不是while,然后程序的结果便会不正确,研究之后发现,在线程被唤醒之后(有一辆车驶离)还要重新争抢同步监听器,如果没抢到的话就会有别人抢先把这个空出来的车位给占了,车库的状况就会发生变化,需要重新判断,所以此处应该用while,下次要注意.
- 一旦有某个用户停车或者驶离就会把整个停车场给锁住,别的线程无法进入,感觉这样不是很妥,特别是对于大停车场,明明有很多空位可以供好多辆车同时停车的,此时却只能一辆一辆地停,性能很受影响,因此我思索出了一个解法二来优化这方面的性能.
解法二:
思路:不再使用Park类作为同步监听器,而是将同步监听器细化为每一个停车位,这里建立一个ParkPlace类表示停车位,Park类中也不再使用boolean数组,而是使用ParkPlace数组,每个ParkPlace对象中有一个boolean型的域表示这个车位是否为空,当调用,调用了Park对象的park()方法后,会不断地尝试获取各个车位的锁,如果得到了某个车位的锁就进行且该车位没车就进行相关操作,leave()方法也是类似,为了实现这一点,这里不再使用synchronized来控制同步,而是给每个ParkPlace对象一个Lock域来控制同步,使用Lock的tryLock()方法尝试获得同步锁,返回true则说明成功获得锁,返回false则去尝试获得下一个车位的锁。最后会打印停车场停车与驶离次数总计,方法与解法一一样,如果两个值一样则程序正确.
源代码如下:
以下类文件在同一包下,主函数为Main类的main()方法,其中User类直接使用上一解法中的User类即可
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ParkPlace {
private String id;
private boolean status;
//用于统计数据使用
private int parkNum;
private int leaveNum;
public ParkPlace(String id) {
this.id = id;
}
private final Lock lock = new ReentrantLock();
public boolean getStatus() {
return status;
}
private void setStatus(boolean status){
if ( status ){
parkNum++;
} else {
leaveNum++;
}
this.status = status;
}
private boolean isEmpty(){
if ( status ){
return false;
}
return true;
}
public boolean park() {
if ( !lock.tryLock() ){
return false;
}
try {
if ( isEmpty() ){
//停车
setStatus(true);
System.out.println( Thread.currentThread().getName() + "在车位" + id + "停车");
} else {
return false;
}
} finally {
lock.unlock();
}
return true;
}
public boolean leave() {
if ( !lock.tryLock() ){
return false;
}
try {
if ( !isEmpty() ){
//离开
setStatus(false);
System.out.println(Thread.currentThread().getName() + "从车位" + id + "驶离");
} else {
return false;
}
} finally {
lock.unlock();
}
return true;
}
public int getParkNum() {
return parkNum;
}
public int getLeaveNum() {
return leaveNum;
}
}
public class Park {
private ParkPlace places[];
public Park(){
places = new ParkPlace[3];
for ( int i = 0; i < places.length; i++ ){
places[i] = new ParkPlace(i + "");
}
}
public void park() {
boolean flag = false;
while (true){
for ( ParkPlace place : places ){
if ( place.park() ){
flag = true;
break;
}
}
if ( flag ){
break;
}
}
}
public void leave() {
boolean flag = false;
while (true){
for ( ParkPlace place : places ){
if ( place.leave() ){
flag = true;
break;
}
}
if ( flag ){
break;
}
}
}
//数据统计使用
public int getParkTotal() {
int parkTotal = 0;
for ( ParkPlace place : places ){
parkTotal += place.getParkNum();
}
return parkTotal;
}
public int getLeaveTotal() {
int leaveTotal = 0;
for ( ParkPlace place : places ){
leaveTotal += place.getLeaveNum();
}
return leaveTotal;
}
}
public class Main {
public static void main(String[] args) {
Park park = new Park();
User u1 = new User(park);
User u2 = new User(park);
User u3 = new User(park);
User u4 = new User(park);
User u5 = new User(park);
u1.start();
u2.start();
u3.start();
u4.start();
u5.start();
try {
u1.join();
u2.join();
u3.join();
u4.join();
u5.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("停车次数总计:" + park.getParkTotal());
System.out.println("离开次数总计:" + park.getLeaveTotal());
}
}
反复运行后打印的结果(这里只给出最后三行):
Thread-3从车位2驶离
停车次数总计:50
离开次数总计:50
停车总计=离开总计,程序正确.
解题反思:
- 可以将同步锁细化来提升程序性能,在这里将整个停车场的锁细化为车位的锁来提升性能,因为细化同步锁会使得程序更加复杂,所以这种性能提升只有在停车与驶离是比较耗时的操作时才能看出来,所以在上面的题目中,经过我的测试,解法一运行速度反而更快,但是你如果有兴趣的话可以在上面程序的停车和驶离操作部分分别加一句”Thread.sleep(1000)”,”假装”是比较耗时的操作,再运行一下会发现,解法二比解法一要快好几倍.
- 仔细思考发现解法二也存在一些问题或者说是存在一些可以继续优化的地方,解法二中Park类的park()与leave()方法使用类似于轮询的方式不断地尝试各个车位,设想车位已经都满了,并且很长一段时间也没有车驶离,那么其他线程还是会不停地占用CPU进行没有意义地轮询,所以可以考虑把Park再作为一个同步监听器,在停车之前找空位时,把整个车库给锁住,发现车库已满则wait,停车时再放掉车库锁转而锁住单个车位,当有车驶离时notifyAll,相当于是解法一与解法二的混合体,虽然这个方法我没有尝试用代码写出来,但是可以预计只有在停车与驶离非常耗时且车库常常处于爆满状态时才能看出其优越性,对于常常处于盈余状态的车库解法二的优化已经足够了,算法的优化还是要依照具体情况具体分析.
解法三:
思路:这个解法三是在我距离发表这篇博文两个月后添加的,我一直以为java是不支持信号量,直到最近才偶然发现了java的Semaphore类可以实现信号量的功能。信号量根据在大学里学习操作系统这门课的经验,最适合解决生产者消费者这一类的问题,但是这个问题并不是典型的生产者消费者问题,只设置一个代表车位数的信号量是不够的,进程还需要检查(寻找空的车位)并修改车位的状态,如果多个进程进入的话,检查会出现问题。我解决方案就是直接在解法二的基础(对单个车位上锁)上进行改进,使用信号量表示停车场的空位数,在车辆进入停车场之前必须先申请到一个车位的资源,在车辆驶离时归还资源。用这种方法可以改善解法二中的cpu的轮询,当停车场爆满时,线程无法申请到资源,就会阻塞,而不会继续没有意义地占用cpu资源。同时我还改进了一下Main类中的代码,使用线程池的invokeAll()方法执行6个User线程,这个方法会等待六个线程全部执行完才会执行下面的语句,和之前的join()方法的效果是一样的,invokeAll方法只能够执行实现Callable接口的集合,为了使用这个方法,我也将user由继承Thread类改为了实现Callable接口。
源代码如下:
以下类文件在同一包下,主函数为Main类的main()方法
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Created by 燃烧杯 on 2017/7/4.
*/
public class ParkPlace {
private String id;
private boolean status;
//用于统计数据使用
private int parkNum;
private int leaveNum;
public ParkPlace(String id) {
this.id = id;
}
private final Lock lock = new ReentrantLock();
public boolean getStatus() {
return status;
}
private void setStatus(boolean status){
if ( status ){
parkNum++;
} else {
leaveNum++;
}
this.status = status;
}
private boolean isEmpty(){
if ( status ){
return false;
}
return true;
}
public boolean park() {
if ( !lock.tryLock() ){
return false;
}
try {
if ( isEmpty() ){
//停车
setStatus(true);
System.out.println( Thread.currentThread().getName() + "在车位" + id + "停车");
} else {
return false;
}
} finally {
lock.unlock();
}
return true;
}
public boolean leave() {
if ( !lock.tryLock() ){
return false;
}
try {
if ( !isEmpty() ){
//离开
setStatus(false);
System.out.println(Thread.currentThread().getName() + "从车位" + id + "驶离");
} else {
return false;
}
} finally {
lock.unlock();
}
return true;
}
public int getParkNum() {
return parkNum;
}
public int getLeaveNum() {
return leaveNum;
}
}
import java.util.concurrent.Semaphore;
/**
* Created by 燃烧杯 on 2017/7/4.
*/
public class Park {
//停车场的位置数量
public static final int N = 3;
private ParkPlace places[];
//建立一个资源数为N的信号量
Semaphore sema = new Semaphore(N);
public Park(){
places = new ParkPlace[N];
for ( int i = 0; i < places.length; i++ ){
places[i] = new ParkPlace(i + "");
}
}
public void park() {
try {
//停车之前先申请资源
sema.acquire();
for ( ParkPlace place : places ){
if ( place.park() ){
break;
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void leave() {
for ( ParkPlace place : places ){
if ( place.leave() ){
break;
}
}
//驶离之后归还资源
sema.release();
}
//数据统计使用
public int getParkTotal() {
int parkTotal = 0;
for ( ParkPlace place : places ){
parkTotal += place.getParkNum();
}
return parkTotal;
}
public int getLeaveTotal() {
int leaveTotal = 0;
for ( ParkPlace place : places ){
leaveTotal += place.getLeaveNum();
}
return leaveTotal;
}
}
import java.util.concurrent.Callable;
/**
* Created by 燃烧杯 on 2017/7/4.
*/
public class User implements Callable<Boolean> {
Park park;
public User(Park park) {
this.park = park;
}
@Override
public Boolean call() {
for ( int i = 0; i < 10; i++ ){
park.park();
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
park.leave();
}
return true;
}
}
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Created by 燃烧杯 on 2017/7/4.
*/
public class Main {
public static void main(String[] args) {
//线程池
ExecutorService exec = Executors.newFixedThreadPool(6);
Park park = new Park();
List<User> users = new ArrayList<User>();
for ( int i = 0; i < 6; i++ ){
users.add(new User(park));
}
try {
exec.invokeAll(users);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("停车次数总计:" + park.getParkTotal());
System.out.println("离开次数总计:" + park.getLeaveTotal());
//关闭线程池
exec.shutdown();
}
}
反复运行后打印的结果(这里只给出最后三行):
pool-1-thread-6从车位0驶离
停车次数总计:60
离开次数总计:60
停车总计=离开总计,程序正确。
解题反思:
- 解法三相比解法二唯一的改进就是避免在停车场爆满的情况下cpu忙等,是我到现在所能想到的最优的解法了,这题大约也可以归结为一类问题(不知道前人是否已经归结过),这一类问题比较好的解决方案是,对进程进入停车场的过程用信号量进行控制,保证进入停车场的车都能有车位,之后在找空位和停车的过程中都要采用互斥,离开的时候也必须互斥,之后归还资源
- 我之前都是采用”停车总计=离开总计”来判断程序是否正确,我在这里说明一下这种判断可以成立的原因。想想看出现并发的问题的唯一情况就是当车库一个车位已经停有车了,但是另一辆车任然停了进去,这样就出现问题,因为每一次停车对应着一次离开,如果在一个车位还有车的情况下就又停了一辆车进去,相当于“丢失”了一辆车,这样当其中一个进程想找车驶离时就会找不到车,对于解法一这个过程对应于Park类的getOne方法,这个方法在找不可以驶离的车位时会返回-1,然后这个-1会进入下标导致程序异常终止,所以解法一能够成功运行到最后就已经说明并发没有问题了,对于解法二与解法三,这个过程对应与Park类的leave方法,当发现没有车可以驶离时leave方法会直接归还资源,这样驶离计数器就不会加1,导致最终驶离的总数小于停车的总数,所以通过判断停车总数等于驶离总数能够断定程序没有出现并发问题
解法四:
思路:解法四打算换个角度解决这个问题,为了可以使用java5提供的方便的同步工具BlockingQueue,此解法跳出题目所给的必须要用boolean数组表示车位的限制,将车库看成一个长度为3的阻塞队列,调用BlockingQueue的put方法可以在队列中放入一个元素,如果发现队列满则阻塞,调用take方法可以取出队头元素,如果队列空则阻塞,由于在本题中取车的时候车库里肯定是有车的,所以此处驶离时调用的是remove方法将自己取出。不再使用之前的User类作为线程,而是换成Car类,所谓停车就是对象将自己放入阻塞队列,驶离就是将自己从阻塞队列中取出。使用这种方法可以非常是简洁的实现题目要求的功能,而且效率也很高。
源代码如下:
以下类文件在同一包下,主函数为Main类的main()方法
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
/**
* Created by 燃烧杯 on 2017/9/26.
*/
public class Car implements Callable<Boolean> {
private BlockingQueue<Car> park;
public Car(BlockingQueue<Car> park) {
this.park = park;
}
@Override
public Boolean call() throws Exception {
for ( int i = 0; i < 10; i++ ){
park.put(this);
System.out.println(Thread.currentThread() + "停车");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
park.remove(this);
System.out.println(Thread.currentThread() + "驶离");
}
return true;
}
}
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Created by 燃烧杯 on 2017/9/26.
*/
public class Main {
public static void main(String[] args) {
ExecutorService exec = Executors.newFixedThreadPool(6);
BlockingQueue<Car> park = new ArrayBlockingQueue<Car>(3);
List<Car> cars = new ArrayList<Car>();
for ( int i = 0; i < 6; i++ ){
cars.add(new Car(park));
}
long time1 = System.currentTimeMillis();
try {
exec.invokeAll(cars);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("车库剩余车辆:" + park.size());
System.out.println("总计用时:" + (System.currentTimeMillis() - time1));
}
exec.shutdown();
}
反复运行结果(此处只给出最后三行):
Thread[pool-1-thread-6,5,main]驶离
车库剩余车辆:0
总计用时:4025