Java是一个支持多线程(Multi-threading)的编程语言,JVM为多线程编程提供了内在的支持。JVM可以看做宿主系统的一个进程,不管你的程序是否显式的采用了多线程的编程方式,JVM本身总是以多线程的方式来执行程序。例如,在JVM中除了运行你个人的线程之外,还会有垃圾收集、鼠标与键盘时间分发等以守护线程(daemon thread)形式运行的线程。
线程(thread)是CPU进行调度的基本单位,与进程不同的是,每一个进程都享有独立的地址空间(代码、数据以及系统资源),但每一个线程除了有自己的程序计数器、寄存器集和堆栈之外,同一程序中的线程共享代码段、数据段和操作系统的资源,因此又把线程称为轻量级进程(light-weight process)。线程之间可以通过共享的数据区间进行通信,因而开销较小,效率更高。
下面主要简单介绍Java 多线程的一些基本概念、如何创建线程以及线程同步。
线程状态
Java线程有如下几种状态:
- New 线程已经创建,等待运行(start())
- Runnable 线程可执行
- Running 线程正在执行
- Blocked 线程阻塞(I/O blocking, suspend(), wait(), sleep() etc)
- Dead 线程终止
注意:wait()/notfiy()/notifyAll() 需要持有对象实例的锁时才能调用,因此这些方法的调用必须放在以synchronized标识的同步代码中,否则运行时会抛出一个java.lang.IllegalMonitorStateException
创建线程
Java 有两种方式来创建线程:
- 实现(implements) Runnable 接口(interface)
- 继承(extends) Thread 类
public interface Runnable{
public abstract void run();
}
public class Thread extends Object implements Runnable {
...
private Runnable target;
...
private final static int MIN_PRIORITY = 1;
private final static int NORM_PRIORITY = 5;
private final static int MAX_PRIORITY = 10;
...
public Thread();
public Thread(String name); // Thread name
public Thread(Runnable R); // Thread ? R.run()
public Thread(Runnable R, String name);
...
public void start(); // begin thread execution
...
// if this thread was contructed using a separated Runnable run object
// then that Runnable object's run method is calld; otherwise,this method
// does nothing and returns
public void run(){
if(target != null){
target.run();
}
};
...
}
- 首先,实现run() 方法
- 实例化一个Thread对象 Thread(Runnable threadObj, String threadName)
- 通过调用 start(),启动线程
class MyRunnable implements Runnable{
private Thread t;
private String threadName;
MyRunnable(String name){
threadName = name;
System.Out.println("Creating " + threadName);
}
public void Run(){
System.Out.println("Running " + threadName);
try{
for(int i = 0; i < 10; ++i){
System.Out.println("Counter = " + i);
// sleep for a while
Thread.sleep(50);
}
}catch(InterruptedException e){
System.Out.println("Thread " + threadName + " is interrupted");
}
System.Out.println("Thread " + threadName + " is exiting");
}
public void start(){
System.Out.println("Starting " + threadName);
if(t == null){
t = new Thread(this, threadName);
t.start();
}
}
}
public class RunnableTest{
public static void main(String args[]){
MyRunnable r1 = new MyRunnable("thread-1");
r1.start();
MyRunnable r2 = new MyRunnable("thread-2");
r2.start();
}
}
- 首先,覆盖Thread类中的run()方法
- 调用start()创建线程
class MyThread implements Thread{
private Thread t;
private String threadName;
MyThread(String name){
threadName = name;
System.Out.println("Creating " + threadName);
}
public void Run(){
System.Out.println("Running " + threadName);
try{
for(int i = 0; i < 10; ++i){
System.Out.println("Counter = " + i);
// sleep for a while
Thread.sleep(50);
}
}catch(InterruptedException e){
System.Out.println("Thread " + threadName + " is interrupted");
}
System.Out.println("Thread " + threadName + " is exiting");
}
public void start(){
System.Out.println("Starting " + threadName);
if(t == null){
t = new Thread(this, threadName);
t.start();
}
}
}
public class RunnableTest{
public static void main(String args[]){
MyThread r1 = new MyThread("thread-1");
r1.start();
MyThread r2 = new MyThread("thread-2");
r2.start();
}
}
线程同步
为确保多个并发线程可互斥地共享资源,Java使用管程(monitor)机制,每一个实例对象都与管程相关联,称之为“锁”(lock)。每一个实例对象有唯一的锁,每一类对象也只有唯一的一个锁。Java管程支持两种线程同步方式:互斥(mutual exclusion)和 协作(cooperation)。互斥是通过Lock机制来实现的,它允许多个线程独立的访问某个数据而不会相互干扰;协作则是通过Object类中的wait(), notify(), notifyAll()来实现的。
互斥
Java中使用关键字synchronized来定义同步代码锁的控制:在进入同步代码之前,线程必须获取指定对象实例的锁,若无法获得锁则该线程进入锁等待队列中等候;同步代码执行完或者出现异常中断时,线程需释放锁并唤醒锁等待队列中的线程。在任一时刻,一个对象实例的锁只能由一个线程持有,从而保证了特定代码段之间的互斥。
一般,synchronized关键字可用于如下两种不同情况:
- 将某个代码块作为代码临界区
- 将整个方法作为代码临界区
示例代码:
public class Stack{
...
// 代码块
public void push(char c){
synchronized(this){
++top;
data[top] = c;
}
}
// 整个方法
public synchronized char pop(){
char temp = data[top];
--top;
return temp;
}
}
线程协作
线程协作主要用的如下几个方法:
- sleep(long millis): 使调用的当前前程在指定的一段时间内处于阻塞状态。线程进入睡眠状态时,不会释放已占有的任何锁;
- join(long millis)/join(): 让当前执行的线程处于阻塞状态,等待指定线程(即调用join的线程)运行结束后再重新运行。若没有指定时间参数,则当调用线程执行结束后返回可运行状态;若指定了时间参数,则当调用线程执行结束后,或执行时间超过指定时间长度,均会导致线程重返可运行状态;
- wait(): 通知调用线程释放锁(管程),进入睡眠状态直到其他线程进入同一管程并且调用notify();
- notify():唤醒调用某个对象的wait()的线程
- notifyAll(): 唤醒所有调用某个对象wait()的线程,具有最高优先级的首先运行
下面的代码是一个producer-consumer的例子:如果Producer线程填满了buffer,则其需要释放管程的锁,等待Consumer线程从buffer中取出部分字符才能继续运行;同样地,如果buffer为空,则Consumer线程需要释放管程的锁,使Producer线程可以运行。
class Buffer{
private char[] buffer;
private int count = 0;
private int in = 0;
private int out = 0;
public Buffer(int size){
buffer = new char[size];
}
public synchronized void put(char c){
while(count == buffer.length){
try{
wait();
}catch(InterruptException e){
e.printStackTrace();
}finally{
// do something here
}
}
System.out.println("Producing " + c + " ...");
buffer[in] = c;
in = (++in)%buffer.length;
++count;
notify();
}
public synchronized char get(){
while(count == 0){
try{
wait();
}catch(InterruptException e){
e.printStackTrace();
}finally{
//do something here
}
}
char c = buffer[out];
out = (++out)%buffer.length;
--count;
System.out.println("Consuming " + c + " ...");
notify();
return c;
}
}
一般地,在实践中,对于线程同步,可以参照如下几条规则:
- If two or more threads modify an object, declare the methods that carry out the modification as synchronized(如果有两个以上的线程修改一个对象,将修改对象的代码声明为synchronized);
- If a thread must wait for the state of an object to change, it should wait inside the object, not outside, by entering the synchronized method and calling wait() (如果一个线程需要等待一个对象状态的变化,它应该通过调用wait()方法在对象内进行等待而不是对象的外面);
- Whennever a method changes the state of an object, it should call notify()/notifyAll(), which gives the waiting threads a chance to see if circumstances have changed(不管什么时候一个方面改变了对象的状态,它应该调用notify(),这样使得正在等待的线程可以看到运行状态的变化).