Skip to content

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

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

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

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

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

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

配图

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

配图

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

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

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

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

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

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

配图

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

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

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

配图

java
继承Thread类,重写run()方法,调用start()方法启动线程
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();
    }
}

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

java
实现Callable接又,重写call()方法,这种方式可以通过FutureTask获取任务执行的返回值
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 的r un 方法,这才起到多线程的效果。

配图

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

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(long timeout, int nanos),其内部调用的是 wait(long timout)函数。

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

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 实际并没有被中断, 会继续往下执行。

boolean isInterrupted() 方法: 检测当前线程是否被中断。
boolean interrupted() 方法: 检测当前线程是否被中断,与 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(long millis)和join(long

millis,intnanos) 两个具备超时特性的方法。

使用ThreadLocal

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

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

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

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

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