Java 面试之线程与锁
发布日期:2021-06-30 17:39:14 浏览次数:3 分类:技术文章

本文共 4884 字,大约阅读时间需要 16 分钟。

原文:

进程、线程

  进程是操作系统分配资源的最小单元,线程是操作系统调度的最小单元。一个进程一般包括多个线程,这些线程共享本进程的内存和资源。

线程的状态

线程状态

实现线程的几种方式

  • 继承Thread类创建线程
  • 实现Runnable接口创建线程
  • 实现Callable接口创建新线程(可用Future返回结果)

各种比较

sleep()和wait(),yield()和notify()

  • sleep()是Thread类的一个静态函数,它会使调用线程睡眠(阻塞)一段时间,让其他线程有机会继续执行,但它不释放锁
  • wait()是Object类的方法,它会使当前线程阻塞,直到调用notify(),则被唤醒,它会释放锁
  • yield()是Thread类的方法,它会使运行中的线程重新变为就绪状态,让同优先级线程重新竞争。
  • notify()是Object类的方法,它会唤醒单个线程。

synchronized和volatile的区别

  1. synchronized是Java中的关键字,是一种同步锁。有如下几种用法:

    //1. 修饰方法(方法锁)public synchronized void syncMethod() {
    //doSomething}

//2. 修饰代码块(对象锁)

public int synMethod(int arg){
synchronized(arg) {
//doSomething
}
}

//3. 修饰类(类锁)

public class SyncClass {
public void method() {
synchronized(SyncClass.class) {
//doSomething
}
}
}

