并发编程面试八股文背诵版

文章目录:

  • 什么是进程?是什么线程? * * *

  • 进程和线程的关系?(区别) * * *

  • 并行和并发的区别? *

  • 多线程的优缺点(为什么使用多线程、多线程会引发什么问题) * *

  • 线程的上下文切换 *

  • Java中守护线程和用户线程的区别? *

  • 线程死锁是如何产生的,如何避免 * * *

  • 用Java实现死锁,并给出避免死锁的解决方案        *     *

  • Java中的死锁、活锁、饥饿有什么区别? *

  • 线程的生命周期和状态 * * *

  • 创建线程一共有哪几种方法? * * *

  • runnable 和 callable 有什么区别? * * *

  • 线程的run()和start()有什么区别?  *    *     *

  • 为什么调用start()方法时会执行run()方法,而不直接执行run()方法?   *     *      *

  • 线程同步和线程调度相关的方法问题

    • 线程同步以及线程调度相关的方法有哪些? * * *

    • 线程的sleep()方法和yield()方法有什么不同?   *     *      *

    • sleep()方法和wait()方法的区别?      *     *    *

    • wait()方法一般在循环块中使用还是if块中使用?    *     *     *

    • 线程通信的方法有哪些? * * *

    • 为什么wait()、notify()、notifyAll()被定义在Object类中而不是在Thread类中?    *     *

    • 为什么wait(),notify()和notifyAll()必须在同步方法或者同步块中被调用?    *     *

    • 为什么Thread类的sleep()和yield()方法是静态的? *

    • 如何停止一个正在运行的线程? * *

    • 如何唤醒一个阻塞的线程?    *      *

    • Java如何实现两个线程之间的通信和协作?    *      *

    • 同步方法和同步方法块哪个效果更好?    *     *

    • 什么是线程同步?什么是线程互斥?他们是如何实现的? * * *

    • 在Java程序中如何保证线程的运行安全?    *     *      *

    • 线程类的构造方法、静态块是被哪个线程调用的?    *

    • 一个线程运行时异常会发生什么?      *

    • 线程数量过多会造成什么异常? *

  • 三个线程T1、T2、T3,如何让他们按顺序执行? * * *

  • synchronized关键字 * * *

    • 什么是synchronized关键字?

    • Java内存的可见性问题

    • synchronized关键字三大特性是什么?

    • synchronized关键字可以实现什么类型的锁?

    • synchronized关键字的使用方式

    • synchronized关键字的底层原理

    • Jdk1.6为什么要对synchronized进行优化?

    • jDK1.6对synchronized做了哪些优化?

  • volatile关键字 * * *

    • volatile的作用是什么?

    • volatile的特性有哪些?

    • Java内存的可见性问题

    • 为什么代码会重排序?

    • 重排序会引发什么问题?

    • as-if-serial规则和happens-before规则的区别?

    • voliatile的实现原理?

    • volatile实现内存可见性原理

    • volatile实现有序性原理

    • 编译器对内存屏障插入策略的优化

    • volatile能使一个非原子操作变成一个原子操作吗?

    • volatile、synchronized的区别?

  • ConcurrentHashMap * * *

    • 什么是ConcurrentHashMap?相比于HashMap和HashTable有什么优势?

    • java中ConcurrentHashMap是如何实现的?

    • ConcurrentHashMap结构中变量使用volatile和final修饰有什么作用?

    • ConcurrentHashMap有什么缺点?

    • ConcurrentHashMap默认初始容量是多少?每次扩容为原来的几倍?

    • ConCurrentHashMap 的key,value是否可以为null?为什么?HashMap中的key、value是否可以为null?

    • ConCurrentHashmap在JDK1.8中,什么情况下链表会转化为红黑树?

    • ConcurrentHashMap在JDK1.7和JDK1.8版本中的区别?

    • ConcurrentHashMap迭代器是强一致性还是弱一致性?

  • ThreadLocal * * *

    • 什么是ThreadLocal?有哪些应用场景?

    • ThreadLocal原理和内存泄露?

  • 线程池 * * *

    • 什么是线程池?为什么使用线程池

    • 创建线程池的几种方法

    • ThreadPoolExecutor构造函数的重要参数分析

    • ThreadPoolExecutor的饱和策略(拒绝策略)

    • 线程池的执行流程

    • execute()方法和submit()方法的区别

  • CAS * * *

    • 什么是CAS?

    • CAS存在的问题

    • CAS的优点

  • Atomic 原子类 * *

  • AQS * *

    • 什么是AQS?

    • AQS的原理

    • AQS的资源共享方式有哪些?

    • 如何使用AQS自定义同步器?

