Java多线程01:多线程编程+线程的常用操作方法

Java高级编程01

本文基于 阿里云大学:Java高级编程 整理记录,仅用于个人学习/交流使用。

一、Java多线程编程

继承Thread类实现多线程

Java里面提供有一个java.lang.Thread的程序类,那么一个类只要继承了此类就表示这个类为线程的主体类,但是并不是说这类就可以直接实现多线程处理了,因为还需要覆写Thread类中提供的一个run()方法 (public void run()),而这个方法就属于线程的主方法。

  • 一个标准的线程主体类
public class MyThread extends Thread{  //线程的主体类
    private String title;
    public MyThread(String title){
        this.title=title;
    }

    @Override
    public void run() { //线程的主体方法
        for (int i = 0; i < 5; i++) {
            System.out.println(this.title+"运行,i="+i);
        }
    }
}

多线程要执行的功能都应该在run()方法中进行定义。

需要说明的是:在正常情况下如果要想使用一个类中的方法,那么肯定要产生实例化对象,而后去调用类中提供的方法,但是run()方法是不能够被直接调用的,因为这里面牵扯到一个操作系统的资源调度问题,所以要想启动多线程必须使用start()方法完成。

  • 错误的调用方法及运行结果
class ThreadDemo01{
    public static void main(String[] args) {
        new MyThread("线程A").run();
        new MyThread("线程B").run();
        new MyThread("线程C").run();
    }
}

/*
线程A运行,i=0
线程A运行,i=1
线程A运行,i=2
线程A运行,i=3
线程A运行,i=4
线程B运行,i=0
线程B运行,i=1
线程B运行,i=2
线程B运行,i=3
线程B运行,i=4
线程C运行,i=0
线程C运行,i=1
线程C运行,i=2
线程C运行,i=3
线程C运行,i=4
 */
  • 正确的调用方法
class ThreadDemo02{
    public static void main(String[] args) {
        new MyThread("线程A").start();
        new MyThread("线程B").start();
        new MyThread("线程C").start();
    }
}

通过此时的调用你可以发现,虽然调用了是start()方法,但是最终执行的是run()方法,并且所有的线程对象都是交替执行的

疑问:为什么多线程的启动不直接使用run()方法而必须使用Thread类中的start()方法呢?。

如果要想清楚这个问题,最好的做法是查看一下 start()方法的实现操作,可以直接通过源代码观察。

  • 程序源码(部分)
public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();  //抛出异常

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
            start0(); //调用了start0
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

    private native void start0(); //只是定义,没有实现

发现在start()方法里面会抛出一个“IlgalThreadStateException”异常类对象,但是整个的程序并没有使用throws或者是明确的try. …catch处理,因为该异常一定 是RuntimeException的子类,每一个线程类的对象只允许启动一次,如果重复启动则就抛出此异常,例如:下面的代码就会抛出异常。

  • 重复线程启动报异常
class ThreadDemo03{
    public static void main(String[] args) {
        MyThread myThread=new MyThread("线程A");
        myThread.start();
        myThread.start();//重复了线程的启动
    }
}

image-20210309180348651

源码中 start0 的声明和调用:

在Java程序执行的过程之中考虑到对于不同层次开发者的需求,所以其支持有本地的操作系统函数调用,而这项技术就被称为JNI (java Native Inteface) 技术,但是Java开发过程之中并不推荐这样使用,利用这项技术可以使用一些操作系统提供底层函数进行一些特殊的处理,而在Thread类里面提供的start0就表示需要将此方法依赖于不同的操作系统实现

image-20210309175558894

任何情况下,只要定义了多线程,多线程的启动永远只有一种方案: Thread 类中的start()方法。

Runnable接口实现多线程

虽然可以通过Thread类的继承来实现多线程的定义,但是在Java程序里面对于继承永远都是存在有单继承局限的,所以在Java里面又提供有第二种多线程的主体定义结构形式:实现java lang Runnable接口,此接口定义如下:

