Java面试手册:线程专题 ④

1、进程死锁的四个必要条件以及解除死锁的基本策略:

  • 互斥条件:线程对资源的访问是排他性的,如果一个线程对占用了某资源,那么其他线程必须处于等待状态,直到资源被释放。
  • 请求和保持条件:线程T1至少已经保持了一个资源R1占用,但又提出对另一个资源R2请求,而此时,资源R2被其他线程T2占用,于是该线程T1也必须等待,但又对自己保持的资源R1不释放。
  • 不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放
  • 环路等待条件:在死锁发生时,必然存在一个“进程-资源环形链”。
  • 解除死锁的基本策略
    • 预防死锁:通过设置一些限制条件,去破坏产生死锁的必要条件
    • 避免死锁:在资源分配过程中,使用某种方法避免系统进入不安全的状态,从而避免发生死锁
    • 检测死锁:允许死锁的发生,但是通过系统的检测之后,采取一些措施,将死锁清除掉-解除死锁:该方法与检测死锁配合使用
2、如何避免死锁?

  • 死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。这是一个严重的问题,因为死锁会让你的程序挂起无法完成任务,死锁的发生必须满足以下四个条件:
    • 互斥条件:一个资源每次只能被一个进程使用。
    • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
    • 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
    • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
  • 避免死锁最简单的方法就是阻止循环等待条件,将系统中所有的资源设置标志位、排序,规定所有的进程申请资源必须以一定的顺序(升序或降序)做操作来避免死锁。这篇教程有代码示例和避免死锁的讨论细节。
  • 分析死锁,我们需要查看Java应用程序的线程转储。我们需要找出那些状态为BLOCKED的线程和他们等待的资源。每个资源都有一个唯一的id,++用这个id我们可以找出哪些线程已经拥有了它的对象锁++。
  • 避免嵌套锁,只在需要的地方使用锁和避免无限期等待是避免死锁的通常办法
3、怎么检测一个线程是否拥有锁?

  • 在java.lang.Thread中有一个方法叫holdsLock(),它返回true如果当且仅当当前线程拥有某个具体对象的锁
4、解释一下活锁:

  • 是指线程1可以使用资源,但它很礼貌,让其他线程先使用资源,线程2也可以使用资源,但它很绅士,也让其他线程先使用资源。释放完后,双方发现资源满足需求了,又都去强占资源,但是又只拿到一部分,就这样,资源在各个线程间一直往复。这样你让我,我让你,最后两个线程都无法使用资源。
5、Java中活锁和死锁有什么区别?

  • 活锁和死锁类似,不同之处在于处于活锁的线程或进程的状态是不断改变的,活锁可以认为是一种特殊的饥饿。一个现实的活锁例子是两个人在狭小的走廊碰到,两个人都试着避让对方好让彼此通过,但是因为避让的方向都一样导致最后谁都不能通过走廊。简单的说就是,活锁和死锁的主要区别是前者进程的状态可以改变但是却不能继续执行。
