知识整理

目录

多线程

多线程的使用场景

在不阻塞主线程的基础上启动其它线程去完成某些比较耗时的任务,例如JavaWeb就是主线程监听用户Http请求,然后启动子线程去处理用户http请求。Jvm垃圾回收。

如何使用多线程

两种方式,继承Thread类,实现Runnable接口。Java是单继承,更多使用实现接口。new 一个Thread后,执行start启动子线程。

Runnable可以实现资源共享。

Callable和Runnable的区别

实现Callable接口的任务线程能返回执行结果;而实现Runnable接口的任务线程不能返回结果;

Callable接口的call()方法允许抛出异常,而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛;

Callable接口支持返回执行结果,此时需要调用FutureTask.get()方法实现,此方法会阻塞主线程直到获取‘将来’结果;当不调用此方法时,主线程不会阻塞。

进程/线程间如何通讯

进程间通讯:socket

线程间通讯:

  1. synchronized关键字,多线程访问同一个共享变量,获取到对象的锁的可以执行
  2. wait/notify机制,Object类的方法

线程的状态

  1. 新建:线程对象已经创建,但还没有调用start()方法
  2. 可运行状态:当前线程有资格运行,但还没被选定为运行线程,当start()方法调用时,线程进入可运行状态,从阻塞、等待、睡眠状态回来后也返回到此状态
  3. 运行running ,获取到CPU的时间片
  4. 睡眠/阻塞/等待:线程仍然存活,但是没有条件运行,通过某些唤醒操作,可以返回到可运行状态
  5. 死亡dead

线程阻塞的原因

  1. 线程执行Thread.sleep方法,放弃CPU执行权,睡眠,不放弃锁
  2. 线程需要执行一段同步代码,但是无法获得同步锁,进入阻塞状态
  3. 线程执行了一个对象的wait方法,进入阻塞状态,放弃锁,等待其它线程唤醒
  4. 线程执行IO操锁因为等待相关资源进入阻塞状态

Synchronized原理和使用

Java中的每个对象都有一个监视器,来监测并发代码的重入。在synchronized 范围内,监视器发挥作用。

修饰方法上

同步的方法,某个线程调用此方法时,会获取该实例的对象锁,方法未结束,其它线程只能等待,基础为所有线程使用的是同一对象实例(这个类加载后实例化的对象)。

修饰静态方法

调用此方法的线程会获取到该类的锁,其它线程等待。

修饰对象

如果多个线程共享一个Object对象,拥有obj对象的锁执行,其余线程等待。

当obj == this时,表示当前调用该方法的实例对象,等同于修饰在方法上。

Synchronized和ReenTrantLock的区别

  1. 实现依赖:前者依赖JVM实现,后者是JDK实现
  2. 性能区别:前者优化前性能比后者差,优化后差不多
  3. ReenTrantLock独有的功能:
    • 可以指定锁的公平性,前者只能为非公平锁,公平锁:队列先进先出,非公平:无序,允许插队,jvm根据自己调度来选择唤醒
    • 后者提供一个Condition类,实现线程的分组唤醒,前者只能随机唤醒或者全部唤醒
    • 后者提供中断等待锁的线程机制,lock.lockInterruptibly()
  4. ReenTrantLock底层实现锁采用CAS无锁操作

ReenTrantLock

重入锁主要集中在Java层面,所有没有请求到锁的线程会进入到等待队列,有线程释放锁后,系统从等待队列中唤醒线程。

实例化时可指定是否公平,finally中显示的释放锁。

实现原理
  1. 锁被获取次数state:volatile修饰
  2. 获取到锁的线程
  3. 其余等待队列

一个线程获取到锁了则state+1,可重入表示可继续获取同一个锁,重入时把状态值进行累加,直到释放锁后state到0,表示锁被完全释放,其余线程才可以获取到锁。

可重入的概念

如果某个线程试图获取一个已经由它自己持有的锁时,那么这个请求会立刻成功,并且会将这个锁的计数值加1,而当线程退出同步代码块时,计数器将会递减,当计数值等于0时,锁释放。

  • 重入锁:Java中ReentrantLock和Synchronized都是可重入锁,前者对逻辑控制的灵活性要远远好于Synchronized。
  • 作用:一个线程可以允许连续多次获得同一把锁,如果不允许,那么同一个线程在第二次获得锁时,将会和自己产生死锁
  • 使用注意:如果同一个线程多次获得锁,在释放锁的时候也必须释放相同次数,直到state为0。
