Java面试手册:线程专题 ③

Posted Java大联盟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java面试手册:线程专题 ③相关的知识,希望对你有一定的参考价值。

  Java大联盟

  致力于最高效的Java学习


Java面试手册:线程专题 ③

1、Thread类中的yield方法有什么作用?
  • Yield方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法而且只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU,执行yield()的线程有可能在进入到暂停状态后马上又被执行。点击这里查看更多yield方法的相关内容。

2、Runnable接⼝和Callable接⼝的区别
  • Runnable接⼝中的run()⽅法的返回值是void,它只是纯粹地去执⾏run()⽅法中的代码⽽已;

  • Callable接⼝中的call()⽅法是有返回值的,是⼀个泛型,和Future、FutureTask配合可以⽤来获取异步执⾏的结果。

3、线程安全的级别

代码在多线程下执⾏和在单线程下执⾏永远都能获得⼀样的结果,那么代码就是线程安全的。线程安全也是有级别之分的:

  • 不可变: 像String、Integer、Long这些,都是final类型的类,要改变除⾮新创建⼀个。

  • 绝对线程安全: 不管运⾏时环境如何都不需要额外的同步措施。Java中有绝对线程安全的类,⽐如CopyOnWriteArrayList、CopyOnWriteArraySet。

  • 相对线程安全: 像Vector这种,add、remove⽅法都是原⼦操作,不会被打断。如果有个线程在遍历某个Vector,同时另⼀个线程对其结构进⾏修改,会出现ConcurrentModificationException(failfast机制)。

  • 线程⾮安全: 这个就没什么好说的了,ArrayList、LinkedList、HashMap等都是线程⾮安全的类。

4、java中的锁
  1. 在Java多线程中,synchronized实现线程之间同步互斥,JDK1.5以后,Java类库中新增了Lock接口用来实现锁功能。

  2. 锁为对共享数据进行保护,同一把锁保护的共享数据,任何线程访问都需要先持有该锁。一把锁一个线程,当该锁的持有线程对数据访问结束之后必须释放该锁,让其他线程持有。++锁的持有线程在锁的获得和锁的释放之间的这段时间所执行的代码被称为临界区++。

  3. 锁能够保护共享数据以实现线程安全,主要作用有保障原子性、保障可见性和保障有序性。由于锁具有互斥性,因此当线程执行临界区中的代码时,其他线程无法做到干扰,临界区中的代码也就具有了不可分割的原子特性。

  4. 锁具有排他性,即一个锁一次只能被一个线程持有,被称之为排他锁或互斥锁。当然,新版JDK为了性能优化,推出了读写锁,读写锁是排它锁的改进。5.按照Java虚拟机对锁的实现方式划分,Java平台中的锁包括==内部锁==(主要是通过synchronized实现)和==显式锁==(主要是通过Lock接口及其实现类实现).

5、公平锁和非公平锁:
  • 锁Lock分为"公平锁"和"非公平锁":

    • 公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的FIFO先进先出顺序。

    • 非公平锁就是一种获取锁的抢占机制,是随机获得锁的,先来的不一定先得到锁,可能造成某些线程一直拿不到锁,即不公平了。

6、内部锁——众所周知的synchronized

Java平台中的任何一个对象都有唯一一个与之关联的锁,这种锁被称之为监视器(或者叫内部锁)。内部锁是一种排它锁,它能保证原子性、可见性和有序性。内部锁就由synchronized关键字实现。

  • synchronized可以修饰方法或者代码块.

    • synchronized修饰方法,该方法内部的代码就属于一个临界区,该方法就属于一个同步方法。此时一个线程对该方法内部的变量的更新就保证了原子性和可见性,从而实现了线程安全.

    • synchronized修饰代码块,需要一个锁句柄(一个对象的引用或者是一个可以返回对象的表达式),此时synchronized关键字引导的代码块就是临界区;同步块的锁句柄可以写为this关键字,表示当前对象,锁句柄对应的监视器就被称之为相应同步块的引导锁。

    • 作为锁句柄的变量通常以private final修饰,防止锁句柄变量的值改变之后,导致执行同一个同步块的多个线程使用不同的锁,从而避免了竞态。

    • 注意Java虚拟机会为每一个内部锁分配一个入口集用于存放等待获得相应内部锁的线程,当内部锁的持有线程释放当前锁的时候,可能是入口集中处于BLOCKED状态的线程获得当前锁也可能是处于RUNNABLE状态的其他线程。内部锁的竞争是激烈的,也是不公平的,可能等待了长时间的线程没有获得锁,也可能是没有经过等待的线程直接就获得了锁。

7、显式的加锁和解锁——Lock接口
  • 在Java5.0之前,在协调对共享对象的访问时可以使用的机制只有synchronized和volatile,在Java 5.0中:Lock接口(以及其实现类如ReentrantLock等),Lock接口中定义了一组抽象的加锁操作。不同的是,synchronized可以方便的隐式的获取锁,而Lock接口则提供了一种显式获取锁(排它锁)。

