Java多线程实现

作者: 一个有故事的程序员 | 来源:发表于2017-09-20 23:00 被阅读35次

    导语

    Java是一门为数不多的多线程支持的编程语言。

    主要内容

    • 掌握Java中三种多线程的实现方式

    具体内容

    如果想在Java之中实现多线程有两种途径:

    • 继承Thread类。
    • 实现Runnable接口(Callable接口)

    继承Thread类

    Thread类是一个支持多线程的功能类,只要有一个子类它就可以实现多线程的支持。

    // 线程操作主类
    public class MyThread extends Thread {  // 这就是一个多线程的操作类
        
    }
    
    public class TestDemo {  // 主类
        public static void main(String args[]) {
            
        }
    }
    

    所有程序的起点是main()方法,但是所有线程也一定要有一个自己的起点,那么这个起点就是run()方法,也就是说在多线程的每个主体类之中都必须覆写Thread类中所提供的run()方法。

    public void run() {}
    

    这个方法上没有返回值,线程一旦开始就要一直执行,不能够返回内容。

    // 线程操作主类
    public class MyThread extends Thread {  // 这就是一个多线程的操作类
        private String name;
        public MyThread(String name) {  // 定义构造方法
            this.name = name;
        }
        @Override
        public void run() {  // 覆写run()方法,作炎线程的主体操作方法
            for(int i = 0; i < 200; i++) {
                System.out.println(this.name + "-->" + i);
            }
        }
    }
    
    public class TestDemo {  // 主类
        public static void main(String args[]) {
            MyThread mt1 = new MyThread("线程A");
            MyThread mt2 = new MyThread("线程B");
            MyThread mt3 = new MyThread("线程C");
            
            mt1.run();
            mt2.run();
            mt3.run();
        }
    }
    

    输出结果

    线程A-->0
    线程A-->1
    ...
    线程A-->198
    线程A-->199
    线程B-->0
    线程B-->1
    ...
    线程B-->198
    线程B-->199
    线程C-->0
    线程C-->1
    ...
    线程C-->198
    线程C-->199
    

    本线程类的功能是进行循环的输出操作,所有的线程与进程是一样的,都必须轮流去抢占资源,所以多线程的执行应该是多个线程彼此交替执行,也就是说如果直接调用了run()方法,那么并不能够启动多线程,多线程的启动的唯一方法就是Thread类中的start()方法:public void start() (调用此方法执行的方法体是run()方法定义的)。

    修改代码

    public class TestDemo {  // 主类
        public static void main(String args[]) {
            MyThread mt1 = new MyThread("线程A");
            MyThread mt2 = new MyThread("线程B");
            MyThread mt3 = new MyThread("线程C");
            
            mt1.start();
            mt2.start();
            mt3.start();
        }
    }
    

    输出结果

    线程A-->0
    线程A-->1
    线程C-->0
    线程B-->0
    线程C-->1
    线程B-->1
    线程A-->2
    ...
    

    此时每一个线程对象交替执行。

    观察Thread的源代码

    public synchronized void start() {
        if (threadStatus != 0)
            throw new IllegalThreadStateException();
        group.add(this);
        started = false;
        try {
            nativeCreate(this, stackSize, daemon);
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
    
            }
        }
    }
    
    private native static void nativeCreate(Thread t, long stackSize, boolean daemon);
    

    但是每个线程只能执行一次start()方法,否则会抛出IllegalThreadStateException异常。
    发现在start()方法里面要调用一个nativeCreate()方法,而且此方法的结构与抽象方法类似,使用了native声明,在Java的开发里面有一门技术称为JNI技术(Java Native Interface),这门技术的特点:使用Java调用本机操作系统提供的函数。但是这样的技术有一个缺点,不能够离开特定的操作系统。
    如果想要线程能够执行,需要操作系统来进行资源分配,所以此操作严格来讲主要是由JVM负责根据不同的操作系统而实现的。
    即:使用Thread类的start()方法不仅仅要启动多线程的执行代码,还要去根据不同的操作系统进行资源分配。

    实现Runnable接口

    虽然Thread类可以实现多线程的主体类定义,但是它有一个问题,Java具有单继承局限,正因为如此在任何情况下针对于类的继承都应该是回避的问题,那么多线程也一样,为了解决单继承的限制,在Java里面专门提供了Runnable接口,此接口定义如下。

    @FunctionalInterface
    public interface Runnable {
        public void run();
    }
    

    那么只需要让一个类实现Runnable接口即可,并且也需要覆写run()方法。

    public class MyThread implements Runnable {  // 这就是一个多线程的操作类
        private String name;
        public MyThread(String name) {  // 定义构造方法
            this.name = name;
        }
        @Override
        public void run() {  // 覆写run()方法,作炎线程的主体操作方法
            for(int i = 0; i < 200; i++) {
                System.out.println(this.name + "-->" + i);
            }
        }
    }
    

    与继承Thread类相比,此时的MyThread类在结构上与之前 是没有区别的,但是有一点是有严重区别的,如果此时继承了Thread类,那么可以直接继承start()方法,但是如果实现的是Runnbale接口,并没有start()方法可以被继承。

    不管何种情况下,如果想要启动多线程一定依靠Thread类完成,在Thread类里面定义有以下的构造方法:public Thread(Runnable target),接收的是Runnable接口对象。

    启动多线程:

    public class TestDemo {  // 主类
        public static void main(String args[]) {
            MyThread mt1 = new MyThread("线程A");
            MyThread mt2 = new MyThread("线程B");
            MyThread mt3 = new MyThread("线程C");
            
            new Thread(mt1).start();
            new Thread(mt2).start();
            new Thread(mt3).start();
        }
    }
    

    此时就避免了单继承局限,那么也就是说在实际工作中使用Runnable接口是最合适的。

    多线程两种实现方法的区别

    通过讲解已经清楚了多线程的两种实现方式,那么这两种方式有哪些区别呢?
    首先一定要明确的是,使用Runnable接口与Thread类相比,解决了单继承的定义局限,所以不管后面的区别与联系是什么,至少这 一点上就能发现,Runnable接口更优。

    首先观察一下Thread类的定义。

    public class Thread implements Runnable {}
    

    发现Thread类实现了Runnable接口,那么这样一来程序就变为了以下的形式。

    定义结构

    此时,整个的定义结构看起来非常像代理设计模式,如果是代理设计模式,客户端调用的代理类的方法也应该是接口里提供的方法,那么也应该是run()才对(当时技术不成熟)。
    除了以上的联系之外,还有一点:使用Runnable接口可以比Thread类能够更好的描述数据共享这一概念。此时的数据共享指的是多个线程访问同一资源的操作。

    范例:观察代码(每一个线程对象都必须通过start()启动)

    public class MyThread extends Thread {
        private int ticket = 10;
        @Override
        public void run() {  // 覆写run()方法,作炎线程的主体操作方法
            for(int i = 0; i < 100; i++) {
                if(this.ticket > 0) {
                    System.out.println("卖票,ticket = " + this.ticket--);
                }
            }
        }
    }
    
    public class TestDemo {  // 主类
        public static void main(String args[]) {
            // 由于MyThread类有start()方法,所以每一个MyThread类对象就是一个线程对象,可以直接启动
            MyThread mt1 = new MyThread();
            MyThread mt2 = new MyThread();
            MyThread mt3 = new MyThread();
            
            mt1.start();
            mt2.start();
            mt3.start();
        }
    }
    

    输出结果

    卖票,ticket = 10
    卖票,ticket = 10
    卖票,ticket = 9
    卖票,ticket = 10
    ...
    

    本程序声明了三个MyThread类对象,并且分别调用了三次start()方法,启动线程对象。但是最终的结果发现每一个线程对象都在卖各自的10张票,因为此时产生了三个线程对象,此时并不存在有数据共享这一概念。

    范例:利用Runnable来实现

    public class MyThread implements Runnable {
        private int ticket = 10;
        @Override
        public void run() {  // 覆写run()方法,作炎线程的主体操作方法
            for(int i = 0; i < 100; i++) {
                if(this.ticket > 0) {
                    System.out.println("卖票,ticket = " + this.ticket--);
                }
            }
        }
    }
    
    public class TestDemo {  // 主类
        public static void main(String args[]) {
            MyThread mt = new MyThread();
            new Thread(mt).start();
            new Thread(mt).start();  
            new Thread(mt).start();  
        }
    }
    

    输出结果

    卖票,ticket = 10
    卖票,ticket = 9
    卖票,ticket = 8
    卖票,ticket = 7
    ...
    

    此时也属于三个线程对象,可是唯一的区别是,这三个线程对象都直接占用了同一个MyThread类的对象引用,也就是说这三个线程对象都直接访问同一个数据资源。

    Thread类与Runnable接口实现多线程的区别:

    • Thread类是Runnable接口的子类,使用Runnable接口实现多线程可以避免单继承局限。
    • Runnable接口实现的多线程可以比Thread类实现的多线程更加清楚的描述了数据共享的概念。

    第三种实现方式(理解)

    使用Runnable接口实现的多线程可以避免单继承局限,但是有一个问题,Runnable接口里面的run()方法,不能返回操作结果。为了解决这样的矛盾,提供了一个新的接口java.util.conurrent.Callable<V>接口。

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

    call()方法执行完线程的主题功能之后可以返回一个结果,而返回结果的类型由Callable接口上的泛型来决定。

    范例:定义一个线程主体类

    class MyThread implements Callable<String> {
        private int ticket = 10;
        
        @Override
        public String call() throws Exception {
            for(int i = 0; i < 100; i++) {
                if(this.ticket > 0) {
                    System.out.println("卖票,ticket = " + this.ticket--);
                }
            }
            return "票已卖光!";
        }
    }
    

    观察发现Thread类里面没有发现直接支持Callable接口的多线程应用。
    从JDK1.5开始提供有java.util.concurrent.FutureTask<V>类。这个类主要是负责Callable接口对象操作的,这个接口的定义结构:

    public class FutureTask<V> implements RunnableFuture<V> {}
    
    public interface RunnableFuture<V> extends Runnable, Future<V> {}
    

    在FutureTask类里面定义有如下的构造方法:

    public FutureTask(Callable<V> callable) {}
    

    接收Callable对象的目的只有一个,那么就是取得call()方法的返回结果。
    启动线程的代码如下。

    public class TestDemo {
        public static void main(String args[]) throws Exception {
            MyThread mt = new MyThread();
            FutureTask<String> task = new FutureTask(mt);  // 目的是为了取得call()返回结果,记得设置返回结果类型
            // FutureTask是Runnable接口子类,所以可以使用Thread类的构造来接收task对象
            new Thread(task).start();  // 启动多线程
            // 多线程执行完毕后可以取得内容,依靠FutureTask的父接口Future中的get()方法完成
            System.out.println("线程的返回结果:" + task.get();
        }
    }
    

    输出结果

    卖票,ticket = 10
    卖票,ticket = 9
    卖票,ticket = 8
    ...
    卖票,ticket = 1
    票已卖光!
    

    最麻烦的问题在于需要接收返回值信息,并且又要与原始的多线程(Thread类)的实现靠拢。

    总结

    • 对于多线程的实现,重点在于Runnable接口与Thread类启动的配合上。
    • 对于JDK1.5新特性,Callable区别就在于返回值的实现。

    更多内容戳这里(整理好的各种文集)

    相关文章

      网友评论

      本文标题:Java多线程实现

      本文链接:https://www.haomeiwen.com/subject/qbpgsxtx.html