Java线程①-创建线程和Thread类

一:创建线程的方式

(1)继承 Thread 类

继承 Thread 类来创建一个线程类,然后重写run方法

重写父类这里的run()就相当于线程的入口方法,线程具体跑起来之后,要做啥事,都是通过这个run入口来描述

Thread这里是直接把要完成的工作放到Thread类的run()方法里

class MyThread extends Thread{           //继承Thread类
    @Override
    public void run() {     //重写父类Thread中的run()方法,这个run方法表示线程执行后需要干什么
        System.out.println("Hello World!!!");     //线程执行后打印"Hello World!!!"
    }
}


public class Demo1 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();    //实例化;创建一个MyThread类型的对象

        myThread.start();     //启动线程
    }
}

(2)实现Runnable接口

通过Runnable接口的实现来创建线程,同样重写run方法

Runnable分开来,把要完成的工作放到Runnable中,再让Runnable和Thread配合

除了创建Runnable对象,还要创建Thread对象,然后都实例化,将Runnable引用变量作为构造方法的参数传给Thread然后由Thread的引用变量调用方法

//使用Runable接口实现创建线程
class MyRunnable implements Runnable{    //我创建一个类MyRunnable来实现Runnable接口
    @Override
    public void run() {
        System.out.println("Hello World!!!");
    }
}


public class Demo3 {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();

       //还需要搞个Thread对象,因为只有myRunnable是没有办法直接运行start()的
       //将myRunnable作为构造方法的参数传到Thread

        Thread thread = new Thread(myRunnable);
        thread.start();
    }
}

(3)匿名内部类创建Thread子类对象

继承Thread,重写run,基于匿名内部类

1.匿名内部类
public class Demo {
    public static void main(String[] args) {
        类 引用变量 = new 对象() {
            @Override
            重写方法
        };
    }
}
2.代码示例
public class Demo4 {
    public static void main(String[] args) {
      //创建一个子类,子类继承Thread,同时,这个子类没有名字,所以称为匿名内部类
      //一般来说,我们的类都是独立的,但是这个属于内部类,它可以存在于一个类的内部,就像这个匿名子类创建于Demo4类之中
            Thread t1 = new Thread() {   
                @Override
                public void run() {     //在匿名子类中重写run方法
                    System.out.println("使用匿名类创建 Thread 子类对象");
                }
            };

            t1.start();      //启动线程
      }
}

(4)匿名内部类创建 Runnable 子类对象

public class Demo5 {
    public static void main(String[] args) {
        //此处的new Runnable()一整段都是在创建Runnable子类,作为参数通过构造方法传给Thread

        Thread t2 = new Thread(new Runnable() {    
            @Override
            public void run() {          //重写run方法
                System.out.println("使用匿名类创建 Runnable 子类对象");
            }
        });    //表示Thread构造方法的结束

        t2.start();     //启动线程
    }
}

(5)lambda 表达式创建 Runnable 子类对象

1.lambada表达式

本质上是一个“匿名函数”,这样的匿名函数,主要就可以作为回调函数来使用

回调函数:不需要主动调用,而是在合适的时机自动被调用

2.lambada语法

->:可理解为“被用于”的意思

3.代码示例

★不用重写run方法!

★把要执行的逻辑写在lambda里面!

public class Demo6 {
    public static void main(String[] args) {
        //Thread里的内括号是可以写形参的
        Thread t4 = new Thread(() -> {
            System.out.println("使用匿名类创建 Thread 子类对象");
        });

        t4.start();   //启动线程
    }
}

二:认识Thread类的start()和run()

(1)start()和run()

start():创建新线程
run():无法创建新线程

★前提:我们需要知道,每次当我们点击运行程序的时候,就会先创建出一个Java进程,这个进程就包括了至少一个线程,而这个线程也叫做主线程,也就是负责执行main方法的线程

(2)通过println打印来简单理解start()和run()

//用Thread类创建对象写一个简单的Hello World
//Thread来自于java.lang这个包,因此无需import即可默认使用