什么是进程?是什么线程? * * *

线程是处理器任务调度和执行的基本单位,进程是操作系统资源分配的基本单位。

进程是程序的一次执行过程,是系统运行的基本单位。线程是一个比进程更小的执行单位,一个进程可以包含多个线程。

进程和线程的关系?(区别) * * *

定义:线程是处理器任务调度和执行的基本单位;进程是操作系统资源分配的基本单位。

包含关系:一个进程可以包含多个线程。

从Java虚拟机的角度来理解:Java虚拟机的运行时数据区包含堆、方法区、虚拟机栈、本地方法栈、程序计数器。各个进程之间是相互独立的,每个进程会包含多个线程,每个进程所包含的多个线程并不是相互独立的,这个线程会共享进程的堆和方法区,但这些线程不会共享虚拟机栈、本地方法栈、程序计数器。即每个进程所包含的多个线程共享进程的堆和方法区,并且具备私有的虚拟机栈、本地方法栈、程序计数器,如图所示,假设某个进程包含三个线程。

并发编程面试八股文背诵版_第1张图片

由上面可知以下进程和线程在以下几个方面的区别:

内存分配:进程之间的地址空间和资源是相互独立的,同一个进程之间的线程会共享线程的地址空间和资源(堆和方法区)。

资源开销:每个进程具备各自的数据空间,进程之间的切换会有较大的开销。属于同一进程的线程会共享堆和方法区,同时具备私有的虚拟机栈、本地方法栈、程序计数器,线程之间的切换资源开销较小。

并行和并发的区别? *

并行:单位时间多个处理器同时处理多个任务。

并发:一个处理器处理多个任务,按时间片轮流处理多个任务。

多线程的优缺点(为什么使用多线程、多线程会引发什么问题) * *

优点:当一个线程进入等待状态或者阻塞时,CPU可以先去执行其他线程,提高CPU的利用率。

缺点:

  • 上下文切换:频繁的上下文切换会影响多线程的执行速度。

  • 死锁

  • 资源限制:在进行并发编程时,程序的执行速度受限于计算机的硬件或软件资源。在并发编程中,程序执行变快的原因是将程序中串行执行的部分变成并发执行,如果因为资源限制,并发执行的部分仍在串行执行,程序执行将会变得更慢,因为程序并发需要上下文切换和资源调度。

线程的上下文切换 *

即便是单核的处理器也会支持多线程,处理器会给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给每个线程的执行时间,一般来说时间片非常的短,所以处理器会不停地切换线程。

CPU会通过时间片分配算法来循环执行任务,当前任务执行完一个时间片后会切换到下一个任务,但切换前会保存上一个任务的状态,因为下次切换回这个任务时还要加载这个任务的状态继续执行,从任务保存到在加载的过程就是一次上下文切换。

Java中守护线程和用户线程的区别? *

任何线程都可以设置为守护线程和用户线程,通过方法Thread.setDaemon(bool on) 设置,true则是将该线程设置为守护线程,false则是将该线程设置为用户线程。同时,Thread.setDaemon()必须在Thread.start()之前调用,否则运行时会抛出异常。

用户线程:平时使用到的线程均为用户线程。

守护线程:用来服务用户线程的线程,例如垃圾回收线程。

守护线程和用户线程的区别主要在于Java虚拟机是后存活。

用户线程:当任何一个用户线程未结束,Java虚拟机是不会结束的。

守护线程:如何只剩守护线程未结束,Java虚拟机结束。

线程死锁是如何产生的,如何避免 * * *

这块内容很重要,面试时也可能让手写死锁的代码示例。

死锁:由于两个或两个以上的线程相互竞争对方的资源,而同时不释放自己的资源,导致所有线程同时被阻塞。

死锁产生的条件:

  • 互斥条件:一个资源在同一时刻只由一个线程占用。

  • 请求与保持条件:一个线程在请求被占资源时发生阻塞,并对已获得的资源保持不放。

  • 循环等待条件:发生死锁时,所有的线程会形成一个死循环,一直阻塞。

  • 不剥夺条件:线程已获得的资源在未使用完不能被其他线程剥夺,只能由自己使用完释放资源。

避免死锁的方法主要是破坏死锁产生的条件。

  • 破坏互斥条件:这个条件无法进行破坏,锁的作用就是使他们互斥。

  • 破坏请求与保持条件:一次性申请所有的资源。

  • 破坏循环等待条件:按顺序来申请资源。

  • 破坏不剥夺条件:线程在申请不到所需资源时,主动放弃所持有的资源。

用Java实现死锁,并给出避免死锁的解决方案        *     *

