秋招Java后端面试经验总结
秋招了好久,找工作确实很难。我先说一些我的建议吧:
- 找工作确实要多看面经,我醒悟这点有些晚了,所以之前在拿大厂的面试当经验,吃了很多亏,希望看到我这个帖子的朋友都重视一些,牛客这个平台的确很棒,写这篇也是回馈牛友吧。
- 我觉得在平时的学习中,一个比较重要的东西就是:平时查资料,搜问题使用比较好的搜索引擎,搜出来的结果差异太大了。如果用谷歌、多吉搜索经常会是‘掘金’,‘开源中国’,‘个人博客’,‘简书’等等含金量较高的文章,学习起来深度和广度都较为全面。所以我的默认搜索引擎是谷歌,但是如果用一些不太好的搜索引擎,往往搜出的结果不能让你满意。
说了经验,下面说一些具体的面经吧:(有些公司面了,但是没有写,因为面的太少,所以直接放到下面的笔记中了,大家可以自行查阅)
58 转转 (当时没有记录,所以不全有些忘了)
一面:
JVM 多线程 线程池 锁
二面:
项目主要的技术 ES ActiveMQ
hr 面:
有三个月试用期可以接受吗
北森云计算:
一面:
项目中遇到的问题 解决 list,set的区别 两个Integer比较大小比较 局部变量在什么位置(是线程共享还是独享的) 调整栈大小的JVM参数 IO流,BIO,NIO 数据库默认隔离级别,以及它们的问题 HTTP头部报文 反射实现 CPU 占用过高怎么解决
二面:
线程池的实现 一致性哈希算法 虚拟节点作用:两点 COW Array 如何线程安全 线程池如何实现 缓存一致性的解决 Reentratelock 实现 递归和动态规划关系 链表优化:跳表 深克隆浅克隆区别
三面:
项目用到的技术 match 查询 query 查询区别 ES 实现原理 MQ讲一讲 Redis和数据库的数据一致性 HashMap 查询时间复杂度,有冲突呢 HashCode 函数设计 如何用HashMap 加锁 保证线程安全,不可以用JUC下类 CPU 100%排查 接口调用超时排查
hr面:
哪里人,对地区有没有要求 收到的 offer 有哪些 可能要转语言,你怎么看 反思:你行动的慢了 开始的慢,总结问题不够 行动晚 提前想 hr 建议:有什么要求就说,尤其是在企业,不说没人知道。
奇安信:(服务端开发)
一面: http 和 https 的区别 栈和队列区别 红黑树特性 数据库的隔离级别,事务特点 行锁和表锁 乐观锁悲观锁 乐观锁的实现,通过版本号 了解的设计模式 几种线程池介绍一下 单例模式特点 饿汉模式线程安全吗,为什么(多线程安全,但是反射,序列化不安全) linux 常用命令 查看运行的日志用什么指令 tail -f springboot 常用注解 @Value @Resource 使用 get post区别(post为什么更安全,因为参数没有以明文在请求行中) cookie session 区别 Redis 的持久化 Kafka 了解吗 gc机制 面试官: 如果有二面希望你好好准备一下
二面:
Redis 讲一讲持久化 Redis 的数据类型除了set list 那些还有什么 缓存不够了,怎么办 数据库的一张表可以存多大数据,一百万数据的一张表有多大 数据库的慢查询你怎么排查 HashMap 的插入时间复杂度 给一个栈除了出栈入栈操作加入指向最大值的操作,要最大值的时间复杂度是O(1) volatile synchronized 和 lock 体系锁区别 SpringMVC 的流程 怎么保证一个TCP连接的唯一 ES 得到集群健康度的查询 spring boot 的优势 给文件让服务器加载,用springboot 怎么做(初始化时候加载) 设计一个高并发的系统(我说使用并发高的框架+池化的技术(降低资源消耗)+保证最终一致性 大家也可以在讨论区说一下自己的看法)
hr面:
项目遇到的问题 解决 目前offer 参加的比赛问了一下 你平时怎么学习的 你有什么问题
你以为完了吗? 下面是我的一些知识的总结+面试其他公司的面经(其中就有大厂的面经)+其他人的面经!
1.注解@Controller@Service@Component@Repository区别
在目前的 Spring 版本中,这 3 个注释和 @Component 是等效的,但是从注释类的命名上,很容易看出这 3 个注释分别和持久层、业务层和控制层(Web 层)相对应。
虽然目前这 3 个注释和 @Component 相比没有什么新意,但 Spring 将在以后的版本中为它们添加特殊的功能。
所以,如果Web 应用程序采用了经典的三层分层结构的话,最好在持久层、业务层和控制层分别采用 @Repository、@Service 和 @Controller 对分层中的类进行注释,而用 @Component 对那些比较中立的类进行注释。
@Controller@Service@Repository 的底层源码是一模一样的,并且都使用了 @Component ,以 @Controller 为例看一段注解源码
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Component public @interface Controller { @AliasFor(annotation = Component.class) String value() default ""; }
@Component 源码
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Indexed public @interface Component { String value() default ""; }
Java 面试题
2.死锁产生的原因和解决
① 产生死锁的四个必要条件
- 互斥条件:一个资源每次只能被一个进程使用
- 请求与保持条件:一个进程因为请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
死锁的产生和解锁
②操作系统中定义:死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于一种永久等待的状态。
③ 在数据库中避免死锁的条件
- 按照同一顺序访问 避免出现循环
- 避免事务中的用户交互 减少资源持有的时间
- 保持事务简短并处于一个批处理中 减少资源持有的时间
- 保持较低的隔离级别 使用较低的事务隔离级别,例如已提交读比可串行化持有共享锁的时间更短,减少锁竞争
- 使用基于行版本的事务隔离级别
- 使用绑定连接
④ 数据库死锁的处理方法
先查看哪个spid 处于wait 状态,然后用 kill spid 来杀死这个进程
⑤ Java如果有代码发生死锁,则使用jstack 查看死锁线程id,或者在jdk目录下使用控制台输入jconsole 线程查看器,去查看死锁的id,然后杀死这个进程。
3.Synchronized与ReentrentLock性能比较
Synchronized与 Lock区别:
- ==构成层面==Synchronized 为 JVM 层面的,是Java 的关键字 ,Lock 属于API 层面的锁 ,是jdk 1.5之后出现的。Synchronized 底层为 monitor 对象,只有在 Synchronized 块中可以调用 wait / notify 等方法。汇编层面的 synchronized 是 monitorenter 进入,monitorexit 退出锁
- ==使用方法==:Synchronized 不需要手动释放锁,程序执行完成会自动释放锁,而 ReentrentLock 需要手动释放锁,若没有释放,有可能导致程序出现死锁。
- ==等待是否可以中断== Synchronized 不会被中断,除非程序运行完成或者抛出异常,否则不会中断。而ReentrentLock 可以中断
interrupt()
- ==加锁是否公平== Synchronized 非公平、ReentrentLock 默认非公平,但是可以设置公平锁。
- ==锁绑定多个条件 Condition== ReentrentLock 用来实现分组需要唤醒的线程,可以精确唤醒,而不是像 synchronized 那样要么随机唤醒,要么全部唤醒
Synchronized与ReentrantLock
相同点:synchronized 和 ReentrantLock 是非公平的可重入锁,
可重入锁:同一个线程外层函数获得锁之后,内层递归函数任然能够获取该锁的代码。线程可以进入一个它已经拥有的锁所同步的代码块。可重入锁的最大作用是避免死锁。
同理,不可重入锁是一种独占的锁,排它的锁,必须先释放锁才可以在进行加锁,称为不可重入锁。也叫自旋锁。 (可重入锁也叫递归锁)
JDK5 新添加了一种ReentrantLock,在次之前只有synchronized,和 volatile ,ReentrantLock 提供了更多的功能
public interface Lock { void lock(); void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tryLock(long time, TimeUnit unit) throws InterruptedException; void unlock(); Condition newCondition(); }
// ReentrantLock 是 Lock 接口的实现类。
- 方法具体功能
- lockInterruptibly() 获取锁时可以中断
- tryLock() 可轮询避免死锁
- tryLock(long time, TimeUnit unit)可定时,可中断
- unlock() 释放锁
volatie 的特点:
- 可见性
- 不保证原子性 (但是在特殊情况:在读64位数据(long, double)的时候是原子性的。)
- 禁止指令重排序
随着内置锁不断的优化,synchronized 效率高于ReentrantLock 。
- 偏向锁/轻量锁/内置锁/重量级锁 是锁的三种状态,并且针对于Synchronized。在 JDK5中引入锁升级来实现高效的synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
- 偏向锁是指一段同一代码块一直被同一个线程访问,那么该线程会自动获取锁,降低获取锁的代价。
- 轻量级锁是指锁是偏向锁时,被另一个线程访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式获取锁,不会阻塞,提高性能。
- 重量级锁是指,锁是轻量级锁时,另一个线程虽然是自旋,但是自旋并不会持续下去,当自旋到一定次数的时候,(到底多少次?)还没有获取到锁,就会进入阻塞态,该锁升级为重量级锁,重量级锁会让其他申请的线程进入阻塞,性能降低。
公平性
ReentrantLock 提供公平和非公平锁,非公平锁性能高是因为它允许“插队”,提高了吞吐量
Doug Lea 大神更推荐synchronized , 使用方便性更好,当synchronized 无法满足你的需求时,在考虑 ReentrantLock
深入浅出synchronized
JUC 锁之公平锁
new 对象
别人的面经
1. JDK8中Arrays.sort底层原理及其排序算法的选择
2. map如何保证插入时的数据顺序,如何保证线程安全?
使用 LinkedHashMap,使插入有序
而要让map线程安全,可以有三种方法:
- 首先知道HashMap 是线程不安全的
- HashTable
方法级锁,get/put 方法都加入了synchronized 的关键字,它虽然保证的线程安全,但是效率低下,相当于把多线程的降低为单线程了。并且不可以同时 get/put 不推荐使用 - Collections.synchronized(new HashMap())
它相当于给 HashMap 加了synchronized 关键字,也不推荐使用 - ConcurrentHashMap
推荐使用,在jdk 1.7 之前是使用了分段锁技术(也就是局部加锁),每次加锁只锁一个桶,因为有16 个桶,所以多线程下最多保证16 个线程同时使用。
在jdk 8 之后加入了红黑树和CAS 等来实现 ,put 时给Node 加synchronized。
- HashTable
- 细谈ConcurrentHashMap 是如何保证线程安全的
在jdk 7 中ConcurrentHashMap定位一个元素的过程需要进行两次Hash操作。第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部。所以jdk 7 时ConcurrentHashMap 的缺陷是Hash 的时间长度要长于HashMap ,但是处理并发能力大大增强。
在jdk 8 中,ConcurrentHashMap 使用了大量的 CAS 操作
内部类Node 四个值 hash key val next,val和next使用volatile 保证并发下的可见性
class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; volatile V val; volatile Node<K,V> next; //... 省略部分代码 }
jdk 8 下的ConcurrentHashMap在判断如果是第一个元素插入的时候用CAS 保证线程安全,之后的插入则用 synchronized 保证安全 。 锁的粒度为对每个数组元素加锁(Node)。锁的粒度减小了。 并且在有冲突时使用红黑树的查询效率为 O(log k) 高于jdk 之前的 O(k)
k是冲突节点个数,在不冲突时查询的时间复杂度O(1)
3. 在Linux下哪个指令可以查看当前目录下占用空间最大的文件目录?
du -sh *
查看根目录下每个文件夹的大小
4.Linux下动态库(.so)和静态库(.a) 的区别
静态库在程序编译时连接到目标代码中,程序运行时不在需要该静态库。编译之后程序文件大,但加载快,隔离性也好
动态库在编译时并不会被连接到目标代码,而是在程序运行时被载入,因此程序还需要动态库。多个应用程序可以使用同一个动态库。启动应用程序,只需要将动态库加载到内存一次即可。
面经
String、 StringBuffer 、 StringBuilder
String 、StringBuffer 、 StringBuilder 的底层实现是 char [],
但是String 的底层是 final 修饰的字节数组,属于不可变对象。给String 对象 +" " 属于重新分配对象。
StringBuffer 是线程安全的适合作为经常变动的字符串类(线程安全是被 synchronized 修饰)。
而StringBuilder 是去掉线程安全相关的修饰。StringBuilder 和
StringBuffer 都是可变长的动态数组,它们的初始长度为 16 ,扩容为 len * 2 + 2 。
CAS 是什么 ?
CAS : compare and swap 比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,如果不同继续比较直到主内存中的值和工作内存的值一致为止。
也就是比较并交换,是一种乐观锁,通过某种方式不加锁来处理资源,比如通过给记录加 version 来获取数据。
内存位置(V)、预期原值(A)和新值(B)。如果内存地址 V 里面的值和A的值是一样的,那么就将内存里面的值更新成B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。
CAS 底层: 自旋锁 UnSafe 类(是CAS 的核心类)
- CAS 通过UnSafe 类保证原子性,UnSafe 类是 jdk 自身在 rt.jar 就携带的类, rt.sun.misc.UnSafe
UnSafe 类所有的方法都是Native 的,其内部的操作可以像指针一样直接操作内存。是最底层的。所以原子类即使不使用锁,执行i++ 也保证了原子性。
public final int getAddIncrement() { return unsafe.getAndAddInt(this,valueOffset,1); //2. valueOffset 是变量在内存的偏移地址 }
- 变量 value 用 volatile 修饰,保证了多线程之间的可见性
compare and swap 是一条CPU 并发原语,原语执行必须是连续的,并且不可以被中断,也就是说 CAS 是一条 CPU 原子指令,不会造成数据不一致问题 。
A:
==atomicInteger.getAndIncrement();==
B:
public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); }
C:
自旋锁 :尝试获取锁的线程不会立即阻塞,而是采用循环的方式获取锁,减少了线程上下文切换的消耗,缺点是循环会消耗 CPU
public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); // 如果var1 == var5 那么给 var5 加上var4 并且返回true, // 在while 的判断条件中为!true ,跳出循环,函数return var5 return var5; }
A-->B-->C
所以说为什么用 CAS 而不使用 synchronized ?
使用synchronized 加锁,同一时间段只允许有一个线程访问,一致性得到保证,但是并发性下降。但是 CAS 没有加锁,可以不断循环判断直到访问成功为止。 既保证了一致性,又提高了并发性。
CAS 的缺点
- CAS 如果长时间一直不成功,会给 CPU 带来较大的开销
- 一个共享变量,可以使用 CAS 提高效率,但是多个共享变量操作时,CAS 就无法保证原子性了,这时就需要使用加锁。
- ABA 问题: 两个线程同时对一个共享变量a做修改,但是1线程慢,2 号线程快,当 2线程将a 改变为b ,之后又改变为a ,1线程发现期望值和 真实值都是 a ,于是执行它的操作,以为共享变量没有发生改变,尽管1 线程的 CAS 成功,但并不代表整个过程没有问题 。
==原子引用 AtomicReference<v>可以对特殊类型用泛型封装,做原子操作== </v>
解决 ABA 问题
解决ABA 问题,就是给变量加一个版本号,或者说时间戳。每修改一次变量版本号自增一次 使用 JUC 自带的类:AtomicStampedReference<t> </t>
public class ABAdemo { /** 真实值 */ static AtomicReference<Integer> atomicReference = new AtomicReference <>(100); /** 真实值 版本号 */ static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference <>(100,1); public static void main(String[] args) { /** ABA 问题 */ new Thread(() -> { /** 期望值 要修改的值 */ atomicReference.compareAndSet(100, 101); // B atomicReference.compareAndSet(101, 100); // A }, "t1").start(); new Thread(() -> { /** 睡1秒 让t1 线程完成了 ABA */ try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get()); }, "t2").start(); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("=====================以下是 ABA 的解决============================================"); new Thread(() -> { /** 获取版本号 */ int stamp = atomicStampedReference.getStamp(); System.out.println(Thread.currentThread().getName()+"\t 第一次版本号:"+atomicStampedReference.getStamp()); /** 睡 1秒,让 t4 线程也拿到 atomicStampedReference */ try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } /** atomicStampedReference 的compareAndSet 四个参数: 期望值,想要修改的值,现在版本号,修改之后的版本号 */ atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1); System.out.println(Thread.currentThread().getName()+"\t 第二次版本号:"+atomicStampedReference.getStamp()); atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1); System.out.println(Thread.currentThread().getName()+"\t 第三次版本号:"+atomicStampedReference.getStamp()); }, "t3").start(); new Thread(() -> { int stamp = atomicStampedReference.getStamp(); System.out.println(Thread.currentThread().getName()+"\t 第一次版本号:"+atomicStampedReference.getStamp()); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } boolean b = atomicStampedReference.compareAndSet(100,2019,stamp,stamp+1); System.out.println(Thread.currentThread().getName()+" 修改成功否:"+b); System.out.println(Thread.currentThread().getName()+"以为的版本号 :"+stamp+" 当前最新的版本号 :"+atomicStampedReference.getStamp()); System.out.println(" 当前实际最新的值 :" +atomicStampedReference.getReference()); }, "t4").start(); } }
集合
1. 为什么 ArrayList 线程不安全
30 个线程同时访问 ArrayList
List<String> list = new ArrayList <>(); for (int i = 0; i <30 ; i++) { new Thread(()->{ list.add(UUID.randomUUID().toString().substring(0,8)); System.out.println(list); },String.valueOf(i)).start(); }
==出现了 java.util.ConcurrentModificationException==
怎么解决?
- 用 Vector ,线程安全的集合 但是性能下降,不建议使用
- 用 Collections.synchronizedList(new ArrayList<>());
- 用
new CopyOnWriteArrayList<>
CopyOnWriteArrayList 写时复制 读写分离的思想 ,add 方法;
public boolean add(E e) { final ReentrantLock lock = this.lock; lock.lock(); // 开启锁 try { Object[] elements = getArray(); int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len + 1); // 复制且长度加 1 newElements[len] = e; // 赋值 setArray(newElements); return true; } finally { lock.unlock(); }
只在写时加锁,读时无锁,如果在读写并发,读:读取原数组,写:加锁写数组副本
2. set
public HashSet() { map = new HashMap<>(); }
HashSet 底层使用 HashMap 的键
3. map
线程安全的map
- 加锁
- 用 Collections.synchronizedHashMap(new HashMap());
- 用
new ConCurrentHashMap<>()
ConCurrentHashMap 前面已经介绍过为什么线程安全了(CAS+synchronized)锁
Lock lock = new ReentrantLock();
源码 :
// This is equivalent to using {@code ReentrantLock(false)}. public ReentrantLock() { sync = new NonfairSync(); // 无参的ReentrantLock 默认非公平锁 }
Lock lock = new ReentrantLock(true);
源码:
public ReentrantLock(boolean fair) { // 公平锁 非公平锁 sync = fair ? new FairSync() : new NonfairSync(); }
公平锁和非公平锁的区别 :
- 公平锁在并发情况下,每个线程获取锁会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个就占有锁,否则就会加入等待队列,以后按照 FIFO 规则取到自己
- 非公平锁 比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再次采用类似公平锁的方式. 非公平锁的优点在于吞吐量比公平锁大
synchronized 也是非公平锁
可重入锁demo:
class phone implements Runnable{ public synchronized void sendems(){ System.out.println(Thread.currentThread().getId()+"\t sendems"); sendeEmail(); } public synchronized void sendeEmail(){ System.out.println(Thread.currentThread().getId()+"\t sendeEmail"); } Lock lock = new ReentrantLock(); public void run() { get(); } public void get(){ lock.lock(); try { System.out.println(Thread.currentThread().getId()+"\t get"); set(); }finally { lock.unlock(); } } public void set(){ lock.lock(); try { System.out.println(Thread.currentThread().getId()+"\t ##$$%%set"); }finally { lock.unlock(); } } } public class RentarDemo { public static void main(String[] args) { phone ph = new phone(); new Thread(()->{ ph.sendems(); },"t1").start(); new Thread(()->{ ph.sendems(); },"t2").start(); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("\t"+"\t"+"\t"); Thread t3 = new Thread(ph,"t3"); Thread t4 = new Thread(ph,"t4"); t3.start();t4.start(); } }
output :
11 sendems 11 sendeEmail 12 sendems 12 sendeEmail 13 get 13 set 14 get 14 set
==手写自旋锁==
public class ZIXUANLOCK { /** 原子引用线程 */ AtomicReference<Thread> atomicReference = new AtomicReference <>(); public void myLock() { Thread t1 = Thread.currentThread(); System.out.println(Thread.currentThread().getName()+"\t come in "); while (!atomicReference.compareAndSet(null,t1)){ } } public void myUnlock(){ Thread t1 = Thread.currentThread(); atomicReference.compareAndSet(t1,null); System.out.println(Thread.currentThread().getName()+"\t myUnlock "); } public static void main(String[] args) { ZIXUANLOCK spinLockDemo = new ZIXUANLOCK(); new Thread(()->{ spinLockDemo.myLock(); try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } spinLockDemo.myUnlock(); },"A").start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(()->{ spinLockDemo.myLock(); // your things spinLockDemo.myUnlock(); },"B").start(); } }
==main中 等待1秒,所以A线程先执行,获取到锁,然后去myLock ,发现初次为 null ,将一个线程给了 atomicReference ,此时要等5秒,而线程B
也执行了myLock ,然后因为比较并交换时atomicReference 不为null 所以一直循环等待,5秒后A执行myUnlock, atomicReference 置为 null,此时B
线程检测到atomicReference 为 null ,拿到锁,执行任务,释放锁,程序结束==
手写读写锁demo:
ReentrantReadWriteLock
/** 读写锁,写(put)的时候只允许有一个线程,但是读 (get)的时候可以有多个线程 */ class MyCache{ private volatile Map<String,Object> map = new HashMap <>(); private ReentrantReadWriteLock rowlock = new ReentrantReadWriteLock(); public void put(String key,Object value){ rowlock.writeLock().lock(); try { System.out.println(Thread.currentThread().getName()+"\t 正在写入 :"+key); try { TimeUnit.SECONDS.sleep(3 ); } catch (InterruptedException e) { e.printStackTrace(); } map.put(key,value); System.out.println(Thread.currentThread().getName()+"\t 写入完成"); } catch (Exception e) { e.printStackTrace(); } finally { rowlock.writeLock().unlock(); } } public void get(String key ){ rowlock.readLock().lock(); try { System.out.println(Thread.currentThread().getName()+"\t 正在读取 " ); try { TimeUnit.SECONDS.sleep(3 ); } catch (InterruptedException e) { e.printStackTrace(); } Object o = map.get(key); System.out.println(Thread.currentThread().getName()+"\t 读取完成"+o); } catch (Exception e) { e.printStackTrace(); } finally { rowlock.readLock().unlock(); } } public void clear(){ map.clear(); } } public class ReadWriteLockDemo { public static void main(String[] args) { MyCache myCache = new MyCache(); for (int i = 0; i <5 ; i++) { final int temp = i ; new Thread(()->{ myCache.put(temp+"",temp+""); },String.valueOf(i)).start(); } for (int i = 0; i <5 ; i++) { final int temp = i ; new Thread(()->{ myCache.get(temp+"" ); },String.valueOf(i)).start(); } } }
CountDownLatch
让某个线程等待其他线程执行完毕之后在执行的工具 ,做减法 减到0 再执行
public static void main(String[] args) throws Exception{ CountDownLatch countDownLatch = new CountDownLatch(6); for (int i = 0; i <6 ; i++) { new Thread(()->{ System.out.println(Thread.currentThread().getName()+"\t 上完自习 离开教室"); countDownLatch.countDown(); },String.valueOf(i)).start(); } countDownLatch.await(); // 让计数的值减到0 才可以继续往下执行 System.out.println("班长锁门,最后离开教室"); }
output:
3 上完自习 离开教室
2 上完自习 离开教室
1 上完自习 离开教室
5 上完自习 离开教室
6 上完自习 离开教室
4 上完自习 离开教室
班长锁门,最后离开教室
枚举可以当作小型数据库来使用,很实用
public class CDLM { public static void main(String[] args) throws Exception{ CountDownLatch countDownLatch = new CountDownLatch(6); for (int i = 1; i <=6 ; i++) { new Thread(()->{ System.out.println(Thread.currentThread().getName()+"\t 国被灭"); countDownLatch.countDown(); },CountryEnum.forEachEnum(i).getRetMessage()).start(); } countDownLatch.await(); System.out.println(" 秦国实现大一统 "); } } public enum CountryEnum { ONE(1,"齐"),TWO(2,"楚"),THREE(3,"燕"),FOUR(4,"赵"),FIVE(5,"魏"),SIX(6,"韩"); @Data private Integer retCode; @Data private String retMessage; public static CountryEnum forEachEnum(int index){ CountryEnum [] countryEnums = CountryEnum.values(); for (CountryEnum cEnum:countryEnums) { if (index==cEnum.getRetCode()) return cEnum; } return null; } }
CyclicBarrier
做加法,加到规定数目才可以执行程序
public static void main(String[] args) { CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{ System.out.println(" 召唤神龙 "); }); for (int i = 1; i <=7 ; i++) { final int trmp = i ; new Thread(()->{ System.out.println("收集到第"+ trmp +"颗龙珠"); try { cyclicBarrier.await(); } catch (Exception e) { e.printStackTrace(); } },trmp+"").start(); } }
收集到第1颗龙珠 收集到第3颗龙珠 收集到第4颗龙珠 收集到第2颗龙珠 收集到第5颗龙珠 收集到第6颗龙珠 收集到第7颗龙珠 召唤神龙
CyclicBarrier和CountDownLatch的区别:
1.CountDownLatch减计数,CyclicBarrier加计数。
2.CountDownLatch是一次性的,CyclicBarrier可以重用。
Semaphore
多个线程抢夺多个资源 类似于秒杀
public class SemaphoreDemo { public static void main(String[] args) { /** 模拟三个停车位 */ Semaphore semaphore = new Semaphore(3); /** 模拟6 辆车 */ for (int i = 1; i <=6 ; i++) { new Thread(() -> { try { semaphore.acquire(); /** 代表占到车位了 */ System.out.println(Thread.currentThread().getName()+"\t 抢到车位了! "); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"\t 停车三秒后离开车位 "); } catch (InterruptedException e) { e.printStackTrace(); }finally { semaphore.release(); /** 释放资源 */ } }, String.valueOf(i)).start(); } } }
阻塞队列
队列为空,从队列获取元素会被阻塞;队列已满,向对列添加元素会被阻塞
==七大阻塞队列==
- ArrayBlockQueue 数组构成的有界阻塞队列
- LinkedBlockQueue 由链表构成的阻塞队列 (界限为Interger.MAX_VALUE)
- PriorityBlockQueue 支持优先级的排序的无界阻塞队列
- DelayQueue 使用优先级队列实现的延迟无界阻塞队列
- SynchrousQueue 单元素的队列
- LinkedTransferQueue 由链表结构构成的无界阻塞队列
- LinkedBlockingDeque 由链表构成的双向阻塞队列
Queue -> BlockingQueue -> 七个阻塞队列
Exception:
- java.lang.IllegalStateException: Queue full 对满
- java.util.NoSuchElementException 队列空异常
blockingQueue.add("c") ; blockingQueue.offer("e"); blockingQueue.remove(); blockingQueue.poll(); add 和 remove 会抛出异常, 但offer 和 poll 不会抛出异常 如果offer 添加失败会返回false 如果poll 移除失败则返回 null
public class SynchrousQueueDemo { public static void main(String[] args) { SynchronousQueue<String> synchrousQueue = new SynchronousQueue<>(); new Thread(()->{ try { System.out.println(Thread.currentThread().getName()+"\t put 1"); synchrousQueue.put("1"); /** 很神奇的特性,要是队列中的消息没有被消费掉,程序不会往下执行,或者说 * 会在这里加一个指针,等到队列中消息被消费后才会继续执行这段程序 */ System.out.println(Thread.currentThread().getName()+"\t put 2"); synchrousQueue.put("2"); System.out.println(Thread.currentThread().getName()+"\t put 3"); synchrousQueue.put("3"); } catch (InterruptedException e) { e.printStackTrace(); } },"TTT").start(); new Thread(()->{ try { TimeUnit.SECONDS.sleep(3); System.out.println(Thread.currentThread().getName()+"\t take"+ synchrousQueue.take()); TimeUnit.SECONDS.sleep(3); System.out.println(Thread.currentThread().getName()+"\t take"+ synchrousQueue.take()); TimeUnit.SECONDS.sleep(3); System.out.println(Thread.currentThread().getName()+"\t take"+synchrousQueue.take()); } catch (InterruptedException e) { e.printStackTrace(); } },"VVV").start(); } }
多线程的判断都要用 while 判断,而不能用 if 判断
Callable 和 Runnable
回顾一下实现多线程的方法有四种:
实现Runnable 接口
继承 Thread 类
线程池
实现Callable 接口
==那么实现 Runnable 接口 和Callable 接口 有什么区别呢?==
- 源码和API 层面
- Runnable 是lang 包下就自带的多线程实现接口,并且没有抛出异常,没有返回值。
- 而Callable 是JUC 包下的一个泛型类,抛出了异常。有返回值,返回值是Callable 的泛型。当有线程出错有返回值的时候就比较适用,可以自定义按照返回值设定出错的类型。Runnable 的接口实现的方法是 run() ,而Callable 实现的方法是 call()
- Runnable 是lang 包下就自带的多线程实现接口,并且没有抛出异常,没有返回值。
- 在业务层面
- 如果是Runnable 执行线程的任务,如果中间有某个线程过于耗时,会被阻塞。而Callable 不会去阻塞,它会单独将这个过于耗时的重开一个线程去执行,和其他不太耗时的任务并行执行
Callable 的使用要通过一个适配器类: FutureTask<t> ,这个类的构造器既有Callable 接口,又实现了 Runnable 接口</t>
多个线程要启动FutureTask 只会启动一次,如果要启动多次,需要多个 FutureTask
demo:
import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; class MyThread2 implements Callable<Integer> { public Integer call() throws Exception { TimeUnit.SECONDS.sleep(2); System.out.println(" call "); return 1024; } } public class CallableDemo { public static void main(String[] args) throws ExecutionException, InterruptedException { FutureTask<Integer> futureTask = new FutureTask<>(new MyThread2()); Thread thread = new Thread(futureTask,"ABC"); Thread thread2 = new Thread(futureTask,"ABC"); thread2.start(); thread.start(); System.out.println(Thread.currentThread().getName()+" *** "); int res2 = 100 ; System.out.println(futureTask.get()+res2); } }
死锁编码以及解决
死锁是指多个线程因为抢夺资源而造成的互相等待的过程,若无外力干涉,它们都将无法推进下去。
死锁demo:
class HoldLock implements Runnable{ private String lockA; private String lockB; public HoldLock(String lockA, String lockB) { this.lockA = lockA; this.lockB = lockB; } public void run() { synchronized (lockA){ System.out.println(Thread.currentThread().getName()+"\t 持有A 尝试获得:"+lockB); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lockB){ System.out.println(Thread.currentThread().getName()+"\t 持有B 尝试获得:"+lockA); } } } } public class DeadLock { public static void main(String[] args) { String lockA = "lockA"; String lockB = "lockB"; new Thread(new HoldLock(lockA,lockB),"aaa").start(); new Thread(new HoldLock(lockB,lockA),"bbb").start(); } }
死锁排查:
- 程序运行之后一直卡住,有可能是死循环、有可能是死锁、有可能是其他情况,如何确定死锁:
- 在 Terminal 中执行
jps -l
查看进行的java 进程
jps -l 1984 org.jetbrains.jps.cmdline.Launcher 3376 sun.tools.jps.Jps 11860 多线程.demoB.DeadLock 6232
- 知道程序进程号是 11860 ,执行
jstack 11860
打印日志如下"bbb": at 多线???.demoB.HoldLock.run(DeadLock.java:24) - waiting to lock <0x00000000eb30e578> (a java.lang.String) - locked <0x00000000eb30e5b0> (a java.lang.String) at java.lang.Thread.run(Thread.java:748) "aaa": at 多线程.demoB.HoldLock.run(DeadLock.java:24) - waiting to lock <0x00000000eb30e5b0> (a java.lang.String) - locked <0x00000000eb30e578> (a java.lang.String) at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock.
4. ==Found 1 deadlock.== 由此可知程序出现死锁
JVM 和 GC
回顾一下:
gc 的作用域 :方法区 、 堆区
判断对象是否死亡:
引用计数法 无法解决循环引用一般不使用
可达性分析算法
常见的垃圾回收算法 :
复制算法
标记清除
标记整理
分代收集算法
- 垃圾是什么 :
- GCRoot对象作为引用链的更访问不到的为不可达对象,也就是垃圾
- 哪些可以是GCRoot的对象?
- 虚拟机栈
- 方法区中的类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中 JNI(Native 方法) 引用的对象
JVM 参数类型
- 标准参数
- java -version
- java -help
- java -showverison
- X 参数
- -Xint 解释执行
- -Xcomp 第一次使用就编译成本地代码
- -Xmixed 混合模式 (先编译,后执行)
- ==XX 参数==
- Boolean 类型
- -XX:+PrintGCDetails
- KV 设值型
- -XX:MetaspaceSize=20000006 键=值 ,自己设置,元空间
- -XX:InitHeapSize= 266338014 初始化堆大小
查看正在运行 java 程序是否开启 JVM 参数: jinfo -flag PrintGCDetails 7876 jinfo -flag JVM参数 进程ID号
- Boolean 类型
在 Idea 中 -> Run -> Edit Configurations 中开启JVM 参数:
++V++M options: -XX:+PrintGCDetails
如果开启,在命令行中查看是否开启,则为:
-XX:+PrintGCDetails
否则是 -XX:-PrintGCDetails
参数前面是+ 表示开启了参数,为 - 表示没有开启
这个参数我并没有配置,是 JVM 默认的参数,那么默认 大小为21.8 M 左右 jinfo -flag MetaspaceSize 2608 -XX:MetaspaceSize=21807104
jinfo -flag MaxTenuringThreshold 2608 -XX:MaxTenuringThreshold=15 年轻代默认 15 此没有被引用的话放入老年代
jinfo -flags 9828
查看所有的默认 JVM 参数 ,9828 是一个正在运行的java 程序进程ID ,如果自己没有设置其他的JVM 参数,那么显示所有的默认JVM 参数
-
-Xms 相当于 -XX:InitialHeapSize ,JXM 初始参数和系统、硬件有关, Xms 一般为物理内存的
1/64
-
-Xmx 等价于 -XX: MaxHeapSize ,一般为物理内存的 1/4
java 查看初始化参数:
java -XX:+PrintFlagsInitial
bool UseInlineCaches = true {product} bool UseInterpreter = true {product} bool UseJumpTables = true {C2 product} bool UseLWPSynchronization = true {product} bool UseLargePages = false {pd product} bool UseLargePagesInMetaspace = false {product} bool UseLargePagesIndividualAllocation := false {pd product} bool UseLockedTracing = false {product} bool UseLoopCounter = true {product} 为等号的是JVM 默认且没有修改的参数,而是 := 表示因为硬件、系统、人为等的原因修改了 JVM 参数,修改之后的值
java -XX:+PrintFlagsFinal -version 查看最终修改的
==运行java命令同时打印参数==
java -XX:+PrintFlagsFinal -Xss128 test test 为类名 -Xss128 参数可以替换
查看简略的JVM 初始化参数
java -XX:+java -XX:+PrintCommandLineFlags -version -XX:InitialHeapSize=65754688 -XX:MaxHeapSize=1052075008 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX: -UseLargePagesIndividualAllocation -XX:+UseParallelGC java version "1.8.0_191" Java(TM) SE Runtime Environment (build 1.8.0_191-b12) Java HotSpot(TM) 64-Bit Server VM (build 25.191-b12, mixed mode) 最后一个参数 -XX:+UseParallelGC 表示垃圾回收器的类型,jdk 8 默认并行GC
垃圾收集器
新生代、老年代
==垃圾收集器在新生代和老年代的使用是一一对应的,只要新生代配置,老年代会自动激活相应的垃圾收集器==
- 串行GC(Serial)
- 最古老 最稳定的垃圾收集器,它仍然是java 虚拟机在 Client 模式下的新生代垃圾收集器 。(现在我们使用的都是 Server 模式的JVM)
- Serial <-> Serial (old)
-XX:+UseSerialGC
开启后会使用 Serial (young) + Serial (old) 收集器组合
Serial (old) 在 jdk 8 以后已经不再适用了
并行GC(ParNew)
只是新生代用并行,老年代不使用并行收集器。- ParNew <-> CMS
- ParNew <-> Serial (old)
-XX:+UseParNewGC
启动会使用ParNew 收集器,老年代启用Serial (old) 收集器,不推荐使用。 (JVM 给出的警告)只影响新生代 ,不影响老年代。 新生代使用复制算法,老年代使用标记-整理算法
并行回收GC(Parallel)
- Parallel Scavenge <-> Parallel Old
相当于新生代老年代 都是并行收集器,都是串行收集器的并行化。 Parallel Scavenge 和 ParNew 类似,但是可以控制吞吐量和具有自适应调节策略。
吞吐量: 程序执行时间/(程序运行时间+垃圾回收时间)
配置-XX:UseParallerGC
表示使用Parallel Scavenge <-> Parallel Old 这也是默认的 GC
CMS 并发标记清除收集器
是一种获取最短回收停顿时间为目标的收集器 ,非常重视服务器的响应速度,希望系统停顿时间最短。需要较大的堆空间。做 JVM 优化一般要选用此垃圾收集器 。除了 G1 收集器外的首选收集器
- Parallel Scavenge <-> Parallel Old
==四个步骤==
- 初始标记 CMS inital mark
只是标记一下GC Roots能直接关联到的对象,速度很快,但仍然需要所有线程暂停工作。 - 并发标记 CMS concurrent mark
进行 GC Roots 的跟踪,和用户线程一起工作,主要标记过程,标记全部对象 - 重新标记 CMS remark
为了修正在并发标记期间,因用户程序继续运行而导致标记变动的那一部分对象标记记录,用来做确认或修正,仍然需要暂停所有的线程 - 并发清除 CMS concurrent sweep
清除 GC Roots 不可达对象,不需要暂停线程,基于标记结果,直接清理对象。
==优点== : 并发收集低停顿
==缺点== : 并发执行对 CPU 压力较大,采用标记清除会导致大量碎片
- 垃圾收集器的选择
- 多 CPU 大吞吐量 后台计算应用
- XX:+UseParallerGC 或 -XX:+UsrParallerOldGC
- 多CPU 追求低停顿 快速响应
- XX:+UseConcMarkSweepGC
参数 | 新生代收集器 | 新生代算法 | 老年代垃圾收集器 | 老年代算法 |
---|---|---|---|---|
-XX:+UseSerialGC | SerialGC | 复制 | SerialOldGC | 标记整理 |
-XX:+UsrParNew | ParNew | 复制 | SerialOldGC | 标记整理 |
-XX:+UseParallerGC/ -XX:+UsrParallerOldGC | Parallel Scavenge | 复制 | Parallel Old | 标记整理 |
-XX:UseConcMarkSweepGC | ParNew | 复制 | CMS+SerialOld (SerialOld为备用) | 标记清除 |
-XX:+UseG1GC | G1 整体采用标记整理,局部采用复制算法,不会产生内存碎片 |
G1 垃圾收集器
G1 是一种服务端的垃圾收集器,在处理多核大容量内存环境中,实现高吞吐量的同时,尽可能满足时间暂停的要求。
G1 收集器可以与应用程序并发
不需要更大的堆空间
不希望牺牲大量的吞吐性能
整理空闲时间更快
但是需要更多的时间预测 GC 的停顿时长
==G1 的 Stop the Worls 更可控,G1 在停顿时间上添加了预测机制,用户可以指定期望停顿时间。 G1 不会产生内存碎片。==
CMS 虽然减少了暂停应用所耗费的时间,但仍然存在内存碎片,为了去除内存碎片化,同时保留这种 CMS 低暂停时间的优点,jdk 7 发布了 G1 收集器。并且在 jdk 9 中将默认的 CMS 收集器换为 G1 收集器。
使用 G1 收集器: -XX:+UseG1GC
- G1 为什么不会产生内存碎片?
因为G1 收集器改变了内存扫描方式,以前是 Eden 区 + Survivor + Tenured 区。而现在是一个个大小一样的 region 区。 每个region 1M - 32 M 不等。一个region 在微观上可能属于 Survivor 或 Tenured 区。
好处 : 避免了全内存扫描 ,所以快
在堆的使用上,G1 并不要求对象的存储一定是物理上连续的,只要是逻辑上连续的即可。 每个分区也不会固定为某个代服务,可以在年轻代和老年代之间切换。可以通过参数 -XX:G1HeapRegionSize=n
来区分指定大小(1M - 32M ,且必须是2 的幂) 默认将堆划分为 2048 个区。
G1 的四个步骤
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
G1 常用参数 :
-XX:+UseG1GC 开启G1 收集器 -Xmx:32g 设置最大内存(本来最大为 64G 2048*32m = 64G) -XX:MaxGCPauseMillis = 100 设置最大停顿时间,100 毫秒
JVM 遇到的异常
- java.lang.StackOverFlow
栈溢出,栈默认大小 512 k - 1024 k
一般出现在递归调用中 (是错误)
栈大小通过-Xss 设置 - java.lang.OutOfMemeoryError : Java Heap Space
堆溢出 堆内存一般 4 G 左右,如果看到这个代表程序出现堆溢出 。
出现这个一般是对象太多了,
// 先设置 堆大小为 26 m ,然后 执行以下语句 byte [] bytes = new byte[80 * 1024 * 1024] ; 堆溢出 (new 了80 m数据)。
java.lang.OutOfMemoryError: Requested array size exceeds VM limit
数组太大, 最终长度超过平台限制值, 但小于 Integer.MAX_INT
为了测试系统限制, 故意分配长度大于 2^31-1 的数组。java.lang.OutOfMemoryError :GC overhead limit exceeded
GC 回收时间过长,超过 98% 时间用来 GC 并且回收了不到 2% 的堆内存。连续多次 GC 都只回收了不到 2% 的情况下才会抛出此异常,如果不抛出堆内存会很快占满,CPU 使用率一直是 100% 却没有 GC 的成果 。
public static void main(String[] args) { int i = 0 ; List<String> list = new ArrayList <>(); try { while (true){ list.add(String.valueOf(i++).intern()); } } catch (Throwable e) { System.out.println("*** i ***"+i); e.printStackTrace(); throw e; } }
- java.lang.OutOfMemoryError : Direct buffer memory
==直接内存溢出==
一般出现在 NIO 或者以NIO 为底层技术的技术中(比如 Netty);
它可以调用 Native 函数库来直接分配堆外内存,然后通过存储在 Java 堆中的一个对象作为这块内存中的引用来操作 。
因为NIO 提供两种分配内存的方式,一种是默认收到 JVM 管辖的堆内存中,ByteBuffer.allocate()
这样做需要拷贝,效率慢,第二种是直接分配在操作系统本机内存的ByteBuffer.allocateDirect()
,不属于 GC 管辖范围,不需要拷贝所以效率快。 但是如果不断分配本机内存,堆内存很少使用,JVM 就不会执行 GC DirectByteBuffer 对象就不会被回收,这时堆内存充足,但是本机内存可能使用完了,在尝试分配本地内存就会出现这样的错误。
JVM 可以使用的内存大概是本机内存的四分之一,如果16G 内存,就是4G作用 。 - java.lang.OutOfMemoryError : unable to create new native thread
==不能在创建新的本地线程了,一般出现在高并发中==
出现此错误的两种情况:
一、你的应用创建了太多线程,一个应用创建多个线程,超过系统承载极限。
二、你的服务器不允许你创建那么多线程,linux 默认非root账户允许单个进程创建的最大线程数是 1024 ,超过就会报错 。
public static void main(String [] args){ for(int i = 1 ;;i++){ System.out.println("***i="+i); new Thread(()->{ Thread.sleep(1000); },"i").start(); } }
- java.lang.OutOfMemoryError : Metaspace
java 8 以后新生代、老年代、永久代变为元空间(Metaspace) 。
==此错误就是元空间错误==
永久代(java 8 以后被元空间取代)存放以下信息:- 虚拟机加载类的信息
- 常量池
- 静态变量
- 即时编译后的代码
JVM 调优和 springboot 微服务优化
- IDEA 开发完微服务工程
- maven 进行 clean package
- 要求启动微服务时,同时配置 JVM/GC 调优参数
- 打了包以后,将 jar/war 包启动时使用
`java -server -Xms1024m -XX:+UseG1GC -jar jar包名 `
linux
- 五个常用的linux 命令
- top
查看系统的整机性能,可以查看 CPU 和 内存 等
load average : 查看系统的负载均衡,后面会有三个值,如果三个值相加/3 大于60% 表示系统负载过重。
top 就相当于windows 的任务管理器。uptime 是 top 的精简版。 - vmstat
vmstat -n 2 3
每隔两秒采样一次,共采样三次。 查看系统性能cpu 等参数
proce-r: 运行和等待的cpu 线程数
proce-b: 等待资源的进程数 - free
free 查看内存 ,free -m
更加好 - df
df -h
可读性更高的查看系统硬盘使用百分比 - iostat , ifstat 磁盘IO 网络 IO
iostat -xdk 2 3
ifstat 1 查看网络 IO (1秒1次)
- top
CPU 占用过高怎么办?
- 先用 top 找出 cpu 占用最高的,按1查看CPU使用情况
- ps -ef 或者 jps 定位哪个程序占用,得到进程号
- 通过
ps -mp 5101 -o THREAD,tid,time
或者top -H -p 5101
查看这个进程号下哪个线程CPU 占用率高 - 将需要的线程ID 转换为16 进制格式(英文小写)
- jstack 进程ID | grep tid(16进制英文小写) -A60
// 打印前60行
此时可以直接定位到哪一行代码出现问题。
序列化的作用 : 在网络中传输和持久化对象到硬盘 。
Spring 的事务
同步IO 和异步 IO 的区别
同步,就是我调用一个功能,该功能没有结束前,我死等结果。
异步,就是我调用一个功能,不需要知道该功能结果,该功能有结果后通知我(回调通知)。
同步IO和异步IO的区别就在于:数据拷贝的时候进程是否阻塞
进程和线程的区别,创建线程和进程的开销指什么
==进程是资源(CPU 、 内存等)分配的最基本单位==,它是程序执行的一个实例。程序运行就会为之创建一个进程,并且为它分配资源,然后把该进程放入进程就绪队列,进程调度器选中的时候就会为它分配 CPU 时间,程序开始真正运行 。
(Linux 通过 fork 函数创建进程)
==线程是程序执行的最小单位==,它是 CPU 调度和分派的基本单位,一个进程可以由多个线程组成,线程间共享进程的所有资源,每个进程有自己的堆栈和局部变量。线程由 CPU 独立调度执行,在多CPU 环境下就允许多个线程同时运行。同样多个线程也可以并发操作。
- 进程和线程的资源消耗
- 进程有自己的独立地址空间,没启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段、数据段,这样的操作非常昂贵。 而线程是共享进程中的数据,使用相同的地址空间,因此 CPU 切换一个线程的花费远比线程小很多,同时创建一个线程的开销也比进程小很多。
- 线程之间的通信更加方便,同一进程下的线程共享全局变量,静态变量等的数据。而进程间的通信要以管道、消息队列、信号量、共享内存、Socket 等的方式
- 但是多进程的程序更加健壮 。
TCP 的安全机制体现在哪里?采用了哪些机制?
TCP 的安全机制体现在可靠传输的实现。
采用了以字节为单位的滑动窗口
发送方通过维持一个发送滑动窗口来确保不会发生由于发送方报文发送太快接收方无法及时处理的问题。此时发送方的报文分为四类, 第一类是已经发送并且得到接收方确认的报文,第二类是已经发送但是没有接收到确认的报文,第三类是发送方还没发送,但是滑动窗口还足够巨大,允许被发送的报文, 第四类是还没发送并且窗口已经被占满,不允许发送的报文。 一般来说,滑动窗口的最左端都是介于第一类跟第二类报文的分界线,最右端是第三类跟第四类报文的分界线。
滑动窗口的流量控制可以包括那么几个协议:
a、停等协议。 滑动窗口的大小为1, 每个发送报文都要等到被确认以后,发送方才继续发送下一个报文。
- b、后退n步协议。 该协议下,滑动窗口大于1,发送方可以一直发送报文,但是当接收到接收方发送的三个连续的同一序列号的ACK报文时,说明该序列号的报文是已经丢失的,那么此时重发该丢失报文以及该报文以后的报文(包括那些已经发送的)。
- c、选择重传。在后退n步协议当中,如果某个报文丢失。那么将要重新发送这个丢失报文及以后的所有报文(包括已经发送的),选择重传协议不用做此要求,只要重新发送丢失的报文即可。
- 超时重传时间的选择
(TCP 采用了一种自适应算法,根据报文往返时间得到一个较为平滑的往返时间, 然后这个超时重传的时间会略大于平滑往返的时间。如果过了这个时间还没有收到确认,就会重传报文。)
选择确认 SACK
- (收到的报文段完整,但是顺序未按序号,中间还缺少一些序号的数据,可以只传送缺少的数据。它的实现是通过指针标记出这些缺失的报文段,然后在接收窗口记录这些数据然后把信息高速发送方,让发送方不要重复发送这些数据。 这种方法不怎么使用,因为开启它会占用 TCP 首部报文。较好的还是再重传一次数据 。 )
AQS
AbstractQueuedSynchronizer
LRU
LRU是什么?按照英文的直接原义就是Least Recently Used,==最近最少使用页面置换算法==,它是按照一个非常注明的计算机操作系统基础理论得来的:
最近使用的页面数据会在未来一段时期内仍然被使用,已经很久没有使用的页面很有可能在未来较长的一段时间内仍然不会被使用。
基于这个思想,会存在一种缓存淘汰机制,每次从内存中找到最久未使用的数据然后置换出来,从而存入新的数据!它的主要衡量指标是使用的时间,附加指标是使用的次数。在计算机中大量使用了这个机制,它的合理性在于优先筛选热点数据,所谓热点数据,就是最近最多使用的数据!因为,利用LRU我们可以解决很多实际开发中的问题,并且很符合业务场景。
- 核心操作的步骤:
- save(key, value),首先在 HashMap 找到 Key 对应的节点,如果节点存在,更新节点的值,并把这个节点移动队头。如果不存在,需要构造新的节点,并且尝试把节点塞到队头,如果LRU空间不足,则通过 tail 淘汰掉队尾的节点,同时在 HashMap 中移除 Key。
- get(key),通过 HashMap 找到 LRU 链表节点,把节点插入到队头,返回缓存的值。
常见的缓存算法
- LRU (Least recently used) 最近最少使用,如果数据最近被访问过,那么将来被访问的几率也更高。淘汰不常常使用的数据 。
- 新数据插入到链表头部
- 每当缓存命中(即缓存数据被访问),则将数据移到链表头部
- 当链表满的时候,将链表尾部的数据丢弃
- LFU (Least frequently used) ==最近最不常用页面置换算法==
- FIFO (Fist in first out) 先进先出, 如果一个数据最先进入缓存中,则应该最早淘汰掉。
为什么wait和notify必须在同步方法或同步块中调用
总结一下,在Java中,我们为什么必须在synchronized方法或同步块中调用wait(),notify()或notifyAll方法的原因:
1) 避免IllegalMonitorStateException
2) 避免任何在wait和notify之间潜在的竞态条件
操作系统的锁实现
TEST AND SET
MVCC
MVCC(Mutil-Version Concurrency Control),就是多版本并发控制。MVCC 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问。
Mysql 借用MVCC 实现了读的非阻塞而已。
Github
常用词汇
- watch 会持续收到项目的动态
- fork 复制项目到自己的本地仓库
- star 点赞
- clone 下载到本地
- follow 关注作者
github 之 in
- xxx in : name 项目名称包含xxx 的
- xxx in :description 项目描述包含xxx 的
- xxx in : readme 项目的readme 文件包含 xxx 的
- 组合使用 : seckill in: name,readme 项目的readme 和名字有seckill 的
- 例子: seckill in: name,readme
github 之 star 、fork
- xxx stars 通配符 :> 或者 :>=
- 例子 springboot stars:>=5000 点赞超过5000 的项目
- 查找fork 数大于500 的springcloud 项目: springcloud forks:>500
- 查找区间: 用 .. 连接
- springboot 项目fork 数在10 到 300 之间,star 数在10 到 3000 之间: springboot stars:100..3000 forks:10..300
github 之 awesome
- awesome redis 搜索优秀的redis 框架 、 教程
指针关键代码行数
项目内搜索
- 进入某作者的项目内后 按下 t 就可以项目内搜索
地点搜索 西安的java 大牛
- location:xian language:java
线程池的使用 : cpu 密集型和 IO 密集型
cpu 密集型:会消耗大量cpu 资源,例如:需要大量的计算、运算,这时应该避免切换线程,所以线程数一般只需要 cpu的核数就可以(主要出现在业务发咋的计算和逻辑处理中)
IO 密集型 :cpu 使用率低,程序中有大量的IO 操作,导致cpu 空闲,所以通常需要开cpu 核数的两倍线程,当线程进行IO 操作cpu 空闲时启用其他线程继续使用cpu 。 这一类型在开发中主要出现在一些读写操作频繁的业务逻辑中 。
- 高并发 一般可以设置线程数为cpu 核数+1
topk 优化
减治法 (partion) :
topk