面试题分享
一、volatile部分
1.请你说说volatile的理解?
volatile是java虚拟机提供的轻量级的同步机制。拥有三大特性:保证可见性、进制指令重排、不保证原子性
2,JMM的理解
JMM(java内存模型)本身是一种抽象的概念并不真实的存在,他描述的是一种规范,通过这组规范定义了各种变量(实例字段,静态字段和构成数组对象的元素)的访问方式。
JMM规定:
- 线程解锁前,必须把共享变量的值刷新会主内存
- 线程加锁前,必须读取主内存的最新值到自己的工作内存
- 加锁解锁用的是同一把锁
案例1: 数据可见性
import java.util.concurrent.TimeUnit;
/**
* 验证volatile的可见性
*/
public class VolatileDemo {
volatile int number = 0;
public void changeNum(){
this.number = 50;
}
public static void main(String[] args) {
VolatileDemo volatileDemo = new VolatileDemo();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t the primary value!");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
volatileDemo.changeNum();
System.out.println(Thread.currentThread().getName()+"\t change value");
},"aaa").start();
while(volatileDemo.number == 0){
}
System.out.println(Thread.currentThread().getName()+"\t finally value");
}
}
结果:
案例2: 不保证原子性
public class VolatileDemo {
volatile int number = 0;
public void changeNum(){
number++;
}
public static void main(String[] args) {
VolatileDemo volatileDemo = new VolatileDemo();
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 1; j <= 100; j++) {
volatileDemo.changeNum();
}
},String.valueOf(i)).start();
}
while (Thread.activeCount() > 2){
//线程让步
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"\t finally number value:"+volatileDemo.number);
}
}
3、如何解决原子性操作:
1.加synchronized
2,使用我们的juc下的AtomicInteger
4.线程安全如何保证?
1.工作内存与主内存同步延迟现象导致可见性问题
- Synchronized
- Volatile
2.对于指令重排导致可见性问题和有序性问题
volatile关键字作用可以禁止指令重排。
5、哪些地方使用volatile?
- 单例模式DCL代码
- CAS底层
二、CAS理解
1、谈谈你对CAS的理解?
比较并替换,它是一种CPU并发的原语,功能是判断 内存中的某个位置是否为预选期,如果是则更改为新的值,该过程是原子性(底层Unsafe)的。
并发原语的执行必须是连续的,在执行的过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓数据不一致的问题。
CAS机制当中使用了3个基本操作数: 内存地址V,旧的预期值A,要修改的新值B,当你更新一个变量的时候,只有当变量的预期值A和内存地址V中的实际值相同时,才会将内存地址V对应的值修改为B。
2.认识CAS底层 unsafe .getAndAddInt
unsafe类 +CAS思想(自旋实现)
unsafe 是CAS的核心类,由于java语言无法直接访问底层系统,需要通过本地的native方法来访问,unsafe相当于一个后门,该类可以直接操作特定内存的数据,unsafe来存在于sun.misc包中,java中CAS操作的执行依赖于insafe类的方法。
CAS案例:
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2);
System.out.println(atomicInteger.compareAndSet(2, 10) +"\t current date:" + atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(2, 100) +"\t current date:" + atomicInteger.get());
atomicInteger.getAndIncrement();
}
}
结果:
3、CAS缺点
-
循环时间长开销很大
分析源码知,getAndAddInt方法执行时,有一个do while,如果CAS失败,会一直进行尝试,如果CAS长时间一直不成功,可能会给CPU带来很大的压力。 -
只能保证一个共享变量的原子性操作
-
ABA问题
案例: ABA问题以及解决方案
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
//ABA问题演示
public class ABDemo {
static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100,1);
public static void main(String[] args) {
System.out.println("=============ABA问题的产生=============");
new Thread(()->{
atomicReference.compareAndSet(100,101);
atomicReference.compareAndSet(101,100);
},"t1").start();
new Thread(()->{
try {
//为了确保线程1 ABA 执行成功
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicReference.compareAndSet(100, 200) + "\t update Number Value: "+ atomicReference.get().toString());
},"t2").start();
// 暂停一会线程
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("=============ABA问题的解决=============");
new Thread(()->{
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t 第一次的版本号:"+stamp);
// 暂停一秒钟 3线程
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t 第二次的版本号:"+ atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t 第三次的版本号:"+ atomicStampedReference.getStamp());
},"t3").start();
new Thread(()->{
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t 第一次的版本号:"+stamp);
// 暂停3秒钟4线程,保证上面的3线程完成一次ABA操作
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1);
System.out.println(Thread.currentThread().getName()+"\t 是否修改成功: "+ result +"\t 当前最新实际版本号:" + atomicStampedReference.getStamp());
System.out.println(Thread.currentThread().getName()+"\t 当前实际最新值:" +atomicStampedReference.getReference());
},"t4").start();
}
}
结果:
三、集合类HashSet
1、hashset的底层是hashmap,为什么new HashSet<>.add(“a”)不出错(HashMap是基于key value形式存储数据)?
hashSet的底层是hashMap,当执行add()操作时,会执行put()方法,
public boolean add(E e){
return map.put(e.PRESENT) == null;
}
hashset中的add,就是hashmap中的put元素的key,而value是Object类型的常量,因此value是恒定的。
private static final Object PRESENT = new Object();
2、为什么说hashset不允许有重复的元素?
HashSet中不允许有重复的元素,因为hashset的底层是hashmap,而hashSet中的元素都存放在HashMap的key上,value是恒定的。
3、怎样检测某个元素是否存在?
通过重写元素中的equals()和hashcode()方法,当向Set中添加数据的时候,先通过key计算出hashcode()方法,此时,就可确定key元素的位置。
四、锁
1、公平锁与非公平锁
并发包中ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁和非公平锁。**默认是非公平锁。**非公平锁的优点在于吞吐量比公平锁大。
公平锁: 在并发中,每个线程在获取锁时会先查看当前此锁维护的等待队列,之后按照FIFO的规则从队列中获取自己。
非公平锁: 该线程一上来就直接尝试占有锁,如果尝试失败,就采用类似公平锁的那种方式。
2、可重入锁(又称递归锁)
概念:指的是同一个线程的外层方法获取锁的时候,在进入内层方法会自动获取锁。即 线程可以进入任何一个它已经拥有的锁所同步着的代码块。
实现方式: lock 和 Synchronized
案例1:基于synchronized 实现可重入锁
//synchronized 实现可重入锁
public class SynLock {
public static void main(String[] args) {
Object o = new Object();
new Thread(()->{
synchronized (o){
System.out.println(Thread.currentThread().getName()+" 最外层");
synchronized (o) {
System.out.println(Thread.currentThread().getName() + " 最中层");
synchronized (o) {
System.out.println(Thread.currentThread().getName() + " 最内层");
}
}
}
},"AAA").start();
}
}
案例2: 基于lock 实现可重入锁
//lock 实现可重入锁
public static void main(String[] args) {
Lock lock = new ReentrantLock();
//创建线程
new Thread(()->{
try{
lock.lock();
System.out.println(Thread.currentThread().getName()+" 最外层");
try{
lock.lock();
System.out.println(Thread.currentThread().getName()+" 最中层");
try{
lock.lock();
System.out.println(Thread.currentThread().getName()+" 最内层");
}finally {
lock.unlock();
}
}finally {
lock.unlock();
}
}finally {
lock.unlock();
}
},"AAA").start();
}
}
结果:
3.自旋锁
指尝试获取锁的线程不会立即阻塞,而是采用循环的方式尝试获取锁,好处是减少线程上下文的切换的消耗,缺点就是循环会消耗CPU.
案例:自旋锁
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
public class SpinLockDemo {
AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void myLock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName()+"\t come in");
while (!atomicReference.compareAndSet(null,thread)){
}
}
public void myUnLock(){
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread,null);
System.out.println(Thread.currentThread().getName()+"\t invoked myUnlock!");
}
public static void main(String[] args) {
SpinLockDemo spinLockDemo = new SpinLockDemo();
new Thread(()->{
spinLockDemo.myLock();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
spinLockDemo.myUnLock();
},"AAA").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
spinLockDemo.myLock();
spinLockDemo.myUnLock();
},"BBB").start();
}
}
结果:
4.独占锁与共享锁
独占锁:指该锁一次只能被一个线程所持有,对ReentrantLock和Synchronized而言都是独占锁。
共享锁: 指该锁可被多个线程锁持有。对于ReentrantReadWriteLock其读锁是共享锁,写锁是独占锁。
区别: 读读共享。读写,写写不共享。
写操作; 原子 + 独占 , 整个过程必须是一个完整的统一体,中间不允许有分割。
案例:读写锁案例
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
//资源类
class MyCache{
private volatile Map<String,Object> map = new HashMap<>();
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
public void put(String key,Object value){
rwLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"\t 正在写入一个资源信息: "+key);
try {
TimeUnit.MICROSECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(key,value);
System.out.println(Thread.currentThread().getName()+"\t 写入完成:");
}catch (Exception e){
e.printStackTrace();
}finally {
rwLock.writeLock().unlock();
}
}
public void get(String key){
// 读锁
rwLock.readLock().lock();
try{
System.out.println(Thread.currentThread().getName() + "\t 正在读取:");
try{
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object result = map.get(key);
System.out.println(Thread.currentThread().getName() + "\t 读取完成: " +result);
}catch (Exception e){
e.printStackTrace();
}finally {
rwLock.readLock().unlock();
}
}
}
public class ReadAndWriteDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
for (int i = 1; i <= 5 ; i++) {
final int temp = i;
new Thread(()->{
myCache.put(temp+"",temp+"");
},String.valueOf(i)).start();
}
for (int i = 1; i <= 5 ; i++) {
final int tempInt = i;
new Thread(()->{
myCache.get(tempInt+"");
},String.valueOf(i)).start();
}
}
}
结果:
五、阻塞队列升级版
1、种类分析:
1、ArrayBlockingQueue
由数组结构组成的有界阻塞队列。
2、LinkedBlockingQueue
由链表结构组成的有界(默认值的大小为Integer.MAX_VALUE)阻塞队列。但是Integer.MAX_VALUE的值很大,大概有21亿之大,英雌箱相当于无界。
3、SynchronizedQueue
不存储元素的阻塞队列,也就是说只要存一个元素就要取出来
案例:SynchronizedQueue 的使用
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;
public class SynchronizedQueue {
public static void main(String[] args) {
BlockingQueue<String> blockingQueue = new SynchronousQueue<>();
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+"\t put 1" );
blockingQueue.put("1");
System.out.println(Thread.currentThread().getName()+"\t put 2" );
blockingQueue.put("2");
System.out.println(Thread.currentThread().getName()+"\t put 3" );
blockingQueue.put("3");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"AAA").start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
System.out.println(Thread.currentThread().getName()+"\t" +blockingQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
System.out.println(Thread.currentThread().getName()+"\t" +blockingQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
System.out.println(Thread.currentThread().getName()+"\t" +blockingQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"BBB").start();
}
}
结果:
4、PriorityBlockingQueue
支持优先级排序的无界阻塞队列。
5、DelayQueue
使用优先级队列实现的延误无界阻塞队列。
6、LinkedTransferQueue
由链表结构组成的无界阻塞队列。
7、LinkedBlockingQueue
由链表结构组成的双向阻塞队列。
2、线程之间的通信(生产者与消费者)
注意: 对于多线程 ,一般用while( )语句进行判断,减少 if语句的使用。
1.0版本过度到2.0版本
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
// 资源类
class ShareData{
private int number = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void increment() throws InterruptedException {
lock.lock();
try {
//1.判断
while (number != 0){
//等待,不能生产
condition.await();
}
// 2.干活做事
number++;
System.out.println(Thread.currentThread().getName()+"\t"+ number);
//3.通知唤醒
condition.signalAll();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void decrement() throws InterruptedException {
lock.lock();
try {
//1.判断
while (number == 0){
//等待,不能生产
condition.await();
}
// 2.干活做事
number--;
System.out.println(Thread.currentThread().getName()+"\t"+ number);
//3.通知唤醒
condition.signalAll();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
/**
* 题目: 一个初始值为0的变量,两个线程对其操作,一个加1,一个减一,来5轮
* 1. 线程 操作 资源类
* 2. 判断 做事 通知
* 3. 防止虚假唤醒机制
*/
public class ProdConsumer_TraditionDemo {
public static void main(String[] args) {
ShareData shareData = new ShareData();
new Thread(()->{
for (int i = 1; i <= 5; i++) {
try {
shareData.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"AAA").start();
new Thread(()->{
for (int i = 1; i <= 5; i++) {
try {
shareData.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"BBB").start();
}
}
结果:
3.synchronized和Lock有什么区别?用新的Lock有啥好处?
1、原始构成
synchronized是关键字,属于JVM层面。
synchronized底层是通过monitor、monitorexit对象来完成。
Lock是具体的类,是属于api底面的锁。
2.使用方法
synchronized不需要手动释放锁,当synchronized代码执行完后系统会自动让线程释放对锁的占用。
ReentrantLock则需要用户手动释放锁,如果没有手动释放锁,可能会造成死锁的现象。因此,需要lock()和unLock()方法配合try/finally语句来完成。
3、加锁是否公平
synchronized非公平锁
ReentrantLock即可公平也可非公平,默认是公平锁,构造方法可以传入boolean值,true是公平锁,false是非公平锁。
4、等待是否中断
synchronized不可中断,除非抛出异常或者正常运行完成。ReentrantLock可中断,需要设置超时方法,tryLock(long timeout,TimeUnit unit),使用interrupt()方法可中断。