Java 多线程深入分析(一)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Y1258429182/article/details/74783983

多线程在 Java 中的重要性不言而喻 , 所以我这里做一个细致的总结。

1. 概念

1.1 线程概念

线程:程序中单独顺序的流控制。

  • 线程本身不能运行,它只能用于程序中。

说明:线程是程序内的顺序控制流,只能使用分配给程序的资源和环境。

1.2 进程概念

进程:操作系统中执行的程序。

  • 程序是静态的概念,进程是动态的概念。

  • 一个进程可以包含一个或多个线程。

  • 一个进程至少要包含一个线程。

1.3 单线程

单个程序中只有一个执行路径就是单线程。  

当程序启动运行时,就自动产生一个线程,主方法main就在这个主线程上运行。我们的程序都是由线程来执行的。

1.4 多线程

多线程指在单个程序中可以同时运行多个不同的线程执行不同的任务。

多线程编程的目的,利用 Amdahl 定律 最大限度的 “压榨” 计算机的运算能力 ,当某一线程的处理不需要占用 CPU 而只和 IO 等资源打交道时,让需要占用CPU的其他线程有机会获得CPU资源。从根本上说,这就是多线程编程的最终目的。

一个程序实现多个代码同时交替运行就需要产生多个线程。

CPU随机地抽出时间,让我们的程序一会做这件事情,一会做另外的事情。

1.5 并行和串行

从宏观角度来看,多个线程在同时执行(宏观并行),但是微观上来看,处理器的个数决定了某一个时刻可以同时运行的最大线程数,

如单核CPU某一时刻只能有一个线程在执行(微观串行),双核的CPU在某一个时刻,最多可以运行两个线程,可以做到微观并行。

2. 线程的实现方式

我们平时写一个线程有一下两种,Thread、Runnable 的实现方式 ,下面我们详细介绍这两种写法。

2.1 Thread

这里我们分析Thread 主要关注两方面,一个是线程的写法,一个是 run() 和 start() 方法的区别。

2.1.1 Thread 的写法

我们如果用继承 Thread 的方式来实现线程,一般会这么写。

public class MyThread extends Thread {
    @Override
    public void run() {
        super.run();
    }
}

然后我们直接把对象实例化调用方法 start() 方法即可 , 就可以调用线程了,但是有人开始说我也可以直接调用 run() 方法呀,为什么调用 start() 方法,我们下面就讨论这个问题。

2.1.2 run() 和start()的区别

我们这里写一个例子,Thread 和 运行代码如下:
Thread:

public class MyThread extends Thread {
    private String mThreadName;

    public MyThread(String mThreadName) {
        super();
        this.mThreadName = mThreadName;
    }

    @Override
    public void run() {
        super.run();
        for (int i = 0; i < 3; i++) {
            System.out.println(mThreadName + ": " + i);
        }
    }
}

main方法:

    public static void main(String[] args) {
        // run方法直接调用
        // testMyThreadRunMethod();
        // start方法调用
         testMyThreadStartMethod();

    }
    private static void testMyThreadStartMethod() {
        MyThread threadA = new MyThread("线程 A");
        MyThread threadB = new MyThread("线程 B");
        threadA.start();
        threadB.start();
    }

    private static void testMyThreadRunMethod() {
        MyThread threadA = new MyThread("线程 A");
        MyThread threadB = new MyThread("线程 B");
        threadA.run();
        threadB.run();
    }

我们实例化对象两个线程,然后调用分别调用 run() 方法和 start() 方法,运行结果如下:
run() :

线程 A0
线程 A1
线程 A2
线程 B:   0
线程 B:   1
线程 B:   2

start():

线程 A0
线程 B:   0
线程 B:   1
线程 B:   2
线程 A1
线程 A2

从结果我们可以看出来 start() 和run()方法的区别,其实 run() 方法当作普通方法的方式调用,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,这样就没有达到写线程的目的程序还是要顺序执行,还是要等待上个线程 run() 方法体执行完毕后才可继续执行下面的代码。

但是执行 start() 方法,我们通过查看 Thread 源码中的 start() 方法会执行本地方法,通过 jni 调用 c++ 代码,从而创建出新线程独有的空间,所以执行到可以并发的,不是运行在主线程中。

    public synchronized void start() {
        checkNotStarted();

        hasBeenStarted = true;
        // Thread的调用本地方法
        nativeCreate(this, stackSize, daemon);
    }

2.2 Runnable

对于 Runnable 的关注在于他的用法即可,因为Runnable是一个接口。

public interface Runnable {
    void run();
}

如果想使用 Runnable 需要创建一个类实现 Runnable,而不是继承,因为实现接口可以实现多继承的效果,所以工作中我们大都是用实现 Runnable 这种方式。使用方法如下:

public class MyThreadRunnable implements Runnable{
    @Override
    public void run() {
    }
}
// 实例化 MyThreadRunnable 
MyThreadRunnable threadA = new MyThreadRunnable();
//把 MyThreadRunnable 当参数添加Tread中,并调用start() 方法
new Thread(threadA).start();

3. Thread 和 Runnable的比较

当我们工作的时候我们一般都用 Runnable 来书写线程,但是面试的时候或者考试的时候总是会将这两种线程实现方式进行比较,那我们比较一下区别。

