多线程基础
多线程基础
交替任务
现代操作系统(如Windows、macOS、Linux)都可以执行多任务。多任务就是指系统同时运行多个任务,例如:在系统上同时打开浏览器、音乐播放器、视频播放器等就是多任务。
cpu执行代码是一条一条顺序执行的,事实上,即使是单核cpu,也可以同时运行多个任务,因为操作系统执行多任务实际上就是让cpu对多个任务轮流交替执行。
举个栗子:假如我们有语文、数学、英语三门作业要完成,每个作业需要30分钟的时间。把这3门作业看作3个任务,我们可以先做1分钟语文,再做1分钟数学,再做1分钟英语。这样轮流做下去,在现实世界中看起来也许速度很慢,但是把这样的模式迁移到计算机世界,在cpu的强大处理能力之下,做作业的速度就会看起来很快,就像是同时在做3门作业。
类似的,操作系统轮流让多个任务交替执行,举个栗子:让浏览器执行0.001秒,让qq执行0.001秒,再让音乐播放器执行0.001秒,在人看起来,cpu就像是在同时执行多个任务。
由于任务数往往是多于cpu核数的,所以即使是多核cpu也是任务交替执行的。
进程
在计算机中,一个任务就是一个进程,浏览器就是一个进程,视频播放器是另一个进程,类似的,音乐播放器和Word都是进程。在系统的任务管理器中,我们可以看到当前系统正在进行的进程有哪些。
某些进程内部还需要执行多个子任务。例如,在使用word时,word可以一边打字,一边进行拼写检查,同时也可在后台进行打印任务,子任务称为线程
进程和线程的关系就是:一个进程可以包含一个或多个线程,但至少会有一个线程
操作系统调度的最小任务单位其实不是进程,而是线程。常用的Windows、Linux等操作系统都采用抢占式多任务,如何调度进程完成有操作系统决定,程序自己不能决定什么时候执行以及执行多长时间。
因为同一个应用程序,既可以有多个进程,也可以有多个线程,因此,实现多任务的方法,有以下几种:
多进程模式(每个进程只有一个线程):
┌──────────┐ ┌──────────┐ ┌──────────┐
│Process │ │Process │ │Process │
│┌────────┐│ │┌────────┐│ │┌────────┐│
││ Thread ││ ││ Thread ││ ││ Thread ││
│└────────┘│ │└────────┘│ │└────────┘│
└──────────┘ └──────────┘ └──────────┘
多线程模式(一个进程有多个线程):
┌────────────────────┐
│Process │
│┌────────┐┌────────┐│
││ Thread ││ Thread ││
│└────────┘└────────┘│
│┌────────┐┌────────┐│
││ Thread ││ Thread ││
│└────────┘└────────┘│
└────────────────────┘
多进程+多线程模式(复杂度最高):
┌──────────┐┌──────────┐┌──────────┐
│Process ││Process ││Process │
│┌────────┐││┌────────┐││┌────────┐│
││ Thread ││││ Thread ││││ Thread ││
│└────────┘││└────────┘││└────────┘│
│┌────────┐││┌────────┐││┌────────┐│
││ Thread ││││ Thread ││││ Thread ││
│└────────┘││└────────┘││└────────┘│
└──────────┘└──────────┘└──────────┘
进程 VS 线程
进程和线程是包含关系,但是多任务既可以由多进程实现,也可以由单进程内的多线程实现,还可以混合多进程+多线程。具体使用哪种方式,要考虑到进程和线程的特点。
和多线程相比,多进程的缺点在于:
- 创建进程比创建线程开销更大,尤其在Windows系统上;
- 进程间的通信比线程慢,因为线程之间的通信就是读写同一个变量,速度很快。
而多进程的优点在于:多进程稳定性比多线程高,因为在多进程的情况下,一个进程的崩溃不会影响其他进程,而在多线程的情况下,任何一个线程的崩溃都会直接导致整个进程崩溃。
多线程
Java语言内置了多线程支持:一个Java程序实际上就是一个JVM进程,JVM进程用一个主线程来执行main()方法,在main()方法内部也可以启动多个线程。此外,JVM还有负责垃圾回收的其他工作线程。因此对于大多数Java程序来说,就是如何使用多线程实现多任务
和单线程相比,多线程编程的特点在于:多线程经常需要读写共享数据,并且需要同步。例如,播放电影时,就必须由一个线程播放视频,另一个线程播放音频,两个线程需要协调运行,否则画面和声音就不同步。因此,多线程编程的复杂度高,调试更困难。
Java多线程编程的特点又在于:
- 多线程模型是Java程序最基本的并发模型;
- 后续读写网络、数据库、Web开发等都依赖Java多线程模型。
创建新线程
Java语言内置了多线程支持。当Java程序启动的时候,实际上是启动了一个JVM进程,然后,JVM启动主线程来执行main()方法。在main()方法中,又可以启动其他线程。
要创建一个新线程很简单,只需要实例化一个Thread
实例,然后调用它的start()
方法。
public class Main {
public static void main(String[] args) {
Thread t = new Thread();
t.start(); // 启动新线程
}
}
这个线程是极其简单的,实际上没有什么也不做就立刻结束了,实际使用中,我们希望新线程能够执行指定的代码,有以下多种方法。
-
从
Thread
派生一个自定义类,然后覆写run()
方法public class Main2 { public static void main(String[] args) { Thread thread = new MyThread(); thread.start(); } } class MyThread extends Thread { @Override public void run() { System.out.println("Start new Thread!"); } }
-
创建
Thread
实例时,传入一个Runnable
实例public class Main3 { public static void main(String[] args) { Thread thread = new Thread(new MyRunnable()); thread.start(); } } class MyRunnable implements Runnable { @Override public void run() { System.out.println("Start new Thread!"); } }
能够体现线程执行的语句和main()方法执行的区别的代码
public class Main {
public static void main(String[] args) {
System.out.println("main start...");
Thread t = new Thread() {
public void run() {
System.out.println("thread run...");
System.out.println("thread end.");
}
};
t.start();
System.out.println("main end...");
}
}
main
线程执行的代码有4行,首先打印main start
,然后创建Thread
对象,紧接着调用start()
启动新线程。当start()
方法被调用时,JVM就创建了一个新线程,我们通过实例变量t
来表示这个新线程对象,并开始执行。
接着,main
线程继续执行打印main end
语句,而t
线程在main
线程执行的同时会并发执行,打印thread run
和thread end
语句。
当run()
方法结束时,新线程就结束了。而main()
方法结束时,主线程也结束了。
我们再来看线程的执行顺序:
main
线程肯定是先打印main start
,再打印main end
;t
线程肯定是先打印thread run
,再打印thread end
。
但是,除了可以肯定,main start
会先打印外,main end
打印在thread run
之前、thread end
之后或者之间,都无法确定。因为从t
线程开始运行以后,两个线程就开始同时运行了,并且由操作系统调度,程序本身无法确定线程的调度顺序。
要模拟并发执行的效果,可以在线程中调用Thread.sleep()
,强迫当前线程暂停一段时间:
public class Main {
public static void main(String[] args) {
System.out.println("main start...");
Thread t = new Thread() {
public void run() {
System.out.println("thread run...");
try {
Thread.sleep(10);
} catch (InterruptedException e) {}
System.out.println("thread end.");
}
};
t.start();
try {
Thread.sleep(20);
} catch (InterruptedException e) {}
System.out.println("main end...");
}
}
sleep()
传入的参数是毫秒。调整暂停时间的大小,我们可以看到main
线程和t
线程执行的先后顺序。
线程的优先级
可以对线程设定优先级,设定的方法是:
Thread.setPriority(int n) // 1~10, 默认值5
优先级高的线程被操作系统调度的优先级较高,操作系统对高优先级线程可能调度更频繁,但我们决不能通过设置优先级来确保高优先级的线程一定会先执行。