Java高级编程——多线程(基本概念+线程的创建及应用)

1、基本概念:程序、进程、线程

1.1程序(program)

是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象

1.2进程(process)

进程是程序执行的一次执行过程,或是正在允运行的一个程序。是一个动态的过程:有它自身的产生、存在、消亡的过程。——生命周期

1.3线程(thread)

进程可进一步细分为线程,是一个程序内部的一条执行路径
若一个进程同一时间并行执行多个线程,就是支持多线程的
线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),多个线程,共享同一个进程中的结构(方法区、堆) 线程切换的开销小
一个进程的多个线程共享相同的内存单元/内存地址空间->他们从同一对堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全隐患

一个Java应用程序java.exe至少有三个进程:main(主线程),gc(垃圾回收线程),异常处理线程。如果发生异常会影响主线程

1.4并行与并发

并行

多个CPU同时执行多任务.比如:多个人同时做不同事

并发

一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做一件事

1.5使用多线程的优点

①提高应用程序的响应。对图形化界面更有意义,可增强用户体验
②提高计算机系统CPU的利用率
③改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改

1.6多线程的应用场景

①程序要同时执行两个或多个任务
②程序需要实现一些需要等待的任务时,如用户输入、文件读写、网络操作、搜索等
③需要一些后台运行的程序时

2、线程的创建和应用(重点)

2.1创建多线程的方式一:java.lang.Thread

2.1.1通过继承Thread创建线程

步骤:

/**
 * 多线程的创建
 * 方式一:继承Thread类
 *      1.创建一个继承于Thread类的子类
 *      2.重写run()方法
 *      3.创建子类对象
 *      4.通过此对象带调用start()
 */
public class ThreadTest{
 //例一遍历一千以内所有偶数
    public static void main(String[] args) {
		//3.创建子类对象
        MyThread t1 = new MyThread();
        //4.通过此对象带调用start()
        t1.start();
               for (int i = 0; i < 100; i++) {
            if ((i % 2) == 0 ) {
                System.out.println("-----主线程----"+i);
            }
        }
    }
}

//1.创建一个继承于Thread类的子类
class MyThread extends Thread{
	 //2.重写run()方法
    @Override
    public void run() {
        //将创建的线程要做的操作声明在run方法中
        for (int i = 0; i < 1000; i++) {
            if ((i % 2) == 0 ) {
                System.out.println(i);
            }
        }
    }
}

结果说明:此代码中存在两个线程,主线程和我们自己创建的线程,这两个线程时同时执行的,所有输出结果没有先后
截取部分输出:

...
main:-----主线程----0
main:-----主线程----2
main:-----主线程----4
main:-----主线程----6
Thread-0:0
Thread-0:2
Thread-0:4
Thread-0:6
Thread-0:8
Thread-0:10
Thread-0:12
Thread-0:14
Thread-0:16
main:-----主线程----8
Thread-0:18
Thread-0:20
Thread-0:22
Thread-0:24
main:-----主线程----10
main:-----主线程----12
main:-----主线程----14
main:-----主线程----16
...
Process finished with exit code 0
2.1.2创建过程中的两个问题
2.1.2.1问题一:不能直接调用run()方法来执行线程

1、start()方法的作用

①启动当前线程
②调用当前线程的run()方法

2、使用start()和直接调用run()方法的区别
上述代码直接调用run()方法的输出

...
main:982
main:984
main:986
main:988
main:990
main:992
main:994
main:996
main:998
main:-----主线程----0
main:-----主线程----2
main:-----主线程----4
main:-----主线程----6
main:-----主线程----8
main:-----主线程----10
main:-----主线程----12
main:-----主线程----14
main:-----主线程----16
main:-----主线程----18
main:-----主线程----20
...
Process finished with exit code 0

无论执行多少次,两个循环的执行结果输出都是固定的先后顺序,且两个循环都属于main线程。这说明,直接调用run方法,并没有成功启动Thread线程,这里只相当于是一个对象去调用了一个方法。

2.1.2.2 问题二:不能对同一线程执行两次start()

示例代码

    public static void main(String[] args) {

        MyThread t1 = new MyThread();
        t1.start();
        t1.start();
    }

运行结果,异常:IllegalThreadStateException

Exception in thread "main" java.lang.IllegalThreadStateException
	at java.lang.Thread.start(Thread.java:708)
	at com.guigu.java20210927Thread.ThreadTest.main(ThreadTest.java:19)
Thread-0:0
Thread-0:2