class DeadLockDemo {
    private static Object resource1 = new Object();
    private static Object resource2 = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);   //线程休眠,保证线程2先获得资源2
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "线程 1").start();

        new Thread(() -> {
            synchronized (resource2) {
                System.out.println(Thread.currentThread() + "get resource2");
                try {
                    Thread.sleep(1000); //线程休眠,保证线程1先获得资源1
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource1");
                synchronized (resource1) {
                    System.out.println(Thread.currentThread() + "get resource1");
                }
            }
        }, "线程 2").start();
    }
}
Thread[线程 1,5,main]get resource1
Thread[线程 2,5,main]waiting get resource1
Thread[线程 1,5,main]waiting get resource2

上面代码产生死锁的原因主要是线程1获取到了资源1,线程2获取到了资源2,线程1继续获取资源2而产生阻塞,线程2继续获取资源1而产生阻塞。解决该问题最简单的方式就是两个线程按顺序获取资源,线程1和线程2都先获取资源1再获取资源2,无论哪个线程先获取到资源1,另一个线程都会因无法获取线程1产生阻塞,等到先获取到资源1的线程释放资源1,另一个线程获取资源1,这样两个线程可以轮流获取资源1和资源2。代码如下:

    private static Object resource1 = new Object();
    private static Object resource2 = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);  
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "线程 1").start();

        new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000); 
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "线程 2").start();
    }
}

Java中的死锁、活锁、饥饿有什么区别? *

活锁:任务或者执行者没有被阻塞,由于某些条件没有被满足,导致线程一直重复尝试、失败、尝试、失败。例如,线程1和线程2都需要获取一个资源,但他们同时让其他线程先获取该资源,两个线程一直谦让,最后都无法获取

活锁和死锁的区别:

  • 活锁是在不断地尝试、死锁是在一直等待。

  • 活锁有可能自行解开、死锁无法自行解开。

饥饿:一个或者多个线程因为种种原因无法获得所需要的资源, 导致一直无法执行的状态。以打印机打印文件为例,当有多个线程需要打印文件,系统按照短文件优先的策略进行打印,但当短文件的打印任务一直不间断地出现,那长文件的打印任务会被一直推迟,导致饥饿。活锁就是在忙式等待条件下发生的饥饿,忙式等待就是不进入等待状态的等待。

产生饥饿的原因:

  • 高优先级的线程占用了低优先级线程的CPU时间

  • 线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。

  • 线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的wait()方法),因为其他线程总是被持续地获得唤醒。

死锁、饥饿的区别:饥饿可自行解开,死锁不行。

线程的生命周期和状态 * * *

线程状态的划分并不唯一,但是都大同小异,这里参考《Java并发编程的艺术》,主要有以下几种状态:

状态
NEW 初始状态,注意此时还未调用start()方法
RUNNABLE 运行状态,包含就绪和运行中两种状态
BLOCKED 阻塞状态
WAITING 等待状态
TIME_WAITING 超时等待状态,和等待状态不同的是,它可以在制定的时间自行返回
TERMINATED 终止状态,线程运行结束

线程转化过程如下:

并发编程面试八股文背诵版_第2张图片

创建线程一共有哪几种方法? * * *

  • 继承Thread类创建线程

  • 实现Runnable接口创建线程

  • 使用CallableFuture创建线程

  • 使用线程池例如用Executor框架

继承Thread类创建线程,首先继承Thread类,重写run()方法,在main()函数中调用子类实实例的start()方法。

public class ThreadDemo extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " run()方法正在执行");
    }

}
public class TheadTest {
    public static void main(String[] args) {
        ThreadDemo threadDemo = new ThreadDemo();  
        threadDemo.start();
        System.out.println(Thread.currentThread().getName() + " main()方法执行结束");
    }

}

输出结果:

main main()方法执行结束
Thread-0 run()方法正在执行

**实现Runnable接口创建线程:**首先创建实现Runnable接口的类RunnableDemo,重写run()方法;创建类RunnableDemo的实例对象runnableDemo,以runnableDemo作为参数创建Thread对象,调用Thread对象的start()方法。

public class RunnableDemo implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " run()方法执行中");
    }

}
public class RunnableTest {
    public static void main(String[] args) {
        RunnableDemo  runnableDemo = new RunnableDemo ();
        Thread thread = new Thread(runnableDemo);
        thread.start();
        System.out.println(Thread.currentThread().getName() + " main()方法执行完成");
}

输出结果:

main main()方法执行完成
Thread-0 run()方法执行中

使用Callable和Future创建线程: 1. 创建Callable接口的实现类CallableDemo,重写call()方法。2. 以类CallableDemo的实例化对象作为参数创建FutureTask对象。3. 以FutureTask对象作为参数创建Thread对象。4. 调用Thread对象的start()方法。

class CallableDemo implements Callable {

