自己平时学习整理的笔记,分享给各位,希望可以帮助各位,文章很长,点击收藏慢慢看吧!以后会分别对各个知识点进行透彻分析,敬请期待!
1.7
1.8
JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法,那么他们为什么要这样做呢?
为什么HashMap的容量总是2的n次幂?
扩容机制
static final float DEFAULT_LOAD_FACTOR = 0.75f;
扩容大小为原数组的2倍
为什么在JDK1.8中进行对HashMap优化的时候,把链表转化为红黑树的阈值是8?
1.7是先扩容再进行插入
load_factor 负载因子越大
load_factor 负载因子越小
并发情况下的问题
为什么 HashMap 中 String、Integer 这样的包装类适合作为 key 键?
为什么不直接采用经过hashCode()
处理的哈希码 作为 存储数组table
的下标位置?
为什么采用 哈希码 与运算(&) (数组长度-1) 计算数组下标?
hash计算规则
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
put 源码
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
get 源码
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
CountDownLatch和CyclicBarrier的区别?
CountDownLatch:一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行
CyclicBarrier:N个线程相互等待,任何一个线程完成之前,所有的线程都必须等待
对于CountDownLatch来说,重点是那个“一个线程”, 是它在等待, 而另外那N的线程在把“某个事情”做完之后可以继续等待,可以终止。而对于CyclicBarrier来说,重点是那N个线程,他们之间任何一个没有完成,所有的线程都必须等待
从字面上理解,CountDown表示减法计数,Latch表示门闩的意思,计数为0的时候就可以打开门闩了。Cyclic Barrier表示循环的障碍物。两个类都含有这一个意思:对应的线程都完成工作之后再进行下一步动作,也就是大家都准备好之后再进行下一步。然而两者最大的区别是,进行下一步动作的动作实施者是不一样的。这里的“动作实施者”有两种,一种是主线程(即执行main函数),另一种是执行任务的其他线程,后面叫这种线程为“其他线程”,区分于主线程。对于CountDownLatch,当计数为0的时候,下一步的动作实施者是main函数;对于CyclicBarrier,下一步动作实施者是“其他线程”
import java.util.Random;
import java.util.concurrent.CountDownLatch;
public class CountDownLatchTest {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(4);
for(int i = 0; i < latch.getCount(); i++){
new Thread(new MyThread(latch), "player"+i).start();
}
System.out.println("正在等待所有玩家准备好");
latch.await();
System.out.println("开始游戏");
}
private static class MyThread implements Runnable{
private CountDownLatch latch ;
public MyThread(CountDownLatch latch){
this.latch = latch;
}
@Override
public void run() {
try {
Random rand = new Random();
int randomNum = rand.nextInt((3000 - 1000) + 1) + 1000;//产生1000到3000之间的随机整数
Thread.sleep(randomNum);
System.out.println(Thread.currentThread().getName()+" 已经准备好了, 所使用的时间为 "+((double)randomNum/1000)+"s");
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
对于CyclicBarrier,假设有一家公司要全体员工进行团建活动,活动内容为翻越三个障碍物,每一个人翻越障碍物所用的时间是不一样的。但是公司要求所有人在翻越当前障碍物之后再开始翻越下一个障碍物,也就是所有人翻越第一个障碍物之后,才开始翻越第二个,以此类推。类比地,每一个员工都是一个“其他线程”。当所有人都翻越的所有的障碍物之后,程序才结束。而主线程可能早就结束了,这里我们不用管主线程
import java.util.Random;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierTest {
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(3);
for(int i = 0; i < barrier.getParties(); i++){
new Thread(new MyRunnable(barrier), "队友"+i).start();
}
System.out.println("main function is finished.");
}
private static class MyRunnable implements Runnable{
private CyclicBarrier barrier;
public MyRunnable(CyclicBarrier barrier){
this.barrier = barrier;
}
@Override
public void run() {
for(int i = 0; i < 3; i++) {
try {
Random rand = new Random();
int randomNum = rand.nextInt((3000 - 1000) + 1) + 1000;//产生1000到3000之间的随机整数
Thread.sleep(randomNum);
System.out.println(Thread.currentThread().getName() + ", 通过了第"+i+"个障碍物, 使用了 "+((double)randomNum/1000)+"s");
this.barrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
}
}
总结:CountDownLatch和CyclicBarrier都有让多个线程等待同步然后再开始下一步动作的意思,但是CountDownLatch的下一步的动作实施者是主线程,具有不可重复性;而CyclicBarrier的下一步动作实施者还是“其他线程”本身,具有往复多次实施动作的特点
Semaphore?
volatile 总线风暴?
1.7
1.8
数组 + 链表 + 红黑树
CAS + synchronized — cas失败自旋保证成功 — 再失败就用sync保证
在ConcurrentHashMap中通过一个Node
第一次添加元素的时候,默认初期长度为16,当往map中继续添加元素的时候,通过hash值跟数组长度取与来决定放在数组的哪个位置,如果出现放在同一个位置的时候,优先以链表的形式存放,在同一个位置的个数又达到了8个以上,如果数组的长度还小于64的时候,则会扩容数组。如果数组的长度大于等于64了的话,在会将该节点的链表转换成树
在扩容完成之后,如果某个节点的是树,同时现在该节点的个数又小于等于6个了,则会将该树转为链表
put源码
/*
* 当添加一对键值对的时候,首先会去判断保存这些键值对的数组是不是初始化了,
* 如果没有的话就初始化数组,然后通过计算hash值来确定放在数组的哪个位置
* 如果这个位置为空则直接添加,如果不为空的话,则取出这个节点来
* 如果取出来的节点的hash值是MOVED(-1)的话,则表示当前正在对这个数组进行扩容,复制到新的数组,则当前线程也去帮助复制
* 最后一种情况就是,如果这个节点,不为空,也不在扩容,则通过synchronized来加锁,进行添加操作
* 然后判断当前取出的节点位置存放的是链表还是树
* 如果是链表的话,则遍历整个链表,直到取出来的节点的key来个要放的key进行比较,如果key相等,并且key的hash值也相等的话,则说明是同一个key,则覆盖掉value,否则的话则添加到链表的末尾
* 如果是树的话,则调用putTreeVal方法把这个元素添加到树中去
* 最后在添加完成之后,会判断在该节点处共有多少个节点(注意是添加前的个数),如果达到8个以上了的话,
* 则调用treeifyBin方法来尝试将处的链表转为树,或者扩容数组
*/
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();//K,V都不能为空,否则的话跑出异常
int hash = spread(key.hashCode());//取得key的hash值
int binCount = 0;//用来计算在这个节点总共有多少个元素,用来控制扩容或者转移为树
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();//第一次put的时候table没有初始化,则初始化table
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//通过哈希计算出一个表中的位置因为n是数组的长度,所以(n-1)&hash肯定不会出现数组越界
if (casTabAt(tab, i, null,//如果这个位置没有元素的话,则通过cas的方式尝试添加,注意这个时候是没有加锁的
new Node<K,V>(hash, key, value, null)))//创建一个Node添加到数组中区,null表示的是下一个节点为空
break; // no lock when adding to empty bin
}
/*
* 如果检测到某个节点的hash值是MOVED,则表示正在进行数组扩张的数据复制阶段,
* 则当前线程也会参与去复制,通过允许多线程复制的功能,一次来减少数组的复制所带来的性能损失
*/
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
/*
* 如果在这个位置有元素的话,就采用synchronized的方式加锁,
* 如果是链表的话(hash大于0),就对这个链表的所有元素进行遍历,
* 如果找到了key和key的hash值都一样的节点,则把它的值替换到
* 如果没找到的话,则添加在链表的最后面
* 否则,是树的话,则调用putTreeVal方法添加到树中去
*
* 在添加完之后,会对该节点上关联的的数目进行判断,
* 如果在8个以上的话,则会调用treeifyBin方法,来尝试转化为树,或者是扩容
*/
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {//再次取出要存储的位置的元素,跟前面取出来的比较
if (fh >= 0) {//取出来的元素的hash值大于0,当转换为树之后,hash值为-2
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {//遍历这个链表
K ek;
if (e.hash == hash &&//要存的元素的hash,key跟要存储的位置的节点的相同的时候,替换掉该节点的value即可
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)//当使用putIfAbsent的时候,只有在这个key没有设置值得时候才设置
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {//如果不是同样的hash,同样的key的时候,则判断该节点的下一个节点是否为空,
pred.next = new Node<K,V>(hash, key,//为空的话把这个要加入的节点设置为当前节点的下一个节点
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {//表示已经转化成红黑树类型了
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,//调用putTreeVal方法,将该元素添加到树中去
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)//当在同一个节点的数目达到8个的时候,则扩张数组或将给节点的数据转为tree
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);//计数
return null;
}
get源码
/*
* 相比put方法,get就很单纯了,支持并发操作,
* 当key为null的时候回抛出NullPointerException的异常
* get操作通过首先计算key的hash值来确定该元素放在数组的哪个位置
* 然后遍历该位置的所有节点
* 如果不存在的话返回null
*/
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
扩容机制
/**
* 把数组中的节点复制到新的数组的相同位置,或者移动到扩张部分的相同位置
* 在这里首先会计算一个步长,表示一个线程处理的数组长度,用来控制对CPU的使用,
* 每个CPU最少处理16个长度的数组元素,也就是说,如果一个数组的长度只有16,那只有一个线程会对其进行扩容的复制移动操作
* 扩容的时候会一直遍历,直到复制完所有节点,没处理一个节点的时候会在链表的头部设置一个fwd节点,这样其他线程就会跳过他,
* 复制后在新数组中的链表不是绝对的反序的
*/
所以引起数组扩容的情况如下:
那么在扩容的时候,可以不可以对数组进行读写操作呢?
那么,多个线程又是如何同步处理的呢?
直接使用普通for循环进行删除;
不能再foreach中进行删除,但是可以使用普通的for循环,因为普通的for雄厚没有用到Iterator的遍历,所以就不会进行 fail-fast的检验
List<String> userNames = new ArrayList<String>() {{
add("Hollis");
add("hollis");
add("HollisChuang");
add("H");
}};
for (int i = 0; i < 1; i++) {
if (userNames.get(i).equals("Hollis")) {
userNames.remove(i);
}
}
System.out.println(userNames);
这种方式存在一个问题,那就是remove操作会改变List中元素的下标,可能存在漏删的问题
直接使用Iterator进行删除
List<String> userNames = new ArrayList<String>() {{
add("Hollis");
add("hollis");
add("HollisChuang");
add("H");
}};
Iterator iterator = userNames.iterator();
while (iterator.hasNext()) {
if (iterator.next().equals("Hollis")) {
iterator.remove();
}
}
System.out.println(userNames);
使用Iterator提供的remove方法,就可以修稿到expectedModCount的值,那么就不会抛出异常了8中
使用Java8中提供的filter过滤生成新的集合
枚举单例模式
public enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("doSomething");
}
// 调用方式
public static void main(String[] args) {
Singleton.INSTANCE.doSomething();
}
}
通过将定义好的枚举反编译,其实枚举在经过javac
的编译之后,会被转换成形如public final class T extends Enum
的定义,枚举中的各个枚举项是通过 static 来定义的
这个类是 final 类型的,不能被继承
public enum T {
SPRING,SUMMER,AUTUMN,WINTER;
}
public final class T extends Enum
{
//省略部分内容
public static final T SPRING;
public static final T SUMMER;
public static final T AUTUMN;
public static final T WINTER;
private static final T ENUM$VALUES[];
static
{
SPRING = new T("SPRING", 0);
SUMMER = new T("SUMMER", 1);
AUTUMN = new T("AUTUMN", 2);
WINTER = new T("WINTER", 3);
ENUM$VALUES = (new T[] {
SPRING, SUMMER, AUTUMN, WINTER
});
}
}
当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的(因为虚拟机在加载枚举的类的时候,会使用ClassLoader的loadClass方法,而这个方法使用同步代码块保证了线程安全)所以,创建一个enum类型是线程安全的
阻塞式IO模型
当用户线程发出IO请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除block状态
data = socket.read();
如果数据没有就绪,就会一直阻塞在read方法
非阻塞IO模型
当用户线程发起一个read操作后,并不需要等待,而是马上就得到了一个结果。如果结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回,所以事实上,在非阻塞IO模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞IO不会交出CPU,而会一直占用CPU
while(true) {
data = socket.read();
if (data != error) {
// 处理数据
break;
}
}
但是对于非阻塞IO就有一个非常严重的问题,在while循环中需要不断地去询问内核数据是否就绪,这样会导致CPU占用率非常高,因此一般情况下很少使用while循环这种方式来读取数据
IO 复用模型
异步IO模型
public class switchDemoString {
public static void main(String[] args) {
String str = "world";
switch (str) {
case "hello":
System.out.println("hello");
break;
case "world":
System.out.println("world");
break;
default:
break;
}
}
}
反编译后
public class switchDemoString
{
public switchDemoString()
{
}
public static void main(String args[])
{
String str = "world";
String s;
switch((s = str).hashCode())
{
default:
break;
case 99162322:
if(s.equals("hello"))
System.out.println("hello");
break;
case 113318802:
if(s.equals("world"))
System.out.println("world");
break;
}
}
}
原来字符串的switch是通过equals()和hashCode()方法来实现的
泛型擦除,所有类型都是Object类型
对于Java虚拟机来说,他根本不认识Map map
这样的语法。需要在编译阶段通过类型擦除的方式进行解语法糖
Map<String, String> map = new HashMap<String, String>();
map.put("name", "hollis");
map.put("wechat", "Hollis");
map.put("blog", "www.hollischuang.com");
解语法糖后
Map map = new HashMap();
map.put("name", "hollis");
map.put("wechat", "Hollis");
map.put("blog", "www.hollischuang.com");
虚拟机中没有泛型,只有普通类和普通方法,所有泛型类的类型参数在编译时都会被擦除,泛型类并没有自己独有的Class类对象。比如并不存在List.class或是List.class,而只有List.class
自动装箱
public static void main(String[] args) {
int i = 10;
Integer n = i;
}
反编译后
public static void main(String args[])
{
int i = 10;
Integer n = Integer.valueOf(i);
}
自动拆箱
public static void main(String[] args) {
Integer i = 10;
int n = i;
}
反编译后
public static void main(String args[])
{
Integer i = Integer.valueOf(10);
int n = i.intValue();
}
在装箱的时候自动调用的是Integer
的valueOf(int)
方法。而在拆箱的时候自动调用的是Integer
的intValue
方法
public static void main(String[] args)
{
print("Holis", "公众号:Hollis", "博客:www.hollischuang.com", "QQ:907607222");
}
public static void print(String... strs)
{
for (int i = 0; i < strs.length; i++)
{
System.out.println(strs[i]);
}
}
反编译后
public static void main(String args[])
{
print(new String[] {
"Holis", "\u516C\u4F17\u53F7:Hollis", "\u535A\u5BA2\uFF1Awww.hollischuang.com", "QQ\uFF1A907607222"
});
}
public static transient void print(String strs[])
{
for(int i = 0; i < strs.length; i++)
System.out.println(strs[i]);
}
从反编译后代码可以看出,可变参数在被使用的时候,他首先会创建一个数组,数组的长度就是调用该方法是传递的实参的个数,然后再把参数值全部放到这个数组当中,然后再把这个数组作为参数传递到被调用的方法中
public enum t { SPRING,SUMMER;}
反编译后
public final class T extends Enum
{
private T(String s, int i)
{
super(s, i);
}
public static T[] values()
{
T at[];
int i;
T at1[];
System.arraycopy(at = ENUM$VALUES, 0, at1 = new T[i = at.length], 0, i);
return at1;
}
public static T valueOf(String s)
{
return (T)Enum.valueOf(demo/T, s);
}
public static final T SPRING;
public static final T SUMMER;
private static final T ENUM$VALUES[];
static
{
SPRING = new T("SPRING", 0);
SUMMER = new T("SUMMER", 1);
ENUM$VALUES = (new T[] {
SPRING, SUMMER
});
}
}
通过反编译后代码我们可以看到,public final class T extends Enum,说明,该类是继承了Enum类的,同时final关键字告诉我们,这个类也是不能被继承的。当我们使用enmu来定义一个枚举类型的时候,编译器会自动帮我们创建一个final类型的类继承Enum类,所以枚举类型不能被继承
public class Test { public static void main(String... args) { int i = 10_000; System.out.println(i); }}
反编译后
public class Test{ public static void main(String[] args) { int i = 10000; System.out.println(i); }}
反编译后就是把_删除了。也就是说 编译器并不认识在数字字面量中的_,需要在编译阶段把他去掉
public static void main(String... args) { try (BufferedReader br = new BufferedReader(new FileReader("d:\\ hollischuang.xml"))) { String line; while ((line = br.readLine()) != null) { System.out.println(line); } } catch (IOException e) { // handle exception }}
其实背后的原理也很简单,那些我们没有做的关闭资源的操作,编译器都帮我们做了。所以,再次印证了,语法糖的作用就是方便程序员的使用,但最终还是要转成编译器认识的语言
public static void method(List<String> list) { System.out.println("invoke method(List list)" ); } public static void method(List<Integer> list) { System.out.println("invoke method(List list)" ); }}
上面这段代码,有两个重载的函数,因为他们的参数类型不同,一个是List另一个是List ,但是,这段代码是编译通不过的。因为我们前面讲过,参数List和List编译之后都被擦除了,变成了一样的原生类型List,擦除动作导致这两个方法的特征签名变得一模一样
将一些细小的锁,其实不会出现安全问题,但是很多细小的锁,会导致锁竞争,效率低下,可以将多个小锁扩
展到一个大锁,这样可以减少锁的竞争。这里的大指的的范围
死锁产生的条件
死锁案例
public class DeadLockDemo implements Runnable{
public static int flag = 1;
//static 变量是 类对象共享的
static Object o1 = new Object();
static Object o2 = new Object();
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ":此时 flag = " + flag);
if(flag == 1){
synchronized (o1){
try {
System.out.println("我是" + Thread.currentThread().getName() + "锁住 o1");
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName() + "醒来->准备获取 o2");
}catch (Exception e){
e.printStackTrace();
}
synchronized (o2){
System.out.println(Thread.currentThread().getName() + "拿到 o2");//第24行
}
}
}
if(flag == 0){
synchronized (o2){
try {
System.out.println("我是" + Thread.currentThread().getName() + "锁住 o2");
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName() + "醒来->准备获取 o2");
}catch (Exception e){
e.printStackTrace();
}
synchronized (o1){
System.out.println(Thread.currentThread().getName() + "拿到 o1");//第38行
}
}
}
}
public static void main(String args[]){
DeadLockDemo t1 = new DeadLockDemo();
DeadLockDemo t2 = new DeadLockDemo();
t1.flag = 1;
new Thread(t1).start();
//让main线程休眠1秒钟,保证t2开启锁住o2.进入死锁
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.flag = 0;
new Thread(t2).start();
}
}
代码中, t1创建,t1先拿到o1的锁,开始休眠3秒。然后 t2线程创建,t2拿到o2的锁,开始休眠3秒。然后 t1先醒来,准备拿o2的锁,发现o2已经加锁,只能等待o2的锁释放。 t2后醒来,准备拿o1的锁,发现o1已经加锁,只能等待o1的锁释放。 t1,t2形成死锁
排查死锁
解决办法
类的生命周期
常量在编译阶段会存入到调用这个常量的方法所在类的常量池中,调用类并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。将常量存放到了MyTest的常量池中,之后MyTest和MyParent没有任何关系,即使把MyParent的class文件删除也不影响执行
public class MyTest {
public static void main(String[] args) {
System.out.println(MyParent.str);
}
}
class MyParent {
public static final String str = "hello world";
public static final String str2 = UUID.randomUUID().toString; // 如果引用str2,那么MyParent会初始化,因为编译期无法确定
static {
System.out.println("MyParent static block");
}
}
当一个接口初始化时,并不要求其父接口完成初始化
类与类加载器
双亲委派模式
启动类加载器,使用C++语言实现,是虚拟机自身的一部分
其他类加载器,都是继承自抽象类java.lang.ClassLoader
启动类加载器(Bootstrap ClassLoder)
扩展类加载器(Extension ClassLoader)
应用类加载器(Application ClassLoader)
双亲委派模型的工作过程:
破坏双亲委派模型
线程上下文类加载器(Thread Context ClassLoader)
默认线程上下文类加载器为 系统类加载(App ClassLoader)
在双亲委托模型下,类加载器是由上至下的,即下层的类加载器会委托上层进行加载。但是对于SPI来说,有些接口是Java核心库所提供的,而Java核心库是由启动类加载器来加载的,而这些接口的实现却来自于不同的jar包(厂商提供),Java的启动类加载器是不会加载其他来源的jar包,这样传统的双亲委托模型就无法满足SPI的要求,而通过给当前线程设置上下文类加载器,就可以有设置的上下文类加载器来实现对于接口实现类的加载
这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果线程创建时还未设置,它将会从父线程继承一个,如果应用程序的全局范围内都没有设置过的话,那这个类加载器就是默认类加载器
JNDI服务使用这个线程上下文加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作
Java中所有涉及SPI的加载动作基本都采用这种方式,例如:JNDI、JDBC、JCE、JAXB和JBI等
线程上下文类加载器的一般使用模式(获取 - 使用 - 还原)
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
try {
// 自定义的ClassLoader,设置以后 下面的方法就会使用自定义类加载器
// 记得还原类加载器,这样不影响后面的操作
Thread.currentThread().setContextClassLoader(targetClassLoader);
myMethod();
} finally {
Thread.currentThread().setContextClassLoader(classLoader);
}
ServiceLoader
类加载器的命名空间
类加载路径
Class.forName()和ClassLoader都可以对类进行加载
热点代码
热点探测
确定了检测热点代码的方式,如何计算具体的次数呢?
什么是字节码、机器码、本地代码?
什么是 JIT?
什么是编译和解释?
为何 HotSpot 虚拟机要使用解释器与编译器并存的架构?
在 HotSpot 中,解释器和 JIT 即时编译器是同时存在的,他们是 JVM 的两个组件。对于不同类型的应用程序,用户可以根据自身的特点和需求,灵活选择是基于解释器运行还是基于 JIT 编译器运行。HotSpot 为用户提供了几种运行模式供选择,可通过参数设定,分别为:解释模式、编译模式、混合模式,HotSpot 默认是混合模式,需要注意的是编译模式并不是完全通过 JIT 进行编译,只是优先采用编译方式执行程序,但是解释器仍然要在编译无法进行的情况下介入执行过程。
逃逸分析
JIT 运行模式
JVM JIT参数
-Xint
-Xcomp
Xmixed
-XX:+TieredCompilation
除了纯编译和默认的mixed之外,jvm 从jdk6u25 之后,引入了分层编译。HotSpot 内置两种编译器,分别是client启动时的c1编译器和server启动时的c2编译器,c2在将代码编译成机器代码的时候需要搜集大量的统计信息以便在编译的时候进行优化,因此编译出来的代码执行效率比较高,代价是程序启动时间比较长,而且需要执行比较长的时间,才能达到最高性能;与之相反, c1的目标是使程序尽快进入编译执行的阶段,所以在编译前需要搜集的信息比c2要少,编译速度因此提高很多,但是付出的代价是编译之后的代码执行效率比较低,但尽管如此,c1编译出来的代码在性能上比解释执行的性能已经有很大的提升,所以所谓的分层编译,就是一种折中方式,在系统执行初期,执行频率比较高的代码先被c1编译器编译,以便尽快进入编译执行,然后随着时间的推移,执行频率较高的代码再被c2编译器编译,以达到最高的性能
作者:worldcbf
链接:https://www.jianshu.com/p/318617435789
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
为什么不全都去编译执行呢?
第一次握手
第二次握手
第三次握手
为什么要发送特定的数据包,随便发不行吗?
为什么三次握手,两次不行吗?
共享锁:对某一资源加共享锁,自身可以读该资源,其他人也可以读该资源(也可以再继续加共享锁,即 共享锁可多个共存),但无法修改。要想修改就必须等所有共享锁都释放完之后。语法为:
select * from table lock in share mode
对某一资源加排他锁,自身可以进行增删改查,其他人无法进行任何操作。语法为:
select * from table for update
防止幻读的发生
锁定索引记录间隙,确保索引记录的间隙不变。间隙锁是针对事务隔离级别为可重复读或以上级别的
读用乐观锁,写用悲观锁
悲观锁:
乐观锁:
使用 version 或者 timestamp 进行比较
并发量小
CAS
update items set inventory=inventory-1 where id=100 and inventory-1>0;
利用数据版本号(version)机制是乐观锁最常用的一种实现方式。一般通过为数据库表增加一个数字类型的 “version” 字段,当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值+1。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据,返回更新失败
需要注意的是,如果你的数据表是读写分离的表,当master表中写入的数据没有及时同步到slave表中时会造成更新一直失败的问题。此时,需要强制读取master表中的数据(即:将select语句放在事物中,查询的就是master主库了)
商品库存扣减时,尤其是在秒杀、聚划算这种高并发的场景下,若采用version号作为乐观锁,则每次只有一个事务能更新成功,业务感知上就是大量操作失败,修改如下:
// 仍挑选以库存数作为乐观锁
//step1: 查询出商品信息
select (inventory) from items where id=100;
//step2: 根据商品信息生成订单
insert into orders(id,item_id) values(null,100);
//step3: 修改商品的库存
update items set inventory=inventory-1 where id=100 and inventory-1>0;
没错!你参加过的天猫、淘宝秒杀、聚划算,跑的就是这条SQL,通过挑选乐观锁,可以减小锁力度,从而提升吞吐~
MERGE_THRESHOLD
(默认页体积的50%),InnoDB会开始寻找最靠近的页(前或后)看看是否可以将两个页合并以优化空间使用INFOMATION_SCHEMA.INNODB_METRICS
中的index_page_merge_successful
将会增加聚集索引一个表只能有一个,而非聚集索引一个表可以存在多个
聚集索引存储记录是物理上连续存在,而非聚集索引是逻辑上的连续,物理存储并不连续
聚集索引表记录的排列顺序和索引的排列顺序一致,所以查询效率快,只要找到第一个索引值记录,其余就连续性的记录在物理也一样连续存放。聚集索引对应的缺点就是修改慢,因为为了保证表中记录的物理和索引顺序一致,在记录插入的时候,会对数据页重新排序
非聚集索引制定了表中记录的逻辑顺序,但是记录的物理和索引不一定一致,两种索引都采用B+树结构,非聚集索引的叶子层并不和实际数据页相重叠,而采用叶子层包含一个指向表中的记录在数据页中的指针方式。非聚集索引层次多,不会造成数据重排。这个指针并不是地址,而是主键,通过主键就可以查到数据
创建
如何解决非聚集索引的二次查询问题?即回表?
复合索引(覆盖索引)
建立两列以上的索引,即可查询复合索引里的列的数据而不需要进行回表二次查询,如index(col1, col2),执行下面的语句
将单列索引(name)升级为联合索引(name, sex),也可以避免回表
select col1, col2 from t1 where col1 = '213';
5.6版本后新增索引下推优化,减少回表次数
在使用非主键索引(又叫普通索引或者二级索引)进行查询时,存储引擎通过索引检索到数据,然后返回给MySQL服务器,服务器然后判断数据是否符合条件
使用索引下推时,如果存在某些被索引的列的判断条件时,MySQL服务器将这一部分判断条件传递给存储引擎,然后由存储引擎通过判断索引是否符合MySQL服务器传递的条件,只有当索引符合条件时才会将数据检索出来返回给MySQL服务器
索引条件下推优化可以减少存储引擎查询基础表的次数,也可以减少MySQL服务器从存储引擎接收数据的次数
举例:
SELECT * from user where name like '陈%'
-- 根据 "最佳左前缀" 的原则,这里使用了联合索引(name,age)进行了查询,性能要比全表扫描肯定要高
-- 问题来了,如果有其他的条件呢?假设又有一个需求,要求匹配姓名第一个字为陈,年龄为20岁的用户,此时的sql语句如下:
SELECT * from user where name like '陈%' and age=20
没有索引下推
有索引下推
根据explain解析结果可以看出Extra的值为Using index condition,表示已经使用了索引下推
索引下推在非主键索引上的优化,可以有效减少回表的次数,大大提升了查询的效率
如果A表TID是自增长,并且是连续的,B表的ID为索引
select * from a,b where a.tid = b.id and a.tid>500000 limit 200;
如果A表的TID不是连续的,那么就需要使用覆盖索引.TID要么是主键,要么是辅助索引,B表ID也需要有索引。
select * from b , (select tid from a limit 50000,200) a where b.id = a .tid;
基本原理
MySQL之间数据复制的基础是二进制日志文件(binary log file)。一台MySQL数据库一旦启用二进制日志后,其作为master,它的数据库中所有操作都会以“事件”的方式记录在二进制日志中,其他数据库作为slave通过一个I/O线程与主服务器保持通信,并监控master的二进制日志文件的变化,如果发现master二进制日志文件发生变化,则会把变化复制到自己的中继日志中,然后slave的一个SQL线程会把相关的“事件”执行到自己的数据库中,以此实现从数据库和主数据库的一致性,也就实现了主从复制
对于每一个主从复制的连接,都有三个线程。拥有多个从库的主库为每一个连接到主库的从库创建一个binlog输出线程,每一个从库都有它自己的I/O线程和SQL线程
系统如何不停机迁移到分库分表?
分库分表动态扩容方案
分库分表后全局id
mysql读写分离?
mysql主从复制原理?
如何解决mysql主从同步的延时问题?
容器通用规则
String
Redis 的字符串是动态字符串,是可以修改的字符串,内部类似Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配
当字符串长度小于1M时,扩容2倍;当字符串长度大于1M时,每次扩容1M;字符串最大长度为512M
底层
SDS (Simple Dynamic Stirng,简单动态字符串),它的结构是一个带长度信息的字节数组
struct SDS<T> {
T capacity; // 数组容量
T len; // 数组长度
byte flags; // 特殊标识位
byte[] content; // 数组内容
}
List
Hash 字典
Set
ZSet
位图 BitMap
HyperLogLog
布隆过滤器
GeoHah
Scan
Pub/Sub
redis线程模型
为什么redis单线程还这么快?
redis过期策略
手写一个LRU算法
public class LRUCache<K,V> extends LinkedHashMap<K,V> {
private final int CACHE_SIZE;
// cacheSize表示最多可以换成多少条数据
public LRUCache(int cacheSize) {
// 设置一个LinkedHashMap的初始大小,true表示让LinkedHashMap按照访问顺序进行排序,
// 最近访问的放在前面,最早访问的放在后面
super((int) Math.ceil(cacheSize / 0.75) + 1, 0.75f, true);
CACHE_SIZE = cacheSizel;
}
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
// 当map中的数据量大于指定的缓存个数的时候,就自动删除
return size() > CACHE_SIZE;
}
redis主从架构,锁失效的问题
redis replication的核心机制
redis完整主从复制流程
redis主从架构原理
redis主从复制断点续传
redis无磁盘复制
redis过期key
redis高可用(故障转移failover,主备切换)
redis哨兵模式
为什么redis哨兵模式2个节点无法正常工作?
redis主从复制数据丢失情况
redis主观宕机和客观宕机
redis哨兵和slave集群的自动发现机制
redis slave 的选举
redis quorum和majority
redis configuration epoch
redis configuration传播
redis持久化
redis cluster 和 replication sentinel
redis cluster 基本原理
为什么删除缓存,而不是更新缓存?
缓存与数据库双写不一致的情况,如何处理?
redis并发竞争的问题?如何解决?了解redis事务的CAS方案吗?
生产环境的redis部署结构?用了哪种集群?有没有做高可用保证?有没有开启持久化机制可以进行数据恢复?线上redis分配几个G的内存?设置了哪些参数?压测后你们redis集群承载多少QPS?
场景
延时双删
public void write(String key,Object data){
redisUtils.del(key);
db.update(data);
Thread.Sleep(100);
redisUtils.del(key);
}
这么做,可以将1秒内所造成的缓存脏数据,再次删除。这个时间设定可根据业务场景进行一个调节
分布式锁
为什么是删除缓存,而不是更新缓存?
setnx ex
zset 窗口滑动、zset 会越来越大
令牌桶算法
定时push、然后 leftpop
令牌桶算法提及到输入速率和输出速率,当输出速率大于输入速率,那么就是超出流量限制了
也就是说我们每访问一次请求的时候,可以从Redis中获取一个令牌,如果拿到令牌了,那就说明没超出限制,而如果拿不到,则结果相反
依靠List的leftPop来获取令牌
// 输出令牌public Response limitFlow2(Long id){ Object result = redisTemplate.opsForList().leftPop("limit_list"); if(result == null){ return Response.ok("当前令牌桶中无令牌"); } return Response.ok(articleDescription2);}
再依靠Java的定时任务,定时往List中rightPush令牌,当然令牌也需要唯一性,所以我这里还是用UUID进行了生成
// 10S的速率往令牌桶中添加UUID,只为保证唯一性@Scheduled(fixedDelay = 10_000,initialDelay = 0)public void setIntervalTimeTask(){ redisTemplate.opsForList().rightPush("limit_list",UUID.randomUUID().toString());}
漏斗限流 funnel
redis cell
一个Watch事件是一个一次性的触发器,当被设置了Watch的数据发生了改变的时候,则服务器将这个改变发送给设置了Watch的客户端,以便通知它们
为什么不是永久的,举个例子,如果服务端变动频繁,而监听的客户端很多情况下,每次变动都要通知到所有的客户端,给网络和服务器造成很大压力
一般是客户端执行getData(“/节点A”,true),如果节点A发生了变更或删除,客户端会得到它的watch事件,但是在之后节点A又发生了变更,而客户端又没有设置watch事件,就不再给客户端发送。在实际应用中,很多情况下,我们的客户端不需要知道服务端的每一次变动,我只要最新的数据即可
ZK分布式锁羊群效应
优点
缺点
高可用
集群特点
集群模式
集群工作流程
心跳机制
队列数量
管理界面:rocketmq-externals 可以在GitHub上直接搜索
消息类型
消费消息
组件
RocketMQ具有很好动态伸缩能力(非顺序消息),伸缩性体现在Topic和Broker两个维度
消息存储
高可用性机制
负载均衡
消息重试
死信队列
消息幂等性
RocketMQ如何保证生产者消息不丢失?
RocketMQ如何保证broker消息不丢失?
RocketMQ如何保证消费者消息不丢失?
Kafka丢失消息场景
Kafka如何保证不丢失消息?
如何保证消息的顺序性?
如何解决消息队列的延时以及过期失效问题?
消息队列满了以后该怎么处理?
有几百万消息持续堆积几个小时,如何处理?
如果让你来设计一个消息队列?
将Broker(size=n)和待分配的Partition排序
将第i个Partition分配到第(i%n)个Broker上
将第i个Partition的第j个Replica分配到第((i + j) % n)个Broker上
生产者指定分区
一个消息如何算投递成功,Kafka提供了三种模式
强一致性和弱一致性
当acks设置为0时,生产者端不会等待Server Broker回执任何的ACK确认信息
当acks设置为1时,生产者发送消息将等待这个分区的Leader Server Broker 完成它本地的消息记录操作,但不会等待这个分区下其它Follower Server Brokers的操作
当acks设置为“all”时,消息生产者发送消息时将会等待目标分区的Leader Server Broker以及所有的Follower Server Brokers全部处理完,才会得到ACK确认信息。这样的处理逻辑下牺牲了一部分性能,但是消息存储可靠性是最高的
消息生产者配置中的“request.required.acks”属性来设置消息的复制性要求
故障处理
消费者
Partition ack
message状态
message持久化
message有效期
Produer
Kafka高吞吐量
批量发送
push-and-pull
push-and-pull
Kafka集群中broker之间的关系
负载均衡方面
同步异步
分区机制partition
离线数据装载
实时数据与离线数据
插件支持:
解耦:
冗余:
扩展性
峰值
可恢复性
顺序保证性
缓冲
异步通信:
Kafka的分区器、序列化器、拦截器执行顺序:拦截器 -> 序列化器 -> 分区器
消费者提交消费位移时提交的是当前的最新消息的offset还是offset+1?
有哪些情况会造成重复消费?
有哪些情况会造成漏消费?
topic的分区可以不可以增加或减少?
Kafka有没有内部的topic?
Kafka监控平台:Kafka Eagle
实时数据与离线数据:Kafka既支持离线数据也支持实时数据,因为Kafka的message持久化到文件,并可以设置有效期,因此可以把Kafka作为一个高效的存储来使用,可以作为离线数据供后面的分析。当然作为分布式实时消息系统,大多数情况下还是用于实时的数据处理的,但是当cosumer消费能力下降的时候可以通过message的持久化在淤积数据在Kafka
插件支持:现在不少活跃的社区已经开发出不少插件来拓展Kafka的功能,如用来配合Storm、Hadoop、flume相关的插件
一个典型的Kafka集群中包含若干Producer(可以是web前端FET,或者是服务器日志等),若干broker(Kafka支持水平扩展,一般broker数量越多,集群吞吐率越高),若干ConsumerGroup,以及一个Zookeeper集群。Kafka通过Zookeeper管理Kafka集群配置:选举Kafka broker的leader,以及在Consumer Group发生变化时进行rebalance,因为consumer消费Kafka topic的partition的offsite信息是存在Zookeeper的。Producer使用push模式将消息发布到broker,Consumer使用pull模式从broker订阅并消费消息
分析过程分为以下4个步骤
topic中partition储存分布
partition中文件存储方式
partiton中segment文件存储结构
在partition中如何通过offset查找message
无状态的Kafka Broker
Message的交付与生命周期
压缩
消息可靠性
备份机制
Kafka可以将主题划分为多个分区(Partition),会根据分区规则选择把消息存储到哪个分区中,只要如果分区规则设置的合理,那么所有的消息将会被均匀的分布到不同的分区中,这样就实现了负载均衡和水平扩展。另外,多个订阅者可以从一个或者多个分区中同时消费数据,以支撑海量数据处理能力
若没有分区,一个topic对应的消息集在分布式集群服务组中,就会分布不均匀,即可能导致某台服务器A记录当前topic的消息集很多,若此topic的消息压力很大的情况下,服务器A就可能导致压力很大,吞吐也容易导致瓶颈。有了分区后,假设一个topic可能分为10个分区,Kafka内部会根据一定的算法把10分区尽可能均匀分布到不同的服务器上,比如:A服务器负责topic的分区1,B服务器负责topic的分区2,在此情况下,Producer发消息时若没指定发送到哪个分区的时候,Kafka就会根据一定算法上个消息可能分区1,下个消息可能在分区2。当然高级API也能自己实现其分发算法
Kafka在于分布式架构,RabbitMQ基于AMQP协议来实现,RocketMQ的思路来源于Kafka,改成了主从结构,在事务性可靠性方面做了优化。广泛来说,电商、金融等对事务性要求很高的,可以考虑RabbitMQ和RocketMQ,对性能要求高的可考虑Kafka
分布式理论
分布式事务
分布式数据库
分布式文件系统
分布式缓存
限流降级
分布式算法