锁的级别有对象级别和类级别,1和2属于对象级别,3属于类级别

  • volatile 是在告诉JVM当前变量在cpu缓存中的值是不确定的,需要从主存中读取(禁止指令的重排序); synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。volatile与synchronized的区别如下:

  • 比较点 volatile synchronized
    阻塞 不会发生线程阻塞 会发生线程阻塞
    修饰 变量 方法、代码块、类
    原子性 不能保证 可以保证
    安全性 非线程安全 线程安全
    解决问题 变量在多线程之间的可见性 多线程之间访问资源的同步性

    sychronized、Lock

    比较点 sychronized Lock
    解释 Java关键字 Java接口
    显隐 隐式锁 需显示指定起始位置和终止位置
    释放锁 获取锁的线程会在执行完同步代码后自动释放锁(或者JVM会在线程执行发生异常时释放锁) 在finally中必须释放锁,不然容易造成线程死锁
    等待 一个线程获得锁后阻塞,其他线程会一直等待 线程不会一直等待,超时会释放
    锁类型 可重入但不可中断、非公平 可重入、可中断、可公平也可不公平

    ThreadLocal

      设计理念是为了减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。作用是提供线程内部的局部变量,这些变量在多线程环境下访问(get/set)时能保证与其它线程里的变量相对独立。打个比方,多人(多个线程)使用自己的交通卡(线程私有变量)乘公交转地铁(两个函数)。

      注意:使用 ThreadLocal 时要保证能够管理它的创建、销毁,否则会出问题。因为 ThreadLocal 是和 Thread 绑定的,如果 Thread 是从 ThreadPool 中拿出来的,那么意味着 Thread 可能会被复用。如果被复用,你就一定得保证这个 Thread 上一次结束的时候,其关联的 ThreadLocal 被清空掉,否则就会串到下一次使用。

    应用场景:银行转账包含一系列的操作,把转出账户的余额减少,把转入账户的余额增加,这两个操作要在同一个事务中完成,它们必须使用相同的数据库连接对象,而这个连接信息可以用ThreadLocal保存。

    ThreadPool的用法与优势

    线程池的用法

      使用ThreadPoolExecutor创建线程池,其中5参构造函数参数列表如下:

    • int corePoolSize:线程池中核心线程数,一般为cpu数量

    • int maximumPoolSize: 线程池中线程总数,一般为2*cpu数量(I/O密集型时maxSize设置大一点,提高cpu利用率)

    • long keepAliveTime:线程池中非核心线程闲置超时时长

    • TimeUnit unit:keepAliveTime的单位

    • BlockingQueue workQueue:线程池中的任务队列,维护着等待执行的Runnable对象

      • SynchronousQueue:接收到任务时,会直接提交给线程处理,而不保留它
      • LinkedBlockingQueue:接收到任务时,如果当前线程数小于核心线程数,则新建核心线程处理任务;如果当前线程数等于核心线程数,则进入队列等待
      • ArrayBlockingQueue:接收到任务时,如果没有达到corePoolSize的值,则新建核心线程执行任务;如果达到了,则入队等候;如果队列已满,则新建非核心线程执行任务;如果总线程数到了maximumPool,且队列也满了,则发生错误
      • DelayQueue:接收到任务时,首先先入队,只有达到了指定的延时时间,才会执行任务。注意:队列内元素必须实现Delayed接口,这就意味着你传进去的任务必须先实现Delayed接口
    • RejectedExecutionHandler handler:BlockingQueue 打满时的几种拒绝策略

      • Abort策略:默认策略,新任务提交时直接抛出未检查的异常。
      • RejectedExecutionException,该异常可由调用者捕获。
      • CallerRuns策略:为调节机制,既不抛弃任务也不抛出异常,而是将某些任务回退到调用者。不会在线程池的线程中执行新的任务,而是在调用exector的线程中运行新的任务。
      • Discard策略:新提交的任务被抛弃。
      • DiscardOldest策略:队列的是“队头”的任务,然后尝试提交新的任务。

      向线程池提交一个要执行的任务threadPoolExecutor.execute(runnable);

      当新提交一个任务时:

    1. 如果当前线程数<corePoolSize,新增一个核心线程处理新的任务。
    2. 如果当前线程数=corePoolSize,新任务会被放入阻塞队列等待。
    3. 如果阻塞队列满了,且当前线程数<maximumPoolSize,新增线程来处理任务。
    4. 如果阻塞队列满了,且当前线程数=maximumPoolSize,那么线程池已达极限,此时会根据饱和策略RejectedExecutionHandler拒绝新的任务。

    使用线程池的优势:

    • 降低资源消耗:重复利用已创建的线程,降低创建和销毁造成的消耗。
    • 提高响应速度:任务可以不需要等到线程创建就能立即执行(参考上条)。
    • 提高管理性:可以进行统一的分配、调优和监控。

    三大常用并发工具类

    • Semaphore:并发控制,控制刷新账单并发数,tryAcquire()、release()
    • CountDownLanch:计数器,与会人员到齐了会议才能开始,await()、countDown()
    • Cyclicbarrier:同步屏障(可以被重用),等待本周每天的账单都计算完之后,再计算日均开销,await()

    concurrent包

    • Executor接口:具体Runnable任务的执行者。
    • Executors类:创建线程池工具类(阿里手册禁止用此工具类)。
    • ExecutorService接口:线程池管理者,可提交Runnable、Callable让其调度。
    • ThreadPoolExecutor类:线程池工具类。(ExecutorService的一种具体实现)。
    • CompletionService接口:ExecutorService的扩展,可以获得线程执行结果。
    • ReentrantLock类:可重入互斥锁(实现Lock接口)。
    • BlockingQueue接口:阻塞队列。
    • Future接口:一个线程执行结束后取返回的结果,还提供了cancel()终止线程。
    • CountDownLatch类:当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。

    死锁的必要条件,怎么处理死锁

    死锁产生的必要条件

    • 互斥条件:进程对所分配的资源进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
    • 不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程主动释放。
    • 请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
    • 环路等待条件:存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。

    避免死锁:

    • 加锁顺序(线程按一定顺序加锁,若所有线程都按相同顺序获得锁,就能避免死锁)
    • 加锁时限(线程获取锁时加上时限,超时则放弃并释放所占有的锁,就能避免死锁)
    • 死锁检测(一个更优的预防机制,主要针对不可能实现按序加锁和加锁时限的场景)

    锁的类型

    解释
    公平锁/非公平锁 公平锁是指多个线程按照申请锁的顺序来获取锁
    可重入锁 同一线程在外层方法已获取锁时,进入内层方法会自动获取锁
    独享锁/共享锁 一次可被单个/多个线程所持有,例如ReadWriteLock的写锁/读锁
    互斥锁/读写锁 一种互斥锁:ReentrantLock;一种读写锁:ReadWriteLock
    乐观锁/悲观锁 悲观锁认为对于同一个数据的并发操作一定会发生修改
    分段锁 一种锁的设计,具体应用有ConcurrentHashMap
    自旋锁 尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取
    偏向锁/轻量级锁/重量级锁 指锁的状态,并且是针对synchronized

    偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但当自旋一定次数还没获取到锁的时候,就会进入阻塞,该锁膨胀为重量级锁。

    分布式锁

      一种跨服务器(JVM)控制共享资源访问的互斥机制。在分布式系统环境下,一个方法在同一时间只能被一台机器的一个线程执行。

    1. 基于数据库实现分布式锁:在数据库中创建一张表,表中包含方法名字段,并在方法名字段上创建唯一索引,想要执行某个方法,就使用这个方法名向表中插入数据,成功插入则获取锁,执行完成后删除对应的行数据释放锁。
    2. 基于缓存(Redis等)实现分布式锁:获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为当前时间加上锁定时间,释放锁的时候执行delete进行释放。
    3. 基于Zookeeper实现分布式锁:①创建一个目录dislock;②线程A想获取锁就在dislock目录下创建临时顺序节点;③获取dislock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;④线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;⑤线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。

转载地址:https://lemonstone.blog.csdn.net/article/details/85613989 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:Java 面试之数据库
下一篇:Java 面试之 JVM

发表评论

最新留言

逛到本站,mark一下
[***.202.152.39]2024年04月30日 10时02分57秒

关于作者

    喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!

推荐文章