[JavaEE]多线程程序的实现

Author:MTingle
人工智能专业
荃者所以在鱼,得鱼而忘荃;蹄者所以在兔,得兔而忘蹄;言者所以在意,得意而忘 言。吾安得夫忘言之人而与之言哉!


一、第一个多线程程序

package MyThread;

class MyThread extends Thread{
    public void run(){
        System.out.println("hello world");
    }
}


public class ThreadDemo1 {
    public static void main(String[] args) {
        Thread t=new MyThread();
        t.start();

    }
}

下面我们来解答一下上述代码的一些基础问题:

1.为什么Thread类不需要导入类就可以直接使用?而Scanner,ArrayList等就需要.

Java标准库中,有一个特殊的包:java.lang

2.为什么这个类前面没有public?能不能写public?

一个.java文件中,只能有一个public的类,这个类如果没有被public作用于,就只能在当前包里被其他类使用.

3.run方法是什么?

run方法是该线程的入口方法,类似于main方法,main方法是java进程的入口方法,一个进程中至少会有一个线程,这个进程中的第一个线程也就称为"主线程",main方法就是主线程的入口方法,此处的run方法不需要程序员手动调用,会在合适的时机(线程创建好之后),被jvm自动调用执行,类似这种风格的函数也被称为"回调函数"(callback)

引入多线程之后,代码中就可以同时具备多个执行流.

二、多线程的随机调度