6、如何确保线程安全,servlet线程安全吗?

  • 在Java中可以有很多方法来保证线程安全---同步、使用原子类(atomic concurrent classes)、实现并发锁、使用volatile关键字、使用不变类和线程安全类。
    • 使用java.util.concurrent.atomic包中的Atomic Wrapper类。例如AtomicInteger
    • 使用java.util.concurrent.locks包中的锁。
    • 使用线程安全集合类,请查看此文章以了解ConcurrentHashMap的使用情况以确保线程安全。
    • 使用带有变量的volatile关键字使每个线程从内存中读取数据,而不是从线程缓存中读取。
  • 同步是java中最简单和最广泛使用的线程安全工具,同步是我们可以实现线程安全的工具,JVM保证同步代码一次只能由一个线程执行。java关键字synchronized用于创建同步代码,在内部它使用Object或Class上的锁来确保只有一个线程正在执行同步代码
    • Java同步在锁定和解锁资源时起作用,在任何线程进入同步代码之前,它必须获取对象的锁定,并且当代码执行结束时,它解锁可以被其他线程锁定的资源。同时,其他线程处于等待状态以锁定同步资源。
    • 我们可以用两种方式使用synchronized关键字,一种是使一个完整的方法同步,另一种方法是创建synchronized块。
    • 当方法同步时,它会锁定Object,如果方法是静态的,它会锁定Class,因此最好使用synchronized块来锁定需要同步的方法的唯一部分。
    • 在创建synchronized块时,我们需要提供将获取锁的资源,它可以是XYZ.class或类的任何Object字段。
    • synchronized(this) 将在进入同步块之前锁定对象。
    • 您应该使用最低级别的锁定,例如,如果类中有多个同步块,并且其中一个锁定了Object,则其他同步块也将无法由其他线程执行。当我们锁定一个Object时,它会获取Object的所有字段的锁定。
    • Java同步提供了性能成本的数据完整性,因此只有在绝对必要时才应该使用它。
    • Java同步仅在同一个JVM中工作,因此如果您需要在多个JVM环境中锁定某些资源,它将无法工作,您可能需要考虑一些全局锁定机制。
    • Java synchronized关键字不能用于构造函数和变量。
    • 最好创建一个用于同步块的虚拟私有对象,这样它的引用就不能被任何其他代码更改。例如,如果您正在同步的Object的setter方法,则可以通过其他一些代码更改其引用,以并行执行synchronized块。
    • 我们不应该例如使用字符串不应该被用于同步的是保持在常量池中的任何对象,因为如果任何其他代码也需要在同一个String锁,它会尝试从相同的参考对象上获取锁串池和即使两个代码都不相关,它们也会相互锁定。
  • servlet不是线程安全的,每个servlet都只被实例化一次,每个调用都是servlet的同一个实例,并且对类变量没有线程安全,数据量大的时候容易造成异常
7、你对线程优先级的理解是什么?

  • 每一个线程都是有优先级的,一般来说,高优先级的线程在运行时会具有优先权,但这依赖于线程调度的实现,这个实现是和操作系统相关的(OS dependent)。
  • 我们可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行。线程优先级是一个int变量(从1-10),1代表最低优先级,10代表最高优先级,默认为5。
  • 我们调用setPriority(),方法来设置优先级。
8、什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing)?

  • 线程调度器是一个操作系统服务,它负责为Runnable状态的线程分配CPU时间
  • 一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。
  • 时间分片是指将可用的CPU时间分配给可用的Runnable线程的过程。==分配CPU时间可以基于线程优先级或者线程等待的时间==。线程调度并不受到Java虚拟机控制,所以由应用程序来控制它是更好的选择(++也就是说不要让你的程序依赖于线程的优先级++)。
9、你如何确保main()方法所在的线程是Java程序最后结束的线程?

  • 我们可以使用Thread类的joint()方法来确保所有程序创建的线程在main()方法退出前结束。
  • 关于Thread类的joint()方法:
    • join()方法的作用,是等待这个线程结束,也就是说,t.join()方法阻塞调用此方法的线程(calling thread),直到线程t完成,此线程再继续;通常用于在main()主线程内,等待其它线程完成再结束main()主线程
    • Join方法实现是通过wait(Objecte提供的方法)。当main线程调用t.join时候,main线程会获得线程对象t的锁(wait 意味着拿到该对象的锁),调用该对象的wait(等待时间),直到该对象唤醒main线程 ,比如退出后。这就意味着main线程调用t.join时,必须能够拿到线程t对象的锁。
10、在多线程中,什么是上下文切换(context-switching)?

  • 上下文切换是存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。上下文切换是多任务操作系统和多线程环境的基本特征。
11、 Java中什么是竞态条件? 举个例子说明。

  • 当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件,竞态条件会导致程序在并发情况下出现一些bugs
  • 多线程对一些资源的竞争的时候就会产生竞态条件,如果首先要执行的程序竞争失败排到后面执行了,那么整个程序就会出现一些不确定的bugs。这种bugs很难发现而且会重复出现,因为线程间的随机竞争.
  • 导致竞态条件发生的代码区称作临界区。在临界区中使用适当的同步就可以避免竞态条件。
    • 临界区实现方法有两种,一种是用synchronized,一种是用Lock显式锁实现。
    • 有临界区是为了让更多的其它线程能安全够访问资源,临界区就是修改对象状态标记的代码区
