Java虚拟机
- 通过编译器把Java文件编译成.class文件,应该字节码验证
- 把字节码文件(即.class文件)通过类加载(ClassLoader)到java虚拟机执行
- java虚拟机和计算机操作系统进行数据传输和处理
类加载与New:
Class文件内容:
类加载
- 指JVM把.class文件中类信息加载进内存,并进行解析生成对应的class对象的过程;
- JVM不是一开始就把所有的类都加载进内存中,而是只有第一次遇到某个需要运行的类时才会加载,且只加载一次;
- 加载--链接---初始化;
加载:用类加载器装把class文件装载入内存中 - 其中链接包括:验证(元数据/字节码)--准备(分配内存/分初始值)-- 解析 zhuanlan.zhihu.com/p/33509426
双亲委托模型:
避免重新加载,避免核心被串改一级一级往上查找,一级一级往下分派,防止冲突重复加载,类加载器(过实现自己的自定义类加载器进行解密,最后再加载,安全性)
创建对象
- 在堆区分配对象需要的内存;2 .对实例变量赋默认值;3. 执行实例初始化代码
JVM内存分区:
- 程序计数器:用于记录下一条需要执行的JVM指令
- 虚拟机栈: 是线程私有的内存空间,它和java线程同一时间创建,保存了局部变量(byte:8位,short:16位,int:32位,long:64位,float:32位,double:64位,char:16位,Boolean),部分结果,Stack Frame,并参与方法的调用和返回
- 本地方法栈:和java虚拟机栈的功能相似,而本地方法栈用于管理本地方法的调用,由C实现
- 方法区:元空间,放了类加载的信息,静态变量等,还有常量池
- java堆: Java中所有通过new创建的对象实例在这分配, Heap中的对象的内存需要等待GC进行回收
- 常量池:存放的类中的固定的常量信息、方法和引用信息等。JVM在加载类时会为每个class分配一个独立的常量池,但是运行时常量池中的字符串常量池是全局共享的
垃圾回收(分代模型):
-
新生代(复制算法,MinorGC):虚拟拟机每次用新Eden和其中一块Survivor(From),在经过一次Minor GC后,将Eden和Survivor中还存活的对象一次性地复制到另一块Survivor,最后清理掉Eden和刚才用过的Survivor(From)空间,MinorGC每次收集后Eden区和一块Survivor区都被清空
-
老年代(Full GC,标记-清除算法):
复制:一分为二,容易产生碎片 :
标记-清除:标记阶段标记出不可达的对象的对象,清除阶段就是回收被标记的对象所占用的空间,内存碎片:
标记-整理,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存
可达性分析
- 引用计数,会造成循环依赖
- Java的GCroot,会开一个链,如果不在一个链上表示可以回收了
判定回收?
- 从root搜索不到,而且经过第一次标记、清理后,仍然没有复活的对象
- 可以作为GC root的:虚拟机栈中引用的对象,方法区中类静态属性引用的对象,方法区中常量引用的对象,本地方法栈中引用的对象
www.oracle.com/webfolder/t…
GC垃圾回收算法
- 分代收集:新生代使用速度快的垃圾收集器,MinorGC频繁;老年代使用空间效率高的垃圾收集器。
- FullGC:全局清理,整个新生代,老年代,元空间
- MinorGC:年轻代的回收
GC收集器
-
Serial GC(单线程,复制-新生代,标记清除-老年):
新生代 GC:eden区空间分配满的情况下触发,存活对象晋升为old gen
FullGC:准备YoungGC时发现新晋的对象比old gen空间大,进行full GC;有元空间的话,要在元空间配但是没有足够空间时,也要触发一次full GC; 或者System.gc()、heap空间分配很大的对象,heap dump带GC,也是触发full GC -
Parallel GC(多线程标记整理): Serial 收集器的多线程本,MinorGC和fullGC是并行的,其余的行为和 Serial 收集器完全一样,
在具有N个CPU的主机上,并行垃圾收集器在收集中使用N个垃圾收集器线程。垃圾收集器线程的数量可以通过命令行选项控制: -XX:ParallelGCThreads= -
CMS:用户线程和垃圾收集器同时执行,目的是降低停顿时间(fullGC,UI系统);
针对老年代,标记清除为主,混合类标记整理,会容忍一些垃圾碎片,达到某个值时会触发标记整理
阶段:初始标记,并发标记,重新标记,并发清除
在初始标记和重新标记的时候不是线程并行,会产生STW,并发标记/整理耗时长,但是和用户线程一起并发的;
CMS主要是针对并发模式失效以至于晋升失败的调优,因为有时候老年代碎片化导致空间被错估,导致CMS不能足够快清理老年代
可以调整老年代并发启动的时机,把70%-60%.同时设置CMSInitiatingOccupancyFraction + UseCMSiniatingOccupancyOnly
调整CMS后台线程总数量ConGCThreads,有个计算公式
调整元空间,让其收集不再载入的类,CMSClassUnloadingEnabled
堆空间小于4G时,CCMS比G1要好,对于大型堆,G1可以分割工作,表现更好
- G1 GC(默认收集器):分区收集器, 分区可以归属于老年代也可以属于新生代,默认分为2048个区,G1专注于垃圾最多的区
主要包括:minorGC,后台收集。混合垃圾收集,fullGC
调优的主要目标也是避免并发模式失败,避免fullGC和减少停顿
增加总的堆空间,调整老年代/新生代的比例
增加后台线程的数目(ConGCThreads)
增加G1收集的频率(InitaingHeapOccupancePercent,跟CMS不一样,这个针对整个堆。而不是老年代)
在MixedGC内完成更多的回收操作(减少G1MixedGcTarget的参数)
可以控制停顿时间,实现低停顿垃圾回收。基于标记-整理算法,不产生内存碎片。整体上看属于标记整理算法, 局部(region之间)属于复制算
。堆比较大的时候采用复制算法,碎片化问题不严重。
晋升&Survivor空间调优
为什么垃圾回收
释放垃圾占用的空间,防止内存泄露。有效的使用可以使用的内存,对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。
GC调优步骤
- 堆大小的调整,XMs,初始堆,XMx,最大堆
- 代空间调整,-XX:NewRatio = N新生代与老年代比率(动态调整大小比值),NewNewSize新生代初始空间大小,
- 元空间(永久代)大小调整:metaSpaceSize=N
- 垃圾回收器线程并发数量调整,ParallelGCThread=N
- JVM自适应调整,UseAdaptiveSizePolicy
Error和Exception
Error和RuntimeException属于UncheckedEception,其他属于CheckedException
- Error 一般是跟虚拟机相关或者硬件相关的问题,系统崩溃,虚拟机错误,空间不足等, VisualMachineError, NoClassFoundError,Error错误Java编译器检查不到,对于这种问题导致的程序中断,靠程序本身无法恢复,只能终止程序;
- Exception是程序运行中可以捕获的,可以处理,恢复 RuntimeException:编译时不会检查,NullPointerException,IndexOutofBoundsException
- IOException(检查异常):编译时可以检测到,如果抛出受查异常,都会包含 try-catch,try-catch-finally, throws 关键字
NoClassDefFoundError
Error 类型的异常,是由 JVM 引起的。原因是 JVM 或 ClassLoader 尝试加载某类时在内存中找不到该类的定义,该动作发生在运行期间,即编译时该类存在,但是在运行时却找不到了,可能是变异后被删除了等原因导致;
JVM 是如何处理异常的
在一个方法中如果发生异常,这个方法会创建一个一场对象,交给JVM,该异常对象包含异常名称,异常描述以及异常发生时应用程序的状态。创建异常对象并转交给 JVM 的过程称为抛出异常。
throw与throws
- throw关键字用来抛出方法或代码块中的异常,受查异常和非受查异常都可以被抛出。
- throws 关键字用在方法签名处,用来标识该方法可能抛出的异常列表
接口与抽象
接口
- 一种特殊的类,里面是由全局常量和抽象方法所组成。接口里有方法的特征但是没有方法的实现,用Implement
- 接口是解决Java无法使用多继承的一种手段
- 例如:Spring里的service, serviceimpl是把mapper和service进行整合的文件
- Dubbo的注册中心里的api包包括NotifyListen(监听器)/RegistryService(是注册中心模块的接口,有注册/取消注册/订阅等功能)/RegeisterFactor(注册中心的工厂接口),这些被剥离放在api包下了
接口场景: 1。 类与类之间需要特定的接口进行协调,2。️需要实现特定的多项功能,而这些功能之间可能完全没有任何联系
抽象类:
- 抽象类是指不允许被实例化的类;一个类只能使用一次继承关系Extend
- 类中如果有一个或多个abstract方法,则该类必须声明为abstract;
- 一个抽象类如果没有抽象方法,定义为抽象类有什么意义?
这么做目的只有一个,就是不让其他类创建本类对象,交给子类完成; - abstract不能和哪些关键字共存abstract和static
被abstract修饰的方法没有方法体
被static修饰的可以用类名.调用,但是类名.调用抽象方法是没有意义的 - abstract和final 被abstract修饰的方法强制子类重写
- abstract和private
被abstract修饰的是为了让子类看到并强制重写
被private修饰不让子类访问,所以他俩是矛盾的
意义:定义了一组接口,但是不能使每个实现类都实现所有的借口,这时候使用抽象类定义一组方法体,由子类选择需要的方法来覆盖
string和string builder:
- String(底层使用了final修饰,数组长度不可变) String 对象一旦创建,其值是不能修改的,如果要修改,会重新开辟内存空间来存储修改之后的对象,即修改了 String 的引用。
- StringBuffer:底层也是数组储存字符串的值,创建了字符串缓冲区,线程安全,所有方法都是由synchronized修饰的,
每次取toString都会使用缓存区的toStringCache来构造字符串,str长度+16"的字符串缓冲区,然后把 str的值追加到此字符串序列中。
所以有16个字符的空间,修改范围超过16就需要扩容 - StringBuilder:它们拥有同一个父类 AbstractStringBuilder,同时实现的接口也是完全一样。
StringBuffer 对方法都实现了同步(synchronized),StringBuilder 没有实现同步 zhuanlan.zhihu.com/p/84047711
HashMap/ConcurretHashMap/HashTable
HashMap
-
数据结构: 储存的是key-vale,数组+链表,1.8:数组+链表+红黑树 当数据被Hash后,得到数组下标,把数据放在对应下标元素的链表上。
-
线程不安全: put的时候,会出现扩容,这个会重新将原数组的内容重新hash到新的扩容数组中rehash,多线程下,存在同时其他的元素也在进行put操作,如果hash值相同,可能造成链表循环(1.7头插法),下一次get会出现死循环,所以线程不安全。
-
1.8后Entry变成Node结点,链表大于8会变成红黑树,put也优化了,变成尾插法。仍然不安全------通过源码看到put/get方法都没有加同步锁,多线程:无法保证上一秒put的值,下一秒get的时候还是原值,所以线程安全还是无法保证
-
哈希碰撞: 元素位置分布均匀,使得每个位置上的元素只有一个;步骤:取hashCode值 h = key.hashCode() //高位参与运算 h ^ (h >>> 16) //取模运算h & (length-1)
-
Put:
判断键值对数组table[i]是否为空,否则进行扩容;
如果table为空,说明没有产生hash碰撞,则直接插入node节点,否则进行下一步判断;
这几种情况:
1.table[i]的第一个元素是否和key一样,如果相同直接覆盖value;
2.hash值不相等,说明key不相等,为红黑树,放入树中,赋值给node节点e;
3.hash值不相等,说明key不相等,为链表,遍历链表,赋值给最后一个节点,并返回node节点(这里面还有长度的判断,当链表长度大于8之后需要将链表转化为红黑树);
判断新插入这个值是否导致size已经超过了threhold,是则进行扩容 -
扩容: 元素超过数组*loadFactor时,就会进行数组扩容; 两个因素:Capacity:HashMap当前长度;负载因子:默认0.75
扩容的两步:1. 创建一个新的Entry空数组,长度是原数组的2倍。
2.ReHash:遍历原Entry数组,把所有的Entry重新Hash到新数组。
扩容后,hash规则发生变化, index = HashCode(Key) & (Length - 1), 如果采取头插法,可能会形成死循环。1.8后有尾插法,并且红黑树会进行判断
HashTable
- 实现方式不同:Hashtable 继承了 Dictionary类(废弃),而 HashMap 继承的是 AbstractMap 类。
- 初始化容量:HashMap 的初始容量为:16,Hashtable,初始容量为:11
- 扩容机制不同:HashMap 扩容规则为当前容量翻倍,Hashtable 扩容规则为当前容量翻倍 + 1。
- 迭代器不同:HashMap 中的 Iterator 迭代器是有安全机制的(可以Null),而 Hashtable 的 Enumerator 不是 fail-fast 的。
ConcurrentHashMap
1.7
采用的是分段锁技术,Segment继承了ReentrantLock,
是由 Segment 数组、HashEntry 组成,和 HashMap 一样,仍然是数组加链表;
每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment,数量大小是16他的并发度就是16,可以同时允许16个线程操作16个Segment
HashEntry跟HashMap差不多的,但是不同点是,他使用volatile去修饰了他的数据Value还有下一个节点next。
1.8
CAS +Synchronized设计,不再使用segement
HashMap很像,也把之前的HashEntry改成了Node,但是作用不变,把值和next采用了volatile去修饰,保证了可见性,并且也引入了红黑树
Put操作会使用CAS编程方式,也会使用synchronized
- 根据 key 计算出 hashcode ------判断是否需要进行初始化。
- 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
- 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。
- 如果都不满足,则利用 synchronized 锁写入数据。
- 如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。
重复key问题?
如果重复添加的话,hashmap会自动覆盖key一样的数据,保证一个key对应一个value 可以继承一下HashMap,改写它的put方法应该可以实现
ArrayList
ArrayList/Vector/LinkedList:
- ArrayList,数组,初始化0,add默认是10,扩容1.5倍,允对元素随机访问,插入删除的时候,要对数组进行复制/移动,适合随机查找和遍历(通过角标去查),不适合插入删除
- Vector,都加了synchronized,复合操作,只是同步方法并没有解决线程安全的问题。要真正达成线程安全,还需要以vector对象为锁,来进行操作,扩容两倍
- LinkedList,头尾结点的双向链表,提供头插法(LinkedFirst)/尾插法(LinkedLast),适合经常插入和删除,定义了List接口中没有的方法,用于操作表头和表尾,查询会慢(链表,从第一个开始一个一个比)
- HashSet,哈希表放的哈希值,元素存放是按着哈希值来存/取,元素哈希值通过hashcode方法获取,HashSet会判断两个元素哈希值,如果一样就使用equal比较,如何还一样就说明是一个元素;
- TreeSet,使用二叉树进行add,增加时会排序
线程
线程常见创建方式:
- 创建一个继承Thread类的子类----重写Thread的run,将此线程的操作逻辑代码在run中 --------创建Thread类的子类的对象------通过此对象调用start()
- Runnable 创建一个类实现Runnable接口---------------重写run()方法------------- ------------创建实现Runnable接口的类的对象,把这个对象当做Thread类的构造方法中的参数传进去------------用Thread类的构造方法创建一个对象,并调用start()
- 线程池 用Executors类的NewFixedThreadPool方法创建一个(数量设置)的线程池 -------------------调用线程池中的execute()方法执行由实现Runnable接口创建的线程 --------------------shutdown()方法关闭线程池
线程为什么用start
必须使用start(),不是run,因为start同时包括启动当前线程+run() 如果再启动一个线程,必须重新创建一个Thread子类的对象,调用次对象的start()
因为为类只能继承一个类,实现多个接口,如果你继承了Tread来实现线程,将无法继承其它的类,用实现Runable接口能解决这个问题。具体怎么用看情况。
线程状态
- 新建状态(New): 当用new操作符创建一个线程时, 例如new Thread(r),...
- 就绪状态(Runnable) 一个新创建的线程并不自动开始运行,要执行线程,必须调用线程...
- 运行状态(Running) 当线程获得CPU时间后,它才进入运行状态,真正开始执行run...
- 阻塞状态(Blocked) 线程运行过程中,可能由于各种原因进入阻塞状态: 1>...
- 死亡状态(Dead) 有两个原因会导致线程死亡: 1) run方法正常退出而自然死
ThreadLocal和synchronized区别:
没在项目用过,但是之前看到这个知识点写过demo,ThreadLocal是构建一个存放线程私有变量的东西,解决了变量的线程安全。ThreadLocal和同步都能解决线程安全,同步是时间换空间,ThreadLocal是空间换时间。
追问:那底层是怎么实现的呢?
ThreadLocal底层是一个Map,它本身作为key,变量作为value。
相同:都是为了解决多线程环境下相同变量的访问冲突问题
不同:synchronized值提供了一份变量,让不同的线程去排队,
reentrantlock与Sychronized区别
- Syc是JVM底层的对象,reentantlock是实际上就是个类,需要手动编码
- Syc直接同步代码块/同步当法,不需要手动释放锁, lock需要手动释放(try···finally),否则死锁
- Syc不可中断,异常或正常运行完成,可重入,非公平;
- lock可以调用interrupt中断,可重入,客公平,用condition,指定唤醒绑定到condition上的机制,实现选择性唤醒
- 锁绑定多个条件,syc不可以,随即唤醒, lock可以使用condition唤醒
- lock适合大量同步的代码问题,syn适合代码量少的同步问题
Synchronized底层原理
-
对于同步代码块,可以指定任意的锁,对象作为锁)
在方法上时,锁住的便是对象实例(this)
对静态方法,锁是对象对应的Class实例,静态方法锁相当于该类的一个全局锁; -
在Java里,对于每个对象,有对象锁;这个锁叫监视器monitor;通过看字节码可以知道.class:,
代码块同步 monitorenter/monitorexit在编译后插入到同步代码块开始处/结束处和异常处,
一个线程来时,发现对象头中锁标志位是无锁,尝试加一个互斥锁对象,跟另一个对象关联,就是监视器monitor,monitor会+1,将monitor的指针写入到一个对象头中表示,然后修改锁标志位为10,就是重量级锁
这个过程是可重入的,不会因为每次出去进来,加锁/释放锁,只需要锁记录+1,加锁完,其他线程来时会检查锁对象头中monitor不为0,就会等待竞争这个锁 -
方法同步,常量池有ACC_SYNCHRONIZED标示符,相当于一个flag
当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED,
访问标志是否被设置,如果设置了,执行线程将先获取monitor,,
monitor操作本质上是通过内存屏障来实现的,其实就是加lock指令。
Java对象
对象头 ->
- MarkWord标记字(表示对象的线程锁状态,还可以用来配合GC;用lock标志位来表示锁的状态)
- 类指针(Klass Word是一个指向方法区中Class信息的指针,意味着该对象可随时知道自己是哪个Class的实例)
- 数组长度
Mark Word
虚拟机栈有开辟一个空间,LockRecorder,将锁对象的MarkWord写入,再尝试将LockRecorder指针去用CAS去修改所对象头的那个区域,完成加锁过程 正常(01) - - -- 偏向锁(01) - - - - - 轻量级锁(00) - - - -重量级锁 /锁膨胀(10) - - - -- GC标记(11) - - - -- - - - -- - - - -- - - - -- - - - -- - - - -- - - - -- - - - --
- 初期锁对象刚创建时,这偏向锁标识位是0,无锁,锁状态01。
- 当有一个线程来竞争锁时,先用偏向锁,偏向于这个线程优先再获得这个锁,如果没有获得这个锁,升级轻量级
- 锁会升级为轻量级锁,CAS的乐观锁,有一个比较交换的过程,CAS如果没有设置成功的话,进行自旋操作,自旋到一定次数升级为SYnchronized
- 锁(互斥锁),锁对象MarkWord再次发生变化,会指向一个监视器对象,这个监视器对象用集合的形式,来登记和管理排队的线程。 blog.csdn.net/scdn_cp/art…
监视器对象
www.open-open.com/lib/view/op…
线程通/信线程同步:
- ️使用Volatile关键字,使多个线程监听一个变量
- ️两个线程先后运行,使用join()
- ️两个线程交替运行,wait()和notify():wait()和notify()
- 创建共享锁 lock=new Object(),A线程得到锁完成操作,实行wait B线程得到锁,完成操作,实行notify,通知A线程继续工作
- ️四个线程运行,等ABC全部执行完再运行D,使用CountDownLatch
- ️使用ReentrantLock结合 condition
wait()和sleep()
Wait使线程进入阻塞状态,通过notify或notifyALL唤醒线程,notify放在同步代码块syn下面第一行
- 这两个方法来自不同的类分别是Thread和Object
- 最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
- wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用
- sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常
Volatile:
可议保证线程的可见性(主要修饰变量):因为计算机有多层缓存,线程在缓存中计算
- 当写线程写一个volatile变量时,修改了某个变量的值,volatile关键字让这个值对其他线程来说是立即可见的。(实现可见性),因为JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。每次读取都是最新数据
- 不断刷新到主内存 - 总线嗅探机制 - 总线风暴--宽带达到峰值
- 有序性(顺序执行,禁止编译器和处理器的重排),使用的是MemoryBarrier,一种cpu指令,通过在写volatile的机器指令前加上lock前缀
- 无原子性:i++其实要分成3步:1)读取volatile变量值到local; 2)增加变量的值;3)把local的值写回,让其它的线程可见
- 和CAS结合,保证了原子性,例如AtomicInteger
CAS
(内存地址V,预期值A,修改的新值B): 且仅当预期值A和内存值V相同时,才会将内存值修改为B并返回true,否则什么都不做并返回false
- 乐观锁的实现基础
- 内部实现主要包括, Unsafe,硬件层面上的源自操作,其中compareAndSwapInt包含了内存地址V,预期值A,修改的新值B
- ValueOffset(AtomicInteger对象value在内存中的偏移地址),value变量的内存地址
- 造成ABA问题,在修改变量的时候,无法记录变量的状态,比如修改的次数,否修改过这个变量。这样就很容易在一个线程将A修改成B时,另一个线程又会把B修改成A, 造成多次执行的问题
- 解决办法,增加版本号,增加标志位
- AtomicInteger:解决多线程下I++不安全问题,底层使用incrementAndGet,是个无限循环,CAS的自旋
线程池
阻塞队列:
ArrayBlockingQueue(有界队列,OutOfMemory);LinkedBlockingQueue(无界队列,适合高并发,消费者/生产者采用类独立锁来控制同步);SynchronousBlockingQueue(不保任务,不存取数据,可用于传递顺序,每一个put操作等待一个take);DelayWorkedQueue
线程池参数:
- threadFactor,一定要指定名称
- coolPoolsize,(核心线程,维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁),
- maximumPoolsize,线程池最大数,unit,线程存活时间,workqueue,工作队列 Java通过Executor(工具类,线程池工厂类)创建线程池:
线程池拒绝策略:
AbortPolicy,CallerRunPolicy(交给线程池调用所在的线程进行处理) DiscardPolicy(直接丢弃任务),DiscardOldPolicy(丢弃队列最老线程,重新提交给线程池)
常见线程池
- newFixedThreadExecutor(密集型CPU应用)
队列使用LinkedBlockingQueue,一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待,
提交任务-核心线程数-最大线程数-阻塞队列-执行完任务去阻塞队列取线程
无边界队列可能导致内存飙升,LinkedBlockingQueue,获得任务后等待时间比较长,就会积累越来越多 - ️newCacheThreadExecutor(可缓存线程池,适用于并发执行量大的短期小任务)
可缓存线程池,如果线程池长度超过处理需要,可回收空闲线程,若无可回收,则新建线程
队列使用SynchronousBlockingQUeue,核心线程数为零,最大线程数为Max_Value,非核心线程存活时间60s
线程池执行步骤:
- 线程池判断核心线程池里的线程是否都在执行任务。如果不是,创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则执行二
- 线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这个工作队列里进行等待。如果工作队列满了,则三
- 线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务
CountdownLatch
- CountdownLatch,适合单线-并发执行-但线程,适合一个线程等待一批线程到达一个同步点,计数器不能重用
- CyclicBarrier,没有一个线程一个线程等待,是一批线程同时到达 调用reset方法重置计数器,可以在线程故障重新启用线程调用
AQS抽象队列同步器:
- 一种实现阻塞锁和FIFO等待队列的同步器的框架, AQS的基本原理就是当一个线程请求共享资源的时候会判断是能不能操作这个资源,如果可以就会把这个共享资源设置为锁定状态,如果当前共享资源已经被锁定了,那就把这个请求的线程阻塞住,也就是放到队列中等待
- ReentrantLock、CountDownLatch等并发类均是基于AQS来实现的
AQS内部有3个对象,一个是state(用于计数器,类似gc的回收计数器,volatile实现的),一个是线程标记(当前线程是谁加锁的),一个是阻塞队列。)
AQS实际上以双向队列的形式连接所有的Entry,比方说ReentrantLock,所有等待的线程都被放在一个Entry中并连成双向队列,)
前面一个线程使用ReentrantLock好了,则双向队列实际上的第一个Entry开始运行。
AQS定义了对双向队列所有的操作,而只开放了tryLock和tryRelease方法给开发者使用)
开发者可以根据自己的实现重写tryLock和tryRelease方法,以实现自己的并发功能。
CPU使用过高:
- 代码中有死循环或者接近死循环的操作
- 快速创建大量临时变量,新生代设置过小,也会频繁触发gc,导致频繁触发gc回收、
死锁
就是多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放
Java常用排查工具:
- 高CPU/GC问题:用top命令查找CPU占用率高的进程PID,用Jstack命令登上进程去查看是哪一块出现问题
- OOM/死锁: Jivisualvm工具,看看JVM的CPU,也可以把它的堆栈给dump下来,然后进行信息排查
- jstat查看垃圾回收情况,堆内存情况,编译统计, -class 监视类加载,卸载数量以及耗时情况
final关键字:
- 对于final修饰的对象,如果是基本类型,final会使数值恒定不变;
- 对于对象引用,final使引用恒定不变,一旦引用被初始化一个对象,就无法将它指定为另一对象;但是对象自身的值是可以改变的,因此可以执行append操作;
计算机网络
七层网络模型
- 物理层:主要定义物理设备标准,如网线的接口类型、光纤的接口类型、各种传输介质的传输速率 等。这一层的数据叫做比特。
- 数据链路层:主要将从物理层接收的数据进行 MAC 地址(网卡的地址)的封装与解封装。常把这 一层的数据叫做帧。在这一层工作的设备是交换机
- 网络层:主要将从下层接收到的数据进行 IP 地址(例 192.168.0.1)的封装与解封装。在这一层工 作的设备是路由器,常把这一层的数据叫做数据包。
- 传输层:定义了一些传输数据的协议和端口号(WWW 端口 80 等),如:TCP,udp
- 会话层:通过传输层(端口号:传输端口与接收端口)建立数据传输的通路。主要在你的系统之间 发起会话或或者接受会话请求
- 表示层:主要是进行对接收的数据进行解释、加密与解密、压缩与解压缩等(也就是把计算机能够 识别的东西转换成人能够能识别的东西(如图片、声音等))
- 应用层 主要是一些终端的应用
访问网站流程:
- 浏览器根据域名解析IP地址(DNS),并查DNS缓存
- 浏览器与WEB服务器建立一个TCP连接
- 浏览器给WEB服务器发送一个HTTP请求(GET/POST):一个HTTP请求报文由请求行(request line)、请求头部(headers)、空行(blank line)和请求数据(request body)4个部分组成。
- 服务端响应HTTP响应报文,报文由状态行(status line)、相应头部(headers)、空行(blank line)和响应数据(response body)4个部分组成。
- 浏览器解析渲染
http状态码:
1开头通知,2开头正常,3重定向,4客户端错误,5服务器错误
http的请求(post,get,put,head(报头),delelte,trace(回显,测试)) www.cnblogs.com/jayxuan/p/1…
HTTPS和HTTP的区别主要如下:
- http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议,依靠证书来验证服务器的身份,并为浏览器和服务器之间的通信加密
- https = http+sll
- 用的端口也不一样,前者是80,后者是443。
- http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。
长连接
- 因为http协议是无状态的,浏览器和服务器的请求响应一次,下一次会重新创建连接,对于交互性比较高的网页就表现不好
- 用websocket进行全双工的交互,浏览器和服务器有互相感应
get和post的区别:
- 都是http的请求方式,Get一般用于获取和查询资源信息,Post用于更新资源
- Get的数据会在地址栏显示,数据会跟在URL之后,传输数据有限,
- Post将数据放在http的包里,不会因为地址限制而数据受到限制,安全性高
Session与Cookie:
- session服务器端用户权限验证,存于服务端,在进行数据交互时使用的,登录失效这,就是因为session在中间起到了作用。
- cookie是保存在本地的数据,在页面中输入账号,自动弹出密码,这个密码之所以会弹出就是因为本地cookie的原因, 包括历史记录这些,之所以会有记录,就是因为内容存储在本地的cookie文件中。
- session 的运行依赖 session id,而 session id 是存在 cookie 中的,也就是说,如果浏览器禁用了 cookie ,同时 session 也会失效
TCP流量控制:
滑动窗口,流量控制,本质上是动态缓冲区,接收端根据自己的能力在tcp的header中动态调整窗口大小 TCP解决网络拥塞,
- 重传的机制:慢启动,拥塞避免,快速恢复
TCP状态码
segmentfault.com/a/119000001…
(三次握手四次挥手,SYN,WAIT等等)
- 为什么三次握手:--------服务器陷入假性等待连接。问:那超时关闭不就好了? 答:超时关闭可以尽快是否但无法避免前期假性等待的时间开销
- tcp 首部核心标记字段有哪些 (源端口 目的端口 序号 确认号 数据长度 相关标志号 一些额外指针空间)
TCP UDP区别:
- TCP是传输控制协议,面向连接的,需要三次握手(SYN - ACK - ACK),比较稳定,信息包标题20个字节,保证数据顺序
- UDP是用户数据包协议,非连接协议,不建立连接,不维护连接状态,没有拥堵控制,吞吐量不受约束,信息包标题8个字节,不保证数据顺序, Ping命令就是发送udp包,
- (TCP慢?)TCP流量控制、拥塞控制,UDP没有相关限制,不会缩小对方发送缓冲区,因此速度快。加快TCP的传输 可以扩大缓冲区大小,或者减小流量控制的力度,让数据每次都刚好能完整发送一个包单位
操作系统
线程与进程:
- 进程是资源(CPU、内存等)分配的基本单位,是程序执行时的一个实例。程序运行时系统就会创建一个进程,并为它分配资源,系统会为它分配CPU时间,程序开始真正运行,整个 Java 就是一个进程,比如你在里面用做调度任务就会开启线程
- 线程是程序执行时的最小单位,它是进程的一个执行流,是CPU调度和分派的基本单位,一个进程可以由很多个线程组成,线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量
并发/并行:
- 并发和并行都可以是多线程,并行就是多线程被多个cpu执行,并发是多个线程被一个CPU轮转执行
- 并发就是指代码逻辑上可以并行,有并行的潜力,但是不一定当前是真的以物理并行的方式运行;
- 并发指的是代码的性质,并行指的是物理运行状态
父线/子线程:
- 线程分为守护线程和非守护线程,通过isDaemon()可以查看。
- Java进程在所有的非守护线程结束后,会全部结束,所有守护线程都会停止。如果只是某个父线程结束,它产生的子线程不会受到影响。
- 一个线程是否是守护线程默认和他的父线程及ThreadGroup有关,但可以在start以前通过setDaemon()自行设置。
BFS/DFS
-
深度优先遍历DFS:
使用栈(stack,pop,push),因为栈是垂直结构;
从某个顶点出发,首先访问这个顶点,然后访问该顶点的第一个未被访问的邻结点,以此邻结点为顶点继续访问,同时记录其余未访问的邻接点,当一个顶点的所有邻接点都被访问时,回退一个顶点 -
栈:按照先进后出的原则存储数据,先进入的数据被压来入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据(最源后一个数据被第一个读出来),跟我们浏览玩也的逻辑是一样的。(递归函数的调用和返回)
-
广度优先遍历BFS: 使用队列(PriorityQueue、LinkedList)
从某个顶点出发,首先访问这个顶点,然后找出这个结点的所有未被访问的邻接点,访问完后再访问这些结点中第一个邻接点的所有结点,重复此方法。 -
队列: 队列为一种线性表,它是水平结构的,它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,先进先出(FIFO—first in first out)线性表 (列的最主要用途是异步任务,异步的思路主要用来缓解瞬间压力、耗时操作、并行任务等;)
-
堆:
堆中某个节点的值总是不大于或不小于其父节点的值; 完全二叉树;最大堆;最小堆
堆排序:当一个结点插入时,放在堆的末尾)------然后把这个结点与父节点比较,看是否大于或小于父节点(取决于大根/小根堆),判断堆序----------不满足的话,与父节点交换,再与父节点的父节点比较,直到满足堆序----------当跟节点被弹出时,尾结点到头节点位置,与子节点比较,放入合适的位置
二叉树
-
前序遍历:访问根结点 - 左子树 - 右子树
首先把root入栈
出栈的元素同时放进结果队列
先把右儿子节点入栈,再把左儿子节点入栈,这样出栈的顺序是先左后右(根节点已出)
按照这个次序继续,直到stack为空 -
层级遍历
传统的用一个队列Queue 进行 push & pop,
pop前先取一下这一层的数量,然后把这一层的结点循环一遍,结果加入到一个临时队列中
collections
关键字
- defualt不能修饰变量
- abstract不能被实例化
- 普通变量不能用abstract修饰,abstract一般修饰方法和类
- abstract final double hyperbolicCosine( ),abstract需要被子类继承,而fianl修饰的又不能被继承和改写