@FunctionalInterface
public interface Runnable{
    public void run();// 从JDK 1.8引入了Lambda表达式之后就变为了函数式接口。
}

但是此时由于不再继承Thread父类了,那么对于此时的MyThread类中也就不再支持有start()这个继承的方法,可是如果不使用Thread.start()方法是无法进行多线程启动的,那么这个时候就需要去观察一下 Thread类所提供的构造方法。

  • Runnable接口实现多线程
public class MyThread implements Runnable{  //线程的主体类
    private String title;
    public MyThread(String title){
        this.title=title;
    }

    @Override
    public void run() { //线程的主体方法
        for (int i = 0; i < 5; i++) {
            System.out.println(this.title+"运行,i="+i);
        }
    }
}

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

        Thread threadA=new Thread(new MyThread("线程A"));
        Thread threadB=new Thread(new MyThread("线程B"));
        Thread threadC=new Thread(new MyThread("线程C"));
        threadA.start();
        threadB.start();
        threadC.start();

    }
}

这个时候的多线程实现里面可以发现,由于只是实现了Runnable 接口对象,所以此时线程主体类上就不再有单继承局限了,那么这样的设计才是一个标准型的设计。

函数式接口定义,所以也可以直接利用Lambda表达式进行线程类实现。

  • Lambda表达式进行线程类实现
class ThreadDemo05{
    public static void main(String[] args) {
        for (int x = 0; x < 3; x++) {
            String title="线程对象--"+x;
            Runnable run=()->{
                for (int y = 0; y <10 ; y++) {
                    System.out.println(title+"运行,y="+y);
                }
            };
            new Thread(run).start();

        }
    }
}
  • 优化Lambda表达式进行线程类实现
class ThreadDemo06{
    public static void main(String[] args) {
        for (int x = 0; x < 3; x++) {
            String title="线程对象--"+x;
            new Thread(()->{
                for (int y = 0; y <10 ; y++) {
                    System.out.println(title+"运行,y="+y);
                }
            }).start();
        }
    }
}

在以后的开发之中对于多线程的实现,优先考虑的就是Runnable接口实现,并且永恒都是通过Thread实例调用start方法。

Thread与Runnable关系

经过一系列的分析之后可以发现,在多线程的实现过程之中已经有了两种做法: Thread 类Runnable接口

如果从代码的结构本身来讲肯定使用Runnable是最方便的,因为其可以避免单继承的局限,同时也可以更好的进行功能的扩充

但是从结构上也需要来观察Thread与Runnable的联系,打开Thread类的定义:

public class Thread extends object implements Runnable {}

发现现在Thread类也是Runnable接口的子类,那么在之前继承Thread类的时候实际上覆写的还是Runnable接口的run()方法,于是此时来观察一下程序的类结构。

image-20210309184127806

多线程的设计之中,使用了代理设计模式的结构,用户自定义的线程主体只是负责项目核心功能的实现,而所有的辅助功能的实现全部交由Thread类来处理。

在进行Thread 启动多线程的时候调用的是start()方法,而后找到的是run()方法, 但通过Thread 类的构造方法传递了一个Runnable接口对象的时候,那么该接口对象将被Thread类中的target属性所保存,在start()方法执行的时候会调用Thread类中的run()方法,而这个run()方法去调用Runnable接口子类被覆写过的run()方法。

image-20210309185326012

多线程开发的本质实质上是在于多个线程可以进行同一资源的抢占,那么Thread主要描述的是线程,而资源的描述是通过Runnable完成的。

Callable接口实现多线程

从最传统的开发来讲如果要进行多线程的实现肯定依靠的就是Runnable,但是Runnable接口有一个缺点:当线程执行完毕之后无法获取一个返回值,所以从JDK 1.5 之后就提出了一个新的线程实现接口: java.util.concurent.Callable 接口,首先来观察这个接口的定义:

@FunctionalInterface
public interface Callable<V>{
    public V call throws Exception;
}
  • Thread / Runnable / Callable之间的关系

