Java多线程开发--多线程起步 Java多线程-线程状态的转换

内容学习于:edu.aliyun.com


具体内容:

  所有的Java程序的执行是需要通过一个主方法完成的,主方法会作为程序的起点,但是如果要进行多线程的编程也需要有一个线程的起点结构,此结构就成为线程类,那么所有的线程类都是有继承要求的,可以有三种实现模式,继承Thread类、实现Runnable接口、实现Callable接口。
在DOS系统的时代,其本身有一个特征:如果你的电脑上出现了病毒,那么所有的程序将无法执行,因为传统的DOS采用的是单进程处理,而单进程处理的最大特点:在同一个时间段上只允许一个程序在执行。Windows则采用的多进程处理。如下图:在这里插入图片描述

1. 继承Thread类实现多继承

  Java.lang.Thread是由系统定义的一个线程处理类,任何子类只需要继承此类就可以得到一个线程的处理能力,在继承的同时一定要覆写run()方法,那么该方法将作为一个线程启动的主方法存在。
代码:

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

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

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

代码:

例子1:

public class Demo1 {
    public static void main(String[] args) {
        //run()是顺序执行
        new MyThread("线程A").run();
        new MyThread("线程B").run();
        new MyThread("线程C").run();
    }
}

结果如下图所示:
在这里插入图片描述

例子2:

public class Demo1 {
    public static void main(String[] args) {
        //start()是交替执行
        new MyThread("线程A").start();
        new MyThread("线程B").start();
        new MyThread("线程C").start();
    }
}

  通过此时的调用你可以发现,虽然调用了是start()方法,但是最终执行的run()方法,并且所有的线程对象都是交替执行的。
  疑问?为什么多线程的启动不直接使用run()方法而必须使用Thread类中的start()方法呢?
  如果要想清楚这个问题,最好的做法是查看一下start()方法的实现操作,可以直接通过源代码观察。

代码:

//start()源代码分析
public synchronized void start() {
        if (threadStatus != 0)//判断线程的状态
            throw new IllegalThreadStateException();//抛出一个异常

        group.add(this);

        boolean started = false;
        try {
            start0();//执行了该方法
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {

            }
        }
    }
//只定义了方法名称,但没有实现,同时需要注意native关键字
private native void start0();

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

代码:

public class Demo1 {
    public static void main(String[] args) {
        MyThread mt = new MyThread("线程A");
        mt.start();
        mt.start();//重复进行线程的启动
    }
}
  • 执行结果:Exception in thread “main” java.lang.IllegalThreadStateException

  那么为什么必须要用start()方法启动多线程呢?
  在Java程序执行的过程之中考虑到对于不同层次开发者的需求,所以其支持有本地的操作系统函数调用,而这项技术就被称为JNI (Java Native Inteface) 技术,但是Java开发过程之中并不推荐这样使用,利用这项技术可以使用一些操作系统提供底层函数进行一些特殊处理,而在Thread类里面提供的start0()就表示需要将此方法依赖于不同的操作系统实现。 如下图:在这里插入图片描述
  任何情况下,只要定义了多线程,多线程的启动永远只有一种方案: Thread 类中的start()方法。

2. 基于Runnable接口实现多线程

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

  • @FunctionalInterface
    public interface Runnable{//从JDK1.8引入Lambda表达式后就变成了函数式的接口
    }

代码:

例子:

//通过接口实现线程主题类
class MyThread implements Runnable {//线程的主体类
    private String title;

    public MyThread(String title) {
        this.title = title;
    }

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

    }
}

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

  • 构造方法:public Thread(Runnable target)

代码:

//调用多线程
public class Demo1 {
    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 接口对象,所以此时线程主体类上就不再有单继承局限了,那么这样的设计才是一个标准型的设计。可以发现从JDK 1.8开始,Runnable 接口使用了函数式接口定义,所以也可以直接利用Lambda表达式进行线程类实现。

代码:

例子1:

//利用lambda表达式实现(传统):
public class Demo1 {
    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();
        }

    }
}

例子2:

//利用lambda表达式实现(简化):
public class Demo1 {
    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类对象启动多线程。

3. Thread和Runnable的关系

  经过一系列的分析之后可以发现,在多线程的实现过程之中已经有了两种做法: Thread 类、Runnable 接口,如果从代码的结构本身来讲肯定使用Runnable 是最方便的,因为其可以避免单继承的局限,同时也可以更好的进行功能的扩充。但是从结构上也需要来观察Thread与Runnable的联系,打开Thread类的定义:

  • public class Thread implements Runnable

  发现现在Thread类也是Runnable接口的子类,那么在之前继承Thread类的时候实际上覆写的还是Runnable接口的run()方法。

代码:

@Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

如下图所示:在这里插入图片描述
  多线程的设计之中,使用了代理设计模式的结构,用户自定义的线程主体只是负责项目核心功能的实现,而所有的辅助实现全部交由Thread类来处理。
在进行Thread启动多线程的时候调用的是start()方法,而后找到的是run()方法,但通过Thread类的构造方法传递了一个Runnable接口对象的时候,那么该接口对象将被Thread类中的target属性所保存,在start()方法执行的时候会调用Thread类中的run()方法,而这个run()方法去调用Runnable接口子类被覆写过的run()方法。多线程开发的本质实质上是在于多个线程可以进行同一资源的抢占,那么Thread主要描述的是线程,而资源的描述是通过Runnable完成的。如下图所示:在这里插入图片描述

代码实现:

//多个线程访问一个资源
class MyThread implements Runnable {//线程的主体类
    private int ticket = 5;