获取锁
  • 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区别

  1. wait会释放对象的锁,sleep不会
  2. wait针对同步代码块加锁的对象,sleep是针对一个线程
  3. yield暂停当前正在执行的线程,只会让优先级相同的线程有机会执行
  4. sleep后的线程在唤醒之后不保证能获取到CPU,它会先进入就绪态,与其他线程竞争CPU
  5. join等待调用join方法的线程结束,再继续执行
  6. suspend使线程进入阻塞状态,不会自动恢复,必须其对应的resume被调用才可以进入可执行状态,suspend和resume会释放锁

CAS和原子操作

CAS使用乐观策略,同步与锁使用悲观策略。

Compare and Swap, 翻译成比较并交换 ,非阻塞算法,没有锁竞争和线程间的调度带来的系统开销,性能优越。

原子操作:不可中断的一个或一系列操作,利用锁或Atomic等原子类,实现原理:内部声明了volatile变量,保证存储和读取的一致性。

  • 算法过程:三个操作数,内存值V ,旧的预期值A,要修改的新值B。当且仅当预期值A与内存值V相同时,将内存值V修改为B,否则不做操作。最后返回当前内存值的真实值。当多个线程同时使用CAS操作一个变量时,只有一个会胜出并更新成功。失败的线程不会被挂起,仅被告知失败,并允许再次尝试。
  • AtomicInteger实现:atomic包,对整数的原子操作。compareAndSet(int expect, int u)如果当前值为expect,则设置为u。底部无限循环,现获取当前值current,修改后的值next为当前值+1,直到如果compareAndSet(current, next)成功,返回next。
  • 底层实现:Java中的指针Unsafe类
    • 是否可以自己使用Unsafe类:
  • AtomicStampedReference:带有时间戳的原子对象引用,内部不仅维护对象之还维护了一个时间戳,当数据被修改,时间戳会自动更新。
  • CAS的使用场景:在竞争不是很激烈的情况下,替换锁对数据的修改做原子操作。若竞争激烈,CAS底层的无限循环会造成cpu的空转浪费资源。
  • CAS存在的问题:CAS操作可能会造成ABA问题,就是在多线程操作的情况下一个值从A变成了B然后又变成了A,CAS操作不能发现这个数据被修改过发生过变化,处理这类问题可以使用带时间戳版本的CAS类AtomicStampedReference;

volatile关键字

volatile保证变量在线程工作内存和主存之间一致,它强制线程每次从主内存中读取变量,而不是从线程的私有内存中读取变量,从而保证了数据的可见性

  1. 修饰:前者只能修饰变量,后者还可以修饰方法。
  2. 阻塞:前者只保证数据的可见性,不能用来同步,多个线程访问volatile修饰的变量不会阻塞。

死锁/活锁/饥饿

  1. 死锁:两个或以上的线程在执行过程中争夺同一资源造成的互相等待的现象
  2. 活锁:两个或者多个线程礼让资源造成的互相等待,最后都无法使用资源
  3. 饥饿:线程等待访问一个资源,因为优先级低始终轮不到自己

ThredLocal

提供线程内部的局部变量,在线程生命周期内起作用,隔离其它线程,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。

如果设置成全局变量,在多线程中获取到的是同一个值,没有区分单个线程。

ThreadLocal为每个使用该变量的线程分配一个独立的变量副本。

和线程的同步机制的区别

如果你需要进行多个线程之间进行通信,则使用同步机制;如果需要隔离多个线程之间的共享冲突,可以使用ThreadLocal,这将极大地简化你的程序,使程序更加易读、简洁。

实现原理

