线程
线程是进程中的一个单一的连续控制流程,一个进程可以拥有多个线程。线程又称为轻量级进程,它和进程一样拥有独立的执行控制,由操作系统负责调度,区别在于线程没有独立的存储空间,而是和所属进程中的其他线程共享一个存储空间,这使得线程间的通信远比进程简单。线程本身的数据通常只有寄存器数据,以及一个程序执行时使用的堆栈,所以线程的切换比进程切换的负担要小的多。线程也是动态的,具有一定的生命周期,分别经历从创建、就绪、执行、阻塞直到消亡的过程。下面是一个线程状态转换图:
创建线程(new)
当利用new关键字创建线程对象实例后,它仅仅作为一个对象实例存在,JVM没有为其分配CPU时间片等待线程运行资源。要想真正运行这个线程,就得调用start()方法,这时线程处于就绪状态。
就绪状态(runnable)
在处于新建状态的线程中调用start()方法将线程的状态转换为就绪状态。这个时候,线程已经得到了除CUP时间之外的其他系统资源,只等JVM的线程调度器按照线程的优先级对该线程进行调度,从而使该线程拥有能够获得CPU时间片的机会,一旦该线程获得CPU时间片,就进入运行状态。
运行状态(running)
线程获得CPU时间片之后就进入运行状态,这个时间片过后,CPU又要调度其他线程来运行,这个时候该线程又进入了就绪状态。运行的线程也可以调用yield()自动放弃CPU,从而回到就绪状态,以便其他线程能够运行。当运行着的线程调用sleep()、wait()或者进入synchronized代码保护区又没获得锁时,就进入了阻塞状态。
阻塞状态(blocked)
阻塞指的是暂停一个线程的执行以等待某个条件发生,若线程处于阻塞状态,调度机制不给它分配任何CPU时间,直接跳过它。比如要读入磁盘文件数据,读磁盘是非常慢的操作,CPU就可以把等待文件数据到来的时间去运行其他的线程,当前线程就进入了阻塞状态,磁盘文件数据到来时该线程就进入了就绪状态等待CPU调度。
死亡状态(dead)
当线程体运行结束或者调用线程对象的stop方法后线程将终止运行,由JVM收回线程占用的资源。
对象的互斥锁
在并发程序中,对多线程共享的资源或数据称为临界资源,而把每个线程中访问临界资源的那一段代码段称为临界代码段。通过为临界代码段的设置,就可以保证资源的完整性,从而安全地访问共享资源。为了实现这种机制,Java语言提供了以下两方面的支持:
(1)为每个对象设置了一个“互斥锁”标记。该标记保证在每一个时刻,只能有一个线程拥有该互斥锁,其他线程如果需要获得该互斥锁,必须等待当前拥有该锁的线程将其释放,该对象就成了一个互斥对象。
(2)为了配合使用对象的互斥锁,Java语言提供了保留字synchronized。如下:
synchronized(互斥对象){
临界代码段
}
当一个线程执行到该代码段时,首先检测该互斥对象的互斥锁。如果该互斥锁没有被别的线程所拥有,则该线程获得该互斥锁,并执行临界代码段,直到执行完毕并释放互斥锁;如果该互斥锁已被其他线程占用,则该线程自动进入该互斥对象的等候队列,等待其他线程释放该互斥锁。
class
ShareData
... {
int sharedata;
public ShareData(int sharedata)
...{
this.sharedata=sharedata;
}
// public synchronized void method(int sharedata)
public void method(int sharedata)
...{
synchronized(this)
...{
if(this.sharedata>=sharedata)
...{
this.sharedata=this.sharedata-sharedata;
System.out.println("success!");
}
else
...{
System.out.println("failed!");
}
}
}
}
public class MultiThread extends Thread ... {
ShareData sd;
public MultiThread(ShareData sd)
...{
this.sd=sd;
}
public void run()
...{
sd.method(100);
}
public static void main(String[] args) ...{
ShareData sd = new ShareData(100);
MultiThread mt1 = new MultiThread(sd);
MultiThread mt2 = new MultiThread(sd);
mt1.start();
mt2.start();
}
}
... {
int sharedata;
public ShareData(int sharedata)
...{
this.sharedata=sharedata;
}
// public synchronized void method(int sharedata)
public void method(int sharedata)
...{
synchronized(this)
...{
if(this.sharedata>=sharedata)
...{
this.sharedata=this.sharedata-sharedata;
System.out.println("success!");
}
else
...{
System.out.println("failed!");
}
}
}
}
public class MultiThread extends Thread ... {
ShareData sd;
public MultiThread(ShareData sd)
...{
this.sd=sd;
}
public void run()
...{
sd.method(100);
}
public static void main(String[] args) ...{
ShareData sd = new ShareData(100);
MultiThread mt1 = new MultiThread(sd);
MultiThread mt2 = new MultiThread(sd);
mt1.start();
mt2.start();
}
}
上面的例子是两个线程共享对象的变量上如果大于或者等于100就减去100,synchronized加锁的对象可以用this指是当前对象,也可以直接在方法的权限关键字之后使用,两者的效果是一样的。
线程同步
在实际应用中,多个线程之间不仅需要互斥机制来保证对共享数据的完整性,而且有时需要多个线程之间互相协作,按照某种既定的步骤共同完成任务。关于线程同步,要记住下面几点:
1.线程同步就是为了避免多个线程同时访问共享数据,也就是需要线程排队访问,线程同步就是线程排队。
2.只有共享资源的读写访问时才需要用到线程同步,如果不是共享资源就根本没必要使用线程同步。
3.只有共享变量才需要线程同步,如果是常量多个线程访问都不会变的就无需使用线程同步。至少有一个线程会修改共享资源的,这个时候就需要使用线程同步了。
4.多个线程访问的可能是同一份代码,也可能不是同一份代码;但是无论是否执行同一份代码,只要在这些代码中访问到了同一份共享的变量,这个时候就需要使用线程同步。
下面是一个很经典的生产者-消费者模型的例子。
class
ProduceComsumer
... {
long number; //并发访问共享变量
ProduceComsumer()
...{
this.number=0;
}
public synchronized void Produce() //生产者
...{
if(number!=0)
...{
try
...{
wait();
}
catch(Exception e)
...{
e.printStackTrace();
}
}
number = System.nanoTime();
System.out.println("Produced number = " + number);
notify();
}
public synchronized void Comsumer() //消费者
...{
if(number==0)
...{
try
...{
wait();
}
catch(Exception e)
...{
e.printStackTrace();
}
}
System.out.println("The number is " + number + " and Comsumed");
number = 0;
notify();
}
}
class ProduceThread extends Thread // 生产线程
... {
ProduceComsumer pc;
public ProduceThread(ProduceComsumer pc)
...{
this.pc = pc;
}
public void run()
...{
int i = 0;
while(i++ != 10)
...{
this.pc.Produce();
}
}
}
class ComsumerThread extends Thread // 消费线程
... {
ProduceComsumer pc;
public ComsumerThread(ProduceComsumer pc)
...{
this.pc = pc;
}
public void run()
...{
int i =0;
while(i++ != 10)
...{
this.pc.Comsumer();
}
}
}
public class ThreadSynchronized ... {
public static void main(String [] args)
...{
ProduceComsumer pc = new ProduceComsumer();
ProduceThread pt = new ProduceThread(pc);
ComsumerThread ct = new ComsumerThread(pc);
ct.start();
pt.start();
}
}
... {
long number; //并发访问共享变量
ProduceComsumer()
...{
this.number=0;
}
public synchronized void Produce() //生产者
...{
if(number!=0)
...{
try
...{
wait();
}
catch(Exception e)
...{
e.printStackTrace();
}
}
number = System.nanoTime();
System.out.println("Produced number = " + number);
notify();
}
public synchronized void Comsumer() //消费者
...{
if(number==0)
...{
try
...{
wait();
}
catch(Exception e)
...{
e.printStackTrace();
}
}
System.out.println("The number is " + number + " and Comsumed");
number = 0;
notify();
}
}
class ProduceThread extends Thread // 生产线程
... {
ProduceComsumer pc;
public ProduceThread(ProduceComsumer pc)
...{
this.pc = pc;
}
public void run()
...{
int i = 0;
while(i++ != 10)
...{
this.pc.Produce();
}
}
}
class ComsumerThread extends Thread // 消费线程
... {
ProduceComsumer pc;
public ComsumerThread(ProduceComsumer pc)
...{
this.pc = pc;
}
public void run()
...{
int i =0;
while(i++ != 10)
...{
this.pc.Comsumer();
}
}
}
public class ThreadSynchronized ... {
public static void main(String [] args)
...{
ProduceComsumer pc = new ProduceComsumer();
ProduceThread pt = new ProduceThread(pc);
ComsumerThread ct = new ComsumerThread(pc);
ct.start();
pt.start();
}
}
wait()和notify()这两个方法必须位于临界代码段中。也就是说,执行该方法的线程必须已获得了互斥对象的互斥锁。这是因为这两个方法实际上也是操作互斥对象的互斥锁:当一个线程调用wait()方法进入阻塞状态,同时会释放互斥对象的互斥锁;只有当另一个线程调用互斥对象的notify()方法被调用时,该互斥对象等待队列中的第1个线程才能进入就绪状态。这也就是为什么这两个方法是作为互斥对象的方法来实现,而不是作为Thread类的方法实现的原因。sleep()是作为Thread类的方法实现的,当一个线程通过调用sleep()方法进入阻塞状态时,它并不放弃对象的互斥锁,也就是说该线程可能仍然拥有对象的互斥锁。wait()和notify()必须同时配对使用。当某个线程由于调用某个互斥对象的wati()方法进入阻塞状态,只有另一个线程调用该互斥对象的notify()方法才能唤醒该线程,使其进入就绪状态,否则该线程将永远处于阻塞状态。notifyAll()方法使所有想获得该互斥锁的线程都唤醒,但是只有一把锁,就让他们自己根据优先级等竞争那唯一的锁,竞争到的线程执行,其他线程继续wait。
线程通信
线程之间的通信是指线程之间相互传递信息,这些信息包括数据、控制指令等。数据共享也是一种线程通信方式,还有一种是输入输出流中的线程管道。管道有以下几个特点:
1.管道是单向的。一个线程充当发送者,另一个线程充当接受者。建立一个输入管道和一个输入管道,这两个必须是成对出现的。
2.线程管道必须是面向连接的。因此在程序设计中,一方线程必须建立起对应的端点,由另一方线程来建立连接,也就是输入管道和输出管道要连接起来。
3.管道中的数据严格按照发送的顺序进行传送的。因此接受方收到的数据和发送方发送的数据完全一致。
线程管道就一种特殊的IO流,也有面向字节流和字符流的类。一对是PipedOutputStream和PipedInputStream,用于建立基本字节的管道通信;另一对是PipedWriter和PipedReader,用于基本字符的管道通信。
线程死锁
在程序运行中,多个线程竞争共享资源可能会发生如下的状态:线程1拥有资源1并等待资源2,线程2拥有资源2并等待资源3,线程3拥有资源3并等待资源1。在这种状态下,所有线程等待的资源永远也获得不了,永远等待下去,这个就是线程死锁。需要指出的是,线程死锁并不是必然会发生的,在某些情况下会非常偶然发生的。死锁这种状态出现的机会是非常小的,因此简单的测试往往也无法发现。一般来说,要出现死锁必须同时满足下面四个条件:
1. 互斥条件。至少存在一个资源,不能被多个线程同时共享的。
2. 至少存在一个线程,它拥有一个资源,并等待获得另一个线程当前所拥有的资源。
3. 线程拥有的资源不能被强行剥夺,只能有线程资源释放。
4. 线程对资源的请求形成一个环形。
所以,在设计多线程并发时,如果可能会出现死锁的情况,就破坏上面的任意一个条件,就不会发生死锁了。