Java基础性问题
Java的内存模型知道吗 (1)Java所有变量都存储在主内存中 (2)每个线程都有自己独立的工作内存,里面保存该线程的使用到的变量副本(该副本就是主内存中该变量的一份拷贝) 集合你平时用的多的有哪些 答: 集合的话像Collection下的ArrayList, LinkedList; Set下的HashSet; Map下的HashMap,如果涉及到多线程,线程安全的话,会使用ConcurrentHashMap。 问:我们先聊一下ArrayList和LinkedList吧,它们两个在什么场景下会使用它? 答:ArrayList的底层数据结构是数组,不指定ArrayList大小的时候初始化的大小是0,第一次add的时候size会变成10,扩容的话会是之前的1.5倍。扩容因子默认时0.75,也就是原始大小是10,写入第8个元素的时候,数组长度会扩容至15; ArrayList由于底层是数组,因此随机查找的速度很快,插入和删除效率比较低。也因为它的底层是数组,因此分配内存空间的时候要求是连续的内存单元,所以如果需要存储的数据量很大的情况下,不建议使用ArrayList,因为可能不大容易在jvm中找到这么一大块连续空间,会导致大的GC。 LinkedList的底层是一个带有头节点和尾节点的双向链表,提供了头插(LinkedFirst)和尾插(LinkedLast),插入和删除比较快,不支持随机查询,LinkedList数据的存储不要求内存空间是连续的。 常规的来聊一下HashMap 答:HashMap在1.7的时候底层使用数组+单链表的数据结构,使用的是头插;1.8使用数组+单链表/红黑树,使用的是尾插。单链表和红黑树之间的转换,当单链表的长度大于等于8,并且它的hash桶大于等于64的时候,它会将单链表转换成红黑树形式存储;它在红黑树的节点的数量小于等于6的时候,它会重新再转换成一个单链表,这是它底层结构的一个变化;当我们往hashmap中put元素的时候,先根据key的hash值得到这个元素在数组中的位置(即下标),然后就可以把这个元素放到对应的位置中了。如果这个元素所在的位子上已经存放有其他元素了,那么在同一个位子上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。从hashmap中get元素时,首先计算key的hashcode,找到数组中对应位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素。从这里我们可以想象得到,如果每个位置上的链表只有一个元素,那么hashmap的get效率将是最高的;另外一点是关于它Hash桶的数量,默认是16个,默认的阀值是0.75,这关系到它的扩容。 问:它扩容是怎么扩容的? 答:扩容的时候首先检测数组中的元素个数,负载因子默认是loadFactor=0.75,因此当它哈希桶占用的容量大于12的时候,就会触发扩容,哈希桶会扩容成之前的两倍,把之前的元素再进行一次哈希运算,然后添加到新的哈希桶,然后按照链表或者红黑树重新排列起来。 问:你给我说下它是不是线程安全的? 答:它不是线程安全的,因为在插入操作的时候多线程会有数据覆盖的可能;另外它在1.7的时候,它在put的时候可能会有一个resize的过程,这个过程可能会照成它的头插会形成一个环形链表形成一个死循环,1.8之后改成尾插了。 问:你平时开发的时候怎么去保证它的线程安全? 答:我平时开发的时候会使用ConcurrentHashMap来保证它的线程安全。 问:聊一聊ConcurrentHashMap 答:1.7的时候底层是一个分片数组,使用了segment锁(继承自ReentrantLock),通过每次只给一个段加锁来保证它的线程安全和并发度;另外,在1.8的时候,它改成了和HashMap一样的数据结构,使用数据加单链表或者红黑树的数据结构,在1.8的时候渐渐放弃了这种分片锁机制,而使用的是synchronized加CAS来做的,我们知道在1.6版本的时候JVM对synchronized的优化是非常大的,现在也是用这个方法来保证线程安全。 问:CAS是什么? 答:Compare And Sweep,比较并替换,是乐观锁的一种实现,在修改之前要将之前读到的值与当前内存中的值进行比较,如果一致就写入。CAS可以认为是一种轻量级的锁,在低并发的时候时候效率很快,在高并发的情况下,CAS需要涉及到CPU的计算,容易引起CPU性能的消耗,高并发的情况下还是建议使用状态机或者锁之类的。另外一点就是会产生ABA的问题,因为在读取和写入之间,可能有其他线程率先完成修改,然后又将值改回到之前,这样就误以为当前的值和读取的值是一致的,这个问题可以通过加一个戳或标志位来标识,也就是相当于加个版本号。 刚你提到了synchronized,再跟我聊下它 答:关于synchronized,可以用在同步代码块(可指定任意锁)/方法(指定this)/静态方法(指定class对象)。在1.6升级还是蛮大的,首先提供了无锁状态,然后是偏向锁,然后是轻量级锁,然后是重量级锁。偏向锁的话,见名知意,它偏向于获得第一个锁的线程,它会将id写到锁对象的对象头中,等其他线程来的时候,立即会升级到轻量级锁的状态(如果是同一个线程再次进入,先判断是否获取过锁,如果获取过标记位+1,否则修改为1),轻量级锁主要是在低并发的情况下来消除锁的方式,它主要是在你的虚拟机栈中开辟一个空间,叫做Lock Record,将锁对象的Mark Work写到Lock Record,再尝试Lock Record的指针使用CAS去修改锁对象头的那个区域来完成一个加锁的过程,如果再有线程进入的话,并尝试修改这个指针,轻量级锁会升级为一个自旋锁,如果10次未成功就会膨胀成一个重量级锁,也就是一个互斥锁的过程。重量级锁,使用synchronized的时候会在你的代码块前后加上两个指令,monitorenter和monitorexist,通过一个monitor监视器通过计数器的方式来监视这个锁的状态。如果是同步方法的时候,使用的是一个ACC_SYNCHRONIZED标志位,相当于flag,它自动走的是一个同步方法调用的策略,这个原理是比较简单的。 synchronized 的代码块同一时间,只能有一个线程可以进入执行,也就满足了代码块整体的原子性,可见性,有序性。 问:什么时候用它?什么时候用ReentrantLock,这个你有考虑吗? 答:有,它两对比的话区别还是蛮大的。从JVM层面来说,synchronized是JVM的一个关键字,ReetrantLock其实是一个类,你需要手动去编码,synchronized使用其实是比较简单的,不需要关心锁的释放;但是使用ReetrantLock的时候你需要手动去lock,然后配合try finally然后去确保锁的释放,然后ReentrantLock相比synchronized有几个高级特性,当一个线程长期等待得不到锁的时候,你可以手动的去调用一个lockInterruptibly方法尝试中断掉不去等待;另外,它提供了一个公平锁的方式;此外,它提供了一个condition,你可以指定去唤醒绑定到condition身上的线程,来实现一个选择性通知的方式。如果不需要ReentrantLock需要的高级特性的话,建议还是使用synchronized的关键字。 例:因为锁不可逆,如果在早高峰的时候,滴滴打车上的所有锁都升级为重量级锁,那么等过了这个高峰,锁依然是重量级锁,会影响系统的QPS的,所以在使用的时候还是要更具具体的场景来使用 volatile看过吗? 参考知道这些,面试时volatile就稳了 答:volatile修饰的变量保证了多线程下的可见性,当CPU写数据时,发现此变量被volatile修饰时,发现其他CPU中也存在该变量的副本,会发出信号通知其他CPU该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中的变量行是无效的,就重新去内存读取。它是通过计算机的总线嗅探机制(MESI)来实现的,当然它也会照成一个问题,就是volitale会一直嗅探,导致一些无效的交互,引发总线风暴。 volatile是JVM提供的轻量级的同步机制。保证可见性,保证有序性,禁止指令重排,不保证原子性(需要借助synchronized或者CAS) 问: volatile 如何保证的有序性 volatile 可以通过添加内存屏障来保证有序性。这里需要先介绍另外一个东西,叫 happens-before 原则.我举其中的一个例子你们感受一下,一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作,呃呃这种我们看起来不是很正常的嘛,原理就是 JVM 在后面默默付出呀,但是!我们的 JVM 为了优化我们的代码,它还会进行指令重排!一行代码可能不单单只有一个指令呀,就是这个指令重排让我们原本有序的代码变成了无序的指令,所以可能会出现安全问题。...