每个Thread维护一个ThreadLocalMap映射表,这个映射表的Key是ThreadLocal实例本身,Value是真正需要存储的Object。

ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap获取 value。

  • set方法:先获得当前线程对象,然后通过getMap()拿到线程的ThreadLocalMap,并将值set进ThreadLocalMap。设置到ThreadLocal中的数据,也正是写入了threadLocals这个Map,其中key为ThreadLocal当前对象,value为存入的对象。threadLocals本身保存了当前自己所在线程的所有局部变量,也就是一个ThreadLocal变量的集合。
  • get方法:先获得当前线程的ThreadLocalMap,然后通过将自己作为key取得内部的实际数据。

  • 当Thread销毁后对应的ThreadLocalMap也销毁,能减少内存使用量

  • ThreadLocalMap是使用ThreadLocal的弱引用作为Key的,因为是弱引用,所以gc会有可能回收
内存泄漏问题

多线程间ThreadLocal变量都维护在Thread类内部的ThreadLocalMap中,意味着只要线程不退出,对象的引用一直存在。线程退出时,Thread类会进行清理ThreadLocalMap。如果使用线程池的话,任务结束后线程未必会退出(固定大小的线程池,线程总是存在),可能会出现内存泄漏。

ThreadLocalMap使用ThreadLoca的弱引用作为key,不过弱引用只是针对key,每个key都弱引用指向ThreadLocal。当把ThreadLocal实例置为null以后,没有任何强引用指向ThreadLocal实例,所以ThreadLocal将会被gc回收, 但是value却不能回收,因为存在一条从current thread连接过来的强引用。只有当前thread结束以后,current thread就不会存在栈中,强引用断开,Current Thread,Map,value将全部被GC回收。

在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。但是这些被动的预防措施并不能保证不会内存泄漏。

为什么使用弱引用而不是强引用?

由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障弱:引用ThreadLocal不会内存泄漏,对应的key为null的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。由于ThreadLocalMap的生命周期Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

每个线程的变量副本存储在哪里

存储在当前线程的ThreadLocalMap中,通过当前线程的ThreadLocal实例为key的弱引用获取到数据。

使用

建议将ThreadLocal变量定义成private static,这样ThreadLocal的生命周期更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal不会被回收,也就可以保证任何时候都可以根据ThreadLocal的弱引用访问到Entry的Value值,然后手动remove,防止内存泄漏。

锁的种类及使用场景

java锁分类

公平锁/非公平锁
  • 公平按顺序来
  • 非公平指唤醒随机;
可重入锁

递归锁,防止自己与自己产生死锁;

独享锁/共享锁
  • 独享锁:该锁一次只能被一个线程所持有
  • 共享锁:该锁可以被多个线程所持有
互斥锁/读写锁

独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现

  • 互斥锁:ReentrantLock
  • 读写锁:ReadWriteLock
乐观锁/悲观锁

不是指具体的什么类型的锁,而是指看待并发同步的角度

  • 悲观锁:认为对于同一个数据的并发操作,一定是会发生修改的,悲观锁采取加锁的形式
  • 乐观锁:认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。使用CAS无锁实现
分段锁

一种锁的设计,并发的实现就是通过分段锁的形式来实现高效的并发操作,例如ConcurrentHashMap

偏向所/轻量级锁/重量级锁

指锁的状态,针对Synchronized。三种锁的状态是通过对象监视器在对象头中的字段来表明的

  • 偏向锁:一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价
  • 轻量级锁:当锁是偏向锁的时候,被另外一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能
  • 重量级锁:当锁为轻量级锁时,虽然另一个线程自旋,但自旋不会一直持续下去,当自旋一定次数后还没获取到锁则进入阻塞状态,该锁变为重量级锁。重量级锁会让其它申请的线程直接进入阻塞,性能降低
自旋锁/自适应自旋锁
  • 自旋锁:尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗;缺点是循环会消耗CPU;优点是可以保证大吞吐率和执行效率;
  • 自适应自旋锁:就是自旋的次数是通过JVM在运行时收集的统计信息,动态调整自旋锁的自旋次数上界。

线程同步

线程同步的几种方法

互斥锁、读写锁、条件变量、信号量、令牌

为什么runnable可以实现资源共享

Thread:一个线程只能启动一次,通过Thread实现线程时线程和所要执行的任务是捆绑在一起的,一个任务只能启动一个线程;

Runnable:一个任务可以启动多个线程,开辟一个线程将任务传递进去,该线程去执行,所有线程执行的都是同一任务,所以资源是共享的。

