一:多线程
1.ThreadLocal
好了正式开始.
ThreadLocal我也不想去背书,因为我也记不住,那他具体是怎么回事呢?
首先我们先看看这个对象是咋new出来的
ThreadLocal threadlocal = new ThreadLocal;
官方其实已经解释非常清晰明了了,就是维护当前线程内的局部变量,为什么这么说呢,看源码就能知道其所以然
//这是ThreadLocal内部的set方法
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程的ThreadLocalMap,由此可知Thread内部竟然还维护了一个ThreadLocalMap 对象
ThreadLocalMap map = getMap(t);
//判断当前对象是否为null
if (map != null) {
//如果不为null那么就把当前对象作为key,值作为value扔到这个map里
map.set(this, value);
} else {
//
createMap(t, value);
}
}
//这个对象其实就是获取当前线程内部的ThreadLocalMapMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
//如果当前线程内部的ThreadLocalMap为null 那么就自己初始化一个
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
丛上面的代码中我们可以看到,Thread 里面竟然还维护一个ThreadLocalMap,那么这个破玩应到底是干嘛的,它其实是ThreadLocal的一个静态内部类,他的作用简单的来说就是用来缓存当前线程的一个大map,重点就在这里看下面的源码
注意这是ThreadLocal内存泄露的原因
//重点来了哇,我靠存储一个弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
//对比弱引用这竟然是个强引用
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
上面是什么鬼为啥有强有弱,跟据java虚拟机的情况我们知道在java虚拟机里,只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。而对于强引用,如果一个对象具有强引用,那垃圾回收器绝不会回收它.
我靠重点又来了,我的ThreadLocal还在用,我还在往里放值呢,那么当我java虚拟机的内存不够用了,即使你把key回收了,那我的value还在,岂不是要oom(内存泄露了),
那么问题有来了呀,为甚么jdk元老还要这么设计
key 使用强引用:这样会导致一个问题,引用的 ThreadLocal 的对象被回收了,但是 ThreadLocalMap 还持有 ThreadLocal 的强引用,如果没有手动删除,ThreadLocal 不会被回收,则会导致内存泄漏。key 使用弱引用:这样的话,引用的 ThreadLocal 的对象被回收了,由于 ThreadLocalMap 持有 ThreadLocal 的弱引用,即使没有手动删除,ThreadLocal 也会被回收。value 在下一次 ThreadLocalMap 调用 set、get、remove 的时候会被清除。
比较以上两种情况,我们可以发现:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果都没有手动删除对应 key,都会导致内存泄漏,但是使用弱引用可以多一层保障,弱引用 ThreadLocal 不会内存泄漏,对应的 value 在下一次 ThreadLocalMap 调用 set、get、remove 的时候被清除,算是最优的解决方案。
原来如此豁然开朗,牛逼吹的可以
我靠,我又有疑问了,线程过多的话,那他是怎么保证这个map的hash不会冲突呢,你哪里来的这么多问题?因为我的疑问很多哇
那么我们看看下面的源码
这是ThreadLocalMap的源码
//下一个索引
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* 上一个索引.
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
卧槽这是什么鬼,
和HashMap的最大的不同在于,ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式,所谓线性探测,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。
副本是怎么回事,这个入口在哪里呢,丛ThreadLocal内部的静态类ThreadLocalMap构造函数入手吧
//这是一个公共构造,就是上面默认创建的
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
//这是一个私有构造根据官网解析,这个只供ThreadLocal内部createInheritedMap方法调用
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (Entry e : parentTable) {
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
ThreadLocal内部方法
丛官网上看只供Thread调用,卧槽绕来绕去又回到了Thread
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
到这里又恍然大悟,原来Thread里竟然维护了一个ThreadLocalMap,估计晚上不用睡觉了,那么让我们来看看Thread源码
//第一次调用
public Thread(Runnable target) {
this(null, target, "Thread-" + nextThreadNum(), 0);
}
//第二次调用注意最后一个参数inheritThreadLocals=true
public Thread(ThreadGroup group, Runnable target, String name,
long stackSize) {
this(group, target, name, stackSize, null, true);
}
//第三次调用 只看中文
private Thread(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
Thread parent = currentThread();
SecurityManager security = System.getSecurityManager();
if (g == null) {
if (security != null) {
g = security.getThreadGroup();
}
if (g == null) {
g = parent.getThreadGroup();
}
}
g.checkAccess();
if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(
SecurityConstants.SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}
g.addUnstarted();
this.group = g;
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
this.target = target;
setPriority(priority);
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
//就是丛这里初始化了ThreadLocalMap
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
this.stackSize = stackSize;
this.tid = nextThreadID();
}
原来这是一切的入口,那么接下来我们抛弃以前所有的东西,继续往下看ThreadLocalMap里的私有构造
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (Entry e : parentTable) {
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
丛上面我们可以看出
1、Thread 拥有属于自己的一个map,key为 ThreadLocal,value为值
2、ThreadLocal 获取值时实际上是从当前 Thread 的map中获取(以自己为key)
这也就是为什么 ThreadLocal 能在每个 Thread 中保持一个副本,实际上数据是放在 Thread 中的。
取值没有说附上源码,今天实在熬不动了,改天再说
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
研究了一晚上终于对ThreadLocal了解一点点了,
那么简单的概述一下就是在Tread创建的时候,ThreadLocalMap其实就已经存在了,只不过是为了不改变原来的基础上复制出来一份,放到ThreadLocal里面,作为副本便于我们操作,而真正的值却没有改变!
使用场景
比如数据库连接,或日期转化SimpleDateFormate
这篇文章很好可以看一下
https://www.cnblogs.com/zz-ksw/p/12684877.html
2.多线程的5个状态
1).新建状态
当用new操作符创建一个线程时。此时程序还没有开始运行线程中的代码。
2).就绪状态
一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的start()方法。当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法。当start()方法返回后,线程就处于就绪状态。
处于就绪状态的线程并不一定立即运行run()方法,线程还必须同其他线程竞争CPU时间,只有获得CPU时间才可以运行线程。因为在单CPU的计算机系统中,不可能同时运行多个线程,一个时刻仅有一个线程处于运行状态。因此此时可能有多个线程处于就绪状态。对多个处于就绪状态的线程是由Java运行时系统的线程调度程序来调度的。
3).运行状态(running)
当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法。
4).阻塞状态(blocked)
线程运行过程中,可能由于各种原因进入阻塞状态:
①线程通过调用sleep方法进入睡眠状态;
②线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者;
③线程试图得到一个锁,而该锁正被其他线程持有;
④线程在等待某个触发条件;
所谓阻塞状态是正在运行的线程没有运行结束,暂时让出CPU,这时其他处于就绪状态的线程就可以获得CPU时间,进入运行状态。
==============================================
堵塞状态是前述四种状态中最有趣的,值得我们作进一步的探讨。线程被堵塞可能是由下述五方面的原因造成的:
(1) 调用sleep(毫秒数),使线程进入"睡眠"状态。在规定的时间内,这个线程是不会运行的。
(2) 用suspend()暂停了线程的执行。除非线程收到resume()消息,否则不会返回"可运行"状态。
(3) 用wait()暂停了线程的执行。除非线程收到nofify()或者notifyAll()消息,否则不会变成"可运行"(是的,这看起来同原因2非常相象,但有一个明显的区别是我们马上要揭示的)。
(4) 线程正在等候一些IO(输入输出)操作完成。
(5) 线程试图调用另一个对象的"同步"方法,但那个对象处于锁定状态,暂时无法使用。
5).死亡状态(dead)
有两个原因会导致线程死亡:
①run方法正常退出而自然死亡;
②一个未捕获的异常终止了run方法而使线程猝死;
为了确定线程在当前是否存活着(就是要么是可运行的,要么是被阻塞了),需要使用isAlive方法,如果是可运行或被阻塞,这个方法返回true;如果线程仍旧是new状态且不是可运行的,或者线程死亡了,则返回false。
线程池有几种状态
3.sleap 和 wait的区别
对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。
sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。
在调用sleep()方法的过程中,线程不会释放对象锁。
而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备
获取对象锁进入运行状态。
4.如何让线程交替执行
方法一:
创建一个ReentrantLock对象,有几个线程通过ReentrantLock对象创建几个Condition,Condition类也可以实现等待/通知模式。
用notify()通知时,JVM会随机唤醒某个等待的线程, 使用Condition类可以进行选择性通知, Condition比较常用的两个方法:
● await()会使当前线程等待,同时会释放锁,当其他线程调用signal()时,线程会重新获得锁并继续执行。
● signal()用于唤醒一个等待的线程。
在调用Condition的await()/signal()方法前,也需要线程持有相关的Lock锁,调用await()后线程会释放这个锁,在singal()调用后会从当前Condition对象的等待队列中,唤醒 一个线程,唤醒的线程尝试获得锁, 一旦获得锁成功就继续执行。
package com.cs.threadDemo1;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadOrderWithCondition {
private static final ReentrantLock LOCK = new ReentrantLock();
private static final Condition C_A = LOCK.newCondition();
private static final Condition C_B = LOCK.newCondition();
private static final Condition C_C = LOCK.newCondition();
/**
* init for A to run first
*/
private volatile int flag = 'A';
Runnable a = () -> {
while (true) {
LOCK.lock();
if (flag == 'A') {
System.out.println("A");
flag = 'B';
// signal B to run
C_B.signal();
} else {
try {
// block and wait signal to invoke
C_A.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
LOCK.unlock();
}
};
Runnable b = () -> {
while (true) {
LOCK.lock();
if (flag == 'B') {
System.out.println("B");
flag = 'C';
// signal C to run
C_C.signal();
} else {
try {
// block and wait signal to invoke
C_B.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
LOCK.unlock();
}
};
Runnable c = () -> {
while (true) {
LOCK.lock();
if (flag == 'C') {
System.out.println("C");
flag = 'A';
// signal A to run
C_A.signal();
} else {
try {
// block and wait signal to invoke
C_C.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
LOCK.unlock();
}
};
public void runTest() {
Thread threadA = new Thread(a);
Thread threadB = new Thread(b);
Thread threadC = new Thread(c);
threadA.start();
threadB.start();
threadC.start();
}
public static void main(String[] args) {
ThreadOrderWithCondition o = new ThreadOrderWithCondition();
o.runTest();
}
}
方法二:
用java原生的wait,notify
package com.cs.threadDemo1;
class Num {
int i=1;
boolean flag = false; //两个线程,交替执行的一个标志
}
//打印奇数的线程
class PrintQi implements Runnable{
Num num ;
public PrintQi(Num num)
{
this.num = num;
}
public void run()
{
while(num.i<= 100)
{
synchronized (num) {
if(num.flag)
{
try {
num.wait();
} catch (Exception e) {
}
}
else {
System.out.println("奇数"+num.i);
num.i++;
num.flag = true;
num.notify();
}
}
}
}
}
//打印偶数的线程
class PrintOu implements Runnable{
Num num;
public PrintOu(Num num) {
this.num = num;
}
public void run()
{
while(num.i<=100)
{
synchronized (num)/* 必须要用一把锁对象,这个对象是num*/ {
if(!num.flag)
{
try
{
num.wait(); //操作wait()函数的必须和锁是同一个
} catch (Exception e)
{}
}
else {
System.out.println("oushu-----"+num.i);
num.i++;
num.flag = false;
num.notify();
}
}
}
}
}
//主函数
public class main {
public static void main(String[] args) {
Num num = new Num(); //声明一个资源
PrintQi pQi = new PrintQi(num);
PrintOu pOu = new PrintOu(num);
Thread aThread = new Thread(pQi);
Thread bThread = new Thread(pOu);
aThread.start();
bThread.start();
}
}
5.创建线程有哪几种方式
1.继承 Thread 类
2.实现 Runnable 接口
3.使用 Executor 框架
4.使用 FutureTask
最简单的两种方式
1.继承 Thread 类
2.实现 Runnable 接口
比较实用的两种方式
3.使用 Executor 框架
Executor 框架是 Java5 之后引进的,在 Java 5 之后,通过 Executor 来启动线程比使用 Thread 的 start 方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免 this 逃逸问题。
补充:this 逃逸是指在构造函数返回之前其他线程就持有该对象的引用. 调用尚未构造完全的对象的方法可能引发令人疑惑的错误。
Executor 框架不仅包括了线程池的管理,还提供了线程工厂、队列以及拒绝策略等,Executor 框架让并发编程变得更加简单。
为了能搞懂如何使用 Executor 框架创建
Executor 框架结构(主要由三大部分组成)
1) 任务(Runnable /Callable)
执行任务需要实现的 Runnable 接口 或 Callable接口。Runnable 接口或 Callable 接口 实现类都可以被 ThreadPoolExecutor 或 ScheduledThreadPoolExecutor 执行。
2) 任务的执行(Executor)
如下图所示,包括任务执行机制的核心接口 Executor ,以及继承自 Executor 接口的 ExecutorService 接口。ThreadPoolExecutor 和 ScheduledThreadPoolExecutor 这两个关键类实现了 ExecutorService 接口。
4.使用 FutureTask
6.线程池有哪些
1.newCachedThreadPool创建一个可缓存线程池程
2.newFixedThreadPool 创建一个定长线程池
3.newScheduledThreadPool 创建一个周期性执行任务的线程池
4.newSingleThreadExecutor 创建一个单线程化的线程池
1.newCachedThreadPool,是一种线程数量不定的线程池,并且其最大线程数为Integer.MAX_VALUE,这个数是很大的,一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。但是线程池中的空闲线程都有超时限制,这个超时时长是60秒,超过60秒闲置线程就会被回收。调用execute将重用以前构造的线程(如果线程可用)。这类线程池比较适合执行大量的耗时较少的任务,当整个线程池都处于闲置状态时,线程池中的线程都会超时被停止,执行第二个任务的时候第一个任务已经完成,会复用执行第一个任务的线程,不用每次新建线程。
2.newFixedThreadPool 创建一个指定工作线程数量的线程池,每当提交一个任务就创建一个工作线程,当线程 处于空闲状态时,它们并不会被回收,除非线程池被关闭了,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列(没有大小限制)中。由于newFixedThreadPool只有核心线程并且这些核心线程不会被回收,这样它更加快速底相应外界的请求。等待前面线程执行完在执行.
3.newScheduledThreadPool 创建一个线程池,它的核心线程数量是固定的,而非核心线程数是没有限制的,并且当非核心线程闲置时会被立即回收,它可安排给定延迟后运行命令或者定期地执行。这类线程池主要用于执行定时任务和具有固定周期的重复任务。
4.newSingleThreadExecutor这类线程池内部只有一个核心线程,以无界队列方式来执行该线程,这使得这些任务之间不需要处理线程同步的问题,它确保所有的任务都在同一个线程中按顺序中执行,并且可以在任意给定的时间不会有多个线程是活动的。
7.使用线程池有哪些优点
1.重用线程池的线程,避免因为线程的创建和销毁锁带来的性能开销
2.有效控制线程池的最大并发数,避免大量的线程之间因抢占系统资源而阻塞
3.能够对线程进行简单的管理,并提供一下特定的操作如:可以提供定时、定期、单线程、并发数控制等功能
8.线程池的状态有哪些
线程池的5种状态:Running、ShutDown、Stop、Tidying、Terminated。
1、RUNNING
(1) 状态说明:线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。
(2) 状态切换:线程池的初始化状态是RUNNING。换句话说,线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0!
2、SHUTDOWN
(1) 状态说明:线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。
(2) 状态切换:调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。
3、STOP
(1) 状态说明:线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
(2) 状态切换:调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。
4、TIDYING
(1) 状态说明:当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。
(2) 状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。
当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。
5、 TERMINATED
(1) 状态说明:线程池彻底终止,就变成TERMINATED状态。
(2) 状态切换:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。
9.锁
公平锁:是指多个线程按照申请锁的顺序来获取锁
非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发的情况下,有可能会造成优先级反转或者饥饿现象
公平锁/非公平锁
并发包中ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁和非公平锁,默认是非公平锁
关于两者的区别:
公平锁:Threads acquire a fair lock in the order in which they requested it
公平锁,就是很公平,在并发环境中,每个线程在获取锁时会查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则丛队列中取到自己
非公平锁:比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式
ReentrantLock而言,
通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比非公平锁大
对于Synchronized而言,也是一种非公平锁
可重入锁(也称递归锁) 【ReentrantLock 和 Synchronized 都是可重入锁】
指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁
也即是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块.
可重入锁最大的作用是避免死锁
堆管理存储 栈管理运行
自旋锁--demo
=========================================================================================================
import java.util.concurrent.atomic.AtomicReference;
public class SpinLockDemo {
AtomicReference<Thread> atomicReference = new AtomicReference<Thread>();
public void lock(){
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "==----- lock");
while (!atomicReference.compareAndSet(null,thread)){
System.out.println(thread.getName() + "====");
}
}
public void unlock() {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "==----- unlock");
atomicReference.compareAndSet(thread,null);
}
public static void main(String[] args) {
SpinLockDemo spinLockDemo = new SpinLockDemo();
new Thread(new Runnable() {
@Override
public void run() {
spinLockDemo.lock();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
spinLockDemo.unlock();
}
},"A").start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(new Runnable() {
@Override
public void run() {
spinLockDemo.lock();
spinLockDemo.unlock();
}
},"B").start();
}
}
=========================================================================================================
独占锁(写锁)/共享锁(读锁)/互斥锁
独占锁:指该锁一次只能被一个线程所持有。对ReentrantLock和Synchronized而言都是独占锁
共享锁:指该锁可以被多个线程所持有
ReentrantReadWriteLock其读锁是共享锁,其写锁是独占锁.
读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的.
CounttDownLatch有什么用?
允许一个或多个线程等待直到在其他线程中执行的一组操作完成的同步辅助(类)
CountDownLatch 用给定的计数初始化。await( ) 方法阻塞,直到由于 countDown( ) 方法的而导致当前计数达到零,之后所有等待线程被释放,并且任何后续的 await 调用立即返回。 这是一个一次性的现象 - 计数无法重置。 – 这是jdk文档的说法
说白了,可以把它看成是一个内部维护着一个count的计数器,只不过对这个计数器的操作都是原子操作,即同时只能有一个线程去操作这个计数器.
CountDownLatch通过构造函数传入一个初始计数值,调用者可以通过调用CounDownLatch对象的cutDown()方法,来使计数减1;如果调用对象上的await()方法,那么调用者就会一直阻塞在这里,直到别人通过cutDown方法. 当计数减到0时,才可以继续执行。
CountDownLatch使用例子?
=========================================================================================================
import java.util.concurrent.CountDownLatch;
/** 直到CountDownLatch 总数减到0为止*/
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
int finalI = i;
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("第 " + finalI + "个线程");
countDownLatch.countDown();
}
}).start();
}
countDownLatch.await();
System.out.println("main 线程");
}
}
=========================================================================================================
CountDownLatch使用场景?
确保某个计算在其需要的所有资源都被初始化之后才继续执行.
确保某个服务在其依赖的所有其他服务都已启动后才启动.
等待知道某个操作的所有者都就绪在继续执行。
CyclicBarrier
定义几个线程执行之后执行配置任务
=========================================================================================================
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
/** CyclicBarrier parties指代 多少为一批 可以用于多线程计算数据,最后合并计算结果的场景*/
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(6,()->{
System.out.println(Thread.currentThread().getName() + "------------------------执行了");
});
for (int i = 0; i < 6; i++) {
final int temp = i;
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "=================== 线程要执行了");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "=================== 线程执行完了");
},i + "").start();
}
}
}
=========================================================================================================
CountDownLatch、CyclicBarrier 原理和区别?
原理:
CountDownLatch:
线程调用CountDownLatch await方法直到计数器减为0.
主要通过计数器,在调用CountDownLatch wait方法等时候,计数器减一,
CyclicBarrier:
再CyclicBarrier内部定义了一个Lock对象,每当一个线程调用await方法的时候,计数器减1,同时判断计数器是否为0,为0执行runable方法,重置栅栏,并唤醒所有在lock队列里面等待的线程,否则进入lock的等待队列。
区别:
CyclicBarrier强调的是n个线程相互等待,直到完成再执行任务,计数器可以重置,复用,所以叫循环栅栏。
CountDownLatch允许一个或多个线程等待直到其他线程完成操作,只能使用一次。
Semaphore(资源抢占)
=========================================================================================================
import java.util.concurrent.Semaphore;
public class SemaphoreDemo {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(2);
for (int i = 0; i < 8; i++) {
new Thread(()->{
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " ======抢到资源");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release();
}
System.out.println(Thread.currentThread().getName() + "归还资源");
},String.valueOf(i)).start();
}
}
}
=========================================================================================================
Semaphore的主要方法摘要:
void acquire():从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断。
void release():释放一个许可,将其返回给信号量。
int availablePermits():返回此信号量中当前可用的许可数。
boolean hasQueuedThreads():查询是否有线程正在等待获取。
可以理解成强车位
9.阻塞队列
ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序.
LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO(先进先出)排序元素,吞吐量通常高于ArrayBlocakingQueue.
SynchronousQueue:一个不存储元素的阻塞队列.每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高
10.redis的持久化策略
Redis持久化操作分为两种:一种是AOF,一种是RDB
快照RDB持久化机制
Redis可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis主从结构,主要用来提高Redis性能),还可以将快照留在原地以便重启服务器的时候使用。
快照持久化是Redis默认采用的持久化方式,在redis.conf配置文件中默认有此下配置:
---------------------------------------------------------------------------------------------
save 900 1 #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
---------------------------------------------------------------------------------------------
根据配置,快照将被写入dbfilename选项指定的文件里面,并存储在dir选项指定的路径上面。如果在新的快照文件创建完毕之前,Redis、系统或者硬件这三者中的任意一个崩溃了,那么Redis将丢失最近一次创建快照写入的所有数据。
举个例子:假设Redis的上一个快照是2:35开始创建的,并且已经创建成功。下午3:06时,Redis又开始创建新的快照,并且在下午3:08快照创建完毕之前,有35个键进行了更新。如果在下午3:06到3:08期间,系统发生了崩溃,导致Redis无法完成新快照的创建工作,那么Redis将丢失下午2:35之后写入的所有数据。另一方面,如果系统恰好在新的快照文件创建完毕之后崩溃,那么Redis将丢失35个键的更新数据。
创建快照的办法有如下几种:
BGSAVE命令: 客户端向Redis发送 BGSAVE命令 来创建一个快照。对于支持BGSAVE命令的平台来说(基本上所有平台支持,除了Windows平台),Redis会调用fork来创建一个子进程,然后子进程负责将快照写入硬盘,而父进程则继续处理命令请求。
SAVE命令: 客户端还可以向Redis发送 SAVE命令 来创建一个快照,接到SAVE命令的Redis服务器在快照创建完毕之前不会再响应任何其他命令。SAVE命令不常用,我们通常只会在没有足够内存去执行BGSAVE命令的情况下,又或者即使等待持久化操作执行完毕也无所谓的情况下,才会使用这个命令。
save选项: 如果用户设置了save选项(一般会默认设置),比如 save 60 10000,那么从Redis最近一次创建快照之后开始算起,当“60秒之内有10000次写入”这个条件被满足时,Redis就会自动触发BGSAVE命令。
SHUTDOWN命令: 当Redis通过SHUTDOWN命令接收到关闭服务器的请求时,或者接收到标准TERM信号时,会执行一个SAVE命令,阻塞所有客户端,不再执行客户端发送的任何命令,并在SAVE命令执行完毕之后关闭服务器。
一个Redis服务器连接到另一个Redis服务器: 当一个Redis服务器连接到另一个Redis服务器,并向对方发送SYNC命令来开始一次复制操作的时候,如果主服务器目前没有执行BGSAVE操作,或者主服务器并非刚刚执行完BGSAVE操作,那么主服务器就会执行BGSAVE命令
如果系统真的发生崩溃,用户将丢失最近一次生成快照之后更改的所有数据。因此,快照持久化只适用于即使丢失一部分数据也不会造成一些大问题的应用程序。不能接受这个缺点的话,可以考虑AOF持久化。
AOF持久化:
与快照持久化相比,AOF持久化 的实时性更好,因此已成为主流的持久化方案。默认情况下Redis没有开启AOF(append only file)方式的持久化,可以通过appendonly参数开启:
appendonly yes
开启AOF持久化后每执行一条会更改Redis中的数据的命令,Redis就会将该命令写入硬盘中的AOF文件。AOF文件的保存位置和RDB文件的位置相同,都是通过dir参数设置的,默认的文件名是appendonly.aof。
在Redis的配置文件中存在三种同步方式,它们分别是:
appendfsync always #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
appendfsync everysec #每秒钟同步一次,显示地将多个写命令同步到硬盘
appendfsync no #让操作系统决定何时进行同步
appendfsync always 可以实现将数据丢失减到最少,不过这种方式需要对硬盘进行大量的写入而且每次只写入一个命令,十分影响Redis的速度。另外使用固态硬盘的用户谨慎使用appendfsync always选项,因为这会明显降低固态硬盘的使用寿命。
为了兼顾数据和写入性能,用户可以考虑 appendfsync everysec选项 ,让Redis每秒同步一次AOF文件,Redis性能几乎没受到任何影响。而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操作的时候,Redis还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。
appendfsync no 选项一般不推荐,这种方案会使Redis丢失不定量的数据而且如果用户的硬盘处理写入操作的速度不够的话,那么当缓冲区被等待写入的数据填满时,Redis的写入操作将被阻塞,这会导致Redis的请求速度变慢。
虽然AOF持久化非常灵活地提供了多种不同的选项来满足不同应用程序对数据安全的不同要求,但AOF持久化也有缺陷——AOF文件的体积太大。
重写/压缩AOF
AOF虽然在某个角度可以将数据丢失降低到最小而且对性能影响也很小,但是极端的情况下,体积不断增大的AOF文件很可能会用完硬盘空间。另外,如果AOF体积过大,那么还原操作执行时间就可能会非常长。
为了解决AOF体积过大的问题,用户可以向Redis发送 BGREWRITEAOF命令 ,这个7
auto-aof-rewrite-min-size 64mb
无论是AOF持久化还是快照持久化,将数据持久化到硬盘上都是非常有必要的,但除了进行持久化外,用户还必须对持久化得到的文件进行备份(最好是备份到不同的地方),这样才能尽量避免数据丢失事故发生。如果条件允许的话,最好能将快照文件和重新重写的AOF文件备份到不同的服务器上面。
随着负载量的上升,或者数据的完整性变得越来越重要时,用户可能需要使用到复制特性。
11.spring分布式事务
==========
12.==和equels的区别?
1.1 基本概念区分
1) 、对于==,比较的是值是否相等
如果作用于基本数据类型的变量,则直接比较其存储的 值是否相等,
如果作用于引用类型的变量,则比较的是所指向的对象的地址是否相等。
其实==比较的不管是基本数据类型,还是引用数据类型的变量,比较的都是值,只是引用类型变量存的值是对象的地址
2) 、对于equals方法,比较的是是否是同一个对象
首先,equals()方法不能作用于基本数据类型的变量,
另外,equals()方法存在于Object类中,而Object类是所有类的直接或间接父类,所以说所有类中的equals()方法都继承自Object类,在没有重写equals()方法的类中,调用equals()方法其实和使用==的效果一样,也是比较的是引用类型的变量所指向的对象的地址,不过,Java提供的类中,有些类都重写了equals()方法,重写后的equals()方法一般都是比较两个对象的值,比如String类。
Object类equals()方法源码:
public boolean equals(Object obj) {
return (this == obj);
}
String类equals()方法源码:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
1.2 举几个例子就知道了
示例1:
int x = 10;
int y = 10;
String str1 = new String("abc");
String str2 = new String("abc");
System.out.println(x == y); // true
System.out.println(str1 == str2); // false
System.out.println(str1.equals(str2)); // true
示例2:
String str3 = "abc";
String str4 = "abc";
System.out.println(str3 == str4); // true
各位客官看懂了吗?
其实,str3与str4相等的原因是用到了内存中的常量池,当运行到str3创建对象时,如果常量池中没有,就在常量池中创建一个对象"abc",第二次创建的时候,就直接使用,所以两次创建的对象其实是同一个对象,它们的地址值相等。
再来一个:
示例3:
先定义学生Student类
package com.zwwhnly.springbootaction;
public class Student {
private int age;
public Student(int age) {
this.age = age;
}
}
然后创建两个Student实例来比较
Student student1 = new Student(23);
Student student2 = new Student(23);
System.out.println(student1.equals(student2)); // false
此时equals方法调用的是基类Object类的equals()方法,也就是==比较,所以返回false。
然后我们重写下equals()方法,只要两个学生的年龄相同,就认为是同一个学生。
package com.zwwhnly.springbootaction;
public class Student {
private int age;
public Student(int age) {
this.age = age;
}
@Override
public boolean equals(Object obj) {
Student student = (Student) obj;
return this.age == student.age;
}
}
此时再比较刚刚的两个实例,返回true。
Student student1 = new Student(23);
Student student2 = new Student(23);
System.out.println(student1.equals(student2)); // true
13.hashcode和equels的关系?
1.1 基本概念区分
1) 、对于==,比较的是值是否相等
如果作用于基本数据类型的变量,则直接比较其存储的 值是否相等,
如果作用于引用类型的变量,则比较的是所指向的对象的地址是否相等。
public class DemoTest {
public static void main(String[] args) {
Object obj = new Object();
System.out.println(obj.hashCode());
}
}
通过调用hashCode()方法获取对象的hash值。
02、equals介绍
equals它的作用也是判断两个对象是否相等,如果对象重写了equals()方法,比较两个对象的内容是否相等;如果没有重写,比较两个对象的地址是否相同,
价于“==”。同样的,equals()定义在JDK的Object.java中,这就意味着Java中的任何类都包含有equals()函数。
public class DemoTest {
public static void main(String[] args) {
Object obj = new Object();
System.out.println(obj.equals(obj));
}
}
03、hashCode() 和 equals() 有什么关系?
接下来,我们讨论另外一个话题。网上很多文章将 hashCode() 和 equals 关联起来,有的讲的不透彻,有误导读者的嫌疑。在这里,我们梳理了一下
“hashCode() 和 equals()的关系”。我们以“类的用途”来将“hashCode() 和 equals()的关系”分2种情况来说明。
3.1、不会创建“类对应的散列表”
这里所说的“不会创建类对应的散列表”是说:我们不会在HashSet, HashTable, HashMap等等这些本质是散列表的数据结构中,用到该类。
例如,不会创建该类的HashSet集合。
在这种情况下,该类的“hashCode() 和 equals() ”没有半毛钱关系的!
equals() 用来比较该类的两个对象是否相等,而hashCode() 则根本没有任何作用,所以,不用理会hashCode()。
public class DemoNormalTest {
public static void main(String[] args) {
// 新建2个相同内容的Person对象,
// 再用equals比较它们是否相等
Person p1 = new Person("eee", 100);
Person p2 = new Person("eee", 100);
Person p3 = new Person("aaa", 200);
System.out.printf("p1.equals(p2) : %s; p1(%d) p2(%d)\n", p1.equals(p2), p1.hashCode(), p2.hashCode());
System.out.printf("p1.equals(p3) : %s; p1(%d) p3(%d)\n", p1.equals(p3), p1.hashCode(), p3.hashCode());
}
private static class Person {
private String name;
private int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
/*** 重写equals方法 */
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
// 如果是同一个对象返回true,反之返回false
if (this == obj) {
return true;
}
// 判断是否类型相同
if (this.getClass() != obj.getClass()) {
return false;
}
Person person = (Person) obj;
return name.equals(person.name) && age == person.age;
}
}
}
结果:
p1.equals(p2) : true; p1(2018699554) p2(1311053135)p1.equals(p3) : false; p1(2018699554) p3(1735600054)
从结果也可以看出:p1和p2相等的情况下,hashCode()也不一定相等。
3.2、会创建“类对应的散列表”
这里所说的“会创建类对应的散列表”是说:我们会在HashSet, HashTable, HashMap等等这些本质是散列表的数据结构中,用到该类。例如,创建该类的HashSet集合。
在这种情况下,该类的“hashCode() 和 equals() ”是有关系的:
如果两个对象相等,那么它们的hashCode()值一定相同。这里的相等是指,通过equals()比较两个对象时返回true。
如果两个对象hashCode()相等,它们并不一定相等。因为在散列表中,hashCode()相等,即两个键值对的哈希值相等。然而哈希值相等,并不一定能得出键值对相等,此时就出现所谓的哈希冲突场景。
举个例子
public class DemoConflictTest {
public static void main(String[] args) {
// 新建Person对象,
Person p1 = new Person("eee", 100);
Person p2 = new Person("eee", 100);
Person p3 = new Person("aaa", 200);
// 新建HashSet对象
HashSet<Person> set = new HashSet<>();
set.add(p1);
set.add(p2);
set.add(p3);
// 比较p1 和 p2, 并打印它们的hashCode()
System.out.printf("p1.equals(p2) : %s; p1(%d) p2(%d)\n", p1.equals(p2), p1.hashCode(), p2.hashCode());
// 打印set
System.out.printf("set:%s\n", set);
}
private static class Person {
private String name;
private int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
/*** 重写toString方法*/
@Override
public String toString() {
return "("+name + ", " +age+")";
}
/*** 重写equals方法 */
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
// 如果是同一个对象返回true,反之返回false
if (this == obj) {
return true;
}
// 判断是否类型相同
if (this.getClass() != obj.getClass()) {
return false;
}
Person person = (Person) obj;
return name.equals(person.name) && age == person.age;
}
}
}
运行结果:
p1.equals(p2) : true; p1(2018699554) p2(1311053135)set:[(eee, 100), (aaa, 200), (eee, 100)]
结果分析:
我们重写了Person的equals()。但是,很奇怪的发现:HashSet中仍然有重复元素:p1 和 p2。为什么会出现这种情况呢?
这是因为虽然p1 和 p2的内容相等,但是它们的hashCode()不等;所以,HashSet在添加p1和p2的时候,认为它们不相等。
举个例子,我们同时覆盖equals() 和 hashCode()方法。
public class DemoConflictTest {
public static void main(String[] args) {
// 新建Person对象,
Person p1 = new Person("eee", 100);
Person p2 = new Person("eee", 100);
Person p3 = new Person("aaa", 200);
Person p4 = new Person("EEE", 100);
// 新建HashSet对象
HashSet<Person> set = new HashSet<>();
set.add(p1); set.add(p2);
set.add(p3); set.add(p4);
// 比较p1 和 p2, 并打印它们的hashCode()
System.out.printf("p1.equals(p2) : %s; p1(%d) p2(%d)\n", p1.equals(p2), p1.hashCode(), p2.hashCode());
// 比较p1 和 p4, 并打印它们的hashCode()
System.out.printf("p1.equals(p4) : %s; p1(%d) p4(%d)\n", p1.equals(p4), p1.hashCode(), p4.hashCode());
// 打印set
System.out.printf("set:%s\n", set);
}
private static class Person {
private String name;
private int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
/*** 重写toString方法 */
@Override
public String toString() {
return "(" + name + ", " + age + ")";
}
/** * 重写equals方法 */
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
// 如果是同一个对象返回true,反之返回false
if (this == obj) {
return true;
}
// 判断是否类型相同
if (this.getClass() != obj.getClass()) {
return false;
}
Person person = (Person) obj;
return name.equals(person.name) && age == person.age;
}
/** * 重写hashCode方法 */
@Override
public int hashCode() {
int nameHash = name.toUpperCase().hashCode();
return nameHash ^ age;
}
}
}
运行结果:
p1.equals(p2) : true; p1(68545) p2(68545)p1.equals(p4) : false; p1(68545) p4(68545)set:[(eee, 100), (EEE, 100), (aaa, 200)]
结果分析:
这下,equals()生效了,HashSet中没有重复元素。 比较p1和p2,我们发现:它们的hashCode()相等,通过equals()比较它们也返回true。所以,p1和p2被视为相等。 比较p1和p4,我们发现:虽然它们的hashCode()相等;但是,通过equals()比较它们返回false。所以,p1和p4被视为不相等。
为什么HashSet会用到hashCode()呢?
以看出,hashSet使用的是hashMap的put方法,而hashMap的put方法,使用hashCode()用key作为参数计算出hash值,然后进行比较,如果相同,再通过equals()比较key值是否相同,如果相同,返回同一个对象。
所以,如果类使用再散列表的集合对象中,要判断两个对象是否相同,除了要覆盖equals()之外,也要覆盖hashCode()函数。否则,equals()无效。
04、有哪些覆写hashCode的诀窍
一个好的hashCode的方法的目标:为不相等的对象产生不相等的散列码,同样的,相等的对象必须拥有相等的散列码。
1、把某个非零的常数值,比如17,保存在一个int型的result中;
2、对于每个关键域f(equals方法中设计到的每个域),作以下操作:
a.为该域计算int类型的散列码;
i.如果该域是boolean类型,则计算(f?1:0),ii.如果该域是byte,char,short或者int类型,计算(int)f,iii.如果是long类型,计算(int)(f^(f>>>32)).iv.如果是float类型,计算Float.floatToIntBits(f).v.如果是double类型,计算Double.doubleToLongBits(f),然后再计算long型的hash值vi.如果是对象引用,则递归的调用域的hashCode,如果是更复杂的比较,则需要为这个域计算一个范式,然后针对范式调用hashCode,如果为null,返回0vii. 如果是一个数组,则把每一个元素当成一个单独的域来处理。
b.result = 31 * result + c;
3、返回result
14.lambda 和匿名内部类的区别?
1.匿名内部类可以为任意接口创建实例,不管接口中包含多少个抽象方法,只要在匿名内部类中实现所有抽象方法即可。
但在lambda表达式中只能为函数式接口创建实例。
2.匿名内部类可以为抽象类甚至普通类创建实例;但lambda表达式只能为函数式接口创建实例。
3.匿名内部类实现的抽象方法可以允许调用接口中定义默认方法。但lambda表达式的代码块不允许调用接口中定义默认方法
15.lambda 内部的泛型化?
16.netty的pipline内部的消息传递?
通过ChannelHandlerContext下的fireChannelRead(Object msg)进行消息传递
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
/**
* @author
*/
public class InBoundHandlerA extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("InBoundHandlerA: " + msg);
ctx.fireChannelRead(msg);
}
}
17.netty的bytebuf和java的bytebufer的区别?
jdk:ByteBuffer通过position和limit来控制读取和写入,每次切换读写时都需要调用flip方法,
netty:ByteBuf通过writerIndex和readerIndex来简化控制。
jdk:ByteBuffer中的capacity是一个固定值,
netty:ByteBuf可以调用calculateNewCapacity方法来重新计算capacity值。
jdk:ByteBuffer缓冲区的长度固定,分多了会浪费内存,分少了存放大的数据时会索引越界,所以使用ByteBuffer时,为了解决这个问题,我们一般每次put
操作时,都会对可用空间进行校检,如果剩余空间不足,需要重新创建一个新的ByteBuffer,然后将旧的ByteBuffer复制到新的ByteBuffer中去。
netty:ByteBuf:而ByteBuf则对其进行了改进,它会自动扩展,具体的做法是,写入数据时,会调用ensureWritable方法,传入我们需要写的字节长度,判断
是否需要扩容:
注意:
1.当申请的新空间大于阀值时,采用每次步进4MB的方式进行扩张内存,而不是倍增,因为这会造成内存膨胀和浪费
2.而但申请的新空间小于阀值时,则以64为基数进行倍增而不是步进,因为当内存比较小的时候,倍增是可以接受的(64 -> 128 和 10Mb -> 20Mb相比)
18.oom异常?
19.jwt的具体流程?
20.Synchronized锁对象的范围
用它来修饰需要同步的方法和需要同步代码块,默认是当前对象作为锁的对象
也就是this,谁调用方法,就是谁.
在修饰类时(或者修饰静态方法),默认是当前类的Class对象作为所的对象.
故存在着方法锁、对象锁、类锁
在方法内可以用,类锁、对象锁
21.HiKariCP和Druid对比
我们所熟知的C3P0,DBCP,Druid, HiKariCP为我们所常用的数据库连接池,其中C3P0已经很久没有更新了。DBCP更新速度很慢,基本处于不活跃状态,而Druid和HikariCP处于活跃状态的更新中,这就是我们说的二代产品了。
HiKariCP
字节码精简 :优化代码,直到编译后的字节码最少,这样,CPU缓存可以加载更多的程序代码;
优化代理和拦截器 :减少代码,例如HikariCP的Statement proxy只有100行代码,只有BoneCP的十分之一;
自定义数组类型(FastStatementList)代替ArrayList :避免每次get()调用都要进行range check,避免调用remove()时的从头到尾的扫描;
自定义集合类型(ConcurrentBag :提高并发读写的效率;
其他针对BoneCP缺陷的优化。
Druid
Druid提供性能卓越的连接池功能外,还集成了SQL监控,黑名单拦截等功能,
强大的监控特性,通过Druid提供的监控功能,可以清楚知道连接池和SQL的工作情况。
监控SQL的执行时间、ResultSet持有时间、返回行数、更新行数、错误次数、错误堆栈信息;
SQL执行的耗时区间分布。什么是耗时区间分布呢?比如说,某个SQL执行了1000次,其中01毫秒区间50次,110毫秒800次,10100毫秒100次,1001000毫秒30次,1~10秒15次,10秒以上5次。通过耗时区间分布,能够非常清楚知道SQL的执行耗时情况;
监控连接池的物理连接创建和销毁次数、逻辑连接的申请和关闭次数、非空等待次数、PSCache命中率等。
方便扩展。Druid提供了Filter-Chain模式的扩展API,可以自己编写Filter拦截JDBC中的任何方法,可以在上面做任何事情,比如说性能监控、SQL审计、用户名密码加密、日志等等。
总的来说:
1、HiKariCP性能比Druid高
2、HiKariCP是Spring Boot 2+官方支持,和Spring Boot兼容性更好
3、Druid的优势是监控完善,扩展性更好(但拦截过多也会增加框架复杂度以及框架性能)
22 消息中间件的作用:
解耦:在项目启动之初来预测将来会碰到什么需求是极其困难的。消息中间件在处理过程中间插入了一个隐含的、基于数据的接口层,两边的处理过程都要实现这一接口,这允许你独立地扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束即可。
冗余(存储):有些情况下,处理数据的过程会失败。消息中间件可以把数据进行持久化直到它们已经被完全处理,通过这一方式规避了数据丢失风险。在把一个消息从消息中间件中删除之前,需要你的处理系统明确地指出该消息已经被处理完成,从而确保你的数据被安全地保存直到你使用完毕。
扩展性:因为消息中间件解耦了应用的处理过程,所以提高消息入队和处理的效率是很容易的,只要另外增加处理过程即可,不需要改变代码,也不需要调节参数。
削峰:在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量并不常见。如果以能处理这类峰值为标准而投入资源,无疑是巨大的浪费。使用消息中间件能够使关键组件支撑突发访问压力,不会因为突发的超负荷请求而完全崩溃。
可恢复性:当系统一部分组件失效时,不会影响到整个系统。消息中间件降低了进程间的耦合度,所以即使一个处理消息的进程挂掉,加入消息中间件中的消息仍然可以在系统恢复后进行处理。
顺序保证:在大多数使用场景下,数据处理的顺序很重要,大部分消息中间件支持一定程度上的顺序性。
缓冲:在任何重要的系统中,都会存在需要不同处理时间的元素。消息中间件通过一个缓冲层来帮助任务最高效率地执行,写入消息中间件的处理会尽可能快速。该缓冲层有助于控制和优化数据流经过系统的速度。
异步通信:在很多时候应用不想也不需要立即处理消息。消息中间件提供了异步处理机制,允许应用把一些消息放入消息中间件中,但并不立即处理它,在之后需要的时候再慢慢处理。
23 RabbitMQ的具体特点
可靠性:RabbitMQ使用一些机制来保证可靠性,如持久化、传输确认及发布确认等。
灵活的路由:在消息进入队列之前,通过交换器来路由消息。对于典型的路由功能,RabbitMQ已经提供了一些内置的交换器来实现。针对更复杂的路由功能,可以将多个交换器绑定在一起,也可以通过插件机制来实现自己的交换器。
扩展性:多个RabbitMQ节点可以组成一个集群,也可以根据实际业务情况动态地扩展集群中节点。
高可用性:队列可以在集群中的机器上设置镜像,使得在部分节点出现问题的情况下队列仍然可用。
多种协议:RabbitMQ除了原生支持AMQP协议,还支持STOMP、MQTT等多种消息中间件协议。
多语言客户端:RabbitMQ几乎支持所有常用语言,比如Java、Python、Ruby、PHP、C#、JavaScript等。
管理界面:RabbitMQ提供了一个易用的用户界面,使得用户可以监控和管理消息、集群中的节点等。
插件机制:RabbitMQ提供了许多插件,以实现从多方面进行扩展,当然也可以编写自己的插件。
24 RabbitMq主要工作关联
交换器、路由键、绑定
交换器:生产者将消息发送到Exchange(交换器,通常也可以用大写的“X”来表示),由交换器将消息路由到一个或者多个队列中。如果路由不到,或许会返回给生产者,或许直接丢弃.
RoutingKey:路由键。生产者将消息发给交换器的时候,一般会指定一个RoutingKey,用来指定这个消息的路由规则,而这个Routing Key需要与交换器类型和绑定键(BindingKey)联合使用才能最终生效。
在交换器类型和绑定键(BindingKey)固定的情况下,生产者可以在发送消息给交换器时,通过指定RoutingKey来决定消息流向哪里
Binding:绑定。RabbitMQ中通过绑定将交换器与队列关联起来,在绑定的时候一般会指定一个绑定键(BindingKey),这样RabbitMQ就知道如何正确地将消息路由到队列了
27 RabbitMq交换器的类型
fanout
它会把所有发送到该交换器的消息路由到所有与该交换器绑定的队列中。
direct
direct类型的交换器路由规则也很简单,它会把消息路由到那些BindingKey和RoutingKey完全匹配的队列中。
topic
direct类型的交换器路由规则是完全匹配BindingKey和RoutingKey,但是这种严格的匹配方式在很多情况下不能满足实际业务的需求。topic类型的交换器在匹配规则上进行了扩展,它与direct类型的交换器相似,也是将消息路由到BindingKey和RoutingKey相匹配的队列中,但这里的匹配规则有些不同,它约定:
RoutingKey为一个点号“.”分隔的字符串(被点号“.”分隔开的每一段独立的字符串称为一个单词),如“com.rabbitmq.client”、“java.util.concurrent”、“com.hidden.client”;
BindingKey和RoutingKey一样也是点号“.”分隔的字符串;
BindingKey中可以存在两种特殊字符串“*”和“#”,用于做模糊匹配,其中“#”用于匹配一个单词,“#”用于匹配多规格单词(可以是零个)。
●路由键为“com.rabbitmq.client”的消息会同时路由到Queue1和Queue2;
●路由键为“com.hidden.client”的消息只会路由到Queue2中;
●路由键为“com.hidden.demo”的消息只会路由到Queue2中;
●路由键为“java.rabbitmq.demo”的消息只会路由到Queue1中;
●路由键为“java.util.concurrent”的消息将会被丢弃或者返回给生产者(需要设置mandatory参数),因为它没有匹配任何路由键。
headers
headers类型的交换器不依赖于路由键的匹配规则来路由消息,而是根据发送的消息内容中的headers属性进行匹配。在绑定队列和交换器时制定一组键值对,当发送消息到交换器时,RabbitMQ会获取到该消息的headers(也是一个键值对的形式),对比其中的键值对是否完全匹配队列和交换器绑定时指定的键值对,如果完全匹配则消息会路由到该队列,否则不会路由到该队列。headers类型的交换器性能会很差,而且也不实用,基本上不会看到它的存在。
26 RabbitMq的RoutingKey和BindingKey的区别与联系
生产者将消息发送给交换器时,需要一个RoutingKey,当BindingKey和RoutingKey相匹配时,消息会被路由到对应的队列中。在绑定多个队列到同一个交换器的时候,这些绑定允许使用相同的BindingKey。BindingKey并不是在所有的情况下都生效,它依赖于交换器类型,
比如:
fanout类型的交换器就会无视BindingKey,而是将消息路由到所有绑定到该交换器的队列中。
direct交换器类型下,RoutingKey和BindingKey需要完全匹配才能使用.
topic交换器类型下,RoutingKey和BindingKey之间需要做模糊匹配,两者并不是相同的。
对于初学者来说,交换器、路由键、绑定这几个概念理解起来会有点晦涩,
交换器相当于投递包裹的邮箱,RoutingKey相当于填写在包裹上的地址,BindingKey相当于包裹的目的地,当填写在包裹上的地址和实际想要投递的地址相匹配时,那么这个包裹就会被正确投递到目的地,最后这个目的地的“主人”——队列可以保留这个包裹。如果填写的地址出错,邮递员不能正确投递到目的地,包裹可能会回退给寄件人,也有可能被丢弃。
有经验的读者可能会发现,在某些情形下,RoutingKey与BindingKey可以看作同一个东西。
在使用绑定的时候,其中需要的路由键是BindingKey。涉及的客户端方法如:channel.exchangeBind、channel.queueBind,对应的AMQP命令为Exchange.Bind、Queue.Bind。
在发送消息的时候,其中需要的路由键是RoutingKey。涉及的客户端方法如channel.basicPublish,对应的AMQP命令为Basic.Publish。
27 RabbitMQ运转流程
生产者发送消息:
(1)生产者连接到RabbitMQ Broker,建立一个连接(Connection),开启一个信道(Channel)。
(2)生产者声明一个交换器,并设置相关属性,比如交换机类型、是否持久化等。
(3)生产者声明一个队列并设置相关属性,比如是否排他、是否持久化、是否自动删除等。
(4)生产者通过路由键将交换器和队列绑定起来。
(5)生产者发送消息至RabbitMQ Broker,其中包含路由键、交换器等信息。
(6)相应的交换器根据接收到的路由键查找相匹配的队列。
(7)如果找到,则将从生产者发送过来的消息存入相应的队列中。
(8)如果没有找到,则根据生产者配置的属性选择丢弃还是回退给生产者。
(9)关闭信道。
(10)关闭连接。
消费者接收消息的过程:
(1)消费者连接到RabbitMQ Broker,建立一个连接(Connection),开启一个信道(Channel)。
(2)消费者向RabbitMQ Broker请求消费相应队列中的消息,可能会设置相应的回调函数,以及做一些准备工作(详细内容请参考3.4节)。
(3)等待RabbitMQ Broker回应并投递相应队列中的消息,消费者接收消息。
(4)消费者确认(ack)接收到的消息。
(5)RabbitMQ从队列中删除相应已经被确认的消息。
(6)关闭信道。
(7)关闭连接。
28 RabbitMq为什么要引入信道(channel)
试想这样一个场景,一个应用程序中有很多个线程需要从RabbitMQ中消费消息,或者生产消息,那么必然需要建立很多个Connection,也就是许多个TCP连接。然而对于操作系统而言,建立和销毁TCP连接是非常昂贵的开销,如果遇到使用高峰,性能瓶颈也随之显现。RabbitMQ采用类似NIO(Non-blocking I/O)的做法,选择TCP连接复用,不仅可以减少性能开销,同时也便于管理。
每个线程把持一个信道,所以信道复用了Connection的TCP连接。同时RabbitMQ可以确保每个线程的私密性,就像拥有独立的连接一样。当每个信道的流量不是很大时,复用单一的Connection可以在产生性能瓶颈的情况下有效地节省TCP连接资源。但是当信道本身的流量很大时,这时候多个信道复用一个Connection就会产生性能瓶颈,进而使整体的流量被限制了。此时就需要开辟多个Connection,将这些信道均摊到这些Connection中,至于这些相关的调优策略需要根据业务自身的实际情况进行调节
24 kafka工作原理:
1.生产者调用send方法首先需要经过productIntercept(拦截器)
2.对所需要的key和value进行序列化(serializer[序列化器])
3.进入partitioner(分区器),选着存放的分区,这里默认采用MurmurHash2算法对key进行hash运算,减少碰撞几率.如果key为null则会以轮询的方式往主题内的各个分区上(这里注意尽量不要空key存储,如果想改变这一策略可以自定义分区器).
4.将初步处理好的消息放到消息累加器(默认32m,由buffer.memory参数控制)中(当消息进入时首先判断队列存不存在,不存在创建),如果存满服务端的send()方法要么被阻塞、要么抛异常.消息累加器里会根据不同的分区创建不同的双端队列,双端队列的存储类型为Deque<ProducerBatch>,ProducerBatch是指一个消息批次,写:丛尾部写,读:丛头部读。在累加器里同时还维护了一个bufferPool(由batch.size参数控制,默认16kb),主要用来实现ByteBuffer的复用,以实现缓存的高效利用
5.sender线程丛消息累加器获取消息以后,会进一步将原本<分区,Deque<ProducerBatch>>的保存形式转变成<Node,List< ProducerBatch>的形式,其中Node表示kafka集群的broker节点.对于网络连接来说,生产者客户端是具体的broker节点建立的连接,也就是向具体broker节点发送信息,而不关心属于哪一个分区,而对于生产者而言,只关注向哪个分区发送那些消息.
6.在转换成<Node,List<ProducerBatch>>的形式之后,Sender 还会进一步封装成<Node,Request>的形式,这样就可以将Request请求发往各个Node了,这里的Request是指Kafka的各种协议请求,对于消息发送而言就是指具体的 ClientRequest (ProduceRequest),
7.封装成ClientRequest 之后,还会保存到InFlightRequests中,InFlightRequests保存对象的具体形式为 Map<NodeId,Deque<Request>>,它的主要作用是缓存了已经发出去但还没有收到响应的请求(NodeId 是一个 String 类型,表示节点的 id 编号),InFlightRequests还提供了许多管理类的方法,并且通过配置参数还可以限制每个连接(也就是客户端与Node之间的连接)最多缓存的请求数。这个配置参数为max.in.flight.requests.per.connection,默认值为 5,即每个连接最多只能缓存 5 个未响应的请求,超过该数值之后就不能再向这个连接发送更多的请求了,除非有缓存的请求收到了响应(Response)。通过比较Deque<Request>的size与这个参数的大小来判断对应的Node中是否已经堆积了很多未响应的消息,如果真是如此,那么说明这个 Node 节点负载较大或网络连接有问题,再继续向其发送请求会增大请求超时的可能.
8.会把相应的Deque<Request>>封装成相应的请求(第九步)发送到kafka.
25RabbitMq和kafka的不同点
1.存储方式
RabbitMQ中消息都只能存储在队列中,这一点和Kafka这种消息中间件相反。Kafka将消息存储在topic(主题)这个逻辑层面(日志存储),而相对应的队列逻辑只是topic实际存储文件中的位移标识。RabbitMQ的生产者生产消息并最终投递到队列中,消费者可以从队列中获取消息并消费。
2.广播
RabbitMQ不支持队列层面的广播消费,如果需要广播消费,需要在其上进行二次开发,处理逻辑会变得异常复杂,同时也不建议这么做。
23线程池的工作状态
二叉树算法
uml图