面试之 Java 并发编程(三)

17、多线程同步和互斥有几种实现方法,都是什么

线程同步是指线程之间所具有的一种制约关系, 一个线程的执行依赖另一个线程的消息, 当它没有得到另一个线程的消息时应等待, 直到消息到达时才被唤醒。线程互斥是指对 于共享的进程系统资源, 在各单个线程访问时的排它性。当有若干个线程都要使用某一共 享资源时, 任何时刻最多只允许一个线程去使用, 其它要使用该资源的线程必须等待, 直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步。 线程间的同步方法大体可分为两类: 用户模式和内核模式。顾名思义, 内核模式就是指利 用系统内核对象的单一性来进行同步, 使用时需要切换内核态与用户态, 而用户模式就是不需 要切换到内核态, 只在用户态完成操作。 用户模式下的方法有: 原子操作( 例如一个单一的全局变量), 临界区。内核模式下 的方法有: 事件, 信号量, 互斥量。

 

18、什么是竞争条件?你怎样发现和解决竞争? 当多个进程都企图对共享数据进行某种处理, 而最后的结果又取决于进程运行的顺序时, 则我们认为这发生了竞争条件( race condition)。 19、你将如何使用 thread dump?你将如何分析 Thread dump? 新建状态( New) 用 new 语句创建的线程处于新建状态,此时它和其他 Java 对象一样,仅仅在堆区中被分 配了内存。 就绪状态( Runnable)

当一个线程对象创建后,其他线程调用它的 start()方法,该线程就进入就绪状态, Java 虚 拟机会为它创建方法调用栈和程序计数器。处于这个状态的线程位于可运行池中, 等待获 得 CPU 的使用权。 运行状态( Running) 处于这个状态的线程占用 CPU, 执行程序代码。只有处于就绪状态的线程才有机会转到 运行状态。 阻塞状态( Blocked) 阻塞状态是指线程因为某些原因放弃 CPU, 暂时停止运行。当线程处于阻塞状态时, Java 虚拟机不会给线程分配 CPU。直到线程重新进入就绪状态, 它才有机会转到运行状 态。 阻塞状态可分为以下 3 种: 位于对象等待池中的阻塞状态( Blocked in object’ s wait pool) : 当线程处于运行状态时, 如果执行了某个对象的 wait()方法, Java 虚拟机就会把线程放 到这个对象的等待池中, 这涉及到“ 线程通信” 的内容。 位于对象锁池中的阻塞状态( Blocked in object’ s lock pool) : 当线程处于运行状态时, 试图获得某个对象的同步锁时, 如果该对象的同步锁已经被其 他线程占用, Java 虚拟机就会把这个线程放到这个对象的锁池中, 这涉及到“ 线程同 步” 的内容。 其他阻塞状态( Otherwise Blocked) : 当前线程执行了 sleep()方法,或者调用了其他线程的 join()方法,或者发出了 I/O 请求 时, 就会进入这个状态。

死亡状态( Dead) 当线程退出 run()方法时, 就进入死亡状态, 该线程结束生命周期。 我们运行之前的那个死锁代码 SimpleDeadLock.java, 然后尝试输出信息( /* 时间,jvm 信息 */ 2017-11-01 17:36:28 Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.144-b01 mixed mode): /* 线程名称:DestroyJavaVM 编号:#13 优先级:5 系统优先级:0 jvm 内部线程 id:0x0000000001c88800 对应系统线程 id(NativeThread ID):0x1c18 线程状态: waiting on condition [0x0000000000000000] (等待某个条件) 线程详细状态:java.lang.Thread.State: RUNNABLE 及 之 后 所 有 */ "DestroyJavaVM" #13 prio=5 os_prio=0 tid=0x0000000001c88800 nid=0x1c18 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "Thread-1" #12 prio=5 os_prio=0 tid=0x0000000018d49000 nid=0x17b8 waiting for monitor entry [0x0000000019d7f000] /* 线程状态:阻塞(在对象同步上) 代码位置:at com.leo.interview.SimpleDeadLock$B.run(SimpleDeadLock.java:56) 等待锁:0x00000000d629b4d8 已 经 获 得 锁 :0x00000000d629b4e8*/ java.lang.Thread.State: BLOCKED (on object monitor) at com.leo.interview.SimpleDeadLock$B.run(SimpleDeadLock.java:56)

