Java多线程(面试篇)

线程与进程的区别

什么叫做进程:当一个程序被允许,从磁盘加载到这个程序的代码至内存,这时就开启了一个进程。

什么叫做线程:线程就是一个指令流,将指令流中的一条条指令以一定顺序交给CPU执行。

两者区别:

  • 进程是正在运行程序的实例,进程中包含了多个线程,每个线程执行不同的任务。
  • 不同的进程使用不同的内存空间,当前进程下的所有线程可以共享内存空间。
  • 线程比进程更轻量,线程上下文切换成本一般比进程低(上下文切换指的是一个线程切换到另一个线程)

并行和并发的区别

现在都是多核CPU,在多核CPU下
并发:同一时间应对多件事情的能力,多个线程轮流使用一个或多个CPU。

并行:同一时间动手做多见事情的能力,4核CPU同时执行4个线程。

创建线程的方式有哪些

  • 继承Thread类
  • 实现Runnable接口
  • 实现Callable接口
  • 线程池创建线程(项目中常用的方式)

追加->runnable与callable有什么区别?

1.runnable没有返回值,callable的返回值是泛型。

2.runnable不能向上抛出异常,只能内部消化,callable可以抛出异常

3.callable可以用Future、FutureTask获取线程返回值。

追加->线程的run和start有什么区别?

run相当于一个普通方法,可以在一个线程中多次执行,而start是开启一个线程,一个线程只能开启一次,多次调用就会抛出异常。

线程包括哪些状态,状态之间如何变换

线程的状态可以参考JDK中的Thread类中的枚举State。

有6种状态分别为:新建(new)、可执行(runnable)、阻塞(blocked)、等待(waiting)、时间等待(timed_waiting)、终止(terminated)

线程状态之间的变化

  • 创建线程对象是新建状态。
  • 调用start()方法转变为可执行状态。
  • 线程获取到了CPU的执行权,执行结束是终止状态。
  • 在可执行状态的过程中,如果没有获取到CPU的执行权,可能会切换其它的状态。
    • 如果没有获取到锁(synchronized或lock)进入阻塞状态,获得锁再切换为可执行状态。
    • 如果线程调用了wait()方法进入等待状态,其它线程调用notify()唤醒可切换为可执行状态。
    • 如果线程调用了sleep()方法,进入计时等待状态,到时间后可切换为可执行状态。

新建T1、T2、T3三个线程,如何保证它们按顺序执行

可以使用线程中的join方法解决。

notify和notifyAll有什么区别

notify:只随机唤醒一个wait方法。
notifyAll:唤醒所有wait方法。

在java中wait和sleep()方法有什么区别

共同点:

wait()、wait(long)和sleep的效果都是让当前线程暂时放弃CPU的使用权,进入阻塞状态。

不同点:

1.方法的归属不同

  • sleep(long)是Thread的静态方法。
  • 而wait()、wait()都是Object的成员方法,每个对象都有。

2.醒来的时机不同

  • 执行sleep(long)和wait(long)的线程都会在等待相应毫秒后醒来。
  • wait(long)和wait()还可以别notify唤醒,wait()如果不唤醒就一直等下去。
  • 它们都可以被打断唤醒。

3.锁的特性不同(重点)

  • wait()方法的调用必须先获取wait对象的锁,而sleep则无此限制。
  • wait()方法执行后会释放锁对象,允许其它线程获得该对象锁(我放弃cpu,但你们还可以使用)。
  • 而sleep如果在synchronized代码块中执行,并不会释放对象锁(我方法cpu,你们也用不了)。

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

  • 使用退出标志,使线程正常的退出,也就是run方法完成后线程终止。
  • 使用stop强行终止(不推荐,方法作废)
  • 使用interrupt方法中断线程。
    • 打断阻塞的线程(join、wait、sleep),线程会抛出异常。
    • 打断正常的线程,可以根据打断状态来标记是否退出线程。

synchronized关键字的底层原理

基本使用方式

Synchronized[对象锁]采用互斥的方式让同一时刻至多只有一个线程能持有[对象锁],其它线程想要获取这个[对象锁]就会堵塞

Monitor->被翻译为监听器,由jvm提供,c++语言实现。

  • Synchronized[对象锁]采用互斥的方式让同一时刻至多只有一个线程能持有[对象锁]。
  • 它的底层由monitor实现的,monitor是jvm级别的对象(C++实现),线程获得锁需要使用对象(锁)关联monitor。
  • 在monitor内部有三个属性,分别是owner、entrylist、waitset。
  • 其中owner是关联的获得锁的线程,并且只能关联一个线程;entrylist关联的是处于阻塞状态的线程;waitset关联的是处于waiting状态的线程。
  • 进阶->Monitor实现的锁属于重量级锁,你了解过锁升级吗?

谈谈JMM(Java内存模型)

JMM(Java Memory Model)Java内存模型,定义了共享内存中多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性。

  • JMM(Java内存模型),定义了共享内存中多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性。
  • JMM把内存分为两块,一块是私有线程的工作区(工作内存),一块是所有线程的共享区域(主内存)。
  • 线程跟线程中间是相互隔离,线程跟线程交互需要通过主内存。

CAS你知道吗?

  • CAS的全称是:Compare And Swap(比较再交换),它体现的一种乐观锁的思想,在无锁情况下保证线程操作共享数据的原子性。

  • CAS使用的地方很多:AQS框架、Atomicxxx类。

  • 在操作共享变量的时候使用的自旋锁,效率上更高。

  • CAS的底层是调用的Unsafe类中的方法,都是操作系统提供的,其它语言实现。

    追问—>乐观锁和悲观锁的区别

  • CAS是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃点亏再重试。

  • synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解锁,你们才有机会。

