一个程序同时执行多个任务。通常,每一个任务称为一个线程( thread ), 它是线程控制的简称。可以同时运行一个以上线程的程序称为多线程程序(multithreaded)。
进程与线程的区别
区别 | 进程 | 线程 |
---|---|---|
根本区别 | 作为资源分配的单位 | 调度和执行的单位 |
开销 | 每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销 | 线程可以看成是轻量级的进程,同一类线程共享代码和数据空间,每一个线程有独立的运行栈和程序计数器(PC),线程切换的开销小 |
所处环境 | 在操作系统中能同时运行多个任务(程序) | 在同一个应用程序中有多个顺序流同时执行 |
分配内存 | 系统在运行时会为每个进程分配不同的内存区域 | 除了CPU之外,不会为线程分配内存(线程所使用的资源是它所属进程的资源),线程组只能共享资源 |
包含关系 | 没有线程的进程是可以被看作单线程的,如果一个进程内拥有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的 | 线程是进程的一部分,所以线程有的时候被称为是轻权进程或者轻量级进程 |
Java实现多线程的方法
1. 继承Thread类
- Java中负责多线程功能的类是
java.lang.Thread
- 通过创建 Thread 的实例来创建新的线程
- 每个线程都是通过特定的Thread对象所对应的方法run()(线程体)来完成操作
- 调用 Thread 类的start()方法来启动一个线程
- 缺点:如果类已经继承了一个类(如小程序必须继承自 Applet 类),则无法再继承 Thread 类
1 | public class TestThread extends Thread{ // 继承Thread类 |
2. 实现Runnable接口
- 实现 Runnable 接口的同时可以继承某个类,此方法更通用一些
1 | public class TestThread2 implements Runnable{ // 实现 Runnable 接口 |
线程的状态
线程有以下5中状态(ps:《Java核心技术I 》中是6种)
要确定一个线程的当前状态,可调用 getState 方法
- 新生状态(NEW)
用new关键字(如new Thread(r))建立一个线程对象后,该线程就处于新生状态 - 就绪状态(Runnable)
具备运行条件,但没被分配到CPU,处于“线程就绪状态”,等待系统为其分配CPU。就绪状态并不是执行状态,当系统选定一个等待执行Thread对象后,它就会进入执行状态,一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。
导致线程进入就绪状态的4个原因:- 新建线程:调用
start()
方法,进入就绪状态 - 阻塞线程:阻塞解除,进入就绪状态
- 运行线程:调用
yield()
方法,直接进入就绪状态 - 运行线程:JVM将CPU资源从本线程切换到其它线程
- 新建线程:调用
- 运行状态(Running)
在运行状态的线程执行自己run方法中的代码,直到调用其他方法而终止或等待某资源而阻塞或完成任务而死亡。如果在给定的时间片内没有执行结束,就会被系统给换下来回到就绪状态。也可能由于某些“导致阻塞的事件”而进入阻塞状态。 - 阻塞状态(Blocked)
暂停一个线程的执行以等待某个条件发生(如某资源就绪)。
导致阻塞的4种原因:- 执行
sleep(int millsecond)
方法,使当前线程休眠,进入阻塞状态。当到了指定的时间后,线程进入就绪状态。 - 执行
wait()
方法,使当前线程进入阻塞状态。当使用notify()
方法唤醒该线程后,进入就绪状态 - 线程运行时,某个操作进入阻塞状态,比如执行IO流操作(
read()/write()
方法本身就是阻塞的方法)。只有当引起该操作阻塞的原因消失后,线程进入就绪状态 join()
线程联合: 当某个线程等待另一个线程执行结束后,才能继续执行时,使用join()方法
- 执行
- 死亡状态(Terminated)
死亡状态是线程生命周期中的最后一个阶段。
线程死亡的两个原因:- 正常运行的线程完成了它 run() 方法内的全部工作
- 线程被强制终止,如通过执行 stop() 或 destroy() 方法(这两个方法已过时)来终止一个线程
当一个线程进入死亡状态以后,就不能再回到其它状态了
终止线程的典型方式
设置一个 boolean型的终止变量,当它为 false 时,终止线程的运行
1 | public class TestThreadCiycle implements Runnable { |
暂停线程sleep/yield
sleep()
:可以让正在运行的线程进入阻塞状态,直到休眠时间满了,进入就绪状态yield()
:可以让正在运行的线程直接进入就绪状态,让出CPU的使用权
1 | public class TestThreadState { |
线程的联合 join()
线程A在运行期间,可以调用线程B的join()
方法,让线程B和线程A联合。这样,线程A就必须等待线程B执行完毕后,才能继续执行。
1 | package thread; |
获取线程基本信息的方法
方法 | 功能 |
---|---|
isAlive() |
判断线程是否还未终止 |
getPriority() |
获取线程的优先级数值 |
setPriority() |
设置线程的优先级数值 |
setName() |
设置线程名称 |
getName() |
获取线程名称 |
currentThread() |
获取当前正在运行的线程对象,即取得自己本身 |
线程的优先级
- 线程的优先级用数字表示,范围是 1 ~ 10,一个线程的缺省优先级是 5
- 优先级低只是意味着获得调度的概率低。并不是绝对 先调用优先级高的线程 后调用优先级低的线程
1 | public class TestThread { |
线程同步
- 多个线程访问同一个对象,且某些线程还想修改这个对象。此时,需要用到“线程同步”。
- 线程同步其实是一个等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程再使用
线程不同步——银行取钱
1 | package thread; |
实现线程同步——银行取钱
synchronized 方法
- synchronized 方法控制对“对象的类成员变量”的访问
- 每个对象对应一把锁,每个 synchronized 方法都必须获得调用该方法的对象的锁方能执行,否则所属线程阻塞
- 方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态
- 缺点:若将一个大的方法声明为synchronized 将会大大影响效率。
1
2
3
4public synchronized void method(int args)
{
// method body
}synchronized 块
1
2
3synchronized(syncObject){
// 允许访问控制的代码
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72package thread;
public class TestSync {
public static void main(String[] args) {
Account a = new Account("A", 100);
Drawing drawing1 = new Drawing(a, 80, "我取钱");
Drawing drawing2 = new Drawing(a, 80, "她取钱");
drawing1.start();
drawing2.start();
}
/**
* 银行账户
*/
static class Account {
String name; // 账户名称
int money; // 金额
public Account(String name, int money) {
this.name = name;
this.money = money;
}
}
/**
* 模拟提款操作
*/
static class Drawing extends Thread {
Account account; // 提款的账户
int drawingNum; // 提取的金额
int expenseTotal; // 总共提取的金额
public Drawing(Account account, int drawingNum, String name) {
super(name);
this.account = account;
this.drawingNum = drawingNum;
}
public void run() {
draw();
}
public void draw() {
// 可提高性能
if(account.money<=0){
return;
}
synchronized (account) { // 线程需获得account对象的“锁”才有资格运行同步块中的代码
if (account.money - drawingNum < 0) {
System.out.println(this.getName() + "-- 余额不足!");
return;
}
try {
Thread.sleep(1000); // 判断完后阻塞。其他线程开始运行。
} catch (InterruptedException e) {
e.printStackTrace();
}
account.money -= drawingNum;
expenseTotal += drawingNum;
}
System.out.println(this.getName() + "--账户余额:" + account.money);
System.out.println(this.getName() + "--总共取了:" + expenseTotal);
}
}
}
/*
我取钱--账户余额:20
她取钱-- 余额不足!
我取钱--总共取了:80
*/
死锁
- 多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
- 死锁产生的四个必要条件:
- 互斥使用。当资源被一个线程使用(占有)时,别的线程不能使用
- 不可抢占。资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放
- 请求和保持。当资源请求者在请求其它资源的同时保持对原有资源的占有
- 循环等待。存在一个等待队列:P1占有P2的资源,P2占有P3的 资源;P3占有P1的资源,形成一个等待环路
- 打破其中任一条件便可解决死锁问题
死锁问题
1 | /** |
解决死锁
死锁是由于“同步块需要同时持有多个对象锁造成”的,所以同一个代码块,不要同时持有两个对象锁就可以解决了
1 | public class LockTest { |
线程并发协作(生产者/消费者模式)
- 生产者:负责生产数据的模块(模块可能是方法、对象、线程、进程)
- 消费者:负责处理数据的模块
- 缓冲区:消费者不能直接使用生产者的数据,生产者将生产好的数据放入“缓冲区”,消费者从“缓冲区”拿取要处理的数据
- 缓冲区是实现并发的核心,缓冲区的设置的3个好处
- 实现线程的并发协作。有了缓冲区以后,生产者线程只需要往缓冲区里面放置数据,而不需要管消费者消费的情况;同样,消费者只需要从缓冲区拿数据处理即可,也不需要管生产者生产的情况。 这样,就从逻辑上实现了“生产者线程”和“消费者线程”的分离
- 解耦了生产者和消费者。生产者不需要和消费者直接打交道
- 解决忙闲不均,提高效率。生产者产生数据慢时,缓冲区任有数据,不影响消费者消费;消费者处理数据慢时,生产者然可以继续玩缓冲区里放置数据
wait()
、notify()
、notifyAll()
均是java.lang.Object
类的方法,只能在同步方法或同步代码中使用,否则会抛出异常
1 | public class TestProduce { |
任务定时调度
java.util.Timer
: 定时或每隔一定时间触发一次线程。Timer类本身是一个线程,只是它用来调用其它的线程java.util.TimerTask
:TimerTask类是一个抽象类,实现了Runnable接口,具备多线程能力。- 在实际使用时,一个Timer可以启动任意多个TimerTask实现的线程,但是多个线程之间会存在阻塞。所以如果多个线程之间需要完全独立的话,最好还是一个Timer启动一个TimerTask实现
- 可以使用开源框架quanz,更加方便的实现任务定时调度
1 | import java.util.Timer; |
参考《 Java核心技术 卷 I 》及互联网