    @Override
    public Integer call() {
        System.out.println(Thread.currentThread().getName() + " call()方法执行中");
        return 0;
    }

}

 class CallableTest {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask futureTask = new FutureTask(new CallableDemo());
        Thread thread = new Thread(futureTask);
        thread.start();
        System.out.println("返回结果 " + futureTask.get());
        System.out.println(Thread.currentThread().getName() + " main()方法执行完成");
    }

}

输出结果:

Thread-0 call()方法执行中
返回结果 0
main main()方法执行完成

使用线程池例如用Executor框架: Executors可提供四种线程池,分别为:

  • newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

  • newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

  • newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。

  • newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。

下面以创建一个定长线程池为例进行说明,

class ThreadDemo extends Thread {

    @Override

    public void run() {

        System.out.println(Thread.currentThread().getName() + "正在执行");

    }

}

class TestFixedThreadPool {

        public static void main(String[] args) {

        //创建一个可重用固定线程数的线程池

        ExecutorService pool = Executors.newFixedThreadPool(2);

        //创建实现了Runnable接口对象,Thread对象当然也实现了Runnable接口

        Thread t1 = new ThreadDemo();

        Thread t2 = new ThreadDemo();

        Thread t3 = new ThreadDemo();

        Thread t4 = new ThreadDemo();

        Thread t5 = new ThreadDemo();

        //将线程放入池中进行执行

        pool.execute(t1);

        pool.execute(t2);

        pool.execute(t3);

        pool.execute(t4);

        pool.execute(t5);

        //关闭线程池

        pool.shutdown();

        }

        }

输出结果:

pool-1-thread-2正在执行
pool-1-thread-1正在执行
pool-1-thread-1正在执行
pool-1-thread-2正在执行
pool-1-thread-1正在执行

runnable 和 callable 有什么区别? * * *

相同点:

  • 两者都是接口

  • 两者都需要调用Thread.start启动线程

不同点:

  • callable的核心是call()方法,允许返回值,runnable的核心是run()方法,没有返回值

  • call()方法可以抛出异常,但是run()方法不行

  • callablerunnable都可以应用于executorsthread类只支持runnable

线程的run()和start()有什么区别?  *    *     *

  • 线程是通过Thread对象所对应的方法run()来完成其操作的,而线程的启动是通过start()方法执行的。

  • run()方法可以重复调用,start()方法只能调用一次

为什么调用start()方法时会执行run()方法,而不直接执行run()方法?   *     *      *

start()方法来启动线程,真正实现了多线程运行,这时无需等待run()方法体代码执行完毕而直接继续执行下面的代码。通过调用Thread类的 start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法,这里方法run()称为线程体,它包含了要执行的这个线程的内容,run()方法运行结束,此线程随即终止。

run()方法只是类的一个普通方法而已,如果直接调用run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run()方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。

调用start()方法可以开启一个线程,而run()方法只是thread类中的一个普通方法,直接调用run()方法还是在主线程中执行的。

线程同步和线程调度相关的方法问题

线程同步以及线程调度相关的方法有哪些? * * *

  • wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;

  • sleep():使当前线程进入指定毫秒数的休眠,暂停执行,需要处理InterruptedException

  • notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关。

  • notifyAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态。

  • jion():与sleep()方法一样,是一个可中断的方法,在一个线程中调用另一个线程的join()方法,会使得当前的线程挂起,知直到执行join()方法的线程结束。例如在B线程中调用A线程的join()方法,B线程进入阻塞状态,直到A线程结束或者到达指定的时间。

  • yield():提醒调度器愿意放弃当前的CPU资源,使得当前线程从RUNNING状态切换到RUNABLE状态。

线程的sleep()方法和yield()方法有什么不同?   *     *      *

  • sleep()方法会使得当前线程暂停指定的时间,没有消耗CPU时间片。

  • sleep()使得线程进入到阻塞状态,yield()只是对CPU进行提示,如果CPU没有忽略这个提示,会使得线程上下文的切换,进入到就绪状态。

  • sleep()一定会完成给定的休眠时间,yield()不一定能完成。

  • sleep()需要抛出InterruptedException,而yield()方法无需抛出异常。

sleep()方法和wait()方法的区别?      *     *    *

相同点:

  • wait()方法和sleep()方法都可以使得线程进入到阻塞状态。

  • wait()sleep()方法都是可中断方法,被中断后都会收到中断异常。