class MyThread2 extends Thread{
    public void run() {
        while (true) {
            System.out.println("Hello world");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
public class ThreadDemo2 {
    public static void main(String[] args) {
        Thread t=new MyThread2();
        t.start();
        while (true) {
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

当我们真正运行程序的时候,两个循环都在执行,这两个线程就是两个独立的执行流,调用start方法创建线程之后,兵分两路,一路沿着main方法继续执行,打印 hello main,一路进入到线程的run方法中,打印hello thread,彼此独立,互不干扰

1.当有多个线程的时候,这些线程执行的先后顺序是不确定的!这一点是因为操作系统的内核中,有一个"调度器"模块,这个模块的实现方式是一种类似"随机调度"的效果

2.什么叫做随机调度?

1.一个线程,什么时候被调度到cpu上执行,时机是不确定的

2.一个线程,什么时候从cpu上下来,给被人让位,时机也是不确定的

3.以上的称作抢占式执行,当前的主流操作系统都是抢占式执行

每秒钟sleep到时间唤醒,到底是run先执行还是main先执行是不确定的(随机调度,抢占式执行)

三.线程创建的几种方式

1.继承Thread重写run

class MyThread2 extends Thread{
    public void run() {
        while (true) {
            System.out.println("Hello world");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
public class ThreadDemo2 {
    public static void main(String[] args) {
        Thread t=new MyThread2();
        t.start();
        while (true) {
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

2.实现Runnable接口,重写run

class MyThread3 implements Runnable{    //Runnable表示可执行的
    @Override
    public void run(){
        while (true) {
            System.out.println("hello runnable");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

}
public class ThreadDemo3 {
    public static void main(String[] args) {
      /*  Runnable runnable=new MyThread3();
        Thread t=new Thread(runnable);//要执行的任务和线程解耦合
        t.start();*/ //写法一
        Thread t=new Thread(new MyThread3());
        t.start(); //写法二

        while (true) {
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

Runnable可以理解为“可执行的”,通过这个接口,就可以抽象表示出一段可以被其他实体来执行的代码。上述提供两种可以创建的方法。第一种写法,其实就是把线程和要执行的任务进行解耦合了。

3.继承Thread,重写run,但是使用匿名内部类

public class ThreadDemo4 {
    public static void main(String[] args) {

        Thread t=new Thread(){
            public void run(){
                while (true) {
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };
        t.start();

        while (true) {
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

    }

}

1.什么是内部类?

在一个类里面定义的类。

2.使用匿名内部类的好处?

没有名字是匿名内部类最大的用途,意味着不能重复使用,使用一次就扔了,写{}的意思是要定义一个类,与此同时,这个新的类继承自Thread,此处的{}种可以定义子类的属性和方法,此处最主要的目的是重写run方法。与此同时,这个代码还创建了子类的实例,t指向的实例,并非是单纯的Thread,而是Thread的子类

4.实现Runnable,重写run,匿名内部类

public class ThreadDemo5 {
    public static void main(String[] args) {
        Thread t=new Thread(new Runnable(){
            @Override
            public void run(){
                while (true) {
                    System.out.println("hello Runnable");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        });    //Thread构造方法的参数填写了Runnable的匿名内部类的实例
        t.start();

        while (true) {
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

Thread构造方法的参数填写了Runnable的匿名内部类的实例

5.【常用/推荐】使用lanbda表达式

    public static void main(String[] args) {
        // **
        // lambda表达式创建线程
        Thread t=new Thread(()->{
            while (true) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();

        while (true) {
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

形参列表这里能带参数,线程的入口不需要参数,这个写法相当于实现Runnable重写run,lambda代替了Runnable的位置,方法不能脱离类单独存在,这就导致为了设置回调函数,不得不套上一层类,因此引入了lambda表达式(匿名函数/方法).

四.前台进程和后台进程

前台进程会阻止进程结束,后台进程不会阻止进程结束

public class ThreadDemo7 {
    public static void main(String[] args) {
        Thread t=new Thread(()->{
            while (true) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"这是我的进程");
        //设置为true为后台进程,不设置为前台进程
        t.setDaemon(true);
        t.start();
    }
}

由运行结果可知,当t设置为后台进程时,他不会阻止main线程结束,故不会进行打印.

t.setDaemon(true)(设置为后台进程)

t.setDaemon(false)(默认,前台进程)

五.Runnable和start的区别

public class ThreadDemo10 {
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            System.out.println("hello1");
        });
        Thread t2=new Thread(()->{
            System.out.println("hello2");

        });
        t1.start();
        t2.start();
    }
}
class MyThread4 extends Thread{
    public void run(){
       // System.out.println("hello");
        while (true) {
            System.out.println("hello thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

class MyThread4 extends Thread{
    public void run(){
       // System.out.println("hello");
        while (true) {
            System.out.println("hello thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

public class ThreadDemo11 {
    public static void main(String[] args) {
        Thread t=new MyThread4();
        t.start();
      //  t.run();//这个操作还是在main线程中打印的hello
      //  t.start();//创建一个新的进程,由新的进程来打印hello
        while (true) {
            System.out.println("heelo main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

    }
}

调用start会创建出新的线程,start会调用系统api来完成创建线程的操作

run线程虽然在打印hello,但还是停留在main主线程当中.如果我们在run后面设置一个循环打印,此时他是不会执行的 !

六.终止线程

为了让线程结束,我们需要引入标志位

public class ThreadDemo12 {
    private static boolean isQuit=false;
    public static void main(String[] args) {
        Thread t=new Thread(()->{
           while (!isQuit) {
               System.out.println("我是一个线程,工作中!");
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }
           }
            System.out.println("线程工作完毕!");
        });
        t.start();

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("让t线程结束");
        isQuit=true;

    }
}

如果把isQuit作为main方法中的局部变量是否可行?

答案是不可行!此处涉及一个叫变量捕获的概念,变量捕获,本质上就是把外面的变量当做参数传进来了(参数是隐藏的),这个捕获的变量必须是"final"或者是"事实final",而此处的isQuit不是final也不是事实final,所以就不能把他设置为局部变量,因此必须把他写作成员变量!

那为什么可以把它写成成员变量呢?这是由于java的语法规定!!!  lambda表达式本质上是"函数式接口"(匿名内部类)内部类访问外部类的成员,这件事情本身就是合法的,这个事情就不受到变量捕获的影响了.

为什么java这里对于变量捕获有final限制?

isQuit是局部变量的时候是属于main方法的栈帧,但是Thread lambda是有自己独立的栈帧的,两个栈帧的生命周期不一样这就可能会导致main方法执行结束,栈帧销毁了,同时Thread的栈帧还在,还想继续使用isQuit,而java对于这个问题的解决方式十分粗暴,变量捕获的本质就是传参,换句话说,就是让lambda表达式在自己的栈帧中创建一个新的isQuit,并把外面的isQuit的值拷贝进来,为了避免里外的isQuit值不相同,java干脆就不让程序员对isQuit进行修改.

七.等待线程(thread.join())

多个线程的执行顺序是不确定的,随机调度,抢占式执行,虽然系统底层的调度顺序是无序的,但是可以再应用程序当中通过一些api来影响线程的执行顺序,join就是其中一种可以影响线程结束的先后顺序,比如t2线程等待t1线程结束,此时一定是t1线程先结束,t2线程后结束,join可能会让t2线程进入阻塞状态等待t1线程先结束.

public class TreadDemo14 {
    public static void main(String[] args) {
        Thread t=new Thread(()->{
            for (int i = 0; i < 5; i++) {
                System.out.println("我是线程1,正在工作中!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("线程执行结束");
        });
        Thread t2=new Thread(()->{
            try {
                t.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("这是线程2");
        });
        t.start();
        t2.start();
        try {
            t.join();//只能确定结束顺序
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        try {
            t2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("这是主线程,希望这个日志在t结束后打印");
    }
}

但是如果一个线程t1先在main线程中执行join,t2后在线程中执行join,这能意味着t1会比t2先在main线程中结束吗?

答案是不能,如果是在主线程中join只能保证这个线程比主线程先结束,但是先调用join的线程不一定会比后调用join的线程先结束!!!

public class TreadDemo14 {
    public static void main(String[] args) {
        Thread t=new Thread(()->{
            for (int i = 0; i < 5; i++) {
                System.out.println("我是线程1,正在工作中!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("线程执行结束");
        });
        Thread t2=new Thread(()->{
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("这是线程2");
        });
        t.start();
        t2.start();
        try {
            t.join();//只能确定结束顺序
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        try {
            t2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("这是主线程,希望这个日志在t结束后打印");
    }
}

观察上述代码,线程t比线程t2先调用join,可是t2线程先结束!!!

如果我们先线程t比线程t2先结束,我们可以参考等待线程的第一个代码,在线程t2中加入t.join!

总结一下,在哪个线程中调用join,就是让那个线程等待调用join的线程结束,例如在main线程中调用t.join就是让main线程等待t线程先结束!!!


总结

本文主要介绍了线程的几种创建方式,以及如何终止等待线程,下一篇文章作者会针对线程安全进行详细讨论!!!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值