解决:创建线程的新对象,并start()

    public static void main(String[] args) {

        MyThread t1 = new MyThread();
        t1.start();
        MyThread t2 = new MyThread();
        t2.start();
    }

输出

Thread-1:62
Thread-1:64
Thread-0:90
Thread-1:66
Thread-1:68
Thread-1:70
Thread-1:72
Thread-1:74
Thread-1:76
Thread-1:78
Thread-0:92
Thread-0:94
Thread-0:96
Thread-1:80
Thread-1:82
Thread-1:84
Thread-1:86
2.1.3 创建Thread的匿名子类

例:创建两个线程分别求100以内的奇数和偶数

public class ThreadTest {

    public static void main(String[] args) {
    //方法一、创建两个Thread的子类
//        Thread1 t1 = new Thread1();
//        Thread2 t2 = new Thread2();
//
//        t1.start();
//        t2.start();
        //方法二、创建Thread类的匿名子类
        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    if ((i % 2) == 0 ) {
                        continue;
                    }
                    System.out.println(Thread.currentThread().getName()+":"+i);
                }
            }
        }.start();

        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    if ((i % 2) != 0 ) {
                        continue;
                    }
                    System.out.println(Thread.currentThread().getName()+":"+i);
                }
            }
        }.start();
    }
}

class Thread1 extends Thread
{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if ((i % 2) == 0 ) {
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }
    }
}


class Thread2 extends Thread
{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if ((i % 2) == 0 ) {
                continue;
            }
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }

}
2.1.4Thread的常用方法

------void start():启动线程,并执行对应的run方法
------run():线程在被调度时执行的操作,通常需要重写
------String getName():返回该线程的名称
------void setName(String name):设置该线程名称
------static Thread currentThread():返回当前线程。在Thread子类中就是this,通常用于主线程和Runnable

setName()、getName、start、run、currentThread()
    public static void main(String[] args) {
        //创建Thread类的匿名子类
        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    if ((i % 2) == 0 ) {
                        continue;
                    }
                    //设置线程名称
                    setName("匿名线程一");
                    //获取线程名称
                    System.out.println(Thread.currentThread().getName()+":"+i);
                }
            }
        }.start();
    }

输出:
在这里插入图片描述

补充:通过构造器命名线程

public class ThreadMethodTest {

    public static void main(String[] args) {
    //创建对象时调用带参构造器给线程命名
        Thread3 t3 = new Thread3("构造器命名的线程");
        t3.start();
        
	//给主线程命名
	Thread.currentThread().setName("主线程");
	
    }
    
}

class Thread3 extends Thread{

    Thread3 (){
    }
    
//建造带参构造器
    Thread3 (String name){
        super(name);
    }
    
    //重写run方法
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if ((i % 2) == 0 ) {
                continue;
            }
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
    
}

输出
在这里插入图片描述

yield():释放当前CPU的执行权
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if ((i % 2) == 0 ) {
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
            if(i%20 == 0){
                //当i%20=0时释放当前线程占用的CPU
                this.yield();
                //另一种写法:Thread.currentThread().yield();
            }
        }
    }

