目录
一、什么是进程?
程序(Program)是为实现特定目标或解决特定问题而用计算机语言(比如Java语言)编写的命令序列的集合。
进程(process)指一个程序的一次执行过程。
Java中的进程,听起来很宽泛的一个概念,及我们俗称的进度,Java中,程序无非运行和停止两种状态,进程即是我们程序的运行状。想像这样一个场景:我是一个公司职员,我的领导给我安排了一项事务,他会说:“你先去.......,然后.........,在做........的时候把.......给完成............”,这一系列的基本口令构成了这一项事务,我开始着手,一段时间后,领导会来问:“事情进行到哪里了?”,“进行到哪里”提问的对象即为“进程”,在整个事务未完成的情况下,可以理解为进展。事务完成,则进程结束。
二、什么是线程?
线程(thread)又称为轻量级进程,线程是一个程序中实现单一功能的一个指令序列,是一个程序的单个执行流,存在于进程中,是一个进程的一部分。
线程即口令,即上面场景中领导给我的一系列口令。
三、线程与进程的关系
一个进程可以包含多个线程,而一个线程必须在一个进程之内运行;同一进程中的多个线程之间采用抢占式独立运行;进程结束,则线程跟着结束,如果一个进程没有可执行的线程,进程也结束;
回到上面场景中,领导交给我的事务,最起码有一条口令,否则领导的这个事务就不会产生;而我在执行事务中的这些口令时提高效率,一定不会一条一条执行,肯定是动态的,随机应变的执行,万一执行某条口令时出了问题,不能影响领导交给我的这个事务的完成,所以要暂时切换到另一条口令;但万一领导告诉我,这个是务因为某些原因要停止,那么我就要停止所有口令的执行;如果领导交给我的事务中所有口令我都执行完了,那么好,这个事务我完成了,也就停止了。
四、Java中多线程的实现
创建线程:
Java中Thread类即为线程,想要创建线程有两种方式。
(1)继承java.lang.Thread类,重写run方法;
class CountThread extends Thread {//线程1
@Override
public void run() {
for (int i = 0; i < 15; i++) {
System.out.println("计数1: " + i);//这里我做一个简单的循环输出
}
}
}
(2)实现java.lang.Runnable接口,实现run抽象方法。
class TimeThread implements Runnable {//线程2
@Override
public void run() {
for (int i = 0; i < 15; i++) {
System.out.println("计数2: " + i);
}
}
}
开启线程:
在主类中开启线程:
package Mars;
public class Test {
public static void main(String[] args) {
new CountThread().start();//继承自Thread类的线程类对象可通过直接调用start()方法来开启线程
new Thread(new TimeThread()).start();//Runnable接口的实现类在开启线程时要通过Thread类有参构造方法创建Thread类对象,实现类创建的对象作为参数传入
}
}
运行:
由于输出数据较少,所以这里无法观察到,线程之间的抢占式的运行模式,不妨给两个线程的每次输出之间设置时间间隔:
修改线程1:
class CountThread extends Thread {//线程1
@Override
public void run() {
for (int i = 0; i < 15; i++) {
try {
sleep(1000);//sleep()是Thread类中的静态方法,所以子类继承后可以直接调用,会使得线程的运行进入暂时地等待的状态,用参数设置等待时间,这里设置为1000毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("计数1: " + i);
}
}
}
修改线程2:
class TimeThread implements Runnable {//线程2
@Override
public void run() {
for (int i = 0; i < 15; i++) {
try {
Thread.sleep(1000);//该类不是Thread类的子类,所以需要用Thread类名调用
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("计数2: " + i);
}
}
}
运行:
由图可知,在进程开始时,线程1先抢占内存,先进入休眠,先输出,所以穿插输出且每次都为线程1先输出。
线程生命周期:
因为线程是一个一个独立的口令,所以在程序进程中也有一定生命周期:
Java中,线程有5种不同状态,分别是:新建(New)、就绪(Runable)、运行(Running)、阻塞(Blocked)和死亡(Dead)。
当我们创建的线程执行start()方法时,实际上线程只是进入了就绪状态,随时等待运行,因为前面说到,线程运行的方式抢占式运行,所以一旦有线程先抢占CPU开始运行,那么其他线程都要进入就绪状态。
阻塞也会使得一个线程进入就绪状态,示例中的sleep()方法就是一个阻塞线程的方法。即使该线程已率先抢占CPU运行,一旦被阻塞,其他线程就直接抢占CPU使用权。所以,在死亡之前,线程始终在阻塞,运行,就绪三个状态之间转换。所以,当start()方法执行时,线程只是进入了就绪状态,运行与否受到是否得到CPU的使用权来决定。
在进行下面的叙述之前,先分析一下上方整个代码的执行过程:
package Mars;
public class Test {
public static void main(String[] args) {
new CountThread().start();// 继承自Thread类的线程类对象可通过直接调用start()方法来开启线程
new Thread(new TimeThread()).start();// Runnable接口的实现类在开启线程时要通过Thread类有参构造方法创建Thread类对象,实现类创建的对象作为参数传入
}
}
class CountThread extends Thread {// 线程1
@Override
public void run() {
for (int i = 0; i < 15; i++) {
try {
sleep(1000);// sleep()是Thread类中的静态方法,所以子类继承后可以直接调用,会使得线程的运行进入暂时地等待的状态,用参数设置等待时间,这里设置为1000毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("计数1: " + i);
}
}
}
class TimeThread implements Runnable {// 线程2
@Override
public void run() {
for (int i = 0; i < 15; i++) {
try {
Thread.sleep(1000);// 该类不是Thread类的子类,所以需要用Thread类名调用
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("计数2: " + i);
}
}
}
首先,在我们没创建新线程之前,整个程序的运作就只有以main方法为参照的主线程,之所以称之为主线程,是因为main方法在Java程序中是执行标杆。那么我们退一步看,当我们新创建了两个线程后,实际上这个类,每个类都只是一个线程而已,都需要抢占CPU使用权。我管这叫线程平等性,这些享有同样的权利,即使是main方法也一样。 这个观点在我的下篇博客中显得十分重要。
上方代码中main方法比较简单,只是开启了两个线程,这两条语句执行后,主线程也就终止了,但是,其他两个线程还没有终止,这就体现了线程平等性。而当我们某个线程出现运行时异常其他线程也不受影响,这是线程的各自独立性。
线程池:
我们知道,计算机的运行内存是有限的,假设我们我们在一个程序进程中一次开启了多个进程,是相当占用运行内存的,势必会为计算机带来负担。这时我们就引入了线程池的概念,为程序指定线程数,按使用为程序分配线程。
在JDK5之前,必须手动才能实现线程池,从JDK5开始新增了一个Executors工厂类,通过该工厂类可以实现线程池:
public static ExecutorService newFixedThreadPool(int nThreads):
创建一个可重用的、具有固定线程数的线程池;
package Mars;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(2);
pool.submit(() -> {// 线程1
for (int i = 0; i < 15; i++) {
try {
Thread.sleep(1000);// 该类不是Thread类的子类,所以需要用Thread类名调用
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("计数1: " + i);
}
});
pool.submit(() -> {// 线程2
for (int i = 0; i < 15; i++) {
try {
Thread.sleep(1000);// 该类不是Thread类的子类,所以需要用Thread类名调用
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("计数2: " + i);
}
});
pool.submit(() -> {// 线程3,因为线程池线程数有限,所以要等线程1,2结束,线程池才会给线程3分配线程
for (int i = 0; i < 15; i++) {
try {
Thread.sleep(1000);// 该类不是Thread类的子类,所以需要用Thread类名调用
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("计数3: " + i);
}
});
}
}