进程和线程的概述
- 进程就是应用程序在内存中分配的空间,也可理解为一个正在执行中的程序。每一个进程执行都有一个执行顺序,该顺序就是一个执行路径或者叫一个控制单元。
- 线程就是进程中负责程序执行的执行单元,也可理解为进程中的一个独立的控制单元。线程在控制着进程的执行。
多线程
多线程的概述
一个进程中至少有一个线程在负责该进程的运行。如果一个进程中出现了多个线程,就称该程序为多线程程序。
多线程解决的问题
多线程这门技术的出现解决了多部分代码同时执行的需求,这样做的好处就是可以提高用户的体验效果。
这里有一个疑问——多线程真的能提高效率吗?显然不是,反倒容易死机,但可合理的使用CPU资源。
JVM中的多线程与垃圾回收
多线程的运行是根据CPU的切换完成的,怎么切换CPU说了算,所以多线程运行有一个随机性(CPU的快速切换造成的)。
本节我首先给出结论——JVM中的多线程至少有两个线程:
- 一个是负责自定义代码运行的,这个从main方法开始执行的线程称之为主线程。
- 一个是负责垃圾回收的。
然后我通过一个简单的案例来演示JVM中的多线程。例如,有如下实验代码:
class Demo
{
// 定义垃圾回收方法
public void finalize()
{
System.out.println("demo ok");
}
}
class FinalizeDemo
{
public static void main(String[] args)
{
new Demo();
new Demo();
new Demo();
System.gc(); // 启动垃圾回收器。
System.out.println("Hello World!");
}
}
运行FinalizeDemo类,可能在屏幕上打印(截图如下):
通过实验会发现每次的结果不一定相同,那是因为随机性造成的。而且每一个线程都有自己的代码内容,这个称之为线程的任务,之所以创建一个线程就是为了去运行指定的任务代码。而线程的任务都封装在特定的区域中,比如:
- 主线程运行的任务都定义在main方法中。
- 垃圾回收线程在收垃圾时都会运行finalize方法。
创建线程方式一
如何在自定义的代码中,自定义一个线程呢?也即如何建立一个执行路径呢?
答:通过对API的查找,java已经提供了对线程这类事物的描述,即Thread
类。该类的描述中创建线程有两种方式,下面我就来讲解其第一种方式。
创建线程的第一种方式:继承Thread
类。
步骤:
- 继承
Thread
类 - 重写
Thread
类中的run()
。目的:将自定义的代码存储在run()
,让线程运行 - 创建子类对象也即创建线程对象
- 调用线程的
start()
。该方法有2个作用:启动线程,调用run()
例,
class Demo extends Thread {
public void run() {
for (int x = 0; x < 60; x++) {
System.out.println("demo run---"+x);
}
}
}
class ThreadDemo {
public static void main(String[] args) {
Demo d = new Demo(); // 创建好一个线程
// d.start(); // 开启线程,并执行该线程的run()
d.run(); // 仅仅是对象的调用方法,而线程创建了,并没有被运行
for (int x = 0; x < 60; x++) {
System.out.println("Hello World!---"+x);
}
}
}
发现运行结果每一次都不同。
因为多个线程都在获取CPU的执行权,CPU执行到谁,谁就运行。明确一点,在某一个时刻,只能有一个程序在运行(多核除外)。CPU在做着快速的切换,以达到看上去是同时运行的效果。我们可以形象地把多线程的运行形容为互相抢夺CPU的执行权,这就是多线程的一个特点:随机性。谁抢到谁执行,至于执行多长,CPU说了算。
问题一、为什么要覆盖run()呢?
答:Thread
类用于描述线程,该类就定义了一个功能,用于存储线程要运行的代码,该存储功能就是run()
。也就是说Thread
类中的run()
用于存储线程要运行的代码。
问题二、调用start方法和调用run方法的区别?
答:调用start方法会开启线程,让开启的线程去执行run方法中的线程任务;而直接调用run方法,线程并未开启,去执行run方法的只有主线程。
练习:创建两个线程,和主线程交替执行。
解:
class Test extends Thread {
Test(String name) {
super(name);
}
public void run() {
for (int x = 0; x < 60; x++) {
System.out.println((Thread.currentThread()==this)+"..."+this.getName()+" run..."+x);
}
}
}
class ThreadTest {
public static void main(String[] args) {
Test t1 = new Test("one---");
Test t2 = new Test("two+++");
t1.start();
t2.start();
for (int x = 0; x < 60; x++) {
System.out.println("main..."+x);
}
}
}
通过上例,可发现原来线程都有自己默认的名称:Thread-编号
,该编号从0开始。
static Thread currentThread()
:获取当前线程对象getName()
:获取线程名称setName()或者构造函数
:设置线程名称
多线程的运行状态
多线程的运行状态用图来表示即为:
创建线程方式二
以此例引申出创建线程的第二种方式:
例,需求:简单的卖票程序。多个窗口同时卖票。
class Ticket implements Runnable {
private int tick = 100;
public void run() {
while(true) {
if(tick > 0) {
try { Thread.sleep(10); } catch(Exception e) {}
System.out.println(Thread.currentThread().getName()+"...sale:"+tick--);
}
}
}
}
class TicketDemo {
public static void main(String[] args) {
Ticket t = new Ticket();
Thread t1 = new Thread(t); // 创建一个线程
Thread t2 = new Thread(t); // 创建一个线程
Thread t3 = new Thread(t); // 创建一个线程
Thread t4 = new Thread(t); // 创建一个线程
t1.start();
t2.start();
t3.start();
t4.start();
}
}
创建线程的第二种方式:实现Runnable
接口。
步骤:
- 定义类实现
Runnable
接口 - 覆盖
Runnable
接口中的run()
。目的:将线程要运行的代码存放在该run()
中 - 通过
Thread
类建立线程对象 - 将
Runnable
接口的子类对象作为实际参数传递给Thread
类的构造函数。
为什么要将Runnable
接口的子类对象作为实际参数传递给Thread
类的构造函数?
答:因为自定义的run()
所属的对象是Runnable
接口的子类对象,所以要让线程去运行指定对象的run()
,就必须明确该run()
所属的对象。 - 调用
Thread
类的start()
开启线程并调用Runnable
接口子类的run
方法。
实现Runnable接口的好处
现将实现Runnable接口的好处总结如下:
- 避免了继承Thread类的单继承的局限性。
- Runnable接口的出现更符合面向对象,将线程任务单独进行了对象的封装。
- Runnable接口的出现降低了线程任务和线程对象的耦合性。
所以,以后创建线程都使用第二种方式。