互斥锁和读写锁

对临界资源(多线程共享的资源)的保护,当多线程试图访问临界资源的时候,都必须通过获取锁的方式来访问临界资源。当读写线程获取锁的频率差别不大时,一般采用互斥锁,如果读线程访问临界资源频率大于写线程时采用读写锁。

条件变量

提供线程间的异同通知机制,当某一条件满足时,线程A可以通知阻塞条件变量上的线程B,B锁期望的条件已经满足,可以解除在条件变量上的阻塞操作,继续做其它事情。

信号量

提供对临界资源安全分配。如果存在多份临界资源,在多个线程争抢临界资源的情况下,向线程提供安全分配临界资源的方法,如果临界资源数为1,则退化为锁。

信号量可以控制某个资源可被同时访问的个数,单个信号量的Semaphore对象可以实现互斥锁的功能

Semaphore维护了当前访问的个数,提供同步机制,控制同时访问的个数。在数据结构中链表可以保存“无限”的节点,用Semaphore可以实现有限大小的链表

令牌

一种高级的线程同步方法,它既提供锁的安全访问临界资源的功能,又利用了条件变量使得线程争夺临界资源时是有序的。

synchronized、锁、多线程同步的原理

synchronized、锁、多线程同步的原理是咋样的

线程池

线程池的使用场景

避免多线程频繁的开启销毁线程造成jvm内存的消耗。

常见的线程池有哪几种

  1. newSingleThreadExecutor ,单个线程的线程池,线程池中每次只有一个线程工作
  2. newFixedThreadPool(n),固定数量的线程池,每提交一个任务就是一个线程,达到最大值进入等待队列
  3. newCachedThreadPool,推荐使用,可缓存线程池,JVM会自动回收及添加线程
  4. newScheduledThreadPool ,大小无限制的线程池,支持定时和周期性执行线程

线程池构造

一组线程和一个存放任务的队列,线程的创建使用销毁由线程池来管理

corePoolSize:线程池大小,核心池大小

maximumPoolSize:线程池最大容积,超过拒绝,大于corePoolSize开始执行补救措施

线程池的状态

  1. running
  2. shutdown ,不能接受新任务,等待已有任务执行完
  3. stop ,不能接受新任务,终止当前已有任务
  4. terminal

任务处理策略

  1. 如果当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务;
  2. 如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务;
  3. 如果当前线程池中的线程数目达到maximumPoolSize,则会采取任务拒绝策略进行处理;
  4. 如果线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止。

任务缓存策略

  1. ArrayBlockingQueue:基于数组的先进先出队列,创建时必须指定大小
  2. LinkedBlockingQueue:基于链表的先进先出,不指定大小则默认最大值
  3. synchronousQueue:不会保存提交的任务,直接新建线程来执行新任务

ArrayBlockingQueue和LinkedBlockingQueue的区别

  1. 锁的实现不同:前者读写是同一个锁,后者分离
  2. 生产或消费时操作不同:前者在生产和消费时直接将对象插入或移除,后者需要把对象转换成Node进行插入和移除,影响性能
  3. 队列大小初始化方式不同:前者必须指定队列大小,后者可以不指定大小,默认Integer.Max_VALUE

集合类

集合类框架

  • 集合类两个根派生接口:Collection(一组纯数据)和Map(一组kv)
    • Collection:Set(HashSet,SortedSet)、List(ArrayList,Vector)、Queue(队列)
    • Map:HashMap, HashTable
  • 集合的三大类:
    • List:有序集合,可重复
    • Set:无序集合,不重复
    • Map:key-value形式元素
  • 集合的遍历
    • Iterator迭代器
    • foreach循环

序列化

什么是序列化

对象的创建使用必须在JVM处于运行时才能生效,但在实际运用中,往往需要重复读取被保存的对象,序列化可以把把对象保存起来,保存为一组字节,反序列化时再将字节组装成对象。

实现序列化的方式
继承接口
fastjson的实现
  1. IdentityHashMap和Hashmap的区别:比较key是否相等时前者使用引用相等==,后者使用对象相等equals()。前者可以避免HashMap并发时的死循环(?)。