image-20210309191420211


  • Callable接口实现多线程
public class MyThread implements Callable<String> {  //线程的主体类
    @Override
    public String call() throws Exception { //线程的主体方法
        for (int i = 0; i < 5; i++) {
            System.out.println("运行,i="+i);
        }
        return "程序执行完毕";
    }
}


class ThreadDemo07{
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<String> task=new FutureTask<>(new MyThread());
        new Thread(task).start();
        System.out.println("线程返回数据:"+task.get());
    }
}

面试题:请解释Runnable与Callable的区别?.

  • Runnable是在JDK1.0的时候提出的多线程的实现接口,而Callable是在JDK1.5之后提出的
  • java.lang ,Runnable接口之中只提供有-一个run(方法,并且没有返回值
  • java.util.concurrent.Callable 接口提供有call0)方法,可以有返回值

线程运行状态

对于多线程的开发而言,编写程序的过程之中总是按照:定义线程主体类,而后通过Thread类进行线程的启动,但是并不味着你调用了start()方法,线程就已经开始运行了,因为整体的线程处理有自己的一套运行的状态。

image-20210309193104690

1、任何一个线程的对象都应该使用Thread类进行封装,所以线程的启动使用的是start(), 但是启动的时候实际上若干个线程都将进入到一种就绪状态,现在并没有执行;

2、进入到就绪状态之后就需要等待进行资源调度,当某一个线程调度成功之后则进入到运行状态(run()方法), 但是所有的线程不可能一致持续执行下去, 中间需要产生一些暂停的状态, 例如:某个线程执行一段时间之后就需要让出资源,而后这个线程就将进入到阻塞状态,随后重新回归到就绪状态;

3、当 run方法执行完毕之后,实际上该线程的主要任务也就结束了,那么此时就可以直接进入到停止状态

二、线程常用操作方法

线程的命名和取得

多线程的运行状态是不确定的,那么在程序的开发之中为了可以获取到一些需要使用到线程就只能够依靠线程的名字来进行操作。所以线程的名字是一个至关重要的概念,这样在Thread类之中就提供有线程名稠的处理。

  • 构造方法:public Thread(Runnable target, String name);
  • 设置名字:public final void setName(String name);
  • 取得名字:public final String getName();

对于线程对象的获得是不可能只是依靠一个this 来完成的,因为线程的状态不可控,但是有一点是明确的,所有的线程对象定要执行run()方法,那么这个时候可以考虑获取当前线程,在Thread类里面提供有获取当前线程的方法

  • 获取当前线程:public static Thread currentThread();

  • 范例:观察线程的命名操作

public class MyThread implements Runnable {  //线程的主体类
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}


class ThreadDemo08{
    public static void main(String[] args)  {
        MyThread myThread=new MyThread();
        new Thread(myThread,"线程A").start();
        new Thread(myThread,"线程B").start();
        new Thread(myThread).start();
        new Thread(myThread).start();
        new Thread(myThread).start();
    }
}

/*
线程A
线程B
Thread-0
Thread-2
Thread-1
*/

设置了名字的时候就使用设置的名字,而如果没有设置名字,则会自动生成一个不重复的名字。

这种自动的属性命名主要是依靠了static属性完成的,在Thread类里面定义有如下操作:

image-20210310101431252

  • 主方法也是一个线程
public class MyThread implements Runnable {  //线程的主体类
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}


class ThreadDemo09{
    public static void main(String[] args)  {
        MyThread myThread=new MyThread();
        new Thread(myThread,"线程A").start();
        myThread.run();
    }
}

/*
main
线程A
*/

通过此时的代码可以发现当使用了“mt.run()”直接在主方法之中调用线程类对象中的 run()方法所获得的线程对象的名字为“mian”,所以可以得出一个结论:主方法也是一个线程

那么现在的问题来了,所有的线程都是在进程上的划分,那么进程在哪里?

每当使用java 命令执行程序的时候就表示启动了一个JVM的进程,一台电脑上可以同时启动若干个JVM进程,所以每一个JVM进程都会有各自的线程。