class MyThread extends Thread{          //我创建一个MyThread类然后继承java标准库中现场的Thread类
    @Override
    public void run() {                 //重写父类Thread中的run()方法
        System.out.println("Hello World!!!");
    }
}


public class Demo1 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();    //创建一个MyThread类型的对象,对象名(引用变量)叫myThread

        myThread.start();

        myThread.run();

    }
}

①重写父类这里的run()就相当于线程的入口方法,线程具体跑起来之后,要做啥事,都是通过这个run入口来描述
(如图所示,我的线程运行起来后就是打印“Hello World!!!”)

②这里的由引用变量myThread调用的方法start()就是在创建新的线程


这个start()操作就会在底层调用操作系统提供的“创建线程”的API,同时就会在操作系统内核里创建出对应的PCB结构,并且加入到对应的链表中
(管理进程要先描述后组织,在组织的时候说过创建进程是如何创建的!!!)
此时,这个新创建出来的线程就会参与到CPU的调度中,这个线程接下来要执行的工作,就是刚刚上面重写的run方法~


这个时候就有两个线程,一个主线程,一个由start()创建的新线程!


运行后,就会打印“Hello World!!!”

③这里的由引用变量myThread调用的方法run()实际就是个入口普通方法


这个run()操作既没有在底层调用操作系统提供的“创建线程”的API,也没有创建出真正的线程来,因此,自始至终只有主线程在执行代码

此时,我们这里仍然只有一个主线程,并没有创建出新的线程!


运行后,也会打印“Hello World!!!”

(3)通过while循环来更深理解start()和run()

class MyThread1 extends Thread{          //我创建一个MyThread1类然后继承java标准库中现场的Thread类
    @Override
    public void run() {                 //重写父类Thread中的run()方法
        while (true) {
            System.out.println("Hello Thread!!!");
        }
    }
}


public class Demo2 {
    public static void main(String[] args) {
        MyThread1 myThread1 = new MyThread1();    //创建一个MyThread1类型的对象,对象名(引用变量)叫myThread1

        myThread1.start();

        myThread1.run();

        while (true) {
            System.out.println("Hello main!!!");
        }

    }
}

①运行myThread1.start()时的结果:


现象:这个时候main方法里的while循环和run方法里的while循环这两个循环是同时执行的,都在交替式的打印!


原因:因为这里有两个线程,一个主线程执行main里的while循环,一个start创建的新线程执行重写run方法里的while循环,两个线程都在分别执行自己的循环,这两个线程都能参与到CPU的调度中!

执行流程:

从main方法开始
先执行第一条语句    MyThread1 myThread1 = new MyThread1();
再执行第二条语句     myThread1.start();

(而这个start会创建出新的线程,这个新的线程就会来执行重写run方法里的while循环代码;此时原来的主线程则继续执行第三条语句)
最后执行第三条语句     
while (true) {
   System.out.println("Hello main!!!");
}


★这个时候,主线程打印着"Hello main",新线程打印着“Hello Thread”,两个线程是并发执行状态!!!

②运行myThread1.run()时的结果:


现象这个时候只有run方法里的while循环在打印!

原因因为此时只有一个主线程,run()并没有像start()一样创建出新线程,所以当主线程执行到myThread1.run()时,它要去执行重写run方法里的while循环代码,又因为这个while循环是无限循环,所以无法停下,自然也就执行不到main方法里的while循环!

(4)结论

①每个线程,都是一个独立执行流
②每个线程,都可以执行一段代码
③多个线程,它们之间是并发关系(并不是执行完这个才能执行那个,而是一起执行)

三:深入了解Thread属性和方法

(1)Thread类的构造方法

①Thread()                              //无参;创建线程对象

②Thread(Runnable target)    //使用 Runnable 对象作为参数;创建线程对象

③Thread(String name)         //创建线程对象,并命名

④Thread(Runnable target, String name)      //使用 Runnable 对象作为参数,并命名

1.举例子

Thread t1 = new Thread();

Thread t2 = new Thread(new MyRunnable());

Thread t3 = new Thread("这是我的名字");

Thread t4 = new Thread(new MyRunnable(), "这是我的名字");

★用④作为例子写一段代码更好的理解:

public class Demo7 {
    public static void main(String[] args) {
        //使用了Thread(Runnable target, String name)这个构造方法
            Thread t = new Thread(() -> {
                while (true) {
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "myThread");     //自定义线程名字叫myThread

            t.start();    //启动线程
    }
}

2.分析代码

 ①通过jconsole我们可以看到自己创建且自定义名字的线程myThread

但是!!!main线程也就是主线程没有了!!!


原因:main线程往下执行,当执行到t.start()时,myThread线程就会去执行lambda里的逻辑,也就是去执行while(true),当然,因为是无限循环,所以线程无法停下!但此时main线程已经执行完毕,main线程就结束了,因此我们就看不到main线程

结论:

线程是通过start()创建

主线程的入口执行与完毕靠的是main

其他线程的入口执行与完毕靠的是run/lambda

(如果while循环执行完毕,则myThread线程也就消失了)

(2)Thread的常见属性和普通方法

1.ID

①概念

ID 是由JVM给线程的唯一标识,相当于线程身份证,不同线程不会重复

②获取ID方法

引用变量.getId();

2.名称

①概念

名称是各种调试工具用到

②获取名称方法

引用变量.getName();

3.状态

①概念

状态表示线程当前所处的一个情况(在进程调度的时候说过)

②获取状态方法

引用变量.getState();

4.优先级

①概念

优先级高的线程理论上来说更容易被调度到(在进程调度的时候说过)

★一般我们会使用默认的优先级

因为设置/获取优先级的作用并不大,因为进程的调度主要是在内核上的,而且系统调度的速度是极快的,因此我们感知不到!

②获取优先级方法

引用变量.getPriority();

5.后台线程

①概念

一:后台线程也称为守护线程

二:后台线程不影响进程的结束

三:当一个进程中所有的前台线程都执行完成,退出进程;即使还存在一些后台线程没有执行完,也会跟着进程一起退出

②引入前台线程

一:前台线程就是会影响进程结束,如果前台线程没有执行完,进程是不会结束的

二:只要有任何前台还在运行,程序就不会终止。比如,执行main()的就是一个前台线程

③设置后台线程

创建的线程默认是前台线程,可以通过设置后台线程

引用变量.setDaemon(true);

④判断是否是后台线程

引用变量.isDaemon();

⑤代码理解

public class Demo8 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (true){
                System.out.println("Hello Thread");
            }
        },"myThread");   //创建的线程名字叫myThread

        //将myThread设置成后台线程,也就是说,只要main线程执行完t.start()退出,myThread也会跟着退出,不再无限循环打印
        t.setDaemon(true);
        t.start();
    }
}

6.存活

①概念

线程是否存活

简单的理解为 run 方法运行是否结束了


②区分Thread对象与线程的生命周期

★Thread对象与线程并不是“同生共死”!

一:从创建角度来说

先有Thread对象后有线程

一般是Thread对象先创建好

直到手动调用start方法,内核才真正创建出线程

二:从消亡角度来说

可能是Thread对象先结束

可能是线程先结束,因为此时已经把run执行完了


③判断线程还是否存活

引用变量.isAlive();

四:启动一个线程-start()

🔺前面已经有很详细的介绍了,这里不过多赘述

1.start的作用

start()就是在创建并启动新的线程

通过start来执行父类重写的run方法,执行线程具体要干什么

2.深入理解

这个start()操作就会在底层调用操作系统提供的“创建线程”的API,同时就会在操作系统内核里创建出对应的PCB结构,并且加入到对应的链表中

五:获取当前线程引用

1.方法

public static Thread currentThread();       //返回当前线程对象的引用

2.代码

public class Demo12 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            //Thread.currentThread()就相当于hlizoo
            System.out.println(Thread.currentThread().getName());    
        },"hlizoo");

        t.start();
    }
}

六:终止一个线程

1.概念

终止线程想办法让这个run方法尽快或立刻执行完毕

★一般来说,不会出现run还没执行完,线程就突然没了的情况;除非你强行拔电源

2.方法一

手动设置标志位,通过标志位让run方法尽快结束

