Java多线程面试知识点总结

Java多线程面试知识点总结

2019/10/31 周四


程序、进程、线程、协程的概念区分:

  • 程序:含有指令和数据的文件,存储在磁盘上,是可执行的静态代码
  • 进程:内存中正在运行的程序,是应用程序的执行实例,是系统分配资源的独立单位,有自己独立的内存空间
  • 线程:进程中执行运算的最小单位,CPU调度和分配的基本单位;不能独立存在,一个进程至少拥有一个线程,同一个进程中的线程共享同一个内存空间;有自己的局部变量
  • 协程:更小的线程单位,协程的调度完全由用户控制;一个线程可以拥有多个协程,一个进程也可以单独拥有多个协程
  • 进程和线程都是同步机制,协程是异步,能保留上一次调用时的状态

开启线程的方式

  1. 继承 java.lang.Thread 类,重写run()方法
  2. 实现 java.lang.Runnable 接口,重写run()方法,使用时还要new一个Thread类的实例,把Runnable实例作为参数
  3. 实现 java.util.concurrent.Callable 接口,重写call()方法,会抛出异常,有返回值;必须指定泛型,返回值就是泛型
  4. 从线程池中获取(注:Thread线程对象不能使用线程池)
Runnable接口和Callable接口的区别:

(1)Runnable中run方法没有返回值,而Callable中call方法有返回值,返回值类型是泛型类型
(2)Callable中的call方法会抛出异常,而Runnable的run方法不会
(3)Callable可以在没有获取到需要的数据的情况下取消线程任务
(4)在线程池中Runnable线程对象用execute()方法执行,Callable对象用submit()方法执行线程(因为有返回值)

线程的生命周期

  1. New-新生状态/初始状态 (用new关键字实例化一个线程)
  2. Runnable-就绪状态/可运行状态 (调用t.start()方法)
  3. Running-运行状态 (获取到cpu时间片,run()方法正在执行)
  4. Blocked-阻塞状态:
    (1)sleep线程休眠阻塞,休眠结束了阻塞状态就结束了
    (2)wait线程等待阻塞,进入等待状态,需要notify或notifyAll唤醒
    (3)join阻塞,当前线程调用了其他线程的join方法,等其他线程的join方法结束了,阻塞状态就结束了
    (4)IO阻塞,如等待用户在控制台输入时阻塞,用户输入完阻塞结束
    (5)yield线程礼让,归还时间片,让CPU重新分配,但还是有可能抢到CPU资源,这种情况就没有阻塞
    注意:阻塞状态结束后,线程回到就绪状态,而不是马上到运行状态
  5. Dead-死亡状态:
    (1)线程的run方法执行完成,线程正常销毁
    (2)执行run方法时,抛出异常

sleep和wait的区别

  1. sleep是线程Thread类的方法,wait是Object类的方法
  2. sleep必须捕获异常,wait不需要
  3. sleep可以在任何地方使用,wait需要在同步方法或同步代码块中使用
  4. sleep没有释放锁,只是让出了CPU,而wait释放了锁,使其他方法可以使用
  5. sleep可以指定休眠时间,时间到了自然醒,而wait需要notify或notifyAll唤醒

线程之间的通信

  1. 通过wait()和notify()/notifyAll()等待通知机制
    wait()的作用是使当前线程等待,代码停止执行,直到接到通知或被终止;调用wait()之前必须获得对象的锁,即只能在同步方法或代码块中调用wait()方法,执行wait()方法后当前线程释放锁
    notify()用来通知等待对象锁的其他线程,使该线程退出等待状态,进入就绪状态;如果有多个线程等待,则由线程规划器随机挑选一个,通知它准备去获取该对象的锁;notify()执行完后,当前线程不会马上释放对象锁,而是要等执行notify()方法的线程执行完
  2. 使用join()方法
    join()方法是主线程创建并启动子线程,在等待子线程执行完或执行一定时间后再执行;可以获取子线程的结果进行运算,以此实现通信;join()的主要作用就是同步,使并行变为串行
  3. 使用ThreadLocal关键字
    线程局部变量,每个线程有自己单独的变量,一般是静态的,在该线程内可以共享,主要解决的是线程之间的隔离性

