【Java八股文之基础篇(二十一)】多线程的面试高频问题(一)

Java多线程

在了解多线程之前,让我们回顾一下操作系统中提到的进程概念:

进程是程序执行的实体,每一个进程都是一个应用程序(比如我们运行QQ、浏览器、LOL、网易云音乐等软件),都有自己的内存空间,CPU一个核心同时只能处理一件事情,当出现多个进程需要同时运行时,CPU一般通过时间片轮转调度算法,来实现多个进程的同时运行。

img
在早期的计算机中,进程是拥有资源和独立运行的最小单位,也是程序执行的最小单位。但是,如果我希望两个任务同时进行,就必须运行两个进程,由于每个进程都有一个自己的内存空间,进程之间的通信就变得非常麻烦(比如要共享某些数据)而且执行不同进程会产生上下文切换,非常耗时,那么能否实现在一个进程中就能够执行多个任务呢?(这段话可以理解为:早期的进程就是现在的线程,但是早期的进程的切换和现在是一样的,非常耗时,所以后来就出现了比进程更小的单位——线程)
img
后来,线程横空出世,一个进程可以有多个线程,线程是程序执行中一个单一的顺序控制流程,现在线程才是程序执行流的最小单元,各个线程之间共享程序(进程)的内存空间(也就是所在进程的内存空间,这就省下了进程间通信的时间,因为如果进程是最小单位,每个进程都有自己的内存空间,那么如果按照早期计算机的做法,执行两个任务要用两个进程,而这个两个任务又要共享某些数据,那么这些共享数据就要在两个进程各自的内存空间,每次切换进程时都要通过进程间的通信来更新这些共享数据,这很耗时,但是如果是两个线程去执行两个任务,他们的内存空间是共享进程的内存,所以就不要进程的通信来更新共享数据),上下文切换速度也高于进程。

在Java中,我们从开始,一直以来编写的都是单线程应用程序(运行main()方法的内容,main方法是主线程),也就是说只能同时执行一个任务(无论你是调用方法、还是进行计算,始终都是依次进行的,也就是同步的),而如果我们希望同时执行多个任务(两个方法同时在运行或者是两个计算同时在进行,也就是异步的),就需要用到Java多线程框架。实际上一个Java程序启动后,会创建很多线程,不仅仅只运行一个主线程:

package javase.day15;

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;

public class ThreadTest {
    public static void main(String[] args) {
        ThreadMXBean bean = ManagementFactory.getThreadMXBean();
        long[] ids = bean.getAllThreadIds();
        ThreadInfo[] infos = bean.getThreadInfo(ids);
        for (ThreadInfo info : infos) {
            System.out.println(info.getThreadName());
        }
    }
}

运行结果:

Monitor Ctrl-Break
Attach Listener
Signal Dispatcher
Finalizer
Reference Handler
main

关于除了main线程默认以外的线程,涉及到JVM相关底层原理,在这里不做讲解,了解就行。

线程的创建和启动

通过创建Thread对象来创建一个新的线程,Thread构造方法中需要传入一个Runnable接口的实现(其实就是编写要在另一个线程执行的内容逻辑)同时Runnable只有一个未实现方法,因此可以直接使用lambda表达式:

//函数式接口
@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

创建好后,通过调用start()方法来运行此线程:

public static void main(String[] args) {
    Thread t = new Thread(() -> {    //直接编写逻辑
        System.out.println("我是另一个线程!");
    });
    t.start();   //调用此方法来开始执行此线程
}

可能上面的例子看起来和普通的单线程没两样,那我们先来看看下面这段代码的运行结果:

public static void main(String[] args) {
    Thread t = new Thread(() -> {
        System.out.println("我是线程:"+Thread.currentThread().getName());
        System.out.println("我正在计算 0-10000 之间所有数的和...");
        int sum = 0;
        for (int i = 0; i <= 10000; i++) {
            sum += i;
        }
        System.out.println("结果:"+sum);
    });
    t.start();
    System.out.println("我是主线程!");
}

结果:

我是主线程!
我是线程:Thread-0
我正在计算 0-10000 之间所有数的和...
结果:50005000

我们发现,这段代码执行输出结果并不是按照从上往下的顺序了,因为他们分别位于两个线程,他们是同时进行的!如果你还是觉得很疑惑,我们接着来看下面的代码运行结果:

public static void main(String[] args) {
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 50; i++) {
            System.out.println("我是一号线程:"+i);
        }
    });
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 50; i++) {
            System.out.println("我是二号线程:"+i);
        }
    });
    t1.start();
    t2.start();
}

结果:

我是一号线程:0
我是一号线程:1
我是一号线程:2
我是一号线程:3
我是一号线程:4
我是二号线程:0
我是二号线程:1
我是二号线程:2
我是二号线程:3
我是二号线程:4
我是二号线程:5
.........
我是一号线程:44
我是一号线程:45
我是一号线程:46
我是一号线程:47
我是一号线程:48
我是一号线程:49

我们可以看到打印实际上是在交替进行的,也证明了他们是在同时运行!

我们将程序的start方法修改为run()方法,看输出结果

我是一号线程:0
我是一号线程:1
我是一号线程:2
我是一号线程:3
......
我是一号线程:48
我是一号线程:49
我是二号线程:0
我是二号线程:1
我是二号线程:2
我是二号线程:3
.......
我是二号线程:48
我是二号线程:49

注意:我们发现还有一个run方法,也能执行线程里面定义的内容,但是run是直接在当前线程执行,并不是创建一个线程执行

下面这张图展示了线程的几种状态:
img
实际上,线程和进程差不多,也会等待获取CPU资源,一旦获取到,就开始按顺序执行我们给定的程序,当需要等待外部IO操作(比如Scanner获取输入的文本),就会暂时处于休眠状态,等待通知,或是调用sleep()方法来让当前线程休眠一段时间:

public static void main(String[] args) throws InterruptedException {
    System.out.println("l");
    Thread.sleep(1000);    //休眠时间,以毫秒为单位,1000ms = 1s
    System.out.println("b");
    Thread.sleep(1000);
    System.out.println("w");
    Thread.sleep(1000);
    System.out.println("nb!");
}

我们也可以使用stop()方法来强行终止此线程:

public static void main(String[] args) throws InterruptedException {
    Thread t = new Thread(() -> {
        Thread me = Thread.currentThread();   //获取当前线程对象
        for (int i = 0; i < 50; i++) {
            System.out.println("打印:"+i);
            if(i == 20) me.stop();  //此方法会直接终止此线程
        }
    });
    t.start();
}

虽然stop()方法能够终止此线程,但是并不是所推荐的做法,有关线程中断相关问题,我们会在后面继续了解。

思考:猜猜以下程序输出结果:

private static int value = 0;

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 10000; i++) value++;
        System.out.println("线程1完成");
    });
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 10000; i++) value++;
        System.out.println("线程2完成");
    });
    t1.start();
    t2.start();
    Thread.sleep(1000);  //主线程停止1秒,保证两个线程执行完成
    System.out.println(value);
}

value的值在10000到20000之间都有可能。每次执行都不一样。
我们发现,value最后的值并不是我们理想的结果(10000),有关为什么会出现这种问题,在我们学习到线程锁的时候,再来探讨。

参考资料来源:青空の霞光

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Kplusone

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值