八股文学习三(jvm+线程池+锁)

1. jvm

(1)概念

JVM是可运行 Java 代码的假想计算机 ,包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收,堆 和 一个存储方法域。JVM 是运行在操作系统之上的,它与硬件没有直接的交互。

java运行过程:

我们都知道 Java 源文件,通过编译器,能够生产相应的.Class 文件,也就是字节码文件,而字节码文件又通过 Java 虚拟机中的解释器,编译成特定机器上的机器码 。

(2)JVM 运行时内存

Java 堆从 GC 的角度还可以细分为: 新生代(Eden 区、From Survivor 区和 To Survivor 区)和老年代。

Xms 是指设定程序启动时占用内存大小。一般来讲,大点,程序会启动的快一点,但是也可能会导致机器暂时间变慢。

Xmx 是指设定程序运行期间最大可占用的内存大小。如果程序运行需要占用更多的内存,超出了这个设置值,就会抛出OutOfMemory异常。

Xss 是指设定每个线程的堆栈大小。这个就要依据你的程序,看一个线程大约需要占用多少内存,可能会有多少线程同时运行等。
 

新生代

是用来存放新生的对象。一般占据堆的 1/3 空间。由于频繁创建对象,所以新生代会频繁触发MinorGC 进行垃圾回收。新生代又分为 Eden 区、ServivorFrom、ServivorTo 三个区。

Eden 区

Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当 Eden 区内存不够的时候就会触发 MinorGC,对新生代区进行一次垃圾回收。

SurvivorFrom

上一次 GC 的幸存者,作为这一次 GC 的被扫描者。

SurvivorTo

保留了一次 MinorGC 过程中的幸存者。

MinorGC 的过程(复制->清空->互换)

MinorGC 采用复制算法

1:eden、SurvivorFrom 复制到 SurvivorTo,年龄+1

