JUC并发编程

JUC并发编程

一、JUC基础知识复习

1.软件方面
  • 充分利用多核处理器
  • 提高程序性能,高并发系统
  • 提高篇程序吞吐量,异步加回调等生产需求
2.弊端和问题
  • 线程安全问题
  • 线程锁问题
  • 线程性能问题

二、Future

FutureTask
  • 实现Runnable,Future,RunnableFuture接口

  • 调用get()方法求结果会出现阻塞,

  • isDone()消耗cpu资源

CompletableFuture
  • 没有指定线程池会使用默认线程池
  • 是Future的增强版,减少阻塞和轮询
  • 可以进行顺序执行
函数式接口名称方法名称参数返回值
Runnablerun无参数无返回值
Functionapply1个参数有返回值
Consumeaccept1个参数无返回值
supplierget没有参数有返回值
BiConsumeraccept2个参数无返回值

get和join的区别是get需要抛出异常

常用方法
  1. 获得结果和触发计算

    1. get()
    2. get(long timeout,Time unit)
    3. join()
    4. getNow(T valueIfbsent) 立即获取结果不阻塞
  2. 对计算结果进行处理

    1. thenApply 根据前一步结果进行下一步

      ExecutorService threadPool = Executors.newFixedThreadPool(3);
              CompletableFuture.supplyAsync(()->{
                  try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
                  System.out.println("第一步");
                  return 1;
              },threadPool).thenApply(r ->{
                  System.out.println("第二步");
                  return r+1;
              }).thenApply(r->{
                  System.out.println("第三步");
                  return r+2;
              }).whenComplete((v,e)->{
                  if(e==null){
                      System.out.println("结果"+v);
                  }
              }).exceptionally(e->{
                  e.printStackTrace();
                  System.out.println(e.getMessage());
                  return null;
              });
              threadPool.shutdown();
      
    2. handle 有异常依然会进行下一步

  3. 对计算结果进行消费

    1. thenAccept 跟thenApply类似,但是无返回结果
    2. thenRun和thenRunAsync thenRun是使用和前一个一样的线程池,thenRunAsync是不会使用和前一个一样的线程池,会直接使用ForkJoin的默认线程池
  4. 对计算结果进行选用

    1. applyToEither 判断哪个进程谁最先返回结果
  5. 对计算结果进行合并

    1. thenCombine 对两个结果进行处理取得一个结果

三、锁

乐观锁和悲观锁
悲观锁
  • 适合操作多的场景,只能让一个人进行操作,比如synchronized和lock的实现类都是悲观锁。
乐观锁
  • 认为不会有别的线程修改数据和资源所以不会添加锁,无锁编程实现,更新数据时会判断,没有则写入,有则选择放弃修改或者重试抢锁
  • 采用CAS(compare and swap )算法进行判断或者Version版本号
  • 并发性强,安全性降低,因为读的操作性能得到的很大的提升
8锁案例
  1. 标准访问ab两个线程,先a后b
  2. a中加了延时三秒,依然先a后b
  3. 添加一个普通方法c,先c后a
  4. 两个对象分别访问ab,先b后a
  5. ab加上静态static,先a后b
  6. ab加上静态static,两个对象分别访问ab,依然先a后b
  7. a加上静态static,先b后a
  8. a加上静态static,两个对象分别访问ab,先b后a
总结:
  1. synchronized锁的是资源类,同一时间只能访问一个synchronized方法
  2. 加上静态方法,锁的就是类锁不是对象锁
synchronized同步代码块
  1. javap -c ****.class可以反编译
 public void m1();
    Code:
       0: aload_0
       1: dup
       2: astore_1
       3: monitorenter
       4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       7: ldc           #3                  // String 离谱
       9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      12: aload_1
      13: monitorexit
      14: goto          22
      17: astore_2
      18: aload_1
      19: monitorexit
      20: aload_2
      21: athrow
      22: return

第一个exit属于正常退出,第二个是抛出异常情况下。synchronized使用的命令是monitorenter和monitorexit

synchronized普通方法

1.javap -v ****.class查看详细信息

public synchronized void m2();
    descriptor: ()V
    flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String 离谱
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 13: 0
        line 14: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   Lcom/lzj/LockSynDemo;

ACC_SYNCHRONIZED表明已经上锁

