首页 > 编程语言 >JUC并发编程与源码分析

JUC并发编程与源码分析

时间:2024-02-20 09:56:36浏览次数:30  
标签:JUC 对象 编程 源码 volatile 内存 线程 方法 public

基础

JUC是java.util.concurrent在并发编程中使用的工具包。

线程的start()方法底层使用本地方法start0()调用C语言接口,再由C语言接口调用操作系统创建线程。

 public class demo(){
  public static void main(Strings[] args){
    Thread t1 = new Thread(() -> {
      System.out.println("启动线程");
    },"t1").start();
  }
}

管程,monitor,也就是我们平时所说的锁。

CompletableFuture

CompletableFuture提供了一种观察者模式类似的机制,可以让任务执行完成后通知监听的一方。

public class CompletableFuture<T> implements Future<T>,CompletionStage<T>

CompletableFuture的默认线程池是守护线程,因此在主线程结束时会跟着结束,因此最好使用自定义的线程池。

CompletableFuture的优点:异步任务结束时,会自动回调某个对象的方法。

join()方法与get()方法类型,只是不会抛出编译时异常。

合并结果使用thenCombine()方法。

public class CompletableFutureDemo{
  public static void main(String[] args) throws Execption{
    ExecutorSevice threadpool = Executors.newFixedThreadPool(3);
    CompletableFuture<int> completablefuture1 = CompletableFuture.SupplyAsync(() -> {
      int result = 7;
      return result;
    },threadpool).whenComplete((result,exception) -> {
      if(exception == null){
        System.out.println(result);
      }
      //关闭线程池
      threadpool.shutdown();
    });
  }
}

Future接口

Future接口(FutureTask实现类)定义了操作异步任务执行一些方法,如获取异步任务的执行结果、取消任务的执行、判断任务是否被取消、判断任务执行是否完毕等。

Future的缺点是get()方法容易导致阻塞,isDone()方法轮询问题,因此对于结果的获取不友好。

public class CompletableFutreDemo(){
  public static void main(String[] args) throws Exception{
    FutureTask<String> futureTask = new FutureTask<>(new Mythread());
    Thread t1 = new Thread(futureTask);
    t1.start();
    System.out.println(futureTask.get());
  }
}

public class MyThread implements Callable<String>{
  @Override
  public String call() throws Exception{
    System.out.println("进入方法");
    return "hello";
  }
}

函数式接口

函数式接口名称 方法名称 参数 返回值
Runnable run
Function apply 1
Consumer accept 1
Supplier get
BiConsumer accept 2

synchronized的方法并不影响普通方法的使用。

对于普通同步方法,锁的是当前实例对象,通常指this,所有的普通同步方法用的都是同一把锁,即实例对象本身。对于静态同步方法,锁的是当前类的Class对象。对于同步方法块,锁的是synchronized括号内的对象。

具体实例对象this和唯一模板CLass,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的。

通过反编译,synchronized的底层实现是monitorenter和monitorexit指令,而且会有两个monitorexit以保证能够正常退出。对象锁会有ACC_SYNCHRONIZED标记位。

每个对象都带有一个对象监视器,每个锁住的对象都会与Monitor关联起来。在java的头文件中存储了锁的相关信息,每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。

ReentrantLock(true)可以将非公平锁设置为公平锁。公平锁会按照先到先得的顺序获取锁。默认是非公平锁,因为能更充分地利用CPU资源,而且减少线程的切换。

可重入锁,又称为递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象)不会因为之前已经获取过还没释放而阻塞。

LockSupport与线程中断

线程中断

一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。

中断只是一种协作协商机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现,interrupt()方法仅仅是将线程对象的中断标识设置为true。

Thread.interrupted()静态方法判断线程是否被中断并清除当前中断状态。

中断运行中的线程:使用volatile变量、使用具有原子性的AtomicBoolean和Thread的API方法,思路都是使用标记退出程序。

