不是吧阿 sir,你这多线程并发也太熟了吧,震惊面试官第四年
java 乐观锁机制:
? 乐观锁体现的是悲观锁的反面。它是一种积极的思想,它总是认为数据是不会被修改的,所以是不会对数据上锁的。但是乐观锁在更新的时候会去判断数据是否被更新过。乐观锁的实现方案一般有两种(版本号机制和 CAS)。乐观锁适用于读多写少的场景,这样可以提高系统的并发量。在 Java 中 java.util.concurrent.atomic 下的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。
乐观锁,大多是基于数据版本 (Version)记录机制实现。即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来 实现。 读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提 交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据 版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
CAS 思想:
? CAS 就是 compare and swap(比较交换),是一种很出名的无锁的算法,就是可以不使用锁机制实现线程间的同步。使用 CAS 线程是不会被阻塞的,所以又称为非阻塞同步。CAS 算法涉及到三个操作:
? 需要读写内存值 V;
? 进行比较的值 A;
? 准备写入的值 B
? 当且仅当 V 的值等于 A 的值等于 V 的值的时候,才用 B 的值去更新 V 的值,否则不会执行任何操作(比较和替换是一个原子操作-A 和 V 比较,V 和 B 替换),一般情况下是一个自旋操作,即不断重试
缺点:
? 高并发的情况下,很容易发生并发冲突,如果 CAS 一直失败,那么就会一直重试,浪费 CPU 资源
原子性:
? 功能限制 CAS 是能保证单个变量的操作是原子性的,在 Java 中要配合使用 volatile 关键字来保证线程的安全;当涉及到多个变量的时候 CAS 无能为力;除此之外 CAS 实现需要硬件层面的支持,在 Java 的普通用户中无法直接使用,只能借助 atomic 包下的原子类实现,灵活性受到了限制
6、synchronized 使用方法?底层实现?
使用方法:主要的三种使??式
? 修饰实例?法: 作?于当前对象实例加锁,进?同步代码前要获得当前对象实例的锁
? 修饰静态?法: 也就是给当前类加锁,会作?于类的所有对象实例,因为静态成员不属于任何?个实例对象,是类成员( static 表明这是该类的?个静态资源,不管 new 了多少个对象,只有?份)。所以如果?个线程 A 调??个实例对象的?静态 synchronized ?法,?线程 B 需要调?这个实例对象所属类的静态 synchronized ?法,是允许的,不会发?互斥现象,因为访问静态 synchronized ?法占?的锁是当前类的锁,?访问?静态 synchronized ?法占?的锁是当前实例对象锁。
? 修饰代码块: 指定加锁对象,对给定对象加锁,进?同步代码库前要获得给定对象的锁。
? 总结:synchronized 锁住的资源只有两类:一个是对象,一个是类。
底层实现:
? 对象头是我们需要关注的重点,它是 synchronized 实现锁的基础,因为 synchronized 申请锁、上锁、释放锁都与对象头有关。对象头主要结构是由 Mark Word 和 Class Metadata Address 组成,其中 Mark Word 存储对象的 hashCode、锁信息或分代年龄或 GC 标志等信息,Class Metadata Address 是类型指针指向对象的类元数据,JVM 通过该指针确定该对象是哪个类的实例。
? 锁也分不同状态,JDK6 之前只有两个状态:无锁、有锁(重量级锁),而在 JDK6 之后对 synchronized 进行了优化,新增了两种状态,总共就是四个状态:无锁状态、偏向锁、轻量级锁、重量级锁,其中无锁就是一种状态了。锁的类型和状态在对象头 Mark Word 中都有记录,在申请锁、锁升级等过程中 JVM 都需要读取对象的 Mark Word 数据。
? 每一个锁都对应一个 monitor 对象,在 HotSpot 虚拟机中它是由 ObjectMonitor 实现的(C++实现)。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如 monitor 可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。
7、ReenTrantLock 使用方法?底层实现?和 synchronized 区别?
? 由于 ReentrantLock 是 java.util.concurrent 包下提供的一套互斥锁,相比 Synchronized,ReentrantLock 类提供了一些高级功能,主要有以下三项:
1.等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于 Synchronized 来说可以避免出现死锁的情况。通过 lock.lockInterruptibly()来实现这个机制。
2.公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized 锁非公平锁,ReentrantLock 默认的构造函数是创建的非公平锁,可以通过参数 true 设为公平锁,但公平锁表现的性能不是很好。
3.锁绑定多个条件,一个 ReentrantLock 对象可以同时绑定对个对象。ReenTrantLock 提供了一个 Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像 synchronized 要么随机唤醒一个线程要么唤醒全部线程。
使用方法:
? 基于 API 层面的互斥锁,需要 lock()和 unlock()方法配合 try/finally 语句块来完成
底层实现:
? ReenTrantLock 的实现是一种自旋锁,通过循环调用 CAS 操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。想尽办法避免线程进入内核的阻塞状态是我们去分析和理解锁设计的关键钥匙。
和 synchronized 区别:
? 1、底层实现上来说,synchronized 是 JVM 层面的锁,是 Java 关键字,通过 monitor 对象来完成(monitorenter 与 monitorexit),对象只有在同步块或同步方法中才能调用 wait/notify 方法;ReentrantLock 是从 jdk1.5 以来(java.util.concurrent.locks.Lock)提供的 API 层面的锁。synchronized 的实现涉及到锁的升级,具体为无锁、偏向锁、自旋锁、向 OS 申请重量级锁;ReentrantLock 实现则是通过利用 CAS(CompareAndSwap)自旋机制保证线程操作的原子性和 volatile 保证数据可见性以实现锁的功能。
? 3、是否可手动释放:synchronized 不需要用户去手动释放锁,synchronized 代码执行完后系统会自动让线程释放对锁的占用; ReentrantLock 则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象。一般通过 lock()和 unlock()方法配合 try/finally 语句块来完成,使用释放更加灵活。
? 4、是否可中断 synchronized 是不可中断类型的锁,除非加锁的代码中出现异常或正常执行完成; ReentrantLock 则可以中断,可通过 trylock(long timeout,TimeUnit unit)设置超时方法或者将 lockInterruptibly()放到代码块中,调用 interrupt 方法进行中断。
? 5、是否公平锁 synchronized 为非公平锁 ReentrantLock 则即可以选公平锁也可以选非公平锁,通过构造方法 new ReentrantLock 时传入 boolean 值进行选择,为空默认 false 非公平锁,true 为公平锁。
8、公平锁和非公平锁区别?为什么公平锁效率低?
公平锁:
? 公平锁自然是遵循 FIFO(先进先出)原则的,先到的线程会优先获取资源,后到的会进行排队等待
? 优点: 所有的线程都能得到资源,不会饿死在队列中。
? 缺点: 吞吐量会下降,队列里面除了第一个线程,其他的线程都会阻塞,cpu 唤醒阻塞线程的开销大
非公平锁:
? 多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
? 优点: 可以减少 CPU 唤醒线程的开销,整体的吞吐效率会高点,CPU 也不必取唤醒所有线程,会减少唤起线程的数量。
? 缺点: 你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁
公平锁效率低原因:
? 公平锁要维护一个队列,后来的线程要加锁,即使锁空闲,也要先检查有没有其他线程在 wait,如果有自己要挂起,加到队列后面,然后唤醒队列最前面线程。这种情况下相比较非公平锁多了一次挂起和唤醒。
? 线程切换的开销,其实就是非公平锁效率高于公平锁的原因,因为非公平锁减少了线程挂起的几率,后来的线程有一定几率逃离被挂起的开销。
9、锁优化。自旋锁、自适应自旋锁、锁消除、锁粗化、偏向锁、轻量级锁、重量级锁解释
锁优化:
? 【1】减少锁的时间:? 不需要同步执行的代码,能不放在同步快里面执行就不要放在同步快内,可以让锁尽快释放;
? 【2】减少锁的粒度:? 它的思想是将物理上的一个锁,拆成逻辑上的多个锁,增加并行度,从而降低锁竞争。它的思想也是用空间来换时间;java 中很多数据结构都是采用这种方法提高并发操作的效率,比如:
? ConcurrentHashMap:
? java 中的 ConcurrentHashMap 在 jdk1.8 之前的版本,使用一个 Segment 数组:Segment< K,V >[] segments
? Segment 继承自 ReenTrantLock,所以每个 Segment 是个可重入锁,每个 Segment 有一个 HashEntry< K,V >数组用来存放数据,put 操作时,先确定往哪个 Segment 放数据,只需要锁定这个 Segment,执行 put,其它的 Segment 不会被锁定;所以数组中有多少个 Segment 就允许同一时刻多少个线程存放数据,这样增加了并发能力。
Segment 继承自 ReenTrantLock,所以每个 Segment 就是个可重入锁,每个 Segment 有一个 HashEntry< K,V >数组用来存放数据,put 操作时,先确定往哪个 Segment 放数据,只需要锁定这个 Segment,执行 put,其它的 Segment 不会被锁定;所以数组中有多少个 Segment 就允许同一时刻多少个线程存放数据,这样增加了并发能力。
? 【3】锁粗化:? 大部分情况下我们是要让锁的粒度最小化,锁的粗化则是要增大锁的粒度;
? 在以下场景下需要粗化锁的粒度:
? 假如有一个循环,循环内的操作需要加锁,我们应该把锁放到循环外面,否则每次进出循环,都进出一次临界区,效率是非常差的;
? 【4】使用读写锁:
? ReentrantReadWriteLock 是一个读写锁,读操作加读锁,可并发读,写操作使用写锁,只能单线程写;
? 【5】使用 cas:
? 如果需要同步的操作执行速度非常快,并且线程竞争并不激烈,这时候使用 cas 效率会更高,因为加锁会导致线程的上下文切换,如果上下文切换的耗时比同步操作本身更耗时,且线程对资源的竞争不激烈,使用 volatiled+cas 操作会是非常高效的选择;
自旋锁:
? 自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
? 缺点: 如果锁被其他线程长时间占用,一直不释放 CPU,会带来许多的性能开销;自旋次数默认值是 10
自适应自旋锁:
? 对上面自旋锁优化方式的进一步优化,它的自旋的次数不再固定,其自旋的次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,这就解决了自旋锁带来的缺点
锁消除:
? 锁削除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行削除。
锁粗化:
? 假如有一个循环,循环内的操作需要加锁,我们应该把锁放到循环外面,否则每次进出循环,都进出一次临界区,效率是非常差的;
偏向锁:
? 所谓的偏向,就是偏心,即锁会偏向于当前已经占有锁的线程;也就是说,这个线程已经占有这个锁,当他在次试图去获取这个锁的时候,他会已最快的方式去拿到这个锁,而不需要在进行一些 monitor 操作,因此这方面他是会对性能有所提升的,因为在大部分情况下是没有竞争的,所以锁此时是没用的,所以使用偏向锁是可以提高性能的;
重量级锁:
? 重量级锁的加锁、解锁过程和轻量级锁差不多,区别是:竞争失败后,线程阻塞,释放锁后,唤醒阻塞的线程,不使用自旋锁,不会那么消耗 CPU,所以重量级锁适合用在同步块执行时间长的情况下。
10、Java 内存模型
? Java 内存模型(Java Memory Model,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了 Java 程序在各种平台下对内存的访问都能保证效果一致的机制及规范。
? JMM 是一种规范,是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。目的是保证并发编程场景中的原子性、可见性和有序性。
? 所以,Java 内存模型,除了定义了一套规范,还提供了一系列原语,封装了底层实现后,供开发者直接使用。我们前面提到,并发编程要解决原子性、有序性和一致性的问题。
原子性:
? 在 Java 中,为了保证原子性,提供了两个高级的字节码指令 Monitorenter 和 Monitorexit。这两个字节码,在 Java 中对应的关键字就是 Synchronized。因此,在 Java 中可以使用 Synchronized 来保证方法和代码块内的操作是原子性的。
可见性:
? Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值的这种依赖主内存作为传递媒介的方式来实现的。Java 中的 Volatile 关键字修饰的变量在被修改后可以立即同步到主内存。被其修饰的变量在每次使用之前都从主内存刷新。因此,可以使用 Volatile 来保证多线程操作时变量的可见性。除了 Volatile,Java 中的 Synchronized 和 Final 两个关键字也可以实现可见性。只不过实现方式不同
有序性
? 在 Java 中,可以使用 Synchronized 和 Volatile 来保证多线程之间操作的有序性。区别:Volatile 禁止指令重排。Synchronized 保证同一时刻只允许一条线程操作。
11、volatile 作用?底层实现?单例模式中 volatile 的作用?
作用:
? 保证数据的“可见性”:被 volatile 修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象。
? 禁止指令重排:在多线程操作情况下,指令重排会导致计算结果不一致
底层实现:
? “观察加入 volatile 关键字和没有加入 volatile 关键字时所生成的汇编代码发现,加入 volatile 关键字时,会多出一个 lock 前缀指令”
lock 前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供 3 个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他 CPU 中对应的缓存行无效。
单例模式中 volatile 的作用:
防止代码读取到 instance 不为 null 时,instance 引用的对象有可能还没有完成初始化。
class Singleton{
private volatile static Singleton instance = null; //禁止指令重排
private Singleton() {
}
public static Singleton getInstance() {
if(instance==null) {
synchronized (Singleton.class) {
if(instance==null)
instance = new Singleton();
}
}
return instance;
}
}
12、AQS 思想,以及基于 AQS 实现的 lock, CountDownLatch、CyclicBarrier、Semaphore 介绍
? AQS 的全称为(AbstractQueuedSynchronizer)抽象的队列式的同步器,是?个?来构建锁和同步器的框架,使?AQS 能简单且?效地构造出应??泛的?量的同步器,如:基于 AQS 实现的 lock, CountDownLatch、CyclicBarrier、Semaphore
? AQS 核?思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的?作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占?,那么就需要?套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是?CLH(虚拟的双向队列)队列锁实现的,即将暂时获取不到锁的线程加?到队列中。
lock:
? 是一种可重入锁,除了能完成 synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。默认为非公平锁,但可以初始化为公平锁; 通过方法 lock()与 unlock()来进行加锁与解锁操作;
CountDownLatch:
? 通过计数法(倒计时器),让一些线程堵塞直到另一个线程完成一系列操作后才被唤醒;该?具通常?来控制线程等待,它可以让某?个线程等待直到倒计时结束,再开始执?。
? 假设我们有这么一个场景,教室里有班长和其他 6 个人在教室上自习,怎么保证班长等其他 6 个人都走出教室在把教室门给关掉。
public class CountDownLanchDemo {
public static void main(String[] args) {
for (int i = 0; i < 6; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 离开了教室...");
}, String.valueOf(i)).start();
}
System.out.println("班长把门给关了,离开了教室...");
}
}
此时输出:
0 离开了教室...
1 离开了教室...
2 离开了教室...
3 离开了教室...
班长把门给关了,离开了教室...
5 离开了教室...
4 离开了教室...
发现班长都没有等其他人理他教室就把门给关了,此时我们就可以使用 CountDownLatch 来控制
public class CountDownLanchDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
new Thread(() -> {
countDownLatch.countDown();
System.out.println(Thread.currentThread().getName() + " 离开了教室...");
}, String.valueOf(i)).start();
}
countDownLatch.await();
System.out.println("班长把门给关了,离开了教室...");
}
}
此时输出:
0 离开了教室...
1 离开了教室...
2 离开了教室...
3 离开了教室...
4 离开了教室...
5 离开了教室...
班长把门给关了,离开了教室...
CyclicBarrier:
? 字面意思是可循环(Cyclic)使用的屏障(Barrier)。他要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过 CyclicBarrier 的 await()方法。
我们假设有这么一个场景,每辆车只能坐 4 个人,当车满了,就发车。
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(4, () -> {
System.out.println("车满了,开始出发...");
});
for (int i = 0; i < 8; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 开始上车...");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
输出结果:
Thread-0 开始上车...
Thread-1 开始上车...
Thread-3 开始上车...
Thread-4 开始上车...
车满了,开始出发...
Thread-5 开始上车...
Thread-7 开始上车...
Thread-2 开始上车...
Thread-6 开始上车...
车满了,开始出发...
Semaphore:
? 信号量主要用于两个目的,一个是用于多个共享资源的互斥作用,另一个用于并发线程数的控制。
假设我们有 3 个停车位,6 辆车去抢;指定多个线程同时访问某个资源。
public class SemaphoreDemo {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 6; i++) {
new Thread(() -> {
try {
semaphore.acquire(); // 获取一个许可
System.out.pri
ntln(Thread.currentThread().getName() + " 抢到车位...");
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName() + " 离开车位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 释放一个许可
}
}).start();
}
}
}
评论