image-20210310101802601

在任何的开发之中,主线程可以创建若干个子线程,创建子线程的目的是可以将一些复杂逻辑或者比较耗时的逻辑交由子线程处理。

class ThreadDemo10{
    public static void main(String[] args)  {
        System.out.println("执行任务1");
        new Thread(()->{ //子线程负责统计
            int temp=0;
            for (int i = 0; i <Integer.MAX_VALUE ; i++) {
                temp+=i;
            }
        }).start();
        System.out.println("执行任务2");
        System.out.println("执行任务3");
    }
}

主线程负责处理整体流程,而子线程负责处理耗时操作。

线程休眠

如果说现在希望某一个线程可以暂缓执行,那么就可以使用休眠的处理,在Thread 类之中定义的休眠方法如下:

  • 休眠: public static void sleep(long millis) throws InterruptedException;

  • 休眠: public static void sleep(long millis,int nanos) throws InterruptedException;.

  • 在进行休眠的时候有可能会产生中断异常“InterruptedExeeption”。 中断异常属于Exeeption的子类,所以证明该异常必须处理

休眠的主要特点是可以自动实现线程的唤醒,以继续进行后续的处理。但是需要注意的是,如果现在有多个线程对象,那么休眠也是有先后顺序的。

  • 范例:产生多个线程对象进行休眠处理.