如果线程处于被阻塞状态,在别的线程中调用当前线程对象的interrupt()方法,那么线程将清除中断状态,立即退出被阻塞状态,并抛出InterruptedException异常。中断不活动的线程不会产生任何影响。

LockSupport

LockSupport是用来创建锁和其他同步类的基本线程阻塞原语,其中park()方法和unpark()方法的作用分别是阻塞线程和解除阻塞线程。

阻塞和唤醒线程的方法:使用Object中的wait()方法让线程等待和notify()方法唤醒线程,与synchronized搭配使用。使用JUC包中Condition的await()方法让线程等待和使用signal()方法唤醒线程,与ReentrantLock搭配使用;LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程。

由于LockSupport底层使用类似信号量的机制,每个最多只有一个通行证,因此unpark()可以在park()前执行。

//ReentrantLock的使用
public class CompletableFutreDemo(){
  public static void main(String[] args) throws Exception{
    Lock UseLock = new ReentrantLock();
    Condition condition = Uselock.newCondition();
    new Thread(() -> {
      UseLock.lock();
      condition.await();
      UseLock.unlock();
    },"t1").start();
    new Thread(() -> {
      UseLock.lock();
      condition.signal();
      UseLock.unlock();
    },"t2").start();
  }
}

//LockSupport的使用
public class CompletableFutreDemo(){
  public static void main(String[] args) throws Exception{
    Thread t1 = new Thread(() -> {
      LockSupport.park();
    },"t1");
    t1.start();
    //而且即使unpark()先执行,park()仍能成功执行。
    new Thread(() -> {
      LockSupport.unpark(t1);
    },"t2").start();
  }
}

Java内存模型JMM

由于CPU的运行是先把内存中的数据读到缓存,因此JVM试图使用JMM来屏蔽掉各种硬件和操作系统的内存访问差别。

JMM本身是抽象的概念,是一组约定,围绕多线程的原子性、可见性和有序性展开。

JMM三大特性:可见性、原子性和有序性。

可见性是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道该变更。

指令重排可以保证串行语义一致,但并不保证多线程的语义一致。

从源代码到最终执行:源代码->编译器优化的重排->指令并行的重排->内存系统的重排->最终执行的指令。

volatile

volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取。

volatile拥有可见性和有序性,但不具备原子性。

内存屏障是CPU或编译器在对内存随机访问的操作中的一个同步点,使得之前的所有读写操作都执行后才可以执行改点之后的操作。

内存屏障分为读屏障和写屏障。读操作会在后面加上两个内存屏障,写操作会在前后各加上一个内存屏障。

public class VolatileTest{
  int i = 0;
  volatile boolean flag = false;
  
  public void write(){
    //i和flag由于没有数据依赖性,因此可能发生指令重排序
    i = 2;
    //但volatile会在flag=true前加入StoreStore屏障禁止上面的普通写与下面的volatile写重排序,后面假设StoreLoad屏障禁止下面可能有的volatile读写重排序
    flag = true;
  }
  
  public void read(){
    //在每一个volatile读操作后面插入一个LoadLoad屏障,禁止处理器把下面的普通读重排序。在每一个volatilei读操作后面插入一个LoadStore屏障,禁止与下面的普通写重排序
    if(flag){
      //如果flag先赋值为true,那么会打印i=0。
      System.out.println("i="+i);
    }
  }
}

Java内存模型中定义的8种每个线程自己的工作内存与主物理内存之间的原子操作:read(读取)→load(加载)→use(使用)→assign(赋值)→store(存储)→write(写入)→lock(锁定)→unlock(解锁)。

步骤中包含加锁和解锁但没有保证原子性的原则是在读取和写入的阶段加锁,但是保证原子性要将读取、计算、写入这整个阶段才能保证。因为在A读,B读,A计算,B计算,A写入,B写入,A的操作就被覆盖了。因此volatile变量不参与变量的计算,常用于布尔值标记。

在单例模式中,在创建实例singleton = new SafeDoubleCheckSingleton()的时候会有三个步骤,开辟内存空间、新建对象,将指针指向新建对象。但是由于指令重排,第二和第三步逆转会导致其他线程获取到空对象,因此需要将singleton声明为volatile。

