有的地方,当你呼吸到那儿的空气,你身体里的每一根骨头都会告诉你,你不喜欢。
今天是崔小胖和我恋爱六周年的纪念日。有她在,我的世界就有光,任他千难万险,我都不会迷茫。加油吧,年轻人!
如果一个对象作为HashMap的Key,那么我们需要重写这个对象的 hashCode() 和 equals() 方法。
在put一个键值对的时候时候,hashCode() 先调用;put(K key, V value) 底层实际调用的是 putVal(hash(key), key, value, false, true); 而这里的 hash(key) 方法就会首先调用到 key.hashCode();
原因是,可能存在hash冲突,使得两个不同的值在进行hash()函数计算过后,得到相同的hashCode;
对于hash冲突,常用的解决方案有两大类:
对于链表法,我们常见的 HashMap,就采用的是这种方式来解决hash冲突。而对于 开放寻址法,又可以分为 线性探测法、二次探测法 和 双重散列法 等。
如果对一个 Thread对象调用两次 start()方法,第一次正常调用,第二次会报错 IllegalThreadStateException;
一个线程需要另一个线程的执行结果,可以采用下面两种方式:
在调用 notifyAll() 之后,会唤醒所有正在该对象上等待的线程,这些线程都会参与锁竞争,最终只有一个线程能持有锁,未拿到锁的,将阻塞。
对于Lock接口,我是结合着 ReentrantLock 来讲的,参考:https://blog.csdn.net/Zereao/article/details/105627948#t4
线程中断,涉及到的有三个方法:
// 为调用线程设置一个中断标记
public void interrupt();
// 检查调用线程是否被设置过中断标记
public boolean isInterrupted()
// Thread类的静态方法,判断当前线程是否被中断,并清除中断状态。如果连续两次调用该方法,则第二次调用将返回 false
public static boolean interrupted();
前两个是Thead类的实例方法,最后一个方法是Thread类的静态方法。需要注意的是,当我们调用interrupt()方法后,会出现下面两种情况之一:
对于synchronized关键字,在javac编译过后,会在同步块的前后插入 monitorenter 和 monitorexit 两条字节码指令。这两条字节码指令都需要指定一个 reference 类型的参数来指明要锁定和解锁的对象。如果 Java代码中的 synchronized 明确指定了对象参数,那么就以这个对象的引用作为 reference;如果没有明确指定,那就根据 synchronized 修饰的方法类型(实例方法还是类方法),来决定是取代码所在的对象实例还是取类型对应的Class对象来作为线程要持有的锁。
锁的可重入性是指一个线程能够反复进入被他自己持有锁的同步块。可重入性是通过锁关联的计数器来实现的:在执行 monitorenter 指令时,首先要去尝试获取对象的锁。如果这个对象没有被锁定,或者这个对象已经持有了那个对象的锁,就把锁的计数器的值增加 1,而在执行 monitorexit 指令的时候计数器的值就会减1。一旦计数器的值为0,锁就会被释放掉。
一个ReentrantLock可以绑定多个 condition,同时,一个ReentrantLock对象也可以多次调用 lock() 方法实现多次锁定,但与之对应的是,释放的时候也需要释放多次。
常见的线程池都定义在 Executors类中,有下面三种:
此外还有两个比较特殊的:
在我们的项目中,我们一般是通过ThreadPoolExecutor来自定义线程池的,上面几种常见的线程池实际上也是 ThreadPoolExecutor 的封装。
corePoolSize指定了通常情况下线程池中的线程数量。当我们向线程池中提交一个任务的时候:
拒绝策略有四种:
线程池的几个配置,我们一般都是根据经验来定各项数值的大小的,考虑的因素就是 任务并发量、主机配置等。
Java内存模型规定,所有的变量都存储在主内存中,每条线程都有自己的工作内存,工作内存保存了主内存中变量的副本。线程对变量的所有操作都必须在 工作内存 中,不能直接操作主内存中的变量。不同线程也不能访问对象的工作内存,线程之间的同步必须要通过主内存来完成。
这里的工作内存,相当于Java内存区域中的Java虚拟机栈,栈中存放了 局部变量表、操作数栈、动态链接、方法出口等信息。
对于volatile关键字,当一个变量被volatile修饰时,有两个效果:
第一点,保证可见性。在多线程环境下,各工作内存中的变量可能仍然是不一致的,但是由于每次使用前,都需要从主内存刷新,使用完后必须刷新回主内存,Java执行引擎就看不见这种不一致,变现出来就是当一个线程更改了这个值,对其他线程来说是立即可见的。
第二点,禁止指令重排优化。这是通过“内存屏障”来实现的。所谓内存屏障,是指在volatile修饰的变量在赋值完成后,会多执行一条 lock前缀 空操作指令,这个操作就相当于一个“内存屏障”。指令重排时,后边的指令不能重排到内存屏障之前的位置。
对于lock前缀,其作用是 使本CPU的缓存写入内存,同时使其他CPU也无效化其缓存。
Spring是通过“三级缓存”来解决循环依赖的问题的。在Spring中定义了三个Map,来作为缓存:
当我们需要创建一个bean的时候,首先会从一级缓存singletonObjects中去尝试获取这个bean;如果没有,则会尝试去二级缓存earlySingletonObjects中获取;如果也没有,则会从三级缓存中去获取,找到对应的工厂,获取未完全填充完毕的bean。然后删除三级缓存的数据,并将这个bean填充到二级缓存。
假如依赖关系是 A -> B -> A 这样一个依赖关系。当需要获取A的时候,从一级、二级缓存中获取,都没有,于是就从三级缓存获取A,并将未完全填充完毕的A bean暴露到二级缓存。当继续填充A的其他属性的时候,发现A依赖了B。于是又从一级、二级缓存中去获取B,也没有,于是又从三级缓存获取B,并继续填充B的其他属性。此时发现B又依赖了A,从一级缓存获取A,没有,又从二级缓存获取A,将未完全填充完毕的A赋值给B。这样B就填充完毕了,B会被放到一级缓存中去,同时删除掉B的二级、三级缓存。B填充好了,A也就能填充好了。
如果是构造方式注入,Spring解决不了循环依赖的问题。Spring容器会将每一个正在创建的Bean的名称放到一个 Set中。如果在Bean的创建过程中如果发现自己已经在创建中了,就会抛出 BeanCurrentlyInCreationException 异常。
所以,要解决循环依赖的问题,我们一般使用 属性方式注入 或 setter方式注入。
当我们在使用 docker run 创建容器时,可以使用 --net 来指定容器的网络模式。docker有下面4中网络模式:
CPU调度算法有下面几种:
具体的详情参考:https://www.cnblogs.com/PIRATE-JFZHOU/p/8094790.html
当一个进程运行用户自己的代码时,处于用户态;当进程因为系统调用执行内核代码时,处于内核态。
1、《计算机网络》第七版,谢希仁·著,第一章
2、https://www.cnblogs.com/PIRATE-JFZHOU/p/8094790.html