说明:虽然当前线程使用yield(释放了当前占用的CPU但,该线程仍可能立刻再次被分配到CPU
在这里插入图片描述

join():释放当前CPU的执行权

在线程a中调用线程b的join方法,此时线程a进入阻塞状态。直到线程b执行完以后,线程a才结束阻塞状态,等待CPU分配资源继续执行

public class ThreadMethodTest {

    public static void main(String[] args) {

        Thread3 t3 = new Thread3("构造器命名的线程");
        t3.start();
        Thread.currentThread().setName("主线程");
        for (int i = 0; i < 100; i++) {
            if ((i % 2) == 0 ) {
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
            if (i == 20) {
                try {
                //此处在主线程中调用t3线程的join方法,则开始执行t3线程直到该线程结束继续执行主线程
                    t3.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

    }

}


class Thread3 extends Thread{

    Thread3 (){

    }

    Thread3 (String name){
        super(name);
    }
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if ((i % 2) == 0 ) {
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
            if(i%20 == 0){

                this.yield();
                //Thread.currentThread().yield();
            }
        }
    }
}

输出:
开始执行t3
在这里插入图片描述t3线程执行完毕继续执行主线程
在这里插入图片描述

sleep(long millitime):当前线程阻塞指定的时间
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if ((i % 2) == 0 ) {
                try {
                    //一旦线程执行到此处,当前线程阻塞1秒
                    sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
            if(i%20 == 0){

                this.yield();
                //Thread.currentThread().yield();
            }
        }
    }
isAlive():判断当前线程是否存活,线程结束返回false
public class ThreadMethodTest {

    public static void main(String[] args) {

        Thread3 t3 = new Thread3("构造器命名的线程");
        t3.start();
        Thread.currentThread().setName("主线程");
        for (int i = 0; i < 100; i++) {
            if ((i % 2) == 0 ) {
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
            if (i == 20) {
                try {
                    t3.join();

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        //此处t3线程已经执行结束,应返回false
        System.out.println(t3.isAlive());

    }

}


class Thread3 extends Thread{

    Thread3 (){

    }

    Thread3 (String name){
        super(name);
    }
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if ((i % 2) == 0 ) {
                try {
                    //一旦线程执行到此处,当前线程阻塞1秒
                    sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
            if(i%20 == 0){
                //此处当前线程正在执行,每一次的返回都是true
                System.out.println(this.isAlive());
                this.yield();
                //Thread.currentThread().yield();
            }
        }
    }
}

输出1:执行过程中,返回true
在这里插入图片描述

输出情况2:当前线程执行完成,返回false
在这里插入图片描述

2.2 线程的调度

2.2.1 调度的策略

  • 时间片策略:
  • 抢占式:优先级高的线程抢占CPU

2.2.1 Java的调度方法

同优先级线程组成先进先出队列(先到先服务),使用时间片策略
对高优先级,使用优先调度的抢占式策略

2.2.1 Java线程的优先级

三个常量 MAX_PRIORITY:10(最大优先级)、MIN_PRIORITY:1(最小优先级)、NORM_PRIORITY(默认优先级):5
查看优先级和设置优先级
  • getPriority():返回线程优先级
public class ThreadMethodTest {

    public static void main(String[] args) {

        Thread3 t3 = new Thread3("构造器命名的线程");
        Thread4 t4 = new Thread4();
        
        t3.start();
        t4.start();

    }

}


class Thread4 extends  Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
        //获取当前线程的优先级
            System.out.println("优先级:"+getPriority());
        }
    }
}


class Thread3 extends Thread{

    Thread3 (){

    }

    Thread3 (String name){
        super(name);
    }
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if ((i % 2) == 0 ) {
            //获取当前线程的优先级
                System.out.println(getName()+":"+i+"优先级:"+getPriority());
            }
            if(i%20 == 0){
                this.yield();
            }
        }
    }
}

输出1:

...
构造器命名的线程:58优先级:5
构造器命名的线程:60优先级:5
优先级:5
优先级:5
优先级:5
构造器命名的线程:62优先级:5
构造器命名的线程:64优先级:5
构造器命名的线程:66优先级:5
构造器命名的线程:68优先级:5
构造器命名的线程:70优先级:5
构造器命名的线程:72优先级:5
优先级:5
优先级:5
构造器命名的线程:74优先级:5
构造器命名的线程:76优先级:5
...

主线程的优先级

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getPriority());
    }
5

Process finished with exit code 0
  • setPriority(int newPriority):改变线程优先级
        Thread4 t4 = new Thread4();
        System.out.println(t3.getPriority());
        t3.setPriority(Thread.MAX_PRIORITY);
        System.out.println(t3.getPriority());

输出

5
10

Process finished with exit code 0
  • 不一定优先级高的线程一定比优先级低的线程先执行
    public static void main(String[] args) {

        Thread3 t3 = new Thread3("构造器命名的线程");
        //修改线程优先级
        t3.setPriority(Thread.MAX_PRIORITY);
        t3.start();
     Thread.currentThread().setName("主线程");
       for (int i = 0; i < 100; i++) {
            if ((i % 2) == 0 ) {
                System.out.println(Thread.currentThread().getName()+":"+"优先级"+Thread.currentThread().getPriority());
            }
        }

    }

输出:

...
主线程:优先级5
主线程:优先级5
主线程:优先级5
构造器命名的线程:优先级:10
构造器命名的线程:优先级:10
主线程:优先级5
主线程:优先级5
主线程:优先级5
构造器命名的线程:优先级:10
构造器命名的线程:优先级:10
构造器命名的线程:优先级:10
构造器命名的线程:优先级:10
构造器命名的线程:优先级:10
构造器命名的线程:优先级:10
...

Process finished with exit code 0

说明:线程创建时继承父线程的优先级
低优先级只是获得调度的概率降低,并非一定实在高优先级线程之后才被执行

多窗口售票的例子——继承Thread类实现
public class TicketWidowTest {

    public static void main(String[] args) {


        Window w1 = new Window("窗口1");
        Window w2 = new Window("窗口2");
        Window w3 = new Window("窗口3");

        w1.start();
        w2.start();
        w3.start();

    }
}


class Window extends  Thread{

    private static int ticket = 100;

    //构造器给线程命名
    Window(){}

    Window(String name){
        super(name);
    }
    @Override
    public void run() {
/*        for (;ticket>0;){
            ticket--;
            System.out.println(getName()+"窗口售出一张票,当前余票"+(ticket));
        }*/
        while (true){
            if (ticket > 0) {
                ticket--;
                System.out.println(getName()+"窗口售出一张票,当前余票"+(ticket));
            }else break;
        }
    }
}

输出:

...
窗口2窗口售出一张票,当前余票77
窗口1窗口售出一张票,当前余票57
窗口3窗口售出一张票,当前余票68
窗口3窗口售出一张票,当前余票54
窗口3窗口售出一张票,当前余票53
窗口1窗口售出一张票,当前余票55
窗口1窗口售出一张票,当前余票51
窗口1窗口售出一张票,当前余票50
窗口1窗口售出一张票,当前余票49
窗口1窗口售出一张票,当前余票48
窗口2窗口售出一张票,当前余票56
窗口1窗口售出一张票,当前余票47
窗口3窗口售出一张票,当前余票52
窗口3窗口售出一张票,当前余票44
窗口3窗口售出一张票,当前余票43
...
Process finished with exit code 0

2.3创建多线程的方式二:实现Runnable接口

上部分多窗口售票的代码使用static修饰 ticket 属性,以此来保证三个对象共用一个属性,还有一种创建多线程的方式可以替代static达到同样的目的。

  • 1、创建一个实现了Runnable接口的类
  • 2、实现类去实现Runnable中的抽象方法 run()
  • 3、创建实现类的对象
  • 4、将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
  • 5、通过Thread类的对象调用start方法
public class ThreadTest1 {
    public static void main(String[] args) {
        MThread mThread = new MThread();
        Thread t1 =  new Thread(mThread);
        t1.start();
    }
}

class MThread implements  Runnable{


    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if ((i % 2) == 0 ) {
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }
    }
}
多窗口售票的例子——实现Runnable接口实现
/**
 * 通过实现Runnable接口实现多窗口售票
 * @author fuyaling
 * @date 2021-08-28 - 20:25
 */
