一、简介
1
、什么是线程
要说线程,就必须先说说进程,进程就是程序的运行时的一个实例。线程呢可以看作单独地占有
CPU
时间来执行相应的代码的。对早期的计算机(如
DOS
)而言,线程既是进程,进程既是进程,因为她是单线程的。当然一个程序可以是多线程的,多线程的各个线程看上去像是并行地独自完成各自的工作,就像一台一台计算机上运行着多个处理机一样。在多处理机计算机上实现多线程时,它们确实可以并行工作,而且采用适当的分时策略可以大大提高程序运行的效率。但是二者还是有较大的不同的,线程是共享地址空间的,也就是说多线程可以同时读取相同的地址空间,并且利用这个空间进行交换数据。
2
、为什么要使用线程
为什么要使用多线程呢?学过《计算机体系结构》的人都知道。将顺序执行程序和采用多线程并行执行程序相比,效率是可以大大地提高的。比如,有五个线程
thread1, thread2, thread3, thread4, thread5
,所耗的
CPU
时间分别为
4
,
5
,
1
,
2
,
7
。(假设
CPU
轮换周期为
4
个
CPU
时间,而且线程之间是彼此独立的)顺序执行需要花费
19
个
CPU
时间,而并行需要的时间肯定少于
19
个
CPU
时间,至于具体多少时间要看那些线程是可以同时执行的。这是在非常小规模的情况下,要是面对大规模的进程之间的交互的话,效率可以表现得更高。
3
、
java
中是如何实现多线程的
与其他语言不一样的是,线程的观念在
java
是语言中是重要的,根深蒂固的,因为在
java
语言中的线程系统是
java
语言自建的,
java
中有专门的支持多线程的
API
库,所以你可以以最快的速度写一个支持线程的程序。在使用
java
创建线程的时候,你可以生成一个
Thread
类或者他的子类对象,并给这个对象发送
start()
消息(程序可以向任何一个派生自
Runnable
接口的类对象发送
start()
消息的),这样一来程序会一直执行,直到
run
返回为止,此时该线程就死掉了。
在
java
语言中,线程有如下特点:
§
在一个程序中而言,主线程的执行位置就是
main
。而其他线程执行的位置,程序员是可以自定义的。值得注意的是对
Applet
也是一样。
§
每个线程执行其代码的方式都是一次顺序执行的。
§
一个线程执行其代码是与其他线程独立开来的。如果诸线程之间又相互协作的话,就必须采用一定的交互机制。
§
前面已经说过,线程是共享地址空间的,如果控制不当,这里很有可能出现死锁。
各线程之间是相互独立的,那么本地变量对一个线程而言就是完全独立,私有的。所以呢,线程执行时,每个线程都有各自的本地变量拷贝。对象变量
(instance variable)
在线程之间是可以共享的,这也就是为什么在
java
中共享数据对象是如此的好用,但是
java
线程不能够武断地访问对象变量:他们是需要访问数据对象的权限的。
二、准备知识
在分析这个例子之前,然我们先看看关于线程的几个概念,上锁,信号量,和
java
所提供的
API
。
上锁
对于大多数的程序而言,他们都需要线程之间相互的通讯来完成整个线程的生命周期,二实现线程之间同步的最简单的办法就是上锁。为了防止相互关联的两个线程之间错误地访问共享资源,线程需要在访问资源的时候上锁和解锁,对于锁而言,有读锁,写锁和读写锁等不同的同步策略。在
java
中,所有的对象都有锁;线程只需要使用
synchronized
关键字就可以获得锁。在任一时刻对于给定的类的实例,方法或同步的代码块只能被一个线程执行。这是因为代码在执行之前要求获得对象的锁。
信号量
通常情况下,多个线程所访问为数不多的资源,那怎么控制呢?一个比较非常经典而起非常简单的办法就是采用信号量机制。信号量机制的含义就是定义一个信号量,也就是说能够提供的连接数;当有一个线程占用了一个连接时,信号量就减一;当一个线程是放了连接时,信号量就加一。采用这种方法就可以简单有效地控制线程的同步问题,而且实现起来也特别方便。看下面的代码:
class Semaphore {
private int count;
public Semaphore(int count) {
this.count = count;
}
public synchronized void acquire() {
while(count == 0) {
try {
wait();
} catch (InterruptedException e) {
//keep trying
}
}
count--;
}
public synchronized void release() {
count++;
notify(); //alert a thread that´
s blocking on this semaphore
}
}
java
中提供了哪些
api
以编写多线程程序
这里只列出几个常用的方法和属性值。
属性值,有三个
MAX_PRIORITY
,
MIN_PRIORITY
,
NORM_PRIORITY
方法:
Thread(); //
建立一个线程
void run(); //
对于一个继承了
Runnable
接口的
class
而言,
//
他运行一个线程
,
否着他什么都不做
void setPriority(int newPriority); //
设置优先级
void start(); //
运行一个程序
void sleep(long millis); //
线程睡眠
millis
毫秒
static void yield(); //
临时
pause
一个程序以便起他线程运行
三、程序示例
例一、
让我们看看下面的例子。取钱的流程是输入密码,然后确定要取得金额,如果所取的金额小于或等于可以取出的金额,
WITHDRAW
则返回
TRUE
,然后
ATM
机出钱,然后打印清单;否则返回
FALSE
,然后打印清单。如下图:
public class AutomatedTellerMachine extends Teller {
public void withdraw(float amount) {
Account a = getAccount();
if (a.deduct(amount))
dispense(amount);
printReceipt();
}
}
public class Account {
private float total;
public boolean deduct(float t) {
if (t <= total) {
total -= t;
return true;
}
return false;
}
}
就这个例子而言,假设有这种情况,对同一个账号可以在不同的地方取钱,在同一时间,不同地点,妻子和丈夫取钱,妻子输入了账号上的最大金额,丈夫也是一样,假如妻子输入后已经得到
true
的返回值,但是丈夫的线程所得到的值还没有更新,这样丈夫也能够得到
true
的返回值,这样就出现了问题!这个问题怎么解决呢?在
java
里面提供了控制机制以保证
deduct
操作时的原子性,那就是关键字
synchronized
。
在
Account
的
deduct
方法加入
synchronized
就可以解决这个问题。
例二、
在这里我们用多线程中最典型的例子,生产者与消费者问题。在这个例子里面我们定义了生产者
Producer
,消费者
Consumer
和仓库
Warehouse
三个类,在整个程序的生命周期里,生产者随机地制造出产品放到仓库中,消费者也是随即地从仓库中取出产品。
import exception.ProducerConsumerException;
/**
* Consumer.java
* Consumer
* By: Jiabo
* Date: Mar 21, 2004
* Time: 2:47:58 PM
*/
public class Consumer extends Thread {
private Warehouse warehouse;
private String id;
public Consumer(Warehouse warehouse, String id) {
this.warehouse = warehouse;
this.id = id;
}
public void run() {
int tmp = (int) Math.random() * 10;
try {
warehouse.get(tmp);
System.out.println("Consumer # " + this.id + " get " + tmp);
} catch (ProducerConsumerException e) {
e.printStackTrace();
}
try {
sleep((int) (Math.random() * 100));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在这个类中,值得注意的一点是
run
方法中必须使用
try-catch
,因为,消费者从仓库中取东西时有可能诸如仓库中的储量不够得异常,在消费者里面也是一样,只不过异常变为仓库已满。
import exception.*;
/**
* Producer.java
* Producer
* By: Jiabo
* Date: Mar 21, 2004
* Time: 2:47:45 PM
*/
public class Producer extends Thread {
private Warehouse warehouse;
private String id;
public Producer(Warehouse warehouse, String id) {
this.warehouse = warehouse;
this.id = id;
}
public void run() {
int tmp = (int) Math.random() * 10;
if (tmp != 0) {
try {
warehouse.put(tmp);
System.out.println("Consumer # " + this.id + " put " + tmp);
} catch (ProducerConsumerException e) {
e.printStackTrace();
}
}
try {
sleep((int) (Math.random() * 100));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
最重要的一部分在
Warehouse
类,如上所说为了保证
get
何
set
的原子性,在这里使用了
synchronized
关键字,并且在操作时抛出了可能跑出的异常。
import exception.*;
/**
* Warehouse
* By: Jiabo
* Date: Mar 21, 2004
* Time: 2:48:10 PM
*/
public class Warehouse {
// max capability of the warehouse
private int MAX;
private int contents;
// init with max capacity
public Warehouse(int max) {
this.MAX = max;
this.contents = 0;
}
public synchronized void get(int amount) throws ProducerConsumerException {
// the amount you want to get is bigger than the contends that the warehouse stores
if (amount > this.contents) {
throw new NotEnoughGoodsException();
}
amount -= contents;
}
public synchronized void put(int amount) throws ProducerConsumerException {
// the amount you want to put is out of the capability of the warehouse
if (amount > (this.MAX - this.contents)) {
throw new WarehouseFullException();
} else if (this.contents == 0) {
// warehouse is empty
throw new WarehouseEmptyException();
}
amount += contents;
}
}