java高级编程——多线程(一)之实现多线程

文章目录

  • 线程与进程
  • 并行与并发
  • JVM虚拟机是多线程的吗
  • 多线程实现
    • 继承Thread类
    • 实现Runnable接口
    • 两种方法的区别与联系
    • 利用Callable接口实现多线程

线程与进程

线程和进程一样,都是实现并发的一个基本单位。

线程是依赖进程存在的。先来说进程,进程就是程序的一次动态执行过程。通俗来讲,进程就是正在运行的程序,它是系统进行资源分配和调用的独立单位。每一个进程都有它自己的内存空间和系统资源。

那么,单进程就是一个时间段只能有一个程序执行。不难理解单进程的计算机只能做一件事。最早期的DOS系统就是单进程的,那么一旦系统中了病毒就会直接死机,到了后来的Windows,只要不是致命性的病毒,计算机也可以正常使用,因为它是多进程的,一个时间段上就可以运行多个程序。注意是一个时间段而不是一个时间点,由于CPU具备分时机制,所以每个进程都能循环获得自己的时间片。由于CPU的执行速度非常快,所以给人的感觉是同时在进行。

但是进程的启动所消耗的时间是非常长的(想象你打开一些比较大的软件),所以有必要对其进一步划分以提高性能。这就是线程,一个进程可以同时有多个线程(实际上也确实如此)。线程是比进程更小的执行单位,相当于一个应用程序中同时进行多个任务。

假如你现在想要听歌,是不是就要打开听歌软件?打开软件需要一段比较长的时间(即进程的启动),在这个听歌软件中你可以一边听歌,一边写评论,还可以同时进行搜索功能。这就是多线程。

多线程的作用不是提高执行速度,而是为了提高应用程序的使用率

我们程序在运行的使用,都是在抢CPU的时间片(执行权),如果是多线程的程序,那么在抢到CPU的执行权的概率应该比较单线程程序抢到的概率要大。那么也就是说,CPU在多线程程序中执行的时间要比单线程多,所以就提高了程序的使用率。但是哪个线程能抢占到CPU的资源呢,这个是不确定的,因为多线程具有随机性

重申一下,所有的线程一定要依附于进程才能够存在,一旦进程消失,线程一定也会消失。java是为数不多的支持多线程的开发语言之一。

java高级编程——多线程(一)之实现多线程_第1张图片

并行与并发

  • 并发是逻辑上同时发生,指在某一个时间内同时运行多个程序。
  • 并行是物理上同时发生,指在某一个时间点同时运行多个程序。
  1. 并发
    当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间 段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。这种方式我们称之为并发(Concurrent)。
  2. 并行
    当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。
  3. 区别
    并发和并行是即相似又有区别的两个概念,并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔内发生。在多道程序环境下,并发性是指在一段时间内宏观上有多个程序在同时运行,但在单处理机系统中,每一时刻却仅能有一道程序执行,故微观上这些程序只能是分时地交替执行。倘若在计算机系统中有多个处理机,则这些可以并发执行的程序便可被分配到多个处理机上,实现并行执行,即利用每个处理机来处理一个可并发执行的程序,这样,多个程序便可以同时执行。

JVM虚拟机是多线程的吗

java命令会启动java虚拟机,相当于启动了一个应用程序,相当于启动了一个进程。虚拟机会开启一个主线程去寻找main方法,所以说main方法是运行在主线程中的。但是虚拟机在工作时还会启动垃圾回收机制,也就相当于开启了另一个线程。所以说,我们的JVM虚拟机是多线程的

多线程实现

简单来说,要实现多线程有三种方式:

  1. 继承Thread类
  2. 实现Runnable接口
  3. 实现Callable接口

三种方式怎么选择?

先上结论:尽量避免继承Thread类,优先考虑实现接口的方法

原因:因为java采用的是单继承的模式,继承Thread类就会带来这种局限性,没法再继承其他类;另外,实现接口可以更方便的实现数据共享的概念,这个下面会解释。总之,记住一点就可以,使用实现接口的方式来写多线程更合理。

继承Thread类

在java中,要想实现多线程,就必须依靠一个线程的主类。不管是实现Runnable或者Callable接口,还是直接继承Thread类,都是为了定义这个主类。

  • 线程主体类的定义格式
class 类名 extends Thread {
	属性;
	方法;
	public void run(){
		线程主体方法;
	}
}
  • 案例演示:定义一个线程操作类
public class MyThread extends Thread {

    private String name;

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

    // 重写run方法,作为线程的主操作方法
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(this.name + "--->" + i);
        }
    }
}

好,我们按照模板写了一个简单的线程操作类,用来打印循环。