    @Override
    public void run() {//线程的主体方法
        for (int x = 0; x < 100; x++) {
            if (ticket > 0)
                System.out.println("卖票,ticket=" + ticket--);
        }

    }
}

public class Demo1 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        new Thread(myThread).start();//第一个线程启动
        new Thread(myThread).start();//第二个线程启动
        new Thread(myThread).start();//第三个线程启动
    }
}


  • 结果:
    卖票,ticket=3
    卖票,ticket=2
    卖票,ticket=5
    卖票,ticket=4
    卖票,ticket=1

  通过内存图分析该程序执行结构,如下图:在这里插入图片描述
每一个target对象指向的都是同一个资源

4. Callable实现多线程

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

代码:


// Callable定义:
@FunctionalInterface
public interface Callable<V>{
V call() throws Exception;
}

//FutureTask定义:
public class FutureTask<V> implements RunnableFuture<V>

//FutureTask构造方法:
FutureTask(Callable<V> callable)

//RunnableFuture定义:
public interface RunnableFuture<V> extends Runnable, Future<V>


//Future<V>方法:
public V get()

  可以发现Callable定义的时候可以设置一个泛型,此泛型的类型就是返回数据的类型,这样的好处是可以避免向下转型所带来的安全隐患。如下图所示:在这里插入图片描述

代码实现:

例子:

//Callable实现多线程
class MyThread implements Callable<String> {//线程的主体类

    @Override
    public String call() throws Exception {
        for (int x = 0; x < 10; x++) {
            System.out.println("*******线程执行,x=" + x + "********");
        }
        return "线程执行完毕";
    }
}

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

    }
}

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

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

5. 线程运行状态

  对于多线程的开发而言,编写程序的过程之中总是按照:定义线程主体类,而后通过Thread类进行线程的启动,但是并不意味着你调用了start()方法,线程就已经开始运行了,因为整体的线程处理有自己的一套运行的状态。
  1、任何一个线程的对象都应该使用Thread类进行封装,所以线程的启动使用的是start(),但是启动的时候实际上若千个线程都将进入到一种就绪状态,现在并没有执行;
  2、进入到就绪状态之后就需要等待进行资源调度,当某一个线程调度成功之后则进入到运行状态(run()方法),但是所有的线程不可能一致持续执行下去,中间需要产生一些暂停的状态,例如:某个线程执行一段时间之后就需要让出资源,而后这个线程就将进入到阻塞状态,随后重新回归到就绪状态;
  3、 当run()方法执行完毕之后,实际上该线程的主要任务也就结束了,那么此时就可以直接进入到停止状态。

详细内容转载于他人博客:

Java多线程-线程状态的转换

版权声明:本文为CSDN博主「codding`」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_40293674/article/details/87875056


进程是一个正在执行的程序,每个进程都有自己的执行顺序,和执行路径,或者叫控制单元;而线程就是进程里面的执行路径,每个进程里面至少有一个线程,两个线程或两个以上的线程则称为多线程;而多线程的执行是靠争取CPU的执行权力,一个CPU只能执行一个线程,只不过是切换的快,我们所看到的是一块执行;多线程是多个代码同时执行。

线程从创建、运行到结束总是处于下面五个状态之一:新建状态、就绪状态、运行状态、堵塞状态以及死亡状态

1、新建(new)

当用New操作符创建一个线程时,例如 new Thread(r),线程还没开始运行,此时线程处于新建状态。当一个线程处于新生状态时,程序还没有开始运行线程中的代码。

2、就绪(runnable)

一个新创建的线程并不会自动开始运行,要执行线程必须调用线程的start()方法。当线程对象调用了start()方法之后,即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法。当start()方法返回后,线程就会进入到就绪状态。

线程进入到就绪状态并不一定会立即执行run()方法,线程必须同其他线程进行竞争CPU时间,获取时间片,这有这样才能运行。因为在单CPU的计算机系统中,不能同时运行多个线程,一个时刻只有一个线程处于运行状态。这里Java采用的是抢占式的调度算法

3、运行状态(running)

当线程获取CPU时间后,它才进入运行状态,真正开始执行run()方法

4、阻塞状态(Blocked)

1):线程通过调用sleep方法进入睡眠状态

2):线程调用一个在I/O上被堵塞的操作,即该操作在输入输出完成之前不会返回到它的调用者

3):线程试图获取一个锁,而该锁正在被其他线程持有

4):线程正在等待某个触发条件

总而言之,堵塞状态就是正在运行的线程没有运行结束,暂时让出CPU,这时其他处于就绪状态的线程就会获得CPU时间,进入运行状态

5、死亡(dead)

1):run方法正常退出而自然死亡

2):一个未捕获的异常中止了run方法的运行而使线程猝死

发布了43 篇原创文章 · 获赞 13 · 访问量 2667

猜你喜欢

转载自blog.csdn.net/qq_43040688/article/details/103979628