知识沉淀
多线程
多线程的使用场景
在不阻塞主线程的基础上启动其它线程去完成某些比较耗时的任务,例如JavaWeb就是主线程监听用户Http请求,然后启动子线程去处理用户http请求。Jvm垃圾回收。
如何使用多线程
两种方式,继承Thread类,实现Runnable接口。Java是单继承,更多使用实现接口。new 一个Thread后,执行start启动子线程。
Runnable可以实现资源共享。
进程/线程间如何通讯
进程间通讯:socket
线程间通讯:
- synchronized关键字,多线程访问同一个共享变量,获取到对象的锁的可以执行
- wait/notify机制,Object类的方法
线程的状态
- 新建:线程对象已经创建,但还没有调用start()方法
- 可运行状态:当前线程有资格运行,但还没被选定为运行线程,当start()方法调用时,线程进入可运行状态,从阻塞、等待、睡眠状态回来后也返回到此状态
- 运行running ,获取到CPU的时间片
- 睡眠/阻塞/等待:线程仍然存活,但是没有条件运行,通过某些唤醒操作,可以返回到可运行状态
- 死亡dead
Synchronized和ReenTrantLock的区别
- 实现依赖:前者依赖JVM实现,后者是JDK实现
- 性能区别:前者优化前性能比后者差,优化后差不多
- ReenTrantLock独有的功能:
- 可以指定锁的公平性,前者只能为非公平锁
- 后者提供一个Condition类,实现线程的分组唤醒,前者只能随机唤醒或者全部唤醒
- 后者提供中断等待锁的线程机制,lock.lockInterruptibly()
ReenTrantLock
重入锁主要集中在Java层面,所有没有请求到锁的线程会进入到等待队列,有线程释放锁后,系统从等待队列中唤醒线程。
实例化时可指定是否公平。
- 获取锁:
- lock(),如果锁已经被占用则等待
- tryLock(),获取成功true,失败false;tryLock(long, TimeUint)定时获取锁,不等待立即返回
- 中断锁:lockInterruptibly()获得锁的时候响应中断
- 条件变量Condition:
- 一个lock可以对应多个condition,一个condition对象对应一个等待队列
- 功能和Object.wait()和Object.notify()大致相同
- await()使当前线程等待同时释放锁,其它线程使用signal()或者signalAll()时,线程会重新获得锁并执行,或者线程被中断时也可以跳出等待
- signal()方法唤醒一个线程,signalAll()唤醒所有在等待中的线程
- 信号量Semaphore:为多线程协作提供更强大的控制方法,对锁的扩展
- 无论是内部锁synchronized还是重入锁ReentrantLock,一次只允许一个线程访问资源,但是信号量可以指定**多个线程,同时访问某一资源**
- 实例化时可指定是否公平
ReentrantReadWriteLock
读写分离锁,减少锁的竞争
wait/sleep/yield/join/suspend/resume区别
- wait会释放对象的锁,sleep不会
- wait针对同步代码块加锁的对象,sleep是针对一个线程
- yield暂停当前正在执行的线程,只会让优先级相同的线程有机会执行
- sleep后的线程在唤醒之后不保证能获取到CPU,它会先进入就绪态,与其他线程竞争CPU
- join等待调用join方法的线程结束,再继续执行
- suspend使线程进入阻塞状态,不会自动恢复,必须其对应的resume被调用才可以进入可执行状态,suspend和resume会释放锁
原子操作,如何同步
不可中断的一个或一系列操作,利用锁或Atomic等原子类,实现原理:内部声明了volatile变量,保证存储和读取的一致性。
volatile关键字的作用,和synchronized的区别
volatile保证变量在线程工作内存和主存之间一致,它强制线程每次从主内存中讲到变量,而不是从线程的私有内存中读取变量,从而保证了数据的可见性。
- 前者只能修饰变量,后者还可以修饰方法。
- 前者只保证数据的可见性,不能用来同步,多个线程访问volatile修饰的变量不会阻塞。
死锁/活锁/饥饿
- 死锁:两个或以上的线程在执行过程中争夺同一资源造成的**互相等待**的现象
- 活锁:两个或者多个线程礼让资源造成的互相等待,最后都无法使用资源
- 饥饿:线程等待访问一个资源,因为优先级低始终轮不到自己
ThredLocal
提供**线程内部的局部变量**,在线程生命周期内起作用,**隔离其它线程**,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
如果设置成全局变量,在多线程中获取到的是同一个值,没有区分单个线程。
实现原理:
- 早期:每个ThreadLocal类创建一个Map,用线程Id作为Map的Key,实例对象作为Map的Value
- 现在:每个Thread维护一个ThreadLocalMap映射表,这个映射表的Key是ThreadLocal实例本身,Value是真正需要存储的Object
优势:
- 每个Map的Entry数量变小了,之前是Thread的数量,现在是ThreadLocal的数量
- 当Thread销毁后对应的ThreadLocalMap也销毁,能减少内存使用量
- ThreadLocalMap是使用ThreadLocal的弱引用作为Key的,因为是弱引用,所以gc会有可能回收,导致内存泄漏
使用:建议将ThreadLocal变量定义成private static,这样ThreadLocal的生命周期更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal不会被回收,也就可以保证任何时候都可以根据ThreadLocal的弱引用访问到Entry的Value值,然后remove,防止内存泄漏。
线程池
线程池的使用场景
避免多线程频繁的开启销毁线程造成jvm内存的消耗。
常见的线程池有哪几种
- newSingleThreadExecutor ,单个线程的线程池,线程池中每次只有一个线程工作
- newFixedThreadPool(n),固定数量的线程池,每提交一个任务就是一个线程,达到最大值进入等待队列
- newCachedThreadPool,**推荐使用**,可缓存线程池,JVM会自动回收及添加线程
- newScheduledThreadPool ,大小无限制的线程池,支持定时和周期性执行线程
线程池构造
一组线程和一个存放任务的队列,线程的创建使用销毁由线程池来管理
corePoolSize:线程池大小,核心池大小
maximumPoolSize:线程池最大容积,超过拒绝,大于corePoolSize开始执行补救措施
线程池的状态
- running
- shutdown ,不能接受新任务,等待已有任务执行完
- stop ,不能接受新任务,终止当前已有任务
- terminal
任务处理策略
- 如果当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务;
- 如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务;
- 如果当前线程池中的线程数目达到maximumPoolSize,则会采取任务拒绝策略进行处理;
- 如果线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止。
任务缓存策略
- ArrayBlockingQueue:基于数组的先进先出队列,创建时必须指定大小
- LinkedBlockingQueue:基于链表的先进先出,不指定大小则默认最大值
- synchronousQueue:不会保存提交的任务,直接新建线程来执行新任务
ArrayBlockingQueue和LinkedBlockingQueue的区别
- 锁的实现不同:前者读写是同一个锁,后者分离
- 生产或消费时操作不同:前者在生产和消费时直接将对象插入或移除,后者需要把对象转换成Node进行插入和移除,影响性能
- 队列大小初始化方式不同:全部这个必须指定队列大小,后者可以不指定大小,默认Integer.Max_VALUE
集合类
HashMap实现原理
- 存储键值对,允许null值和null键,使用containsKey()判断一个key是否存在,不能使用get(key)来判断
- 数据结构:数组+链表,链表散列,每个 Map.Entry 其实就是一个key-value对,初始值16个bucket
- 工作原理:put()方法存储数据时,先对key调用hashCode()方法,返回的hashCode用于定位bucket的位置,如果有相同的hashCode(hash碰撞)使用equals()方法比较是否相同,如果相同抛出异常,不相同存入数据。get()方法同理,先定位bucket,再使用Keys.equals()定位到key在链表中的节点位置。
- 数据超过负载因子如何处理:默认负载因子0.75,一个map填满了75%的bucket时候,调用rehashing,实现扩容,为原来2倍。
- Fast-Fail机制:HashMap不是线程安全的,如果迭代过程中有其他线程修改map结构,抛出异常。
- 通过Collections.synchronizeMap(hashMap)可使hashMap线程安全,不过效率低,只有一个锁
- 插入数据后校验是否需要扩容
HashMap和HashTable区别
- 前者允许null值和null键,后者不允许
- 前者非线程安全,后者线程安全
- 前者初始值16,大于0.75扩容原来2倍,后者初始值11,大于0.75扩容原来2倍+1
ConcurrentHashMap结构(并发包)
- 由Segment数组结构和HashEntry数组结构组成,Segment时一种可重入锁ReentrantLock,Segment的结构和HashMap类似,是一种数组和链表结构,一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素
- 琐分段,每一个segment一个锁,所有数据除了value使用final关键字修饰,value使用volatile修饰,final修饰表示不能从hash链的中间或尾部添加或删除节点,volatile修饰为了避免加锁
- 基本操作:put()操作,一律添加到Hash链的头部,remove()操作中间删除一个节点,会将要删除节点前面所有节点复制一遍,最后一个节点指向要删除节点的下一个节点,删除后复制回来。
- get()操作不需要锁,因为值的定义为volatile,
- 首先访问count变量,由于每次修改操作在修改完后要修改count变量,通过这种机制,保证get操作可以获取到最新的数据
- 然后根据hash和key对hash链进行遍历找到要获取的节点,没找到直接返回null
- 如果值为null,否则在有锁的状态下重新读取一遍
- put()操作在锁定的正哥segment中执行,超过负载因子时,进行rehash,如果key重复直接覆盖,不重复则新建一个节点放在hash链表头部,并修改modCount和count的值
- 如何扩容:**插入数据前校验是否需要扩容**,扩容只针对某个segment,创建一个两倍容量的数组,然后再hash后插入到新的数组里
CopyOnWriteArrayList结构(并发包)
- 写时复制的容器,这样做的好处:并发读取的时候不需要加锁
- 应用场景:读多写少的并发场景,例如白名单、黑名单、商品类目
ConcurrentLinkedQueue结构(并发包)
线程安全的linkedList,高性能的读写队列,不使用锁而使用非阻塞算法,通过循环判断尾指针是否改变
CAS:CAS有三个操作数,内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做并返回false。
WeakHashMap结构
对hashMap的一种改进,key实行弱引用,一个key不再被外部引用则key可以被gc回收
String/StringBuffer/StringBuilder
- StringBuilder > StringBuffer>String
- String使用final修饰,不可变。
- StringBuilder/StringBuffer是可变字符序列,字符串缓冲区,StringBuilder非线程安全,StringBuffer线程安全
- StringBuilder/StringBuffer扩容:初始值都为16,当前数组容量扩充为原数组容量的2倍+2,如果新容量小于预定的最小值,将容量定位最小值,最后判断是否溢出,若溢出则将容量设定为整形最大值
JVM
JVM的内存划分
- 程序计数器,当前线程执行字节码的行号指示器。
- 栈区,描述的是Java方法执行的内存模型,每个方法被执行时需要创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等。每个方法被调用到完成,即出栈。**此区域为线程私有的内存**。
- 本地方法栈,虚拟机栈为虚拟机执行Java方法,本地方法栈则是为虚拟机使用到的Native方法服务。
- 堆区,所有**对象实例**和**数组**都在堆区分配,gc主要在这个区域出现。此区域为**所有线程共享区域**
- 新生代,分为一个Eden和两个Survivor区
- 老年代
- 方法区,**所有线程共享区域**,存储被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。gc很少在这个区域出现,回收目标是针对常量池的回收和类型的卸载,也称**永久代**。
- 运行时常量池,方法区的一部分,存放编译器生成的各种自变量和符号引用
GC在什么时候对什么做了什么操作
- 什么时候回收
- Minor GC:对象优先在Eden中分配,当Eden中内存不够,虚拟机会发生一次Minor GC,Minor GC非常频繁,速度也很快
- Full GC:发生在老年代GC,当老年代没有足够空间时发生Full GC,发生Full GC时一般会伴随这一次Minor GC。大对象直接进入老年代,例如字符串数组。
- 发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于则进行一次Full GC,如果小于,则会查看HandlePromotionFailure设置是否允许担保失败,如果允许,那只会进行一次Minor GC,如果不允许,则改为进行一次Full GC。
- 哪些内存需要回收:JVM对不可用的对象进行回收
- 如何判断一个对象是否可以被回收:采用根搜索算法(GC Root Tracing),当一个对戏那个到GC Roots没有任何引用相连接,GC Roots到这个对象不可达,则此对象可以被回收。
- 什么时候被回收:要被回收的对象需要经历至少两次标记过程,需要判断对象在finalize()方法中可能自救,如果重新与引用链上的对象建立关联则不会被回收,如果finalize()方法已经被虚拟机调用执行一次了或没有要执行的finalize()方法,则将会被GC。
- 如何回收:选择不同的垃圾收集器,收集算法也不同
- 新生代:大批对象死去,少量存活,使用复制算法,每次使用Eden去和一个Survivor区,当回收时将Eden区和Survivor区还存活的对象一次性拷贝到另一个Survivor区,最后清理掉Eden区和Survivor区。Eden和Survivor默认比例时8:1。保证内存的连续,不会留下内存碎片。
- 老年代中对象存活率高,使用标记-清理或标记-压缩算法
- 标记-清理:从根节点开始标记所有可达对象,回收后空间不连续
- 标记-压缩:标记后不复制,存活对象压缩到内存另一边,清理边界外的所有对象。
类加载过程
- 类的加载:类加载机制中的第一步加载,用户可以通过自定义的类加载器,JVM主要完成三件事
- 通过一个类的名称(包名与类名)来获取定义此类的class文件
- 将class文件锁代表的静态存储结构转化为方法区的运行时数据结构,**方法区**是用来存放已被加载的**类信息,常量,静态变量,编译后的代码**运行时内存区域
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的数据的访问入口。此对象并没有放在堆内存中,而是放在方法区中
- 类的连接:负责将类的二进制数据合并到Java运行时环境中,分为三个阶段
- 验证:验证被加载后的类的数据结构是否符合虚拟机的要求
- 准备:为类的静态变量在方法区分配内存,并赋默认初始值(0或者null)
- 解析:类的二进制数据中的符号引用转换为直接引用
- 类的初始化:为静态变量赋程序设定的初值
类加载器和双亲委派
- 类相等的判定条件:
- 两个类来自同一个class文件
- 两个类是由同一个虚拟机加载
- 两个类是由同一个类加载器加载
- 类加载器分类
- 启动类加载器:负责Java核心类库
- 扩展类加载器:负责加载扩展目录下的jar包
- 应用程序加载器:加载classpath环境变量所指定的jar包与类路径,用户自定义的类是由该加载器加载
- 双亲委派加载机制:当一个类收到了类加载请求,它首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,只有在父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没找到所需加载的class),子类加载器才会尝试自己去加载。
Spring全家桶
IOC和DI的区别
- 前者是控制反转,将原本在程序中手动创建对象的控制权交给Spring框架去管理
- 后者是依赖注入,在Spring框架负责创建Bean对象时,动态的将对象依赖属性通过配置进行注入
AOP
面向切面编程,弥补了面向对象编程的不足,提供了切面,对关注点进行模块化,例如横切多个类型和对象的事务管理
事务管理
- 编程式事务:通过TransactionTemplate手动管理事务
- 声明式事务:使用XML配置声明式事务,是通过AOP来实现的,常用的为基于注解方式的事务,在业务层类上添加注解@Transactional
Mysql和NoSql
分布式相关
设计模式
排序算法
最后更新:2017-10-24 10:33:48