1、什么是可重入锁(ReentrantLock)?

举例来说明锁的可重入性
public class UnReentrant {
    Lock lock = new Lock();
    public void outer() {
        lock.lock();
        inner();
        lock.inlock();
    }
    public void inner() {
        lock.lock();
        //do something;
        lock.unlick();
    }
}
outer 中调用了 inner,outer 先锁住了 lock,这样 inner 就不能再获取 lock。其实调用 outer 的线程已经获取了 lock 锁,但是不能在 inner 中重复利用已经获取的锁资源,这种锁即称之为不可重入。可重入就意味着:线程可以进入任何一个他已经拥有的锁所同步着的代码块。

synchronized、ReentrantLock 都是可重入的锁,可重入锁相对来说简化了并发编程的开发。

2、当一个线程进入某个对象的一个 synchronized 的实例方法后,其他线程是否可进入此对象的其他方法?

如果其他方法没有 synchronized 的话,其他线程是可以进入的,
所以要开放一个线程安全的对象时,得保证每个方法都是线程安全的。

3、乐观锁和悲观锁的理解及如何实现,有哪些实现方法?

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

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

乐观锁的实现方式:
使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可以采取丢弃和再次尝试的策略。
java 中的 Compare and Swap 即 CAS,当多个线程尝试使用 CAS 同时更新一个变量时,只有其中一个线程能更新变量的值,而其他线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。CAS 操作中包含三个操作数——需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B),如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B。否则处理器不做任何操作。

CAS 缺点:
ABA 问题:
比如说一个线程one 从 内存位置V 中取出 A,这时候另外一个线程two 也从内存中取出 A,并且 two 进行了一些操作变成了 B,然后 two 有将 V位置的数据变成 A,这时候线程one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但可能存在潜藏的问题。从 Java1.5 开始 JDK 的 atomic 包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。
循环时间长开销大:
对于资源竞争严重(线程严重冲突)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized。
只能保证一个共享变量的原子操作:
当对一个共享变量执行操作时,我们可以使用 循环CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁。

4、SynchronizedMap 和 ConcurrentHashMap 有什么区别?

SynchronizedMap 一次锁住整张表来保证线程安全,所以每次只能有一个线程来访问 map。
ConcurrentHashMap 使用分段锁来保证在多线程下的性能。ConcurrentHashMap 中则是一次锁住一个桶。ConcurrentHashMap 默认将 hash表 分为16个桶,诸如,get,put,remove 等常用操作只锁当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时16个写线程执行,并发性能的提高是显而易见的。
另外 ConcurrentHashMap 使用了一种不同的迭代方式。在这种迭代方式中,当 iterator 被创建后集合再发生改变就不再是抛出 ConcurrentModificationException,取而代之的是在改变时 new 新的数据从而不影响原有的数据,iterator 完成后再将头指针替换为新的数据,这样 iterator 线程可以使用原来老的数据,而写线程也可以并发的完成改变。

5、CopyOnWriteArrayList 可以用于什么应用场景?

CopyOnWriteArrayList(免锁容器)的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出 ConcurrentModificationExcption。 在 CopyOnWriteArrayList 中,写入将导致创建整个底层数据的副本,而原数据将保留在原地,使得复制的数据在被修改时,读取操作可以安全地执行。
由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容较多的情况下,可能导致 young gc 或者 full gc。
不能用于实时读的场景,想拷贝数组、新增元素都需要时间,所以调用一个 set 操作后,读取到数据可能还是旧的,虽然 CopyOnWriteArrayList 能做到最终一致性,但是还是无法满足实时性要求。

CopyOnWriteArrayList 透露的思想:
读写分离,读和写分开
最终一致性
使用另外开辟空间的思路,来解决并发冲突。

6、什么叫线程安全?servlet 是线程安全的吗?

线程安全是编程中的术语,指某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。
Servlet 不是线程安全的。servlet 是单实例多线程的。当多个线程同时访问同一个方法,是不能保证共享变量的线程安全的。
Struts2 的 action 是多实例多线程的,是线程安全的,每个请求过来都会 new 一个新的 action 分配给这个请求,请求完成后销毁。
SpringMVC 的 Controller 是线程安全的吗?不是的,和 Servlet 类似的处理流程。
Struts 好处是不用考虑线程安全的问题;Servlet 和 SpringMVC 需要考虑线程安全问题,但是性能可以提升不用处理太多的 gc,可以使用 ThreadLocal 来处理多线程的问题。 

7、volatile 有什么用?能否用一句话说明下 volatile 的应用场景?

volatile 保证内存可见性和禁止指令重排。
volatile 用于多线程环境下的单次操作(单次读或者单次写)。

8、为什么代码会重排序?

在执行程序时,为了提供性能,处理器和编译器常常会对指令进行重排序,但是不能随意重排序,不是你想怎么排序就怎么排序,它需要满足以下两个条件:
在但线程环境下不能改变程序运行的结果;
存在数据依赖关系的不允许重排序。
需要注意的是:重排序不会影响单线程环境的执行结果,但是会破坏多线程的执行语义。

9、在 java 中 wait 和 sleep 方法的不同?

最大的不同是在等待 wait 会释放锁,而 sleep 一直持有锁。Wait 通常被用于线程间交互,sleep 通常被用于暂停执行。

在 Java 中线程的状态一共被分成6种:
初始态:NEW
创建一个 Thread 对象,但还未调用 start()启动线程时,线程处于初始态。
运行态:RUNNABLE
在 Java 中,运行态包括就绪态和运行态。
就绪态该状态下的线程已经获得执行所需的所有资源,只要 CPU 分配执行权就能运行。所有就绪态的线程存放在就绪队列中。
运行态获得 CPU执行权,正在执行的线程。由于一个 CPU 同一时刻只能执行一条线程,因此每个 CPU 每个时刻只有一条运行态的线程。
阻塞态
当一条正在执行的线程请求某一资源失败时,就会进入阻塞态。而在 Java 中,阻塞态专指请求锁失败时进入的状态。由一个阻塞队列存放所有阻塞态的线程。处于阻塞态的线程会不断请求资源,一旦请求成功,就会进入就绪队列,等待执行。PS:锁、IO、Socket等都属于资源。
等待态“
当前线程中调用 wait、join、park 函数时,当前线程就会进入等待态。也有一个等待队列存放

标签: none

添加新评论