一、多线程的概念
想要知道什么是多线程?就会引出线程的概念,而线程和进程之间又是息息相关的。
进程:操作系统中一个程序的执行周期称为一个进程。
线程:一个程序同时执行多个任务。通常,每一个任务就称为一个线程。
多线程:一个进程运行时产生了多个线程。
1.1 进程与线程的区别
进程是资源分配的最小单位,线程是程序执行的最小单位。没有进程就没有线程,进程一旦终止,其内的线程也将不复存在。
进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段。
线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。
多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。
1.2 为什么要使用多线程?
1)更好的利用cpu资源
2)线程之间可以共享数据
3)创建线程代价比较小
二、线程的生命周期
在线程的生命周期中,共有五种状态 : 新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead)
当一个线程启动以后,它不能一直霸占着cpu独自运行,CPU需要在多个线程之间切换,于是每个线程就会在各种状态之间转换
1. 新建状态:当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时仅由JVM为其分配内存,并初始化其成员变量的值
2. 就绪状态:当线程对象调用了start()方法之后,该线程处于就绪状态。Java虚拟机为其创建方法调用栈和程序计数器,等待调度运行
3. 运行状态:如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态
4. 阻塞状态:当处于运行状态的线程失去所占用资源之后,便进入阻塞状态
5.死亡状态:当线程执行完了或者因异常退出了run()的执行,该线程的生命周期就结束了
线程状态转换图:
三、实现多线程的方法
Java实现多线程一共有三种方法,分别为:继承Thread类、实现Runnable接口、实现Callable接口
注:想要实现多线程,必须实现Runnable接口,Thread类和Collable接口都间接实现了Runnable接口
3.1 继承Thread类实现多线程
新建一个线程最简单的方法就是直接继承Thread类,覆写该类中的run()方法,然后调用线程的start()方法
//1.继承Thread类
class MyThread extends Thread{
private String title;
public MyThread(String title){
this.title=title;
}
//2.覆写run()方法
@Override
public void run(){ //所有线程从此处开始执行
for(int i=0;i<10;i++){
System.out.println(this.title+" i="+i);
}
}
}
public class Test{
public static void main(String[] args) {
MyThread myThread1=new MyThread("thread1");
MyThread myThread2=new MyThread("thread2");
MyThread myThread3=new MyThread("thread3");
//3.调用线程的start()方法
myThread1.start();
myThread2.start();
myThread3.start();
}
}
注意:任务中的run()方法指明如何完成这个任务。Java虚拟机会自动调用该方法,无需特意调用它,直接调用run()方法只是在同一个线程中执行该方法,而没有新线程被启动。因此必须通过start()方法来启动线程。
3.2 Runnable接口实现多线程
实现多线程最简单的方法就是继承Thread类,但是该方法有单继承局限;使用Runable接口可以解决这一问题。
范例:利用Runable接口实现线程主体类
//1.定义类实现Runnable接口
class MyThread implements Runnable{
private String title;
public MyThread(String title){
this.title=title;
}
@Override
//2.覆写Runnable接口中的run()方法
//将线程要运行的代码放在该run方法中
public void run(){ //所有线程从此处开始执行
for(int i=0;i<10;i++){
System.out.println(this.title+" i="+i);
}
}
}
public class Test{
public static void main(String[] args) {
MyThread myThread1=new MyThread("thread1");
MyThread myThread2=new MyThread("thread2");
MyThread myThread3=new MyThread("thread3");
//3.通过Thread类建立线程对象
//4.将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数
//5.调用Thread类的start方法开启线程
new Thread(myThread1).start();
new Thread(myThread2).start();
new Thread(myThread3).start();
}
}
注意:使用Runnable接口实现多线程时,由于没有继承Thread类,不能直接调用start()方法,此时需要将Runnable接口的子类对象作为参数传递给Thread类的构造函数(该构造函数的参数就是Runnable对象),最后调用Thread类的start()方法,开启线程。
另外,对于此时的Runnable接口对象可以采用匿名内部类或者Lambda表达式来定义
范例1:使用匿名内部类进行Runnable对象创建
public class Test{
public static void main(String[] args) {
//新建Thread类、Runnable类的匿名对象
new Thread( //匿名内部类可以实例化接口
new Runnable(){ //Runnable类的匿名对象作为Thread类构造方法的参数
@Override
public void run(){ //Runnable类是一个接口,该类有一个抽象方法run(),
System.out.println("hello word");
}
}
).start();
}
}
范例2:使用Lamdba表达式进行Runnable对象创建
public class Test{
public static void main(String[] args) {
Runnable runnable =()->System.out.println("hello word");//lamdba表达式
new Thread(runnable).start();
}
}
3.3 Callable接口实现多线程
Thread类和Runnable接口中的run()方法都没有返回值,但是在某些情况下,线程执行完成后需要带回一些返回值,这时,就需要利用Callable接口来实现多线程。
范例:使用Callable接口实现多线程
//1.定义类实现Callable接口
class MyThread implements Callable<String> {
private int ticket=10;//10张票
@Override
public String call()throws Exception { //2.实现Callable接口的call()方法,该方法有返回值
while (this.ticket > 0) {
System.out.println("剩余票数:" + this.ticket--);
}
return("票卖完啦~~");
}
}
public class Test {
public static void main(String[] args) throws InterruptedException,ExecutionException {
//3.使用FutureTask类来包装Callable对象,FutureTask对象封装了Callable对象的call()方法的返回值
FutureTask<String> task=new FutureTask<>(new MyThread());
//4.使用FutureTask对象作为Thread对象启动线程
new Thread(task).start();
new Thread(task).start();
//5.调用FutureTask对象的get()方法获取子线程执行后的返回值
System.out.println(task.get());
}
}
3.4 线程类继承结构
线程类继承结构图:
(1)根据以上继承结构图可知:Thread类是Runnable接口的子类,覆写了Runnable接口的run()方法
(2) 在多线程的处理上,使用的是代理设计模式:Runnable接口定义了相关协议,Thread类是代理类,MyThread类则是目标实现类。
3.5 三种实现多线程方法对比
区别 | 局限性 | 共享性 | 返回值 |
Thread类 | 单继承 | / | 无返回值 |
Runnable接口 | / | 多个线程共享一个target对象 | 无返回值 |
Callable接口 | / | 多个线程共享一个target对象 | 有返回值 |
范例1:使用Thread类实现数据共享
class MyThread extends Thread{
private int ticket=10;
@Override
public void run(){
while (this.ticket>0){
System.out.println("剩余票数"+this.ticket--);
}
}
}
public class Test {
public static void main(String[] args) {
new MyThread().start();
new MyThread().start();
}
}
结果:
剩余票数10
剩余票数10
剩余票数9
剩余票数8
剩余票数7
剩余票数6
剩余票数5
剩余票数9
剩余票数4
剩余票数8
剩余票数3
剩余票数2
剩余票数1
剩余票数7
剩余票数6
剩余票数5
剩余票数4
剩余票数3
剩余票数2
剩余票数1
此时,启动三个线程实现买票处理,结果为三个线程各自卖自己的票,说明,Thread类实现的多线程不能共享同一个target对象
范例2:使用Runnable接口实现数据共享
class MyThread implements Runnable{
private int ticket=10;
@Override
public void run(){
while (this.ticket>0){
System.out.println("剩余票数"+this.ticket--);
}
}
}
public class Test {
public static void main(String[] args) {
MyThread mythread=new MyThread();
new Thread(mythread).start();
new Thread(mythread).start();
new Thread(mythread).start();
}
}
结果:
剩余票数10
剩余票数8
剩余票数7
剩余票数6
剩余票数5
剩余票数4
剩余票数3
剩余票数1
剩余票数9
剩余票数2
Runnable实现的多线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。
那么,为什么Runnable可以共享同一个target对象呢?
四、多线程的常用操作方法
4.1 线程命名与取得
多线程的运行状态是不确定的,所以对于多线程操作必须有一个明确标识出线程对象的信息,这个信息往往通过名称来描述。
在Thread类中有以下几种关于线程名称的方法:
方法名称 | 类型 | 描述 |
public Thread(Runnable target, String name) | 构造 | 创建线程的时候设置名称 |
public final synchronized void setName(String name) | 普通 | 设置线程名称 |
public final String getName() | 普通 | 取得线程名称 |
取得当前线程对象的方法:
public static native Thread currentThread();
范例:线程命名与取得
class MyThread implements Runnable{
private int ticket=10;
@Override
public void run(){
for(int i=0;i<10;i++){
System.out.println("当前线程"+Thread.currentThread().getName()+" i="+i);
}
}
}
public class Test {
public static void main(String[] args) {
MyThread mythread=new MyThread();
new Thread(mythread).start();
new Thread(mythread,"rachel").start();
}
}
注意:如果线程没有命名,则会自动分配一个名称,另外,设置名称不要重复,中间不要修改
范例2:线程执行结果
class MyThread implements Runnable{
@Override
public void run(){
System.out.println("当前线程"+Thread.currentThread().getName());
}
}
public class Test {
public static void main(String[] args) {
MyThread mythread=new MyThread();
mythread.run();//直接通过对象调用run()方法
new Thread(mythread).start();//通过线程调用
}
}
结果:
当前线程main
当前线程Thread-0
通过以上程序可以发现:主方法本来就是一个线程,所有线程都是通过主方法来创建并启动的。
4.2 线程休眠——sleep()方法
线程休眠:指的是让线程暂缓执行一下,等到了预计时间之后再恢复执行。
线程休眠会交出CPU,让CPU去执行其他的任务。但是sleep()方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。
方法定义:
public static native void sleep(long millis) throws InterruptedException;
4.3 线程让步——yield()方法
线程让步:暂停当前正在执行的线程对象,并执行其他线程。
1)调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出CPU的时间,另外,yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。
2)调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的
方法定义:
public static native void yield();
4.4 等待线程终止——join()方法
等待该线程终止:如果在主线程中调用该方法时就会让主线程休眠,让调用该方法的线程run方法先执行完毕之后在开始执行主线程
class MyThread implements Runnable{
@Override
public void run() {
try {
System.out.println("主线程睡眠前的时间");
Test.printTime();//线程睡眠开始时间
Thread.sleep(2000);//线程休眠2秒钟
System.out.println(Thread.currentThread().getName());//此时获取到的是子线程A的名称
System.out.println("睡眠结束的时间");
Test.printTime();//线程睡眠结束时间
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public class Test {
public static void main(String[] args) throws InterruptedException{
MyThread mt = new MyThread();
Thread thread = new Thread(mt,"子线程A");
thread.start();//子线程A启动,执行run方法内的操作
System.out.println(Thread.currentThread().getName());//此时获得的是主方法线程的名称
thread.join();//主线程休眠,等待子线程A执行完run方法之后继续运行
System.out.println("代码结束");
}
//打印当前时间
public static void printTime() {
Date date=new Date();
DateFormat format=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String time=format.format(date);
System.out.println(time);
}
}
结果:
main
主线程睡眠前的时间
2019-05-06 13:10:56
子线程A
睡眠结束的时间
2019-05-06 13:10:58
代码结束
4.5 停止线程——stop()方法
多线程中有三种方式可以停止线程。
1. 设置标记位,可以使线程正常退出。
2. 使用stop方法强制使线程退出,但是该方法不太安全所以已经被废弃了。
3. 使用Thread类中的一个 interrupt() 可以中断线程。
范例1:设置标记位使线程停止
class MyThread implements Runnable {
private boolean flag = true; //设置标志位为true
@Override
public void run() {
int i = 1;
while (flag) {
try {
Thread.sleep(1000);
System.out.println("第"+i+"次执行,线程名称为:"+Thread.currentThread().getName());
i++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
Thread thread1 = new Thread(myThread, "子线程A");
thread1.start();
Thread.sleep(2000); //主方法休眠
myThread.setFlag(false);//设置标志位为false
System.out.println("代码结束");
}
}
范例2:使用stop()方法使线程停止
class MyThread implements Runnable {
@Override
public void run() {
int i = 1;
try {
Thread.sleep(1000);
System.out.println("第"+i+"次执行,线程名称为:"+Thread.currentThread().getName());
i++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
Thread thread1 = new Thread(myThread,"子线程A");
thread1.start();
Thread.sleep(3000);
thread1.stop();
System.out.println("代码结束");
}
}
注:使用stop()方法会使线程立即停止,它是一种不安全的操作,某些情况下,会造成数据残留,不同步。
范例3:使用interrupt()方法使线程停止
class MyThread implements Runnable {
private boolean flag = true;
@Override
public void run() {
int i = 1;
while (flag) {
try {
/**
* 这里阻塞之后,线程被调用了interrupte()方法,
* 清除中断标志,就会抛出一个异常
* java.lang.InterruptedException
*/
Thread.sleep(1000);
boolean bool = Thread.currentThread().isInterrupted();
if (bool) {
System.out.println("非阻塞情况下执行该操作。。。线程状态" + bool);
break;
}
System.out.println("第"+i+"次执行,线程名称为:"+Thread.currentThread().getName());
i++;
} catch (InterruptedException e) {
System.out.println("退出了");
/**
* 这里退出阻塞状态,且中断标志被系统会自动清除,
* 并且重新设置为false,所以此处bool为false
*/
boolean bool = Thread.currentThread().isInterrupted();
System.out.println(bool);
//退出run方法,中断进程
return;
}
}
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
Thread thread1 = new Thread(myThread,"子线程A");
thread1.start();
Thread.sleep(3000);
thread1.interrupt();
System.out.println("代码结束");
}
调用线程类的interrupted方法,其本质只是设置该线程的中断标志,将中断标志设置为true,并根据线程状态决定是否抛出异常。因此,通过interrupted方法真正实现线程的中断原理是:开发人员根据中断标志的具体值,来决定如何退出线程。
4.6 线程优先级
线程的优先级指的是,线程的优先级越高越有可能先执行,但仅仅是有可能而已
Thread类中关于线程优先级的相关方法:
方法 | 描述 |
public final void setPriority(int newPriority) | 设置优先级 |
public final int getPriority() | 取得优先级 |
对于优先级设置的内容可以通过Thread类的几个常量来决定
1. 最高优先级:public final static int MAX_PRIORITY = 10;
2. 中等优先级:public final static int NORM_PRIORITY = 5;
3. 最低优先级:public final static int MIN_PRIORITY = 1;
范例:设置优先级
class MyThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5 ; i++) {
System.out.println("当前线程:" + Thread.currentThread().getName()+" ,i = "
+i);
}
}
}
public class Test {
public static void main(String[] args) {
MyThread mt = new MyThread();
Thread t1 = new Thread(mt,"1") ;
Thread t2 = new Thread(mt,"2") ;
Thread t3 = new Thread(mt,"3") ;
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.MIN_PRIORITY);
t3.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
t3.start();
}
}
1)主方法只是中等优先级
2)线程是有继承关系的,比如当A线程中启动B线程,那么B和A的优先级将是一样的
4.7守护线程
守护线程是一种特殊的线程,它属于是一种陪伴线程。
Java 中有两种线程:用户线程和守护线程。可以通isDaemon()方法来区别它们:如果返回false,则说明该线程是“用户线程”;否则就是“守护线程”。
典型的守护线程就是垃圾回收线程。只要当前JVM进程中存在任何一个非守护线程没有结束,守护线程就在工作;只有当最后一个非守护线程结束时,守护线程才会随JVM一同停止工作。
主线程是用户线程
范例:守护线程
class A implements Runnable {
private int i;
@Override
public void run() {
try {
while (true) {
i++;
System.out.println("线程名称:" + Thread.currentThread().getName() + ",i=" + i + ",是否为守护线程:"
+ Thread.currentThread().isDaemon());
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.println("线程名称:" + Thread.currentThread().getName() + "中断线程了");
}
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new A(),"子线程A");
thread1.setDaemon(true); // 设置线程A为守护线程,此语句必须在start方法之前执行
thread1.start();
Thread thread2 = new Thread(new A(),"子线程B");
thread2.start();
Thread.sleep(3000);//主线程休眠
thread2.interrupt();// 中断非守护线程
Thread.sleep(10000);//主线程休眠
System.out.println("代码结束");
}
}
注:B是用户线程当它中断了之后守护线程还没有结束,是因为主线程(用户线程)还没有结束,所以说明是所有的用户线程结束之后守护线程才会结束。