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前缀指令给CPU,CPU在计算完之后会立即将这个值写回主内存,同时因为有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变量、加锁线程、等待队列等并发中的核心组件。