HashMap实现原理

  1. 存储键值对,允许null值和null键,使用containsKey()判断一个key是否存在,不能使用get(key)来判断
  2. 数据结构:数组+链表,链表散列,每个 Map.Entry 其实就是一个key-value对,初始值16个bucket
  3. 工作原理:put()方法存储数据时,先对key调用hashCode()方法,返回的hashCode用于定位bucket的位置,如果有相同的hashCode(hash碰撞)使用equals()方法比较是否相同,如果相同覆盖插入,不相同存入数据。get()方法同理,先定位bucket,再使用Keys.equals()定位到key在链表中的节点位置。
  4. 数据超过负载因子如何处理:默认负载因子0.75,一个map填满了75%的bucket时候,调用rehashing,实现扩容,为原来2倍。
  5. Fast-Fail机制:HashMap不是线程安全的,如果迭代过程中有其他线程修改map结构,抛出异常。
  6. 通过Collections.synchronizeMap(hashMap)可使hashMap线程安全,不过效率低,只有一个锁
  7. 插入数据后校验是否需要扩容
为什么默认16个bucket

hash散列算法,保证元素在哈希表中均匀的散列。常用为取模,但会用到除法,效率低,hashMap中使用h&(length-1) 的方法来代替取模。同样为了实现均匀的散列,效率要高。h为key计算出来的hashcode,length为数组长度,hash表的容量。

hash表的容量一定要是2的整数次幂。如果length为2的整数次幂,length-1 的二进制数全为1,再与h做与运算,可以保证最后一位为奇数偶数的概率相同。如果length为奇数,那么length-1 的二进制数最后一位为0,则再与h做与运算,最后一位必然为偶数,碰撞概率高。

resize扩容

原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。非常消耗性能。

先把数组的length扩大一倍,然后重新计算每个元素在数组中的位置,扩容是需要进行数组复制的。

hashcode和equals的联系及改写规则
  1. hashcode
    • 解释:hashcode返回的是对象存储的物理地址,将数据依照特定算法直接指定到一个地址上。
    • 作用:如果每次存入数据的时候调用equals方法去比较值是否存在则效率太低,需依次轮询比较。使用hashcode则在插入数据时调用这个元素的hashCode方法,定位到它应该存放的物理位置上,如果该位置上没有元素,则可以直接存放,有元素后则调用equals方法与新元素进行比较,相同就不存,不同就散列到其它地址。
  2. equals
    • 解释:比较两个对象,内容的比较,从表面上或者从内容上看两个对象是否相等。不比较地址。用户手动调用。
    • 设计遵循原则:
      • 对称性:如果x.equals(y)返回是“true”,那么y.equals(x)也应该返回是“true”。
      • 反射性:x.equals(x)必须返回是“true”。
      • 类推性:如果x.equals(y)返回是“true”,而且y.equals(z)返回是“true”,那么z.equals(x)也应该返回是“true”。
      • 一致性:如果x.equals(y)返回是“true”,只要x和y内容一直不变,不管你重复x.equals(y)多少次,返回都是“true”。
      • 任何情况下:x.equals(null),永远返回是“false”;x.equals(和x不同类型的对象)永远返回是“false”。
  3. 联系
    • 如果两个对象相同,则hashCode值一定相同
    • 如果两个对象的hashCode相同,两个对象不一定相同。
jdk1.8新特性的HashMap
  • 数组存放模式:由HashEntry数组改为Node数组,hashcode计算后产生碰撞后对应到一个数组后的长度大于8个就按照红黑树形式存放,少于8个按照链表存放
  • 计算hashcode:优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。

并发的HashMap为什么会引起死循环

产生死循环的原因

多个线程同时执行rehash操作时,如果一个线程执行到循环中,并得到e和next后线程被挂起,另外线程rehash完成后,继续执行之前线程可能会造成环形链表,此时在之前线程做查询操作则会出现死循环。

HashMap和HashTable区别

  1. 前者允许null值和null键,后者不允许
  2. 前者非线程安全,后者线程安全
  3. 前者初始值16,大于0.75扩容原来2倍,后者初始值11,大于0.75扩容原来2倍+1

ConcurrentHashMap结构(并发包)

