大多数时候,我们编程都是使用的单线程。程序执行的顺序流只有一条。但有时候因为业务需要,我们我们需要程序同时进行多项操作。比如一个服务器应该能够同时为多个用户提供服务。这就需要多线程的支持。从操作系统的角度来看,多个线程的并发执行,主要是指在逻辑上达到的“同时”。如果系统中只有一个 CPU,那么真正意义上的“同时”是无法实现的。只是因为 CPU 的高执行速度使得当多个线程间快速进行切换时,用户根本无法察觉到这种变化的发生,因此用户就会认为多个线程都是同时在工作。
1. Java 多线程概念
Java 在语言级提供了对多线程程序设计的支持,并且还是第一个在语言级支持多线程程序设计的编程语言。线程是程序中的一个执行流,多线程是指一个程序中包含多个执行流,多线程是实现并发机制的一种有效手段。线程又称轻量级进程,和进程一样,拥有独立的执行控制,由操作系统负责调度;线程没有独立的存储空间,和所属进程中的其它线程共享同一存储空间,因此线程间的通信远较进程简单。本身是支持时间片的轮换的,则线程调度器就能支持相同优先级线程间的时间片轮换。
2. 多线程的实现
Java 语言引入了包的概念,使得类的继承更简便,线程的创建就是一个最好的例子。在 Java 中要创建一个新的线程,首先要声明一个类。Java 多线程的创建有两种方式:继承 Thread 类和实现 Runnable 接口。下面对这两种方式分析如下:
让一个类去继承 Thread 类,然后调用 Thread 类中的start 方法,启动这个线程,JVM 就会去调用该线程的 run 方法,而用户写在 run 方法中的代码就会被执行了。继承Thread 类使用方法比较简单,但其有一个明显的缺点,因为java 语言只允许类的单继承,若声明的类已经从一个类继承下来(小程序必须继承自 Applet 类),此时就无法再继承Thread 类。
示例:
public class Test2 {
public static void main(String[] args) {
//创建两个线程对象
XianCheng xc1 = new XianCheng("张三");
XianCheng xc2 = new XianCheng("lisi");
//启动线程
xc1.start();
xc2.start();
}
}
class XianCheng extends Thread {
private String xcName;
public XianCheng () {}
public XianCheng (String xcName) {
this.xcName = xcName;
}
public void run() { //重写run()方法
for (int i = 1; i != 5; i++) {
System.out.println(xcName + "线程----------");
}
}
}
运行结果:
Java 语言中提供的另外一种开发多线程的方法是实现Runnable 接口,并且在该类中定义用于启动线程的 run 方法。其实 Thread 类本身也实现了 Runnable 接口。用实现Runnable 接口来创建多线程应用对象可以继承其它对象而不是必须继承 Thread 类,从而能够增加类定义上的逻辑性。使用 Runnable 接口来实现多线程能在一个类中包容所有的代码,有利于封装。其实实现 Runnable接口和从Thread 类派生而来,这两者的表现行为完全一样。通常情况下,如并不需要修改这个线程类当中除了 run 方法之外的其它行为的方法,可以实现 Runnable 接口。对于采用实现Runnable 接口这种方式去创建一个线程,若有多个线程要访问同一种资源,是极为方便的。本文将采用实现了 Runnable 接口来实现 Java 的多线程技术。
示例:
public class Test2 {
public static void main(String[] args) {
//创建两个线程对象
Thread t1 = new Thread(new Book("大学数学"));
Thread t2 = new Thread(new Book("English"));
//启动线程
t1.start();
t2.start();
}
}
//实现Runnable接口
class Book implements Runnable {
private String name;
public Book() { }
public Book(String name) {
this.name = name;
}
public void run() {
for (int i = 1; i != 5; i++) {
System.out.println(name + "线程----------");
}
}
}
运行结果:
3. 线程的状态
一个线程对象被创建开始一般经历如下状态:运行、就绪、挂起、结束。
4. 多线程同步机制
Java 应用程序的多个线程共享同一进程的数据资源,多个用户线程在并发运行过程中可能同时访问临界区的内容,为了程序的正常运行,在 Java 中定义了线程同步的概念,实现对临界区共享资源的一致性的维护。利用 synchronized 关键字实现多线程同步可以通过 synchronized 块和 synchronized 方法两种方式。当然也可以在程序中使用synchronized 同步方法,但程序的运行效率会有所降低。
对于一个售票系统,需要有多个窗口同时为多个旅客服务。假如当前有四个窗口,则需要创建四个线程来满足售票的服务。具体的程序如下:
class TicketsSystem {
public static void main(String args[]){
SellThread st=new SellThread();
new Thread(st).start();
new Thread(st).start();
new Thread(st).start();
new Thread(st).start();
}
}
class SellThread implements Runnable {
int tickets=100;
public void run() {
while(true) {
if(tickets>0) {
System.out.println(Thread.currentThread().
getName()+"sell tickets:"+tickets);
tickets--;
}
}
}
}
对这段程序进行编译并运行,但是该段程序的运行结果却出现了问题:当还剩下最后一张票时,由于时间片的关系,窗口会打印出 0, -1 不正确的票,最糟糕的情况是打印-2。虽然这种错误并不会经常性地出现,只会在系统长时间的运行中偶尔发生。但这样的错误一旦出现,其后果就是致命的。针对这种状况,可以通过 Thread 类中的sleep 方法使进入到 if 语句的线程休眠一小段时间,以给出关于错误的清晰描述。在具体的运用时,此方法则会抛出 throwsInterruptedException 的异常,需要对这个异常进行捕获。在后面程序中,由于还会用到此方法,此处就不再赘述了。
程序中有如下代码,词类代码被称为临界区。
if(tickets>0) {
…………
…………
tickets--;
}
所谓“临界区”是指在一个多线程的程序中,单独的并发的线程访问代码段中的同一对象,则此代码段就叫做临界区。同一进程的多个线程共享同一片存储空间,在带来方便的同时,也产生了访问冲突的问题。因此需要运用同步机制进行协调管理,即对临界区实施保护,从而避免程序可能出现不确定的状况。
2) Java 同步机制的分析
同步是指有多个线程在临界区上的互相排斥等待和互通消息,可分为共享存储体的同步和分布式系统的同步。在本文中主要讨论共享存储体同步机制。Java 语言引用 Synchronized 关键字来定义临界资源和同步方法,对于临界资源的保护通常由一些同步块或同步方法完成,即通过运行系统给临界资源对象加锁得到保证。在 Java 语言中,每个对象都有一个互斥锁,而任何线程对象在进入 synchronized 锁保护的代码段时首先要获得该互斥锁,代码段执行结束时则释放此互斥锁,若某个线程想要获取的互斥锁已经被其他线程抢先占有,则此线程将会进到互斥锁的等待队列中,直到占用互斥锁的线程将其释放之后,该等待线程才可以进入到同步代码段去执行。
①synchronized 方法实现线程同步
通过在方法声明中加入 synchronized 关键字来指定该方法为同步方法。同步方法控制对类成员变量的访问,控制过程如下:
类的所有对象都对应一把互斥锁,而每个同步方法只有在获得调用该方法的类的对象的互斥锁后才能执行程序,否则该线程将发生阻塞。同步方法一旦进入执行,就独占互斥锁,直至从该方法返回时才将互斥锁释放,被阻塞的线程方能获得互斥锁,从而进入可执行状态。此种机制确保了在同一时刻,对于每一个类实例,其所有声明为 synchronized 的成员方法中至多只有一个处于可执行状态,从而有效避免了类成员变量的访问冲突。需要注意的是,对于同步方法使用的互斥锁只能是 this 变量所代表的对象的锁。
②synchronized 块实现线程同步
Java 语言中,可通过 synchronized 关键字来声明 synchronized 块。语法如下:
synchronized 块中的代码段必须要在线程获得对象 obj的锁后才能执行,具体的同步过程与同步方法类似。但在同步块中所使用的对象锁可以是任意的,因而程序将具有更高的灵活性。本文主要采用同步机制中的同步块来解决临界区所产生的访问冲突问题。
3) 利用同步机制后的正确程序
在实验对应的程序中加入 synchronized 关键字,并引入java 多线程同步思想改进程序,编译后运行,结果表明完全正确。程序如下:
class TicketsSystem {
public static void main(String args[]){
SellThread st=new SellThread();
new Thread(st).start();
new Thread(st).start();
new Thread(st).start();
new Thread(st).start();
}
}
class SellThread implements Runnable {
int tickets=100;
Object obj=new Object();
public void run(){
while(true) {
synchronized(obj)
if(tickets>0){
try {
Thread.sleep(10);
}
catch(Exception e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName
()+"sell tickets:"+tickets);
tickets--;
}
}
}
}
在程序中运用了多线程的同步机制,程序运行的结果显示为:票全部销售出去,并且票号连续。
本段程序主要是利用同步机制对临界资源给予了相应的保护,从而确保了程序运行结果的正确。在 Java 语言中,关于同步思想,引入了“互斥锁”的概念。每个对象都对应于一个可称为“互斥锁”的标记,或者叫监视器(monitor),该锁用来保证在某一时刻,只能有一个线程访问该对象。关键字 synchronized 与对象的锁相关联。当某个对象用synchronized 声明时,则表示该对象在某一时刻只能由一线程访问,因而实现了对临界资源的互斥访问。synchronized锁定一段代码,可称为创建了一个代码临界区,其后线程必须等候特定资源的所有权。
本例中同步机制的具体实施过程:当线程 1 进入到同步的代码段时,首先判断 obj 的监视器是否加锁,若还未加锁,则将 obj 的对象锁上,然后顺序执行,直到遇到 sleep 代码时,休眠 10ms。线程 2 开始运行,运行到同步对象时,发现 obj 的监视器已经被加锁了,JVM 就将线程 2 送入一个等待区域。同理,线程 3,线程 4 也相继被送入等待区域中。当线程 1 被唤醒之后,继续向下执行,直到代码结束,将 obj的监视器解锁。接下来,线程 2 进入到同步代码段中并获得该互斥锁。依次类推,于是形成了多个线程对同一个对象的“互斥”使用方式,该对象则称为“同步对象”。