就是一些随笔,从手机便签中摞到CSDN
- 1. hashmap(key,value)类型中的hashcode和hashset中的hashcode实现不完全一样
1-1hashmap中的key通常是String,且多是用户根据业务自定义的,如name,因而hashcode函数针对的是String进行处理的,如从前到后对每个字符h=31*h+val[i],这样每个不同字符串返回的hashcode必不一样(h是int型,万一溢出呢?取低位?因为取得是31奇素数所以即使溢出低32位也不会相同?)
1-2而hashset中假设存放的东西是对象Object时,则object的hashcode函数是根据对象object的内存地址生成hashcode来确保唯一性的(事实上返回的应该就是虚拟地址),这样才能满足hashset的功能即存入的对象不可能重复(考虑到hashset的底层是hashmap,所以object.hashcode再经过hashmap的hashcode即经历两次才生成了唯一keyCode),注意该keyCode还需经过hash算法才能确定分配到哪个桶中(keyCode=key.hashCode())
- 2. 唯一keyCode经过hash算法才能确定分配到哪个桶中(前面只是确定了唯一keyCode从哪来的)〈hash=(keyCode)^(hash>>>16)〉(无符号右移16位再异或可以在保留高16位的同时使低16位同时具有高低位二进制混合特征)。在hashmap进行resize的时候(大小必为2的倍数,扩的倍数也必为2的倍数,这样resize的时候后大部分的hash值不需要变化,比如size一开始为8时,0-8-16的hash值都为0,当size变为16时,0-16的hash值还是0,8的hash值变为本身8),大部分key的keyCode不会发生变化,有利于减少操作保持高性能,新的hash值会变为:〈新hash=(n-1)&原hash〉,n是新桶数,为2的指数倍,则n-1的二进制值便全是1,再与原hash值做与运算时可以最大程度保存原hash值,而新增了高位特征。如size由8变为16时,key原为8(原keyCode为0)新keyCode为8,因为原key 8与n-1(这里是1111)做与运算时跟原n-1,即111相比刚好多出了最高为的1,使得新keyCode为8而不是三个1比较出的0
- 3. hashmap重新分配大小时要对原数据部分(链表)重新分配,使用尾插法(没想到链表的逆序操作是在了解hashmap的源码过程中知道的)
案例:
找到两个字符串集合(数组)中,共有的串?
解:一般这种题一看就需要用集合A的一个依次和集合B的所有元素进行比较,然后对A的下一个元素做相同操作。这种时间复杂度就变成了O(m*n)效果肯定是不好的。
一般是用hash对每个元素生成唯一一个hashcode,形成hashset,这样才会几乎在常数时间以接近随机定位的速度确定A的某元素是否在B中已存在。
- 4. 红黑树相关
原来TreeSet和TreeMap是红黑树实现的(Tree体现了红黑树的树),而hashset/hashmap至少初衷是数组+链表(映射是hash)。
c++的stl的set,map,linux虚拟内存也是用红黑树实现的
treemap,treeset的实现是红黑树(牛逼版二叉排序树),因而是有序的
redis里面的sortedset实现不是红黑,是一种跳表的数据结构
- 5 .并发
CAS(compareAndSwap)
volatile 关键字,主要用于单个变量
可见性(java虚拟机为了优化性能,变量被修改后不会立刻将值从缓存?刷新到主存中。这样在多线程情况下A线程修改了共享变量,B线程不会立刻知道即见到,因而会对B线程结果产生影响以及未来也可能影响到后续A线程,对volatile关键字后,java虚拟机会立刻将结果刷新给主存,且使其它处理器的缓存失效,使得线程B在遇到volatile变量时越过所在处理器工作缓存,直接从主存中读取最新值)
有序性(java虚拟机为了优化会对指令进行重排序,比如new对象时进行了三步,先分配内存地址,再初始化对象,最后将引用地址指向内存地址。。。。假如虚拟机将第二步和第三部先后顺序反过来,这样在第二步时另一个线程B在if判断引用时虽然不为空,但引用指向的内存地址还没有初始化的对象,则线程B后续就会报错)加volatile关键字的对象在被new或修改值时不会被重排序。
原子性(多条指令要么全执行要么不执行,其与可见性和有序性不重叠,因为也许全执行的是重排序后的指令或全执行完后新值没及时刷新到主存,,,其它两条性质是虚拟机优化带来的)(CAS主要是通过保证原子性实习同步的,将sophomore作为value来进行lock和unlock操作)
volatile关键字修饰过的对象或变量不具备原子性或部分具备原子性。
线程遇到volatile变量时会越过工作缓存直接从主存地址中读最新的value值(但读到最新值后还是会放在工作缓存如寄存器中参与运算,只是读的时候必会读到最新值,这已经保证了可见性,,,但读-改-写之间依然需要结合cas保证形如value++这种实质为复合操作的运算的线程安全性,所以atomicInteger的increaseOne()函数里至少包含getValue()和cas两种来保证自增安全性的操作)
解:所以多线程的自增问题都是CAS+ volatile关键字共同实现
static volatile int value=0;
for(;;)
{
int old=getValue()
int update=old+1
if(compareAndSet(old,update))
{
return update;
}
}
从中也可以看到整个广义的CAS的步骤
先取旧值
再定义和计算新值
再条件判断CAS(在该函数中完成比较和设置新值),
volatile关键字不会引起进程切换及调度(lock直接出现在汇编语句中),因而在只需保证可见性或有序性时可以直接只使用volatile关键字,提升性能(比如仅作为flag标志变量用在if判断中)
知识点:JMM规范,主存屏障
CAS 比较和交换是数据库乐观锁的最后一步也是最重要一步的确保安全的操作
因为从主存中读出内容到缓存(寄存器,cache等)是不互斥也不互相影响的,也就是getValue()的内容。 但由于CAS的整个过程是原子性的,这样数据库写进程在最终提交之前,比较〔version号〕和提交(更新)的过程是一块做的也即原子性的(排他的),故事的写进程对共享变量的更新必是线程安全的。
乐观锁适用于读多写少的场景
CAS广义代码实在太精巧太经典太牛逼了(广义CAS过程是上面的所有代码,含循环,old=getValue(),update=old+1,if…这些)
需要注意的是CAS是一种宏观概念,针对主存或数据库的具体实现必然有差异(一个针对内存一个针对磁盘)
当然最终cas核心函数里是原子性的,同时对其它读写进程排斥(独占总线),但这个独占时间只有最终比较version和提交的消耗,没有业务过程的消耗(比如p,v操作中间的一堆业务代码),因而并发性依然很好
所以atomicInteger用cas和volatile结合;
而数据库要更新的内容很大很多,不能包含volatile而仅用cas(注意数据库的CAS是数据库管理系统实现的,且针对的是磁盘而不是主存,因而不需要保持可见性),(因为对数据库内容的访问一般不需要瞬时读取,而是提交申请select语句)。而对于内存中的共享变量最好用cas加volatile结合(因为在多线程需要瞬时读取,这要求可见性问题)
当然进程内共享数据不可能都是volatile小型变量,大型的共享数据在各自进程内都有各自大缓存,因而某个进程修改自己缓存并更新数据库后需要更新其它进程内的大缓存,有种解决方案是更新完后发送消息队列通知其它进程更新缓存,也有各自进程设置Timer计时器定时从数据库读取(实时性不强情况下),见:https://www.cnblogs.com/weilingfeng/p/11570428.html
但大量线程并发访问同一个cas会导致多个线程自旋(死循环),比如大量线程并发修改同一个atomicInteger
,如何优化呢?如下图
跟concurrenthashmap类似思路,分段锁,降低粒度
注意:虽然对共享变量的读和写分别都线程安全,但组合使用就不一定
- 6. 消息队列的作用
解耦
削峰(突然高并发,双十一零点,秒杀系统)
异步:将已被拆分的异步任务按顺序再整合起来(倘若一个任务太长即函数调用链条过长,串行执行费时影响用户体验,则将任务拆分成多个并行小任务〔能拆的话,比如付款和因付款金额产生用户积分这种就可以拆成并行任务〕)