多线程【概述、原理、创建、获取】
1、多线程概述
进程:正在运行的程序。确切的来说,当一个程序进入内存运行,即变成一个进程
,进程是处于运行过程中的程序,并且具有一定独立功能,进程是系统进行资源分配和调度的一个独立单位。
进程是正在运行的程序,进程负责给程序分配内存空间,而每一个进程都是由程序代码组成的,这些代码在进程中执行的流程就是线程。
线程:线程
是进程
中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程,但至少有一个线程。
一个程序中有多个线程在同时执行。
2、多线程运行原理
大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个任务。比如:现在我们上课一边使用编辑器,一边使用录屏软件,同时还开着画图板,dos窗口等软件。感觉这些软件好像在同时运行着。
其实这些软件在某一时刻,只会运行一个进程。由于CPU(中央处理器)
在做着高速的切换而导致的。对于CPU而言,它在某个时间点上,只能执行一个程序,即就是说只能运行一个进程,CPU不断地在这些进程之间切换。只是我们自己感觉不到。因为CPU的执行速度相对我们的感觉实在太快了,虽然CPU在多个进程之间轮换执行,但我们自己感到好像多个进程在同时执行。
CPU会在多个进程之间做着切换,如果我们开启的程序过多,CPU切换到每一个进程的时间也会变长,我们也会感觉机器运行变慢。所以合理的使用多线程可以提高效率,但是大量使用,并不能给我们带来效率上的提高。
2、主线程介绍
回想我们以前学习中写过的代码,当我们在dos命令行中输入java空格类名回车后,启动JVM,并且加载对应的class文件。虚拟机并会从main方法开始执行我们的程序代码,一直把main方法的代码执行结束。如果在执行过程遇到循环时间比较长的代码,那么在循环之后的其他代码是不会被执行的。如下代码演示:
class Demo
{
String name;
Demo(String name)
{
this.name = name;
}
void show()
{
for (int i=1;i<=20 ;i++ )
{
System.out.println("name="+name+",i="+i);
}
}
}
class ThreadDemo
{
public static void main(String[] args)
{
Demo d = new Demo("小强");
Demo d2 = new Demo("旺财");
d.show();
d2.show();
System.out.println("Hello World!");
}
}
若在上述代码中show方法中的循环执行次数很多,这时书写在d.show();下面的代码是不会执行的,并且在dos窗口会看到不停的输出name=小强,i=值。
原因是:,必然有一个执行路径(线程)从main方法开始的**。一直执行到main方法结束。这个线程在java中称之为主线程。当主线程在这个程序中执行时,如果遇到了循环而导致程序在指定位置停留时间过长,无法执行下面的程序。
多线程
可以解决一个主线程负责执行其中一个循环,由另一个线程负责其他代码的执行。
3、多线程内存图解
多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈。
当执行线程的任务结束了,线程自动在栈内存中释放了。但是当所有的执行线程都结束了,那么进程就结束了。
4、获取线程名称
Thread.currentThread()获取当前线程对象
Thread.currentThread().getName();获取当前线程对象的名称
class Demo extends Thread //继承Thread
{
String name;
Demo(String name)
{
this.name = name;
}
//复写其中的run方法
public void run()
{
for (int i=1;i<=20 ;i++ )
{
System.out.println("name="+name+","+Thread.currentThread().getName()+",i="+i);
}
}
}
class ThreadDemo
{
public static void main(String[] args)
{
//创建两个线程任务
Demo d = new Demo("小强");
Demo d2 = new Demo("旺财");
d2.start();//开启一个线程
d.run();//主线程在调用run方法
}
}
原来主线程的名称: main
自定义的线程: Thread-0 线程多个时,数字顺延。Thread-1......
进行多线程编程时不要忘记了Java程序运行时从主线程开始
,main方法的方法体就是主线程的线程执行体。
5、创建线程两种方式(都要重写run()方法):
创建新执行线程有两种方法:
- 一种方法是将类声明为 Thread 的子类。该子类应重写 Thread 类的 run() 方法。接下来可以分配并启动该子类的实例。
- 创建线程的另一种方法是声明实现
Runnable 接口
。然后重写run
方法。然后可以分配该类的实例,在创建 Thread 时作为一个参数来传递并启动。
5.1、方式一:继承Thread类,重写run()方法
创建线程的步骤:
- 定义一个类继承Thread。
- 重写
run()
方法。 - 创建子类对象,就是创建线程对象。
- 调用
start()
方法,开启线程并让线程执行,同时还会告诉jvm去调用run()方法。
class Demo extends Thread //继承Thread
{
String name;
Demo(String name)
{
this.name = name;
}
//复写其中的run方法
public void run()
{
for (int i=1;i<=20 ;i++ )
{
System.out.println("name="+name+",i="+i);
}
}
}
class ThreadDemo
{
public static void main(String[] args)
{
//创建两个线程任务
Demo d = new Demo("小强");
Demo d2 = new Demo("旺财");
//d.run(); 这里仍然是主线程在调用run方法,并没有开启两个线程
//d2.run();
d2.start();//开启一个线程
d.start();//主线程在调用run方法
}
}
线程对象调用 run()方法和调用start()方法区别:
线程对象调用run()方法不开启线程。仅是对象调用方法。线程对象调用start开启线程,并让jvm调用run()方法在开启的线程中执行。
继承Thread类原理
继承Thread类:因为Thread类描述线程事物,具备线程应该有功能。
Thread t1 = new Thread();
t1.start();
//这样做没有错,但是该start调用的是Thread类中的run方法,而这个run方法没有做什么事情,更重要的是这个run方法中并没有定义我们需要让线程执行的代码。
创建线程的目的
建立单独的执行路径,让多部分代码实现同时执行。也就是说线程创建并执行需要给定的代码(线程的任务)。对于之前所讲的主线程,它的任务定义在main函数
中。自定义线程需要执行的任务都定义在run方法中。Thread类中的run方法内部的任务并不是我们所需要,只有重写这个run方法,既然Thread类已经定义了线程任务的位置,只要在位置中定义任务代码即可。所以进行了重写run方法动作。
5.2、方式二:实现Runnable接口,重写run()方法
查看Runnable接口说明文档:Runnable 接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为 run 的无参数方法。
总结:
创建线程的第二种方式:实现Runnable接口。
- 定义类实现
Runnable
接口。 - 覆盖接口中的
run
方法。 - 创建
Thread
类的对象。 - 将
Runnable
接口的子类对象作为参数传递给Thread
类的构造函数。 - 调用
Thread
类的start
方法开启线程。
class Demo implements Runnable
{
private String name;
Demo(String name)
{
this.name = name;
}
//覆盖了接口Runnable中的run方法。
public void run()
{
for(int i=1; i<=20; i++)
{ System.out.println("name="+name+"..."+Thread.currentThread().getName()+"..."+i);
}
}
}
class ThreadDemo2
{
public static void main(String[] args)
{
//创建Runnable子类的对象。注意它并不是线程对象。
Demo d = new Demo("Demo");
//创建Thread类的对象,将Runnable接口的子类对象作为参数传递给Thread类的构造函数。
Thread t1 = new Thread(d);
Thread t2 = new Thread(d);
//将线程启动。
t1.start();
t2.start();
System.out.println(Thread.currentThread().getName()+"----->");
System.out.println("Hello World!");
}
}
5.2.1、实现Runnable的原理
继承Thread类和实现Runnable接口有啥区别呢?
实现Runnable
接口,避免了继承Thread
类的单继承局限性。覆盖Runnable接口中的run
方法,将线程任务代码定义到run
方法中。创建Thread类的对象,只有创建Thread类的对象才可以创建线程。线程任务已被封装到Runnable接口的run方法中,而这个run
方法所属于Runnable接口的子类对象,所以将这个子类对象作为参数传递给Thread的构造函数,这样,线程对象创建时就可以明确要运行的线程的任务。
5.2.2、实现Runnable的好处
第二种方式实现Runnable接口避免了单继承的局限性,所以较为常用。实现Runnable接口的方式,更加的符合面向对象,线程分为两部分,一部分线程对象
,一部分线程任务
。继承Thread类,线程对象和线程任务耦合在一起。一旦创建Thread类的子类对象,既是线程对象,有又有线程任务。实现runnable接口,将线程任务单独分离出来封装成对象,类型就是Runnable接口类型。Runnable接口对线程对象和线程任务进行解耦。
6、多线程的异常信息
在前面学习过程,编写的程序有时会看到有相应的异常信息打印出来,当时上课我们只说了这个异常信息是什么,怎么去解决异常。可是每个异常的前面还有部分内容并没有给大家完全讲解它的含义。
class Demo extends Thread
{
void show()
{
int[] arr = new int[3];
System.out.println("arr[3]="+arr[3]);
}
}
class ThreadDemo2
{
public static void main(String[] args)
{
Demo d = new Demo();
d.show();
System.out.println("Hello World!");
}
}
运行结果如下:
以前我们讲过被用蓝色线标注出来的是异常的名称。可是被黄色线标注出来的是什么东东呢?
我们修改程序继承运行看看。
class Demo extends Thread
{
public void run()
{
int[] arr = new int[3];
System.out.println("arr[3]="+arr[3]);
System.out.println("over");
}
}
class ThreadDemo2
{
public static void main(String[] args)
{
Demo d = new Demo();
Demo d2 = new Demo();
d.start();
d2.start();
System.out.println("Hello World!");
}
}
继承运行上面的程序发现有点小变化
发现被黄色框选中的部分,有些变化,分别在说明每个异常发生具体的哪一个线程上。并且线程任务中的**System.out.println(“over”);**因为程序终止,所以不会打over
字符串。
我们发现main方法中的Hello World!字符串却打印出来了。其实在这个程序中有三个线程,分别是主线程,Thread-0,Thread-1这三个线程。异常发生在Thread-0,Thread-1线程上,而main线程并没有发生异常,所以主线程正常运行完成。
注意:
当主线程执行完成了,并不代表程序就结束,如果此时还有其他线程正常执行,程序仍然在执行过程中。
当任何一个线程出现了异常,其他线程还是会继续运行的。异常只会影响到异常所属的那个线程。
7、多线程练习
7.1、多线程练习——售票
大家都去过火车站购买车票,我们来模拟窗口售票。售票的动作需要同时执行,所以使用多线程技术。
class Ticket implements Runnable
{
//1、描述票的数量。
private int tickets = 100;
//2、售票的动作,这个动作需要被多线程执行,那就是线程任务代码。需要定义run方法中。
//线程任务中通常都有循环结构。
public void run()
{
while(true)
{
if(tickets>0)
{
//打印线程名称。
System.out.println(Thread.currentThread().getName()+"....."+tickets--);
}
}
}
}
class ThreadDemo3
{
public static void main(String[] args)
{
//1,创建Runnable接口的子类对象。
Ticket t = new Ticket();
//2,创建四个线程对象。并将Runnable接口的子类对象作为参数传递给Thread的构造函数。
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
//3,开启四个线程。
t1.start();
t2.start();
t3.start();
t4.start();
}
}
如果我们创建一个线程对象,多次调用其start()方法,多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动。多次开启同一个线程会发生IllegalThreadStateException - 如果线程已经启动。
8、线程状态图
查阅API关于IllegalThreadStateException这个异常说明信息发现,这个异常的描述信息为:指示线程没有处于请求操作所要求的适当状态时抛出的异常。
线程的状态:
1、新建状态(New): 新创建了一个线程对象。
2、就绪状态(Runnable): 线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
3、运行状态(Running): 就绪状态的线程获取了CPU,执行程序代码。
4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
- 等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
- 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
- 其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)
5、死亡状态(Dead): 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。