不同点:

  • wait()是Object的方法,sleep()是Thread的方法。

  • wait()必须在同步方法中进行,sleep()方法不需要。

  • 线程在同步方法中执行sleep()方法,不会释放monitor的锁,而wait()方法会释放monitor的锁。

  • sleep()方法在短暂的休眠之后会主动退出阻塞,而wait()方法在没有指定wait时间的情况下需要被其他线程中断才可以退出阻塞。

wait()方法一般在循环块中使用还是if块中使用?    *     *     *

在JDK官方文档中明确要求了要在循环中使用,否则可能出现虚假唤醒的可能。官方文档中给出的代码示例如下:

synchronized(obj){
    while(){
         obj.wait();
    }
    //满足while中的条件后执行业务逻辑
}

如果讲while换成if

synchronized(obj){
    if(){
         obj.wait();
    }
    //满足while中的条件后执行业务逻辑
}

当线程被唤醒后,可能if()中的条件已经不满足了,出现虚假唤醒。

线程通信的方法有哪些? * * *

  • 锁与同步

  • wait()/notify()notifyAll()

  • 信号量

  • 管道

为什么wait()、notify()、notifyAll()被定义在Object类中而不是在Thread类中?    *     *

因为这些方法在操作同步线程时,都必须要标识他们操作线程的锁,只有同一个锁上的被等待线程,可以被同一个锁上的notify()notifyAll()唤醒,不可以对不同锁中的线程进行唤醒,也就是说等待和唤醒必须是同一锁。而锁可以是任意对象,所以可以被任意对象调用的方法是定义在Object类中。

如果把wait()notify()notifyAll()定义在Thread类中,则会出现一些难以解决的问题,例如如何让一个线程可以持有多把锁?如何确定线程等待的是哪把锁?既然是当前线程去等待某个对象的锁,则应通过操作对象来实现而不是操作线程,而Object类是所有对象的父类,所以将这三种方法定义在Object类中最合适。

为什么wait(),notify()和notifyAll()必须在同步方法或者同步块中被调用?    *     *

因为wait()暂停的是持有锁的对象,notify()notifyAll()唤醒的是等待锁的对象。所以wait()notify()notifyAll()都需要线程持有锁的对象,进而需要在同步方法或者同步块中被调用。

为什么Thread类的sleep()和yield()方法是静态的? *

sleep()yield()都是需要正在执行的线程调用的,那些本来就阻塞或者等待的线程调用这个方法是无意义的,所以这两个方法是静态的。

如何停止一个正在运行的线程? * *

  • 中断:Interrupt方法中断线程

  • 使用volatile boolean标志位停止线程:在线程中设置一个boolean标志位,同时用volatile修饰保证可见性,在线程里不断地读取这个值,其他地方可以修改这个boolean值。

  • 使用stop()方法停止线程,但该方法已经被废弃。因为这样线程不能在停止前保存数据,会出现数据完整性问题。

如何唤醒一个阻塞的线程?    *      *

如果线程是由于wait()sleep()join()yield()等方法进入阻塞状态的,是可以进行唤醒的。如果线程是IO阻塞是无法进行唤醒的,因为IO是操作系统层面的,Java代码无法直接接触操作系统。

  • wait():可用notify()notifyAll()方法唤醒。

  • sleep():调用该方法使得线程在指定时间内进入阻塞状态,等到指定时间过去,线程再次获取到CPU时间片进而被唤醒。

  • join():当前线程A调用另一个线程B的join()方法,当前线程转A入阻塞状态,直到线程B运行结束,线程A才由阻塞状态转为可执行状态。

  • yield():使得当前线程放弃CPU时间片,但随时可能再次得到CPU时间片进而激活。

Java如何实现两个线程之间的通信和协作?    *      *

  • syncrhoized加锁的线程的Object类的wait()/notify()/notifyAll()

  • ReentrantLock类加锁的线程的Condition类的await()/signal()/signalAll()

  • 通过管道进行线程间通信:1)字节流;2)字符流 ,就是一个线程发送数据到输出管道,另一个线程从输入管道读数据。

同步方法和同步方法块哪个效果更好?    *     *

同步块更好些,因为它锁定的范围更灵活些,只在需要锁住的代码块锁住相应的对象,而同步方法会锁住整个对象。

什么是线程同步?什么是线程互斥?他们是如何实现的? * * *

  • 线程的互斥是指某一个资源只能被一个访问者访问,具有唯一性和排他性。但访问者对资源访问的顺序是乱序的。

  • 线程的同步是指在互斥的基础上使得访问者对资源进行有序访问。

线程同步的实现方法:

  • 同步方法

  • 同步代码块

  • wait()notify()

  • 使用volatile实现线程同步

  • 使用重入锁实现线程同步

  • 使用局部变量实现线程同步

  • 使用阻塞队列实现线程同步

