JAVA篇最全梳理(1-3年经验面经)(二)

2. JUC

总结不易,希望大家给点支持,坚持每日一更,如果需要更加全面的资料可以联系wx:qh950520

java 并发编程并发:多个线程去访问同一个资源并行:各种事情同时去做,一边干什么,一边干什么

1. Synchronized

1.sync的使用

可以用用于代码块,普通方法,静态方法

代码块----对象锁,普通方法---对象锁,静态方法----类锁

2.sync的主要作用

主要使用来解决多线程同步问题的,其可以保证正在被修改的代码任意时刻都只有一个线程执行

3.sync的底层原理

被synchronized修饰的代码,在被编译器编译后在被修饰的代码前后加上了一组字节指令。

在代码开始加入了monitorenter,在代码后面加入了monitorexit,这两个字节码指令配合完成了synchronized关键字修饰代码的互斥访问

在虚拟机执行到monitorenter指令的时候,会请求获取对象的monitor锁,基于monitor锁又衍生出一个锁计数器的概念

当执行monitorenter时,若对象未被锁定时,或者当前线程已经拥有了此对象的monitor锁则锁计数器+1,该线程获取该对象锁

当执行monitorexit时锁计数器-1当计数器为0时,此对象锁就被释放了。那么其他阻塞的线程则可以请求获取该monitor锁。

拓展:sync修饰普通方法底层原理:

普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。

面试:Synchronized的可重入怎么实现的?

重入锁实现可重入性原理或机制是:每一个锁关联一个线程持有者和计数器,当计数器为 0 时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为 1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为 0,则释放该锁

synchronized 方法若发生异常,则JVM会自动释放锁。

锁对象不能为空,否则抛出NPE(NullPointerException)

继承创建了父类对象,并把父类对象的引用交给了子类,但是在super.去调用方法的时候JVM认为调用者依然是子类。所以子类从写父类sync方法,锁对象都是子类。

2. volatile

volatile关键字

对volatile的理解:volatile是java虚拟机提供的轻量级的同步机制,主要两大特性:

保证可见性,禁止指令重排序但不保证原子性

为什么不保证原子性?

java只对基本数据类型变量的赋值和读取是原子操作,想 i++这种是非原子操作,

1、读内存到寄存器;2、在寄存器中自增;3、写回内存。如果被volatile修饰了,肯定能保证每次读取这个变量都是最新的值,但一旦需要对这个变量进行自增这样的非原子操作,就无法保证原子性了。

怎么保证可见性?

对volatile修饰的变量,执行写操作的话,JVM会发送一条lock前缀指令给CPUCPU在计算完之后会立即将这个值写回主内存,同时因为有MESI缓存一致性协议,所以各个CPU都会对总线进行嗅探,自己本地缓存中的数据是否被别人修改

如果发现别人修改了某个缓存的数据,那么CPU就会将自己本地缓存的数据过期,然后这个CPU上执行的线程在读取那个变量的时候,就会从主内存重新加载最新的数据

lock前缀指令 + MESI缓存一致性协议

volatile为什么可以禁止指令重排序?

对于volatile修改变量的读写操作,都会加入内存屏障

每个volatile写操作前面,加StoreStore屏障,禁止上面的普通写和他重排;每个volatile写操作后面,加StoreLoad屏障,禁止跟下面的volatile读/写重排

每个volatile读操作后面,加LoadLoad屏障,禁止下面的普通读和voaltile读重排;每个volatile读操作后面,加LoadStore屏障,禁止下面的普通写和volatile读重排

sync怎么保证有序性?

synchronized则是由“一个变量在同一时刻只允许一条线程对其进行lock操作”这条规则获取的。

3.Lock/ReenTrantLock/ReadWriteLock

synchronized和lock的区别?

sync是个java关键字,lock是个java类

sync无法判断获取锁的状态,lock可以判断是否获取锁

sync会自动释放锁,lock需要手动释放,否则发生死锁

线程获取sync锁后,另外一个线程会一直等待,而lock就不一定会等待(可中断)

适合锁少量同步代码块,Lock适合锁大量同步代码块

java中有哪些锁?

公平锁/非公平锁

公平锁:多个线程按照顺序获取锁

非公平锁:线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁

可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。 sync和reentrantlock

