category
学习思考
date
Oct 26, 2022
icon
Origin
password
slug
Q
status
Published
summary
一些常见面试题
tags
Tags
type
Post
URL
Exception in thread "B" java.lang.IllegalMonitorStateException
at java.lang.Object.notify(Native Method)
at com.example.demo.question.LockSupportDemo.lambda$main$1(LockSupportDemo.java:28)
at java.lang.Thread.run(Thread.java:748)
Exception in thread "B" java.lang.IllegalMonitorStateException
at java.lang.Object.notify(Native Method)
at com.example.demo.question.LockSupportDemo.lambda$main$1(LockSupportDemo.java:28)
at java.lang.Thread.run(Thread.java:748)
1.字符串常量java内部加载:
58同城的java字符串常量池:
public class StringPool58Demo { public static void main(String[] args) { String str1 = new StringBuilder("58").append("tongcheng").toString(); System.out.println(str1); System.out.println(str1.intern()); System.out.println(str1.intern() == str1); System.out.println(); String str2 = new StringBuilder("ja").append("va").toString(); System.out.println(str2); System.out.println(str2.intern()); System.out.println(str2.intern() == str2); } }
intern方法到底讲了什么:
intern是String类里面的一个方法,是native的,调用的底层的native接口。当我们调用这个方法的时候池子包含类似的对象,会直接返回。否则,如果这个对象是没有的,则会新建,并添加到池子里面,并返回他的引用。
/** * Returns a canonical representation for the string object. *<p> * A pool of strings, initially empty, is maintained privately by the * class {@codeString}. *<p> * When the intern method is invoked, if the pool already contains a * string equal to this {@codeString} object as determined by * the {@link#equals(Object)} method, then the string from the pool is * returned. Otherwise, this {@codeString} object is added to the * pool and a reference to this {@codeString} object is returned. *<p> * It follows that for any two strings {@codes} and {@codet}, * {@codes.intern() == t.intern()} is {@codetrue} * if and only if {@codes.equals(t)} is {@codetrue}. *<p> * All literal strings and string-valued constant expressions are * interned. String literals are defined in section 3.10.5 of the *<cite>The Java™Language Specification</cite>. * *@returna string that has the same contents as this string, but is * guaranteed to be from a pool of unique strings. */
方法区和运行时常量池溢出:
由于运行时常量池是方法区的一部分,所以这两个区域的溢出测试可以放到一起进行。HotSpot从JDK7开始逐步“去永久代”,并在JDK8中完全使用元空间来代替永久代。
String::intern()是一个本地方法。在JDK6或更早之前的版本HotSpot虚拟机中,常量池都是分配在永久代中,我们可以通过-XX:PermSize和-XX:MaxPermSize限制永久代的大小,即可间接限制其中常量池的容量。
结果:

一个为true 一个为false ,因为有一个初始化的java字符串(JDK出娘胎自带的),在加载sun.misc.Version这个类的时候进入常量池。
力扣算法第一题:两数之和
1.给定一个数m,求大于该数的最小2的n次幂,返回n
2.给定一个整数数组nums 和一个目标数组 target,请你在数组中找出和为目标数组的那两个整数,并返回他们的数组下标
public class TwoSumDemo { public static void main(String[] args) { int[] nums = new int[]{2,7,11,15}; int target = 9; int[] index =twoSum1(nums,target); } // 暴力破解 public static int[] twoSum1(int[] nums, int target) { for (int i = 0; i < nums.length; i++) { for (int j = i+1; j < nums.length; j++) { if(target-nums[i] == nums[j]){ return new int[]{i,j}; } } } return null; } // 通过hash完成 public static int[] twoSum2(int[] nums, int target) { Map<Integer,Integer> map = new HashMap<>(); for (int i = 0; i < nums.length; i++) { int num = target -nums[i]; if(map.containsKey(num)){ return new int[]{map.get(num),i}; } map.put(nums[i],i); } return null; } }
3.JUC
可重入锁(又名递归锁)
同一个线程,只要持有同一个对象的同一把锁,他可以永远获得自己的。
是指在同一个线程,在外层方法获取锁的时候,在进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象)
Java中ReentrantLock和Synchronized都是可重入锁,可重入锁的一个优点是避免死锁。
可:可以 重:再次 入:进入(进入同步域,即同步代码块/方法或是显式锁锁定的代码) 锁:同步锁
一个线程中,多个流程可以获取同一把锁,持有这把同步锁可以再次进入,自己可以获取自己的内部锁。
可重入锁的种类:
隐式锁(即synchronized关键字使用的锁)默认是可重入锁。
同步代码块:
public class ReEnterLockDemo { static ObjectobjectLockA= new Object(); public static void m1(){ new Thread(()->{ synchronized (objectLockA){ System.out.println(Thread.currentThread().getName() + "\t" + "------外层调用"); synchronized (objectLockA){ System.out.println(Thread.currentThread().getName() + "\t" + "------中层调用"); synchronized (objectLockA){ System.out.println(Thread.currentThread().getName() + "\t" + "------内层调用"); } } } },"t1").start(); } public static void main(String[] args) { m1(); } }
同步方法:
public class ReEnterLockDemo2 { public static void main(String[] args) { new ReEnterLockDemo2().m1(); } public synchronized void m1(){ System.out.println("======外"); m2(); } public synchronized void m2(){ System.out.println("====中"); m3(); } public synchronized void m3(){ System.out.println("====内"); } }
显式锁(Lock)也有ReentrantLock这样的可重入锁。
可重入锁的实现机制:
每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程持有,java虚拟机会将该锁对象的持有线程设置为当前线程,并将其计数器加1
在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么java虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。
当执行monitorexit时,java虚拟机则需将锁对象的计数器减1.计数器为零代表锁已经被释放。
LockSupport
什么是LockSupport:
JUC包下的一个类,用于创建锁和其他同步类的基本线程阻塞语句,线程的等待唤醒机制(wait/notify)的改良加强版本。LockSupport中的park()和unpark()的作用分别是阻塞线程和接触阻塞线程。
3种让线程等待或唤醒的方法
- 使用Object中wait()方法让线程等待,使用Object中notify()方法唤醒线程
- 使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程
- LockSupport类可以阻塞当前线程以唤醒指定被阻塞的线程
Object类中的wait和notify方法实现线程的等待和唤醒:
!!!调用wait方法锁会被释放
如果不在同一个代码块里面,wait和notify是不能一起用的。回报下面的错误。
Exception in thread "B" java.lang.IllegalMonitorStateException at java.lang.Object.notify(Native Method) at com.example.demo.question.LockSupportDemo.lambda$main$1(LockSupportDemo.java:28) at java.lang.Thread.run(Thread.java:748)
结论:Object类中的wait和notify,notifyAll用户线程的等待和唤醒的方法,必须都在synchronized内部执行(必须用到关键字synchronized),将notify放在wait方法前面,程序无法执行,无法被唤醒。wait和notify方法必须要在同步块或者方法里面且成对出现使用,先wait后notify。
Condition接口中的await后signal方法实现线程的等待和唤醒
线程必须先要获得并持有锁,必须在锁块(synchronized或lock)中,必须要先等待后唤醒,线程才能够被唤醒。
LockSupport类中的park等待和unpark唤醒
通过pack和unpark实现阻塞和唤醒线程的操作
pack() 除非许可证可用,否则禁用当前线程以进行线程调度
unpack() 如果给定线程尚不可用,则为其提供许可
LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,没个线程都有一个许可(permit),permit只有两个值1和零,默认是零。可以把许可看成是一种(0,1)信号量(Semaphore),但Semaphore不同的是,许可的累加上限是1。
public static void main(String[] args) { //SynchronizedWaitNotify(); //lockAwaitSignal(); Thread a = new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\t" + "---come in"); LockSupport.park(); // 直接被阻塞了,等待通知,或者是等待放行,需要许可证。 System.out.println(Thread.currentThread().getName() + "\t" + "-----被唤醒"); }, "A"); a.start(); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { throw new RuntimeException(e); } Thread b = new Thread(() -> { LockSupport.unpark(a); // b 唤醒 a 不需要锁块 System.out.println(Thread.currentThread().getName() + "\t" + "-----发出通知"); }, "B"); b.start(); }
先unpark 在 park LockSupport照样支持,提前发通行证。
LockSupport是一个线程阻塞工具类,所有的方法都是镜头方法,可以让线程在任意位置阻塞,阻塞后也有对应的唤醒方法。归根结底,LockSupport调用Unsafe中的native代码。
原理:
LockSupport和每个使用它的线程都有一个许可(permit)关联。permit相当于1,0的开关,默认是0,调用一次unpark就加1变成1。调用一次park就会消费permit,也就是将1变成0,同时park立即返回。如果再次调用park会变成阻塞(因为permit为零了会阻塞在这里,一直到permit变为1),这时调用unpark会把permit置为1。没个线程都有一个相关的permiit,permit最多只有一个,重复调用unpark也不会积累凭证。
AQS-AbstractQueuedSynchronizer(抽象队列同步器)
Q:
1.从集合开始吧,介绍一下常用的集合类,哪些是有序的,哪些是无序的。
2.hashmap是如何寻址的,哈希碰撞后是如何存储数据的,1.8后什么时候变成红黑树、说一下红黑树的原理,红黑树有什么好处
3.concurrenthashmap怎么实现线程安全,一个里面会有几段segment,jdk8后有优化concurrenthashmap吗?分段锁有什么坏处
4.reentrantlock实现原理,简单说一下aqs
5.synchronized实现原理,monitor对象什么时候生成的?
6.刚刚你提到了synchronized的优化过程,详细说一下。偏向锁和轻量级锁有什么区别
7.线程池几个参数说一下,你们项目中如何根据实际场景设置参数的
前置知识:
公平锁和非公平锁:
可重入锁:
LockSupport:
自旋锁:
数据结构之链表:
设计模式之模版设计模式:
技术解释:
是用来构建锁或者其他同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的FIFO队列来完成资源获取线程的排队工作,并通过int类型变量表示持有锁的状态。
和AQS有关的,ReentrantLock、CountDownLatch、ReentrantReadWriteLock、Semaphore
锁和同步器的关系:
锁面向锁的使用者:定义了程序员和锁交互的使用层API,隐藏了实现细节,你调用即可。
同步器面向锁的实现者:比如java并发大神DougLee,提出统一规范并简化了锁的实现,屏蔽了同步状态管理、阻塞线程排队和通知、唤醒机制。
AQS能解决的问题:
- 加锁就会导致阻塞,必须要等持有锁的线程完成了以后,才能进行。有阻塞就需要排队,实现排队必然需要有某种形式的队列来进行管理。
- 抢到资源的线程直接使用处理业务逻辑,抢不到资源的必然涉及一种排队等候机制。抢占资源失败的线程继续去等待(类似银行业务办理窗口都满了,暂时没有手里窗口的顾客只能去候客区等待排队),但等候线程仍然保留获取锁的可能且获取锁流程仍在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)。
- 如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现。它将请求共享资源线程封装成队列的节点(Node),通过CAS、自旋以及LockSupport.park()的方式,维护state变量的状态,使并发达到同步控制的效果。
AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作,将每条要去抢占资源的线程封装成一个Node节点来实现锁的分配,通过CAS完成对State值的修改。
- 向map中put 放的不是键值对,是一个node节点,node<k,v>。AbstractQueuedSynchronizer将线程封装在node<Thread>中,AbstractQueuedSynchronizer里面装的是一个一个的node,node里面有一个前指针prev,一个后指针next,完成双向队列。
- AQS有一个同步状态的State成员变量 private volatitle int state;,类似于银行办理业务的受理窗口状态,等于零就是没人,自由状态可以办理。大于等于1就是有人占用窗口,等着去
- AQS的CLH队列,CLH队列是一个双向队列,通常情况下通过自旋等待,state变量就判断是否阻塞。CLH从尾部入队,从头部出队
- AQS = state + CLH队列
小总结:有阻塞就需要排队,实现排队必然需要队列。
node内部类:头、尾、前、后
node的等待状态 waitState成员变量 volatile int waitState
等候区其他线程的等待状态,队列中每个排队的个体就是一个node
node类内部结构:
node = waitStatus + 前后指针的指向
node类属性:
shared 共享模式 、exclusive 排他模式 、waitStatus 当前节点在队列中的状态
Lock接口的实现类,基本都是通过【聚合】了一个队列同步器的子类完成线程访问控制的
整个ReentrantLock的加锁过程,可以分为三个阶段
1、尝试加锁
2、加锁失败,线程入队列
3、线程入队列后,进入阻塞状态
AQS源码深度分析:
双向链表中:第一个节点为虚节点(也叫哨兵节点),其实不存储任何信息,只是占位。真正的第一个有数据的节点,是从第二个节点开始的。
AQS acquire主要有三条流程:
1、调用tryAcquire→交由子类FairSync实现
2、调用addWaiter→enq入队操作
3、调用acquireQueued→
Spring AOP
AOP的常用注解:
@Before:前置通知:目标方法之前执行
@After:后置通知:目标方法之后执行
@AferReturning:返回后通知:执行方法结束前执行(异常不执行)
@AfterThrowing:异常通知:出现异常时候执行
@Around:环绕通知:环绕目标方法执行
aop的全部通知顺序,springboot或Springboot2对aop的执行顺序影响:
正常情况:环绕通知前 @before @AferReturning @After 环绕通知后
异常情况:环绕通知前 @before @AfterThrowing @After
Spring循环依赖:
Spring的三级缓存
三级缓存分别是什么?三个Map有什么异同
什么是循环依赖
如何检测是否存在循环依赖
多例的情况下循环依赖问题为什么无法解决
什么是循环依赖:
多个bean之间互相依赖,形成了一个闭环。比如:A依赖B,B依赖C,C依赖于A
通常来说,如果问Spring容器内部如何解决循环依赖,一定是指默认的单例Bean中,属性互相引用的场景。
两种注入方式对循环依赖的影响:
循环依赖对构造注入不友好,但是setter注入是可以的
我们AB循环依赖问题只要A的注入方式是setter且singleton,就不会有循环依赖问题。
构造器循环依赖:
public class ServiceB { private ServiceA serviceA; public ServiceB(ServiceA serviceA){ this.serviceA = serviceA; } }
public class ServiceA { private ServiceB serviceB; public ServiceA(ServiceB serviceB){ this.serviceB = serviceB; } }
public class ClientConstructor { public static void main(String[] args) { new ServiceA(new ServiceB(new ServiceA())); } }
构造器循环依赖是无法解决的,你想让构造器注入支持循环依赖,是不存在的(禁止套娃🪆)
Setter方法注入:
public class ServiceAA { private ServiceBB serviceBB; public void setServiceBB(ServiceBB serviceBB){ this.serviceBB = serviceBB; System.out.println("A 里面设置了 B"); } }
public class ServiceBB { private ServiceAA serviceAA; public void setServiceAA(ServiceAA serviceAA){ this.serviceAA = serviceAA; System.out.println("B 里面设置了 A"); } }
public class ClientSet { public static void main(String[] args) { ServiceAA a = new ServiceAA(); ServiceBB b = new ServiceBB(); b.setServiceAA(a); a.setServiceBB(b); } }
Spring容器:
默认的单例(singleton)的场景是支持循环依赖的,不报错
原型(Prototype)的场景是不支持循环依赖的,会报错
DefaultSingletonBeanRegistry:
三级缓存就是spring用来解决循环依赖问题的三个map
三级缓存:Map<String,ObjectFactory<?>>singletonFactories ,存放可以生成Bean的工厂
一级缓存:
第一级缓存(也叫单例池)
singletonObjects:存放已经经历了完整生命周期的Bean对象单例对象缓存,bean名称:bean实例,即:所谓的单例池,表示已经经历了完整生命周期的bean对象。
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);二级缓存:
二级缓存:
earlySingletonObjects,存放早期暴露出来的Bean对象,Bean的生命周期未结束(属性还未填充完)早期的单例对象的高速缓存:bean名称-bean实例
表示bean的生命周期还没走完(bean的属性还没填完)就把这个bean存入该缓存中,也就是实例化但未初始化的bean放入该缓存
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);三级缓存:
三级缓存:
Map<String, ObjectFactory<?>> singletonFactories,存放可以生成Bean的工厂单例工厂的高速缓存:bean名称-ObjectFactory
表示存放生成bean的工厂
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);源码里面的顺序是1-3-2
只有单例的Bean会通过三级缓存提前暴露出来解决循环依赖的问题,而非单例的Bean,每次从容器中获取都是一个新的对象,都会重新创建,所以非单例的bean是没有缓存的,不会将其放到三级缓存中。
实例化/初始化:
实例化:内存中申请一块内存空间(租赁好房子,自己的家具东西还没有搬进去)
初始化:属性填充
3个Map和四大方法,总体相关对象:
创建A的过程中需要创建B,于是A将自己放到三级缓存里面,去实例化B
B实例化的时候发现需要A,于是B先查一级缓存,没有,在查二级缓存,还是没有,在查三级缓存,找到A,然后把三级缓存里面的这个A放到二级缓存里面,并删除三级缓存里面的A
B顺利初始化完毕,将自己放到一级缓存里面(此时B里面的A依然后创建中状态)
然后回来接着创建A,此时B已经创建结束,直接从一级缓存里面拿到B,然后完成创建,并将A自己放到一级缓存里面。
refresh()方法:
加载容器初始化得方法
spring源码里面以do开头的是真正做事情的
Spring创建Bean步骤:
Spring创建Bean主要分为两个步骤,创建原始Bean对象,接着去填充对象属性和初始化
每次创建bean之前,我们都会从缓存中查下,有没有该bean,因为是单例,只能有一个。
当我们创建BeanA的原始对象后,并把它放到三级缓存中,接下来就是填充对象属性了,这是发现依赖了BeanB,接着就去创建BeanB,同样的流程。创建完BeanB填充属性时又发现依赖了BeanA又是同样的流程。
不同的是:
这个时候可以在三级缓存中查到刚刚放进去的原始对象BeanA,所以不需要继续创建,用它注入BeanB,完成BeanB的创建。既然BeanB创建好了,所以BeanA就可以玩哼填充属性的步骤了,接着执行剩下的逻辑,闭环完成。
Spring解决循环依赖依靠的是Bean的“中间态”这个概念,而这个中间态指的是已经实例化但是还没初始化的状态———>半成品。
实例化的过程又是通过构造器创建的,如果A还没创建好出来怎么可能提前曝光,所以构造器的循环依赖无法解决。
Spring为了解决单例的循环依赖问题,使用了三级缓存。
其中一级缓存为单例池(singletonObjects)
二级缓存为提前曝光对象(earlySingletonObjects)
三级缓存为提前曝光对象工厂(singletonFactories)
假设A、B循环引用,实例化A的时候就将其放入三级缓存中,接着填充属性的时候发现依赖了B,同样的流程也是实例化后放入三级缓存,接着又去填充属性发现自己依赖了A,这时候从缓存中查找到早期暴露的A,没有AOP代理的话,直接将A的原始对象注入B,完成B的初始化后,进行属性填充后初始化,这时候B完成后,就去完成剩下的A的步骤,如果有AOP代理,就进行AOP处理获取代理后的对象A,注入B走剩下的流程。