值得一提的是,所有的进程和线程一样,都必须轮流去抢占资源,所以多线程的执行应该是多个线程彼此之间交替进行。直接调用run()方法,并不能启动多线程,多线程启动的唯一方法就是Thread类中的start()方法

  • 启动多线程
public class Test {
    public static void main(String[] args) {
        // 实例化线程操作类
        MyThread threadA = new MyThread("ThreadA");
        MyThread threadB = new MyThread("ThreadB");
        MyThread threadC = new MyThread("ThreadC");
        // 开启多线程
        threadA.start();
        threadB.start();
        threadC.start();
    }
}

本程序中,我们实例化了三个线程类的对象,然后调用了通过Thread类继承而来的start()方法启动了多线程。

注意:多线程抢占CPU的时间片完全是随机的,也就是说,仅仅因为我们在代码中先开启了A线程并不能保证它一定会先执行,也就是说,打印出的结果完全是随机的,而且线程之间是交替进行的。

有人可能会问,线程不是依赖于进程存在的吗?没有进程,为什么可以直接实现线程呢?

这么来说吧,创建进程是系统级的操作,仅靠java语言是不能完成的,也就是说我们不可能靠java来开启一个进程,但是使用Java命令执行一个类是就相当于启动了一个JVM进程,主方法main就是主线程。

这其实也是多线程的开启是通过Thread类中的start方法而不是直接调用run()方法的原因。

我们看一下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) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

    private native void start0();

可以发现,在start()方法里面会调用一个start0()的方法,而且这个方法是用native声明的。java中调用本机操作系统提供的函数的技术叫做JNI(Java Native Interface ),这个技术离不开特定的操作系统,因为多线程必须由操作系统来分配资源。这项操作是根据JVM负责根据不同的操作系统实现的。

java.lang.Thread 是专门负责线程操作的类,任何类只要继承了它就可以成为一个线程的主类。有了主类自然要有它的使用方法,那么主类中只需要重写run()方法就可以,这个run()方法就是线程的主体。

为什么必须是run()方法?
没有什么特殊原因,只是Thread类是如此规定的而已,它相当于线程的入口,我们使用Trread类中的start()方法开启线程后,run()方法里面的代码一定会被执行。

实现Runnable接口

继承Thread的方法会带来单继承的问题,所以,实际开发中实现Runnable接口的方法更普遍。

这个接口非常简单:

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

}

在Runnable接口中也定义了run()方法,所以线程的主类只需要重写此方法即可。

  • Runnable接口来实现多线程
public class MyThread implements Runnable {

    private String name;

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

     //重写run方法,作为线程的主操作方法
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(this.name + "--->" + i);
        }
    }
}

好了,线程操作类写完了。但是有一个问题?怎么开启线程?接口中没有其他方法了呀!

这里说明一下:不管是以何种方法实现多线程,多线程的开启工作一定是由Thread类下的start()方法来完成的

也就是,要想开启多线程,我们还需要实例化Thread对象,不一样的是,我们这次使用的是有参构造:public Thread(Runnable target),这个方法可以接收一个Runnable接口对象:

package com.xx.threadTest;

public class Test {
    public static void main(String[] args) {
        // 实例化线程操作类
        MyThread threadA = new MyThread("ThreadA");
        MyThread threadB = new MyThread("ThreadB");
        MyThread threadC = new MyThread("ThreadC");
    
        // 有参构造实例化Thread对象并开启线程
        new Thread(threadA).start();
        new Thread(threadB).start();
        new Thread(threadC).start();

    }
}

  • 使用Lambda表达式操作

相信你已经发现了,Runnable接口使用了“@FunctionalInterface”的注解,证明Runnable是一个函数式接口,我们可以使用Lambda表达式风格来写:

public class LambdaThread {
    public static void main(String[] args) {
        String name = "线程对象";
        new Thread(()->{
            for (int i = 0; i < 100; i++) {
                System.out.println(name + "--->" + i);
            }
        }).start();
    }
}

它的效果和上面先写线程操作类再写线程启动的效果是完全一样的,它利用了Lambda表达式直接定义线程主体实现操作后启动,非常简便!

两种方法的区别与联系

上面已经讲到,继承Thread类来定义线程操作类存在单继承的问题,那么除了这个意外,Thread类和Runnable接口还有什么练习呢?

  1. Thread类也是Runnable类的接口

这样的话,我们自己实现Runnable接口写的MyThread类和Thread类都继承了Runnable接口!这么模式类似于代理模式,但又不完全是,此处代理类Thread调用的start方法而不是接口中的run方法,这是本质差别。

  1. 使用Runnable接口可以更方便的表示出数据共享的概念

但不是说继承Thread类就不能表现出,只是有些麻烦而已。

我们通过一个简单的卖票问题来说明:

  1. 继承Thread类
package com.xx.ticket;

public class MyThread extends Thread {

