volatile: java虚拟机提供的轻量级的同步机制 三大特性: 1.保证可见性 2.不保证原子性 3.禁止指令重排 JMM(JAVA内存模型): JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。 JMM关于同步的规定: 1线程解锁前,必须把共享变量的值刷新回内存 2线程加锁前,必须读取主内存的最新值到自己的工作内存 3加锁解锁是同一把锁 由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程 三大特性: 1.保证可见性 2.保证原子性 3.有序性 所以volatile是轻量级的,因为他不保证原子性 有序性: 计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分以下3种. 源代码 -> 编译器优化的重排 -> 指令并行的重排 -> 内存系统的重排 -> 最终执行的指令 单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。 处理器在进行重排序时必须要考虑指令之间的数据依赖性 多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测 你在哪些地方用到过volatile? 单例模式DCL代码7 单例模式volatile分析口 DCL (双端检锁)机制不一定线程安全,原因是有指令重排序的存在,加入volatile可以禁止指令重排: 原因在于某一个线程执行到第一次检测,读取到的instance不为nul时,instance的引用对象可能没有完成初始化。 instance = new SingletonDemo();可以分为以下3步完成(伪代码) memory = allocate(); //1.分配对象内存空间 instance(memory); //2. 初始化对象 instance = memory; //3. 设置instance指向刚分配的内存地址,此时instance ! =null 步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变, 因此这种重排优化是允许的。 memory = allocate(); //1. 分配对象内存空间 instance = memory; //3. 设置instance指向刚分配的内存地址,此时instance! =null, 但是对象还没有初始化完成! instance(memory); //2. 初始化对象 但是指令重排只会保证串行语义的执行的一致性(单线程,但并不会关心多线程间的语义一致性。 所以当一条线程访问instance不为nul时,由于instance实例未必己初始化完成,也就造成了线程安全问题。 CAS你知道吗? 比较并交换(可以参照atomicinteger的compareAndSet方法) CAS的全称为Compalre-And-Swap,它是一条CPU并发原语。 它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。 CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语 范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就 是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。 CAS底层原理?如果知道,谈谈你对UnSafe的理解 自旋锁 + unsafe类 public final int getAndIncrement() { //atomicinteger的自增方法 //1.当前对象 2.内存偏移量(内存地址) 3.自增的值 return unsafe.getAndAddInt(this, valueOffset, 1); } 1 Unsafe 是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native) 方法来访问,Unsafe相当于一个后门,基于该 类可以直接操作特定内存的数据。Unsafe 类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中 CAS操作的执行依赖于Unsafe类的方法。 注意Unsafe类中的所有方法都是native修饰的,也就是 说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务 2变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。 public final int getAndIncrement() { return unsafe.getAndAddInt( 0: this, valueoffset, i: 1); } var1 AtomicInteger对象本身。 var2该对象值得引用地址。 var4需要变动的数量。| var5是用过var1 var2找出的主内存中真实的值。 用该对象当前的值与var5比较:. 如果相同,更新var5+var4并且返回true, 如果不同,继续取值然后再比较,直到更新完成。 public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; } 假设线程A和线程B两个线程同时执行getAndAddInt操作(分别跑在不同CPU.上) : 1 AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3, 根据JMM模型,线程A和线程B各自持有 份值为3的value的副本分别到各自的工作内存。 2线程A通过getlntVolatile(var1, var2)拿到value值3,这时线程A被挂起。 3线程B也通过getIntVolatile(var1, var2)方法获取到value值3,此时刚好线程B没有被挂起并执行compareAndSwapInt方法 比较内存值也为3,成功修改内存值为4,线程B打完收工,一切OK。 4这时线程A恢复,执行compareAndSwapInt方法比较, 发现自己手里的值数字3和主内存的值数字4不一致,说明该值已 经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新来一遍了。 5线程A重新获取value值, 因为变量value被volatile修饰, 所以其它线程对它的修改,线程A总是能够看到,线程A继续执 行compareAndSwapInt进行比较替换,直到成功。 3变量value用volatile修饰,保证了多线程之间的内存可见性。 CAS缺点 循环时间长开销很大:如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。 只能保证一个共享变量的原子操作: 当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作, 但是对多个共享变量操作时,循环CAS就法保证操作的原子性,这个时候就可以用锁来保证原子性。 引出来ABA问题??? synchronized是独享所,当一个线程持有锁其他线程都得等待,cas时比较交换没有加锁所以效率高,但相对的耗性能 Unsafe类+CAS思想(自旋) CAS ---> UnSafe ---> CAS底层思想---> ABA ---> 原子引用更新---> 如何规避ABA问题 ABA问题怎么产生的 CAS会导致“ABA问题”。 CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。 比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B, 然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操 作成功。 尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。 解决ABA问题??? -> 理解原子引用+新增一种机制,那就是修改版本号(类似时间戳) AtomicReference<>(原子引用) AtomicStampedReference<>(时间戳的原子引用) arraylist->Vector->Collections.synchronizedList->CopyOnWriteArrayList *写时复制(参考CopyOnWriteArrayList) CopyonWrite容器即写时复制的容器。往一个容器添加元素的时候,不直接往当前容器object[]添加,而是先将当前容器object[]进行Copy, 复制出一个新的容器0bject[] newELements, 然后新的容器object[] newELements里添加元素,添加完元素之后, 再将原容器的引用指向新的容器setArray(newELements)。 这样做的好处是可以对CopyOnWrite容器进行并发的读, 而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnwrite容器也是一种读写分离的思想,读和写不同的容器 Set->Collections.synchronizedSet(new HashSet<>())->CopyOnWriteArraySet<>(底层用的是CopyOnWriteArrayList) 题外话:hashset底层实现时hashmap 源码: private static final Object PRESENT = new Object(); public HashSet() {map = new HashMap<>();} public boolean add(E e) {return map.put(e, PRESENT)==null;} 从上面源码可以看出底层用的时hashmap,add时把值作为hashmap的key,hashmap的value为一个常量。 Map->Collections.synchronizedMap(new HashMap<>())->ConcurrentHashMap 公平锁/非公平锁/可重入锁(递归锁)/递归锁/自旋锁谈谈你的理解?请手写一个自旋锁 公平锁和非公平锁: ReentrantLock lock = new ReentrantLock(true);//(默认非公平) true.公平锁 :先来的先获取锁,先来后到 false.非公平锁 :后申请的线程可能先获取锁 可以跟AQS源码 Java ReentrantLock而言, 通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。 对于Synchronized而言,也是一种非公平锁 关于两者区别: 公平锁: Threads acquire a fair lock in the order in which they requested it 公平锁,就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是待队列的第一 个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己 非公平锁: a nonfair lock permits barging: threads requesting a lock can jump ahead of the queue of waiting threads if the lock happens to be available when it is requested. 非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。 可重入锁(也叫做递归锁-sync和ReentrantLock都是可重入锁):防止死锁 指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁 也即是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块。 例子: public synchronized void A() { B(); } public synchronized void B() { } 自旋锁(spinlock) 是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺 点是循环会消耗CPU public final int getAndSetInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var4));CountDownLatch return var5; } 独占锁(写锁)/共享锁(读锁)/互斥锁 独占锁:指该锁一次只能被一个线程所持有。对ReentrantLock和Synchronized而言都是独占锁 共享锁:指该锁可被多个线程所持有。 对ReentrantReadWriteLock其读锁是共享锁,其写锁是独占锁。 读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。 CountDownLatch/CyclicBarrier/Semaphore使用过吗? CountDownLatch(类似于倒计时): 人走完了才能关门 让一些线程阻塞直到另一些线程完成一系列操作后才被唤醒 CountDownLatch主要有两个方法, 当一个或多个线程调用await方法时, 调用线程会被阻塞 。 其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞), 当计数器的值变为零时,因调用await方法被阻塞的线程会被唤醒,继续执行。 CyclicBarrier(与CountDownLatch相反):人到齐了才能开会 CyclicBarrier的字面意思是可循环(Cyclic) 使用的屏障(Barrier) 。它要做的事情是, 让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时, 屏障才会开门,所有被屏障拦截的线程才会继续干活I线程进入屏障通过CyclicBarrier的await()方法。 Semaphore 计数信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。 队列+阻塞队列 Thread1 -> Put -> BlockingQueue <- Take <- Thread2 线程1往阻塞队列中添加元素,而线程2从阻塞队列中移除元素 当阻塞队列是空时,从队列中获取元素的操作将会被阻塞。 当阻塞队列是满时,往队列里添加元素的操作将会被阻塞。 ArrayBlockingQueue:由数组结构组成的有界阻塞队列(重点)。 LinkedBlockingQueue: 由链表结构组成的有界(但大小默认值为Integer.MAX_ _VALUE)阻塞队列(重点) SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列(重点)。 SynchronousQueue没有容量。 与其他BlockingQueue不同,SynchronousQueue是-个不存储无素的BlockingQueue。 每一个put操作必须要等待-个take操作,否则不能继续添加元素,反之亦然。 PriorityBlockingQueue:支持优先级排序的无界阻塞队列。 DelayQueue:使用优先级队列实现的延迟无界阻塞队列。 LinkedTransferQueue: 由链表结构组成的无界阻塞队列。 LinkedBlockingDeque:由链表结构组成的双向阻塞队列 BlockingQueue的核心方法: --------------------------------------------------------------------------------------- 方法类型 抛出异常 特殊值 阻塞 超时 --------------------------------------------------------------------------------------- 插入 add(e) offer(e) put(e) offer(e,time,unit) 移除 remove() poll() take() poll(time,unit) 检查 element() peek() 不可用 不可用 --------------------------------------------------------------------------------------- 抛出异常 当阻塞队列满时,再往队列里add插入元素会抛llegalStateException: Queue full 当阻塞队列空时,再往队列里remove移除元素会抛NoSuchElementException 特殊值 插入方法,成功ture失败false 移除方法,成功返回出队列的元素,队列里面没有就返回null . 一直阻塞 当阻塞队列满时,生产者线程继续往队列里put元素,队列会一直阻塞生产线程直到put数据or响应中断退出 当阻塞队列空时,消费者线程试图从队列里take元素,队列会一直阻塞 消费者线程直到队列可用。 超时退出 当阻塞队列满时,队列会阻塞生产者线程一定时间,超过后限时后生产者线程会退出 阻塞队列用在哪 生产者消费者模式 线程池 消息中间件 在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞) 一旦条件满足,被挂起的线程又会自动被唤醒 为什么需要BlockingQueue 好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了 在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我 们的程序带来不小的复杂度。 sync->wait->notify别 lock->await->signal替代 多线程第三种获取线程的方法实现Callable,第四种通过线程池 为什么用线程池,优势 线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最 大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。 他的主要特点为:线程复用;控制最大并发数:管理线程。 第一:降低资源消耗。通过重复利用己创建的线程降低线程创建和销毁造成的消耗。 第二:提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。 第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进 行统一的分配,调优和监控 Java中的线程池是通过Executor框架实现的,该框架中用到了Executor, Executors,ExecutorService,ThreadPoolExecutor这几个类。 Executors.newFixedThreadPool(int)固定 Executors.newSingleThreadExecutor()一线程 Executors.newCachedThreadPool()多线程 7大参数7 G 1 .corePoolSize:线程池中的常驻核心线程数园 2.maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1 3.keepAliveTime:多余的空闲线程的存活时间。当前线程池数量超过corePoolsize时,当空闲时间达到keepAliveTime值时, 多余空闲线程会被销毁直到只剩下corePoolSize个线程为止 4.unit: keepAliveTime的 单位。 5.workQueue:在务队划,被提交但尚未被执行的任务。 6.threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程-般用默认的即可 7.handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数(m 流程; 任务进入线程池,当线程池核心线程都忙不过来了任务进入阻塞队列,当队列满了开启新线程,当线程达到最大线程数还处理不过来 就看设置的拒绝策略处理,当处理的过来时,线程处理的过来时空闲的线程时间到了设置的存活时间就回收直到核心线程(默认的 策略时报异常) 1.在创建了线程池后,等待提交过来的任务请求。 2.当调用execute()方法添加一个请求任务时,线程池会做如判断: . 2.1如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务; 2.2如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列; 2.3如果这时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务; 2.4如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。 3.当一个线程完成任务时,它会从队列中取下一个任务来执行。 4.当一个线程无事可做超过一定的时间(keepAliveTime) 时,线程池会判断: 如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。 所以线程池的所有任务完成后它最终会收缩到corePoolSize的大小。, 拒绝策略: 等待队列也已经排满了,再也塞不下新任务了 同时, 线程池中的max线程也达到了,无法继续为新任务服务。 这时候我们就需要拒绝策略机制合理的处理这个问题。 4大拒绝策略 AbortPolicy(默认):直接抛出RejectedExecutionException 异常阻止系统正常运行。 CallerRunsPolicy: "调用者运行"一 种调节机制,该策略既不会拋弃任务,也不会抛出异常,而是回退给调用者 DiscardoldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务 DiscardPolicy: 直接丢弃任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好方案 [强制] 线程池不允许使用Executors去创建,而是通过 ThreadPoolExecutor的方式,这样. 的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明: Executors 返回的线程池对象的弊端如下: 1) FixedThreadPool和SingleThreadPool:允许的请求队列长度为Integer .MAX__VALUE,可能会堆积大量的请求,从而导致00M。 2) CachedThreadPool 和ScheduledThreadPool:允许的创建线程数量为Integer .MAX_ _VALUE,可能会创建大量的线程, 从而导致00M。 合理配置线程池你是如何考虑的? CPU密集型: CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。 CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程), 而在单核CPU上(悲剧吧?(;’n')心),无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就那些。 CPU密集型任务配置尽可能少的线程数量: 一 般公式: CPU核数+1个线程的线程池 I0密集型 1.由于IO密集型任务线程并不是一- 直在执行任务,则应配置尽可能多的线程,如CPU核数*2 2.I0密集型,即该任务需要大量的IO,即大量的阻塞。 在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。 所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU.上,这种加速主要就是利用了被浪费掉的阻塞时间。 I0密集型时,大部分线程都阻塞,故需要多配置线程数: 参考公式: CPU核数1 1-阻塞系数 阻塞系数在0.8~0.9之间 比如8核CPU: 8/1 -0.9= 80个线程数 死锁编码及定位分析 是什么: 死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去, 如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。 解决: 1.jps命令定位进程号 2.jstack找到死锁查看 transient修饰的变量不参与序列化 synchronized与lock的区别 1.synchronized是关键字属于JVM层面,lock是具体类属于api层面的锁 2.synchronized不需要手动释放锁,lock需要手动释放 3.等待可否中断: synchronized不可中断,除非抛出异常或正常运行完成 ReentrantLock可中断,1.设置超时时间trylock(),2.lockInterruptibly()放代码块中,调用interrupt()方法中断 4.synchronized非公平锁,ReentrantLock默认非公平,也可设置程公平锁 5.锁绑定多个条件Condition ReentrantLock可以绑定condition实现精确唤醒,而synchronized要么随机唤醒一个要么全部唤醒 LockSupport: 线程等待唤醒机制的改进加强版,阻塞和唤醒线程 park(),unpark() ==== watit,notify 3种让线程等待唤醒的方法: 1.使用Object种的wait和notify(wait和notify不能脱离synchronize同步代码块,否则会报异常) 2.使用JUC包中Condition的await和signal方法(同上,这两个方法也必须在lock和unlock两个方法之间被调用) 3.LockSupport类可以阻塞当前线程以及唤醒指定的线程(解决了必须使用锁机制才能实现线程的阻塞和唤醒的功能,unpark也可以在park之前执行) LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可Permit,permit只有两个值1和0,默认是0 可以把许可看成是一种(0,1)的信号量 permit默认是0,所以一开始调用park方法,当前线程会被阻塞,直到别的线程将当前线程的permit设为1时,park方法会被唤醒然后将permit再次设置为0并返回 调用unpark方法后,会将thread线程的许可permit设置成1(注意多次调用unpark不会累加,permit值还是1)会自动唤醒thread线程, 即之前阻塞中的locksupport.park()方法会立即返回 底层用的unsafe的native方法 当调用park方法时,如果有凭证则会直接消耗掉这个凭证然后正常退出,如果无凭证就必须阻塞等待凭证可用 而unpark则相反,他会增加一个凭证,但凭证最多只能有一个,累加无效 AQS(AbstractQueuedSynchronizer抽象的队列同步器): 用来构建锁或其他同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类型变量表示持有锁的状态 AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作将每条要去抢占资源的线程封装成一个Node节点来 实现锁的分配,通过CAS完成对state值得修改
JAVA. JUC笔记
最新推荐文章于 2024-09-29 08:33:21 发布