首先,把 Eden 和 SurvivorFrom 区域中存活的对象复制到 SurvivorTo 区域(如果有对象的年龄以及达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1(如果 ServicorTo 不够位置了就放到老年区);

2:清空 eden、SurvivorFrom

然后,清空 Eden 和 SurvivorFrom 中的对象;

3:SurvivorTo 和 SurvivorFrom 互换

最后,SurvivorTo 和 SurvivorFrom 互换,原 SurvivorTo 成为下一次 GC 时的 SurvivorFrom区。

老年代

主要存放应用程序中生命周期长的内存对象。

老年代的对象比较稳定,所以 MajorGC 不会频繁执行。在进行 MajorGC 前一般都先进行了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。

MajorGC 采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC 的耗时比较长,因为要扫描再回收。MajorGC 会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出 OOM(Out of Memory)异常。

 (3)垃圾回收与算法

(4)jvm优化

cpu 100%原因:

常见的cpu飙升原因
程序中存在死循环或者长时间占用 CPU 的操作。比如,不合理的递归操作、循环操作等等。

程序中存在大量的计算操作,例如复杂的算法、大量的数值计算等等。

程序中存在大量的 IO 操作,例如读写文件、网络通信等等。

程序中存在大量的线程创建和销毁操作,以及线程间的竞争和同步操作。

程序中存在内存泄漏或者内存溢出,导致 JVM 不断进行垃圾回收。

程序中存在大量的数据库操作,导致数据库连接池的耗尽和数据库负载过高。


java 应用cpu飙升(超过100%)故障排查_javacpu占用过高原因_java路飞的博客-CSDN博客

java进程 jvm cpu100%问题排查_51CTO博客_java进程cpu使用率高排查

史上最全最详细的JVM优化方案---建议收藏

百度安全验证

JVM详解_夏屿_的博客-CSDN博客

(5)java泛型

Java的泛型是在Jdk 1.5 引入的,在此之前Jdk中的容器类等都是用Object来保证框架的灵活性,然后在读取时强转。但是这样做有个很大的问题,那就是类型不安全,编译器不能帮我们提前发现类型转换错误,会将这个风险带到运行时。 引入泛型,也就是为解决类型不安全的问题,但是由于当时java已经被广泛使用,保证版本的向前兼容是必须的,所以为了兼容老版本jdk,泛型的设计者选择了基于擦除的实现。

泛型通配符在具体使用时,有如下三种实现形式:

  • 未限定通配符(?) : ?表示未知类型的通配符;

  • 上限通配符(? extends T) : ?表示类型上限的通配符,T是一个类或接口;

  • 下限通配符(? super T) : ?表示类型下限的通配符,T是一个类或接口。

百度安全验证

(6)java nio

https://www.cnblogs.com/mikechenshare/p/16587635.html

NIO VS BIO

BIO

BIO全称是Blocking IO,同步阻塞式IO,是JDK1.4之前的传统IO模型。

Java BIO:服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如下图所示:

虽然此时服务器具备了高并发能力,即能够同时处理多个客户端请求了,但是却带来了一个问题,随着开启的线程数目增多,将会消耗过多的内存资源,导致服务器变慢甚至崩溃,NIO可以一定程度解决这个问题。

NIO

Java NIO: 同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理。

一个线程中就可以调用多路复用接口(java中是select)阻塞同时监听来自多个客户端的IO请求,一旦有收到IO请求就调用对应函数处理,NIO擅长1个线程管理多条连接,节约系统资源。

一个Selector能够同时轮询多个channel,这样,一个单独的线程就可以管理多个channel,从而管理多个网络连接,这样就不用为每一个连接都创建一个线程,同时也避免了多线程之间上下文切换导致的开销。

(7)零拷贝

百度安全验证

早期的数据IO,由用户进程向CPU发起,应用程序与磁盘之间的 I/O 操作都是通过 CPU 的中断完成的。CPU还要负责将磁盘缓冲区拷贝到内核缓冲区(pageCache),再从内核缓冲区拷贝到用户缓冲区。为了减少CPU占用,产生了DMA技术,大大解放了CPU。

DMA 的全称叫直接内存存取(Direct Memory Access),是一种允许外围设备(硬件子系统)直接访问系统主内存的机制。目前大多数的硬件设备,包括磁盘控制器、网卡、显卡以及声卡等都支持 DMA 技术。

零拷贝是指在数据传输过程中,避免了数据的多次拷贝,从而提高了数据传输的效率。在传统的IO模型中,数据从磁盘中读取到内核缓冲区,然后再从内核缓冲区拷贝到用户缓冲区,最后再从用户缓冲区拷贝到应用程序中。而在零拷贝模型中,数据可以直接从内核缓冲区拷贝到应用程序中,避免了数据的多次拷贝,提高了数据传输的效率。零拷贝技术可以通过mmap和sendfile等系统调用实现。

1. mmap+write零拷贝技术

以mmap+write的方式替代传统的read+write的方式,减少了一次拷贝。

mmap 是 Linux 提供的一种内存映射文件方法,即将一个进程的地址空间中的一段虚拟地址映射到磁盘文件地址使用 mmap 的目的是将内核中读缓冲区(read buffer)的地址与用户空间的缓冲区(user buffer)进行映射。从而实现内核缓冲区与应用程序内存的共享,省去了将数据从内核读缓冲区(read buffer)拷贝到用户缓冲区(user buffer)的过程。

2.Sendfile零拷贝技术

通过 Sendfile 系统调用,数据可以直接在内核空间内部进行 I/O 传输,从而省去了数据在用户空间和内核空间之间的来回拷贝。

2.限流

1. 限流方案


限流的实现方案有很多种,这里稍微理了一下,限流的分类如下所示:
①合法性验证限流:比如验证码、IP 黑名单等,这些手段可以有效的防止恶意攻击和爬虫采集;
②容器限流:比如 Tomcat、Nginx 等限流手段,其中 Tomcat 可以设置最大线程数(maxThreads),当并发超过最大线程数会排队等待执行;而 Nginx 提供了两种限流手段:一是控制速率,二是控制并发连接数;
③服务端限流:比如我们在服务器端通过限流算法实现限流,此项也是我们本文介绍的重点。
合法性验证限流为最常规的业务代码,就是普通的验证码和 IP 黑名单系统,本文就不做过多的叙述了,我们重点来看下后两种限流的实现方案:容器限流和服务端限流。


2. 服务端限流算法

服务端限流需要配合限流的算法来执行,而算法相当于执行限流的“大脑”,用于指导限制方案的实现。

限流的常见算法有以下三种:

  • 时间窗口算法
  • 漏桶算法
  • 令牌算法

限流(一)---限流的几种方案_谈谈为什么要限流,有哪些限流方案_兢兢业业的子牙的博客-CSDN博客

3. 线程池

(1) 线程池

线程池的工作流程

线程的处理流程如下图所示。

从图中可以看出,当提交一个新任务到线程池时,线程池的处理流程如下。

  1. 默认情况下,创建完线程池后并不会立即创建线程, 而是等到有任务提交时才会创建线程来进行处理。(除非调用prestartCoreThread或prestartAllCoreThreads方法)
  2. 当线程数小于核心线程数时,每提交一个任务就创建一个线程来执行,即使当前有线程处于空闲状态,直到当前线程数达到核心线程数。
  3. 当前线程数达到核心线程数时,如果这个时候还提交任务,这些任务会被放到工作队列里,等到线程处理完了手头的任务后,会来工作队列中取任务处理。
  4. 当前线程数达到核心线程数并且工作队列也满了,如果这个时候还提交任务,则会继续创建线程来处理,直到线程数达到最大线程数。
  5. 当前线程数达到最大线程数并且队列也满了,如果这个时候还提交任务,则会触发饱和策略。
  6. 如果某个线程的空闲时间超过了keepAliveTime,那么将被标记为可回收的,并且当前线程池的当前大小超过了核心线程数时,这个线程将被终止。

线程池详解(ThreadPoolExecutor) - 知乎

(2) 锁

a. 并发竞争概述

竟态条件:多线程在临界区执行,由于代码执行序列不可预知而导致无法预测结果
解决方式:
(1)阻塞式:sync, Lock(ReentrantLock)
(2)非阻塞式:Cas方式(自旋式)

b. synchronized加锁原理

(1)早期实现:sync为JVM内置锁,(JVM层面)基于Monitor(监视器)机制实现,(Linux层面)依赖底层操作系统的Mutex(互斥量)实现,由于进行了内核态及用户态的切换,性能较低
(2)优化后:通过锁升级实现加锁过程:偏向锁,自旋锁,轻量级,重量级
(3)monitor(监视器)原理:
同步方法由Au_SYNCHRONIZED标志实现, 同步代码块由monitorenter与monitorexit实现;
是管理共享变量以及对共享变量操作的过程,让它们支持并发;
synchronized中wait(), notify(), notifyAll()等方法有monitor实现;

volatile和synchronized的区别:
应用范围:
volatile关键字是对变量进行上锁,锁住的是单个变量,而synchronized还能对方法以及代码块进行上锁。

是否保证原子性:
在多线程环境下,volatile可以保证可见性和有序性,不能保证原子性,而synchronized在保证可见性和有序性的基础上,还可以保证原子性。

volatile变量的原子性与synchronized的原子性是不同的。synchronized的原子性是指,只要声明为synchronized的方法或代码块,在执行上就是原子操作,synchronized能保证被锁住的整个代码块的原子性。而volatile是不修饰方法或代码块的,它只用来修饰变量,对于单个volatile变量的读和写操作都具有原子性,但类似于volatile++这种复合操作不具有原子性。所以volatile的原子性是受限制的。所以,在多线程环境中,volatile并不能保证原子性。

使用场景:
volatile主要用于解决共享变量的数据可见性问题,而synchronized主要用于保证访问数据的同步性(同时也能保证可见性)。

保证有序性的方式:
volatile的有序性是通过禁止指令重排序来实现的。synchronized无法禁止指令重排,但是可以通过单线程机制来保证有序性。由于synchronized修饰的代码,在同一时刻只能被一个线程访问,从根本上避免了多线程的情况。而单线程环境下,在本线程内观察到的所有操作都是天然有序的,所以synchronized可以通过单线程的方式来保证程序的有序性。

性能方面:
volatile是线程同步的轻量级实现,性能高于synchronized。多线程访问volatile修饰的变量时不会发生阻塞(主要是因为volatile采用CAS加锁),而访问synchronized修饰的资源时会发生阻塞。
 

c.ReentrantLock

ReentLock 是基于AbstractQueendSynchronized来实现的,所以在了解ReentLock之前先简单的说一下AQS
这里介绍的AbstractQueenSynchronized 同步器(AQS),是基与FIFO队列来实现的,通过state的状态,来实现acquire和release;state为0的表示该锁还没有被任何线程获取可以获取锁;state为1表示已经有线程已经获取了锁。

AQS通过模板方法模式定义了同步组件语义,它的核心包括:同步队列、独占式锁的获取和释放,共享锁的获取和释放,以及可中断锁,超时等待锁获取等实现。AQS使用了一个int成员变量来表示同步状态(state),同时使用Node实现FIFO队列,用于构建锁和其它同步装置;AQS资源共享方式包括独占式锁(Exclusive)和共享锁(Share),独占式锁和共享式锁一般不会一起使用,ReentrantReadWriteLock也是通过两个内部类(读锁和写锁)分别实现了两套API。AQS中获取锁的方式分为独占式(Exclusive)和共享式(Share),独占式指的是只允许一个线程获取同步状态锁,当这个线程没有释放同步状态锁时,其它线程是不能获取锁的,只能加入到同步队列中去。共享式锁允许多个线程同时获取同步状态锁,很多读锁都是共享式锁。
AQS核心设计思想就是共享变量state和FIFO队列,state被关键字volatile修饰,当state>0表明当前对象锁已经被占有,其它线程再次加锁就会失败,会被放入FIFO队列中,线程会被底层UNSAFE.pake()阻塞挂起,等待其它获取锁的线程释放锁后线程才能被唤醒。具体原理如下图所示:

在这里插入图片描述

d.CAS

CAS是Java中Unsafe类里面的方法,它的全称是CompareAndSwap,比较并交换的意思。主要功能是能够保证在多线程环境下,对于共享变量的修改的原子性。

CAS有四个操作数,分别是对象内存位置,对象中的变量的偏移量,变量预期值和新的值。

其机制当且仅当对象中的变量的偏移量的值为变量预期值时,则用新的值更新对象中遍历的偏移量的值。而这个过程是处理器提供的一个原子性命令,不会存在线程安全问题。

然而CAS是一个native方法,还是会先从内存地址中读取state的值,然后去比较,最后再修改,这样还是会存在原子性问题。

所以CAS的底层实现在多核CPU下,会增加一个Lock指令对缓存或者总线加锁,从而保证比较并替换着两个指令的原子性。也就是CAS实际上是利用处理器提供的CMPXCHG指令实现的,而处理器执行CMPXCHG指令是一个原子性操作。

优点:避开synchronized这种大重量级加锁,从底层硬件上实现轻量级的加锁。
但是CAS有三个主要缺点:
1、自旋时间长导致开销大
CAS是非阻塞同步,线程不会被挂起,自旋就会占用CPU资源。

2、ABA问题
因为CAS会检查旧值有没有变化,这里存在这样一个有意思的问题。比如一个旧值A变为了成B,然后再变成A,刚好在做CAS时检查发现旧值并没有变化依然为A,但是实际上的确发生了变化。解决方案可以沿袭数据库中常用的乐观锁方式,添加一个版本号可以解决。原来的变化路径A->B->A就变成了1A->2B->3C。java这么优秀的语言,当然在java 1.5后的atomic包中提供了AtomicStampedReference来解决ABA问题,解决思路就是这样的。

3、只能保证一个共享变量的原子操作
当对一个共享变量执行操作时CAS能保证其原子性,如果对多个共享变量进行操作,CAS就不能保证其原子性。有一个解决方案是利用对象整合多个共享变量,即一个类中的成员变量就是这几个共享变量。然后将这个对象做CAS操作就可以保证其原子性。atomic中提供了AtomicReference来保证引用对象之间的原子性。

Java 之 CAS 原理及实现是怎样的? - 知乎

Java常用锁 - 简书

volatile和synchronized详解_volatile synchronized_渣娃-小晴晴的博客-CSDN博客

JAVA多线程基础篇-AQS(AbstractQueuedSynchronizer)_java aqs_程可爱的博客-CSDN博客

【对线面试官】Redis持久化

4.ThreadLocal

(1)理解ThreadLocalMap数据结构

ThreadLocalMap中存储的键其实就是ThreadLocal的弱引用所关联的对象

 (2)理解ThreadLocal类set方法

线性遍历,首先获取到指定下标的Entry对象,如果不为空,则进入到for循环体内,判断当前的ThreadLocal对象是否是同一个对象

如果是,那么直接进行值替换,并结束方法。如果不是,再判断当前Entry的key是否失效,如果失效,则直接将失效的key和值进行替换。

这两点都不满足的话,那么就调用nextIndex方法进行搜寻下一个合适的位置,进行同样的操作,直到找到某个位置,内部数据为空,也就是Entry为null,那么就直接将键值对设置到这个位置上。最后判断是否达到了扩容的条件,如果达到了,那么就进行扩容。

(3)理解ThreadLocal类get方法

(4)理解ThreadLocal的remove方法

使用ThreadLocal这个工具的时候,一般提倡使用完后及时清理存储在ThreadLocalMap中的值,防止内存泄露

(5)Java内的四大引用

  • 强引用:Java中默认的引用类型,一个对象如果具有强引用那么只要这种引用还存在就不会被回收。比如String str = new String("Hello ThreadLocal");,其中str就是一个强引用,当然,一旦强引用出了其作用域,那么强引用随着方法弹出线程栈,那么它所指向的对象将在合适的时机被JVM垃圾收集器回收。
  • 软引用:如果一个对象具有软引用,在JVM发生内存溢出之前(即内存充足够使用),是不会GC这个对象的;只有到JVM内存不足的时候才会调用垃圾回收期回收掉这个对象。软引用和一个引用队列联合使用,如果软引用所引用的对象被回收之后,该引用就会加入到与之关联的引用队列中。
  • 弱引用:这里讨论ThreadLocalMap中的Entry类的重点,如果一个对象只具有弱引用,那么这个对象就会被垃圾回收器回收掉(被弱引用所引用的对象只能生存到下一次GC之前,当发生GC时候,无论当前内存是否足够,弱引用所引用的对象都会被回收掉)。弱引用也是和一个引用队列联合使用,如果弱引用的对象被垃圾回收期回收掉,JVM会将这个引用加入到与之关联的引用队列中。若引用的对象可以通过弱引用的get方法得到,当引用的对象被回收掉之后,再调用get方法就会返回null。
  • 虚引用:虚引用是所有引用中最弱的一种引用,其存在就是为了将关联虚引用的对象在被GC掉之后收到一个通知。

(6)内存泄漏

当一个线程调用ThreadLocal的set方法设置变量的时候,当前线程的ThreadLocalMap就会存放一个记录,这个记录的键为ThreadLocal的弱引用,value就是通过set设置的值,这个value值被强引用。

如果当前线程一直存在且没有调用该ThreadLocal的remove方法,如果这个时候别的地方还有对ThreadLocal的引用,那么当前线程中的ThreadLocalMap中会存在对ThreadLocal变量的引用和value对象的引用,是不会释放的,就会造成内存泄漏。

考虑这个ThreadLocal变量没有其他强依赖,如果当前线程还存在,由于线程的ThreadLocalMap里面的key是弱引用,所以当前线程的ThreadLocalMap里面的ThreadLocal变量的弱引用在垃圾回收的时候就被回收,但是对应的value还是存在的这就可能造成内存泄漏(因为这个时候ThreadLocalMap会存在key为null但是value不为null的entry项)。

总结:ThreadLocalMap中的Entry的key使用的是ThreadLocal对象的弱引用,在没有其他地方对ThreadLocal依赖,ThreadLocalMap中的ThreadLocal对象就会被回收掉,但是对应的值不会被回收,这个时候Map中就可能存在key为null但是值不为null的项,所以在使用ThreadLocal的时候要养成及时remove的习惯。

ThreadLocal为了解决此问题,会每次在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法
 

(7)子线程共享父线程threadlocal变量

我们使用ThreadLocal的时候,在异步场景下是无法给子线程共享父线程中创建的线程副本数据的。内部原理就是将父线程中的数据拷贝到子线程中去。

使用InheritableThreadLocal。这是一种特殊的ThreadLocal,它可以让子线程继承父线程的ThreadLocal变量。当我们在父线程中设置InheritableThreadLocal变量时,子线程会自动继承该变量,并可以在子线程中访问到它。 

Thread类里面分开记录了ThreadLocal、InheritableThreadLocal的ThreadLocalMap,初始化的时候,会拿到parent.InheritableThreadLocal.

public class InheritableThreadLocalDemo {
    public static void main(String[] args) {
        ThreadLocal<String> ThreadLocal = new ThreadLocal<>();
        ThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
        ThreadLocal.set("父类数据:threadLocal");
        inheritableThreadLocal.set("父类数据:inheritableThreadLocal");

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("子线程获取父类ThreadLocal数据:" + ThreadLocal.get());
                System.out.println("子线程获取父类inheritableThreadLocal数据:" + inheritableThreadLocal.get());
            }
        }).start();
    }
}

