一、使用多线程的背景
-
提高程序速度和响应性:许多应用程序需要同时执行多个任务,例如网络服务器,图形图像处理,模拟程序等。使用多线程可以让程序同时执行多个部分,从而显著提高程序的执行速度、响应速度。
-
充分利用 CPU:在单核 CPU 的情况下,只有一个线程能够使用 CPU 进行计算。但随着 CPU 的多核化,可以同时开启多个线程,从而充分利用 CPU 资源。
-
方便协作编程:多线程编程可以让不同的开发者同时负责程序不同部分的开发工作,从而达成快速开发的目标。
-
提供更好的用户体验:对于复杂的应用程序,使用多线程可以让用户更好的体验程序的交互响应。
总之,使用多线程能够将程序的执行与资源利用分离,使得代码更具可维护性、模块化,同时改善程序的执行效率和用户体验。
二、创建多线程的常见的几种方法(介绍两种常用的)
方式一、通过继承 Thread 类创建线程
public class Thread01 {
public static void main(String[] args) throws InterruptedException {
//创建 Cat 对象,可以当做线程使用
Cat cat = new Cat();
/*
(1)
public synchronized void start() {
start0();
}
(2)
//start0() 是本地方法,是 JVM 调用, 底层是 c/c++实现
//真正实现多线程的效果, 是 start0(), 而不是 run
private native void start0();
*/
cat.start();//启动线程-> 最终会执行 cat 的 run 方法
//cat.run();//run 方法就是一个普通的方法, 没有真正的启动一个线程,就会把 run 方法执行完毕,才向下执行
//说明: 当 main 线程启动一个子线程 Thread-0, 主线程不会阻塞, 会继续执行
//这时 主线程和子线程是交替执行.. System.out.println("主线程继续执行" + Thread.currentThread().getName());//名字 main
for(int i = 0; i < 60; i++) {
System.out.println("主线程 i=" + i);
//让主线程休眠
Thread.sleep(1000);
}}
}
//1. 当一个类继承了 Thread 类, 该类就可以当做线程使用
//2. 我们会重写 run 方法,写上自己的业务代码
//3. run Thread 类 实现了 Runnable 接口的 run 方法
/*
@Override
public void run() {
if (target != null) {
target.run();
}
}
*/
class Cat extends Thread {
int times = 0;
@Override
public void run() {//重写 run 方法,写上自己的业务逻辑
//当 times 到 80, 退出 while, 这时线程也就退出.. }
do {
//该线程每隔 1 秒。在控制台输出 “喵喵, 我是小猫咪”
System.out.println("喵喵, 我是小猫咪" + (++times) + " 线程名=" + Thread.currentThread().getName());
//让该线程休眠 1 秒 ctrl+alt+t
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} while (times != 80);
}
}
方式二、通过实现 Runnable 接口创建线程
由于Java是单继承的,某些情况下一个类可能已经继承了某个父类,这时在用继承Thread类方法来创建线程显然不可能了。此时就可以通过实现Runnable接口来创建线程。
这里实现了用Runnable接口来创建线程并且在主线程中实现了两个子线程。
public class Thread01 {
public static void main(String[] args) {
//main 线程启动两个子线程
Dog dog = new Dog();
//dog.start(); 这里不能调用 start
//创建了 Thread 对象,把 dog 对象(实现 Runnable),放入 Thread
Thread thread = new Thread(dog);
thread.start();//启动第 1 个线程
Tiger tiger = new Tiger();//实现了 Runnable
ThreadProxy threadProxy = new ThreadProxy(tiger);
threadProxy.start();//启动第 2 个线程
}
}
class Animal {
}
class Tiger extends Animal implements Runnable {
int count = 0;
@Override
public void run() { //普通方法
do {
System.out.println("老虎嗷嗷叫..hi" + (++count) +"线程名是:"+ Thread.currentThread().getName());
//休眠 1 秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} while (count != 5);
}
}
//线程代理类 , 模拟了一个极简的 Thread 类
class ThreadProxy implements Runnable {//你可以把 Proxy 类当做 ThreadProxy
private Runnable target = null;//属性,类型是 Runnable
@Override
public void run() {
if (target != null) {
target.run();//动态绑定(运行类型 Tiger)
}
}
public ThreadProxy(Runnable target) {
this.target = target;
}
public void start() {
start0();//这个方法时真正实现多线程方法
}
public void start0() {
run();
}
}
class Dog extends Animal implements Runnable { //通过实现 Runnable 接口,开发线程
int count = 0;
@Override
public void run() { //普通方法
do {
System.out.println("小狗汪汪叫..hi" + (++count) + "线程名是:"+ Thread.currentThread().getName());
//休眠 1 秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} while (count != 10);
}
}
Thread 和 Runnable 接口的区别:
1. Thread 是一个类,Runnable 是接口,在 Java 语言里面的继承特性,接口可以支持多继承,而类只能单一继承。 所以如果在已经存在继承关系的类里面要实现线程的话,只能实现 Runnable 接口。
2. Runnable 表示一个线程的顶级接口,Thread 类其实是实现了 Runnable 这个接 口,我们在使用的时候都需要实现 run 方法。
3. 站在面向对象的思想来说,Runnable 相当于一个任务,而 Thread 才是真正处理的线程,所以我们只需要用 Runnable 去定义一个具体的任务,然后交给 Thread 去处理就可以了,这样达到了松耦合的设计目的。
4. 接口表示一种规范或者标准,而实现类表示对这个规范或者标准的实现,所以站在 线程的角度来说,Thread 才是真正意义上的线程实现。 Runnable 表示线程要执行的任务,因此在线程池里面,提交一个任务传递的类型是 Runnable。
总的来说,Thread 只是实现了 Runnable 接口并做了扩展,所以这两者并没什么可比性。
三、多线程实现1到1千万的求和操作
该程序首先将1到1千万的数字平均分配给指定数量的线程进行求和,每个线程负责求和一部分数字。 每个线程求和完毕后,将结果发送到主线程中进行合并,最终得到总的求和结果。
public class SumMultiThread {
private static final int MAX_NUM = 10000000; // 求和的最大数值
private static final int NUM_THREADS = 1000; // 使用的线程数量
public static void main(String[] args) {
long start = System.currentTimeMillis();
// 创建多个求和线程,并启动它们
List<SumThread> threads = new ArrayList<>();
for (int i = 0; i < NUM_THREADS; i++) {
SumThread thread = new SumThread(i * (MAX_NUM / NUM_THREADS) + 1, (i + 1) * (MAX_NUM / NUM_THREADS));
threads.add(thread);
thread.start();
}
// 等待所有线程执行完毕
for (SumThread thread : threads) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 合并所有线程的结果
long sum = 0;
for (SumThread thread : threads) {
sum += thread.getSum();
}
long end = System.currentTimeMillis();
System.out.println("Sum of 1 to " + MAX_NUM + " is " + sum + ", runtime is " + (end - start) + "ms");
}
static class SumThread extends Thread {
private final int from;
private final int to;
private long sum;
public SumThread(int from, int to) {
this.from = from;
this.to = to;
}
@Override
public void run() {
for (int i = from; i <= to; i++) {
sum += i;
}
}
public long getSum() {
return sum;
}
}
}
四、使用多线程可能遇到的问题
由于CPU、内存、I/O 设备速度有极大差异,为了合理利用 CPU 的高性能,平衡这三者的速度差异,计算机体系结构、操作系统、编译程序都做出了升级,但也随之出现了一些问题。主要体现为:
- CPU 增加了缓存,以均衡与内存的速度差异;(导致
可见性
问题) - 操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异;(导致
原子性
问题) - 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。( 导致
有序性
问题) - 同时多线程的效率跟硬件有关,并不是开的线程越多越好。
以上一个 “1到一千万” 的求和为例,时间计算结果如下: