多线程编程基础
多进程
一个独立程序的每一次运行称为一个进程,例如:用字处理软件编辑文稿时,同时打开mp3播放程序听音乐,这两个独立的程序在同时运行,称为两个进程
-
进程要占用相当一部分处理器时间和内存资源
-
进程具有独立的内存空间
-
通信很不方便,编程模型比较复杂
多线程
一个程序中多段代码同时并发执行,称为多线程,线程比进程开销小,协作和数据交换容易
Java是第一个支持内置线程操作的主流编程语言,多数程序设计语言支持多线程要借助于操作系统“原语(primitives)”
Thread类
直接继承了Object类,并实现了Runnable接口。位于java.lang包中封装了线程对象需要的属性和方法
继承Thread类——创建多线程的方法之一,类派生一个子类,并创建子类的对象,子类应该重写Thread类的run方法,写入需要在新线程中执行的语句段。调用start方法来启动新线程,自动进入run方法。
(实例1)在新线程中完成计算某个整数的阶乘
class FactorialThread extends Thread { private int num; public FactorialThread(int num) { this.num = num; } public void run() { int i = num; int result = 1; System.out.println("new thread started"); while(i > 0) { result = result * i; i--; } System.out.println("The factorial of " + num + " is " + result); System.out.println("new thread ends"); } } public class javatest { public static void main(String args[]) { System.out.println("main thread start"); FactorialThread thread = new FactorialThread(10); thread.start(); System.out.println("main thread ends"); } }
运行结果:
main thread start
main thread ends
new thread started
The factorial of 10 is 3628800
new thread ends
结果说明:
main线程已经执行完后,新线程才执行完。main函数调用thread.start()方法启动新线程后并不等待其run方法返回就继续运行,thread.run函数在一边独自运行,不影响原来的main函数的运行
如果启动新线程后希望主线程多持续一会再结束,可在start语句后加上让当前线程(这里当然是main)休息1毫秒的语句:
try { Thread.sleep(1); } catch(Exception e){};
常用API方法:
名称 | 说明 |
public Thread() | 构造一个新的线程对象,默认名为Thread-n,n是从0开始递增的整数 |
public Thread(Runnable target) | 构造一个新的线程对象,以一个实现Runnable接口的类的对象为参数。默认名为Thread-n,n是从0开始递增的整数 |
public Thread(String name) | 构造一个新的线程对象,并同时指定线程名 |
public static Thread currentThread() | 返回当前正在运行的线程对象 |
public static void yield() | 使当前线程对象暂停,允许别的线程开始运行 |
public static void sleep(long millis) | 使当前线程暂停运行指定毫秒数,但此线程并不失去已获得的锁旗标。 |
public void start() | 启动线程,JVM将调用此线程的run方法,结果是将同时运行两个线程,当前线程和执行run方法的线程 |
public void run() | Thread的子类应该重写此方法,内容应为该线程应执行的任务。 |
public final void stop() | 停止线程运行,释放该线程占用的对象锁旗标。 |
public void interrupt() | 中断此线程 |
public final void join() | 如果此前启动了线程A,调用join方法将等待线程A死亡才能继续执行当前线程 |
public final void join(long millis) | 如果此前启动了线程A,调用join方法将等待指定毫秒数或线程A死亡才能继续执行当前线程 |
public final void setPriority(int newPriority) | 设置线程优先级 |
public final void setDaemon(Boolean on) | 设置是否为后台线程,如果当前运行线程均为后台线程则JVM停止运行。这个方法必须在start()方法前使用 |
public void setName(String name) | 更改本线程的名称为指定参数 |
public final boolean isAlive() | 测试线程是否处于活动状态,如果线程被启动并且没有死亡则返回true |
public final void checkAccess() | 判断当前线程是否有权力修改调用此方法的线程 |
(实例2):创建3个新线程,每个线程睡眠一段时间(0~6秒),然后结束。
class TestThread extends Thread { private int sleeptime; public TestThread(String name) { super(name); sleeptime = (int)(Math.random() * 6000); } public void run() { try { System.out.println(getName() + "going to sleep for " + sleeptime); Thread.sleep(sleeptime); } catch (InterruptedException ex) { } System.out.println(getName() + " finished"); } } public class javatest { public static void main(String args[]) { TestThread thr1 = new TestThread("thread1"); TestThread thr2 = new TestThread("thread2"); TestThread thr3 = new TestThread("thread3"); System.out.println("staring threads"); thr1.start(); thr2.start(); thr3.start(); System.out.println("Thread started, main ends"); } }
运行结果:
staring threads
thread1going to sleep for 2925
Thread started, main ends
thread3going to sleep for 1222
thread2going to sleep for 5007
thread3 finished
thread1 finished
thread2 finished
Runnable接口
Thread类实现了Runnable接口,只有一个run()方法,更便于多个线程共享资源。Java不支持多继承,如果 已经继承了某个基类,便需要实现Runnable接口来生成多线程以实现runnable的对象为参数建立新的线程,start方法启动线程就会运行 run()方法
(实例3)使用Runnable接口实现实例1的功能:
class FactorialThread implements Runnable { private int num; public FactorialThread(int num) { this.num = num; } public void run() { int i = num; int result = 1; while(i > 0) { result = result * i; i--; } System.out.println("The factorial of " + num + " is " + result); System.out.println("new thread ends"); } } public class javatest { public static void main(String args[]) { System.out.println("main thread starts"); FactorialThread t = new FactorialThread(10); new Thread(t).start(); System.out.println("new thread started main thread ends"); } }
(实例4)使用Runnable接口实现实例2的功能:
class TestThread implements Runnable { private int sleepTime; public TestThread() { sleepTime = (int)(Math.random() * 6000); } public void run() { try { System.out.println(Thread.currentThread().getName() + " going to sleep for " + sleepTime); Thread.sleep(sleepTime); } catch(InterruptedException ex) { }; System.out.println(Thread.currentThread().getName() + " finished"); } } public class javatest { public static void main(String args[]) { TestThread thread1 = new TestThread(); TestThread thread2 = new TestThread(); TestThread thread3 = new TestThread(); System.out.println("Starting threads"); new Thread(thread1, "Thread1").start(); new Thread(thread2, "Thread2").start(); new Thread(thread3, "Thread3").start(); System.out.println("Threads started, main ends"); } }
线程间的数据共享
用同一个实现了Runnable接口的对象作为参数创建多个线程
多个线程共享同一对象中的相同的数据
修改实例4,只用一个Runnable类型的对象为参数创建3个新线程:
class TestThread implements Runnable { private int sleepTime; public TestThread() { sleepTime = (int)(Math.random() * 6000); } public void run() { try { System.out.println(Thread.currentThread().getName() + " going to sleep for " + sleepTime); Thread.sleep(sleepTime); } catch(InterruptedException ex) { }; System.out.println(Thread.currentThread().getName() + " finished"); } } public class javatest { public static void main(String args[]) { TestThread threadobj = new TestThread(); System.out.println("Starting threads"); new Thread(threadobj, "Thread1").start(); new Thread(threadobj, "Thread2").start(); new Thread(threadobj, "Thread3").start(); System.out.println("Threads started, main ends"); } }
说明:因为是用一个Runnable类型对象创建的3个新线程,这三个线程就共享了这个对象的私有成员sleepTime,在本次运行中,三个线程都休眠了966毫秒
独立的同时运行的线程有时需要共享一些数据并且考虑到彼此的状态和动作
举个例子:用三个线程模拟三个售票口,总共出售200张票
class SellTicks implements Runnable { private int tickets = 200; public void run() { while(tickets > 0) { System.out.println(Thread.currentThread().getName() + " is selling ticket " + tickets--); } } } public class javatest { public static void main(String args[]) { SellTicks t = new SellTicks(); new Thread(t).start(); new Thread(t).start(); new Thread(t).start(); } }
说明:
在这个例子中,创建了3个线程,每个线程调用的是同一个SellTickets对象中的run()方法,访问的是同一个对象中的变量(tickets)
如果是通过创建Thread类的子类来模拟售票过程,再创建3个新线程,则每个线程都会有各自的方法和变量,虽然方法是相同的,但变量却是各有200张票,因而结果将会是各卖出200张票,和原意就不符了
多线程的同步控制
有时线程之间彼此不独立、需要同步
线程间的互斥
同时运行的几个线程需要共享一个(些)数据
共享的数据,在某一时刻只允许一个线程对其进行操作
“生产者/消费者” 问题
假设有一个线程负责往数据区写数据,另一个线程从同一数据区中读数据,两个线程可以并行执行,如果数据区已满,生产者要等消费者取走一些数据后才能再写。当数据区空时,消费者要等生产者写入一些数据后再取
举个例子:
用两个线程模拟存票、售票过程
假定开始售票处并没有票,一个线程往里存票,另外一个线程则往出卖票,我们新建一个票类对象,让存票和售票线程都访问它。本例采用两个线程共享同一个数据对象来实现对同一份数据的操作
class Tickets { int number = 0; //票号 int size; //总票数 boolean available = false; //表示目前是否有票可售 public Tickets(int size) { //构造函数,传入总票参数 this.size = size; } } class Producer extends Thread { Tickets t = null; public Producer(Tickets t) { this.t = t; } public void run() { while(t.number < t.size) { System.out.println("Producer puts ticket" + (++t.number)); t.available = true; } } } class Consumer extends Thread { Tickets t = null; int i = 0; public Consumer(Tickets t) { this.t = t; } public void run() { while(i < t.size) { if(t.available == true && i <= t.number) System.out.println("Consumer buys ticket" + (++i)); if(i == t.number) t.available = false; } } } public class TicketSell { public static void main(String[] args) { Tickets t = new Tickets(10); new Consumer(t).start(); new Producer(t).start(); } }
运行结果:
Producer puts ticket1
Producer puts ticket2
Consumer buys ticket1
Producer puts ticket3
Consumer buys ticket2
Producer puts ticket4
Producer puts ticket5
Producer puts ticket6
Producer puts ticket7
Producer puts ticket8
Producer puts ticket9
Producer puts ticket10
Consumer buys ticket3
Consumer buys ticket4
Consumer buys ticket5
Consumer buys ticket6
Consumer buys ticket7
Consumer buys ticket8
Consumer buys ticket9
Consumer buys ticket10
通过让两个线程操纵同一个票类对象,实现了数据共享的目的,
线程同步(Synchronization)
互斥:许多线程在同一个共享数据上操作而互不干扰,同一时刻只能有一个线程访问该共享数据。因此有些方法或程序段在同一时刻只能被一个线程执行,称之为监视区
协作:多个线程可以有条件地同时操作共享数据。执行监视区代码的线程在条件满足的情况下可以允许其它线程进入监视区
synchronized ——线程同步关键字
用于指定需要同步的代码段或方法,也就是监视区
可实现与一个锁旗标的交互。例如:
synchronized(对象){ 代码段 }
synchronized的功能是:首先判断对象的锁旗标是否在,如果在就获得锁旗标,然后就可以执行紧随其后的代码段;如果对象的锁旗标不在(已被其他线程拿走),就进入等待状态,直到获得锁旗标
当被synchronized限定的代码段执行完,就释放锁旗标
互斥:存票线程和售票线程应保持互斥关系。即售票线程执行时不进入存票线程、存票线程执行时不进入售票线程
Java 使用监视器机制
–每个对象只有一个“锁旗标” ,利用多线程对“锁旗标”的争夺实现线程间的互斥
–当线程A获得了一个对象的锁旗标后,线程B必须等待线程A完成规定的操作、并释放出锁旗标后,才能获得该对象的锁旗标,并执行线程B中的操作
将需要互斥的语句段放入synchronized(object){}语句框中,且两处的object是相同的
修改上面的代码:
class Producer extends Thread { Tickets t = null; public Producer(Tickets t) { this.t = t; } public void run() { while(t.number < t.size) { synchronized (t) { //申请对象t的锁旗标 System.out.println("Producer puts ticket" + (++t.number)); t.available = true; } //释放对象t的锁旗标 } } } class Consumer extends Thread { Tickets t = null; int i = 0; public Consumer(Tickets t) { this.t = t; } public void run() { while(i < t.size) { synchronized (t) { ////申请对象t的锁旗标 if(t.available == true && i <= t.number) System.out.println("Consumer buys ticket" + (++i)); if(i == t.number) t.available = false; } } //释放对象t的锁旗标 } }
运行结果:
Producer puts ticket1
Producer puts ticket2
Producer puts ticket3
Producer puts ticket4
Producer puts ticket5
Producer puts ticket6
Producer puts ticket7
Producer puts ticket8
Producer puts ticket9
Producer puts ticket10
Consumer buys ticket1
Consumer buys ticket2
Consumer buys ticket3
Consumer buys ticket4
Consumer buys ticket5
Consumer buys ticket6
Consumer buys ticket7
Consumer buys ticket8
Consumer buys ticket9
Consumer buys ticket10
说明:
1、存票程序段和售票程序段为获得同一对象的锁旗标而实现互斥操作
2、当线程执行到synchronized的时候,检查传入的实参对象,并申请得到该对象的锁旗标。如果得不到,那么线程就被放到一个与该对象锁旗标相对应的等待线程池中。直到该对象的锁旗标被归还,池中的等待线程才能重新去获得锁旗标,然后继续执行下去
3、除了可以对指定的代码段进行同步控制之外,还可以定义整个方法在同步控制下执行,只要在方法定义前加上synchronized关键字即可
把上面的程序再修改一下:
class Tickets { int number = 0; //票号 int size; //总票数 int i = 0; //售票序号 boolean available = false; //表示是否有票可售 public Tickets(int size) { //构造函数,传入总票参数 this.size = size; } public synchronized void put() { //同步方法,实现存票的功能 System.out.println("Producer puts ticket" + (++number)); available = true; } public synchronized void sell() { //同步方法,实现售票的功能 if(available == true && i <= number) System.out.println("Consumer buys ticket" + (++i)); if(i == number) available = false; } } class Producer extends Thread { Tickets t = null; public Producer(Tickets t) { this.t = t; } public void run() { //如果存票数小于限定总量,则不断存入票 while(t.number < t.size) t.put(); } } class Consumer extends Thread { Tickets t = null; int i = 0; public Consumer(Tickets t) { this.t = t; } public void run() { //如果售票数小于限定总量,则不断售票 while(i < t.size) t.sell(); } }
线程之间的通信
为了更有效地协调不同线程的工作,需要在线程间建立沟通渠道,通过线程间的“对话”来解决线程间的同步问题
java.lang.Object 类的一些方法为线程间的通讯提供了有效手段
wait() 如果当前状态不适合本线程执行,正在执行同步代码(synchronized)的某个线程A调用该方法(在对象x上),该线程暂停执行而进入对象x的等待 池,并释放已获得的对象x的锁旗标。线程A要一直等到其他线程在对象x上调用notify或notifyAll方法,才能够在重新获得对象x的锁旗标后继 续执行(从wait语句后继续执行)
notify()和notifyAll()方法:
notify() 随机唤醒一个等待的线程,本线程继续执行
线程被唤醒以后,还要等发出唤醒消息者释放监视器,这期间关键数据仍可能被改变
被唤醒的线程开始执行时,一定要判断当前状态是否适合自己运行
notifyAll() 唤醒所有等待的线程,本线程继续执行
修改上面的例子,使每存入一张票,就售一张票,售出后,再存入。 代码如下:
package javatest; import com.sun.org.apache.xalan.internal.xslt.Process; class Tickets { int number = 0; //票号 int size; //总票数 int i = 0; //售票序号 boolean available = false; //表示是否有票可售 public Tickets(int size) { //构造函数,传入总票参数 this.size = size; } public synchronized void put() { if(available) { //如果还有存票待售,则存票线程等待 try { wait(); } catch (Exception e) { // TODO: handle exception } } System.out.println("Producer puts ticket" + (++number)); available = true; notify(); //存票后唤醒售票线程开始售票 } public synchronized void sell() { if(!available) { //如果没有存票,则售票线程等待 try { wait(); } catch(Exception e) { } } System.out.println("Consumer buys ticket" + (number)); available = false; notify(); if(number == size) number = size + 1; //在售完最后一张票后, //设置一个结束标志,number>size表示售票结束 } } class Producer extends Thread { Tickets t = null; public Producer(Tickets t) { this.t = t; } public void run() { //如果存票数小于限定总量,则不断存入票 while(t.number < t.size) t.put(); } } class Consumer extends Thread { Tickets t = null; int i = 0; public Consumer(Tickets t) { this.t = t; } public void run() { //如果售票数小于限定总量,则不断售票 while(i < t.size) t.sell(); } } public class TicketSell { public static void main(String[] args) { Tickets t = new Tickets(10); new Consumer(t).start(); new Producer(t).start(); } }
运行结果:
Producer puts ticket1
Consumer buys ticket1
Producer puts ticket2
Consumer buys ticket2
Producer puts ticket3
Consumer buys ticket3
Producer puts ticket4
Consumer buys ticket4
Producer puts ticket5
Consumer buys ticket5
Producer puts ticket6
Consumer buys ticket6
Producer puts ticket7
Consumer buys ticket7
Producer puts ticket8
Consumer buys ticket8
Producer puts ticket9
Consumer buys ticket9
Producer puts ticket10
Consumer buys ticket10
程序说明:
当Consumer线程售出票后,available值变为false,当Producer线程放入票后,available值变为true
只有available为true时,Consumer线程才能售票,否则就必须等待Producer线程放入新的票后的通知
只有available为false时,Producer线程才能放票,否则必须等待Consumer线程售出票后的通知
后台线程
也叫守护线程,通常是为了辅助其它线程而运行的线程
它不妨碍程序终止
一个进程中只要还有一个前台线程在运行,这个进程就不会结束;如果一个进程中的所有前台线程都已经结束,那么无论是否还有未结束的后台线程,这个进程都会结束
“垃圾回收”便是一个后台线程
如果对某个线程对象在启动(调用start方法)之前调用了setDaemon(true)方法,这个线程就变成了后台线程