ThreadLocal原理讲解 - 知乎

面试官:为什么 ThreadLocalMap 的 key ThreadLocal 是弱引用?_threadlocalmap的key_蜀州凯哥的博客-CSDN博客

为什么ThreadLocalMap中把ThreadLocal对象存储为Key时使用的是弱引用_threadloaclmap中的entry为什么使用threadlocal作为key_JinF~的博客-CSDN博客

Java多并发(三)| 线程间的通信(ThreadLoacl详解)_探测式清理和启发式清理_光看不点赞的博客-CSDN博客

5.ForkJoinPool

(1)分治法的基本思想是将一个规模为N的问题分解为K个规模较小的子问题,这些子问题的相互独立且与原问题的性质相同,求出子问题的解之后,将这些解合并,就可以得到原有问题的解。是一种分目标完成的程序算法。

一个大任务拆分成多个小任务,为了减少线程间的竞争,把这些子任务分别放到不同的队列中,并且每个队列都有单独的线程来执行队列里的任务,线程和队列一一对应。

但是会出现这样一种情况:A线程处理完了自己队列的任务,B线程的队列里还有很多任务要处理。

A是一个很热情的线程,想过去帮忙,但是如果两个线程访问同一个队列,会产生竞争,所以A想了一个办法,从双端队列的尾部拿任务执行。而B线程永远是从双端队列的头部拿任务执行。

注意:线程池中的每个线程都有自己的工作队列(PS,这一点和ThreadPoolExecutor不同,ThreadPoolExecutor是所有线程公用一个工作队列,所有线程都从这个工作队列中取任务),当自己队列中的任务都完成以后,会从其它线程的工作队列中偷一个任务执行,这样可以充分利用资源。(特别注意)

https://www.cnblogs.com/maoyx/p/13991828.html

其他

Leaf——美团点评分布式ID生成系统 - 美团技术团队

Dubbo原理机制

Leaf——美团点评分布式ID生成系统 - 美团技术团队

Netty架构原理

IO五种模型

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值