1.概念
进程(Process):
进程是一个包含自身执行地址的程序,多线程使程序可以同时存在多个执行片段,这些执行片段根据不同的条件和环境同步或者异步工作,由于转换的数独很快,使人感觉上进程像是在同时运行。
现在的计算机基本上都支持多进程操作,比如使用计算机时可以变上网,边听歌。然而计算机上只有一块CPU,并不能同时运行这些进程,CPU实际上是利用不同的时间段去交替执行每个进程。
线程(Thread):
在一个进程内部可以执行多项任务,进程内部的任务被称为线程,线程是进程中的实体,一个进程可以拥有多个线程。
多线程是指在一个程序中同时执行多个线程,每个线程都有自己独立的执行路径。在多线程中,程序的执行可以同时进行多个任务,从而提高系统的资源利用率和响应性能。
在传统的单线程编程模型中,程序按照顺序执行,一次只处理一个任务。这种方式在某些情况下可能会导致效率低下或者无法满足需求。而多线程通过将任务拆分为多个子任务,并且在不同的线程上同时执行,从而实现并发处理。
对于 Java 初学者来说,多线程的很多概念听起来就很难理解。比方说:
- 进程,是对运行时程序的封装,是系统进行资源调度和分配的基本单位,实现了操作系统的并发。
- 线程,是进程的子任务,是 CPU 调度和分派的基本单位,实现了进程内部的并发。
很抽象,对不对?打个比喻,你在打一把王者(其实我不会玩哈 doge):
- 进程可以比作是你开的这一把游戏
- 线程可以比作是你所选的英雄或者是游戏中的水晶野怪等之类的。
带着这个比喻来理解进程和线程的一些关系,一个进程可以有多个线程就叫多线程。是不是感觉非常好理解了?
❤1、线程在进程下进行
(单独的英雄角色、野怪、小兵肯定不能运行)
❤2、进程之间不会相互影响,主线程结束将会导致整个进程结束
(两把游戏之间不会有联系和影响。你的水晶被推掉,你这把游戏就结束了)
❤3、不同的进程数据很难共享
(两把游戏之间很难有联系,有联系的情况比如上把的敌人这把又匹配到了)
❤4、同进程下的不同线程之间数据很容易共享
(你开的那一把游戏,你可以看到每个玩家的状态——生死,也可以看到每个玩家的出装等等)
❤5、进程使用内存地址可以限定使用量
(开的房间模式,决定了你可以设置有多少人进,当房间满了后,其他人就进不去了,除非有人退出房间,其他人才能进)
2.线程的创建
在JAVA语言中,线程也是一种对象,但是并非任何对象都可以称为线程,只有实现了Runnable接口或者继承了Thread类的对象才能成为线程。
2.1 Thread类
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + ":打了" + i + "个小兵");
}
}
public static void main(String[] args) {
//创建MyThread对象
MyThread t1=new MyThread();
MyThread t2=new MyThread();
MyThread t3=new MyThread();
//设置线程的名字
t1.setName("鲁班");
t2.setName("刘备");
t3.setName("亚瑟");
//启动线程
t1.start();
t2.start();
t3.start();
}
}
运行结果:
2.2 Runnable接口
创建一个类实现 Runnable 接口,并重写 run 方法。本质上来讲,Runnable接口是JAVA接口语言中用以实现线程的接口,任何实现线程功能的类都必须实现这个接口。
虽然可以使用继承Thread类的方式来实现线程,但是在java语言中,只能继承一个类,如果用户定义的类已经继承其他类,就无法再继承Thread类,无法使用线程,所以Runnable接口就这么诞生了,实现Runnable接口和继承Thread类具有相同的效果,通过实现这个接口就可以使用线程。Runnable接口定义了一个run()方法,在实例化一个Thread对象的时候,可以传入一个实现Runnable接口作为参数,Thead类会调用Runnable对象的run()方法,继而执行run()方法中的内容。
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {//sleep会发生异常要显示处理
Thread.sleep(20);//暂停20毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "打了:" + i + "个小兵");
}
}
public static void main(String[] args) {
//创建MyRunnable类
MyRunnable mr = new MyRunnable();
//创建Thread类的有参构造,并设置线程名
Thread t1 = new Thread(mr, "张飞");
Thread t2 = new Thread(mr, "貂蝉");
Thread t3 = new Thread(mr, "吕布");
//启动线程
t1.start();
t2.start();
t3.start();
}
}
运行结果:
2.3 实现 Callable 接口
- 实现 Callable 接口,重写 call 方法,这种方式可以通过 FutureTask 获取任务执行的返回值。
- 重写call()方法:Callable接口中的call()方法和run()方法很相似,但是call()可以返回一个值,并且可以抛出检查型异常。
- 获取结果:使用Future对象对get()方法获取callable任务的结果
public class CallerTask implements Callable<String> {
public String call() throws Exception {
return "Hello,i am running!";
}
public static void main(String[] args) {
//创建异步任务
FutureTask<String> task=new FutureTask<String>(new CallerTask());
//启动线程
new Thread(task).start();
try {
//等待执行完成,并获取返回结果
String result=task.get();
System.out.println(result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
3.关于线程的一些疑问
3.1 为什么要重写 run 方法?
这是因为默认的
run()
方法不会做任何事情。为了让线程执行一些实际的任务,我们需要提供自己的
run()
方法实现,这就需要重写run()
方法public class MyThread extends Thread { public void run() { System.out.println("MyThread running"); } }
在这个例子中,我们重写了
run()
方法,使其打印出一条消息。当我们创建并启动这个线程的实例时,它就会打印出这条消息。
2.1 run 方法和 start 方法有什么区别?
run()
:封装线程执行的代码,直接调用相当于调用普通方法。start()
:启动线程,然后由 JVM 调用此线程的run()
方法。
3.1 通过继承 Thread 的方法和实现 Runnable 接口的方式创建多线程,哪个好?
实现 Runable 接口好,原因有两个:
- ♠①、避免了 Java 单继承的局限性,Java 不支持多重继承,因此如果我们的类已经继承了另一个类,就不能再继承 Thread 类了。
- ♠②、适合多个相同的程序代码去处理同一资源的情况,把线程、代码和数据有效的分离,更符合面向对象的设计思想。Callable 接口与 Runnable 非常相似,但可以返回一个结果。
4.控制线程的其他方法
1.sleep
sleep方法用于暂停当前正在执行的线程的执行指定时间(以毫秒为单位)。sleep方法定义在Thread类中,其主要的用途是让当前线程暂停,让其他的线程有机会运行。需要注意的是,sleep 的时候要对异常进行处理。
public class SleepExample {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
try {
System.out.println("线程开始执行,即将进入睡眠状态...");
Thread.sleep(3000); // 暂停3秒
System.out.println("线程从睡眠状态唤醒");
} catch (InterruptedException e) {
System.out.println("线程被中断:" + e.getMessage());
Thread.currentThread().interrupt(); // 重置中断状态
}
});
thread.start();
}
}
2.join
等待这个线程执行完才会轮到后续线程得到 cpu 的执行权,使用这个也要捕获异常。
//创建MyRunnable类
MyRunnable mr = new MyRunnable();
//创建Thread类的有参构造,并设置线程名
Thread t1 = new Thread(mr, "张飞");
Thread t2 = new Thread(mr, "貂蝉");
Thread t3 = new Thread(mr, "吕布");
//启动线程
t1.start();
try {
t1.join(); //等待t1执行完才会轮到t2,t3抢
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
t3.start();
3.setDaemon()
这个是用于将线程设置为守护线程,属于服务类的线程,用于为用户线程提供服务。当所有的用户线程都结束时,守护线程会自动结束。
public class CallerTask{
public static void main(String[] args) {
Thread daemonThread =new Thread(()->{
System.out.println("守护线程启动");
try{
//模拟长时间的运行任务
while (true){
Thread.sleep(1000);
System.out.println("守护线程正在进行");
}
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("守护线程被中断,即将结束");
}
});
daemonThread.setDaemon(true);
daemonThread.start();
//主线程运行一小段时间后结束
try{
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程结束");
}
}
在这个示例中,主线程启动一个守护线程,并将其设置为守护状态。主线程在运行3秒后结束,此时所有的用户线程都已结束,守护线程中断。
4.yield()
这个是Thread中的静态方法,它可以使当前正在执行的线程让出对CPU的使用权,从而使得其它同优先级的线程有机会被CPU调度执行,这个方法主要用于线程之间的协作,尤其是在需要让出CPU一遍其他线程可以运行时。
class YieldExample {
public static void main(String[] args) {
Thread thread1 = new Thread(YieldExample::printNumbers, "刘备");
Thread thread2 = new Thread(YieldExample::printNumbers, "关羽");
thread1.start();
thread2.start();
}
private static void printNumbers() {
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
// 当 i 是偶数时,当前线程暂停执行
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + " 让出控制权...");
Thread.yield();
}
}
}
}
运行结果:
从这个结果可以看得出来,即便有时候让出了控制权,其他线程也不一定会执行。
5.线程的生命周期
- 第一是创建状态。在生成线程对象,并没有调用该对象的 start 方法,这是线程处于创建状态。
- 第二是就绪状态。当调用了线程对象的 start 方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。
- 第三是运行状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行 run 函数当中的代码。
- 第四是阻塞状态。线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。sleep,suspend,wait 等方法都可以导致线程阻塞。
- 第五是死亡状态。如果一个线程的 run 方法执行结束或者调用 stop 方法后,该线程就会死亡。对于已经死亡的线程,无法再使用 start 方法令其进入就绪。
6.Callable,Future和FutureTask
我们讲述了创建线程的 3 种方式,一种是直接继承 Thread,一种是实现 Runnable 接口,另外一种是实现 Callable 接口。
前 2 种方式都有一个缺陷:在执行完任务之后无法获取执行结果。
如果需要获取执行结果,就必须通过共享变量或者线程通信的方式来达到目的,这样使用起来就比较麻烦。
Java 1.5 提供了 Callable、Future、FutureTask,它们可以在任务执行完后得到执行结果,今天我们就来详细的了解一下。
Runnable接口:
- 没有返回值
- 没有异常抛出的限制
- 可以通过Thread类的run()方法执行
Callable接口:
- 有返回值,返回值类型为object
- 可以抛出异常
- 必须通过ExecutorService来执行,并且返回一个Future对象
// Runnable接口实现
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Runnable is running");
}
}
// Callable接口实现
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("Callable is running");
return 123;
}
}
// FutureTask使用
public class FutureTaskExample {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executor = Executors.newSingleThreadExecutor();
// 使用Runnable
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
// 使用Callable和FutureTask
MyCallable myCallable = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
executor.execute(futureTask);
// 获取Callable任务的结果
Integer result = futureTask.get(); // 阻塞直到任务完成
System.out.println("Callable returned: " + result);
executor.shutdown();
}
}
那怎么使用 Callable 呢?
一般会配合 ExecutorServiceopen in new window(后面在讲线程池的时候会细讲,这里记住就行)来使用。
ExecutorService 是一个接口,位于 java.util.concurrent
包下,它是 Java 线程池框架的核心接口,用来异步执行任务。它提供了一些关键方法用来进行线程管理。
异步计算Future接口:
Future接口,它代表了异步的计算结果。当你将一个任务提交给ExectorService执行时,它返回一个Future对象,这个Future对象运行你查询任务的状态,获取任务结果,取消任务等等。
以下是Future接口的一些关键特性和方法:
查询任务状态:
- isDone():检查任务是否完成
- isCanncelled():检查任务是否被取消
获取结果:
- get():获取任务的结果,如果任务尚未完成,则次方法会阻塞
- get(long timeout,TimeUnit unit):获取任务的结果,如果任务在指定的时间内未完成,则会抛出TimeoutException
取消任务:
cancle(boolean mayInterruptIfRunning):尝试取消任务。如果任务尚未开始,或者已经开始但未完成,则可以取消任务。参数mayInterruptIfRunning分running决定是否运行中断正在运行的任务。
获取任务是否被取消:
ifCancelled():返回任务是否被取消。
获取任务是否成功完成:
isDone():返回任务是否成功完成
也就是说 Future 提供了三种功能:
- 1)判断任务是否完成;
- 2)能够中断任务;
- 3)能够获取任务执行结果。
由于 Future 只是一个接口,如果直接 new 的话,编译器是会有一个 ⚠️ 警告的,它会提醒我们最好使用 FutureTask。
实际上,FutureTask 是 Future 接口的一个唯一实现类,我们在前面的例子中 executorService.submit()
返回的就是 FutureTask,通过 debug 模式可以观察到。
异步计算结果 FutureTask 实现类
我们来看一下 FutureTask 的实现:
public class FutureTask<V> implements RunnableFuture<V>
FutureTask 类实现了 RunnableFuture 接口,我们看一下 RunnableFuture 接口的实现:
public interface RunnableFuture<V> extends Runnable, Future<V> {
void run();
}
可以看出 RunnableFuture 继承了 Runnable 接口和 Future 接口,而 FutureTask 实现了 RunnableFuture 接口。所以它既可以作为 Runnable 被线程执行,又可以作为 Future 得到 Callable 的返回值。
FutureTask 提供了 2 个构造器:
public FutureTask(Callable<V> callable) {
}
public FutureTask(Runnable runnable, V result) {
}