volatile写之前的操作都禁止重排序到volatile之后;volatile读之后的操作都紧张重排序到volatile之前。

CAS

CAS(compare and swap),比较并交换,包含三个操作数:内存位置、预期原值及更新值。

CAS是CPU的原子指令(cmpxchg指令),由Unsafe类的本地方法调用,其内部方法操作可以像C的指针一样直接操作内存。

AtomicInteger主要利用CAS、volatile和native方法保证原子操作。

CAS是靠硬件实现的从而在硬件层面提升效率,最底层还是交给硬件来保证原子性和可见性。

public class SpinLock{
  AtomicReference<Thread> atomicReference = new AtomicReference<>();
  
  public void lock(){
    do{
      Thread thread = Thread.currentThread();
    }while(!atomicReference.compareAndSet(null,thread));
  }
  
  public void unLock(){
    Thread thread = Thread.currentThread();
    atomicReference.compareAndSet(thread,null);
  }
}

原子操作类

可以使用countDownLatch类来记录线程个数,用于等待线程全部执行后输出结果。

CAS的缺点是循环时间开销大和ABA问题。ABA问题可以通过添加版本号解决,AtomicStampedReference,或者使用标记位,AtomicMarkableRefence

原子类有针对属性修改的原子类,便于更小粒度的加锁范围,要求更新的对象属性必须使用public volatile修饰符,且因为对象的属性修改类型原子类都是抽象类,所以每次使用都必励使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。

class BankAccount{
  String bankName = "CCB";//不需要特殊处理的属性
  
  public volatile int money = 0;//需要使用public volatile

  //使用newUpdater设置类和属性
  AtomicIntegerFieldUpdate<BankAccount> fieldUpdate = AtomicIntegerFieldUpdate.newUpdater(BankAccount.class,"money");
  
  public void transMoney(BankAccount bankAccount){
    filedupdater.getAndIncrement(bankAccount);
  }
}

如果是JDK8,推荐使用LongAdder对象,比AtomicLong性能更好,原因在于化整为零,最后再求和。LongAdder只能用来计算加法,且从零开始计算,而LongAccumulator提供了自定义的函数操作。

LongAdder是Striped64的子类。最重要的属性是cell数组和base,统计求和:base+cell数组,cell数组中的元素格式是2的倍数。

sum执行时,并没有限制对base和cells的更新。所以LongAdder不是强一致性的,它是最终一致性的。

ThreadLocal

ThreadLocal提供线程局部变量。每个线程在访问ThreadLocal实例的时候都有自己独立初始化的变量副本。

class House{
  Threadlocal<Integer> saleVolume = ThreadLocal.withInitial(() -> 0);
  public void saleVolumeByThreadLocal(){
    saleVolume.set(saleVolume.get()+1);
  }
}

如果在线程池中使用到ThreadLocal,在使用结束后一定要使用remove清除,否则会造成问题。

threadLocalMap实际上就是一个以threadLocal实例为key,任意对象为value的Entry对象。

虚引用必须和引用队列(ReferenceQueue)联合使用;Get方法总是返回null,处理监控通知使用。

ThreadLocal使用弱引用,以避免内存泄漏,由于有线程的强引用指向ThreadLocal对象,因此不用担心在使用过程中被回收掉。

每个线程持有一个只属于自己的专属Map并维护了ThreadLocal对象与具体实例的映射,该Map由于只被持有它的线程访问,故不存在线程安全以及锁的问题。

对象内存布局与对象头

虚拟机要求对象起始地址必须是8字节的整数倍。对象标记8个字节,类型指针8个字节。

Synchronized与锁升级

锁升级过程:无锁、偏向锁、轻量级锁、重量级锁。

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

偏向锁在默认情况下会延时4秒启动,可以立即启动或者选择关闭。从java15逐步废弃偏向锁。

竞争线程尝试CAS更新对象头失败,会等待到全局安全点(此时不会扰行任何代码)撤销偏向锁。