class ThreadDemo10 {
    public static void main(String[] args) {
        Runnable run = () -> {
            for (int x = 0; x < 10; x++) {
                System.out.println(Thread.currentThread().getName() + "、x=" + x);
                try {
                    Thread.sleep(100);//暂缓执行
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        for (int i = 0; i < 5; i++) {
            new Thread(run, "线程对象-" + i).start();
        }
    }
}

/*
线程对象-0、x=0
线程对象-4、x=0
线程对象-3、x=0
线程对象-2、x=0
线程对象-1、x=0
线程对象-4、x=1
线程对象-2、x=1
线程对象-3、x=1
......
*/

此时将产生五个线程对象,并且这五个线程对象执行的方法体是相同的。此时从程序执行的感觉上来讲好像是若千个线程一起进行了休眠,而后一起进行了自动唤醒,但是实际上是有差别的。

image-20210310103131279

线程中断

在之前发现线程的休眠里面提供有一个中断异常,实际上就证明线程的休眠是可以被打断的,而这种打断肯定是由其它线程完成的,在 Thread 类里面提供有这种中断执行的处理方法:。

  • 判断线程是否被中断: public boolean isInterrupted();

  • 中断线程执行:public void interrupt():


  • 范例:观察线程的中断处理操作
class ThreadDemo11 {
    public static void main(String[] args) throws InterruptedException {

        Thread thread=new Thread(()->{
            System.out.println("该睡觉了");
            try {
                Thread.sleep(10000);//睡十秒
                System.out.println("睡够了");
            } catch (InterruptedException e) {
                System.out.println("谁打扰了我的睡眠");
                e.printStackTrace();
            }
        });

        thread.start();//开始睡
        Thread.sleep(1000);//主线程一秒后来到这里
        if (!thread.isInterrupted()){
            //thread.isInterrupted()==true 表示中断了
            //thread.isInterrupted()==false 表示未中断
            System.out.println("主线程:我要打断他的睡眠");
            thread.interrupt();//中断执行
        }


    }
}

/*
该睡觉了
主线程:我要打断他的睡眠
谁打扰了我的睡眠
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.lut.JavaPlus.ThreadDemo10.lambda$main$0(MyThread.java:21)
	at java.lang.Thread.run(Thread.java:745)

*/

所有正在执行的线程都是可以被中断的,中断线程必须进行异常的处理。

线程强制运行

所谓的线程的强制执行指的是当满足于某些条件之后,某一个线程对象将可以一直独占资源,一直到该线程的程序执行结束。

  • 范例:观察一个没有强制执行的程序
class ThreadDemo12 {
    public static void main(String[] args) throws InterruptedException {

       Thread thread=new Thread(()->{
           for (int i = 0; i <100 ; i++) {
               System.out.println(Thread.currentThread().getName()+"执行、i="+i);
           }
       },"玩耍的线程");

       thread.start();

        for (int i = 0; i < 100; i++) {
            System.out.println("[霸道的线程], i="+i );
        }


    }
}

这个时候主线程和子线程都在交替执行着,但是如果说现在你希望主线程独占执行。那么就可以利用Thread类中的方法

  • 强制执行: public final void join() throws InterruptedException;

  • 范例:观察一个有强制执行的程序
class ThreadDemo13 {
    public static void main(String[] args) throws InterruptedException {
        Thread mainthread=Thread.currentThread();
        Thread thread=new Thread(()->{
           for (int i = 0; i <100 ; i++) {

               if (i==3){//霸道线程来了
                   try {
                       mainthread.join();//霸道线程要先执行
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               }

               System.out.println(Thread.currentThread().getName()+"执行、i="+i);
           }
       },"玩耍的线程");

       thread.start();

        for (int i = 0; i < 100; i++) {
            System.out.println("[霸道的线程], i="+i );
        }


    }
}

/*
[霸道的线程], i=0
玩耍的线程执行、i=0
玩耍的线程执行、i=1
玩耍的线程执行、i=2
[霸道的线程], i=1
[霸道的线程], i=2
....
[霸道的线程], i=99
玩耍的线程执行、i=3
玩耍的线程执行、i=4
玩耍的线程执行、i=5
玩耍的线程执行、i=6
...

*/

在进行线程强制执行的时候一定要获取强制执行线程对象之后才可以执行 join()调用。

线程礼让

线程的礼让指的是先将资源让出去让别的线程先执行。线程的礼让可以使用Thread中提供的方法:

  • 礼让: public static void yield()
  • 范例:使用礼让操作
class ThreadDemo13 {
    public static void main(String[] args) throws InterruptedException {
       Thread thread=new Thread(()->{
           for (int i = 0; i <100 ; i++) {
               if (i%3==0){
                   Thread.yield();//线程礼让
                   System.out.println("***玩耍的线程礼让了资源***");
               }
               System.out.println(Thread.currentThread().getName()+"执行、i="+i);
           }
       },"玩耍的线程");

       thread.start();

        for (int i = 0; i < 100; i++) {
            System.out.println("[霸道的线程], i="+i );
        }


    }
}

礼让执行的时候每一次调用yield()方法都只会礼让一次当前的资源。

线程优先级

理论上来讲:线程的优先级越高越有可能先执行(越有可能先抢占到资源)。

在Thread类里面针对于优先级的操作提供有的两个处理方法:

设置优先级:public final void setPriority(intnewPriority)

获取优先级:public final int getPriority()

在进行优先级定义的时候都是通过int型的数字来完成的,而对于此数字的选择在Thraad类里而就定义有3个常量

  • 最高优先级: public static final int MAX_PRIORITY(值为10)

  • 中等优先级: public static final int NORM_PRIORITY(值为5)

  • 最低优先级: public static final int MIN_PRIORITY(值为1)

范例:观察优先级

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

        Runnable run = () -> {
            for (int x = 0; x < 10; x++) {
                System.out.println(Thread.currentThread().getName() + "、x=" + x);
            }
        };

        Thread threadA=new Thread(run,"线程A");
        Thread threadB=new Thread(run,"线程B");
        Thread threadC=new Thread(run,"线程C");
        threadA.setPriority(Thread.MIN_PRIORITY);
        threadB.setPriority(Thread.MIN_PRIORITY);
        threadC.setPriority(Thread.MAX_PRIORITY);
        threadA.start();
        threadB.start();
        threadC.start();

    }
}

主方法是一个主线程,那么线程的优先级呢?

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

/*
5
5
*/

主线程是属于中等优先级

而默认创建的线程也是中等优先级

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值