ConcurrentHashMap

  1. 由Segment数组结构和HashEntry数组结构组成,Segment是一种可重入锁ReentrantLock,Segment的结构和HashMap类似,是一种数组和链表结构,一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素
  2. 琐分段,每一个segment一个锁,所有数据除了value使用final关键字修饰,value使用volatile修饰,final修饰表示不能从hash链的中间或尾部添加或删除节点,volatile修饰为了避免加锁
  3. 基本操作:put()操作,一律添加到Hash链的头部,remove()操作中间删除一个节点,会将要删除节点前面所有节点复制一遍,最后一个节点指向要删除节点的下一个节点,删除后复制回来。
  4. get()操作不需要锁,因为值的定义为volatile
    • 首先访问count变量,由于每次修改操作在修改完后要修改count变量,通过这种机制,保证get操作可以获取到最新的数据(?)
    • 然后根据hash和key对hash链进行遍历找到要获取的节点,没找到直接返回null
    • 如果值为null,则在有锁的状态下重新读取一遍
  5. put()操作在锁定的整个segment中执行,超过负载因子时,进行rehash,如果key重复直接覆盖,不重复则新建一个节点放在hash链表头部,并修改modCount和count的值
  6. 如何扩容:插入数据前校验是否需要扩容,扩容只针对某个segment,创建一个两倍容量的数组,然后再hash后插入到新的数组里
  7. size:首先不加锁循环执行以下操作:循环所有的Segment,获得对应的值以及所有Segment的modcount之和。如果连续两次所有Segment的modcount和相等,则过程中没有发生其他线程修改ConcurrentHashMap的情况,返回获得的值。否则对所有的Segment依次进行加锁,获取返回值后再依次解锁。
jdk1.8新特性的ConcurrentHashMap

摒弃了Segment(锁段)的概念,而是启用了一种全新的方式实现,利用CAS算法。

  1. 不采用segment而采用node,锁住node来实现减小锁粒度;
  2. JDK8中使用synchronized而不是ReentrantLock(?);
问题
  1. 为什么HashEntry的所有插入都是插入到头节点而不是尾节点?

CopyOnWriteArrayList结构(并发包)

  1. 写时复制的容器,这样做的好处:并发读取的时候不需要加锁
  2. 应用场景:读多写少的并发场景,例如白名单、黑名单、商品类目

ConcurrentLinkedQueue结构(并发包)

线程安全的linkedList,高性能的读写队列,不使用锁而使用非阻塞算法,通过循环判断尾指针是否改变

CAS:CAS有三个操作数,内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做并返回false。

WeakHashMap结构

对hashMap的一种改进,key实行弱引用,一个key不再被外部引用则key可以被gc回收

String/StringBuffer/StringBuilder

  1. StringBuilder > StringBuffer>String
  2. String使用final修饰,不可变。
  3. StringBuilder/StringBuffer是可变字符序列,字符串缓冲区,StringBuilder非线程安全,StringBuffer线程安全
  4. StringBuilder/StringBuffer扩容:初始值都为16,当前数组容量扩充为原数组容量的2倍+2,如果新容量小于预定的最小值,将容量定位最小值,最后判断是否溢出,若溢出则将容量设定为整形最大值

JVM

JVM的内存划分

  1. 程序计数器,当前线程执行字节码的行号指示器。
  2. 栈区,描述的是Java方法执行的内存模型,每个方法被执行时需要创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等。每个方法被调用到完成,即出栈。此区域为线程私有的内存
  3. 本地方法栈,虚拟机栈为虚拟机执行Java方法,本地方法栈则是为虚拟机使用到的Native方法服务。
  4. 堆区,所有对象实例数组都在堆区分配,gc主要在这个区域出现。此区域为所有线程共享区域
    • 新生代,分为一个Eden和两个Survivor区
    • 老年代
  5. 方法区,所有线程共享区域,存储被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。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的区别

  1. 前者是控制反转,将原本在程序中手动创建对象的控制权交给Spring框架去管理
  2. 后者是依赖注入,在Spring框架负责创建Bean对象时,动态的将对象依赖属性通过配置进行注入

AOP