    private int ticket = 5;

    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            if (this.ticket >0)
            System.out.println("ticket=" + this.ticket--);
        }
    }
}

package com.xx.ticket;

public class Test1 {
    public static void main(String[] args) {
        MyThread mt1 = new MyThread();
        MyThread mt2 = new MyThread();
        MyThread mt3 = new MyThread();

        mt1.start();
        mt2.start();
        mt3.start();
    }
}

可能的执行结果:

ticket=5
ticket=4
ticket=3
ticket=2
ticket=1
ticket=5
ticket=4
ticket=3
ticket=2
ticket=1
ticket=5
ticket=4
ticket=3
ticket=2
ticket=1

可以看到,我们开启了三个线程,并希望他们共同卖着5张票,结果却不尽人意。

  1. 实现Runnable接口
public class MyThread implements Runnable {

    private int ticket = 5;

    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            if (this.ticket >0)
            System.out.println("ticket=" + this.ticket--);
        }
    }
}
package com.xx.ticket;

public class Test1 {
    public static void main(String[] args) {
        MyThread mt1 = new MyThread();

        new Thread(mt1).start();
        new Thread(mt1).start();
        new Thread(mt1).start();
    }
}

可能的运行结果:

ticket=5
ticket=3
ticket=2
ticket=1
ticket=4

开启了三个线程,但是与使用继承Thread操作不同的是,他们三个都占用着同一个Runnable接口对象的引用,所以是实现了数据共享的操作。

  1. 继承Thread类实现数据共享
package com.xx.ticket;

public class Test {
    public static void main(String[] args) {
        MyThread mt1 = new MyThread();
        
        new Thread(mt1).start();
        new Thread(mt1).start();
        new Thread(mt1).start();
    }
}

这样一来,即使继承Thread类也可以实现数据共享,但我们不推荐这么做,因为mt1本身就是Thread类的子类,还要再实例化Thread类去开启多线程,明显不合适。

利用Callable接口实现多线程

使用Runnable接口实现的多线程可以避免单继承的局限,但是Runnable接口存在一个问题就是没有办法返回run方法的操作结果(public void run())。为了解决这个问题,从JDK1.5开始,引入了这个接口java.util.concurrent.Callable

@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

这个接口中只定义了一个call()方法,而且在call()方法上可以实现线程操作数据的返回,返回类型有Callable接口上的泛型决定。

package com.xx.callable;

import java.util.concurrent.Callable;

public 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 "售完";
    }

}

这个线程操作类继承了Callable接口,并指定了返回类型为String。

想要获取这个返回值,靠Thread类是不可以的。为了解决这个问题,从JDK1.5起,引入了java.util.concurrent.FutureTask类,定义如下:

public class FutureTask<V> extends Object implements RunnableFuture<V>

FutureTask这个类实现了了RunnableFuture这个接口,而RunnableFuture接口又同时实现了Future和Runnable接口。这是它的常用方法:

方法 解释
public FutureTask(Callable callable) 构造,接收Callable接口实例
public FutureTask(Runnable runnable,V result) 构造,接收Runnable接口实例,并指定返回结果类型
public V get() 取得线程操作结果,是由Future接口定义的

我们现在尝试把上面的MyThread类中的返回值接收一下:

package com.xx.callable;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 实例化多线程对象
        MyThread myThread1 = new MyThread();
        MyThread myThread2 = new MyThread();

        // 实例化FutureTask对象
        FutureTask<String> task1 = new FutureTask<>(myThread1);
        FutureTask<String> task2 = new FutureTask<>(myThread2);

        // FutureTask是Runnable接口子类,可以使用Thread接收构造
        new Thread(task1).start();
        new Thread(task2).start();

        // 获取返回值
        String msg1 = task1.get();
        String msg2 = task2.get();

        System.out.println("线程1返回的结果是:" + msg1 + "\t线程2返回的结果是:" + msg2);

    }
}

运行结果:

ticket=10
ticket=10
ticket=9
ticket=8
ticket=7
ticket=6
ticket=5
ticket=4
ticket=3
ticket=9
ticket=2
ticket=8
ticket=7
ticket=6
ticket=5
ticket=4
ticket=3
ticket=2
ticket=1
ticket=1
线程1返回的结果是:售完	线程2返回的结果是:售完

我们利用FutureTask类实现Callable接口的子类包装,由于FutureTask是Runnable接口的子类,所以可以利用Thread类的start()方法启动多线程,并接收返回值。

总结一下:Callable解决了run()方法返回值的问题,FutureTask解决了接收返回值的问题。

输出结果有些小问题,这是由于线程之间的不同步问题造成的,也是多线程主要的学习内容之一,放在下次讨论。

全文完。

你可能感兴趣的:(JavaSE)