synchronized静态方法
public static synchronized void m3();
    descriptor: ()V
    flags: (0x0029) ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String 离谱
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 19: 0
        line 20: 8

ACC_STATIC和ACC_SYNCHRONIZED可以代表类锁

非公平锁

抢夺锁和获取锁的几率不公平,不需要按照顺序获取锁,可以插队

公平锁

抢夺和获取锁的几率公平,在队列等待中按顺序进入线程

Lock lock = new ReentrantLock(true);

默认非公平锁

公平锁和非公平锁的代码的区别就是有没有判断是否有排队的队列

总结

非公平锁没有线程之间的切换,所以可以尽量减少CPU的损耗,提高吞吐量,而公平锁在业务需要公平的时候才需要。

可重入锁

一个线程的多个流程可以获取同一把锁,持有一把锁就可以进入同步代码块中的同步代码块,不需要再获取第二把锁。

synchronized和ReentrantLock都是可重入锁。

死锁

AB两个线程同时拥有属于自己AB锁,但是依然想去获得对方的锁,导致出现等待的情况下,无法推进。

排查死锁

jps -l 找出发生故障的程序

jstack 进程编号

win+R输入jconsole可以进入控制台检查死锁

线程中断

线程中断是指线程在进行过程中,调用另一个线程调用需要中断线程的interrupt方法,来让该线程的中断标识变为ture,这是java自身提供的一种协商机制(请求中断),主线程就可以根据这个标识符判断是否要中断,依然需要自己实现中断的工能。

interrupt将中断状态设置为true,属于一种协商
interrupted返回中断状态值,并将状态置为false
isinterrupted返回中断状态值
如何中断一个线程
  1. 通过volatile变量(设置一个变量,线程通过不断地检索来判断是否需要停止)
  2. AtomicBoolean通过该api进行判断用法类似1的volatile
  3. 使用自带协商机制进行中断主要是interrupt和isinterrupted

不活动的线程再次返回状态值是false

线程处于(join,wait,sleep状态)调用interrupt方法退出阻塞状态会抛出异常,因为中断标识被清空重置为false,需要重新

