多线程的概念
就像操作系统可以在不同程序(进程)上切换执行,让我们感到多个程序同时运行那样,在一个Java程序内部可以开辟线程来同时执行多个任务。
在很多情况下多线程可以发挥作用,比如多线程可以让程序在进行一项非常繁重的工作时,比如读写文件,还可以同时进行其它的任务,比如响应界面事件。
main()方法本身就运行在一个线程中。Swing界面代码需要运行在事件派发线程中。
创建线程
一般来说,可以用两种方式来创建线程
- 继承Thread类并重写run方法
- 让某一个类实现Runnable接口并让Thread对象来运行它
方法1.继承Thread类
定义一个继承于Thread类的类,并重写Thread类的run方法,创建该类的对象后调用start方法启动线程,线程会执行run()方法。比如,定义线程来计算大于某个数的所有素数的线程可以写成
class PrimeThread extends Thread {
long minPrime;
PrimeThread(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
// compute primes larger than minPrime
. . .
}
}
创建线程对象并启动线程的代码如下
PrimeThread p = new PrimeThread(143);
p.start();
java.lang.Thread
类是Java中定义的线程类,该类含有启动线程并有一些管理和查看线程状态的方法。
方法2.实现Runnable接口
另一种实现线程的方式是让一个类实现Runnable
接口,java.lang.Runnable
是Java中定义的接口
public interface Runnable{
public void run();
}
class PrimeRun implements Runnable {
long minPrime;
PrimeRun(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
// compute primes larger than minPrime
. . .
}
}
创建该类的对象后,将它作为参数传给Thread线程对象的构造函数,然后调用线程对象的start()方法启动线程,启动线程代码如下
PrimeRun p = new PrimeRun(143);
new Thread(p).start();
创建线程的例子
下面的代码开辟了3个新线程,在第一个线程里面打印字符a一百次,第2个线程里面打印字符b一百次,第3个线程里面连续打印从1到100中的各个数。
- 用继承Thread的方式来实现
public class TaskThreadDemoWithThread {
public static void main(String[] args) {
// Create threads
PrintCharThread thread1 = new PrintCharThread('a', 100);
PrintCharThread thread2 = new PrintCharThread('b', 100);
PrintNumThread thread3 = new PrintNumThread(100);
// Start threads
thread1.start();
thread2.start();
thread3.start();
}
}
// The thread for printing a specified character in specified times
class PrintCharThread extends Thread {
private char charToPrint; // The character to print
private int times; // The times to repeat
/** Construct a thread with specified character and number of
* times to print the character
*/
public PrintCharThread(char c, int t) {
charToPrint = c;
times = t;
}
@Override /** Override the run() method of thread
*/
public void run() {
for (int i = 0; i < times; i++) {
System.out.print(charToPrint);
}
}
}
// The thread class for printing number from 1 to n for a given n
class PrintNumThread extends Thread {
private int lastNum;
/** Construct a thread for printing 1, 2, ... i */
public PrintNumThread(int n) {
lastNum = n;
}
@Override/** Override the run() method of thread
*/
public void run() {
for (int i = 1; i <= lastNum; i++) {
System.out.print(" " + i);
}
}
}
- 用Runnable的方式来实现
public class TaskThreadDemoWithRunnable {
public static void main(String[] args) {
// Create tasks
Runnable printA = new PrintChar('a', 100);
Runnable printB = new PrintChar('b', 100);
Runnable print100 = new PrintNum(100);
// Create threads
Thread thread1 = new Thread(printA);
Thread thread2 = new Thread(printB);
Thread thread3 = new Thread(print100);
// Start threads
thread1.start();
thread2.start();
thread3.start();
}
}
// The task for printing a specified character in specified times
class PrintChar implements Runnable {
private char charToPrint; // The character to print
private int times; // The times to repeat
/** Construct a task with specified character and number of
* times to print the character
*/
public PrintChar(char c, int t) {
charToPrint = c;
times = t;
}
@Override /** Override the run() method to tell the system
* what the task to perform
*/
public void run() {
for (int i = 0; i < times; i++) {
System.out.print(charToPrint);
}
}
}
// The task class for printing number from 1 to n for a given n
class PrintNum implements Runnable {
private int lastNum;
/** Construct a task for printing 1, 2, ... i */
public PrintNum(int n) {
lastNum = n;
}
@Override /** Tell the thread how to run */
public void run() {
for (int i = 1; i <= lastNum; i++) {
System.out.print(" " + i);
}
}
}
线程池
要创建多个线程,除了像上面创建线程的例子中创建多个Thread对象,还可以使用线程池类,线程池可以理解为多个线程的容器。Executors类的静态方法newFixedThreadPool(int)或者newCachedThreadPool()可以产生不同特性的线程池。创建线程的例子用线程池来实现
import java.util.concurrent.*;
public class ExecutorDemo {
public static void main(String[] args) {
// Create a fixed thread pool with maximum three threads
ExecutorService executor = Executors.newFixedThreadPool(3);
// Submit runnable tasks to the executor
executor.execute(new PrintChar('a', 100));
executor.execute(new PrintChar('b', 100));
executor.execute(new PrintNum(100));
// Shut down the executor
executor.shutdown();
}
}
ExecutorService是线程池的使用接口,而ExecutorService接口又继承于Executor接口,Executors可以产生不同特性的线程池。
- Executors.newFixedThreadPool(int nThreads)创建一个线程池,这个线程池同时最多有nThreads个线程在运行,运行结束的线程可以复用。
- Executors.newCachedThreadPool() 创建一个线程池,这个线程池没有最多同时运行线程数量限制,运行结束的线程可以复用。如果一个线程在60秒内没有被使用,这个线程将被销毁。
shutdown()方法关闭线程池,使线程池不接收新的线程,但是不会妨碍线程池中已有线程的运行。当不需要再往线程池中添加新线程时,建议调用shutdown()方法。
线程Thread类常用方法
除了start()方法可以启动线程,介绍一下线程类Thread其它常用方法。
sleep
static void sleep(long milliseconds) throws InterruptedException
sleep(long milliseconds)方法让当前线程休眠一会,其参数是让线程休眠的毫秒数。如果希望线程长期执行,又避免过度占用cpu,可以调用此方法。创建线程的例子中让PrintNum
线程执行过程中有规律的休眠一会。
public class TaskThreadDemoWithSleep {
public static void main(String[] args) {
Runnable print100 = new PrintNum(100);
Thread thread3 = new Thread(print100);
thread3.start();
}
}
// The task class for printing number from 1 to n for a given n
class PrintNum implements Runnable {
private int lastNum;
/** Construct a task for printing 1, 2, ... i */
public PrintNum(int n) {
lastNum = n;
}
@Override /** Tell the thread how to run */
public void run() {
try {
for (int i = 1; i < lastNum; i++) {
System.out.print(" " + i);
Thread.sleep(100);
}
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
因为sleep()方法会抛出InterruptedException异常,这个异常是检查异常,因此需要try-catch捕捉异常,或者声明函数会抛出这个异常。休眠中的线程被调用interrupt()了方法就会被唤醒,并抛出该异常。如果此时我们不想让线程继续休眠,就不能把try-catch放在循环中,否则线程即使被唤醒,也会再次进入下一个休眠。例如像下面的代码做的那样,
public class TaskThreadDemoWithSleepAndNoRespondToInterrupt {
public static void main(String[] args) {
Runnable print100 = new PrintNum(100);
Thread thread3 = new Thread(print100);
thread3.start();
}
}
// The task class for printing number from 1 to n for a given n
class PrintNum implements Runnable {
private int lastNum;
/** Construct a task for printing 1, 2, ... i */
public PrintNum(int n) {
lastNum = n;
}
@Override /** Tell the thread how to run */
public void run() {
for (int i = 1; i < lastNum; i++) {
System.out.print(" " + i);
try {
Thread.sleep(100);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
}
isAlive
final boolean isAlive()
isAlive()方法判断线程是否在运行,如果线程还没有开始,或者线程运行已经结束,isAlive()方法返回false,否则返回true。
join
void join() throws InterruptedException
假设在线程a中调用线程对象b的join()方法,那么线程a会阻塞(暂停)在调用语句那里,直到线程对象b表示的线程运行结束,比如
public class TaskThreadDemoWithJoin {
public static void main(String[] args) {
Runnable print100 = new PrintNum(100);
Thread thread3 = new Thread(print100);
System.out.println("before thread start, alive is :" + thread3.isAlive() + ", thread is to start");
thread3.start();
try {
thread3.join(); // main函数会阻塞在这里,直到thread3线程运行结束
System.out.println("thread3 end");
}catch(InterruptedException ex) {
ex.printStackTrace();
}
}
}
// The task class for printing number from 1 to n for a given n
class PrintNum implements Runnable {
private int lastNum;
/** Construct a task for printing 1, 2, ... i */
public PrintNum(int n) {
lastNum = n;
}
@Override /** Tell the thread how to run */
public void run() {
try {
for (int i = 1; i < lastNum; i++) {
System.out.print(" " + i);
Thread.sleep(100);
}
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
setPriority
void setPriority(int newPriority)
setPriority(int)方法设置线程的优先级,优先级越高的线程,被cpu选择运行的概率越大。优先级在1到10之间。用整数常量MIN_PRIORITY,NORM_PRIORITY,MAX_PRIORITY分别表示优先级1,5,10。
线程同步
如果多个线程同时访问同一个数据,可能会出现数据的状态与预期结果不一致的问题,比如下面的程序创建100个线程来向t同一个账户中增加1元钱,结果预计是100元,但是实际结果却不是
import java.util.concurrent.*;
public class AccountWithoutSync {
private static Account account = new Account(); // 创建一个Account对象,其balance成员为0
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
// Create and launch 100 threads
for (int i = 0; i < 100; i++) {
executor.execute(new AddAPennyTask()); // 创建一个AddAPennyTask,添加到executor多线程任务中,准备执行它
}
executor.shutdown();
// 线程池shutdown以后,判断executor任务中的线程是否都执行完毕
while (!executor.isTerminated()) {
}
// 在所有线程执行完毕后显示账户中balance的值
System.out.println("What is balance? " + account.getBalance());
}
// A thread for adding a penny to the account,向帐号中增加1元的线程
private static class AddAPennyTask implements Runnable {
public void run() {
account.deposit(1);
}
}
// An inner class for account,Account内部类
private static class Account {
private int balance = 0;
public int getBalance() {
return balance;
}
public void deposit(int amount) {
int newBalance = balance + amount; // 语句1
// This delay is deliberately added to magnify the
// data-corruption problem and make it easy to see.
try {
Thread.sleep(5);
}
catch (InterruptedException ex) {
}
balance = newBalance; // 语句2
}
}
}
上面的程序中,AddAPennyTask
和Account
是静态嵌套类(static nested classes)。(静态嵌套类只能访问外部类的静态成员,不能访问非静态成员。内部类(inner classes),也可称为非静态内部类,还能访问外部类的非静态成员。静态嵌套类仅表达了类层次上的包含关系,内部类还进一步表达了对象层次上的包含关系。)
程序多次运行的结果
What is balance? 4
What is balance? 6
What is balance? 3
结果与我们预期的100相差很大,为什么出现这种结果?
我们以两个线程的情况来说明,如果只有两个线程,那么balance预期应该是2,但是由于每个线程中把1元加到account的balance成员所依赖的两条语句,即语句1和语句2,可能没有连续的执行。中间可能穿插了另外一条线程的语句1、语句2,这样造成balance值的混乱。比如,下面的两种情况都会导致balance的值不对。
可能1:
可能2:
如果每个线程在执行deposit()方法时,语句1和语句2可以连续执行完那么最后的结果一定是我们想要的2。就像下面的图示
可见,因为两个线程要处理同一个变量,即account的balance成员,而由于线程的特性,在不做任何处理的情况下,每个线程的语句1和语句2不一定能连续的执行完成。因此为了得到我们想要的结果,我们需要让同一个线程的语句1和语句2连续的执行,不被其它的线程打断。这样需要连续的执行、即不被其它线程打断执行的代码段称为关键区域( critical region)。如果同一个时间只有一个线程可以进入关键区域,就可以避免结果的混乱。有多种办法可以做到这一点。
- 同步方法
- 同步代码块
- 用Lock来同步代码(可选)
同步方法
因为语句1和语句2在deposit()方法里面,可以把整个deposit()方法当作我们的关键区域,用synchronized
来修饰这个方法,就可以保证线程对这个方法的使用是同步的,即同一个时间只有一个线程可以运行这个方法中的代码,其余线程将被排除在这个方法之外,当已经有一个线程运行到同步方法里,当其它线程运行到调用这个方法时,将被阻塞(暂停)住,直到在方法中的线程运行完这个方法中的代码,白话解释就是同步方法使得各个线程在这个方法中能不被打断的(排它的)运行。
一旦方法被声明为同步的,方法里面的代码只会在某1个灰色的时间片中,不会出现在连续多个时间片中,即方法中的代码能够独享一段时间片,连续的、不被打断的运行。
我们可以这样声明deposit方法是同步的
public synchronized void deposit(double amount)
当我们把程序中的deposit()方法修饰为同步的,程序的结果和我们的预期就是一致的。当某个线程正在deposit方法中执行代码,别的线程就被阻塞在deposit()方法之外,直到在deposit()中的线程退出了deposit()方法。其它阻塞的线程会有一个线程被允许进入deposit()方法中执行代码。
What is balance? 100
同步代码块
把整个方法都同步起来,即在同一个时刻只有一个线程才能进入这个方法中运行代码,如果方法代码比较多,那么进入方法中执行的线程可能要花较长时间才能运行完整个方法,这意味着被阻塞在方法外面的线程可能会等待比较长的时间才能进入方法运行,很多线程都可能会等待较长时间,不经济。除了可以声明整个方法是同步的,还可以声明方法中的若干条语句是同步的,这样单个线程排它运行的代码就不是整个方法,而是方法中的若干条语句,其它线程等待的时间可能就变少。
synchronized (对象变量名){
// 同步的代码块
语句;
...
}
比如,我们可以用account
对象上的锁来同步的account.deposit(1)方法的运行,
// A thread for adding a penny to the account,向帐号中增加1元的线程
private static class AddAPennyTask implements Runnable {
public void run() {
synchronized (account){
account.deposit(1);
}
}
}
用这种方式修饰的代码块称为同步的代码块。多线程运行同步的代码块和运行同步的方法的模式是一致的。一个线程要进入同步的代码块,需要获得对象的锁(监视器),上面代码中用的是account对象的锁。锁是Object类中定义的一种成员变量,因此Java中的对象都有锁。进入到同步代码块的线程必须获得了对象的锁,而任意时刻只能有一个线程可以获得对象的锁,因此其它要进入同步代码块的线程会因为无法获取对象上的锁而被阻塞住。线程执行完同步代码块就释放掉对象上的锁,这样被阻塞的线程才能获得这个对象上的锁,进而进入被这个锁保护的同步代码块中运行。
使用Lock来同步代码(可选)
可以显式对代码块加锁,一个lock是实现了Lock接口的对象,接口中的方法用来获取和释放锁。一个lock可以使用newCondition()来产生多个条件锁,条件锁在线程通讯中使用到。ReentrantLock是一个具体的类,它实现了Lock接口。
import java.util.concurrent.*;
import java.util.concurrent.locks.*;
public class AccountWithSyncUsingLock {
private static Account account = new Account();
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
// Create and launch 100 threads
for (int i = 0; i < 100; i++) {
executor.execute(new AddAPennyTask());
}
executor.shutdown();
// Wait until all tasks are finished
while (!executor.isTerminated()) {
}
System.out.println("What is balance ? " + account.getBalance());
}
// A thread for adding a penny to the account
public static class AddAPennyTask implements Runnable {
public void run() {
account.deposit(1);
}
}
// An inner class for account
public static class Account {
private static Lock lock = new ReentrantLock(); // Create a lock
private int balance = 0;
public int getBalance() {
return balance;
}
public void deposit(int amount) {
lock.lock(); // Acquire the lock
try {
int newBalance = balance + amount;
// This delay is deliberately added to magnify the
// data-corruption problem and make it easy to see.
Thread.sleep(5);
balance = newBalance;
}
catch (InterruptedException ex) {
}
finally {
lock.unlock(); // Release the lock
}
}
}
}
线程通信
多个线程除了可能不能同时进入关键区域外,它们之间可能还需要更加复杂的协作关系,比如上面存储的例子,如果除了存1元钱的线程,还有取1元钱的线程,假设取钱的方法是
public void withDraw(int amount) {
int newBalance = balance - amount;
try {
Thread.sleep(5);
}
catch (InterruptedException ex) {
}
balance = newBalance;
}
基于同步的知识,我们知道不论取钱线程还是取钱线程,这些线程不能同时进入这两个函数,否则会像之前只有单独的存储线程那样造成balance中值的异常。因此这两个函数都需要声明为同步的,任何一个时刻只能有一个线程可以进入它们中的一个,其余线程被阻塞在这两个函数之外。
public synchronized void withDraw(int amount) {
int newBalance = balance - amount;
try {
Thread.sleep(5);
}
catch (InterruptedException ex) {
}
balance = newBalance;
}
但是目前,我们的线程对帐号的操作是有区别的,取钱线程有作用的一个前提是账号上有钱,即balance是大于0的。如果balance等于0,即使取钱线程被允许进入同步函数,它也无法实施真正的取钱操作,即修改balance的值。那么这里就引出一个问题,线程不但要逐个进入关键代码段,而且如果各个线程对关键代码段的操作含义不一样,那么在某些条件下,有些线程可以进入等待状态,只有在那些条件消失的情况下,等待状态下的线程才有必要苏醒了,并准备继续运行。比如,当取钱线程发现当前账号上是0元时,它就可以进入等待状态,并允许其它线程进入同步代码块中运行。在某个存钱线程把钱存入账号后,存钱线程就可以发出通知,让等待的取钱线程准备继续运行。
Java中,可以使用Object类的wait()方法让线程进入等待状态,Object类的notify()或者notifyAll()方法通知等待线程退出等待状态。它们都是在同步的代码块中或者方法中被调用才有意义。
import java.util.concurrent.*;
public class AccountWithoutSync {
private static Account account = new Account(); // 创建一个Account对象,其balance成员为0
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.execute(new DepositTask());
executor.execute(new WithdrawTask());
executor.shutdown();
}
private static class DepositTask implements Runnable {
public void run() {
try {
while(true) {
account.deposit((int)(Math.random() * 10) + 1);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private static class WithdrawTask implements Runnable {
public void run() {
while(true) {
account.withDraw((int)(Math.random() * 10) + 1);
}
}
}
// An inner class for account,Account内部类
private static class Account {
private int balance = 0;
public int getBalance() {
return balance;
}
public synchronized void deposit(int amount) {
balance += amount;
System.out.println("Deposit " + amount + "\t\t\t\t\t" + getBalance());
notifyAll();
}
public synchronized void withDraw(int amount) {
try {
while(amount > balance) {
System.out.println("\t\t\tFail Withdraw " + amount + "\t\t" + getBalance());
wait();
}
int newBalance = balance - amount;
balance = newBalance;
System.out.println("\t\t\tWithdraw " + amount + "\t\t" + getBalance());
}
catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
}
线程通信的一个案例
生产者消费者模式是一个典型的使用线程通信的场景,生产者线程往队列中存入数据,消费者线程从队列中取出数据,首先多个线程访问同一个数据-队列,那么存入和取出数据的代码需要同步,其次,当队列满了生产者线程需要等待消费者取出数据后才能继续生产,当队列空了消费者线程需要等待生产者线程存入数据后才能运行消费,这两个线程之间需要等待和通知继续运行。
class ConsumerProducer{
private static Buffer buffer = new Buffer();
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.execute(new ProducerTask());
executor.execute(new ConsumerTask());
executor.shutdown();
}
// A task for adding an int to the buffer
private static class ProducerTask implements Runnable {
public void run() {
try {
int i = 1;
while (true) {
buffer.write(i++); // Add a value to the buffer
// Put the thread into sleep
Thread.sleep((int)(Math.random() * 10000));
}
}
catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
// A task for reading and deleting an int from the buffer
private static class ConsumerTask implements Runnable {
public void run() {
try {
while (true) {
buffer.read();
// Put the thread into sleep
Thread.sleep((int)(Math.random() * 10000));
}
}
catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
// An inner class for buffer
private static class Buffer {
private static final int CAPACITY = 1; // buffer size
private java.util.LinkedList<Integer> queue =
new java.util.LinkedList<>();
public synchronized void write(int value) {
System.out.println("Producer ready to writes " + value);
try {
while (queue.size() == CAPACITY) {
System.out.println("Wait for notFull condition");
wait();
}
queue.offer(value);
System.out.println("Producer writes " + value + " , " + "buffer: " + queue.toString());
notify();
}
catch (InterruptedException ex) {
ex.printStackTrace();
}
}
public synchronized int read() {
System.out.println("\t\t\tConsumer ready to read");
int value = 0;
try {
while (queue.isEmpty()) {
System.out.println("\t\t\tWait for notEmpty condition");
wait();
}
value = queue.remove();
notify();
}
catch (InterruptedException ex) {
ex.printStackTrace();
}
finally {
System.out.println("\t\t\tConsumer reads " + value + " , " + "buffer: " + queue.toString());
return value;
}
}
}
}
用Condition实现线程通信(可选)
调用lock对象的newCondition()方法,可以创建一个Condition对象,通过调用Condition对象的await()、signal()和signalAll()方法可以进行线程通信。
import java.util.concurrent.*;
import java.util.concurrent.locks.*;
public class ThreadCooperation {
private static Account account = new Account();
public static void main(String[] args) {
// Create a thread pool with two threads
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.execute(new DepositTask());
executor.execute(new WithdrawTask());
executor.shutdown();
System.out.println("Thread 1\t\tThread 2\t\tBalance");
}
public static class DepositTask implements Runnable {
@Override // Keep adding an amount to the account
public void run() {
try { // Purposely delay it to let the withdraw method proceed
while (true) {
account.deposit((int)(Math.random() * 10) + 1);
Thread.sleep(1000);
}
}
catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
public static class WithdrawTask implements Runnable {
@Override // Keep subtracting an amount from the account
public void run() {
while (true) {
account.withdraw((int)(Math.random() * 10) + 1);
}
}
}
// An inner class for account
private static class Account {
// Create a new lock
private static Lock lock = new ReentrantLock();
// Create a condition
private static Condition newDeposit = lock.newCondition();
private int balance = 0;
public int getBalance() {
return balance;
}
public void withdraw(int amount) {
lock.lock(); // Acquire the lock
try {
while (balance < amount) {
System.out.println("\t\t\tWait for a deposit");
newDeposit.await();
}
balance -= amount;
System.out.println("\t\t\tWithdraw " + amount +
"\t\t" + getBalance());
}
catch (InterruptedException ex) {
ex.printStackTrace();
}
finally {
lock.unlock(); // Release the lock
}
}
public void deposit(int amount) {
lock.lock(); // Acquire the lock
try {
balance += amount;
System.out.println("Deposit " + amount +
"\t\t\t\t\t" + getBalance());
// Signal thread waiting on the condition
newDeposit.signalAll();
}
finally {
lock.unlock(); // Release the lock
}
}
}
}