12、一个线程运行时发生异常会怎样?

  • 如果异常没有被捕获,该线程将会停止执行。
  • Thread.UncaughtExceptionHandler是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。
  • 当一个未捕获异常将造成线程中断的时候JVM会使用Thread.getUncaughtExceptionHandler()来查询线程的UncaughtExceptionHandler并将线程和异常作为参数传递给handler的uncaughtException()方法进行处理。
13、如何在两个线程间共享数据?

  • 如果每个线程执行的代码相同,可以使用同一个Runnable对象,这个Runnable对象中有那个共享数据,例如,卖票系统就可以这么做。
  • 如果每个线程执行的代码不同,这时候需要用不同的Runnable对象,例如,设计4个线程。其中两个线程每次对j增加1,另外两个线程对j每次减1,银行存取款
  • 有两种方法来解决此类问题
    • 将共享数据封装成另外一个对象,然后将这个对象逐一传递给各个Runnable对象,每个线程对共享数据的操作方法也分配到那个对象身上完成,这样容易实现针对数据进行各个操作的互斥和通信
    • 将Runnable对象作为一个类的内部类,共享数据作为这个类的成员变量,每个线程对共享数据的操作方法也封装在外部类,以便实现对数据的各个操作的同步和互斥,作为内部类的各个Runnable对象调用外部类的这些方法。
  • 总结:其实多线程间的共享数据最主要的还是互斥,多个线程共享一个变量,针对变量的操作实现原子性即可

原文发布于微信公众号 - Java大联盟(javaunion)

原文发表时间:2018-11-01

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏吴伟祥

Linux下的shell简介(三) 原

        shell的本意是“壳”的意思,其实已经很形象地说明了shell在Linux系统中的作用。shell就是围绕在Linux内核之外的一个“壳”程序...

11830
来自专栏Kevin-ZhangCG

[ Java面试题 ]多线程篇

28070
来自专栏Golang语言社区

Go语言Goroutine与Channel内存模型

Go语言内存模型规定了在一个goroutine中一个变量的读取的情况下,确保能够观察到在其他另外goroutine中写入同样变量的值。也就是说,如果在多个gor...

46340
来自专栏从零开始学自动化测试

pytest文档19-doctest测试框架

doctest从字面意思上看,那就是文档测试。doctest是python里面自带的一个模块,它实际上是单元测试的一种。 官方解释:doctest 模块会搜索那...

16820
来自专栏直播开发

SRS开源直播服务 - StateThreads微线程框架学习

SRS是一个开源流媒体服务器,在目前大火的直播行业中较多的被使用。笔者作为直播行业的后台开发,对SRS的学习必不可少,本文主要讲解SRS底层使用的微线程开源...

1.3K40
来自专栏积累沉淀

干货--Redis 30分钟快速入门

一、 redis环境搭建 1.简介        redis是一个开源的key-value数据库。它又经常被认为是一个数据结构服务器。因为它的value不仅...

342100
来自专栏林欣哲

汇编程序

ISA指令集是由0和1组成的机器语言,难以记忆和阅读,因此人们发明汇编程序帮助记忆。 汇编基本算是和机器指令一一对应的关系,可以认为是给机器指令的每个部分分别起...

35360
来自专栏平凡文摘

详细分析Java中断机制

15840
来自专栏老马说编程

(22) 代码的组织机制 / 计算机程序的思维逻辑

使用任何语言进行编程都有一个类似的问题,那就是如何组织代码,具体来说,如何避免命名冲突?如何合理组织各种源文件?如何使用第三方库?各种代码和依赖库如何编译连接为...

212100
来自专栏Golang语言社区

Go语言Goroutine与Channel内存模型

Go语言内存模型规定了在一个goroutine中一个变量的读取的情况下,确保能够观察到在其他另外goroutine中写入同样变量的值。也就是说,如果在多个gor...

36760

扫码关注云+社区

领取腾讯云代金券