等待唤醒机制
  1. wait和notify

    Object Lock = new Object();
            new Thread(()->{
                synchronized (Lock){
                    System.out.println("进入---");
                    try {
                        Lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("结束");
                }
            },"t1").start();
            try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}
            new Thread(()->{
                synchronized (Lock){
                    Lock.notify();
                    System.out.println("释放");
                }
            },"t2").start();
    
    • 必须要在代码块里

    • 必须按照一定时间顺序执行

  2. await和signal

    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();
    new Thread(() -> {
    
        lock.lock();
        try {
            System.out.println("进入---");
            condition.await();
            System.out.println("结束");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }, "t1").start();
    try { TimeUnit.SECONDS.sleep(3);} catch(InterruptedException e) { e.printStackTrace();}
    new Thread(() -> {
    
        lock.lock();
        try {
            condition.signal();
            System.out.println("释放");
        } finally {
            lock.unlock();
        }
    }, "t2").start();
    
    • 必须要在lock和synchronized里面
  3. locksupport的park和unpark

    Thread t1 = new Thread(() -> {
        System.out.println("进入---");
        LockSupport.park();
        System.out.println("结束");
    }, "t1");
    t1.start();
    
    new Thread(() -> {
        LockSupport.unpark(t1);
        System.out.println("释放");
    
    }, "t2").start();
    
    • 不需要再锁块里面
    • 不需要先等待再唤醒,可以提前唤醒
    • 只能有一个通信证

JMM内存模型

JMM本身是一种抽象的概念并不真实存在它仅仅描述的是一组约定和规范,通过这组规范定义了程序中(尤其是多线程)各个变量的读写访问方式并决定一个线程对共享变量的写入何时以及如何变成对另一个线程可见,关键技术点都是围绕多线程的原子性、可见性和有序性展开的。

原子性:同一时刻只有一个线程进行操作

有序性:只要程序的最终结果与它顺序化执行的结果相等,那么指令的执行顺序可以与代码顺序不一致,叫做重排序

happens-before原则

  1. 次序规则
  2. 锁定规则
  3. volatile变量规则
  4. 传递规则
  5. 线程启动规则
  6. 线程中断规则
  7. 线程终止规则
  8. 对象终结规则

volatile

  • 被修饰变量的特点:可见性,有序性

  • 内存语义:

    1. 当写一个volatile变量时,JMM会把该线程对应的本地内存地址中的共享变量值立即刷新回主内存中。
    2. 当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,重新回到主内存中读取最新共享变量。
  • 依靠内存屏障保证可见性和有序性

    屏障类型指令示例说明
    LoadLoadLoad1;LoadLoad;Load2保证先读1后读2
    StoreStoreStore1;StoreStore;Store2保证写1刷新入主内存再写2
    LoadStoreLoad1;LoadStore;Store2保证读1结束再写2
    StoreLoadStore1;StoreLoad;Load2保证写1刷新入主内存再写2
    第一个操作第二个操作:普通读写第二个操作:volatile读第二个操作:volatile写
    普通读写可以重排可以重排不可以重排
    volatile读不可以重排不可以重排不可以重排
    volatile写可以重排不可以重排不可以重排

    在每一个volatile写操作前面插入一个StoreStore屏障(禁止上面的写和下面的写重排序)

    在每一个volatile写操作后面插入一个StoreLoad屏障(禁止上面的写和下面的读写重排序)

    在每个volatile读操作后面插入一个LoadLoad屏障(禁止上面的读和下面的读重排序)

    在每个volatile读操作后面插入一个LoadStore屏障(禁止上面的读和下面的写重排序)

    使用场景
    • 写入加synchronize保证原子性
    • 不可以适用于运算赋值例如i++,适合单一赋值

    JVM在把字节码生成机器码的时候,发现操作是volatile变量的话,会按照JMM的规范在相应的位置插入内存屏障

CAS

包含三个操作数:内存位置、预期原值以及更新值

位置内存值V,旧的预期值A,修改的更新值B。

当且仅当旧的预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做或重来,当它重来重试的这种行为称为自旋。

CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法(如compareandswapXXX)底层实现即为CPU指令cmpxchg

缺点:dowhile长时间循环CPU开销大

ABA问题:尽管结果正确,过程不一定没问题

版本号解决ABA问题

原子类

基本类型原子类
  1. AtomicInteger

  2. AtomicBoolean

  3. AtomicLong

    class MyNumber{
        AtomicInteger atomicInteger = new AtomicInteger();
    
    
        public void addPlusPlus(){
            atomicInteger.getAndIncrement();
        }
    }
    
    
    public class AtomicIntegerDemo {
        public static final int SIZE = 50;
        public static void main(String[] args) throws InterruptedException {
            CountDownLatch countDownLatch =new CountDownLatch(SIZE);
            MyNumber myNumber = new MyNumber();
            for (int i = 0; i < SIZE; i++) {
                new Thread(()->{
                    try {
                        for (int j = 0; j < 1000; j++) {
                            myNumber.addPlusPlus();
                        }
                    }finally {
                        countDownLatch.countDown();
                    }
                },String.valueOf(i)).start();
            }
            countDownLatch.await();
            System.out.println(myNumber.atomicInteger);
        }
    }
    
数组类型原子类
  1. AtomicIntegerArray
  2. AtomicLongArray
  3. AtomicReferenceArray
引用类型原子类
  1. AtomicReference
  2. AtomicStampedReference 解决修改过几次
  3. AtomicMarkableReference 解决是否修改过
对象属性修改原子类
  1. AtomicIntegerFieldUpdater
  2. AtomicLongFieldUpdater
  3. AtomicRederenceFieldUpdater

使用目的:以一种线程安全的方式操作非线程安全内的某些字段

使用要求:更新的对象属性必须使用public volatile修饰符

class Bank{
    String name = "中国银行";
    public volatile int money = 0;
    AtomicIntegerFieldUpdater<Bank> atomicIntegerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(Bank.class,"money");
    public void addMoney(Bank bank){
        atomicIntegerFieldUpdater.getAndIncrement(bank);
    }
}
public class AtomicIntegerFieldUpdaterDemo {
    public static void main(String[] args) throws InterruptedException {
        Bank bank = new Bank();
        CountDownLatch countDownLatch = new CountDownLatch(10);
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                try {
                    for (int j = 0; j < 1000; j++) {
                        bank.addMoney(bank);
                    }
                }finally {
                    countDownLatch.countDown();
                }
            },String.valueOf(i)).start();
        }
        countDownLatch.await();
        System.out.println(bank.money);

    }

}
原子操作增强类
  1. DoubleAccumlator
  2. DoubleAdder
  3. LongAccumulator
  4. LongAdder

