多线程是什么?
- 多线程也是面试高频考点之一,了解多线程对我们的日常编程也有很大的帮助,在学习过程中,也能让我们理解起来更轻松,先来看看进程和线程是什么?
①进程:操作系统资源分配和调度的基本单位。也就是我们电脑中运行的一个个独立的任务。
②线程:任务调度和执行的最小单位,线程在进程中是独立的,并发的执行流,线程也被称为轻量级进程。
- 线程与进程相比,会具有以下优势:
①系统创建进程时需要为其分配独立的内存单元以及分配大量的相关资源,相比而言,线程的创建简单得多,因此多线程的多任务并发比多进程的执行效率高。
②进程之间共享内存很麻烦,线程之间共享内存则非常容易。
- 那么多线程怎么理解呢?
多线程扩展了进程的概念,使得同一个进程可以同时并发处理多个任务,就像一个大哥将一件很麻烦的工作交给众多小弟去做一样,大家各做各的,最终将这件事完成,大哥露出了欣慰的笑容,这里的并发有必要和并行区分一下。
①并行:在同一时刻,有多条指令在多个CPU上同时执行,只有多处理器的系统中才存在并行。
②并发:同一时刻只有一条指令执行,多个进程指令被迅速切换,从而达到多个进程同时执行的效果,表面上看像是在同时执行一样,其实是因为你根本感知不到转换。
- 注意:
多线程的存在,不是为了提高程序的执行速度,而是为了提高应用程序的使用率,程序的执行都是在抢占CPU资源,多个进程都在抢资源,那么某个进程执行路径比较多的话(也就是有多线程),那么它抢到CPU资源的成功率就高一点。
多线程的优缺点
- 优点:
(1)多线程技术使程序的响应速度更快 ,因为用户界面可以在进行其它工作的同时一直处于活动状态;
(2)占用大量处理时间的任务使用多线程可以提高CPU利用率,即占用大量处理时间的任务可以定期将处理器时间让给其它任务;
(3)多线程可以分别设置优先级以优化性能。
- 缺点:
(1)等候使用共享资源时造成程序的运行速度变慢。这些共享资源主要是独占性的资源 ,如打印机等。
(2)对线程进行管理要求额外的 CPU开销,线程的使用会给系统带来上下文切换的额外负担。如果线程很多的话,各线程之间还要避免发生冲突,也就是多线程的安全问题、
(3)线程的死锁。即对共享资源加锁实现同步的过程中可能会死锁。
(4)对公有变量的同时读或写,可能造成脏读等;
多线程的应用场景
1、常见的浏览器、Web服务(现在写的web是中间件帮你完成了线程的控制),web处理请求,各种专用服务器(如游戏服务器)。
2、servlet多线程。
3、FTP下载,多线程操作文件。
4、数据库用到的多线程。
5、分布式计算。
6、tomcat内部采用多线程,上百个客户端访问同一个WEB应用,tomcat接入后就是把后续的处理扔给一个新的线程来处理,这个新的线程最后调用我们的servlet程序,比如doGet或者dpPost方法。
7、后台任务:如定时向大量(100W以上)的用户发送邮件;定期更新配置文件、任务调度(如quartz),一些监控用于定期信息采集。
8、自动作业处理:比如定期备份日志、定期备份数据库。
9、异步处理:如发微博、记录日志。
10、页面异步处理:比如大批量数据的核对工作(有10万个手机号码,核对哪些是已有用户)。
11、数据库的数据分析(待分析的数据太多),数据迁移。
12、多步骤的任务处理,可根据步骤特征选用不同个数和特征的线程来协作处理,多任务的分割,由一个主线程分割给多个线程完成。
13、desktop应用开发,一个费时的计算开个线程,前台加个进度条显示。
14、swing编程。
线程分析
- 线程的生命周期:新建,就绪,运行,阻塞,死亡。
- 线程的基本类型
①用户级线程: 管理过程全部由用户程序完成,操作系统内核只对进程进行管理。
②系统级线程: 由操作系统内核进行管理,操作系统内核给应用程序提供相应的系统调用和应用程序接口API,以此,用户便可以创建,执行,撤销线程。
实现多线程的几种方法
- 在Java中,实现多线程主要有四种方式。如下:
继承Thread类
public class ThreadDemo01 extends Thread {
public static void main(String[] args) {
ThreadDemo01 t1=new ThreadDemo01();
//对象调用Start方法开启线程
t1.start();
}
//重写run方法
public void run() {
while (true) {
System.out.println(this.getName() + " running...");
try {
Thread.sleep(5000); // 休息1000ms
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 查看源码发现,Thread类也实现了Runnable接口。这就是典型的解耦,Runnable是一个简单的线程任务,里面只有一个run方法,而thread类是线程控制,它包括很多对线程加以操作的方法。
实现Runnable接口
- 使用接口的方式可以让我们的程序降低耦合度。Runnable接口中仅仅定义了一个run方法。
- 其实Runnable就是一个线程任务,线程任务和线程的控制分离,这也就是上面所说的解耦。我们要实现一个线程,可以借助Thread类,Thread类要执行的任务就可以由实现了Runnable接口的类来处理。 这就是Runnable的精髓之所在!
public class ThreadDemo02 implements Runnable {
@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + " running...");
try {
Thread.sleep(5000); // 休息1000ms
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
//Runnable接口的类,初始化一个实例对象
ThreadDemo02 test =new ThreadDemo02();
//将这个实例对象传入Thread中
Thread t2 = new Thread(test);
t2.start();
}
}
实现Callable接口
- 在Callable的源码中,只发现了一个call方法。
public class CallableImpl implements Callable<String> {
public CallableImpl(String acceptStr) {
this.acceptStr = acceptStr;
}
private String acceptStr;
@Override
public String call() throws Exception {
// 任务阻塞 1 秒
Thread.sleep(1000);
return this.acceptStr + " append some chars and return it!";
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<String> callable = new CallableImpl("my callable test!");
FutureTask<String> task = new FutureTask<>(callable);
long beginTime = System.currentTimeMillis();
// 创建线程
new Thread(task).start();
// 调用get()阻塞主线程,反之,线程不会阻塞
String result = task.get();
long endTime = System.currentTimeMillis();
System.out.println("hello : " + result);
System.out.println("cast : " + (endTime - beginTime) / 1000 + " second!");
}
}
- 实现Runnable和实现Callable接口的异同点
相同点: 都用来编写多线程程序,都调用Thread.start()启动线程。
不同点:
①实现Callable接口的任务线程能返回执行结果;而实现Runnable接口的任务线程不能返回结果。
②Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛。
线程池创建线程
注意/总结
-
run()
和start()
的区别:
①run()
仅仅是封装被线程执行的代码,直接调用是普通方法。
②start()
先启动线程,再由jvm
去调用线程的run()
方法执行任务。 -
JVM的启动也是多线程的,不仅仅是main主线程启动了,还有垃圾回收等等线程也启动了。
-
一般使用实现Runnable接口来创建线程。这样做可以避免java中单继承的限制,同时也可以将运行任务和控制机制解耦。