调用run()方法和start()方法的区别

直接调用run()方法相当于调用了一个普通方法,没有开启新线程,是同步的;而start()才是开启了一个新线程

线程池

线程池就是一个容纳多个线程的容器。因为线程的创建和销毁都需要占用CPU,而线程池中的线程可以反复使用,就省去了反复创建的繁琐步骤,也可以节省CPU资源的损耗,提高性能

四种线程池:
  1. Executors.newSingleThreadExecutor()单个线程池,串行
  2. Executors.newFieldThreadPool()固定数量的线程池,达到线程池最大数量后,后面的线程需要排队等待
  3. Executors.newCacheThreadPool可缓存线程池,当有新任务需要执行时,会智能添加新线程,当线程池大小超过处理任务所需的线程时,也会回收闲置线程
  4. Executors.newScheduledThreadPool()定长的线程池,支持定时及周期性任务

锁的种类

  1. 从宏观的角度上分为乐观锁和悲观锁,悲观锁认为读少写多,遇到并发写的可能性比较高,因此每次读写数据都会上锁,synchronized是悲观锁;乐观锁认为读多写少,别人拿到资源不会修改,所以不上锁,但在更新的时候会去判断别人在此期间有没有更新这个数据
  2. 公平锁和非公平锁,公平锁类似于排队获得锁,整体效率会低一点;非公平锁就是会抢占锁,效率高但可能有的线程一直在等待锁;synchronized是非公平锁,且无法实现公平锁
  3. 互斥锁与非互斥锁,从悲观锁和乐观锁的概念引申出来的,互斥锁是一次只能有一个线程持有的锁,synchronized是互斥锁
  4. 可重入锁和不可重入锁(自旋锁),某线程已拥有了某锁同步的代码块,可以再次进入该代码块,synchronized是可重入锁
  5. 类锁和对象锁,类锁使用字节码文件作为锁,比如锁在静态同步方法上,或者在同步代码块中使用.class;对象锁比如锁在同步方法上,或在方法中使用this来锁

死锁

  1. 死锁的原因
    竞争系统资源:两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法继续执行。
    可能是线程推进顺序不当

  2. 产生死锁的条件:
    (1)互斥条件(一个资源只能被一个进程持有);
    (2)请求和保持条件(因请求资源而阻塞时,保持已经拥有的资源不放);
    (3)不剥夺条件(进程获得的资源在没使用完时,其他资源无法对它剥夺占用,只能使用完自己释放);
    (4)循环等待条件(发生死锁时,必然存在一个进程-资源的环路)

  3. 如何避免死锁
    (1)线程按照一定的顺序加锁;
    (2)线程尝试获取锁的时候设置一定的加锁时限,超时则放弃对该锁的请求,并释放已有资源;
    (3)死锁检测

synchronized与lock的异同

  1. synchronized是java关键字,Lock是一个接口类
  2. synchronized是锁在类、方法或代码块上,lock是块范围内的,需要指定起始位置
  3. synchronized是JVM执行的,lock通过代码执行
  4. synchronized会自动释放锁,lock需要手动释放
  5. synchronized无法判断锁的状态,lock可以(即知道有没有获得锁)
  6. lock可以让锁的线程响应中断,synchronized不会,会一直等待

synchronized的缺陷

  1. 试图获得锁不能设置超时,不能控制阻塞时长
  2. 不能中断一个正在试图获得锁的线程,效率低
  3. 无法知道是否成功获得锁

并行与并发

  1. 相同点:在感受上都是多个任务同时进行
  2. 并行是不同代码块同时进行,并发是不同代码块交替执行
  3. 并行是在多个处理器上同时处理多个任务,并发是在一个处理器上交替执行多个任务

你可能感兴趣的:(基础知识)