8、重入锁——ReentrantLock类
  • 如果一个线程持有一个锁的时候还能继续成功的申请该锁,那么我们就称该锁是可重入的,否则我们就称该锁是非可重入的。

    • ReentrantLock是一个可重入锁,ReentrantLock类与synchronized类似,都可以实现线程之间的同步互斥。但ReentrantLock类此外还扩展了更多的功能,如嗅探锁定、多路分支通知等,在使用上也比synrhronized更加的灵活。

    • 线程A和B都要获取对象O的锁定,假设A获取了对象O锁,B将等待A释放对O的锁定, 如果使用 synchronized ,如果A不释放,B将一直等下去,不能被中断 如果 使用ReentrantLock,如果A不释放,可以使B在等待了足够长的时间以后,中断等待,而干别的事情

    • ReentrantLock是一个既公平又非公平的显示锁,所以在实例化ReentrantLock类时,ReentrantLock的一个构造签名为ReentrantLock(boolean fair),传入true时是公平锁。公平锁的开销较非公平锁的开销大,因此显式锁默认使用的是非公平的调度策略。

    • 默认情况下使用内部锁,而当多数线程持有一个锁的时间相对较长或者线程申请锁的平均时间间隔相对长的情况下我们可以考虑使用显式锁。

  • ReentrantLock获取锁定与三种方式

    • lock(), 如果获取了锁立即返回,如果别的线程持有锁,当前线程则一直处于休眠状态,直到获取锁

    • tryLock(), 如果获取了锁立即返回true,如果别的线程正持有锁,立即返回false;

    • c)tryLock(long timeout,TimeUnit unit),如果获取了锁定立即返回true,如果别的线程正持有锁,会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true,如果等待超时,返回false;

    • lockInterruptibly:如果获取了锁定立即返回,如果没有获取锁定,当前线程处于休眠状态,直到或者锁定,或者当前线程被别的线程中断

9、synchronized和ReentrantLock对比
  • 在资源竞争不是很激烈的情况下,偶尔会有同步的情形下,synchronized是很合适的。原因在于,编译程序通常会尽可能的进行优化synchronized,另外可读性非常好,不管用没用过5.0多线程包的程序员都能理解。

  • ReentrantLock提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等。在资源竞争不激烈的情形下,性能稍微比synchronized差点点。但是当同步非常激烈的时候,synchronized的性能一下子能下降好几十倍。而ReentrantLock确还能维持常态。

10、读写锁——(Read/WriteLock):主要用于读线程持有锁的时间比较长的情景下。
  • ReadWriteLock接口是对读写锁的抽象,其默认的实现类是ReentrantReadWriteLock。ReadWriteLock定义了两个方法readLock()和writeLock(),分别用于返回相应读写锁实例的读锁和写锁。这两个方法的返回值类型都是Lock。

  • 读写锁是一种改进型的排它锁,读写锁允许多个线程可以同时读取(只读)共享变量

    • 读写锁是分为读锁和写锁两种角色的,读线程在访问共享变量的时候必须持有相应读写锁的读锁,而且读锁是共享的、多个线程可以共同持有的;

    • 写锁是排他的,以一个线程在持有写锁的时候,其他线程无法获得相应锁的写锁或读锁。总之,读写锁通过读写锁的分离从而提高了并发性。

11、锁的替代
  • 多个线程共享同一个非线程安全对象时,我们往往采用锁来保证线程安全性,但是,锁也有其弊端,比如锁的开销和在使用锁的时候容易发生死锁等

  • Java中也提供了一些对于某些情况下替代锁的同步机制解决方案,如volatile关键字、final关键字、static关键字、原子变量以及各种并发容器和框架.

  • 策略模式:

    • 采用线程特有对象: 各个不同的线程创建各自的实例,一个实例只能被一个线程访问的对象就被称之为线程的特有对象。采用线程特有对象,保障了对非线程安全对象的访问的线程安全。

    • 只读共享:在没有额外同步的情况下,共享的只读对象可以有可以由多个线程并发访问,但是任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象。

    • 线程安全共享:线程安全的对象在其内部实现同步,多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步。

    • 保护对象:被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象。 >   - ==volatile关键字、ThreadLocal二者在锁的某些功能上的替代作用:如下==

12、什么是ThreadLocal?
  • ThreadLocal是Java里一种特殊的变量。它是为创建代价高昂的对象获取线程安全的好方法,比如你可以用ThreadLocal让SimpleDateFormat变成线程安全的,因为那个类创建代价高昂且每次调用都需要创建不同的实例所以不值得在局部范围使用它,如果为每个线程提供一个自己独有的变量拷贝,将大大提高效率。==首先,通过复用减少了代价高昂的对象的创建个数。其次,你在没有使用高代价的同步或者不变性的情况下获得了线程安全==。线程局部变量的另一个不错的例子是ThreadLocalRandom类,它在多线程环境中减少了创建代价高昂的Random对象的个数

  • ThreadLocal用于创建线程的本地变量,我们知道一个对象的所有线程会共享它的全局变量,所以这些变量不是线程安全的,我们可以使用同步技术。但是当我们不想使用同步的时候,我们可以选择ThreadLocal变量。

  • hreadLocal为每个线程维护一个本地变量:采用空间换时间,它用于线程间的数据隔离,为每一个使用该变量的线程提供一个副本,每个线程都可以独立地改变自己的副本,而不会和其他线程的副本冲突。

  • ThreadLocal类中维护一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值为对应线程的变量副本。

