在介绍Java多线程编程之前,首先我们需要掌握线程的概念,即线程是什么。要了解线程是什么,还需要掌握进程的概念。
进程指的就是一个执行程序单位。在计算机发展初期,操作系统是单进程的,如DOS系统,这样一旦有病毒侵入,电脑立即死机,因为单进程环境下,只会有一个进程参与运行,这也显示单进程操作系统的不足。后期,出现Windows操作系统,它是支持多进程的,一个时间段可以有多个进程同时运行,因此即使有病毒侵入,也不会影响其它程序的运行。但是这里需要注意的是,多进程可以在同一时间段同时运行,同一时间点只能运行一个进程。而多进程同时运行,正是由于CPU的轮询机制非常快,我们感觉不到,所以就认为是同时运行,比如,现在我们可以在编辑word的同时,听听音乐,那么这里的word程序和音乐播放器程序是同时运行的两个进程,正是由于CPU轮询的非常快。这样可以看出,多进程提高了CPU的使用效率,明显优于单进程。
线程之于进程,就相当于原子之于分子,一个化学分子由多个原子组成,同样线程也是对执行程序单位的进一步划分,一个进程可以由多个线程参与运行,它们的主要区别在于,进程有自己的内存空间,而进程的多个线程共享该进程的内存空间。我们也可以发现很多多线程运行的例子,例如我们打开Eclipse编辑器,在编写Java代码时,如果出现明显的错误,则编辑器会自动出现红色提示,这种红色提示说明在Eclipse程序中包含有多个线程同时运行,其中有一个线程就是对我们代码错误进行检查。多线程提高了执行程序的运行效率,因为一旦CPU轮询到该执行程序,我们使用多线程参与运行明显没有浪费这次CPU。
相信现在大家对多线程的概念有一定的了解了。那么线程的五种状态有必要介绍一下:新建,就绪,运行,阻塞,死亡状态,下面的线程状态图很好地展示了各个状态及其产生原因,该图借鉴于(http://wangqiang6028.iteye.com/blog/1887342)。
现在看不懂没关系,等到下面先创建出一个进程,之后大家就知道它是怎么回事了,以及怎样使线程处于就绪,运行阻塞和死亡状态。线程的创建,Java给我们提供了两种方式:1)继承Thread类;2)实现Runnable接口
先来介绍一下这两种方式:继承Thread类非常简单,方便,该类实例化对象可以直接通过调用start()方法使其运行;实现Runnable接口的实例化对象必须由Thread包裹起来才可以运行,即将实现Runnable接口的示例对象作为参数传递给Thread构造函数,然后再调用start()方法;由于Java的单继承特性,继承了Thread类之后,就不可以再继承其它类,因此一般采用实现Runnable接口的方式,保留一个继承类的权利。从JDK中Thread类的源代码可以看出,Thread类也实现了Runnable接口,实际上是一种代理设计模式,真实场景还是Runnable接口,而Thread类是一种代理场景,它提供了更多的功能,其start方法为执行线程分配资源,并间接调用Runnable类的run方法。
1.继承Thread类方式,创建线程并运行。
public class TestMultiThread{ public static void main(String[] args) throws Exception { Worker worker = new Worker(); worker.start(); } } class Worker extends Thread { public void run(){ System.out.println("hello"); } }
还可以获取当前线程名称。
public class TestMultiThread{ public static void main(String[] args) throws Exception { Worker worker1 = new Worker(); Worker worker2 = new Worker(); worker1.start(); worker2.start(); } } class Worker extends Thread { public void run(){ System.out.println(Thread.currentThread().getName() + "---->hello"); } }
输出如下:
Thread-1---->hello
Thread-0---->hello
2.以继承Runnable接口方式创建线程,并运行
public class TestMultiThread{ public static void main(String[] args) throws Exception { Worker worker = new Worker(); Thread t1 = new Thread(worker,"worker1"); Thread t2 = new Thread(worker,"worker2"); t1.start(); t2.start(); } } class Worker implements Runnable { public void run(){ System.out.println(Thread.currentThread().getName() + "---->hello"); } }
输出如下:
worker1---->hello
worker2---->hello
我们发现现在线程有了新的名字,那是因为我们在使用woker实例对象来实例化Thread对象时,提供了名称。
多线程并发运行能够很好地提高执行程序的运行效率,也可以方便快速地解决很多实际问题,但是多线程并发运行的环境下,一旦多个线程共享争用同一资源,这时便可能会发生一些更新错误。例如线程A在使用该共享资源时,还没有放弃使用权,线程B也获得CPU使用权,也来使用该共享资源并对其进行修改,可是线程A还不知道情况,只是知道自己刚使用该共享资源的初始状态,这就会造成读取的写入错误。下面以经典的卖票程序来模拟多线程环境下的并发争用共享资源问题。
public class TestMultiThread{ public static void main(String[] args) throws Exception { SoldWindow sw = new SoldWindow(); Thread t1 = new Thread(sw,"window1"); Thread t2 = new Thread(sw,"window2"); t1.start(); t2.start(); } } class SoldWindow implements Runnable { private int tickets = 5;//shared resource public void run(){ for(int i=0;i<20;i++){ if(this.tickets > 0){ try{ Thread.sleep(200); }catch(Exception e){ } System.out.println(Thread.currentThread().getName() + " sold No." + this.tickets + " tickets!"); this.tickets--; } } } }
程序输出如下:
window1 sold No.5 tickets!
window2 sold No.5 tickets!
window1 sold No.3 tickets!
window2 sold No.3 tickets!
window1 sold No.1 tickets!
window2 sold No.1 tickets!
以上输出肯定是不合理的,导致这种不合理的关键在于由于售票窗口网络的延迟,本来一共有5张票,窗口1本来想取第5张票卖出去,可是网络延迟了,它只好等待200毫秒,这个时候窗口2抢到了资源,并卖出了第5张票,把票数更改为4,可是窗口1延迟结束,它的初始票数就是5,所以它也卖出了第5张票,并把票数更改为3。解决这种方法的关键在于,不要让多个线程同时有机会接触共享资源,对于共享资源,一定要保证一次只能由一个线程使用,必须等到该线程使用完,其他线程才能获得使用权,即保证操作的原子性。给容易发生资源抢占的地方(代码段)加锁,在Java中称为同步对象锁,可以成功保证此段代码某个时间段只能被一个线程执行,执行结束,自动释放锁。
class SoldWindow implements Runnable { private int tickets = 5;//shared resource public void run(){ for(int i=0;i<20;i++){ synchronized(this){ if(this.tickets > 0){ try{ Thread.sleep(200); }catch(Exception e){ } System.out.println(Thread.currentThread().getName() + " sold No." + this.tickets + " tickets!"); this.tickets--; } } } } }
修改售票窗口类的代码如上,主要是为与tickets变量有关联的代码段加上同步锁,这样便可以正常输出了:
window2 sold No.5 tickets!
window1 sold No.4 tickets!
window2 sold No.3 tickets!
window2 sold No.2 tickets!
window2 sold No.1 tickets!
也可以以同步方法的方式,解决如下:
class SoldWindow implements Runnable { private int tickets = 5;//shared resource public void run(){ for(int i=0;i<20;i++){ sale(); } } public synchronized void sale(){ if(this.tickets > 0){ try{ Thread.sleep(200); }catch(Exception e){ } System.out.println(Thread.currentThread().getName() + " sold No." + this.tickets + " tickets!"); this.tickets--; } } }
输出与上面一样,同样能达到同步解决的目的。同步要恰当使用,过多使用同步则会造成死锁,死锁的发生在于,线程1在等待线程2释放锁,而线程2也在等待线程1释放锁,因此二者僵持不下,造成程序无法运行。下面以一个例子介绍死锁,售货员等着顾客给钱给他,然后再给他食物;顾客等着售货员给食物给他,然后再给他钱。双方都在等着对方。
class Saler{ public synchronized void say(Person per){ System.out.println("Give me money, I will give you food"); try{ Thread.sleep(500); }catch(Exception e){ } per.give(); } public synchronized void give(){ System.out.println("give you food, I get lots of money"); } } class Person{ public synchronized void say(Saler sal){ System.out.println("Give me food, I will give you money"); try{ Thread.sleep(500); }catch(Exception e){ } sal.give(); } public synchronized void give(){ System.out.println("give you money, I eat lost of food"); } } class DeadLock implements Runnable { private Saler sal = new Saler(); private Person per = new Person(); public DeadLock(){ new Thread(this).start(); sal.say(per); } public void run(){ per.say(sal); } }
只有在主程序main函数加入一条语句创建死锁线程,new DeadLock(),程序输出如下:
Give me money, I will give you food
Give me food, I will give you money
程序卡死在这里,无法继续运行下去。将线程类构造函数里的两个语句颠倒顺序将得到正常输出如下:
Give me money, I will give you food
give you money, I eat lost of food
Give me food, I will give you money
give you food, I get lots of money
下面以多线程中一个经典例子,生产者与消费者来作为本文的结束。伴随此例子,会介绍Obejct自带的wait和notify方法来改变线程执行状态,从而实现线程互斥运行,协同工作。
class GoodStack{ private char goods[] = null; private int size = 5; private int count = 0; public GoodStack(){ this(5);//default size is 5 } public GoodStack(int size){ this.size = size; this.goods = new char[size]; } public boolean isFull(){ if(this.count >= this.size) return true; else return false; } public boolean isEmpty(){ if(this.count == 0) return true; else return false; } public synchronized void push(char c){ if(this.isFull()){ System.out.println("GoodStack is full"); try{ super.wait(); }catch(Exception e){ } } this.goods[count++] = c; System.out.println("produce: " + c); super.notify(); } public synchronized char pop(){ if(this.isEmpty()){ System.out.println("GoodStack is empty"); try { super.wait(); } catch (Exception e) { } } char c = this.goods[--count]; System.out.println("consume: " + c); super.notify(); return c; } } class Producer implements Runnable { private GoodStack stack = null; public Producer(GoodStack stack){ this.stack = stack; } public void produce(){ for(int i=0;i<10;i++){ char c = (char)('a' + i); this.stack.push(c); } } public void run(){ this.produce(); } } class Consumer implements Runnable { private GoodStack stack = null; public Consumer(GoodStack stack){ this.stack = stack; } public void consume(){ for(int i=0;i<10;i++){ this.stack.pop(); } } public void run(){ try { Thread.sleep(100); } catch (Exception e) { } this.consume(); } } public class TestMultiThread{ public static void main(String[] args) throws Exception { //mutual exclusion in producer and consumer GoodStack stack = new GoodStack(); Producer producer = new Producer(stack); Consumer consumer = new Consumer(stack); new Thread(producer).start(); new Thread(consumer).start(); } }
程序输出如下:
produce: a
produce: b
produce: c
produce: d
produce: e
GoodStack is full
consume: e
produce: f
GoodStack is full
consume: f
consume: d
consume: c
consume: b
consume: a
GoodStack is empty
produce: g
produce: h
produce: i
produce: j
consume: j
consume: i
consume: h
consume: g
Obejct类提供了三个方法支持多线程,分别是wait(),notify(),notifyAll()。实际上是切换线程的互斥运行状态,这些方法必须在同步方法中才可以调用。wait方法会抛出异常,线程互斥的关键逻辑在于:
一旦栈满了,生产者线程wait,交出CPU使用权,并随时唤醒消费者线程进行消费;
一旦栈空了,消费者线程wait,交出CPU使用权,并随时唤醒生产者线程再次生产。