Skip to content

《面渣逆袭》Java并发编程 篇 · 第 1/7 章。原版 PDF(下载 / 打印)

1.并行跟并发有什么区别? ​

从操作系统的角度来看,线程是CPU 分配的最小单位。

并行就是同一时刻,两个线程都在执行。这就要求有两个CPU 去分别执行两个线程。

并发就是同一时刻,只有一个执行,但是一个时间段内,两个线程都执行了。并发的实现依赖于

CPU 切换线程,因为切换的时间特别短,所以基本对于用户是无感知的。

配图

就好像我们去食堂打饭,并行就是我们在多个窗又排队,几个阿姨同时打菜;并发就是我们挤在一个窗又,阿姨给这个打一勺,又手忙脚乱地给那个打一勺。

配图

2.说说什么是进程和线程? ​

要说线程,必须得先说说进程。

进程:进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。

线程:线程是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源。

操作系统在分配资源时是把资源分配给进程的,但是 CPU 资源比较特殊,它是被分配到线程的,因为真正要占用CPU 运行的是线程,所以也说线程是 CPU 分配的基本单位。

比如在Java 中,当我们启动 main 函数其实就启动了一个JVM 进程,而 main 函数在的线程就是这个进程中的一个线程,也称主线程。

配图

一个进程中有多个线程,多个线程共用进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈。

3.说说线程有几种创建方式? ​

Java 中创建线程主要有三种方式,分别为继承Thread 类、实现Runnable 交叉、实现Callable 交叉。

配图

继承Thread 类,重写run() 方法,调用start() 方法启动线程

java
public class ThreadTest {

/

  • 继承Thread 类*/
java
    public static class MyThread extends Thread {

@ Override

java
        public void run() {
            System.out.println("This is child thread");
        }
    }
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
    }
}
实现 Runnable 接又,重写run()方法
public class RunnableTask implements Runnable {
    public void run() {
        System.out.println("Runnable!");
    }
    public static void main(String[] args) {
        RunnableTask task = new RunnableTask();
        new Thread(task).start();
    }
}

上面两种都是没有返回值的,但是如果我们需要获取线程的执行结果,该怎么办呢?

实现Callable 交叉,重写call() 方法,这种方式可以通过FutureTask 获取任务执行的返回值

