目录
多线程是并发编程中的一种重要实现方式,在讲解多线程之前先了解一下线程和进程。
在操作系统理论中,进程(Process)是资源分配的独立单位,线程(Thread)是资源调度的独立单位;以现实中的场景为例子,就好比是QQ和里面的每一个聊天框,QQ是得到系统独立的资源分配,类进程机制,同时里面每个聊天框互不干扰,独立进行任务,类线程机制。
一、线程的创建方式
在Java中,创建线程主要有三种方式:通过继承Thread
类、实现Runnable
接口,以及实现Callable
接口。这三种方式各有特点和适用场景,下面逐一演示。
1.继承Thread
类
这是最直接的创建线程的方式。通过继承Thread
类并重写run()
方法,可以定义线程的执行逻辑。
实现步骤
-
定义一个类继承
Thread
。 -
重写
run()
方法:在run()
方法中定义线程的具体任务。 -
创建线程对象并启动:通过调用
start()
方法启动线程。
代码演示
实现一个方法:
class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程正在运行:" + Thread.currentThread().getName());
}
}
//测试(懒得重写一个Test类new MyThread测试了,就在本类中写Main方法凑活看)
public class Main {
public static void main(String[] args) {
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
myThread1.start(); // 启动线程
myThread2.start(); // 启动线程
}
}
注:在
run()
方法中调用Thread.currentThread().getName()
,因为当前执行代码的线程是新启动的myThread
线程,而在代码里并没有显式地给这个线程设置名称,所以 Java 会给它分配一个默认名称。默认情况下,线程的名称格式为Thread-X
,这里的X
是一个整数,从 0 开始依次递增。
得到结果:
实现多个方法:
当然,重写Run方法来实现线程需要的功能并不意味着只能实现一个方法一个功能,还可以用下面的方法实现多个方法:
class MyThread extends Thread {
// 自定义方法 1,用于执行功能 1
private void function1() {
System.out.println("执行功能 1");
}
// 自定义方法 2,用于执行功能 2
private void function2() {
System.out.println("执行功能 2");
}
@Override
public void run() {
function1();
function2();
}
}
public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
优点
-
实现简单,直接继承
Thread
类并重写run()
方法即可。
缺点
-
单继承限制:Java不支持类的多继承,如果一个类已经继承了其他类,就不能再继承
Thread
类。 -
功能局限性:
Thread
类本身已经包含了线程的实现逻辑,继承Thread
类可能会限制线程类的扩展性。
2.实现Runnable
接口
这是更推荐的方式,因为它避免了单继承的限制(一个类可以同时实现多个接口),并且可以更好地分离线程的执行逻辑和线程的控制。
实现步骤
-
定义一个类实现
Runnable
接口。 -
实现
run()
方法:在run()
方法中定义线程的任务。 -
创建
Thread
对象并传入Runnable
实例:通过Thread
类的构造函数将Runnable
实例传递给线程。 -
启动线程:调用
Thread
对象的start()
方法。
代码演示
public class DownLoad implements Runnable {
private String name;
public DownLoad(String name) {
this.name = name;
}
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println(name+"下载了"+i+"%");
}
}
}
测试TestMain:
public class ThreadTest {
public static void main(String[] args) {
//创建线程对象
Thread t = new Thread(new DownLoad("Thread1"));
Thread t1 = new Thread(new DownLoad("Thread2"));
t.start();
t1.start();
}
}
注:上面Main函数中可以这种格式创建线程对象是因为:
Thread
类提供了多个构造方法,其中有一个构造方法的签名如下: public Thread(Runnable target, String name)
得到结果:
由于创建线程本质上还是Thread类,所以可以看到用继承Runnable接口的方式创建线程在Main方法中的实现中是new
Thread,并在 new出的Thread放入实现了Runnable接口的DownLoad类;
在继承Thread类创建线程的方法中,Main函数中可以直接new这个继承了Thread类的子类,这就涉及子类可以继承父类的属性了。
优点
-
避免单继承限制:可以与任何其他类组合使用。
-
更好的扩展性:
Runnable
接口的实现类可以专注于任务逻辑,而线程的控制由Thread
类负责。
缺点
-
代码稍显复杂:需要额外创建一个
Thread
对象来启动线程(上面解释过了,为什么实现Runnable
接口会比继承Thread类的方法更加复杂)。
3.实现Callable
接口
Callable
接口是Runnable
接口的增强版本,它允许线程执行完成后返回一个结果,并且可以抛出异常。Callable
接口通常与Future
和ExecutorService
一起使用,适用于需要线程返回结果的场景。
实现步骤
-
定义一个类实现
Callable
接口。 -
实现
call()
方法:在call()
方法中定义线程的任务,并返回一个结果。 -
使用
ExecutorService
提交任务:通过ExecutorService
的submit()
方法提交Callable
任务。 -
获取结果:通过
Future
对象获取线程的执行结果。
代码演示
import java.util.concurrent.*;
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("线程正在运行:" + Thread.currentThread().getName());
return 42; // 返回一个结果(适用于有线程有返回值的情况)
}
}
public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(new MyCallable()); // 提交Callable任务
Integer result = future.get(); // 获取线程的执行结果
System.out.println("线程返回的结果:" + result);
executor.shutdown(); // 关闭线程池
}
}
注:
Thread.currentThread().getName()
语句会输出当前线程的名称;由于使用的是单线程线程池,默认的线程名称格式为pool-X-thread-Y
,这里X
表示线程池的编号(通常从 1 开始),Y
表示线程的编号(在单线程线程池中通常为 1),所以输出为pool-1-thread-1
。【注意:这里返回格式和是否使用线程池创建或是直接使用 Thread 类创建有关。】
得到结果:
优点
-
返回结果:线程执行完成后可以返回一个结果。
-
异常处理:
call()
方法可以抛出异常,便于处理线程中的错误。 -
线程池支持:通常与
ExecutorService
结合使用,可以更好地管理线程资源。
缺点
-
复杂度较高:需要使用
ExecutorService
和Future
来管理线程和结果,代码相对复杂。 -
适用场景有限:适用于需要线程返回结果的场景,对于简单的线程任务可能过于复杂。
总结
方式 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
继承Thread 类 | 实现简单,直接使用Thread 类 | 单继承限制,功能有限 | 简单的线程任务,不需要返回结果 |
实现Runnable 接口 | 避免单继承限制,更好的扩展性 | 需要额外创建Thread 对象 | 大多数并发任务,不需要返回结果 |
实现Callable 接口 | 支持返回结果和异常处理,与线程池结合使用 | 代码复杂,需要使用ExecutorService 和Future | 需要线程返回结果的复杂任务 |
二、线程的执行原理
线程的并发执行通过多个线程不断的切换CPU的资源,这个速度非常快,我们感知不到,我们能感知到的就是三个线程在并发的执行。
如下,CPU在三个线程中快速切换。
三、线程生命周期
1.新建: 线程被new出来
2.准备就绪:线程具有执行的资格,即线程调用了start(),没有执行的权利
3.运行:具备执行的资格和具备执行的权利(获取到CPU)
4.阻塞:没有执行的资格和执行权利
5.销毁: 线程的对象变成垃圾,释放资源。
四、并发
示例
互联网的项目中存在着大量的并发的案例,如卖火车票,电商网站。
范例:火车站有100张票,4个窗口同时买票。
分析:4个窗口是4个线程同时在运行,100票是4个线程的共享资源。
锁
锁的概念在并发编程中应用极为广泛,是解决多线程并发访问共享资源时数据不一致和竞态条件等问题的关键技术。
语法:
synchronized(锁对象){
//操作共享资源的代码
}
场景示例
此时的厕所作为共享资源就会被上锁。
同步
同步代码加在什么地方?
1.代码被多个线程访问
2.代码中有共享的数据
3.共享数据被多条语句操作。
同步处理(解决示例中售票问题)
1.继承Thread类创建线程
public class SaleTicketThread extends Thread {
private String name;
/**
* 定义共享的数据100张票
*/
static int tickets = 100; //注意这里需要设置static
//创建一个锁对象,这个对象是多个线程对象共享的数据
static Object obj = new Object();
public SaleTicketThread(String name) {
this.name = name;
}
@Override
public void run() {
//卖票是持续的
while (true){
synchronized (obj){
if(tickets > 0){
System.out.println(name+"卖出座位是"+(tickets--)+"号");
}else{
break;
}
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(name+"卖票结束");
}
}
TestMain测试:
public class ThreadTest {
public static void main(String[] args) {
SaleTicketThread t1 = new SaleTicketThread("窗口1");
SaleTicketThread t2 = new SaleTicketThread("窗口2");
SaleTicketThread t3 = new SaleTicketThread("窗口3");
SaleTicketThread t4 = new SaleTicketThread("窗口4");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
2.实现Runnable接口创建线程
public class SaleTicket implements Runnable {
/**
* 多个线程共享的100张票
*/
int tickets = 100; //注意实现Runnable接口实现同步不用再对共享资源设置static(因为Main中只需要创建一个买票的类)
//创建一个锁对象,这个对象是多个线程对象共享的数据
Object obj = new Object();
@Override
public void run() {
//卖票是持续的
while (true){
synchronized (obj){
if(tickets > 0){
System.out.println(Thread.currentThread().getName()+"卖出座位是"+(tickets--)+"号");
}else{
break;
}
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"卖票结束");
}
}
TestMain测试:
public class ThreadTest {
public static void main(String[] args) {
//创建一个卖票的对象
SaleTicket st = new SaleTicket();
Thread t1 = new Thread(st, "窗口1");
Thread t2 = new Thread(st, "窗口2");
Thread t3 = new Thread(st, "窗口3");
Thread t4 = new Thread(st, "窗口4");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
Synchronized如果放在对象方法上
在 Java 中,当 synchronized
关键字放在对象方法上时,该方法会成为同步方法,这意味着同一时间只能有一个线程访问该方法 。
Synchronized如果在类方法上那么锁对象就是类的类对象。
休眠(从这里开始,后面的先了解,后续并发中再详细介绍)
在同步代码块中休眠,线程不会释放锁且不会让出CPU,可能导致其他线程等待锁而阻塞,降低并发效率;而在非同步代码块中休眠,线程仅暂停执行,不会影响其他线程,但也不会让出CPU资源。
线程间的通讯
生产者生成水果,如果水果没有被买走那么就不生产处于等待状态,如果水果被消费者买走这时候消费者会通知生产者告诉他我们已经把水果买走了请生产,消费者同理,如果水果已经生产出来那么就买走,买走之后再通知生产者水果已经没了请生产。
注意:
1.线程间的通信共享数据一定要有同步代码块synchronized
2.一定要有wait和notify,而且二者一定是成对出现。
3.生产者和消费者的线程实现一定是在while(true)里面。
线程的优先级
我们可以通过public final void setPriority(int newPriority)
来设置线程的优先级,但是优先级并不是绝对的,只是先对来说比其他的线程得到CPU的资源机会多一些。(当然这个优先级也只是相对的优先级,毕竟在时间片轮转中总会切走CPU的)
加入线程
join线程会抢先拿到CPU来执行线程,然后其他的线程再来执行
加入线程必须要在先执行的线程的start下面来执行。
让出线程
当前的线程从运行阶段回到就绪阶段,目的是把CPU的资源让给其他的线程。
守护线程
守护线程会随着主线程的结束而结束。
死锁
单例&多例
new出的对象都对应着不同的地址,这就叫多例模式;
单例模式:控制new出的对象是一个。
单例模式的好处:
- 在单例模式中,活动的单例只有一个实例,如果单例类的所有实例化得到的都是相同的一个实例,这样就防止了其它对象对自己的实例化,确保所有的对象都能访问一个实例。
- 单例模式具有一定的伸缩性,类自己来控制实例化进程,类就在改变实例化进程上具有相应的伸缩性。
- 提供了对唯一实例的受控访问。
- 由于在系统内存中只存在一个对象,因此可以节约系统资源,当需要频繁创建和销毁对象时单例模式无疑可以提高系统性能。
- 允许可变数目的实例。
- 避免了对共享资源的多重占用。
wait()&
sleep()&
notify()&
notifyAll()
1. 方法所属
-
wait()
、notify()
、notifyAll()
:这些方法属于Object
类,是所有Java对象的内置方法。 -
sleep()
:属于Thread
类,用于线程控制。
2. 方法的作用
(1) wait()
-
作用:使当前线程进入等待状态,并释放当前对象的锁。线程会暂停执行,直到被其他线程唤醒(通过
notify()
或notifyAll()
)。 -
使用场景:常用于线程间的协作,例如生产者-消费者模型中,消费者线程等待生产者线程的通知。
-
示例:
synchronized (obj) { obj.wait(); // 当前线程等待,释放obj的锁 }
-
注意:只能在同步代码块或同步方法中使用,否则会抛出
IllegalMonitorStateException
。
(2) notify()
-
作用:唤醒一个正在等待当前对象锁的线程(随机选择一个线程)。
-
使用场景:用于通知等待的线程继续执行,例如生产者线程通知消费者线程。
-
示例:
synchronized (obj) { obj.notify(); // 唤醒一个等待obj锁的线程 }
-
注意:只能在同步代码块或同步方法中使用。
(3) notifyAll()
-
作用:唤醒所有正在等待当前对象锁的线程。
-
使用场景:当多个线程等待同一个对象锁时,需要唤醒所有线程,例如在某些复杂的线程协作场景中。
-
示例:
synchronized (obj) { obj.notifyAll(); // 唤醒所有等待obj锁的线程 }
-
注意:只能在同步代码块或同步方法中使用。
(4) sleep()
-
作用:使当前线程暂停执行指定的时间,不释放锁。
-
使用场景:用于控制线程的执行节奏,例如在轮询任务中避免频繁占用CPU。
-
示例:
try { Thread.sleep(1000); // 当前线程暂停1秒 } catch (InterruptedException e) { e.printStackTrace(); }
-
注意:
-
sleep()
不会释放锁。 -
需要捕获
InterruptedException
异常。
-
总结:
方法 | 所属类 | 作用 | 是否释放锁 | 是否需要捕获异常 | 使用场景 |
---|---|---|---|---|---|
wait() | Object | 使线程等待并释放锁 | 是 | 否 | 线程协作,同步代码块中 |
notify() | Object | 唤醒一个等待的线程 | 否 | 否 | 线程协作,同步代码块中 |
notifyAll() | Object | 唤醒所有等待的线程 | 否 | 否 | 线程协作,同步代码块中 |
sleep() | Thread | 暂停线程,不释放锁 | 否 | 是 | 控制线程节奏,任何地方 |