在Java程序中如何保证线程的运行安全?    *     *      *

线程安全问题 主要体现在原子性、可见性和有序性。

  • 原子性:一个或者多个操作在 CPU 执行的过程中不被中断的特性。线程切换带来的原子性问题。

  • 可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到。缓存导致的可见性问题。

  • 有序性:程序执行的顺序按照代码的先后顺序执行。编译优化带来的有序性问题。

解决方法:

  • 原子性问题:可用JDK Atomic开头的原子类、synchronizedLOCK来解决

  • 可见性问题:可用synchronizedvolatileLOCK来解决

  • 有序性问题:可用Happens-Before 规则来解决

线程类的构造方法、静态块是被哪个线程调用的?    *

线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,而run()方法里面的代码才是被线程自身所调用的。

一个很经典的例子:

假设main()函数中new了一个线程Thread1,那么Thread1的构造方法、静态块都是main线程调用的,Thread1中的run()方法是自己调用的。

假设在Thread1中new了一个线程Thread2,那么Thread2的构造方法、静态块都是Thread1线程调用的,Thread2中的run()方法是自己调用的。

一个线程运行时异常会发生什么?      *

Java中的Throwable主要分为ExceptionErrorException分为运行时异常和非运行时异常。运行时异常可以不进行处理,代码也能通过编译,但运行时会报错。非运行时异常必须处理,否则代码无法通过编译。出现Error代码会直接

线程数量过多会造成什么异常? *

  • 消耗更多的内存和CPU

  • 频繁进行上下文切换

三个线程T1、T2、T3,如何让他们按顺序执行? * * *

这是一道面试中常考的并发编程的代码题,与它相似的问题有:

  • 三个线程T1、T2、T3轮流打印ABC,打印n次,如ABCABCABCABC.......

  • 两个线程交替打印1-100的奇偶数

  • N个线程循环打印1-100

  • ......

其实这类问题本质上都是线程通信问题,思路基本上都是一个线程执行完毕,阻塞该线程,唤醒其他线程,按顺序执行下一个线程。下面先来看最简单的,如何按顺序执行三个线程。

  • synchronized+wait/notify

基本思路就是线程A、线程B、线程C三个线程同时启动,因为变量num的初始值为0,所以线程B或线程C拿到锁后,进入while()循环,然后执行wait()方法,线程线程阻塞,释放锁。只有线程A拿到锁后,不进入while()循环,执行num++,打印字符A,最后唤醒线程B和线程C。此时num值为1,只有线程B拿到锁后,不被阻塞,执行num++,打印字符B,最后唤醒线程A和线程C,后面以此类推。

class Wait_Notify_ACB {

    private int num;
    private static final Object LOCK = new Object();