独享锁/共享锁

独享锁是指该锁一次只能被一个线程所持有,ReentrantLock,sync,writeLock

共享锁是指该锁可被多个线程所持有,readLock

互斥锁/读写锁

互斥锁在Java中的具体实现就是ReentrantLock

读写锁在Java中的具体实现就是ReadWriteLock

乐观锁/悲观锁

乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据

悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改

乐观锁可以使用volatile+CAS原语实现,带参数版本来避免ABA问题,在读取和替换的时候进行判定版本是否一致

悲观锁可以使用synchronize的以及Lock

分段锁

分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。

偏向锁/轻量级锁/重量级锁

Synchronized

偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。

轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。

重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁重量级锁会让其他申请的线程进入阻塞,性能降低

自旋锁

在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

原理:

第一个线程: 第一次来获取锁,立刻获取到锁

第二个线程: 线程一还没有处理完,就要加入到队列,并关心它的前驱节点锁状态,然后进行自旋。

第三步: 释放锁, 线程1把自身的locked = false ,同时把当前节点改为了前节点。

死锁是什么?

当两个线程循环依赖于一对同步对象(monitor)时将发生死锁

比如线程1的占用了D资源,线程2的占用了C资源 ,此时线程1需要资源C,线程2需要D资源,他们就一直等待对方释放资源,导致死锁。

死锁的4个必要条件?

当且仅当以下所有条件同时存在于系统中时,才会出现资源上的死锁情况:

相互排斥:系统中必须有非共享的资源,即在任何给定的时刻,只有一个进程可以使用该资源。

保持等待:进程当前持有至少一个资源并请求其他进程持有的其他资源。

不可抢占:资源只能由持有它的进程自愿释放,其他进程不可强行占有该资源。

循环等待:每个进程必须等待另一个进程持有的资源,而该进程又等待第一个进程释放资源

锁优化?

一般并发用到锁就是阻塞的,因此锁优化就是在堵塞的情况下去提高性能

持有时间减少和锁的范围 (比如以前是对整个方法加锁,现在只对方法内代码块加锁)

减小锁粒度(高性能的hash表,他就是做了减少锁粒度的实现,他被拆分好像16个Segment,每个Segment就是一个个小的hashmap.。就是把大的hash表拆成若干个小的hash表。)在减小锁粒度后, ConcurrentHashMap允许若干个线程同时进入

锁分离(分离,就是读写锁分离读不用改变数据,所以所有的读不会产生堵塞,读不会产生堵塞。当写的时候才去进行堵塞,对于读多写少的场景,应用比较好)

