1、进程与线程概述
- 进程(英语:process),是计算机中已运行程序的实体。
进程曾经是分时系统的基本运作单位。在面向进程设计的系统(如早期的UNIX,Linux 2.4及更早的版本)中,进程是程序的基本执行实体;- 在面向线程设计的系统(如当代多数操作系统、Linux 2.6及更新的版本)中,进程本身不是基本运行单位,而是线程的容器。程序本身只是指令、数据及其组织形式的描述,进程才是程序(那些指令和数据)的真正运行实例。
- 如下所示为Windows资源管理器,选择【进程】选项卡,则能看到运行在电脑上的所有进程。
- 线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
- 科普:我们都知道,程序的运行是靠CPU来处理的,若只有一个CPU的情况下,如何保证多个程序能够同时运行呢?如下图所示,我们把CPU的执行时间分成很多的小块,每一小块的时间都是固定的,我们把这个小块称为时间片,时间片的时间是很短的(例如只有1ms),若此时有音乐播放器、代码编辑器、qq等软件同时运行,那么这些软件会随机获取CPU的执行时间,简单说就是比如播放器运行1ms然后把使用权交给代码编辑器,代码编辑器运行1ms然后把使用权交给qq,即这些软件会随机地获取cpu的执行时间,对CPU来说,这些软件是在轮流运行的,但是由于运行时间间隔很短,作为使用者是很难感觉到这种变化的,因此对使用者来说,这些软件是在同时运行的。以上的运行方式称为时间片轮转。
2、线程的创建
- 创建多线程共有三种方式:
- 继承Thread类,重写run()方法,run()方法代表线程要执行的任务。
- 实现Runnable接口,重写run()方法,run()方法代表线程要执行的任务。
- 实现Callable接口,重写call()方法,call()作为线程的执行体,具有返回值,并且可以对异常进行声明和抛出;使用start()方法来启动线程。
2.1、通过Thread类创建线程
- Thread是一个线程类,位于java.lang包下,常用的构造方法如下:
构造方法 | 说明 |
---|---|
Thread() | 创建一个线程对象 |
Thread(String name) | 创建一个具有指定名称的线程对象 |
Thread(Runnable target) | 创建一个基于Runnable接口实现类的线程对象 |
Thread(Runnable target,String name) | 创建一个基于Runnable接口实现类,并且具有指定名称的线程对象 |
- Thread类的常用方法
方法 | 说明 |
---|---|
public void run() | 线程相关的代码写在该方法中,一般需要重写 |
public void start() | 启动线程的方法 |
public static void sleep(long m) | 线程休眠m毫秒的方法 |
public void join() | 抢占资源,优先执行调用调用join()方法的线程 |
- 1.新建一个名为
ThreadProj
的Java项目,新建一个com.cxs.thread
包,在包下新建一个MyThread
类并继承自Thread
类,访问修饰符选择package
。
class MyThread extends Thread {
@Override
public void run() {
System.out.println(getName() + "该线程正在执行!");//getName()获取系统自动分配的线程名
}
}
- 2.在同包下新建一个测试类
ThreadTest
。
public class ThreadTest {
public static void main(String[] args) {
System.out.println("我是主线程");
MyThread mThread = new MyThread();
mThread.start();// 启动线程
System.out.println("我是主线程");
}
}
- 3.运行代码,如下图所示,运行着主线程和Thread-0两个线程。
- 4.每个线程只允许被启动一次,连续两次启动线程则程序会发生异常。
- 5.修改上面的MyThread代码,如下所示。
class MyThread extends Thread {
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName() + "正在运行" + i);
}
}
}
- 6.修改上面的ThreadTest代码,如下所示。
public class ThreadTest {
public static void main(String[] args) {
MyThread mThread1 = new MyThread("线程1");
MyThread mThread2 = new MyThread("线程2");
mThread1.start();
mThread2.start();
}
}
- 7.运行两次代码,得到不同的结果,这说明,线程运行时是随机获取CPU的使用权的。
2.2、通过实现Runnable接口创建线程
- Runnable是Java中用以实现线程的接口
- Runnable接口中只有一个方法run();
- 任何实现线程功能的类都必须实现该接口(Thread类也实现了该接口,详见API文档)
- 上面已经讲了创建多线程的方式,为什么还要通过实现Runnable接口来创建线程?一种方式不够吗?主要有以下两方面的原因。
- 由于Java不支持多继承,若你创建的类已经继承了一个类,再去继承Thread类,这样是不可能的(只能是单继承)。但是接口的话可以实现多个接口。
- 不打算重写Thread类的其他方法,并且在实际使用中,通过实现Runnable接口来创建线程应用的更广泛一些。
- 1.新建一个
com.cxs.runnable
包,在包下新建一个RunnableDemo
类,并实现Runnable
接口
class RunnableDemo implements Runnable {
@Override
public void run() {
int i = 1;
while (i <= 10) {
System.out.println(Thread.currentThread().getName() + "正在运行!" + (i++));
}
}
}
- 2.在包下新建一个
RunnableTest
测试类。
public class RunnableTest {
public static void main(String[] args) {
RunnableDemo r1 = new RunnableDemo();
Thread t1 = new Thread(r1);
t1.start();
RunnableDemo r2 = new RunnableDemo();
Thread t2 = new Thread(r2);
t2.start();
}
}
- 3.运行代码结果如下。
- 4.修改一下两个类的代码。
class RunnableDemo implements Runnable {
int i = 1;//←——将i变为成员变量
@Override
public void run() {
while (i <= 10) {
System.out.println(Thread.currentThread().getName() + "正在运行!" + (i++));
}
}
}
public class RunnableTest {
public static void main(String[] args) {
RunnableDemo r1 = new RunnableDemo();
Thread t1 = new Thread(r1);
t1.start();
Thread t2 = new Thread(r1);// 两个线程运行同一个实现类对象
t2.start();
}
}
- 5.运行代码,结果如下。在实现类中的run()方法可以被多个线程执行,也就是是多个线程在处理同一个任务的情况。
2.3、通过实现Callable接口创建线程
- 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
- 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
- 使用FutureTask对象作为Thread对象的target,创建并启动新线程。
- 调用FutureTask对象的get()方法,来获得子线程执行结束后的返回值。
- 1.创建一个com.cxs.callable包,在包下新建一个CallableDemo类,并实现Callable接口。
class CallableDemo implements Callable<String> {
@Override
public String call() throws Exception {
String str = "多线程的第三种创建方式";
return str;
}
}
- 2.新建一个测试类Test(运行结果略)。
public class Test {
public static void main(String[] args) {
Callable<String> callable = new CallableDemo();
FutureTask<String> futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start();
// 先启动线程后才可以获取call()方法的返回值
try {
System.out.println(futureTask.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
3、线程的生命周期
3.1、生命周期概述
- 线程的状态:
- 新建(New)
- 可运行(Runnable),也称为就绪
- 正在运行(Running)
- 阻塞(Blocked)
- 终止(Dead)
- 如下图所示为线程的生命周期:
- 创建线程对象使线程进入新建状态;
- 通过调用start()方法来启动线程,使线程进入可运行状态【此时线程若获取cpu使用权后才会进入正在运行状态,而cpu使用权是有时间限制的,一旦到了期限(即时间片用完或者调用yield()方法),线程便又会进入可运行状态,去等待获取下一次的cpu使用权】;
- 通过调用join()、wait()、sleep()方法或者进行I/O请求,使线程从正在运行的状态进入阻塞状态【阻塞状态是不能直接变为正在运行状态的,因为只有获取cpu的使用权才能进入正在运行状态,因此要先进入可运行状态,之后才能进入正在运行状态】。
- 阻塞状态只有在以下情况才会进入可运行状态:
- 等待调用join()的线程执行完毕;
- 调用notify()或notifyAll()方法(对应的wait()方法,即调用wait()方法的线程只有调用notify()或notifyAll()方法才能进入可运行状态);
- 之前调用sleep()方法阻塞线程,当休眠超时以后,线程便会重新进入可运行状态;
- 一旦I/O请求完成,线程便会从阻塞状态进入可运行状态。
- 线程执行完毕或异常终止,会进入终止状态。以上四个状态都可以通过调用stop()方法使线程进入终止状态。【stop()现在已经不建议使用了,在jdk中会显示版本过期】。
更多有关线程终止的内容请参考下面两篇博客:
3.2、sleep方法的使用
- sleep()方法是Thread类中的方法,形式为:
public static void sleep(long millis)
,显然该方法是一个静态方法,在调用时一般会使用Thread.sleep()
形式去掉用。 - 该方法作用:在指定的毫秒数内让正在执行的线程休眠(暂停执行,进入阻塞状态)。
- 参数为休眠时间,单位是毫秒。
- 这里不再进行代码演示,比如休眠1秒,则在run()方法中添加
Thread.sleep(1000)
并捕获InterruptedException
异常即可,大家可自行在前面代码的基础上进行尝试。
3.3、join方法的使用
- join()方法也是Thread类中的方法,形式为:
public final void join()
,执行该方法也需捕获InterruptedException
异常。 - join()方法的作用:等待调用该方法的线程结束后才能执行,即调用join()方法的线程优先执行,待其执行完毕,其余线程才会执行。(演示略)
public final void join(long millis)
,该方法为等待该线程终止的最长时间为millis毫秒。
3.4、线程的优先级
- Java为线程类提供了10个优先级。
- 优先级可以用整数1-10表示,超过范围会抛出异常。
- 主线程默认优先级为5,数字越大,优先级越高。
- 除了数字外还可以采用优先级常量来表示线程的优先级:
- MAX_PRIORITY:线程的最高优先级10;
- MIN_PRIORITY:线程的最低优先级1;
- NORM_PRIORITY:线程的默认优先级5;
- 优先级相关的方法:
public int getPriority()
:获取线程的优先级,例如:int mainPriority=Thread.currentThread().getPriority();
可以获取当前线程的优先级。public void setPriority(int newPriority)
:设置线程的优先级。可以在线程启动前为线程设置优先级。(可在前面编写的创建线程的代码基础上进行该方法的尝试,这里不再演示)
注意:即使为线程设置了优先级,但由于程序的运行与操作系统的环境和cpu的工作方式等因素有关,并不能保证优先级高的线程优先执行,还会存在一定的随机性。
4、线程同步
- 多线程运行存在以下问题:
- 各个线程是通过竞争cpu时间而获得运行机会的。
- 各个线程什么时候得到cpu时间,占用多久,是不可预测的。
- 一个正在运行着的线程在什么地方被暂停是不确定的。
下面我们来模拟一个银行存取款的案例 |
- 1.新建一个
com.cxs.bank
的包,在包下新建一个银行类Bank
。
public class Bank {
private String account;// 账号
private int balance;// 账户余额
public Bank(String account, int balance) {
this.account = account;
this.balance = balance;
}
public String getAccount() {
return account;
}
public void setAccount(String account) {
this.account = account;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
@Override
public String toString() {
return "Bank [账号:" + account + ", 余额:" + balance + "]";
}
// 存款
public void saveAccount() {
int balance = getBalance();// 获取当前账号余额
balance += 100;// 存100元并修改余额
setBalance(balance);// 修改当前账户余额
System.out.println("存款后的账户余额为(存款100):" + balance);
}
// 取款
public void drawAccount() {
int balance = getBalance();// 获取当前账号余额
balance -= 200;// 取200元并修改余额
setBalance(balance);// 修改当前账户余额
System.out.println("取款后的账户余额为(取款200):" + balance);
}
}
- 2.在
bank
包下新建一个存款类SaveAccount
并实现Runnable
接口,并将存款操作放于线程中(即run()
方法中)执行。
public class SaveAccount implements Runnable {
Bank bank;
public SaveAccount(Bank bank) {
this.bank = bank;
}
@Override
public void run() {
bank.saveAccount();
}
}
- 3.在
bank
包下新建一个取款类DrawAccount
并实现Runnable
接口,并将取款操作放于线程中(即run()
方法中)执行。
public class DrawAccount implements Runnable {
Bank bank;
public DrawAccount(Bank bank) {
this.bank = bank;
}
@Override
public void run() {
bank.drawAccount();
}
}
- 4.在
bank
包下新建一个测试类Test
。执行过程为:创建一个银行账户,初始余额为1000→启动存款线程执行存款操作→启动取款线程执行取款操作→打印输出账户最终余额
public class Test {
public static void main(String[] args) {
Bank bank = new Bank("s001", 1000);
SaveAccount saveAccount = new SaveAccount(bank);
DrawAccount drawAccount = new DrawAccount(bank);
Thread saveThread = new Thread(saveAccount);
Thread drawThread = new Thread(drawAccount);
saveThread.start();
drawThread.start();
System.out.println("经过存取款操作后,账户的余额为(初始余额为1000):" + bank);
}
}
- 5.程序真会按照我们预先设定好的思路运行吗?运行代码结果如下所示。多次执行代码,发现得到的结果不一样。很显然,由于各个线程是通过竞争cpu时间而获得运行机会的(上面总结的多线程存在的问题有说到),存在不确定性。
为了能让存款和取款的线程优先执行,因此让这两个线程执行join()方法。 |
- 6.修改测试类
Test
代码如下:
public class Test {
public static void main(String[] args) {
Bank bank = new Bank("s001", 1000);
SaveAccount saveAccount = new SaveAccount(bank);
DrawAccount drawAccount = new DrawAccount(bank);
Thread saveThread = new Thread(saveAccount);
Thread drawThread = new Thread(drawAccount);
saveThread.start();
drawThread.start();
// 为了让打印余额的语句最后执行,则让两个线程执行join()方法而优先执行
try {
saveThread.join();
drawThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("经过存取款操作后,账户的余额为(初始余额为1000):" + bank);
}
}
- 7.再次运行程序,结果如下,多次运行后,发现存款和取款的操作执行顺序仍然不确定。
为了模拟更真实的场景,比如在银行存取款的人有好多,那么就有好多存取款的线程,这么多线程在运行,就不能保证存取款的线程都能正常完成,也就是该线程在什么地方被暂停运行是不确定的,这里我们用线程休眠的方式进行模拟。 |
- 8.修改Bank类中的存取款方法,在两个方法的随机位置休眠1秒。
// 存款
public void saveAccount() {
int balance = getBalance();// 获取当前账号余额
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance += 100;// 存100元并修改余额
setBalance(balance);// 修改当前账户余额
System.out.println("存款后的账户余额为(存款100):" + balance);
}
// 取款
public void drawAccount() {
int balance = getBalance();// 获取当前账号余额
balance -= 200;// 取200元并修改余额
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
setBalance(balance);// 修改当前账户余额
System.out.println("取款后的账户余额为(取款200):" + balance);
}
9.运行程序,多次运行程序得到下面两种情形。
- 为了保证在存款或取款的时候,不允许其他线程对账户余额进行操作,需要对Bank对象进行锁定
- 这里我们使用关键字synchronized实现。它能确保共享对象在同一时刻只能被一个线程访问。
- synchronized关键字可以用在三个地方:
- 成员方法:public synchronized void saveAccount(){}
- 静态方法:public static synchronized void saveAccount(){}
- 语句块:synchronized (obj){……}
10.给Bank类中存取款方法加上关键字synchronized。
// 存款
public synchronized void saveAccount() {// ←——第一种方式
int balance = getBalance();// 获取当前账号余额
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance += 100;// 存100元并修改余额
setBalance(balance);// 修改当前账户余额
System.out.println("存款后的账户余额为(存款100):" + balance);
}
// 取款
public void drawAccount() {
synchronized (this) {// ←——第三种方式
int balance = getBalance();// 获取当前账号余额
balance -= 200;// 取200元并修改余额
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
setBalance(balance);// 修改当前账户余额
System.out.println("取款后的账户余额为(取款200):" + balance);
}
}
- 11.再次运行代码,多次运行,结果一致。
5、线程间通信
下面我们模拟这样一个场景,我们建一个生产线名为Queue,有生产者Producer和消费者Consumer两个角色,Producer负责生产(这里是将一个数字加一,代表生产了一件产品),Consumer负责消费,将生产的产品展示出来即代表消费,然后新建两条线程分别进行生产和消费,那么我们会遇到哪些问题呢?又该如何解决? |
- 1.新建一个
com.cxs.queue
的包,在包下新建一个Queue
类。定义一个数字,并编写getter
和setter
方法,并加上同步关键字synchronized
。
public class Queue {
private int n;
public synchronized int getN() {
System.out.println("消费:" + n);
return n;
}
public synchronized void setN(int n) {
System.out.println("生产:" + n);
this.n = n;
}
}
- 2.新建一个
Producer
类并实现Runnable
接口,为了更真实地模拟,在数字加1后让线程休眠1秒。
public class Producer implements Runnable {
Queue queue;
public Producer(Queue queue) {
this.queue = queue;
}
@Override
public void run() {
int i = 0;
while (true) {
queue.setN(i++);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 3.新建一个
Consumer
类并实现Runnable
接口,为了更真实地模拟,在输出数字后让线程休眠1秒。
public class Consumer implements Runnable {
Queue queue;
public Consumer(Queue queue) {
this.queue = queue;
}
@Override
public void run() {
while (true) {
queue.getN();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 4.新建一个测试类
Test
。
public class Test {
public static void main(String[] args) {
Queue queue = new Queue();
new Thread(new Producer(queue)).start();
new Thread(new Consumer(queue)).start();
}
}
- 5.运行应用,结果如下,生产和消费并没有轮流执行。
我们这里可以将Queue看做一个容器,在其中定义一个布尔值变量flag,当flag为false时代表容器中没有数据,这时就需要调用set方法去生产数据,数据生产完毕后,flag的值就变为true,这时就可以调用get方法去获取数据了。同理,当flag为false时,我们是不能消费的,此时需要等待(调用wait()方法使线程进入阻塞状态),当flag为true时,我们就不需要再生产了,此时也需要等待。即建立一种线程间的通信,使得程序按照我们预期进行运转。 |
- 6.修改Queue类的代码。
public class Queue {
private int n;
boolean flag = false;// true代表有数据,false代表没数据
public synchronized int getN() {
if (!flag) {// 若没有数据,则没法消费,则需要等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("消费:" + n);
flag = false;// 消费完毕,容器就没有数据了
return n;
}
public synchronized void setN(int n) {
if (flag) {// 若有数据则需要等待被消费
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("生产:" + n);
this.n = n;
flag = true;// 生产完毕,则容器中就有数据了
}
}
- 7.运行程序,发现程序运行一会儿便停止了生产和消费,但是程序没有停止运行。
这是因为发生了死锁,即两个线程都进入了阻塞状态,这时就需要通过notify()或notifyAll()来,这里我们使用notifyAll()来唤醒所有的线程。 |
- 8.在getter和setter方法中都添加notifyAll()方法,再次运行程序,此时程序就会按照预期运行(结果图略)。
public class Queue {
private int n;
boolean flag = false;// true代表有数据,false代表没数据
public synchronized int getN() {
if (!flag) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("消费:" + n);
flag = false;
notifyAll();// ←——
return n;
}
public synchronized void setN(int n) {
if (flag) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("生产:" + n);
this.n = n;
flag = true;
notifyAll();// ←——
}
}