    private void printABC(String name, int targetNum) {
            synchronized (LOCK) {
                while (num % 3 != targetNum) {    //想想这里为什么不能用if代替while,想不起来可以看上一篇文章
                    try {
                        LOCK.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                num++;
                System.out.print(name);
                LOCK.notifyAll();
            }
    }
    
    public static void main(String[] args) {
        Wait_Notify_ACB  wait_notify_acb = new Wait_Notify_ACB ();
        new Thread(() -> {
            wait_notify_acb.printABC("A", 0);
        }, "A").start();
        new Thread(() -> {
            wait_notify_acb.printABC("B", 1);
        }, "B").start();
        new Thread(() -> {
            wait_notify_acb.printABC("C", 2);
        }, "C").start();
    }
}

输入结果:

ABC
Process finished with exit code 0

接下来看看第一个问题,三个线程T1、T2、T3轮流打印ABC,打印n次。其实只需要将上述代码加一个循环即可,这里假设n=10。

class Wait_Notify_ACB {

    private int num;
    private static final Object LOCK = new Object();


    private void printABC(String name, int targetNum) {
        for (int i = 0; i < 10; i++) {
            synchronized (LOCK) {
                while (num % 3 != targetNum) { //想想这里为什么不能用if代替,想不起来可以看上一篇文章
                    try {
                        LOCK.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                num++;
                System.out.print(name);
                LOCK.notifyAll();
            }
        }

    }
    
    public static void main(String[] args) {
        Wait_Notify_ACB  wait_notify_acb = new Wait_Notify_ACB ();
        new Thread(() -> {
            wait_notify_acb.printABC("A", 0);
        }, "A").start();
        new Thread(() -> {
            wait_notify_acb.printABC("B", 1);
        }, "B").start();
        new Thread(() -> {
            wait_notify_acb.printABC("C", 2);
        }, "C").start();
    }
}

输出结果:

ABCABCABCABCABCABCABCABCABCABC
Process finished with exit code 0

下面看第二个问题,两个线程交替打印1-100的奇偶数,为了减少输入所占篇幅,这里将100 改成了10。基本思路上面类似,线程odd先拿到锁——打印数字——唤醒线程even——阻塞线程odd,以此循环。

class  Wait_Notify_Odd_Even{

    private Object monitor = new Object();
    private volatile int count;

    Wait_Notify_Odd_Even(int initCount) {
        this.count = initCount;
    }

    private void printOddEven() {
        synchronized (monitor) {
            while (count < 10) {
                try {
                    System.out.print( Thread.currentThread().getName() + ":");
                    System.out.println(++count);
                    monitor.notifyAll();
                    monitor.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //防止count=10后,while()循环不再执行,有子线程被阻塞未被唤醒,导致主线程不能退出
            monitor.notifyAll();
        }
    }
    
    public static void main(String[] args) throws InterruptedException {

        Wait_Notify_Odd_Even waitNotifyOddEven = new Wait_Notify_Odd_Even(0);
        new Thread(waitNotifyOddEven::printOddEven, "odd").start();
        Thread.sleep(10);
        new Thread(waitNotifyOddEven::printOddEven, "even").start();
    }
}

运行结果:

odd:1
even:2
odd:3
even:4
odd:5
even:6
odd:7
even:8
odd:9
even:10

再看第三个问题,N个线程循环打印1-100,其实仔细想想这个和三个线程循环打印ABC并没有什么本质区别,只需要加上判断是否到了打印数字的最大值的语句即可。假设N=3,为了能把输出结果完全显示,打印1-10,代码如下:

class Wait_Notify_ACB {

    private int num;
    private static final Object LOCK = new Object();
    private int maxnum = 10;

    private void printABC(String name, int targetNum) {
        while (true) {
            synchronized (LOCK) {
                while (num % 3 != targetNum) { //想想这里为什么不能用if代替,想不起来可以看上一篇文章
                    if(num >= maxnum){
                        break;
                    }
                    try {
                        LOCK.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                if(num >= maxnum){
                    break;
                }
                num++;
                System.out.println(Thread.currentThread().getName() + ": " + num);
                LOCK.notifyAll();
            }
        }

    }
    
        public static void main(String[] args) {
        Wait_Notify_ACB  wait_notify_acb = new Wait_Notify_ACB ();
        new Thread(() -> {
            wait_notify_acb.printABC("thread1", 0);
        }, "thread1").start();
        new Thread(() -> {
            wait_notify_acb.printABC("thread2", 1);
        }, "thread2").start();
        new Thread(() -> {
            wait_notify_acb.printABC("thread3", 2);
        }, "thread3").start();
    }
}

输出结果:

thread1: 1
thread2: 2
thread3: 3
thread1: 4
thread2: 5
thread3: 6
thread1: 7
thread2: 8
thread3: 9
thread1: 10

面试官:大家都是用的synchronized+wait/notify,你能不能换个方法解决该问题?

我:好的,我还会用join方法

下面介绍的方法只给出第一道题的代码了,否则太长了,相信大家可以举一反三

  • join()

join()方法:在A线程中调用了B线程的join()方法时,表示只有当B线程执行完毕时,A线程才能继续执行。基于这个原理,我们使得三个线程按顺序执行,然后循环多次即可。无论线程1、线程2、线程3哪个先执行,最后执行的顺序都是线程1——>线程2——>线程3。代码如下:

class Join_ABC {

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            Thread t1 = new Thread(new printABC(null),"A");
            Thread t2 = new Thread(new printABC(t1),"B");
            Thread t3 = new Thread(new printABC(t2),"C");
            t0.start();
            t1.start();
            t2.start();
            Thread.sleep(10); //这里是要保证只有t1、t2、t3为一组,进行执行才能保证t1->t2->t3的执行顺序。
        }

    }

    static class printABC implements Runnable{
        private Thread beforeThread;
        public printABC(Thread beforeThread) {
            this.beforeThread = beforeThread;
        }
        @Override
        public void run() {
            if(beforeThread!=null) {
                try {
                    beforeThread.join();
                    System.out.print(Thread.currentThread().getName());
                }catch(Exception e){
                    e.printStackTrace();
                }
            }else {
                System.out.print(Thread.currentThread().getName());
            }

        }
    }
}

输出结果:

ABCABCABCABCABCABCABCABCABCABC

面试官:还会其他方法吗?

我:还会Lock。

  • Lock

该方法很容易理解,其实现代码和synchronized+wait/notify方法的很像。不管哪个线程拿到锁,只有符合条件的才能打印。代码如下:

 class Lock_ABC {

    private int num;   // 当前状态值:保证三个线程之间交替打印
    private Lock lock = new ReentrantLock();


    private void printABC(String name, int targetNum) {
        for (int i = 0; i < 10; ) {
            lock.lock();
            if (num % 3 == targetNum) {
                num++;
                i++;
                System.out.print(name);
            }
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        Lock_ABC lockABC = new Lock_ABC();

        new Thread(() -> {
            lockABC.printABC("A", 0);
        }, "A").start();

        new Thread(() -> {
            lockABC.printABC("B", 1);
        }, "B").start();

        new Thread(() -> {
            lockABC.printABC("C", 2);
        }, "C").start();
    }
}

输出结果:

ABCABCABCABCABCABCABCABCABCABC

面试官:该方法存在什么问题,可以进一步优化吗

我:可以使用Lock+Condition实现对线程的精准唤醒,减少对其他线程无意义地唤醒,浪费资源。

  • Lock+Condition

该思路和synchronized+wait/notify方法的更像了,synchronized对应lock,await/signal方法对应wait/notify方法。下面的代码为了能精准地唤醒下一个线程,创建了多个Condition对象。

class LockConditionABC {

    private int num;
    private static Lock lock = new ReentrantLock();
    private static Condition c1 = lock.newCondition();
    private static Condition c2 = lock.newCondition();
    private static Condition c3 = lock.newCondition();

    private void printABC(String name, int targetNum, Condition currentThread, Condition nextThread) {
        for (int i = 0; i < 10; ) {
            lock.lock();
            try {
                while (num % 3 != targetNum) {
                    currentThread.await();
                }
                num++;
                i++;
                System.out.print(name);
                nextThread.signal();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        LockConditionABC print = new LockConditionABC();
        new Thread(() -> {
            print.printABC("A", 0, c1, c2);
        }, "A").start();
        new Thread(() -> {
            print.printABC("B", 1, c2, c3);
        }, "B").start();
        new Thread(() -> {
            print.printABC("C", 2, c3, c1);
        }, "C").start();
    }
}

输出结果:

ABCABCABCABCABCABCABCABCABCABC

面试官:除了该方法,还有什么方法可以避免唤醒其他无意义的线程?

我:可以通过使用信号量来实现。

  • Semaphore

Semaphore:用来控制同时访问某个特定资源的操作数量,或者同时执行某个制定操作的数量。Semaphore内部维护了一个计数器,其值为可以访问的共享资源的个数。

一个线程要访问共享资源,先使用acquire()方法获得信号量,如果信号量的计数器值大于等于1,意味着有共享资源可以访问,则使其计数器值减去1,再访问共享资源。如果计数器值为0,线程进入休眠。

当某个线程使用完共享资源后,使用release()释放信号量,并将信号量内部的计数器加1,之前进入休眠的线程将被唤醒并再次试图获得信号量。

代码如下:

class SemaphoreABC {

    private static Semaphore s1 = new Semaphore(1);  //先打印A,所以设s1中的计数器值为1
    private static Semaphore s2 = new Semaphore(0);
    private static Semaphore s3 = new Semaphore(0);
    

    private void printABC(String name, Semaphore currentThread, Semaphore nextThread) {
        for (int i = 0; i < 10; i++) {
            try {
                currentThread.acquire();   //阻塞当前线程,即调用当前线程acquire(),计数器减1为0
                System.out.print(name);
                nextThread.release();    //唤醒下一个线程,即调用下一个线程线程release(),计数器加1

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SemaphoreABC printer = new SemaphoreABC();
        new Thread(() -> {
            printer.printABC("A", s1, s2);
        }, "A").start();
        Thread.sleep(10);
        new Thread(() -> {
            printer.printABC("B", s2, s3);
        }, "B").start();
        Thread.sleep(10);
        new Thread(() -> {
            printer.printABC("C", s3, s1);
        }, "C").start();
    }
}

输出结果:

ABCABCABCABCABCABCABCABCABCABC

面试官:除了上述五种方法,还有其他方法吗

我:还有LockSupport、CountDownLatch、AtomicInteger等等。

面试官:那如何实现三个线程循环打印ACB,其中A打印两次,B打印三次,C打印四次呢?

我:......

面试官:如何用两个线程交叉打印数字和字符呢?例如A1B2C3......Z26

我:......

大家可以思考下后面两个问题,原理都是相通的。



....博主太懒了字数太多了,不想写了....文章已经做成PDF,有需要的朋友可以私信我免费获取!
 

你可能感兴趣的:(java,后端,面试,面试,java,jvm,职场和发展,开发语言)