Java多线程实现

导语

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区别就在于返回值的实现。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,743评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,296评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,285评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,485评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,581评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,821评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,960评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,719评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,186评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,516评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,650评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,936评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,757评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,991评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,370评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,527评论 2 349

推荐阅读更多精彩内容