13、Java中的volatile 变量是什么?
  • volatile是一个特殊的修饰符,只有成员变量才能使用它,是java提供的一种同步手段,只不过它是轻量级的同步。在Java并发程序缺少同步类的情况下,多线程对成员变量的操作对其它线程是透明的。volatile变量可以保证下一个读取操作会在前一个写操作之后发生,就是上一题的volatile变量规则。

  • 所以线程都会直接读取该变量并且不缓存它。这就确保了线程读取到的变量是同内存中是一致的。

  • 任何被volatile修饰的变量,都不拷贝副本到工作内存,任何 修改都及时写在主存

  • 要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:

    • 对变量的写操作不依赖于当前值。

    • 该变量没有包含在具有其他变量的不变式中.

14、什么场景下可以使用volatile替换synchronized?
  • 只需要保证共享资源的可见性的时候可以使用volatile替代,synchronized保证可操作的原子性一致性和可见性。volatile适用于新值不依赖于就值的情形。

  • Volatile和Synchronized四个不同点:

    • 粒度不同,前者针对变量 ,后者锁对象和类

    • syn阻塞,volatile线程不阻塞

    • syn保证三大特性,volatile不保证原子性

    • syn编译器优化,volatile不优化

15、乐观锁与悲观锁(并发编程)
  • 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。++传统的关系型数据库里边就用到了很多这种锁机制++,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如Java里面的同步原语++synchronized关键字的实现也是悲观锁++。

    • 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。

    • 一个线程持有锁会导致其它所有需要此锁的线程挂起。

    • 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。

  • 乐观锁

    • 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

    • CAS是乐观锁技术:当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

    • CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

16、当一个线程进入某个对象的一个synchronized的实例方法后,其它线程是否可进入此对象的其它方法?
  • A、一个线程在访问一个对象的同步方法时,另一个线程可以同时访问这个对象的非同步方法

  • B、 一个线程在访问一个对象的同步方法时,另一个线程不能同时访问这个同步方法。

17、synchronized和java.util.concurrent.locks.Lock的异同?
  • Lock 和 synchronized 有一点明显的区别 —— lock 必须在 finally 块中释放。否则,如果受保护的代码将抛出异常,锁就有可能永远得不到释放!这一点区别看起来可能没什么,但是实际上,它极为重要。忘记在 finally 块中释放锁,可能会在程序中留下一个定时炸弹,当有一天炸弹爆炸时,您要花费很大力气才有找到源头在哪。而使用同步,JVM 将确保锁会获得自动释放。

18、SynchronizedMap和ConcurrentHashMap有什么区别?
  • java5中新增了ConcurrentMap接口和它的一个实现类ConcurrentHashMap。ConcurrentHashMap提供了和Hashtable以及SynchronizedMap中所不同的锁机制,比起synchronizedMap来,它提供了好得多的并发性

    • 多个读操作几乎总可以并发地执行,同时进行的读和写操作通常也能并发地执行,而同时进行的写操作仍然可以不时地并发进行(相关的类也提供了类似的多个读线程的并发性,但是,只允许有一个活动的写线程)

  • Hashtable中采用的锁机制是一次锁住整个hash表,从而同一时刻只能由一个线程对其进行操作;而ConcurrentHashMap中则是一次锁住一个桶。ConcurrentHashMap默认将hash表分为16个桶,诸如get,put,remove等常用操作只锁当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有16个写线程执行,并发性能的提升是显而易见的。前面说到的16个线程指的是写线程,而读操作大部分时候都不需要用到锁。只有在size等操作时才需要锁住整个hash表。

  • 在迭代方面,ConcurrentHashMap使用了一种不同的迭代方式。在这种迭代方式中,当iterator被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时new新的数据从而不影响原有的数据,iterator完成后再将头指针替换为新的数据,这样iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变。

  • CopyOnWriteArrayList可以用于什么应用场景?

    • CopyOnWriteArrayList(免锁容器)的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出ConcurrentModificationException。在CopyOnWriteArrayList中,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行。

19、同步方法和同步块,哪个是更好的选择?
  • 同步块是更好的选择,因为它不会锁住整个对象(当然你也可以让它锁住整个对象)。同步方法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这通常会导致他们停止执行并需要等待获得这个对象上的锁。




Java面试手册:线程专题 ③









以上是关于Java面试手册:线程专题 ③的主要内容,如果未能解决你的问题,请参考以下文章

Java面试系列之并发编程专题-Java线程池灵魂拷问

Java面试系列之并发编程专题-Java线程池灵魂拷问

BAT大厂面试必问专题之Java多线程

美团面试题:Java-线程池 ThreadPool 专题详解

Java-线程池专题 (美团面试题)

Java面试经典题:线程池专题