Java 并发编程原理解析

一、synchronized关键字的底层原理

synchronized 同步语句块的实现,使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。

当执行 monitorenter 指令时,线程试图获取锁,也就是获取 monitor ( monitor 对象存在于每个 Java 对象的对象头中,synchronized 锁便是通过这种方式获取锁的,这也是为什么 Java 中任意对象都可以作为锁的原因) 的持有权。当计数器为0,则可以成功获取,获取后将锁计数器加1;相应的,在执行 monitorexit 指令后,将锁计数器减1,当值为0时,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

synchronized是支持可重入锁的,所以锁计数器可能加到2在执行两次monitorexit指令后,计数器的值为0,释放锁。

synchronized 修饰方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词), 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor。如果一个同步方法执行期间抛 出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放

 

二、CAS的理解以及其底层实现原理

CAS是什么?

比较并交换,它是一条CPU并发原语。判断内存某个位置的值是否为预期值,如果是更改为新值,这个过程是原子的。

原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题

getAndIncrement()底层调用unsafe类方法,传入三个参数,unsafe.getAndAddInt() 底层使用CAS思想,如果比较成功加1,如果比较失败重新获得,再比较一次,直至成功。

Unsafe类:CAS的核心类,由于java方法无法直接访问底层系统,需要通过本地(native)方法来访问,基于该类可以直接操作特定内存的数据。
变量valueOffset:表示该变量在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。
value用volatile修饰:保证多线程之间的内存可见性

CAS缺点:

  • 循环时间长开销大(如果CAS失败,会一直尝试)
  • 只能保证一个共享变量的原子操作。(对多个共享变量操作时,循环CAS无法保证操作的原子性,只能用加锁来保证)
  • 存在ABA问题

原子类AtomicInteger类ABA问题及解决方案

1、ABA问题是怎么产生的?

当第一个线程执行CAS(V,E,U)操作,在获取到当前变量V,准备修改为新值U前,另外两个线程已连续修改了两次变量V的值,使得该值又恢复为旧值,这样我们就无法正确判断这个变量是否已被修改过。

2、ABA问题的解决方案:

AtomicStampedReference:是一个带有时间戳的对象引用,在每次修改后,不仅会设置新值还会记录更改的时间。

AtomicMarkableReference:维护的是一个boolean值的标识,这种方式并不能完全防止ABA问题的发生,只能减少ABA发生的概率。

三、ConcurrentHashMap实现线程安全的底层原理

ConcurrentHashMap实现线程安全的底层原理到底是什么?

1,JDK 1.8以前,多个数组,分段加锁,一个数组一个锁。

2,JDK 1.8以后,优化细粒度,一个数组,每个元素先进行CAS,如果失败说明有人put过值了,此时synchronized对这个数组元素加锁,链表+红黑树处理。jdk1.8+是对数组每个元素加锁。

 

四、你对JDK中的AQS理解吗?AQS的实现原理是什么?

AQS :Abstract Queue Synchronizer,抽象队列同步器。

AQS的功能分为两种:独占和共享

  • 独占锁,每次只能有一个线程持有锁,例如ReentrantLock就是以独占方式实现的互斥锁
  • 共享锁,允许多个线程同时获取锁,并发访问共享资源,例如ReentrantReadWriteLock

AQS原理:AQS的实现依赖内部的同步队列,也就是FIFO的双向队列,如果当前线程竞争锁失败,那么AQS会把当前线程以及等待状态信息构造成一个Node加入到同步队列中,同时再阻塞该线程。当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程)。

当添加节点时:节点添加至队列尾部,prev指向之前末尾节点,之前末尾节点的next指向当前节点,tail指向该节点

移出节点时:首部节点的下一个节点将prev的指针指向null,head指向首部节点的下一个节点

AQS实现原理:

当lock.lock()的时候,实际上底层是由AQS来完成的加锁,AQS提供了一个state表示加锁状态,默认0表示不加锁,Thread的属性存放加锁线程;

当加锁的时候,通过CAS操作将state+1,则成功获取锁,CAS操作达到了加锁的互斥效果

锁的可重入性,就是通过state不断+1实现,对一个ReentrantLock不断加锁,则state不断+1,释放锁则-1

当加锁失败后,会把失败线程加到AQS中的队列里,等待获取锁

如果是公平锁,当释放锁后会从队列头结点来获取线程加锁,非公平锁则可能新来的线程也可能抢到锁

 

五、线程池的底层工作原理

// int corePoolSize:线程池维护线程的最小数量. 
// int maximumPoolSize:线程池维护线程的最大数量. 
// long keepAliveTime:空闲线程的存活时间. 
// TimeUnit unit: 时间单位,现有纳秒,微秒,毫秒,秒枚举值. 
// BlockingQueue<Runnable> workQueue:持有等待执行的任务队列. 
// RejectedExecutionHandler handler: 
//   用来拒绝一个任务的执行,有两种情况会发生这种情况。 
//     一是在execute方法中若addIfUnderMaximumPoolSize(command)为false,即线程池已经饱和; 
//     二是在execute方法中, 发现runState!=RUNNING || poolSize == 0,即已经shutdown,就调用ensureQueuedTaskHandled(Runnable command),在该方法中有可能调用reject。
 public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

ThreadPoolExecutor池子的处理流程如下:  

1)当池子大小小于corePoolSize就新建线程,并处理请求

2)当池子大小等于corePoolSize,把请求放入workQueue中,池子里的空闲线程就去从workQueue中取任务并处理

3)当workQueue放不下新入的任务时,新建线程入池,并处理请求,如果池子大小撑到了maximumPoolSize就用RejectedExecutionHandler来做拒绝处理

4)另外,当池子的线程数大于corePoolSize的时候,多余的线程会等待keepAliveTime长的时间,如果无请求可处理就自行销毁

其会优先创建  CorePoolSiz 线程, 当继续增加线程时,先放入Queue中,当 CorePoolSiz  和 Queue 都满的时候,就增加创建新线程,当线程达到MaxPoolSize的时候,就会抛出错 误 org.springframework.core.task.TaskRejectedException

另外MaxPoolSize的设定如果比系统支持的线程数还要大时,会抛出java.lang.OutOfMemoryError: unable to create new native thread 异常。

Reject策略预定义有四种:

(1)ThreadPoolExecutor.AbortPolicy:是默认的策略,处理程序遭到拒绝将抛出运行时 RejectedExecutionException。 

(2)ThreadPoolExecutor.CallerRunsPolicy:,调用者的线程会执行该任务,如果执行器已关闭,则丢弃. 

(3)ThreadPoolExecutor.DiscardPolicy:不能执行的任务将被丢弃. 

(4)ThreadPoolExecutor.DiscardOldestPolicy:如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重试执行程序(如果再次失败,则重复此过程).

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值