★必须将标志位设置为成员变量(类内方法外)

①代码举例:
public class Demo9 {

    public static boolean isQuit  = false;     //设置成员变量(类内方法外)isQuit作为标志位

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
           while (!isQuit){      //取反逻辑
               System.out.println("Hello Thread");
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        //启动线程
        t.start();
        //主线程执行sleep逻辑之后,想让t线程结束
        Thread.sleep(3000);
        //将isQuit改为true,取反后就是false,让t结束
        isQuit=true;
        System.out.println("t线程终止");
    }
}

②分析代码:

观察代码运行结果可以看出,执行t.start()后,t线程去执行run方法,由于主线程sleep睡眠了3秒,这个时候t线程正在执行while循环,“Hello Thread”得以打印,但是3秒之后,标志位isQuit改为true,取反后就是false,就会把while循环停下,此时run方法执行完毕,主线程继续执行,就打印了“t线程终止”!!!

③为什么必须将标志位设置为成员变量:

当我把标志位设置在main方法里面会报错

①理论:因为lambda引入了“变量捕获”机制,lambda内部看起来是直接访问外部的变量,其实本质上是把外部的变量给复制一份到lambda里面,为了解决生命周期问题


②原因:我们都知道lambda是一个回调函数,它的执行时机并非立即执行,而是更靠后,是在后续线程被创建好了之后,在这个新线程内部被执行;这就会导致当后续真正执行lambda的时候,局部变量isQuit或许已经随着main方法的执行完毕被销毁了,但此时线程可能还在继续执行,让线程去访问一个已经被销毁的变量显然是不合适的


③变量捕获的限制:要求捕获的变量至少是final或者事实上得是final;如果这个变量想要修改,就不能进行变量捕获!!!

当加了个final,就没有报错了!!!

★虽然没有报错,但是你在接下来也无法改变isQuit的值,因为是final修饰了,这个标识符也就没法让线程终止了

3.方法二

利用Thread给我们提供现成的标志位

Thread.currentThread().isInterrupted()

引用变量.Interrupt()表示将标志位设为true

currentThread()的作用是获取当前线程对象,在哪个线程调用这个方法,就能够获取哪个线程的引用

例:Thread.currentThread()就是能够获取到线程t,也可以说Thread.currentThread就是t

isInterrupted()是Thread内部提供的一个标志位(boolean)

true表示线程应该要结束

false表示线程还不用结束

①代码举例:
public class Demo10 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() ->{
           while(!Thread.currentThread().isInterrupted()){
               System.out.println("Hello Thread");
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });

        t.start();
        Thread.sleep(3000);
        t.interrupt();   //将标志位设为true,此时while条件取反后就是false从而终止t线程
    }
}

②分析代码:

①现象:线程并没有结束,而是持续运行,并且还抛出sleep异常


②原因:t线程正在while循环里的sleep休眠时,被interrupt唤醒,并且唤醒后自动清除前面的标志位,也就导致了isInterrupted()标志位无效,从而不会使线程终止!!!

而对于手动设置标志位,是不会唤醒sleep的!!!


③结论:因此,当线程正在sleep过程中,其他线程调用interrupt方法,就会强制使sleep抛出异常,sleep就被立即唤醒,并且sleep在被唤醒的过程中,会自动清除标志位

例:你设定了sleep1000ms,虽然才过去10ms,没到1000ms,但也会被立即唤醒!


④自动清除标志位的好处:给程序员留下更多的操作空间

操作①-立即结束线程:catch中加个break

操作②-继续做点别的事,一会再结束线程先在catch中执行别的逻辑再break

操作③-忽略唤醒请求,继续执行不加break

七:休眠当前线程-sleep()

(1)sleep()的作用

使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行),具体取决于系统定时器和调度程序的精度和准确性

(2)了解不同操作系统下的sleep

①在Windows上的Sleep休眠单位是毫秒(ms)
②在Linux上的sleep休眠单位是秒(s)
对于Java程序员,会用sleep就行,JVM会根据你用的操作系统来帮你!!!

(3)根据代码理解sleep

①sleep怎么用