java6之后,自旋次数是自适应的,如果自旋成功,下次自旋的最大次数会增加,反之会减少。

锁升级为轻量级或重量级锁后,Mark Word中保存的分别是线程栈帧里的锁记录指针和重量级锁指针,已经没有位置再保存哈希码, GC年龄。因此,当一个对象已经计算过一致性哈希码后,它就再也无法进入偏向锁状态了:而当一个对象当前正处于偏向锁状态,又收到需要计算其一致性哈希码请求时,它的偏向状态会被立即撤销,并且锁会膨胀为重量级锁。在轻量级锁会在当前线程的栈帧中创建一个锁记录空间,用于存储锁对象的Mark Word拷贝;重量级锁的ObjectMonitor类里有字段记录非加锁状态下的Mark Word。

编译期间会进行锁消除和锁粗化的优化。

AQS(AbstractQueuedSynchronizer)

AQS是用来实现锁或者其它同步器组件的公共基础部分的抽象实现,是重量级基础框架及整个JUC体系的基石,主要用于解决锁分配给"谁"的问题。整体就是一个抽象的双向FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量表示持有锁的状态。

AQS将要请求共享资源的线程及自身的等待状态封装成队列的结点对象(Node),通过 CAS、自旋以及LockSupport..park()的方式,维护state变量的状态,使并发达到同步的效果。

ReentrantLock中的sync属性继承AQS,根据是否公平锁,有fairSync和NonfairSync继承sync。在创建完公平锁/非公平锁后,调用Iock方法会进行加锁,最终都会调用到acquire方法。

node加入队列后会进行一次自旋尝试获取锁,如果不成功node节点使用LockSupport进行阻塞。

整个ReentrantLock的加锁过程,可以分为三个阶段:1、尝试加锁;2、加锁失败,线程入队列;3、线程入队列后,进入阻塞状态。

线程入队前会先判断队列是否已经初始化,不为空则将node插入到末尾,否则将执行队列的初始化。

自旋的时候会判断前辈节点是否head和判断前辈节点的waitStatus,因为第一次自旋会将前辈的waitStatus修改为signal,第二次自旋就会根据这个标志位将node阻塞。

waitStatus

枚举 含义
0 初始化后的默认值
CANCELLED(1) 线程获取锁的请求已经取消
CONDITION(-2) 节点在等待队列,等待唤醒
PROPAGATE(-3) 线程在SHARED情况,会进行传播
SINGAL(-1) 线程已准备好,等待资源释放

读写锁

无锁->独占锁->读写锁->邮戳锁的演化。

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

读写锁的缺点在于写锁饥饿问题。可以通过设置为公平锁轻微缓解。

读写锁常用的实现类是ReetrantReadWriteLock

锁降级:线程获取写锁后,在释放写锁前先获取读锁,这样就实现了写锁向读锁的降级。

ReadWriteLock rwlock = new ReentrantReadWriteLock();
rwlock.readlock().lock();
rwlock.writelock().lock();

StampedLock是JDK新增的一个读写锁,对读写锁reentrantReadWriteLock的优化,增加了乐观读的模式。采取乐观策略获取锁后,其他线程尝试获取写锁时不会被阻塞,因此在获取乐观读锁后,还需要对结果进行校验。

StampedLock是不可重入锁,因为重复获取写锁会造成问题。stamp代表了锁的状态。当stamp返回零时,表示线程获取锁失败,当释放锁或者转换锁的候,都要传入最初获取的stamp。

StampedLock的缺点是不支持重入、不支持条件变量,且使用时不要调用中断操作。

StampedLock stampedLock = new StampedLock();
//传统写锁
long stamp = stampedLock.writeLock();
stampedLock.unlockWrite(stamp);
//乐观策略
long stamp = stampedlock.tryoptimisticRead();
if(!stampedLock.validate(stamp)){
  stamp = stampedLock.readLock();//如果发生变化则取消使用乐观模式
}