- waiting to lock (a java.lang.Object) - locked (a java.lang.Object) "Thread-0" #11 prio=5 os_prio=0 tid=0x0000000018d44000 nid=0x1ebc waiting for monitor entry [0x000000001907f000] java.lang.Thread.State: BLOCKED (on object monitor) at com.leo.interview.SimpleDeadLock$A.run(SimpleDeadLock.java:34) - waiting to lock (a java.lang.Object) - locked (a java.lang.Object) "Service Thread" #10 daemon prio=9 os_prio=0 tid=0x0000000018ca5000 nid=0x1264 runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE "C1 CompilerThread2" #9 daemon prio=9 os_prio=2 tid=0x0000000018c46000 nid=0xb8c waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "C2 CompilerThread1" #8 daemon prio=9 os_prio=2 tid=0x0000000018be4800 nid=0x1db4 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "C2 CompilerThread0" #7 daemon prio=9 os_prio=2 tid=0x0000000018be3800 nid=0x810 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE

 

20、为什么我们调用 start()方法时会执行run()方法,为什么 我们不能直接调用run()方法? 当你调用 start()方法时你将创建新的线程, 并且执行在 run()方法里的代码。 但是如果你直接调用 run()方法,它不会创建新的线程也不会执行调用线程的代码, 只会把 run 方法当作普通方法去执行。 21、Java 中你怎样唤醒一个阻塞的线程? 在 Java 发展史上曾经使用 suspend()、resume()方法对于线程进行阻塞唤醒,但随之出 现很多问题, 比较典型的还是死锁问题。 解决方案可以使用以对象为目标的阻塞, 即利用 Object 类的 wait()和 notify()方法实现 线程阻塞。 首先,wait、notify 方法是针对对象的,调用任意对象的 wait()方法都将导致线程阻塞, 阻塞的同时也将释放该对象的锁,相应地,调用任意对象的 notify()方法则将随机解除该对 象阻塞的线程, 但它需要重新获取改对象的锁, 直到获取成功才能往下执行; 其次, wait、notify 方法必须在 synchronized 块或方法中被调用, 并且要保证同步块或方法的 锁对象与调用 wait、notify 方法的对象是同一个, 如此一来在调用 wait 之前当前线程就 已经成功获取某对象的锁,执行 wait 阻塞后当前线程就将之前获取的对象锁释放。

22、在 Java 中CycliBarriar 和CountdownLatch 有什么区 别? CyclicBarrier 可以重复使用, 而 CountdownLatch 不能重复使用。