谈谈你对volatile的理解

  • 保证线程间的可见性:用volatile修饰的共享变量,能够防止编译器优化发生,让一个线程对共享变量的修改对另一个线程可见。
  • 禁止指令重排序:用volatile修饰共享变量会在读、写共享变量时加入不同屏障,阻止其它读写操作越过屏障,从而达到阻止重排序的效果。

什么是AQS

全称是 AbstractQueuedSynchronizer,即抽象队列同步器。它是构建锁或者其他同步组件的基础框架。

AQS与Synchronized的区别

  • 是多线程中的队列同步器。是一种锁机制也是非公平的,它是做为一个基础框架使用的,像ReentrantLock、Semaphore都是基于AQS实现的。
  • AQS内维护了一个先进先出的双向队列,队列中存储的排队的线程。
  • AQS内部还有一个属性state,这个state就相当于是一个资源,默认是0(无锁状态),如果队列中的有一个线程修改成功了state为1,则当前线程就等于获取了资源。
  • 在对state修改的时候使用cas操作,保证多个线程修改的情况下原子性。

ReentrantLock的实现原理

ReentrantLock翻译过来是可重入锁,相对于synchronized它具备一下特点:

  • 可中断
  • 可以设置超时时间
  • 可以设置公平锁
  • 支持多个条件变量
  • 与synchronized一样,都支持重入

    原理:
  • ReentrantLock表示支持重新进入的锁,调用lock方法获取锁之后,再次调用lock,是不会再阻塞
  • ReentrantLock主要利用CAS+AQS队列来实现
  • 支持公平锁和非公平锁,在提供的构造器的中无参默认是非公平锁,也可以传参设置为公平锁。

synchronized和Lock有什么区别

  • 语法层面

synchronized是关键字,源码在jvm中,用c++语言实现。

Lock是接口,源码由jdk提供,用java语言实现

使用synchronized时,退出同步代码块锁会自释放,而使用Lock时,需要手动调用unlock方法释放锁

  • 功能层方面

二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能

Lock提供了许多synchronizde不具备的功能,例如公平锁、可打断、可超时、多条件变量

Lock有适合不同场景的实现,如ReentrantLock,ReentrantReadWriteLock(读写锁)

  • 性能层面

没有竞争时,synchronized做了很多优化,如偏向锁、轻量级锁,性能不赖。

在竞争激烈时,Lock的实现通常会提供更好的性能。

死锁产生条件是什么

死锁:一个线程需要同时获取多把锁,这时就容易发生死锁。

例如:线程t1持有A的锁等待获取B锁,线程t2持有B的锁等待获取A的锁。

如何进行死锁诊断

当程序出现死锁现象,我们可以用jdk自带的工具:jps和jstack

  • jps:输出JVM中运行的进程状态信息。
  • jstack:查看java进程内线程的堆栈信息。

    其它解决工具

聊一下ConcurrentHashMap

  • 底层数据结构
    • jdk1.7底层数据结构是数组+链表。
    • jdk1.8采用的数据结构跟HashMap1.8一样的结构,数组+链表/红黑二叉树。
  • 加锁的方式
    • jdk1.7采用Segment分段锁,底层使用的ReentrantLock。
    • jdk1.8采用的CAS添加新节点,采用synchronized锁定链表或红黑二叉树的首节点,相对Segment分段速锁粒度更细,性能更好。

导致并发程序出现问题的根本原因是什么

Java并发编程三大特性:

  • 原子性:一个线程在CPU中操作不可以暂停,也不可以中断,要不执行完成,要不不执行。
    • synchronized:同步加锁
    • JUC里面的lock:加锁
  • 内存可见性:一个线程对共享变量的修改对另一个线程可见。
    • volatile
    • lock
    • synchronzide
  • 有序性:指令重排:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一直,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
    • volatile

线程池的核心参数(线程池的执行原理)

线程池核心参数

线程池执行流程

线程池中有哪些常见的阻塞队列

如何确定核心线程数

先了解

线程池的种类有哪些

在java.until.concurrent.Executors类中提供了大量创建连接池的静态方法,常见的有4种。

  • newFixedThreadPool:创建使用固定线程数的线程池,可控制线程最大并发数,超出的线程会在队列中等待。
  • newSingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO)执行。
  • newCachedThreadPool:创建一个可以缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
  • newScheduledThreadPool:可以执行延迟任务的线程池,支持定时及周期性执行。

为什么不建议使用

线程池的使用场景

场景一

流程图

场景二

场景三

  • 批量导入:使用线程池——CountDownLatch批量把数据库中的数据导入到了ES(任意)中,避免OOM。
  • 数据总汇:调用多个接口来汇总数据,如果所有接口(或部分接口)的没有依赖关系,就可以使用线程池+future来提升性能。
  • 异步线程(线程池):为了避免下一级方法影响上一级方法(性能考虑),可以使用异步线程调用下一个方法(不需要下一级方法返回值),可以提升方法响应时间。

如何控制每个方法允许并发访问线程的数量

在多线程中提供了一个工具类Semaphore,信号量。在并发的情况下,可以控制方法的访问量。

  • 创建Semaphore对象,可以给一个容量。
  • acquire()可以请求一个信号量,这时候的信号量个数-1。
  • release()释放一个信号量,此时信号量个数+1。

谈谈你对ThreadLocal的理解

ThreadLocal是多线程中对于解决线程安全的一个操作类,它会为每个线程都分配一个独立的线程副本从而解决了变
量并发访问冲突的问题。ThreadLocal 同时实现了线程内的资源共享。

  • 31
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值