面向切面编程,弥补了面向对象编程的不足,提供了切面,对关注点进行模块化,例如横切多个类型和对象的事务管理

事务管理

  1. 编程式事务:通过TransactionTemplate手动管理事务
  2. 声明式事务:使用XML配置声明式事务,是通过AOP来实现的,常用的为基于注解方式的事务,在业务层类上添加注解@Transactional

Mysql

Innodb和MyIsam

索引

底层结构为什么是平衡树

索引底层原理

各种二叉树的数据结构

B+tree的不足及LSMtree

LSMtree

LSM 是日志和传统的单文件索引(B+ tree,Hash Index)的中立,他提供一个机制来管理更小的独立的索引文件(sstable)。

通过管理一组索引文件而不是单一的索引文件,LSM 将B+树等结构昂贵的随机IO变的更快,而代价就是读操作要处理大量的索引文件(sstable)而不是一个,另外还是一些IO被合并操作消耗。

B+tree优势在于查询,大量随机插入数据产生的页分裂会造成性能的降低;

Log Structured-Merge Tree 更好的写操作吞吐量,使用产品:HBase等。

LSMtree原理:将之前使用一个大的查找结构(造成随机读写,影响写性能),变换为将写操作顺序的保存到一些相似的有序文件(也就是sstable)中。所以每个文件包 含短时间内的一些改动。因为文件是有序的,所以之后查找也会很快。文件是不可修改的,他们永远不会被更新,新的更新操作只会写到新的文件中。读操作检查很 有的文件。通过周期性的合并这些文件来减少文件个数。

事务

ACID属性

原子性、持久性、隔离性、一致性。

分库分表

垂直分表
水平分表

读写分离

NoSql

Memcache和Redis区别

  • Redis支持的数据类型更丰富,Redis不仅仅支持kv类型的数据同时还提供list,set,sorted set, hash等数据结构
  • Redis支持数据的备份,master-slave模式的数据备份
  • Redis支持数据持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载

Redis

数据类型及应用场景
  • String
    • 简单的key-value,value类型不仅是String也可以是int
    • 类似Memcached的功能,k-v形式存储数据,对象序列化后存储到指定key
  • Hash
    • 存储一系列的key-value,防止存储一个序列化的数据每次修改数据都需要做一次反序列化再存储的操作,这样存储不利于并发操作,应存储为hash结构
    • Redis Hash对应的Value内部实际就是一个HashMap,两种不同的实现,如果kv对比较少时Redis内部为了节省内存会使用类似一维数组的方式紧凑存储,kv对比较多时会转换为HashMap结构。
  • List
    • 链表结构,常多用做存储列表数据或消息队列
    • 实现为双向链表,可以支持反向查询和遍历
  • Set
    • 类似于List,元素不重复的集合
    • 内部实现为一个value永远为null的HashMap
  • Sorted Set
    • 自动有序的集合,可以通过用户额外提供的优先级参数score来为成员排序
    • 内部使用HashMap和跳跃表SkipList来保证数据的存储和有序,HashMap里放的时成员到score的映射,跳远表里存放的是所有成员
底层数据实现

Redis五种对象类型及底层实现

Redis系列文章

持久化方案
  • RDB方式,快照(默认),二进制文件rdb存储

    原理:当需要持久化数据时,redis会fork一个子进程,子进程将数据卸载磁盘上一个临时rdb文件中,当子进程完成写操作后,将原有rdb文件替换掉,执行过程类似copy-on-write。

    • 优点
    • 可以定时备份某一特定时段的数据
    • 单一文件,方便传输,适用于灾难恢复
    • 父进程不参与备份相关的IO操作,全部由子进程完成
    • 在恢复大的数据集时更快
    • 缺陷
    • redis意外宕机,可能丢失几分钟的数据,具体取决于配置的save时间点,每次保存的是整个数据集,数据量大,通常设置为5分钟
    • 需要经常fork子进程来保存数据集到硬盘,当数据集比较大时,子进程会很耗时,可能导致redis在一些毫秒级别内不能响应客户端的请求,影响CPU性能
  • AOF方式,将执行过的写指令记录下来,在数据恢复时,按照顺序再重新执行一遍

    原理:默认的AOF持久化策略是每秒fsync一次(fsync是把缓存中的写指令记录到磁盘中),如果redis故障,丢失的数据也只是最近1秒的数据。

    如果追加日志时磁盘空间满了等导致的写入不完整,redis有提供工具进行日志修复。

    AOF文件的体积阈值设定,如果AOF文件超过阈值,redis会启动AOF文件内容压缩即redis的AOF文件重写机制,只保留可以恢复数据的最小指令集,类似于多个INCR指令压缩为一条SET指令。

    进行AOF重写时也是采用先写临时文件,后续替换。

    • 优点
    • fsync策略种类多:每秒fsync(默认)或每次写的时候fsync,fsync由后台线程处理,出现问题最多丢失1秒数据
    • AOF文件重写机制,防止文件体积过大
    • 缺点
    • AOF文件体积通常会大于RDB文件
    • 根据所使用的fsync策略,AOF速度会慢于RDB

