spring
springmvc
springboot
计网
jvm相关(看着搞)
rpc框架dubbo(不搞)
Zookeeper(不搞)
String 字符串常量(不可变,在更换值时因为创建新对象要丢弃无引用的对象启用gc效率降低)
StringBuffer 字符串变量(线程安全)
StringBuilder 字符串变量(非线程安全,效率比StringBuffer高)
简要的说, String 类型和 StringBuffer 类型的主要性能区别其实在于 String 是不可变的对象, 因此在每次对 String 类型进行改变的时候其实都等同于生成了一个新的 String 对象,然后将指针指向新的 String 对象,所以经常改变内容的字符串最好不要用 String ,因为每次生成对象都会对系统性能产生影响,特别当内存中无引用对象多了以后, JVM 的 GC 就会开始工作,那速度是一定会相当慢的。
而如果是使用 StringBuffer 类则结果就不一样了,每次结果都会对 StringBuffer 对象本身进行操作,而不是生成新的对象,再改变对象引用。所以在一般情况下我们推荐使用 StringBuffer ,特别是字符串对象经常改变的情况下。而在某些特别情况下, String 对象的字符串拼接其实是被 JVM 解释成了 StringBuffer 对象的拼接,所以这些时候 String 对象的速度并不会比 StringBuffer 对象慢,而特别是以下的字符串对象生成中, String 效率是远要比 StringBuffer 快的:
String S1 = “This is only a” + “ simple” + “ test”;
StringBuffer Sb = new StringBuilder(“This is only a”).append(“ simple”).append(“ test”);
你会很惊讶的发现,生成 String S1 对象的速度简直太快了,而这个时候 StringBuffer 居然速度上根本一点都不占优势。其实这是 JVM 的一个把戏,在 JVM 眼里,这个
String S1 = “This is only a” + “ simple” + “test”; 其实就是:
String S1 = “This is only a simple test”; 所以当然不需要太多的时间了。但大家这里要注意的是,如果你的字符串是来自另外的 String 对象的话,速度就没那么快了,譬如:
String S2 = “This is only a”;
String S3 = “ simple”;
String S4 = “ test”;
String S1 = S2 +S3 + S4;
这时候 JVM 会规规矩矩的按照原来的方式去做
在大部分情况下 StringBuffer > String
StringBuffer
Java.lang.StringBuffer线程安全的可变字符序列。一个类似于 String 的字符串缓冲区,但不能修改。虽然在任意时间点上它都包含某种特定的字符序列,但通过某些方法调用可以改变该序列的长度和内容。
可将字符串缓冲区安全地用于多个线程。可以在必要时对这些方法进行同步,因此任意特定实例上的所有操作就好像是以串行顺序发生的,该顺序与所涉及的每个线程进行的方法调用顺序一致。
StringBuffer 上的主要操作是 append 和 insert 方法,可重载这些方法,以接受任意类型的数据。每个方法都能有效地将给定的数据转换成字符串,然后将该字符串的字符追加或插入到字符串缓冲区中。append 方法始终将这些字符添加到缓冲区的末端;而 insert 方法则在指定的点添加字符。
例如,如果 z 引用一个当前内容是“start”的字符串缓冲区对象,则此方法调用 z.append(“le”) 会使字符串缓冲区包含“startle”,而 z.insert(4, “le”) 将更改字符串缓冲区,使之包含“starlet”。
在大部分情况下 StringBuilder > StringBuffer
java.lang.StringBuilder
java.lang.StringBuilder一个可变的字符序列是5.0新增的。此类提供一个与 StringBuffer 兼容的 API,但不保证同步。该类被设计用作 StringBuffer 的一个简易替换,用在字符串缓冲区被单个线程使用的时候(这种情况很普遍)。如果可能,建议优先采用该类,因为在大多数实现中,它比 StringBuffer 要快。两者的方法基本相同。
sleep 是Thread中的方法,线程暂停,让出CPU,但是不释放锁
wait()是Object中的方法, 调用次方法必须让当前线程必须拥有此对象的monitor(即锁),执行之后 线程阻塞,让出CPU, 同时也释放锁; 等待期间不配拥有CPU执行权, 必须调用notify/notifyAll方法唤醒,(notify是随机唤醒) 唤醒并不意味着里面就会执行,而是还是需要等待分配到CPU才会执行;
clone
是浅拷贝;只克隆了自身对象和对象内实例变量的地址引用,使用它需要实现接口Cloneable
;
使用ObjectStream
进行深度克隆; 先将对象序列化;然后再反序列化;
https://blog.csdn.net/JH39456194/article/details/107304997
TL用于保存本地线程的值, 每个Thread都有一个threadLocals属性,它是一个ThreadLocalMap对象,本质上是一个Entry数组;Entry是k-v结构; 并且是WeakReference弱引用, K存的是 ThreadLocal对象,Value是设置的值; 那么每个线程就可以读自己设置的值了;
会发生内存泄漏
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。
key是弱引用好歹还可以 GC掉key的对象;强引用则不行
使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
InheritableThreadLocal基础 ThreadLocal ; 他跟ThreadLocal区别是 可以传递值给子线程; 每个Thread都有一个inheritableThreadLocals属性, 创建子线程的时候,会把父线程的Entry数组 塞到子线程的Entry数组中; 所以就实现了父子线程的值传递; 注意如果Value是一个非基本类型的对象, 父子线程指向的是相同的引用; 子线程如果修改了值,父线程也是会修改的;
线程不安全: 如果说线程本地变量是只读变量不会受到影响,但是如果是可写的,那么任意子线程针对本地变量的修改都会影响到主线程的本地变量
线程池中可能失效: 在使用线程池的时候,ITL会完全失效,因为父线程的TLMap是通过Thread的init方法的时候进行赋值给子线程的,而线程池在执行异步任务时可能不再需要创建新的线程了,因此也就不会再传递父线程的TLMap给子线程了
https://blog.csdn.net/swpu_ocean/article/details/88917958
https://www.bilibili.com/video/BV1n541177Ea?from=search&seid=13908663008727288935
1.在JDK1.7中,当并发执行扩容操作时会造成环形链的情况。(链表的头插法 造成环形链)
在JDK1.7中,当多个线程同时插入元素时,因为使用头插法,会造成数据丢失
2.在JDK1.8中,在并发执行put操作时会发生数据覆盖的情况。(元素插入时使用的是尾插法)
HashMap在put的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容重新hash到新的扩容数组中,在多线程的环境下,存在同时其他的元素也在进行put操作,如果hash值相同,可能出现同时在同一数组下用链表表示,造成闭环,导致在get时会出现死循环,所以HashMap是线程不安全的。
提高检索效率,在链表长度大于8的时候,将后面的数据存在红黑树中,以加快检索速度。复杂度变成O(logn)
ConcurrentHashMap线程安全
可以看出JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,相对而言,ConcurrentHashMap只是增加了同步的操作来控制并发,从JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+红黑树,相对而言
在JDK1.5中,synchronized是性能低效的。因为这是一个重量级操作,它对性能大的影响是阻塞的是实现,挂起 线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性带来了很大的压力
javaSE1.6引入了偏向锁,轻量级锁(自旋锁)后,synchronized和ReentrantLock两者的性能就差不多了
锁可以升级, 但不能降级. 即: 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁是单向的.
偏向锁
偏向锁: HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得; 偏向锁是四种状态中最乐观的一种锁:从始至终只有一个线程请求某一把锁。
偏向锁的获取: 当一个线程访问同步块并成功获取到锁时,会在对象头和栈帧中的锁记录字段里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,直接进入
偏性锁的撤销: 偏向锁使用了一种等待竞争出现才释放锁的机制,所以当其他线程竞争偏向锁时,持有偏向锁的线程才会释放偏向锁,并将锁膨胀为轻量级锁(持有偏向锁的线程依然存活的时候)
轻量级锁
多个线程在不同的时间段请求同一把锁,也就是说没有锁竞争。
加锁: 线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
解锁:轻量级锁解锁时, 会使用原子的CAS操作将当前线程的锁记录替换回到对象头, 如果成功, 表示没有竞争发生; 如果失败, 表示当前锁存在竞争, 锁就会膨胀成重量级锁.
重量级锁
Java线程的阻塞以及唤醒,都是依靠操作系统来完成的,这些操作将涉及系统调用,需要从操作系统 的用户态切换至内核态,其开销非常之大。
锁粗化:
锁粗化就是将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成为一个范围更大的锁
锁消除:
锁消除即删除不必要的加锁操作。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程, 那么可以认为这段代码是线程安全的,不必要加锁
底层实现上来说,synchronized 是JVM层面的锁,是Java关键字,通过monitor对象来完成(monitorenter与monitorexit),对象只有在同步块或同步方法中才能调用wait/notify方法,ReentrantLock 是从jdk1.5以来(java.util.concurrent.locks.Lock)提供的API层面的锁。
synchronized 不需要用户去手动释放锁,synchronized 代码执行完后系统会自动让线程释放对锁的占用; ReentrantLock则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象。一般通过lock()和unlock()方法配合try/finally语句块来完成,使用释放更加灵活。
synchronized是不可中断类型的锁,除非加锁的代码中出现异常或正常执行完成; ReentrantLock则可以中断,可通过trylock(long timeout,TimeUnit unit)设置超时方法或者将lockInterruptibly()放到代码块中,调用interrupt方法进行中断。
synchronized为非公平锁 ReentrantLock则即可以选公平锁也可以选非公平锁
synchronzied锁的是对象,锁是保存在对象头里面的,根据对象头数据来标识是否有线程获得锁/争抢锁;ReentrantLock锁的是线程,根据进入的线程和int类型的state标识锁的获得/争抢。
当对象的equals()方法被重写时,通常有必要重写 hashCode() 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码
如果两个对象相同(即:用 equals 比较返回true),那么它们的 hashCode 值一定要相同
如果两个对象的 hashCode 相同,它们并不一定相同(即:用 equals 比较返回 false
为了提供程序效率 通常会先进性hashcode的比较,如果不同,则就么有必要equals比较了;
我们知道,对象都是有生命周期的,有的长,有的短,如果长生命周期的对象持有短生命周期的引用,就很可能会出现内存泄露
Vector v = new Vector(10);
for (int i = 0; i < 100; i++) {
Object o = new Object();
v.add(o);
o = null;
}
ThreadLocal使用不当也可能泄漏
在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写 - 读内存中的公共状态来隐式进行通信。Java 的并发采用的是共享内存模型
从 JDK5 开始,java 使用新的 JSR -133 内存模型,提出了 happens-before 的概念
前一个操作的结果可以被后续的操作获取。讲白点就是前面一个操作把变量a赋值为1,那后面一个操作肯定能知道a已经变成了1。
可见性:对一个 volatile 变量的读,总是能看到(任意线程)对这个 volatile 变量最后的写入。
禁止重排序:对于一个非原子性的操作,cpu会始终严格按照执行顺序执行volatile 写的内存语义:当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量刷新到主内存
volatile 读的内存语义: 当读一个 volatile 变量时,JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量
在每个 volatile 写操作的前面插入一个 StoreStore 屏障。
在每个 volatile 写操作的后面插入一个 StoreLoad 屏障。
在每个 volatile 读操作的后面插入一个 LoadLoad 屏障。
在每个 volatile 读操作的后面插入一个 LoadStore 屏障。
通过反编译可以看到,有volatile变量修饰的遍历,会有一个lock前缀的指令,lock前缀的指令在多核处理器下会引发了两件事情
将当前处理器缓存行的数据会写回到系统内存。
这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。
根可达算法
roots:线程栈变量 静态变量 常量池 jni指针
垃圾回收算法:
标记清除:标记哪块是垃圾然后直接清楚
拷贝算法:对于一块空间,设置一半可以存放对象,一半禁止访问,当前一半有对象后,将前一半的所有对象移到禁止访问的那一半空间去,就不会出项碎片化
标记压缩:在标记清楚后,将剩余对象压缩在一起,就不会出现碎片化
Minor GC:回收年轻代
Major GC:回收老年代
Full GC:全回收
对象在内存中的移动过程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m82bx8i2-1623576979706)(C:\Users\17939\AppData\Roaming\Typora\typora-user-images\image-20210527141219213.png)]
10种垃圾回收器:(对上述三种算法的排列组合)
主要追对的是 Java堆 和 方法区 ;
java栈、程序计数器、本地方法栈都是线程私有的,线程生就生,线程灭就灭,栈中的栈帧随着方法的结束也会撤销,内存自然就跟着回收了。所以这几个区域的内存分配与回收是确定的,我们不需要管的。但是java堆和方法区则不一样,我们只有在程序运行期间才知道会创建哪些对象,所以这部分内存的分配和回收都是动态的。一般我们所说的垃圾回收也是针对的这一部分。
标记-清除(Mark-sweep):标记清除算法分为两个阶段,标记阶段和清除阶段。标记阶段任务是标记出所有需要回收的对象,清除阶段就是清除被标记对象的空间。优缺点:实现简单,容易产生内存碎片
复制(Copying)将可用内存划分为大小相等的两块,每次只使用其中的一块。当进行垃圾回收的时候了,把其中存活对象全部复制到另外一块中,然后把已使用的内存空间一次清空掉。 优缺点:不容易产生内存碎片;可用内存空间少;存活对象多的话,效率低下。
标记-压缩(Mark-Compact)先标记存活对象,然后把存活对象向一边移动,然后清理掉端边界以外的内存 优缺点:不容易产生内存碎片;内存利用率高;存活对象多并且分散的时候,移动次数多,效率低下
分代收集算法(目前大部分JVM的垃圾收集器所采用的算法)
年轻代(Young Generation)的回收算法 (回收主要以Copying为主)
年老代(Old Generation)的回收算法(回收主要以Mark-Compact为主)
(1)调用System.gc时,系统建议执行Full GC,但是不必然执行
(2)老年代空间不足
(3)方法区(1.8之后改为元空间)空间不足
(4)创建大对象,比如数组,通过Minor GC后,进入老年代的平均大小大于老年代的可用内存(为了避免新生代晋升到老年代失败)
(5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。(就是eden—》s1–>s2–>old都放不下就会full gc)
GC里边在JVM当中是使用的ROOT算法,ROOT算法 也就是根; 只要看这个两个对象有没有挂在 根 上, 挂在根上了 就不会被回收; 没有挂在根上就会回收;
暂时不看
请描述synchronized和reentrantlock的底层实现及重入的底层原理
synchronized是用来保证线程同步,用的锁存在java对象头中,利用monitorenter和monitorexit指令实现,monitorenter指令是在编译后插入到同步代码块开始位置,而monitorexit是插入到方法结束后和异常处。jdk1.6之后引入了大量的优化,这其中又涉及到锁的四种升级状态:new(无锁) →偏向锁→轻量级锁(自旋锁)→重量级锁。而底层,synchrnoized是利用操作系统的Mutex Lock(互斥锁)来实现的
ReentrantLock(可重入锁)是基于AQS的,AQS是Java并发包中众多同步组件的构建基础,它通过一个int类型的状态变量state和一个FIFO队列来完成共享资源的获取,线程的排队等待等
请描述锁的四种状态和升级过程
答:无锁、偏向锁、轻量级锁、重量级锁,new–偏向锁(当第一个线程调用这个对象时就上了这把锁,里面记录了线程的id号)–轻量级锁(当有另一个线程竞争该对象时,升级为轻量级锁)(无锁,自旋锁,自适应自旋)–重量级锁(等待的线程会进入该锁(非公平锁,队列不有序)的等待队列进行wait,不消耗cpu资源,而轻量级锁是要消耗资源的)
CAS的ABA问题如何解决
答:类似乐观锁的做法,给变量加一个版本号,当其他线程修改变量时,版本号++,从原来的对比变量的值改为对比版本号。
请谈一下AQS,为什么AQS的底层是CAS+volatile
请谈一下你对volatile的理解
答:volatile主要有两个作用一个是保证线程可见性,一个是禁止指令重排
volatile的可见性和禁止指令重排序时如何实现的
答:可见性是通过cpu多级缓存,当标识该变量volatile时,线程每次从主存中重新读取该数据。禁止指令重排序是通过添加内存屏障实现。
CAS是什么
答:CAS(Compare and swap)比较和替换是设计并发算法时用到的一种技术。简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替换当前变量的值。
请描述一下对象的创建过程
答:
开辟内存空间,将变量设为默认值
调用构造函数修改变量的值
将变量名(指针)指向该对象。
对象在内存中的内存布局
答:对象头,类型指针,实例数据,对齐。
DCL(double-checked locking)单例为什么要加volatile
答:在DCL中。创建对象的步骤有三步:
如果上述步骤2,3调换,即执行了1,3步骤,此时跑进来一个线程,因为对象不为空则返回该对象,所以该线程就获得了一个未初始化完成的变量。这样便会出问题,所以需要给变量加上volatile关键字。
Object o = new Object() 在内存中占了多少字节
答:16字节
请描述synchronized和ReentrantLock的异同
底层实现上来说,synchronized 是JVM层面的锁,是Java关键字,通过monitor对象来完成(monitorenter与monitorexit),对象只有在同步块或同步方法中才能调用wait/notify方法,ReentrantLock 是从jdk1.5以来(java.util.concurrent.locks.Lock)提供的API层面的锁。
synchronized 不需要用户去手动释放锁,synchronized 代码执行完后系统会自动让线程释放对锁的占用; ReentrantLock则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象。一般通过lock()和unlock()方法配合try/finally语句块来完成,使用释放更加灵活。
synchronized是不可中断类型的锁,除非加锁的代码中出现异常或正常执行完成; ReentrantLock则可以中断,可通过trylock(long timeout,TimeUnit unit)设置超时方法或者将lockInterruptibly()放到代码块中,调用interrupt方法进行中断。
synchronized为非公平锁 ReentrantLock则即可以选公平锁也可以选非公平锁
synchronzied锁的是对象,锁是保存在对象头里面的,根据对象头数据来标识是否有线程获得锁/争抢锁;ReentrantLock锁的是线程,根据进入的线程和int类型的state标识锁的获得/争抢。
聊聊你对as-if-serial和happens-before语义的理解
你了解ThreadLocal吗?你知道ThreadLocal中如何解决内存泄漏问题吗?
答:通过弱引用解决内存泄漏问题,追到源码中去看,会将值设置为Entry类型,而该Eentry继承了弱引用类。
冒泡排序(将最大数冒泡到数组最后)
public class BubbleSort {
public static void BubbleSort(int[] arr) {
int temp;//定义一个临时变量
for (int i = 0; i < arr.length - 1; i++) {//冒泡趟数
for (int j = 0; j < arr.length - i - 1; j++) {
if (arr[j + 1] < arr[j]) {
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
public static void main(String[] args) {
int arr[] = new int[]{1, 6, 2, 2, 5};
BubbleSort(arr);
System.out.println(Arrays.toString(arr));
}
}
选择排序(找出最小的放最前面)
public class SelectionSort {
public static void main(String[] args) {
int[] arr={1,3,2,45,65,33,12};
System.out.println("交换之前:");
for(int num:arr){
System.out.print(num+" ");
}
//选择排序的优化
for(int i = 0; i < arr.length - 1; i++) {// 做第i趟排序
int k = i;
for(int j = k + 1; j < arr.length; j++){// 选最小的记录
if(arr[j] < arr[k]){
k = j; //记下目前找到的最小值所在的位置
}
}
//在内层循环结束,也就是找到本轮循环的最小的数以后,再进行交换
if(i != k){ //交换a[i]和a[k]
int temp = arr[i];
arr[i] = arr[k];
arr[k] = temp;
}
}
System.out.println();
System.out.println("交换后:");
for(int num:arr){
System.out.print(num+" ");
}
}
}
插入排序(将数组分为左右两个数组,左边数组有序,右边无序,将右边数组的第一位一个一个挪到左边的有序数组中)
import java.util.Arrays;
public class InsertSort {
public static void main(String[] args) {
int[] array={12,73,45,69,35};
int i,j,temp;
for(i=1;i<array.length;i++) {
/*
* 第一个for循环
* 把数组分成两部分,右边为未排序,左边为已排序
* 记录排序与未排序分割点temp(temp为下一个排序对象)
*/
temp=array[i];
for(j=i-1;j>=0;j--) {
/*
* 第二个for循环
* 将排序对象temp与已排序数组比较
* 当temp比最近左边的数大时(按从小到大循序排列时)
* 直接结束本次循环,进行下一个数排序
* 否则比左边这个数小时将这个数后移,腾出这个数的位置
*/
if (temp > array[j]) {
break;
}else{
array[j+1] = array[j];
}
}
array[j+1]=temp;
}
System.out.println(Arrays.toString(array));
}
}
希尔排序(对插入排序的改进)
import java.util.Arrays;
public class Shell {
public static void main(String[] args) {
int[] arr = SortTestHelper.getRandomArray(15, 0, 10);
System.out.println("希尔排序前:"+Arrays.toString(arr));
shellSort(arr);
System.out.println("希尔排序后:"+Arrays.toString(arr));
}
/**
* 希尔排序
* @param arr 待排数组
*/
public static void shellSort(int[] arr) {
for(int gap=arr.length/2; gap>0; gap/=2) { /*步长逐渐减小*/
for(int i=gap; i<arr.length; i++) { /*在同一步长内*/
//同一步长内排序方式是插入排序
int temp = arr[i], j; //待排元素
//j-gap代表有序数组中最大数的下标,j-pag表示有序数组的前一个元素,减pag是减去偏移量就是步长
for(j=i; j>=gap && temp<arr[j-gap]; j-=gap)
arr[j] = arr[j-gap]; //原有序数组最大的后移一位
arr[j] = temp; //找到了合适的位置插入
}
}
}
}
class SortTestHelper {
/**
*
* @param n 生成n个元素的随机数组
* @param rangeL 随机范围[rangeL
* @param rangeR rangeR]
* @return 返回一个随机 int 型数组
*/
public static int[] getRandomArray(int n, int rangeL, int rangeR) {
int[] arr = new int[n];
for(int i=0; i<n; i++) {
arr[i] = (int)(Math.random() * (rangeR - rangeL +1)) + rangeL;
}
return arr;
}
}
归并排序(将数组分成两个有序数组并合并)
public class MergeSort {
public static void main(String []args){
int arr[] = {6,1,7,2,4,3,8,5};
sort(arr);
System.out.println(Arrays.toString(arr));
}
public static void sort(int []arr){
int []temp = new int[arr.length];//在排序前,先建好一个长度等于原数组长度的临时数组,避免递归中频繁开辟空间
sort(arr,0,arr.length-1,temp);
}
private static void sort(int[] arr,int left,int right,int[] temp){
if(left<right){
int mid = (left+right)/2;
sort(arr,left,mid,temp);//左边归并排序,使得左子序列有序
sort(arr,mid+1,right,temp);//右边归并排序,使得右子序列有序
merge(arr,left,mid,right,temp);//将两个有序子数组合并操作
}
}
private static void merge(int[] arr,int left,int mid,int right,int[] temp){
int i = left;//左序列指针
int j = mid+1;//右序列指针
int t = 0;//临时数组指针
while (i<=mid && j<=right){
if(arr[i]<=arr[j]){
temp[t++] = arr[i++];
}else {
temp[t++] = arr[j++];
}
}
while(i<=mid){//将左边剩余元素填充进temp中
temp[t++] = arr[i++];
}
while(j<=right){//将右序列剩余元素填充进temp中
temp[t++] = arr[j++];
}
t = 0;
//将temp中的元素全部拷贝到原数组中
while(left <= right){
arr[left++] = temp[t++];
}
}
}
快速排序
import java.util.Arrays;
public class QuickSort {
public static void quickSort(int[] arr,int low,int high){
int i,j,temp,t;
if(low>high){
return;
}
i=low;
j=high;
//temp就是基准位
temp = arr[low];
while (i<j) {
//先看右边,依次往左递减
while (temp<=arr[j]&&i<j) {
j--;
}
//再看左边,依次往右递增
while (temp>=arr[i]&&i<j) {
i++;
}
//如果满足条件则交换
if (i<j) {
t = arr[j];
arr[j] = arr[i];
arr[i] = t;
}
}
//最后将基准为与i和j相等位置的数字交换
arr[low] = arr[i];
arr[i] = temp;
//递归调用左半数组
quickSort(arr, low, j-1);
//递归调用右半数组
quickSort(arr, j+1, high);
}
public static void main(String[] args){
int[] arr = {10,7,2,4,7,62,3,4,2,1,8,9,19};
quickSort(arr, 0, arr.length-1);
System.out.println(Arrays.toString(arr));
}
}
暂时不看
暂时不看
https://www.cnblogs.com/javazhiyin/p/13839357.html
4.0之前是单线程,4.0之后引入了多线程的概念
优点
缺点
假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在数缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!
缓存分为本地缓存和分布式缓存。以 Java 为例,使用自带的 map 或者 guava 实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着 jvm 的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。
使用 redis 或 memcached 之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持 redis 或 memcached服务的高可用,整个程序架构上较为复杂。
为了解决缓存一致性问题。
1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是O(1);
2、数据结构简单,对数据操作也简单,Redis 中的数据结构是专门进行设计的;
3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
4、使用多路 I/O 复用模型,非阻塞 IO;
5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis 直接自己构建了 VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失。
Redis 提供两种持久化机制 RDB(默认) 和 AOF 机制:
RDB:是Redis DataBase缩写快照
RDB是Redis默认的持久化方式。按照一定的时间将内存的数据以快照的形式保存到硬盘中,对应产生的数据文件为dump.rdb。通过配置文件中的save参数来定义快照的周期。
优点:
缺点:
AOF:持久化
AOF持久化(即Append Only File持久化),则是将Redis执行的每次写命令记录到单独的日志文件中,当重启Redis会重新将持久化的日志中文件恢复数据。
当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复。
优点:
缺点:
优缺点是什么?
我们都知道,Redis是key-value数据库,我们可以设置Redis中缓存的key的过期时间。Redis的过期策略就是指当Redis中缓存的key过期了,Redis如何处理。
过期策略通常有以下三种:
Redis中同时使用了惰性过期和定期过期两种过期策略。
EXPIRE和PERSIST命令。expire key 100 (设置该key 100秒过期) persist key(移除该key的过期时间,使得key永不过期)
除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:
两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。
redis内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。
(1)volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰。
(2)volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰。
(3)volatile-random:从已设置过期时间的数据集中任意选择数据淘汰。
(4)volatile-lfu:从已设置过期时间的数据集挑选使用频率最低的数据淘汰。
(5)allkeys-lru:从数据集中挑选最近最少使用的数据淘汰
(6)allkeys-lfu:从数据集中挑选使用频率最低的数据淘汰。
(7)allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
(8) no-enviction(驱逐):禁止驱逐数据,这也是默认策略。意思是当内存不足以容纳新入数据时,新写入操作就会报错,请求可以继续进行,线上任务也不能持续进行,采用no-enviction策略可以保证数据不被丢失。
内存。
如果达到设置的上限,Redis的写命令会返回错误信息(但是读命令还可以正常返回。)或者你可以配置内存淘汰机制,当Redis达到内存上限时会冲刷掉旧的内容。
可以好好利用Hash,list,sorted set,set等集合类型数据,因为通常情况下很多小的Key-Value可以用更紧凑的方式存放到一起。尽可能使用散列表(hashes),散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。比如你的web系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的key,而是应该把这个用户的所有信息存储到一张散列表里面
Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器(file event handler)。它的组成结构为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型。
虽然文件事件处理器以单线程方式运行, 但通过使用 I/O 多路复用程序来监听多个套接字, 文件事件处理器既实现了高性能的网络通信模型, 又可以很好地与 redis 服务器中其他同样以单线程方式运行的模块进行对接, 这保持了 Redis 内部单线程设计的简单性。
事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。
以multi开始,之后的命令将会被加入队列,若在加入队列的过程中,有某一条命令编译错误,当执行exec命令时就会提示事务提交失败,全部回滚。若在编译阶段没有错误,即入队的时候都成功,但在提交事务exec执行的的时候发生错误则不会回滚,该成功的成功,该失败的失败。
乐观锁:(CAS):用watch关键字实现,可以解决秒杀中的超卖问题(会照成库存遗留问题)
redis事务特性:
原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
事务前后数据的完整性必须保持一致。
多个事务并发执行时,一个事务的执行不应影响其他事务的执行
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响
Redis的事务总是具有ACID中的一致性和隔离性,其他特性是不支持的。当服务器运行在_AOF_持久化模式下,并且appendfsync选项的值为always时,事务也具有耐久性。
Redis 是单进程程序,并且它保证在执行事务时,不会对事务进行中断,事务可以运行直到执行完所有事务队列中的命令为止。因此,Redis 的事务是总是带有隔离性的
Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。
哨兵的介绍
sentinel,中文名是哨兵。哨兵是 redis 集群机构中非常重要的一个组件,主要有以下功能:
哨兵用于实现 redis 集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,互相协同工作。
哨兵的核心知识
redis 集群模式的工作原理能说一下么?在集群模式下,redis 的 key 是如何寻址的?分布式寻址都有哪些算法?了解一致性 hash 算法吗?
简介
Redis Cluster是一种服务端Sharding技术,3.0版本开始正式提供。Redis Cluster并没有使用一致性hash,而是采用slot(槽)的概念,一共分成16384个槽。将请求发送到任意节点,接收到请求的节点会将查询请求发送到正确的节点上执行
方案说明
在 redis cluster 架构下,每个 redis 要放开两个端口号,比如一个是 6379,另外一个就是 加1w 的端口号,比如 16379。
16379 端口号是用来进行节点间通信的,也就是 cluster bus 的东西,cluster bus 的通信,用来进行故障检测、配置更新、故障转移授权。cluster bus 用了另外一种二进制的协议,gossip
协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间。
节点间的内部通信机制
基本通信原理
集群元数据的维护有两种方式:集中式、Gossip 协议。redis cluster 节点间采用 gossip 协议进行通信。
分布式寻址算法
优点
缺点
简介
Redis Sharding是Redis Cluster出来之前,业界普遍使用的多Redis实例集群方法。其主要思想是采用哈希算法将Redis数据的key进行散列,通过hash函数,特定的key会映射到特定的Redis节点上。Java redis客户端驱动jedis,支持Redis Sharding功能,即ShardedJedis以及结合缓存池的ShardedJedisPool
优点
缺点
简介
客户端发送请求到一个代理组件,代理解析客户端的数据,并将请求转发至正确的节点,最后将结果回复给客户端
特征
业界开源方案
redis cluster,10 台机器,5 台机器部署了 redis 主实例,另外 5 台机器部署了 redis 的从实例,每个主实例挂了一个从实例,5 个节点对外提供读写服务,每个节点的读写高峰qps可能可以达到每秒 5 万,5 台机器最多是 25 万读写请求/s。
机器是什么配置?32G 内存+ 8 核 CPU + 1T 磁盘,但是分配给 redis 进程的是10g内存,一般线上生产环境,redis 的内存尽量不要超过 10g,超过 10g 可能会有问题。
5 台机器对外提供读写,一共有 50g 内存。
因为每个主实例都挂了一个从实例,所以是高可用的,任何一个主实例宕机,都会自动故障迁移,redis 从实例会自动变成主实例继续提供读写服务。
你往内存里写的是什么数据?每条数据的大小是多少?商品数据,每条数据是 10kb。100 条数据是 1mb,10 万条数据是 1g。常驻内存的是 200 万条商品数据,占用内存是 20g,仅仅不到总内存的 50%。目前高峰期每秒就是 3500 左右的请求量。
其实大型的公司,会有基础架构的 team 负责缓存集群的运维。
Redis集群没有使用一致性hash,而是引入了哈希槽的概念,Redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽。
Redis并不能保证数据的强一致性,这意味这在实际中集群在特定的条件下可能会丢失写操作。
异步复制
16384个
Redis集群目前无法做数据库选择,默认在0数据库。
可以在同一个服务器部署多个Redis的实例,并把他们当作不同的服务器来使用,在某些时候,无论如何一个服务器是不够的, 所以,如果你想使用多个CPU,你可以考虑一下分片(shard)。
分区可以让Redis管理更大的内存,Redis将可以使用所有机器的内存。如果没有分区,你最多只能使用一台机器的内存。分区使Redis的计算能力通过简单地增加计算机得到成倍提升,Redis的网络带宽也会随着计算机和网卡的增加而成倍增长。
https://www.bilibili.com/video/BV1d4411y79Y?p=5&spm_id_from=pageDriver
Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系Redis中可以使用SETNX命令实现分布式锁。
当且仅当 key 不存在,将 key 的值设为 value。若给定的 key 已经存在,则 SETNX 不做任何动作
SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。
返回值:设置成功,返回 1 。设置失败,返回 0 。
使用SETNX完成同步锁的流程及事项如下:
使用SETNX命令获取锁,若返回0(key已存在,锁已存在)则获取失败,反之获取成功
为了防止获取锁后程序出现异常,导致其他线程/进程调用SETNX命令总是返回0而进入死锁状态,需要为该key设置一个“合理”的过期时间
释放锁,使用DEL命令将锁数据删除
所谓 Redis 的并发竞争 Key 的问题也就是多个系统同时对一个 key 进行操作,但是最后执行的顺序和我们期望的顺序不同,这样也就导致了结果的不同!
推荐一种方案:分布式锁(zookeeper 和 redis 都可以实现分布式锁)。(如果不存在 Redis 的并发竞争 Key 问题,不要使用分布式锁,这样会影响性能)
基于zookeeper临时有序节点可以实现的分布式锁。大致思想为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁。
在实践中,当然是从以可靠性为主。所以首推Zookeeper。
参考:https://www.jianshu.com/p/8bddd381de06
既然Redis是如此的轻量(单实例只使用1M内存),为防止以后的扩容,最好的办法就是一开始就启动较多实例。即便你只有一台服务器,你也可以一开始就让Redis以分布式的方式运行,使用分区,在同一台服务器上启动多个实例。
一开始就多设置几个Redis实例,例如32或者64个实例,对大多数用户来说这操作起来可能比较麻烦,但是从长久来看做这点牺牲是值得的。
这样的话,当你的数据不断增长,需要更多的Redis服务器时,你需要做的就是仅仅将Redis实例从一台服务迁移到另外一台服务器而已(而不用考虑重新分区的问题)。一旦你添加了另一台服务器,你需要将你一半的Redis实例从第一台机器迁移到第二台机器。
Redis 官方站提出了一种权威的基于 Redis 实现分布式锁的方式名叫 Redlock,此种方式比原先的单节点的方法更安全。它可以保证以下特性:
缓存雪崩是指缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方案
缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方案
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决方案
缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
解决方案
当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。
缓存降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。
在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:
服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。
热点数据,缓存才有价值
对于冷数据而言,大部分数据可能还没有再次访问到就已经被挤出内存,不仅占用内存,而且价值不大。频繁修改的数据,看情况考虑使用缓存
对于热点数据,比如我们的某IM产品,生日祝福模块,当天的寿星列表,缓存以后可能读取数十万次。再举个例子,某导航产品,我们将导航信息,缓存以后可能读取数百万次。
数据更新前至少读取两次,缓存才有意义。这个是最基本的策略,如果缓存还没有起作用就失效了,那就没有太大价值了。
那存不存在,修改频率很高,但是又不得不考虑缓存的场景呢?有!比如,这个读取接口对数据库的压力很大,但是又是热点数据,这个时候就需要考虑通过缓存手段,减少数据库的压力,比如我们的某助手产品的,点赞数,收藏数,分享数等是非常典型的热点数据,但是又不断变化,此时就需要将数据同步保存到Redis缓存,减少数据库压力。
缓存中的一个Key(比如一个促销商品),在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
解决方案
对缓存查询加锁,如果KEY不存在,就加锁,然后查DB入缓存,然后解锁;其他进程如果发现有锁就等待,然后等解锁后返回数据或者进入DB查询
Spring是一个轻量级的IoC和AOP容器框架。是为Java应用程序提供基础性服务的一套框架,目的是用于简化企业应用程序的开发,它使得开发者只需要关心业务需求。主要包括以下七个模块:
(1)spring属于低侵入式设计,代码的污染极低;
(2)spring的DI机制将对象之间的依赖关系交由框架处理,减低组件的耦合性;
(3)spring提供了AOP技术,支持将一些通用任务,如安全、事务、日志、权限等进行集中式管理,从而提供更好的复用。
(4)spring对于主流的应用框架提供了集成支持。
(1)IOC就是控制反转,指创建对象的控制权转移给Spring框架进行管理,并由Spring根据配置文件去创建实例和管理各个实例之间的依赖关系,对象与对象之间松散耦合,也利于功能的复用。DI依赖注入,和控制反转是同一个概念的不同角度的描述,即 应用程序在运行时依赖IoC容器来动态注入对象需要的外部依赖。
(2)最直观的表达就是,以前创建对象的主动权和时机都是由自己把控的,IOC让对象的创建不用去new了,可以由spring自动生产,使用java的反射机制,根据配置文件在运行时动态的去创建对象以及管理对象,并调用对象的方法的。
(3)Spring的IOC有三种注入方式 :构造器注入、setter方法注入、根据注解注入。
OOP面向对象,允许开发者定义纵向的关系,但并不适用于定义横向的关系,会导致大量代码的重复,而不利于各个模块的重用。
AOP,一般称为面向切面,作为面向对象的一种补充,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为“切面”(Aspect),减少系统中的重复代码,降低了模块间的耦合度,提高系统的可维护性。可用于权限认证、日志、事务处理。
AOP实现的关键在于 代理模式,AOP代理主要分为静态代理和动态代理。静态代理的代表为AspectJ;动态代理则以Spring AOP为代表。
(1)AspectJ是静态代理,也称为编译时增强,AOP框架会在编译阶段生成AOP代理类,并将AspectJ(切面)织入到Java字节码中,运行的时候就是增强之后的AOP对象。
(2)Spring AOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是每次运行时在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。
ApplicatioinContext是BeanFactory的子接口
未解决
不安全,spring没有对bean进行多线程的封装处理
要安全的话,可以改为原型模式,或者使用Threadlocal
Spring的事务管理器是通过线程相关的ThreadLocal来保存数据访问基础设施(也即Connection实例)
A–>B 以下属性仅加在B的@Transactional上
required:A无,B自建,A有,B加入A
supports:A有,B加入A,A无则B也无
mandatory:A有,B加入A,A无抛异常
required-new:A有,A被挂起,B自建,A,B互不影响,B回滚A可能不回滚
not_supported:B以非事务运行
never:B以非事务方式运行,若存在事务抛异常
nested:A没,B开,A有,A为父事务,B为子事务
首先spring的事务是基于aop实现的,通过生成代理对象进行增强操作。
开启自动装配,需要在xml文件中定义“autowire”属性
去看雷神源码
使用spring+springmvc构建项目时,如果需要引入mybatis等框架,需要到xml中定义mybatis的bean
starter就是定义了一个starter的jar包,写了一个@Configuration配置类,将这些bean定义在里面,然后再starter包的META-INF/spring-factories中写入给配置类的全路径,springboot会按照约定来加载该配置类,完成自动配置