LongAddr和LongAccumulator的性能好,因为减少乐观锁的重试。高并发情况下优势明显。

LongAdder的基本思路就是分散热点,将value值分散到一个Cell数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的哪个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多,如果要获取真正的long值,只要将各个槽中的变量值累加返回。

add源码分析:一开始Cell数组是null,当Base更新失败后,创建2个Cell,如果依然竞争激烈,创建2的次幂的Cell,达到CPU的核数限制为止

AtomicLong是多个线程针对单个热点值value进行原子操作,性能损耗,精度高

LongAdder是每个线程拥有自己的槽位,各个线程对自己槽位中的那个值进行CAS操作,精度有所不准确,性能好。

ThreadLocal

使用完ThreadLocal里面的数据后需要remove,否则会导致线程池中的线程复用,导致数据泄露,和影响业务逻辑,需要用try-finally回收

ThreadLocal和Thread和ThreadLocalMap

ThreadLocalMap实际上就是一个以ThreadLocal实例为key,任意对象为value的Entry对象。当我们为ThreadLocal变量赋值,实际上就是以当前ThreadLocal实例为key,值为value的Entry往这个ThreadLocalMap中存放。(ThreadLocal为key)

ThreadLocalMap的key是弱引用ThreadLocal

ThreadLocal内存泄露

不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露

  1. 强引用:属于默认支持模式,只要有引用的地方就不会回收

  2. 软引用:系统内存充足不会回收,内存不充足会回收

  3. 弱引用:无差别回收

  4. 虚引用:必须和引用队列一起使用,get方法永远返回null,主要用来跟踪垃圾回收的情况

    软引用和弱引用可以作用于大规模的图片

为什么源代码用弱引用

如果这个key为强引用,就会导致key指向的ThreadLocal对象及v指向 的对象不能被gc回收,造成内存泄露

如果这个引用是弱引用就大概率会减少内存泄露问题

ThreadLocal如果没有外部强引用引用他,那么gc会回收ThreadLocal,导致ThreadLocalMap中出现原先的ThreadLocal作为key变为null,value一直存在,需要用remove清除

对象内存布局

对象实例的构成主要是对象头,实例数据,对齐填充。

对象头
  • 对象标记(8字节)
    1. 哈希码
    2. GC标记
    3. GC次数
    4. 同步锁标记
    5. 偏向锁持有者
  • 类元信息:指向方法区的class类元信息,判断是来自哪个类(4字节,如果只有对象头会补充4字节,达到8的倍数)
实例数据(根据类的成员类型的字节)
对齐填充

使对象头和实例数据的合保持在8的倍数,不足则对齐填充到8的倍数

压缩指针开启:对齐填充到8的倍数字节

压缩指针关闭:直接8的倍数字节

Sychronized与锁升级

无锁->偏向锁->轻量级锁->重量级锁

  • 偏向锁:MarkWord存储的是偏向的线程ID;
  • 轻量锁:MarkWord存储的是指向线程栈中LockRecord的指针;
  • 重量锁:MarkWord存储的是指向堆中的monitor对象的指针

没有调用hashcode,是不会在对象头中有哈希编码的(从右下角往上读取)

偏向锁

当一段同步代码一直被同一个线程多次访问,由于只有一个线程那么该线程在后续访问时便会自动获得锁

是否偏向锁:0 锁标志位:01

解释:会偏向于第一个访问的线程,后面没有被其他线程访问,就永远不需要发同步。第一个线程访问时,会将前54位指向线程指针,偏向锁位为1,如果再次访问判断线程指针是否相等,相等则不需要改变

偏向锁开启:需要将延时改成0才会立刻启动,或者等待程序运行4秒

偏向锁撤销:当有另外线程逐步来竞争锁的时候,就不能使用偏向锁,要升级为轻量级锁(撤销需要等待到全局安全点)

偏向锁竞争:如果线程退出执行方法,则会释放锁,恢复线程回偏向锁,如果依然在执行,会升级为轻量级锁等待线程结束,不断自旋

关于hashcode:偏向锁使用过程中调用hashcode会强制变为重量级锁。还未使用偏向锁前调用hashcode会升级为轻量级锁

java15逐渐废弃。

轻量级锁

多线程竞争,但是任意时刻最多只有一个线程竞争,即不存在锁竞争太过激烈的情况,也就没有线程阻塞。