缓存穿透、缓存并发、缓存失效

概念

  1. 缓存穿透:大量请求命中不到缓存就会走数据库,从而对db压力过大,可能造成而已攻击;解决方案:统一把不存在的key路由到指定的key中,业务逻辑不继续往下走到db;
  2. 缓存并发:一般业务逻辑是先读缓存,缓存没数据再读数据库并更新缓存,如果同一时间多个线程操作,则会导致缓存被多次写入;解决方案:在做更新缓存时添加锁,并在写入缓存是判断缓存是否有值或是否已被其余线程更新;
  3. 缓存失效:同一时间点缓存统一都失效了,导致同一时间点大量请求直接到达db;解决方案:失效时间分散;

缓存与数据库的数据一致性

当修改了数据库后,有没有及时修改缓存。

网络问题引起的没有及时更新,可以通过重试机制来解决。

缓存服务器挂了,从而直接访问到数据库。那么我们在修改数据库后,无法修改缓存,这时候可以将这条数据放到数据库中,同时启动一个异步任务定时去检测缓存服务器是否连接成功,一旦连接成功则从数据库中按顺序取出修改数据,依次进行缓存最新值的修改。

分布式相关

分布式事务

分布式锁

负载均衡

一致性哈希算法
  • 使用场景:数据分片与路由,把海量数据/请求均衡的分配到各个机器中
  • 衡量一致性哈希算法的标准:
    • 平衡性:hash出来的结果能够尽量分布到所有缓冲中去,空间得以全部利用
    • 单调性:如果已经有一些数据通过hash分配到了相应的机器上,当新加入了机器到系统中后,hash的结果可以保证原有的数据还是映射分配到原有的或者新的机器中去,不会被映射分配到旧的机器集合中
    • 分散性:相同的内容被存储到了不同缓冲中去,应避免或降低分散性
    • 负载:
  • 一致性哈希的原理
  • 问题
    • 使用md5来保证一致性哈希的平衡性?
    • 虚拟节点,作用?
    • 如何解决添加或删除机器时的大量数据迁移?

MQ的原理

MQ常用名词概念
  • 信道channel:信道是生产者消费者在rabbit通信的渠道,生产者publish或者消费者subscribe一个队列都是通过信道来通信的,信道是建立在TCP连接上的虚拟连接,一条TCP连接上可以建立多个信道来达到多个线程处理,这个TCP被多个线程共享,每个线程对应一个信道,信道在rabbit上有唯一id,保证了信道的私有性,唯一线程使用。
  • 路由器exchange,路由键routing key:服务器会根据路由键将消息从交换器路由到队列上去;
    • 一个路由键对应一个队列;
    • exchange的种类:(direct)1:1,(fanout)1:N,(topic)N:1;header(非路由键匹配,功能和direct类似,很少用);根据功能选择不同的种类路由器,不同功能的路由器对应的路由键个数不同;
    • 交换器本质是一张路由查询表(名称和队列id,类似于hash表),这是一个虚拟出来的东西,并不存在真实的交换器。
  • 消息的生命周期:生产者生产消息,交由信道,信道通过消息(载体和标签)的标签(路由键)放到交换器发送到指定队列上。

MQ的使用场景

  1. 异步处理
  2. 解耦
  3. 流量削峰

设计模式

排序算法

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值