Java初识多线程
在我们编写代码的时候,一个基本的概念就是同时对多个任务加以控制。
比如基础章节的所有示例代码都是单线程代码,只有一个主线程。当我们主线程执行main方法中调用的方法时,主线程会切换到方法去执行方法体内部的代码,执行结束后再继续执行main方法。
当我们的程序设计得越发复杂时这样的单线程效率往往是极其低下的。所以我们要使用多线程来同时处理这些任务。
多线程是为了同步完成多项任务,是为了提高资源的使用效率,而不是提高运行的效率。
Process与Thread
Process(进程)
-
进程本身没有任何执行的含义,是一个系统资源分配的单位。
-
一个程序的运行为一个进程。
-
一个进程可以并发多个线程。
-
一个进程至少有一个线程,否则这个进程没有存在的意义。
Thread(线程)
-
包含在进程中,是进程中的实际运作单位。同时也是CPU操作调度的单位。
-
一个进程可以并发多个线程,每条执行不同的任务。
-
一个进程开始即包含一个主线程为系统的入口执行整个程序。
-
一个进程中,如果有多个线程,那么线程的运行由调度器安排,不能人为干预。
-
当多个线程对同一个资源进行操作时,会发生资源抢夺,需要加入并发控制。
-
线程的增加会影响运行效率。
比如我们使用播放器播放一个视频,执行这个播放器,就是开启了一个进程。而播放器中的各个元素,比如负责声音的是一个线程;负责画面的是一个线程;如果外挂了字幕,那么负责字幕的也是一个线程。
需要注意的是,真正的多线程指多个CPU同时处理多个任务。多线程模拟则是因为一个CPU性能过于强大,执行得太快所以给人感觉是同时执行的。
线程创建
Java提供了三种创建进程的方式:Thread类、Runnable接口、Callable接口。
Thread类
类示例代码:
//继承Thread类
public class Test extends Thread {
//重写父类的run方法
@Override
public void run() {
//线程体,线程需要执行的代码
for (int i = 0; i < 2000; i++) {
System.out.println("我是子线程" + i);
}
}
}
主程序代码:
public class App {
//主线程
public static void main(String[] args) {
//创建对象
Test test = new Test();
//执行start方法
test.start();
for (int i = 0; i < 2000; i++) {
System.out.println("我是主线程" + i);
}
}
}
-
通过继承Thread并重写run方法来使我们的类变成一个”线程类“。
-
run方法中写线程需要执行的线程体。
-
主线程通过对象继承的start方法来调用。不要直接调用run方法!
-
子线程在主线程被创建后执行的时间由CPU安排。
-
不建议使用,因为无法避免单继承的局限性。
Runnable接口
类示例代码:
//继承Runnable接口
public class Test implements Runnable {
//实现Runnable接口的run方法
@Override
public void run() {
//线程体,线程需要执行的代码
for (int i = 0; i < 200; i++) {
System.out.println("我是子线程" + i);
}
}
}
主程序代码:
public class App {
//主线程
public static void main(String[] args) {
//创建对象
Test test = new Test();
//匿名对象,将Runnable接口的实现类对象作为参数传递,执行Thread类的start方法
new Thread(test).start();
for (int i = 0; i < 2000; i++) {
System.out.println("我是主线程" + i);
}
}
}
-
通过继承Runnable接口并实现run方法来使我们的类变成一个Runnable接口的实现类。
-
run方法中写线程需要执行的线程体。
-
主线程通过将“实现类”对象作为参数传递进Thread类中匿名调用start方法。
(有点绕)
-
重点掌握使用,灵活方便,方便同一个对象被多个线程使用。
Callable接口
参考文章:https://blog.csdn.net/m0_37204491/article/details/87930790
实际某些工作中会经常使用到的创建线程模式。Callable接口创建的线程有多种运行方式,目前仅作了解最简单的使用线程池运行。
类示例代码:
import java.util.concurrent.Callable;
//继承Callable接口
public class Test_1 implements Callable {
//实现Callable接口的call方法,该方法有一个返回值。
@Override
public Boolean call() throws Exception {
//线程体,线程需要执行的代码。
for (int i = 0; i < 10; i++) {
System.out.println("我是子线程-" + i);
}
return true;
}
}
主程序代码:
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class App_1 {
public static void main(String[] args) {
//创建对象
Test_1 test_1 = new Test_1();
//声明固定大小的线程池,参数是声明的线程池数量。
ExecutorService service = Executors.newFixedThreadPool(1);
//将Callable接口的实例化类提交给线程池运行,创建Future接口的实例化对象接收返回值。
Future<Boolean> r1 = service.submit(test_1);
try {
//通过布尔类型的变量接收Future接口的方法返回值,该返回值表示线程是否正常执行
//Future接口的get方法表示线程的执行结果。会抛出两个异常,一个中断异常,一个执行异常。
Boolean rs1 = r1.get();
} catch (InterruptedException e) {
System.out.println("程序中断!!请检查!!");
} catch (ExecutionException e) {
System.out.println("执行异常!!请检查!!");
}
for (int i = 0; i < 20; i++) {
System.out.println("我是主线程--" + i);
}
//销毁当前线程池
service.shutdownNow();
}
}
对比Runnable接口的区别:
1、Callable实现线程的方法是call,Runnable实现线程的方法是run。
2、Callable的任务执行后能返回值,而Runnable没有。
3、call方法可以抛出异常,run方法不可以。
4、运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
线程并发问题
多个线程同时操作同一个对象时,线程不安全,数据将会紊乱!
类示例代码:
/**
* 使用线程模拟一个抢票系统,并打印结果。
*/
//继承Runnable接口
public class Test implements Runnable {
private int ticket = 1;
//实现Runnable接口的run方法
@Override
public void run() {
while (true) {
//当票卖完时跳出
if (ticket > 10) {
System.out.println(Thread.currentThread().getName()+"没有买到票,因为票卖完了。");
break;
}
System.out.println(Thread.currentThread().getName() + "抢到了第" + ticket++ + "张票");
}
}
}
-
Thread.currentThread().getName()
-
currentThread类的getName方法可以拿到当前线程的Name属性,也就是线程的名字。
-
主程序代码:
/**
* 使用线程模拟一个抢票系统,并打印结果。
*/
public class App {
//主线程
public static void main(String[] args) {
//创建对象
Test test = new Test();
//匿名,将Runnable接口的实现类对象作为参数传递,执行Thread类的start方法,并给每个线程手动配置一个名字
new Thread(test, "小明").start();
new Thread(test, "小张").start();
new Thread(test, "黄牛").start();
}
}
-
每个线程被创建的时候都是有名字的,如果没有手动设置则系统自动分配。
-
开启线程时可以在Runnable参数后面设置线程的名字。
-
运行结果:
以上是其中一次的模拟运行结果。可以发现,小明和黄牛都同时抢到了第1张票。这就是线程之间的并发问题,可以通过线程同步来解决这个问题。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人