轻量级可以和hashcode共存。

是否偏向锁:0 锁标志位:00

核心是自旋锁,当自旋达到一定次数会变为重量级锁。

自旋次数:自适应,自旋成功会增加下次自旋最大次数,反之亦然。

重量级锁

有大量线程竞争参与锁的竞争

重量级锁可以与hashcode共存

锁标志位:10

优点缺点适用场景
偏向锁加锁和解锁不需要额外的消耗,执行非同步方法相比仅存在纳秒级的差距线程存在竞争,带来撤销锁的消耗只有一个线程访问的同步块
轻量级锁竞争的线程不会阻塞,提高程序响应速度自旋会因为始终得不到锁消耗CPU追求响应时间,同步块执行速度非常快
重量级锁线程不使用自旋,不消耗CPU线程阻塞,响应时间缓慢追求吞吐量,同步块执行速度较长

锁消除:同一方法代码块加的锁都是不同的锁,JIT(即时编译器)会直接忽略,因为没有意义,相当于没加锁。

锁粗化:同一个锁执行不同的方法,语法没问题,JIT会将这些方法合并,减少加锁释放锁的消耗。

AQS(AbstractQueuedSynchronizer)

字面意思:抽象的队列同步器

技术解释:是用来实现锁或者其他同步器组件的公共基础部分的抽象实现,是重量级框架及整个JUC体系的基石,主要用于解决锁分配给”谁”。整体就是一个抽象的FIFO队列来完成资源获取线程的排队工作,通过一个int来表示状态

锁:面向锁的使用者 同步器:面向锁的实现者

CLH:Craig、Landin and Hagersten 队列,是个单向链表,AQS中的队列是CLH变体的虚拟双向队列(FIFO)

addWaiter(Node node):enq(node)双向链表中,第一个节点为虚节点(也叫哨兵节点),其实并不存储任何信息,只是占位,真正的第一个有数据的节点,是从第二个节点开始的。

读写锁

无锁->独占锁->读写锁->邮戳锁

ReentrantReadWriteLock

一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程

读写互斥,读读可以共享,多线程并发可以访问,容许多个线程读取

缺点:读取的时候没有完成,其他线程无法获得

锁降级
  • 遵循获取写锁,获取读锁再释放写锁的次序,将写锁降级为读锁
  • 先获取写锁,然后获取读锁,再释放写锁次序。(按顺序编写)
  • 如果释放了写锁,那么就完全转换为读锁
  • 不可锁升级(在读锁中加写锁)
  • 保证数据可见性

邮戳锁(StampedLock)

  • 可以在读取的时候,写锁可以介入
  1. Reading(读模式悲观)
  2. Writing(写模式)
  3. Optimistic reading(乐观读模式)可以升级为悲观读锁

缺点:

  • 不支持重入,没有Re开头
  • 悲观读锁和写锁不支持条件变量Condition
  • 不要调用中断操作,interrupt()
    通过一个int来表示状态

锁:面向锁的使用者 同步器:面向锁的实现者

CLH:Craig、Landin and Hagersten 队列,是个单向链表,AQS中的队列是CLH变体的虚拟双向队列(FIFO)

addWaiter(Node node):enq(node)双向链表中,第一个节点为虚节点(也叫哨兵节点),其实并不存储任何信息,只是占位,真正的第一个有数据的节点,是从第二个节点开始的。

读写锁

无锁->独占锁->读写锁->邮戳锁

ReentrantReadWriteLock

一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程

读写互斥,读读可以共享,多线程并发可以访问,容许多个线程读取

缺点:读取的时候没有完成,其他线程无法获得

锁降级
  • 遵循获取写锁,获取读锁再释放写锁的次序,将写锁降级为读锁
  • 先获取写锁,然后获取读锁,再释放写锁次序。(按顺序编写)
  • 如果释放了写锁,那么就完全转换为读锁
  • 不可锁升级(在读锁中加写锁)
  • 保证数据可见性

邮戳锁(StampedLock)

  • 可以在读取的时候,写锁可以介入
  1. Reading(读模式悲观)
  2. Writing(写模式)
  3. Optimistic reading(乐观读模式)可以升级为悲观读锁

缺点:

  • 不支持重入,没有Re开头
  • 悲观读锁和写锁不支持条件变量Condition
  • 不要调用中断操作,interrupt()
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值