锁粗化(对同一个锁不停的进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化。因此可以把很多次请求的锁拿到一个锁里面,但前提是:中间不需要的同步的代码块很很快的执行完。

例如:for(in i=0;i<count;i++){

Synchronized()

}

改为

Synchronized(){

for(in i=0;i<count;i++){}

}

锁消除

在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作。

比如StringBuffer ,他本来就是线程安全的,他内部有很多sycn修饰

ConCurrentHashMap分段锁的概念

ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。

当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入

但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。

分段锁的设计目的是细化锁的粒度当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

我们面对ReentrantLock和synchronized改如何选择?

Synchronized相比Lock,为许多开发人员所熟悉,并且简洁紧凑,如果现有程序已经使用了内置锁,那么尽量保持代码风格统一,尽量不引入Lock,避免两种机制混用,容易令人困惑,也容易发生错误。在Synchronized无法满足需求的情况下,Lock可以作为一种高级工具,这些功能包括“可定时的可轮询的可中断的锁获取操作,公平队列,以及非块结构的锁否则还是优先使用Synchronized。最后,未来更可能提升Synchronized而不是Lock的性能,因为Synchronized是JVM的内置属性,他能执行一些优化,例如对线程封闭的锁对象的锁消除优化,通过增加锁的粒度来消除内置锁的同步,而如果基于类库的锁来实现这些功能,则可能性不大

4.CAS

原理:比较当前工作内存中的值和主内存中的值,如果这个值是期望的,那么则执行操作。如果不是就一直循环。

缺点:

底层是自旋锁,耗时

一次性只能保存一个共享变量的原子性

ABA问题

ABA问题:需要取出内存中某时刻的数据,而在下时刻比较并替换,那么在这个时间差类会导致数据的变化。

典型例子: 现有一个用单向链表实现的栈,栈顶为A,这时线程T1已经知道A.next为B,然后希望用CAS将栈顶替换为B:head.compareAndSet(A,B);在T1执行上面这条指令之前,线程T2介入,将A、B出栈,再pushD、C、A。此时轮到线程T1执行CAS操作,检测发现栈顶仍为A,所以CAS成功,栈顶变为B,但实际上B.next为null,这样CD就丢失了。

乐观锁的实现中通常都会用版本戳version来对记录或对象标记,避免并发操作带来的问题,在Java中,AtomicStampedReference<E>也实现了这个作用

AtomicStampedReference(intialRef, initialStamp)

比较initalStamp,然后进行compareAndSet

Unsafe 类

//java无法直接操作内存,java可以调用C++,native方法,C++可以操作内存

java通过Unsafe类可以操作native方法,来操作内存

5.并发集合类

并发包下list集合CopyOnWriteArrayList

1.Vector  已经废弃了,效率太低

2.List<String> list = Collections.synchronizedList(new ArrayList<>());  //效率低

3.List<String> list = new CopyOnWriteArrayList<>(new ArrayList<>());  JUC里面的方法,add加lock锁CopyOnWrite 写入时复制 COW 计算机领域的一种策略:多个线程调用的时候,List读取的时候固定不变,写入的时候存在覆盖问题。

在写入的时候避免覆盖创造的数据问题,复制一份给调用者,调用完毕返回。

并发包下set集合CopyOnWriteArraySet

1.List<String> list = Collections.synchronizedSet(new ArraySet<>());  //效率低

2.List<String> list = new CopyOnWriteArraySet<>(new ArraySet<>());  JUC里面的方法,add加lock锁CopyOnWrite 写入时复制 COW 计算机领域的一种策略:

Queue

抛出异常是:NoSuchElementException


6.并发的三个辅助类

CountDownLatch 减法计数器,当计数器为0时才向下执行,否则await一直阻塞


CycliBarrier 加法计数器,达到指定记数值就会触发下面流程

public class CyclicBarrierTest{

publicstaticvoidmain(String[]args)throwsBrokenBarrierException,InterruptedException{

/*

CyclicBarrier 构造器有两个参数

1. int  要计数到的值

2. Runnable 达到计数器之后要执行的线程

*/

CyclicBarriercyclicBarrier=newCyclicBarrier(7,()->{

System.out.println("召唤神龙成功");

       });

for(inti=1;i<=7;i++) {

finalinttemp=i;

// Lambda 能操作 i 吗

newThread(()->{

System.out.println(Thread.currentThread().getName()+"收集了:"+temp+"颗龙珠");

try{

cyclicBarrier.await();

}catch(InterruptedExceptione) {

e.printStackTrace();

}catch(BrokenBarrierExceptione) {

e.printStackTrace();

               }

},String.valueOf(i)).start();

       }

   }

}

Semaphore sqmaphore.acquire() 假如线程已经满了,就等待有空为止.semaphore.release,


7.读写锁


8.JMM

9.AQS

AbstractQueuedSynchronizer抽象队列同步器简称AQS

ReentrantLock,Semaphore,ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。

ReentrantLcok 底层加锁原理

ReentrantLcok 内有个AQS对象,这个AQS中含有一个核心变量state,是int类型的,代表加锁状态,初始状态是0,另外AQS还有一个关键变量,用来标记当前加锁的是哪个线程,初始为null。

当一个线程调用lock(), 直接利用CAS将state从0变为1,1表示加锁成功,然后再设置当前加锁线程就是自己。

ReenTrantLock 互斥实现原理:

线程2跑过来一下看到,state的值不是0啊?所以CAS操作将state从0变为1的过程会失败,因为state的值当前为1,说明已经有人加锁了!

线程2会将自己放入AQS中的一个等待队列,因为自己尝试加锁失败了,此时就要将自己放入队列中来等待,等待线程1释放锁之后,自己就可以重新尝试加锁了

AQS就是一个并发包的基础组件,用来实现各种锁,各种同步组件的。

它包含了state变量加锁线程等待队列等并发中的核心组件。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。