Redis
Redis 6.0.8:
查看redis版本:redis- server- v
redis 命令不区分大小写,kv区分大小写
redis传统五大数据类型和落地应用:
String:
set key value/get key
同时获取多个键值:mset mget ,递增数字 INCR key 增加指定证书:INCRBY key increment
递减数值:DRCR key 减少指定的整数:DECRBY key decrement,获取字符串长度 STRLEN key
应用场景:
分布式锁 setnx key value set key value[EX seconds][PX milliseconds][NX][XX]
商品编号、订单号采用INCR命令生成,点赞数
list
有点像双向链表,既可以往左边加也可以往右边加
应用场景:
微信订阅号
hash
hash对应java中map<String,map<K,v>>
应用场景:购物车早期,当前中小厂可用
set:
应用场景:
微信抽奖小程序
微信朋友圈点赞
微博好友关注社交关系
QQ内推可能认识的人
zset(sorted set)
应用场景:热搜,点赞数,热评
bitmap
HyperLogLog
geo
stream
知道分布式锁吗?有哪些实现方案?谈谈你对redis分布式锁的理解,删除key的时候有什么问题:
1、mysql 2、zookeeper 3、redis
一般的互联网公司大家都习惯用redis做分布式锁
redis —→redlock ——> redisson lock/unlock
分布式锁
redis最低配是一主二从 一个哨兵,基础是三主三从
redis除了拿来做缓存,你还见过基于redis的什么用法:
redis做分布式锁的时候有需要注意的问题:
如果是redis是单点部署的,会带来什么问题
集群模式下,比如主从模式,有没有什么问题
介绍一下Redlock,谈谈redission
Redis分布式锁如何续期,说说看门狗
redis缓存过期淘汰策略:
redis的LRU算法的简介:
Redis配置:

使用Serializable使可用性更高
基础案例DEMO,开始找问题:

问题:
1.单机版没加锁:加synchronized jvm层面的锁。实际工作中加synchronized和Reentratlock都可以。区别是:synchronized是JVM层面的锁,Reentratlock是一个类。两个锁的特点是不一样的。加synchronized,只有你的加锁代码运行完成了,其他线程才能进来。容易导致线程积压和拥堵。使用Reentratlock可以trylock(我等着觉得时间太长了,放弃等待。给一个规定时间内,拿不到锁放弃)

所以加锁的时候看业务。1、一定要抢到,抢不到就一直等待,用synchronized
2、可以抢不到,有后续的方法,自旋,或者做其他业务。if(lock.trylcok)
2.0:

分布式部署之后,单机锁还是会出现超卖现象(一个商品在两个服务器都卖出),需要分布式锁。redis具有极高的性能,命令对分布式锁友好,借助set命令可以实现加锁处理。

3.0:
增加分布式锁

问题:出异常的话无法释放锁,必须要在代码层面加finally释放锁。
4.0:
问题:宕机了无法走到finally。部署为服务jar包的机器挂了,代码层面无法走到finally这一块,没办法保证解锁,这个key没有被删除,需要加入一个过期时间限定key
5.0:
问题:加锁和设置过期时间并不是原子的,非原子操作没有意义,必须加锁和加过期时间是原子性的。
6.0:
问题:删除了别人的锁。锁线程情况下,A线程的业务时间较慢,系统自动解锁,这时候B线程进入,A线程在做完业务后解除了B线程的锁。

只能删除自己的锁,不能删除别人的,必须在程序里面判断是否是自己的锁。
7.0:
问题:判断加锁和解锁不一定是同一个客户端

需要使用lua脚本,保证原子性删除
不用lua脚本怎么办:
用Redis自身的事物,redis不支持回滚


8.0:
问题:确保过期时间要大于业务执行时间,redis分布式锁实现续期
9.0:
上redLock Redisson实现分布式锁

严谨的写

redis缓存过期淘汰策略:
生产上你们的redis内存设置多少?
如果不设置最大内存大小,或最大内存大小为0,在64位操作系统下不限制内存大小,在32位操作系统下最多使用3G。
打开redis配置文件,设置maxmemory参数,maxmemory是bytes字节类型,注意转换。
一般推荐设redis设置内存为最大物理内存的四分之三
如何配置、修改redis内存大小
1、通过修改配置文件 : maxmemory 104857600(100m)
2、config get maxmemory config get maxmemory 1
如果redis内存满了你怎么办?
内存满了会报OOM错误
redis清理内存的方式?定期删除和惰性删除了解过吗
如果一个key过期了,是不是马上就在内存中被删除?
不是,三种不同的删除策略:
Redis不可能时时刻刻遍历所有呗设置了生存时间的key,来检测数据是否已经到达过期时间,然后对它进行删除。
立即删除能保证内存中数据的最大新鲜度,因为它保证过期键值会在过期后马上被删除,其所占用的内存也会随之释放。但是立即删除对cpu是不友好的。因为删除操作会占用cpu的时间,如果刚好碰上cpu很忙的时候,比如正在做交集或者排序等计算的时候,就会给cpu造成额外的压力。这样会产生大量的能量消耗,同时也会影响数据的读取操作。
1、定时删除:对CPU不友好,用处理器性能换取存储空间(拿时间换空间)
2、惰性删除:数据到达过期时间,不做处理,等下次访问该数据时,如果未过期,返回数据,发现已过期,删除返回不存在。缺点:对内存不友好。
3、定期删除:定期删除时两种策略的折中:
定期删除策略每隔一段时间执行一次删除过期键的错做,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。
周期性轮询redis库中实效性数据,采用随机抽取的策略,利用过期数据占比的方式控制删除额度。
特点1:CPU性能占用设置有峰值,检测频度可以自定义设置
特点2:内存压力不是很大,长期占用内存的冷数据会被清理
总结:周期性抽查存储空间(随机抽查,重点抽查)
必须要有一个兜底方案:内存淘汰策略
redis缓存淘汰策略(8种)
noeviction:不会驱逐任何key
allkeys-lru:所有key使用lru算法进行删除
volatitle-lru:对所有设置了过期时间的key使用lru算法进行删除
allkeys-random:对所有key随机删除
volatitle-random:对所有设置了过期时间的key随机删除
volatitle-ttl:删除马上要过期的key
allkeys-lfu:对所有key使用的lfu算法进行删除
volatitle-lfu:对所有设置了过期时间的key使用lfu算法进行删除
总结:
两个维度:过期键中筛(volatitle)、所有键中筛选(all)
四个维度:LRU、LFU、random、ttl
实际中最通用的是第二个,allkeys-lru
redis的LRU了解过吗?可否手写一个LRU算法
什么是LRU
Least Recently Used 的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的数据予以淘汰。
1、所谓缓存,必须要有读➕写两个操作,按照命中率的思路考虑,写操作➕读操作时间复杂度都需要为O(1)
2、特性要求:
2.1、必须要有顺序之分,一区分最近使用的和很久没使用的数据排序
2.2、写和读操作一次搞定
2.3、如果容量(坑位)满了要删除最不常用的数据,每次新访问还要吧新的数据插入到队头(按照业务你自己设定左右哪一边是对头)
查找快,插入快,删除快,且还需要先后排序
LRU的算法核心是哈希链表
本质就是HashMap➕DoubleLinkedList,时间复杂度是O(1),哈希表➕双向链表的结合体
手写LRU算法:
案例01:
参考LinkedHashMap
案例02:
不使用JDK