Java 的 concurrent 包里面的 CountDownLatch 其实可以把它看作一个计数器, 只不过 这个计数器的操作是原子操作, 同时只能有一个线程去操作这个计数器, 也就是同时只 能有一个线程去减这个计数器里面的值。 你可以向 CountDownLatch 对象设置一个初始的数字作为计数值,任何调用这个对象上 的 await()方法都会阻塞,直到这个计数器的计数值被其他的线程减为 0 为止。 所以在当前计数到达零之前, await 方法会一直受阻塞。之后, 会释放所有等待的线 程,await 的所有后续调用都将立即返回。这种现象只出现一次— — 计数无法被重置。 如果需要重置计数, 请考虑使用 CyclicBarrier。 CountDownLatch 的一个非常典型的应用场景是: 有一个任务想要往下执行, 但必须要 等到其他的任务执行完毕后才可以继续往下执行。假如我们这个想要继续往下执行的任务 调用一个 CountDownLatch 对象的 await() 方法, 其他的任务执行完自己的任务后调用 同一个 CountDownLatch 对象上的 countDown()方法, 这个调用 await()方法的任务将 一直阻塞等待, 直到这个 CountDownLatch 对象的计数值减到 0 为止。 CyclicBarrier 一个同步辅助类, 它允许一组线程互相等待, 直到到达某个公共屏障点 (common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互 相等待, 此时 CyclicBarrier 很有用。因为该 barrier 在释放等待线程后可以重用, 所以 称它为循环 的 barrier。

 

23、什么是不可变对象,它对写并发应用有什么帮助? 不可变对象(Immutable Objects)即对象一旦被创建它的状态( 对象的数据,也即对象属 性值) 就不能改变, 反之即为可变对象(Mutable Objects)。 不可变对象的类即为不可变类(Immutable Class)。Java 平台类库中包含许多不可变类, 如 String、基本类型的包装类、BigInteger 和 BigDecimal 等。 不可变对象天生是线程安全的。它们的常量( 域) 是在构造函数中创建的。既然它们的 状态无法修改, 这些常量永远不会变。不可变对象永远是线程安全的。 只有满足如下状态, 一个对象才是不可变的; 它 的状态不能在创建后再被修改; 所有域都是 final 类型; 并且, 它被正确创建( 创建期间没有发生 this 引用的逸出)。

 

24、什么是多线程中的上下文切换? 在上下文切换过程中, CPU 会停止处理当前运行的程序, 并保存当前程序运行的具体位置 以便之后继续运行。从这个角度来看, 上下文切换有点像我们同时阅读几本书, 在来回切 换书本的同时我们需要记住每本书当前读到的页码。在程序中, 上下文切换过程中的“ 页 码” 信息是保存在进程控制块( PCB) 中的。PCB 还经常被称作“ 切换桢” ( switchframe)。“ 页码” 信息会一直保存到 CPU 的内存中, 直到他们被再次使用。 上下文切换是存储和恢复 CPU 状态的过程, 它使得线程执行能够从中断点恢复执行。上 下文切换是多任务操作系统和多线程环境的基本特征。

 

25、Java 中用到的线程调度算法是什么? 计算机通常只有一个 CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU 的使 用权才能执行指令.所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得 CPU 的 使用权,分别执行各自的任务.在运行池中,会有多个处于就绪状态的线程在等待 CPU,JAVA 虚拟机的一项任务就是负责线程的调度,线程调度是指按照特定机制为多个线程分配 CPU 的使用权. 有两种调度模型: 分时调度模型和抢占式调度模型。 分时调度模型是指让所有的线程轮流获得 cpu 的使用权,并且平均分配每个线程占用的 CPU时间片这个也比较好理解。java 虚拟机采用抢占式调度模型, 是指优先让可运行池中优先级高的线程占用CPU, 如 果可运行池中的线程优先级相同, 那么就随机选择一个线程, 使其占用CPU。处于运行 状态的线程会一直运行, 直至它不得不放弃 CPU。

 

26、什么是线程组,为什么在 Java 中不推荐使用? 线程组和线程池是两个不同的概念, 他们的作用完全不同, 前者是为了方便线程的管理, 后者是为了管理线程的生命周期, 复用线程, 减少创建销毁线程的开销

 

27、为什么使用 Executor 框架比使用应用创建和管理线程好? 为什么要使用 Executor 线程池框架 1、每次执行任务创建线程 new Thread()比较消耗性能, 创建一个线程是比较耗时、耗 资源的。 2、调用 new Thread()创建的线程缺乏管理, 被称为野线程, 而且可以无限制的创 建, 线程之间的相互竞争会导致过多占用系统资源而导致系统瘫痪, 还有线程之间 的频繁交替也会消耗很多系统资源。 3、直接使用 new Thread() 启动的线程不利于扩展, 比如定时执行、定期执行、 定时定期执行、线程中断等都不便实现。 使用 Executor 线程池框架的优点 1、能复用已存在并空闲的线程从而减少线程对象的创建从而减少了消亡线程的开 销。 2、可有效控制最大并发线程数, 提高系统资源使用率, 同时避免过多资源竞争。 3、框架中已经有定时、定期、单线程、并发数控制等功能。 综上所述使用线程池框架 Executor 能更好的管理线程、提供系统资源使用率。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值