java
public class CallerTask implements Callable<String> {
    public String call() throws Exception {
        return "Hello,i am running!";
    }
    public static void main(String[] args) {
        //创建异步任务
        FutureTask<String> task=new FutureTask<String>(new CallerTask());
        //启动线程
        new Thread(task).start();
        try {
            //等待执行完成,并获取返回结果
            String result=task.get();
            System.out.println(result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

4.为什么调用start()方法时会执行run()方法,那怎么不直接调用run()方 法? ​

JVM 执行start 方法,会先创建一条线程,由创建出来的新线程去执行thread 的run 方法,这才起到多线程的效果。

配图

为什么我们不能直接调用run() 方法?​也很清楚,如果直接调用Thread 的run() 方法,那么run 方法还是运行在主线程中,相当于顺序执行,就起不到多线程的效果。

5.线程有哪些常用的调度方法? ​

配图

线程等待与通知

在Object 类中有一些函数可以用于线程的等待与通知。

wait():当一个线程A 调用一个共享变量的 wait() 方法时,线程A 会被阻塞挂起,发生下面几种情况才会返回:

(1) 线程A调用了共享对象 notify()或者 notifyAll()方法;
(2)其他线程调用了线程A的 interrupt() 方法,线程A抛出InterruptedException异常返

回。

wait(longtimeout):这个方法相比 wait() 方法多了一个超时参数,它的不同之处在于,如果线

程A调用共享对象的wait(long timeout)方法后,没有在指定的 timeout ms时间内被其它线程唤

醒,那么这个方法还是会因为超时而返回。

wait(longtimeout,intnanos ),其内部调用的是 wait(longtimout)函数。

上面是线程等待的方法,而唤醒线程主要是下面两个方法:

notify(): 一个线程A 调用共享对象的 notify() 方法后,会唤醒一个在这个共享变量上调用 wait系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。

notifyAll():不同于在共享变量上调用 notify() 函数会唤醒被阻塞到该共享变量上的一个线程,

notifyAll() 方法则会唤醒所有在该共享变量上由于调用 wait 系列方法而被挂起的线程。

Thread 类也提供了一个方法用于等待的方法:

join():如果一个线程A 执行了thread.join() 语句,其含义是:当前线程A 等待thread 线程终止之后才

从thread.join()返回。

线程休眠

java
sleep(long millis) :Thread类中的静态方法,当一个执行中的线程A调用了Thread 的sleep方法

后,线程A 会暂时让出指定时间的执行权,但是线程A 所拥有的监视器资源,比如锁还是持有不让出的。指定的睡眠时间到了后该函数会正常返回,接着参与 CPU 的调度,获取到 CPU 资源后就可以继续运行。

让出优先权

yield():Thread 类中的静态方法,当一个线程调用 yield 方法时,实际就是在暗示线程调度器当前线程请求让出自己的CPU,但是线程调度器可以无条件忽略这个暗示。

线程中断

Java 中的线程中断是一种线程间的协作模式,通过设置线程的中断标志并不能直接终止该线程的执行,而是被中断的线程根据中断状态自行处理。

void interrupt():中断线程,例如,当线程A运行时,线程B可以调用线程interrupt() 方法来设

置线程的中断标志为true 并立即返回。设置标志仅仅是设置标志, 线程A 实际并没有被中断,会继续往下执行。

booleanisInterrupted() 方法:检测当前线程是否被中断。

booleaninterrupted() 方法:检测当前线程是否被中断,与 isInterrupted 不同的是,该方法如果发现当前线程被中断,则会清除中断标志。

6.线程有几种状态? ​

在Java 中,线程共有六种状态:

状态说明
NEW初始状态:线程被创建,但还没有调用start() 方法
RUNNABLE运行状态:Java 线程将操作系统中的就绪和运行两种状态笼统的称作“运行”
BLOCKED阻塞状态:表示线程阻塞于锁
WAITING等待状态:表示线程进入等待状态,进入该状态表示当前线程需要等待其他 线程做出一些特定动作(通知或中断)
TIMEWAITING_超时等待状态:该状态不同于 WAITIND,它是可以在指定的时间自行返回 的
TERMINATED终止状态:表示当前线程已经执行完毕

线程在自身的生命周期中,并不是固定地处于某个状态,而是随着代码的执行在不同的状态之间进行切换,Java 线程状态变化如图示:

配图

7.什么是线程上下文切换? ​

使用多线程的目的是为了充分利用CPU,但是我们知道,并发其实是一个CPU 来应付多个线程。

配图

为了让用户感觉多个线程是在同时执行的, CPU 资源的分配采用了时间片轮转也就是给每个线程分配一个时间片,线程在时间片内占用 CPU 执行任务。当线程使用完时间片后,就会处于就绪状态并让出

CPU 让其他线程占用,这就是上下文切换。

配图

8.守护线程了解吗? ​

Java 中的线程分为两类,分别为 daemon 线程(守护线程)和 user 线程(用户线程)。

在JVM 启动时会调用 main 函数,main 函数所在的线程就是一个用户线程。其实在 JVM 内部同时还启动了很多守护线程,比如垃圾回收线程。

那么守护线程和用户线程有什么区别呢?区别之一是当最后一个非守护线程束时, JVM 会正常退出,而不管当前是否存在守护线程,也就是说守护线程是否结束并不影响 JVM 退出。换而言之,只要有一个用户线程还没结束,正常情况下JVM 就不会退出。

9.线程间有哪些通信方式? ​

配图

volatile和synchronized关键字

关键字volatile 可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。

关键字synchronized 可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性。

等待/ 通知机制

可以通过Java 内置的等待/ 通知机制(wait()/notify())实现一个线程修改一个对象的值,而另一个线程感知到了变化,然后进行相应的操作。

管道输入/ 输出流

管道输入/ 输出流和普通的文件输入/ 输出流或者网络输入/ 输出流不同之处在于,它主要用于线程之间的数据传输,而传输的媒介为内存。

管道输入/ 输出流主要包括了如下4 种具体实现:PipedOutputStream、PipedInputStream、

PipedReader 和PipedWriter,前两种面向字节,而后两种面向字符。

使用Thread.join()

如果一个线程A 执行了thread.join() 语句,其含义是:当前线程A 等待thread 线程终止之后才从

thread.join() 返回。。线程Thread 除了提供join() 方法之外,还提供了join(longmillis) 和join(long millis,intnanos) 两个具备超时特性的方法。

使用ThreadLocal

ThreadLocal,即线程变量,是一个以ThreadLocal 对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal 对象查询到绑定在这个线程上的一个值。

可以通过set(T) 方法来设置一个值,在当前线程下再通过get() 方法获取到原先设置的值。

关于多线程,其实很大概率还会出一些笔试题,比如交替打印、银行转账、生产消费模型等等,后面老三会单独出一期来盘点一下常见的多线程笔试题。

本文整理自三分恶《面渣逆袭》系列的公开内容,仅供个人学习使用

本站仅供个人学习使用,请勿外传