JAVA多线程(一)

线程与进程 并行与并发

开始多线程之前,我们得先说一说这个线程与进程,并行与并发。因为我专业是信息安全,所以啊上课有学过这玩意且不止一门课讲过,所以我大概了解一点,记住的也就这几句话,如果你没看懂或者我才疏学浅的确实是讲不明白,你就先找个大佬博客学一学,肯定详细,起码有个概念。

进程:是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竞争计算机系统资源的基本单位。

线程:是进程的一个执行单元,是进程内的调度实体。比进程更小的独立运行的基本单位。线程也被称为轻量级进程。

一个程序至少一个进程,一个进程至少一个线程。

并行 :是物理上同时发生,指在某一个时间点同时运行多个程序,两个或者多个事件在同一时刻发生,是真正意义上的同时发生。

并发 :是逻辑上同时发生,指在某一个时间内同时运行多个程序,两个或多个事件在同一时间间隔内发生,就是这几个程序在一个时间段内走走停停,你来一下我来一下,在一个时间段内都运行结束,相当于在时间段中同时进行,其实每个时刻只有一个,只是快到你感觉不到。

JVM虚拟机是多线程的吗

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

多线程实现

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

  • 继承Thread类
  • 实现Runnable接口
  • 实现Callable接口

三种方式怎么选择?

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

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

继承Thread类

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

线程主体类的定义格式:

class 类名 extends Thread {
	属性;
	方法;
	public void run(){  //重写run方法
		线程主体方法;
	}
}

eg: 定义一个线程操作类

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()方法的源代码的话会发现,在start()方法里面会调用一个start0()的方法,而且这个方法是用native声明的。java中调用本机操作系统提供的函数的技术叫做JNI(Java Native Interface ),这个技术离不开特定的操作系统,因为多线程必须由操作系统来分配资源。这项操作是根据JVM负责根据不同的操作系统实现的。(当然我们不用多管,了解一下)

java.lang.Thread 是专门负责线程操作的类,任何类只要继承了它就可以成为一个线程的主类。有了主类自然要有它的使用方法,那么主类中只需要重写run()方法就可以,这个run()方法就是线程的主体。它相当于线程的入口,我们使用Tread类中的start()方法开启线程后,run()方法里面的代码一定会被执行。

实现Runnable接口

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

这个接口非常简单:

public interface Runnable {
 
    public abstract void run();

}

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

eg: 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接口对象:

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就阔以
        new Thread(threadA).start();
        new Thread(threadB).start();
        new Thread(threadC).start();

    }
}

两种方法的区别与联系:

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

Thread类也是Runnable类的接口

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

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

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

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

  • 继承Thread类
public class MyThread extends Thread {

    private int ticket = 5;

    @Override
    public void run() {
        for (int i = 0; i < 15; i++) {
            if (this.ticket >0)
            System.out.println("ticket=" + this.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张票,结果却不尽人意。

  • 实现Runnable接口
public class MyThread implements Runnable {

    private int ticket = 5;

    @Override
    public void run() {
        for (int i = 0; i <15; i++) {
            if (this.ticket >0)
            System.out.println("ticket=" + this.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接口对象的引用,所以是实现了数据共享的操作。

eg:继承Thread类实现数据共享:


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接口上的泛型决定。

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<V> callable)	          构造,接收Callable接口实例
public FutureTask(Runnable runnable,V result)	  构造,接收Runnable接口实例,并指定返回结果类型
public V get()	                                  取得线程操作结果,是由Future接口定义的

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

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解决了接收返回值的问题。

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值