public class TicketWindowTest1 {

    public static void main(String[] args) {
        Window1 window1 = new Window1();
        Thread t1 = new Thread(window1);
        Thread t2 = new Thread(window1);
        Thread t3 = new Thread(window1);

        t1.setName("1号窗口");
        t2.setName("2号窗口");
        t3.setName("3号窗口");

        t1.start();
        t2.start();
        t3.start();
    }
}



class Window1 implements  Runnable{
    private static int ticket = 100;
    @Override
    public void run() {
        while (true){
            if (ticket > 0) {
                ticket--;
                System.out.println(Thread.currentThread().getName()+"窗口售出一张票,当前余票"+(ticket));

            }else break;

        }
    }
}

说明:此时的 ticket 变量没有使用static修饰,但由于从始至终都只建了一个关于Window1的对象,所以自然而然,对象t1,t2,t3都共享此变量

匿名内部类的创建方式

Runnable target  = new Runnable(){
	@Override
	public void run(){
	...//具体的执行内容
	}
}

//将任务对象交给Thread处理
Threa t = new Thread(target);
//启动i线程
t.start();

将上述代码进一步简化

//将任务对象交给Thread处理
 new Thread(new Runnable(){
	@Override
	public void run(){
	for(;;){...};
	}
}).start();

Runnable是函数式接口,所以再结合lambada表达式进一步简化

//将任务对象交给Thread处理
 new Thread(()->{
	...;//具体的执行内容
}).start();

Runnable的缺点

  • 如果线程由执行结果无法直接返回

2.4两种创建方式的对比

2.4.1开发中优先选择实现接口Runnable的方式去创建线程

原因:

  • ①实现的方式没有类的单继承性的局限性
  • ②实现的方式更适合来处理多个线程又共享数据的情况。

2.4.2联系

public class Thread implements Runnable

2.4.3 相同点

两种方式都需要重写run方法,将线程执行的逻辑声明在run()方法中

2.5创建多线程的方式三:实现Callable接口 JDK5新增

上述线程创建方式的不足:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值