3.1 Thread 和 Runnable的不同点

  1. Thread 因为是继承所以是单继承,Runnable因为是实现接口所以多继承。
  2. Runnable 能共享资源,Thread 不能共享资源,多线程的有一个优点就是多线程共享同一块内存空间和一组系统资源 , 但是 Thread按照正常的写法不能实现这个优点,但是Runnable可以。

你是否还抱有怀疑精神,对任何事物抱有怀疑精神是对的,那么接下来我们通过卖票的例子来验证一下第二点。

3.2 车站卖票的例子

//Thread
public class ThreadTicket extends Thread {
    private  int ticketCount = 10;
    private String ticketWindow;

    public ThreadTicket(String ticketWindow) {
        super();
        this.ticketWindow = ticketWindow;
    }

    @Override
    public void run() {
        super.run();
        for (int i = 0; i < 20; i++) {
                if (ticketCount > 0) {
                    ticketCount--;
                    System.out.println(ticketWindow + "卖了一张票,还剩下" + ticketCount + " 张票");
                }
        }
    }
}

//Runnable
public class MyThreadRunnable implements Runnable{
    private String mThreadName;

    public MyThreadRunnable(String mThreadName) {
        super();
        this.mThreadName = mThreadName;
    }
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(mThreadName+":   "+i);
        }   
    }

}
//测试代码
public class TestThreadTicket {
    public static void main(String[] args) {
        //三个Thread卖票
        testThreadTicket();
        //三个Runnable卖票
        // testRunnableTicket();
    }

    private static void testRunnableTicket() {
        RunnableTicket runnable1 = new RunnableTicket("窗口1");

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

    private static void testThreadTicket() {
        ThreadTicket ticket1 = new ThreadTicket("窗口1");
        ThreadTicket ticket2 = new ThreadTicket("窗口2");
        ThreadTicket ticket3 = new ThreadTicket("窗口3");

        ticket1.start();
        ticket2.start();
        ticket3.start();
        // 这也可以实现共享,主要是太麻烦了,直接可以start,还在new 一个,还不如直接用runnble呢
        // new Thread(ticket1).start();
        // new Thread(ticket1).start();
        // new Thread(ticket1).start();
    }
}

我们用 testThreadTicket() 方法测试 Thread 是否能正常卖票,我们看一下执行结果:

窗口1卖了一张票,还剩下9 张票
窗口3卖了一张票,还剩下9 张票
窗口2卖了一张票,还剩下9 张票
窗口2卖了一张票,还剩下8 张票
窗口2卖了一张票,还剩下7 张票
窗口3卖了一张票,还剩下8 张票
窗口3卖了一张票,还剩下7 张票
窗口3卖了一张票,还剩下6 张票
窗口1卖了一张票,还剩下8 张票
窗口1卖了一张票,还剩下7 张票
窗口1卖了一张票,还剩下6 张票
窗口1卖了一张票,还剩下5 张票
窗口3卖了一张票,还剩下5 张票
窗口2卖了一张票,还剩下6 张票
窗口2卖了一张票,还剩下5 张票
窗口2卖了一张票,还剩下4 张票
窗口2卖了一张票,还剩下3 张票
窗口3卖了一张票,还剩下4 张票
窗口3卖了一张票,还剩下3 张票
窗口3卖了一张票,还剩下2 张票
窗口3卖了一张票,还剩下1 张票
窗口3卖了一张票,还剩下0 张票
窗口1卖了一张票,还剩下4 张票
窗口1卖了一张票,还剩下3 张票
窗口1卖了一张票,还剩下2 张票
窗口1卖了一张票,还剩下1 张票
窗口1卖了一张票,还剩下0 张票
窗口2卖了一张票,还剩下2 张票
窗口2卖了一张票,还剩下1 张票
窗口2卖了一张票,还剩下0 张票

分析结果,我们发现总共有十张票,居然卖重了这么多张票,所以这表明 Thread 这种写法不能共享资源。

我们接下来分析testRunnableTicket() 方法来测试 Runnable ,运行结果如下:

窗口1卖了一张票,还剩下9 张票
窗口1卖了一张票,还剩下6 张票
窗口1卖了一张票,还剩下7 张票
窗口1卖了一张票,还剩下8 张票
窗口1卖了一张票,还剩下4 张票
窗口1卖了一张票,还剩下2 张票
窗口1卖了一张票,还剩下1 张票
窗口1卖了一张票,还剩下5 张票
窗口1卖了一张票,还剩下0 张票
窗口1卖了一张票,还剩下3 张票

分析结果发现这是我们想要的结果,所以证明 Runnable 可以共享资源。

总结

我们这篇文章就分析到这里,我们分析了线程的相关概念,然后分别介绍了 Thread 和 Runnable 这种实现方式和其中的一些细节,下篇文章我们继续分析多线程中的线程的状态的调度,sychonized、volitile等关键字的区别。


参考:

  1. 深入理解Java虚拟机 JVM高级特性与最佳实践
  2. java多线程详解(1)-多线程入门
  3. Thread和Runnable的区别

猜你喜欢

转载自blog.csdn.net/Y1258429182/article/details/74783983