★sleep是Thread类的静态方法,直接使用 类名.静态方法名() 即可使用

②sleep抛异常细节

★sleep需要抛出异常!

①重写父类Thread中的run()方法中,必须要使用try-catch抛出异常,否则sleep报错

②main()方法里的sleep就可以直接throws异常

③代码图
class MyThread1 extends Thread{          //我创建一个MyThread1类然后继承java标准库中现场的Thread类
    @Override
    public void run() {                 //重写父类Thread中的run()方法
        while (true) {
            System.out.println("Hello Thread!!!");
            try {                                  //使用try-catch抛出异常,否则sleep报错
                // 注意:这里只能用try-catch,不能用throws,因为此处是方法重写,对于父类run()来说,就没有throws异常这种设定
                Thread.sleep(1000);          //sleep是Thread类的静态方法,直接使用类名.静态方法名()即可使用
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class Demo2 {
    public static void main(String[] args) throws InterruptedException {   //main方法直接throws异常即可
        MyThread1 myThread1 = new MyThread1();    //创建一个MyThread1类型的对象,对象名(引用变量)叫myThread1

        myThread1.start();

        while (true) {
            System.out.println("Hello main!!!");
            Thread.sleep(1000);      //这个sleep就可以直接throws异常
        }
    }
}
④运行结果

1.有了sleep之后确实比较有规律和节奏的交替打印

2.但是这里并非是一个很严格的交替(*可以看到箭头部分有两个Hello Thread*)
原因:这两个线程在进行sleep之后,就会进入堵塞状态,当时间到,系统就会唤醒这俩线程,并且恢复这两个线程的调度,但是当这两个线程都唤醒之后,谁先调度,谁后调度,都可以视为“随机”

 3.系统在进行多个调度的时候,并没有一个明确的顺序,而是按照这种“随机”的方式进行调度,而这种"随机"调度的过程,称为“抢占式执行"

八:等待一个线程-join()

1.概念

等待线程,其实就是规划线程结束的先后顺序

2.方法

public void join()                        //等待线程结束(死等)

public void join(long millis)        //等待线程结束,最大等待毫秒时间(推荐使用)

★希望谁先结束,就 先结束线程.join() 然后用另一个线程调用即可

例如:有A、B两个线程,希望B先结束A后结束,此时就用A线程调用B.join()方法


情况一:如果A执行到B.join()时,此时B线程如果没执行完(所谓的执行完就是run方法执行完毕),A线程就会进入阻塞状态(阻塞:代码停止,不继续往下执行),相当于给B留下执行时间,B执行完毕后,A再从阻塞状态恢复回来,并且往后继续执行


情况二:如果A执行到B.join()时,B已经执行完了,A就不必阻塞,继续往下执行

3.代码举例

①正常情况:

B线程循环5次,A线程循环3次,A线程比B线程先结束

public class Demo11 {
    public static void main(String[] args) {
        Thread B = new Thread(() ->{       //B线程
            for (int i = 0; i < 5; i++) {
                System.out.println("Hello B");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("B结束了");
        });


        Thread A = new Thread(() ->{        //A线程
            for (int i = 0; i < 3; i++) {
                System.out.println("Hello A");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("A结束了");
        });

            B.start();     //启动B线程
            A.start();     //启动A线程
    }
}

②join方法:

我们利用在A线程里面调用B.join方法使B先结束,A后结束

join同样需要抛出异常

public class Demo11 {
    public static void main(String[] args) {
        Thread B = new Thread(() ->{       //B线程
            for (int i = 0; i < 5; i++) {
                System.out.println("Hello B");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("B结束了");
        });


        Thread A = new Thread(() ->{        //A线程
            for (int i = 0; i < 3; i++) {
                System.out.println("Hello A");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            try {
                B.join();      //如果B线程此时没有执行完毕,就会使A线程停止不动,不再往下执行
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("A结束了");
        });

            B.start();     //启动B线程
            A.start();     //启动A线程
    }
}

 4.join与sleep共同特点

join和sleep一样,都需要抛出异常

join和sleep一样,产生阻塞后都可以被interrupt唤醒,唤醒后自动清除标志位

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值