第二季:
1.谈谈对volatile的理解:
volatile是java虚拟机提供的轻量级的同步机制,主要有三大特性
1.1 保证可见性
private static void seeOKByVolatile() { MyData myData = new MyData(); //线程要操作资源类 new Thread(()->{ System.out.println(Thread.currentThread().getName() + "\t come in "); // 暂停一会 try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {throw new RuntimeException(e);} myData.addTO60(); System.out.println(Thread.currentThread().getName() + "\t update nember value" + myData.number); },"AAA").start(); // 第二个线程 main线程 while (myData.number == 0){// main线程一直等待循环,直到number值不等于零 } System.out.println(Thread.currentThread().getName() + "\t misson is over,main get number value" + myData.number); }
1.2 不保证原子性
1.3 禁止指令重排
2.JMM(java内存模型)
jmm(java内存模型java memory model,建成jmm)内存模型本身是一种抽象的概念并不真实存在,它描述的是一组规则或者规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)访问方式
JMM关于同步的规定:
1、线程解锁前,必须把共享变量的值刷新回主内存
2、线程加锁前,必须读取主内存的最新值到自己的工作内存
3、加锁解锁是同一把锁

JMM三大特性:
1、可见性
volatile可以保证可见性,即时通知其他线程值已经被修改
2、原子性
3、有序性
number++有几个操作?
三步操作,1、获得初始值 2、复制到各自内存执行加一操作 3、写回主内存
atomic原子类:
底层原理:CAS
在那些地方用到过volitile?
单例模式:
单机版:
public class SingletonDemo { private static SingletonDemoinstance= null; private SingletonDemo(){ System.out.println(Thread.currentThread().getName() + "\t 我是构造方法 singletonDemo()"); } public static SingletonDemo getInstance(){ if(instance== null){ instance= new SingletonDemo(); } return instance; } public static void main(String[] args) { System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance()); System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance()); System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance()); } }
并发多线程后,情况发生了很大的变化
DCL模式(Double Check Lock双端检索机制)public class SingletonDemo { private static SingletonDemoinstance= null; private SingletonDemo(){ System.out.println(Thread.currentThread().getName() + "\t 我是构造方法 singletonDemo()"); } //DCL模式(Double Check Lock双端检索机制) public static SingletonDemo getInstance(){ if(instance== null){ synchronized (SingletonDemo.class){ if(instance == null){ instance = new SingletonDemo(); } } } return instance; } }
但是在多线程情况下,为了性能和效果底层有指令重排,还不是百分百的正确
DCL(双端检索)机制不一定线程安全,原因是有指令重排序的存在
原因在于某一个线程执行到第一次检测,读取到instance不为null时,instance的引用对象可能没有完成初始化。instance = new SingletonDemo(); 可以分为三个步骤完成。

解决:加volatile
public class SingletonDemo { private static volatile SingletonDemoinstance= null; private SingletonDemo(){ System.out.println(Thread.currentThread().getName() + "\t 我是构造方法 singletonDemo()"); } //DCL模式(Double Check Lock双端检索机制) public static SingletonDemo getInstance(){ if(instance== null){ synchronized (SingletonDemo.class){ if(instance== null){ instance= new SingletonDemo(); } } } returninstance; } }
CAS:
比较并交换
Compare and Set
CAS的全称为ComepareAndSwap,他是一条CPU并发原语。
它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用Unsafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,用于CAS是一种系统原语,原语数语操作系统用语范畴,是由若干指令组成的,用于完成某个功能的过程,并且原语的执行必须是连续的,在执行的过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。

假设线程A和线程B两个线程同时执行getAnAddInt操作(分别跑在不同CPU上)
1、AtomicInteger里面Value原始值为3,即主内存中AtomicInteger的Value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存。
2、线程A通过高getIntVolatile(var1,var2)拿到value值3,这是线程A被挂起。
3、线程B也通过getIntVolatile(var1,var2)方法获取到value值3,此时刚好线程B没有被挂起并执行了compareAndSwapInt方法比较内存值也为3,成功修改内存值为4,线程B结束。
4、这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里 的数值3和主内存的数值4不一致。说明该值已经被其他线程抢先一步修改过了,那A线程本次修改失败。只能重现读取值重新比较,再来一次。
5、线程A重新获取value值,因为变量value和volatitle修饰,所以其他线程对他的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功。
Unsafe类 + CAS思想(自旋)
比较当前对象工作内存中的值和主物理内存中的值,如果相同则执行规定操作,否则继续比较直到主内存和工作内存中的值一致为止。
CAS应用:
CAS有3个操作数,内存值V,旧的预期值A,要修改的更新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
AtomicInteger为什么用CAS而不是Sychronized?
Unsafe类:
return unsafe.getAndAddInt(this, valueOffset, 1);valueOffset 内存偏移量,内存地址。表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。
this 当前对象Unsafe:
是CAS的核心类,由于java方法无法访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为java中CAS操作的执行依赖于Unsafe类的方法。
注意:Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务。
变量Value用volatile修饰,保证了多线程之间的内存可见性。
为什么用CAS而不用Sychronized?
Sychronized是加锁,线程安全得到了保证,同一时间内只有一个线程可以工作。
CAS没有加锁,可以反复通过CAS比较直到比较成功为止。
CAS的缺点:
循环时间长,开销大:如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。
只能保证一个共享变量的原子操作:
对于多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
ABA问题。

ABA问题:
CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差会导致数据的变化。比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成了A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。
尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。
原子引用更新:
public class AtomicReferenceDemo { public static void main(String[] args) { User z3 = new User("z3",22); User l4 = new User("l4",25); AtomicReference<User> atomicReference = new AtomicReference<>(); atomicReference.set(z3); System.out.println(atomicReference.compareAndSet(z3, l4) + "\t" + atomicReference.get().toString()); System.out.println(atomicReference.compareAndSet(z3, l4) + "\t" + atomicReference.get().toString()); } } @Data @ToString @AllArgsConstructor class User{ String userName; int age; }

如何规避ABA问题:
新增一种机制,修改版本号(类似时间戳)
public class ABADemo { static AtomicReference<Integer>atomicReference= new AtomicReference<>(100); static AtomicStampedReference<Integer>atomicStampedReference= new AtomicStampedReference<>(100, 1); public static void main(String[] args) { System.out.println("=================ABA问题的产生"); new Thread(() -> { atomicReference.compareAndSet(100, 101); atomicReference.compareAndSet(101, 100); }, "t1").start(); new Thread(() -> { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" +atomicReference.get().toString()); }, "t2").start(); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("=================ABA问题的解决"); new Thread(() -> { int stamp =atomicStampedReference.getStamp(); System.out.println(Thread.currentThread().getName() + "\t 第一次版本号:" + stamp); // 暂停一秒钟 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { throw new RuntimeException(e); } 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 第一次版本号:" + stamp); // 暂停一秒钟 try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { throw new RuntimeException(e); } boolean b =atomicStampedReference.compareAndSet(100, 101, stamp, stamp + 1); System.out.println(Thread.currentThread().getName() + "\t 修改成功否" + b); }, "t4").start(); } }
我们知道ArrayList是线程不安全,请编码写一个不安全的案例并给出解决方案:
当new ArrayLIst<Integer>() ,底层new的是一个数组。是一个初始值是10的空的list,类型是Object。扩容方法,扩原始值的一般,扩到15。
ArrayList线程不安全的例子:
故障现象:
public class ContainerNotSafeDemo { public static void main(String[] args) { ArrayList<String> list = new ArrayList<>(); for (int i = 1; i <=30 ; i++) { new Thread(()->{ list.add(UUID.randomUUID().toString().substring(0,8)); System.out.println(list); },String.valueOf(i)).start(); } //java.util.ConcurrentModificationException } }

Exception in thread "16" Exception in thread "24" java.util.ConcurrentModificationException
导致原因:
并发争抢修改导致,一个人正在写,另外一个人正在抢夺,导致异常
解决方案:
1、使用
List<String> list = new Vector<>(); 2、使用
List<String> list = Collections.synchronizedList(new ArrayList<>()); 3、使用
List<String> list = new CopyOnWriteArrayList<>(); 写时复制CopyOnWrite容器即写时复制的容器。往一个容器添加元素的时候,不直接往当前容器Object[]添加,而是先将当前容器Object[]进行copy,复制一个新的容器Objcet[] newelements,然后新的容器Object[] new Elements里面添加元素,添加完元素之后,再将原容器的引用指向新的容器setArray(newElements)。这样做的好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnwrite容器也是一种读写分离的思想,读和写不同的容器。
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); newElements[len] = e; setArray(newElements); return true; } finally { lock.unlock(); } }
Hashset是不是线程安全的:
hashset 是线程不安全的,
使用
Set<String> set = Collections.synchronizedSet(new HashSet<>())使用
Set<String> set = new CopyOnWriteArraySet<>();Hashset底层是什么:
hashset的底层数据结构就是hashmap,创建了一个初始值是16,负载因子是0.75的hashmap
public HashSet() { map = new HashMap<>(); }
hashset的add就是hashmap的put方法,key。value是一个为PRESENT的Object常量。value
是恒定的
public boolean add(E e) { return map.put(e,PRESENT)==null; }
hashMap和ConcurrentHashMAp<>()底层源码有什么不同
栈管运行,堆管存储
公平锁/非公平锁/可重入锁/递归锁/自旋锁 谈谈你的理解?
手写一个自旋锁
public class SpinLockDemo { // 原子引用线程 AtomicReference<Thread> atomicReference = new AtomicReference<>(); public void myLock() { Thread thread = Thread.currentThread(); System.out.println(Thread.currentThread().getName() + "\t come in "); while (!atomicReference.compareAndSet(null, thread)) { System.out.println(Thread.currentThread().getName() + "进行比较"); } System.out.println(Thread.currentThread().getName() + "加锁"); } public void myUnlock(){ Thread thread = Thread.currentThread(); atomicReference.compareAndSet(thread,null); System.out.println(Thread.currentThread().getName() + "解锁"); } public static void main(String[] args) { SpinLockDemo spinLockDemo = new SpinLockDemo(); new Thread(()->{ spinLockDemo.myLock(); try {TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {throw new RuntimeException(e);} spinLockDemo.myUnlock(); },"AA").start(); try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {throw new RuntimeException(e);} new Thread(()->{ spinLockDemo.myLock(); try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {throw new RuntimeException(e);} spinLockDemo.myUnlock(); },"BB").start(); } }
公平锁和非公平锁
公平锁:多个线程按照申请的顺序来获取锁,类似排队打饭,先来后到
非公平锁:多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发的情况下,有可能会造成优先级反转或者饥饿现象。
ReentrantLock默认非公平锁,非公平锁的优点在于吞吐量比公平锁大
对于Sychronized而言,也是一种非公平锁
可重入锁(又叫递归锁)
指的是同一线程外层函数获得锁后,内层递归函数仍然可以获取该锁的代码,
在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
也即是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块。
可重入锁的最大作用就是避免死锁。
自旋锁
是指尝试获取锁的线程不会立即阻塞,而是采用循环的方法去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU
独占锁(写锁)/共享锁(读锁)/互斥锁
独占锁:指该锁一次只能被一个线程所持有。对ReentrantLock和Synchronized而言都是独占锁。
共享锁:指该锁可以被锁哥线程所持有
对ReentrantReadWriteLock其读锁是共享锁,其写锁是独占锁。
读锁的共享锁可保证并发读诗非常高效的,读写,写读,写写的过程是互斥的。
写操作:原子 + 独占。中间整个过程必须是一个完整的统一体,中间不允许被分割
读写锁例子(缓存原理):
class MyCache{ private volatile Map<String,Object> map = new HashMap<>(); private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); //private Lock lock = new ReentrantLock(); // 写缓存三个操作,写、读、清空 public void put(String key,Object value){ rwLock.writeLock().lock(); try { System.out.println(Thread.currentThread().getName() + "\t 正在写入" + key); try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {throw new RuntimeException(e);} map.put(key,value); System.out.println(Thread.currentThread().getName() + "\t 写入完成" + key); }catch (Exception e){ e.printStackTrace(); }finally { rwLock.writeLock().unlock(); } } public void get(String key){ rwLock.readLock().lock(); try { System.out.println(Thread.currentThread().getName() + "\t 正在读取" ); try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {throw new RuntimeException(e);} Object result = map.get(key); System.out.println(Thread.currentThread().getName() + "\t 读取完成:" +result); }catch (Exception e){ e.printStackTrace(); }finally { rwLock.readLock().unlock(); } } } public class ReadWriteLockDemo { /** *读写锁demo *读锁的共享锁可保证并发读诗非常高效的,读写,写读,写写的过程是互斥的。 * */ public static void main(String[] args) { MyCache myCache =new MyCache(); for (int i = 1; i < 5; i++) { final int tempInt = i; new Thread(()->{ myCache.put(tempInt+ "",tempInt + ""); },String.valueOf(i)).start(); } for (int i = 1; i < 5; i++) { final int tempInt = i; new Thread(()->{ myCache.get(tempInt+""); },String.valueOf(i)).start(); } } }
CountDownLatch/CyclicBarrier/Semaphore使用过吗
CountDownLacth: 做减法,减到0 再去做。倒计时。
CountDownLacth主要有两个方法,当一个或多个线程调用await方法时,调用线程会被阻塞。其他线程调用countDown方法会讲计数器减1(调用countDown放啊的线程不会阻塞),当计数器的值变为0时,因调用wait方法被阻塞的线程会被唤醒,继续执行。
public class CountDownLatchDemo { public static void main(String[] args) throws InterruptedException { CountDownLatch count = new CountDownLatch(6); for (int i = 1; i <=6; i++) { new Thread(()->{ System.out.println(Thread.currentThread().getName() + "\t 上晚自习,离开教室"); count.countDown(); },String.valueOf(i)).start(); } count.await(); System.out.println(Thread.currentThread().getName() + "\t ****** 班长最后关门走人"); } }
CyclicBarrier
做加法
字面意思是可循环的(Cyclic)使用的屏障(Barrier),它要做的事情是,让一组线程达到一个屏障(也可以叫同步点)时被阻塞,知道最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过CyclicBarrier的await()方法。
Semaphore
信号量 (争车位)
信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制
阻塞队列知道吗:

当阻塞队列是空时,从队列获取元素的操作将会被阻塞。
当阻塞队列是满时,往队列里添加元素的操作将会被阻塞。
MQ消息中间件的核心底层原理就是阻塞队列
在很多领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程优惠自动被唤醒。
为什么需要BlockingQueue
好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了
在Concurrent包发布以前,在多线程环境下,我们每个程序员都必须要自己去控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度
BlockingQueue:
ArrayBlockingQueue: 由数组结构组成的有界阻塞队列
LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为Interger.MAX_VALUE)阻塞队列
PriorityBlockingQueue:支持优先级排序的无界阻塞队列
DelayQueue:使用优先级队列实现的延迟无界阻塞队列
SynchronousQueue: 不存储元素的阻塞队列,也即单个元素的队列
LinkedTransferQueue:由链表结构组成的无界阻塞队列。
LinkedBlockingDeque:有链表结构组成的双向阻塞队列

public class SynchronousQueueDemo { public static void main(String[] args) { BlockingQueue<String> blockingQueue = new SynchronousQueue<>(); new Thread(()->{ try { System.out.println(Thread.currentThread().getName() + "\t put 1"); blockingQueue.put("1"); System.out.println(Thread.currentThread().getName() + "\t put 2"); blockingQueue.put("2"); System.out.println(Thread.currentThread().getName() + "\t put 3"); blockingQueue.put("3"); } catch (InterruptedException e) { throw new RuntimeException(e); } },"AA").start(); new Thread(()->{ try { try {TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {throw new RuntimeException(e);} System.out.println(Thread.currentThread().getName() + "\t" + blockingQueue.take()); try {TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {throw new RuntimeException(e);} System.out.println(Thread.currentThread().getName() + "\t" + blockingQueue.take()); try {TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {throw new RuntimeException(e);} System.out.println(Thread.currentThread().getName() + "\t" + blockingQueue.take()); } catch (InterruptedException e) { throw new RuntimeException(e); } },"BB").start(); } }

特点是拿一个放一个,不拿不放,不存储消息。
阻塞队列有没有好的一面
不得不阻塞,如何管理
生产者消费者模式:
class ShareData{ //资源类 private int number = 0; private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); public void increment() throws Exception{ lock.lock(); try { //1、 判断 while (number!= 0){ // 等待,不能生产 condition.await(); } //2、干活 number++; System.out.println(Thread.currentThread().getName()+ "\t" + number); //3、通知唤醒 condition.signalAll(); }catch (Exception e){ e.printStackTrace(); }finally { lock.unlock(); } } public void decrement() throws Exception{ lock.lock(); try { //1、 判断 while (number== 0){ // 等待,不能生产 condition.await(); } //2、干活 number--; System.out.println(Thread.currentThread().getName()+ "\t" + number); //3、通知唤醒 condition.signalAll(); }catch (Exception e){ e.printStackTrace(); }finally { lock.unlock(); } } } public class ProdConsumer_TraditionDemo { /** *题目:一个初始值为0的变量,两个线程对其交替操作,一个加1,一个减1,来5轮。 * * 1、线程 操作 资源类 * 2、判断 干活 通知 * 3、防止虚假唤醒机制 * * */ public static void main(String[] args) { ShareData shareData =new ShareData(); new Thread(()->{ for (int i = 1; i <= 5; i++) { try { shareData.increment(); } catch (Exception e) { throw new RuntimeException(e); } } },"AA").start(); new Thread(()->{ for (int i = 1; i <= 5; i++) { try { shareData.decrement(); } catch (Exception e) { throw new RuntimeException(e); } } },"BB").start(); } }
Sychronized和Lock有什么区别?
1、原始构成:
Sychronized是关键字,属于JVM层面,monitorenter(底层是通过monitor对象完成,其实wait/notify等方法也依赖于monitor对象只有在同步块或者方法中才能调用wait/notify等方法)
Lock是具体类(java.util.concurrent.locks.lock)是api层面的锁
2、使用方法:
Sychronized不需要用户去手动释放锁,当Sychronized代码执行完后,系统会自动让线程释放对锁的占用。
ReentrantLock则需要用户去手动释放锁,若没有主动释放锁,就会有可能导致出现死锁现象。
3、等待是否可中断
Sychronized 不可中断,除非抛出异常或者正常运行完成
ReentrantLock 可以中断 1、设置超时方法tryLock(Long timeout,TimeUnit unit)
2、lockInterruptibly()放代码块中,调用interrupt()方法可中 断。
4、加锁是否公平
Sychronized 非公平锁
ReentrantLock两者都可以,默认非公平锁,构造方法可以穿入boolean值,true为公平锁,false为非公平锁。
5、锁绑定多个条件Condition
Sychronized 没有
ReentrantLock 用来实现分组唤醒需要唤醒的线程们,可以精确唤醒,而不是像Sychronized要么随机唤醒一个线程,要么唤醒全部线程。
Sychronized什么情况是对象锁?什么时候是全局锁为什么?
线程池:
Executors.newFixedThreadPool() :执行长期的任务,性能好很多Executors.newSingleThreadExecutor(); :一个任务一个任务执行的场景Executors.newCachedThreadPool() :执行很多短期异步的小程序或者负载较轻的服务阻塞队列的作用是什么:
使用无界阻塞队列会出现什么问题:
线程池的7大参数:
生产上如何配置合理参数:
谈谈线程池的拒绝策略:
等待队列已经排满了。再也塞不下新任务了。同时线程池中的max线程也达到了,无法继续为新任务服务。这个时候我们就需要拒绝策略机制合理的处理这个问题。
JDK内置的拒绝策略:
AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行
CallerRunsPolicy:调用者运行一种调节机制,该策略及不会抛弃任务,也不会抛出异常,而是将某些任务退回到调用者,从而降低新的任务流量。
DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前 任务加入队列中尝试再次提交当前任务。
DisCardPolicy:直接丢弃任务,不给予任何处理,也不抛出异常。如果允许任务丢失,只是最好的一种方案。
以上内置拒绝策略均实现了:RejectedExecutionHandler接口
【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
说明:使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
【强制】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors返回的线程池对象的弊端如下:
1)FixedThreadPool和SingleThreadPool:
允许请求的队列长度为Integer.MAX_VALUE,可能回堆积大量的请求,从而导致OOM
2)CachedThreadPool和ScheduledThreadPool
允许的创建线程数量为Integer.MAX_VALUE,可能回创建大量的线程,从而导致OOM
工作中如何使用线程池,是否自定义过线程池:
ExecutorService threadPool = new ThreadPoolExecutor(2, 5, 1L, TimeUnit.SECONDS, new LinkedBlockingDeque<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());
合理配置线程池你是如何考虑的:
CPU密集型:
CPU密集的意思是该任务需要大量的运算,二没有阻塞,CPU一直全速运行。CPU密集任务只有在真正的多喝CPU上才能得到加速(通过多线程),而在单核CPU上,无论你开几个模拟的多线程,该任务都不可能得到加速,因为CPU总的运算能力就那些。
CPU密集型任务配置尽可能的少的线程数量:一般公式:CPU核数+1个线程的线程池
IO密集型:
由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如CPU核数*2
参考公式:CPU核数/1 - 阻塞系数。阻塞系数在 0.8-0.9之间
比如8核CPU 8/1-0.9 = 80个线程数
死锁编码及定位分析:
产生死锁的主要原因:
死锁是两个或者多个以上的进程在执行的过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那他们都将无法推进下去,如果系统资源充足,进城的资源请求都能够得到满足,死锁的出现可能就很低,否则就会因为争夺有限的资源而陷入死锁。
JVM

常见的垃圾回收算法:
JVM从用分带收集的方式。
引用计数:有对象引用加一,没对象引用减一,知道为0为止。缺点:每次对对象赋值均要维护引用计数器,且计数器本身也有一定的消耗。较难处理循环引用。(JVM一般不采用这种方式)
复制:年轻代用。MinorGC(复制→清空→交换)
1、eden、SurvivorFrom复制到SurvivorTo,年龄+1
首先,当Eden区满的时候会出发一次GC,把还活着的对象拷贝到SurvivorFrom区,当Eden区再次出发GC的时候,会扫描Eden区和From区域,对这个两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域(如果有对象的年纪已经到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1
2、清空eden、SurvivorFrom
然后,清空Eden和SurvivorFrom中的对象,也即复制之后有交换,谁空谁是to
3、SurvivorTo和SurvivorFrom互换
最后,SurvivorTo和SurvivorFrom互换,原SurvivorTo成为下一次GC时候的SurvivorFrom区。部分对象会在From区和To区复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),如果最终还是存活,就存入老年代。
优点:不会产生内存碎片
缺点:浪费空间
标记清除:
先标记,在清除
优点:没有大面积复制,节约内存空间
缺点:导致内存碎片
标记整理:
标记:向标记清楚一样。压缩:再次扫描,并在一端华东存活对象。
1、JVM垃圾回收的时候如何确定垃圾?是否知道什么是GC Roots?
垃圾:简单来说就是内存中已经不再被使用到的空间就是垃圾。
java中,引用和对象是有关联的。如果要操作对象则必须用引用进行。因此,很显然一个简单的办法就是通过引用计数来判断一个对象是否可以被回收。简单说,给对象中添加一个引用计数器,每当一个地方引用它,计数器的值就+1,每当一个引用失效,计数器值-1,任何时刻计数器值为0的对象就是不可能在被使用的,那么这个对象就是可回收对象。那为什么主流的java虚拟机里面都没有这种算法呢?其中最主要的原因是它很难解决对象之间的互相循环引用的问题。
2、你说你做过JVM调优和参数配置,请问如何盘点查看JVM系统默认值
-Xms (初始的堆空间)
-Xmx (堆的最大值)
-Xss(初始的栈空间)
JVM的参数类型:标配参数、x参数、xx参数
标配参数:-version、-help、java =showversion
x参数:-Xint 解释执行、-Xcomp 第一次使用就编译成本地代码、 -Xmixed 混合模式
xx参数:
- Boolean类型:公式:-XX:+ 或者 - 某个属性值 + 表示开启 - 表示关闭
如何查看一个正在运行中的java程序它的某个jvm参数是否开启?具体值是多少?
jps 查看java的后台进程(查看初始值)
jinfo查看正在运行中的java信息 (查看初始值)
- kv设置类型:公式:属性key=属性值value
case:-XX:MetaspaceSize=128m、-XX:MaxTenuringThreshold=15(多少次可以升级老年区)
- 两个经典参数:-Xms 和 -Xmx
- Xms等价于- XX:InitialHeapSize -Xmx等价于-XX:MaxHeapSize
- 查看JVM默认值
-XX:+PrintFlagsInitial 主要查看初始默认值

-XX:+PrintFlagsFinal -version
3、你平时工作用过的JVM常用的基本参数有哪些
-Xms和-Xmx要配成一样
-Xms:初始大小内存,默认为物理内存的1/64,等价于-XX:InitialHeapSize
-Xmx:最大分配内存,默认为物理内存的1/4,等价于-XX:MaxHeapSize
-Xss: 设置耽搁线程栈的大小,一般默认为512k~1024k,等价于-XX:ThreadStackSize ,0是初始值,一般就是1024k
-Xmn:设置年轻代大小:
-XX:MetaspaceSize 设置元空间大小:元空间的本质和永久带类似,都是对JVM规范中方法区的实现。不过元空间与永久带最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
-Xms10m -Xmx10m -XX:MetaspaceSize=1024m -XX:+PrintFlagsFianl
-XX:+PrintGCDetails
-XX:SurvivorRatio
4、强引用、软引用、弱引用、虚引用分别是什么?
5、请谈谈你对OOM的认识
6、GC垃圾回收算法和垃圾收集器的关系?分别是什么请你谈谈?
7、怎么查看服务器默认的垃圾回收器是哪个?生产上如何配置垃圾回收器的?谈谈你对垃圾收集器的理解?
8、G1垃圾收集器
9、生产环境服务器变慢,诊断思路和氢能评估谈谈?
10、假如生产环境出现CPU占用过高,请你谈谈你的分析思路定位
11、GCRoot如何确定的,哪些对象可以作为GCRoot
要进行垃圾回收,如何判断一个对象是否可以被回收?
1、引用计数法
2、枚举根节点做可达性分析(根搜索路径算法)
为了解决引用计数法的循环引用问题,java使用了可达性分析的方法。所谓GCroots或者说tracingGC的‘根集合’就是一组必须活跃的引用。基本思路就是通过一系列名为‘GCRoots’的对象作为始点,从这个被称为GCroots的对象开始向下搜索,如果一个对象到GCroots没有任何引用链相连时,则说明此对象不可用。也即给定一个集合的引用作为根出发,通过引用关系便利对象图,能被遍历到的(可达到的)对象就被判定为存货,没有被遍历到的就自然被判定为死亡。
那些对象可以作为GCRoots?
1、虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
2、方法区中的类静态属性引用的对象
3、方法区中常量引用的对象
4、本地方法栈中JNI(Native方法)引用的对象
12、GCRoot如何分代的?没代用什么算法回收
- 作者:LiuJixue
- 链接:https://liujixue.cn/article/Q
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。