标签:JUC,对象,编程,源码,volatile,内存,线程,方法,public
From: https://www.cnblogs.com/xiqin-huang/p/18022435

相关文章

  • springMvc源码解析
    流程:    》DispatcherServlet:前端控制器 》HandlerMapping:处理器映射器主要是为了找到处理器执行链,执行链中包含有实际的处理类、拦截器   》HandlerAdapter:处理器适配器主要是根据上一步的handle,适配选择对应的适配器。 》Handler(处理......
  • Python异步编程原理篇之IO多路复用模块selector
    selector简介selector是一个实现了IO复用模型的python包,实现了IO多路复用模型的select、poll和epoll等函数。它允许程序同时监听多个文件描述符(例如套接字),并在其中任何一个就绪时进行相应的操作。这样可以有效地管理并发I/O操作,提高程序的性能和资源利用率。本篇主要......
  • 唯一客服系统:Golang开发客服系统源码,支持网页,H5,APP,微信小程序公众号等接入,商家有PC端
    本系统采用GolangGin框架+GORM+MySQL+Vue+ElementUI开发的独立高性能在线客服系统。客服系统访客端支持PC端、移动端、小程序、公众号中接入客服,利用超链接、网页内嵌、二维码、定制对接等方式让网上所有通道都可以快速通过本系统联系到商家。 服务端可编译为二进制程序包,无......
  • 报废车综合管理系统 系统源码加微信820688215 获取商业授权 体验官方地址 www.lvxun.v
    报废车综合管理系统系统源码加微信820688215获取商业授权体验官方地址 www.lvxun.vip  ......
  • 异步编程简介
    异步编程是一种编程模式,旨在提高程序的性能和响应速度。通过将某些任务异步执行,程序可以在等待结果时继续执行其他任务,从而减少了阻塞和等待的时间。在本篇博客中,我们将详细探讨异步编程的各个方面,并介绍常见的异步编程技术和工具。什么是异步编程?传统的同步编程方式中,代码会按照......
  • Java开发的SRM供应商、在线询价、招投标采购一体化系统源码功能解析
    前言:随着全球化和信息化的发展,企业采购管理面临越来越多的挑战。传统的采购方式往往涉及到多个繁琐的步骤,包括供应商筛选、询价、招投标等,这些过程不仅耗时,而且容易出错。为了解决这些问题,供应商、询价、招投标一体化系统应运而生。该系统通过集成供应商管理、询价管理、招投标......
  • 源码剖析Spring依赖注入:今天你还不会,你就输了
    在之前的讲解中,我乐意将源码拿出来并粘贴在文章中,让大家看一下。然而,我最近意识到这样做不仅会占用很多篇幅,而且实际作用很小,因为大部分人不会花太多时间去阅读源码。因此,从今天开始,我将采取以下几个步骤:首先,我会提前画出一张图来展示本章节要讲解的内容的调用链路,供大家参考。其......
  • Junit5源码分析
    近期使用junit和springtest做公司的一个灰盒自动化项目,即非白盒单测和黑盒接口方式的自动化方式,验证代码中复杂的业务逻辑(金融相关),使用过程中遇到过一些使用问题,业余时间学习了下框架源码,略有收获,遂记录之。创建一个简单测试DEMO如下:新建一个TestApplication和一个server新建......
  • dotnet 异步编程
    异步与多线程是不同的概念异步并不意味着多线程,单线程同样可以异步。异步默认借助线程池。多线程经常会有阻塞的操作,而异步要求不阻塞。异步与多线程适用场景不同多线程:适合CPU密集型操作适合长期运行的任务线程的创建与销毁的开销是比较大的提供更底层的控制,操作线程、......
  • Swoole 源码分析之 Http Server 模块
    首发原文链接:Swoole源码分析之HttpServer模块Swoole源码分析之HttpServer模块Http模块的注册初始化这次我们分析的就是Swoole官网的这段代码,看似简单,实则不简单。在Swoole源码文件swoole_http_server.c中有这样一个函数php_swoole_http_server_minit。这个......