笔记

运算位

按位与运算符(&):两位同时为“1”,结果才为“1”,否则为“0”。

按位或运算符(|):两位有一个为“1”,结果就为“1”,否则为“0”。

异或运算符(^):两位不同,结果为“1”;两位相同,结果为“0”。

取反运算符(~):~0=1,~1=0。

左移运算符(<<):将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0),相当于2的次方。

右移运算符(>>):将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃;正数的位移相当于除以2的次方,负数的右移不等于除法,即负数右移不能按除以2的n次方计算(n表示移动位数)。

无符号右移运算符(>>>):右移后左边空出的位用零来填充。移出右边的位被丢弃。

补码

在计算机系统中,数值一律用补码来表示和存储,其中最高位表示符号位,1表示负数,0表示正数。

正数的补码是原码自身。

负数补码是通过原码计算得到,计算过程为:符号位不变,其余位按照原码取反加1

补码计算示例

以计算十进制-100的补码为例,计算过程为:

-100的原码:10000000 00000000 00000000 01100100 
符号位保持不变,取反:11111111 11111111 11111111 10011011 
加1后,-100补码为:11111111 11111111 11111111 10011100

负数右移:原码->补码->位移计算(高位是什么补什么)->保留符号位取反->取反后+1

如何保证hashCode尽可能不重复?(hashCode的散列算法)

hashMap计算数组下标:(n - 1) & hash等价于hash % n

s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]等价求解hash = s[0]*M^(i-1) + s[1]*M^(i-2) + ... + s[i-1];

求在hash % n余数最多的情况下M的取值

结论:常量M越大越好,且需要是个素数。

为什么常见的hash算法把31作为乘数?

计算机计算31比较快(可以直接采用位移操作得到 1<<5-1)。
曾有人对超过5W个英文单词做了测试,在常量取31情况下,碰撞的次数都不超过7次。

为什么Boolean的hashCode采用的是1231和1237?

素数,个人习惯。

boolean占多少字节?

单个boolean占4个字节(转int处理);boolean数组占1个字节(转byte处理)。

Class.forName()和ClassLoader

Class.forName()执行类的初始化,ClassLoader不执行链接(包含准备,验证,解析),也就是说不执行类的初始化。

数据库为什么使用Class.forName()?

为了在反射回去的时候,能执行到类的static块。

Synchronized为什么引入偏向锁?

HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。

偏向锁是四种状态中最乐观的一种锁:从始至终只有一个线程请求某一把锁。

偏向锁头部Epoch字段值:表示此对象偏向锁的撤销次数。当默认撤销次数是40以上,表示此对象不再适用于偏向锁,当下次线程再次获取此对象时,直接变为轻量级锁。

cas自旋获取锁?

自适应自旋:根据以往自旋等待时间能否获取锁来动态调整自旋的时间(循环次数)。
如果在上次自旋时获取到锁,则会稍微增加下一次自旋时间;否则即稍微减少下一次自旋时间。

final修饰作用?

1、被final修饰的类不可以被继承

2、被final修饰的方法不可以被重写

3、被final修饰的变量不可以被改变

被final修饰的对象引用地址不可变,对象引用指向的内容可变。

String的intern方法 (1.7版本,返回常量池中该字符串的引用)

(1) 当常量池中不存在"xxx"这个字符串的引用,将这个对象的引用加入常量池,返回这个对象的引用。
(2) 当常量池中存在"xxx"这个字符串的引用,返回这个对象的引用;

String s = new String("Hello");//会在常量池里生成“Hello”以及堆中生成“Hello”对象。
String str1 = new String("Hello")+ new String("World");//会在常量池里生成“Hello”和“World”以及堆中生成“HelloWorld”对象。

排序算法

算法的选择:1.当n较小时,对稳定性不作要求时宜用选择排序,对稳定性有要求时宜用插入或冒泡排序。
          2.若待排序的记录的关键字在一个明显有限范围内时,且空间允许是用桶排序。
          3.当n较大时,关键字元素比较随机,对稳定性没要求宜用快速排序。
          4.当n较大时,关键字元素可能出现本身是基本有序的,且对稳定性有要求时,空间允许的情况下,宜用归并排序。
          5.当n较大时,关键字元素可能出现本身是有序的,对稳定性没有要求时宜用堆排序。
稳定性与非稳定性:12429 排序后为 12249,第一个2相对于第二个2位置不变,则为稳定,否则为不稳定。
内排序和外排序:1.在排序过程中,所有需要排序的数都在内存,并在内存中调整它们的存储顺序,称为内排序;
             2.在排序过程中,只有部分数被调入内存,并借助内存调整数在外存中的存放顺序排序方法称为外排序。
//冒泡排序
public static void bubbleSort(int[] arr){
    int length = arr.length;
    if(length<=1){
        return;
    }
    for(int i=0;i<length-1;i++){

        for(int j=i+1;j<length;j++){
            //从小到大
            if(arr[i]>arr[j]){
                int temp = arr[i];
                arr[i] = arr[j];
                arr[j] = temp;
            }
        }
    }
    for (int i:arr){
        System.out.print(i+" ");
    }
}

//选择排序
public static void selectSort(int[] arr){
    int length = arr.length;
    if(length<=1){
        return;
    }

    for(int i = 0;i<length-1;i++){
        //默认第一个下标元素为最小值
        int minIndex = i;

        for(int j = i+1;j<length;j++){
            //赋值下标
            if(arr[minIndex]>arr[j]){
                minIndex = j;
            }
        }
        //根据下标将最小的值放在前面
        if(minIndex != i){
            int temp = arr[i];
            arr[i] = arr[minIndex];
            arr[minIndex] = temp;
        }
    }
    for (int i:arr){
        System.out.print(i+" ");
    }
}

为什么重载equals的同时要重载hashCode?

如果我们只重写equals,而不重写hashCode方法,就会出现两个对象一样,但是hashCode不相等情况,在map等集合中应用时,就会出现问题,因为hashCode不一样,两个一样的对象会放到集合中不同的下标下。

CMC垃圾回收器

启动JVM参数加上-XX:+UseConcMarkSweepGC ,这个参数表示对于老年代的回收采用CMS。

cms回收过程:

  • 初始标记(STW initial mark)
  • 并发标记(Concurrent marking)
  • 并发预清理(Concurrent precleaning)
  • 重新标记(STW remark)
  • 并发清理(Concurrent sweeping)
  • 并发重置(Concurrent reset)

什么时候用cms,什么时候用G1?

cmc:1.硬件好,追求流畅 2.老年代较大

g1:服务端多核CPU、JVM内存占用较大的应用(至少大于4G)。

       应用在运行过程中会产生大量内存碎片、需要经常压缩空间。

       想要更可控、可预期的GC停顿周期;防止高并发下应用雪崩现象。

CMC和G1的区别?

  1. G1在压缩空间方面有优势
  2. G1通过将内存空间分成区域(Region)的方式避免内存碎片问题
  3. Eden, Survivor, Old区不再固定、在内存使用效率上来说更灵活
  4. G1可以通过设置预期停顿时间(Pause Time)来控制垃圾收集时间避免应用雪崩现象
  5. G1在回收内存后会马上同时做合并空闲内存的工作、而CMS默认是在STW(stop the world)的时候做
  6. G1会在Young GC中使用、而CMS只能在old区使用

CMS默认启动的并发线程数是(ParallelGCThreads+3)/4。

BIO/NIO/AIO?

  • BIO,同步阻塞式IO,简单理解:一个连接一个线程
  • NIO,同步非阻塞IO,简单理解:一个请求一个线程(selector轮询channel读写数据,如果channel数据处理时间过长,则也相当于阻塞)
  • AIO,异步非阻塞IO,简单理解:一个有效请求一个线程

消息系统RocketMQ

消息系统设计的问题:消息的顺序问题,消息的重复问题。

消息顺序问题:通过对消息的所有队列做负载均衡,使得同一类消息进入同一个队列保证顺序性。

消息重复问题:消费端处理消息的业务逻辑保持幂等性,保证每条消息都有唯一编号且保证消息处理成功与去重表的日志同时出现

RocketMQ不保证消息不重复,如果你的业务需要保证严格的不重复消息,需要你自己在业务端去重。

RocketMQ实现发送事务消息

RocketMQ第一阶段发送Prepared消息时,会拿到消息的地址,第二阶段执行本地事物,第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。细心的你可能又发现问题了,如果确认消息发送失败了怎么办?RocketMQ会定期扫描消息集群中的事物消息,这时候发现了Prepared消息,它会向消息发送者确认,Bob的钱到底是减了还是没减呢?如果减了是回滚还是继续发送确认消息呢?RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。

TransactionCheckListener:监听Prepared消息,根据策略来决断事务(回滚还是重新发送)

代码:

// 未决事务,MQ服务器回查客户端
// 也就是上文所说的,当RocketMQ发现`Prepared消息`时,会根据这个Listener实现的策略来决断事务
TransactionCheckListener transactionCheckListener = new TransactionCheckListenerImpl();
// 构造事务消息的生产者
TransactionMQProducer producer = new TransactionMQProducer("groupName");
// 设置事务决断处理类
producer.setTransactionCheckListener(transactionCheckListener);
// 本地事务的处理逻辑,相当于示例中检查Bob账户并扣钱的逻辑
TransactionExecuterImpl tranExecuter = new TransactionExecuterImpl();
producer.start()
// 构造MSG,省略构造参数
Message msg = new Message(......);
// 发送消息
SendResult sendResult = producer.sendMessageInTransaction(msg, tranExecuter, null);
producer.shutdown();

sendMessageInTransaction方法的源码

public TransactionSendResult sendMessageInTransaction(.....)  {
    // 逻辑代码,非实际代码
    // 1.发送消息
    sendResult = this.send(msg);
    // sendResult.getSendStatus() == SEND_OK
    // 2.如果消息发送成功,处理与消息关联的本地事务单元
    LocalTransactionState localTransactionState = tranExecuter.executeLocalTransactionBranch(msg, arg);
    // 3.结束事务
    this.endTransaction(sendResult, localTransactionState, localException);
}

endTransaction方法会将请求发往broker(mq server)去更新事物消息的最终状态:

  1. 根据sendResult找到Prepared消息
  2. 根据localTransaction更新消息的最终状态

如果endTransaction方法执行失败,导致数据没有发送到brokerbroker会有回查线程定时(默认1分钟)扫描每个存储事务状态的表格文件,如果是已经提交或者回滚的消息直接跳过,如果是prepared状态则会向Producer发起CheckTransaction请求,Producer会调用DefaultMQProducerImpl.checkTransactionState()方法来处理broker的定时回调请求,而checkTransactionState会调用我们的事务设置的决断方法,最后调用endTransactionOnewaybroker来更新消息的最终状态。

RocketMQ消息订阅

RocketMQ消息订阅有两种模式,一种是Push模式,即MQServer主动向消费端推送;另外一种是Pull模式,即消费端在需要时,主动到MQServer拉取。但在具体实现时,Push和Pull模式都是采用消费端主动拉取的方式。

Push模式示意图
Consumer端每隔一段时间主动向broker发送拉消息请求,broker在收到Pull请求后,如果有消息就立即返回数据,Consumer端收到返回的消息后,再回调消费者设置的Listener方法。如果broker在收到Pull请求时,消息队列里没有数据,broker端会阻塞请求直到有数据传递或超时才返回。

当然,Consumer端是通过一个线程将阻塞队列LinkedBlockingQueue<PullRequest>中的PullRequest发送到broker拉取消息,以防止Consumer一致被阻塞。而Broker端,在接收到Consumer的PullRequest时,如果发现没有消息,就会把PullRequest扔到ConcurrentHashMap中缓存起来。broker在启动时,会启动一个线程不停的从ConcurrentHashMap取出PullRequest检查,直到有数据返回。

ICMP协议

ICMP不是高层协议,而是IP层的协议,当传送IP数据包发生错误。比如主机不可达,路由不可达等等,ICMP协议将会把错误信息封包,然后传送回给主机。给主机一个处理错误的机会,这 也就是为什么说建立在IP层以上的协议是可能做到安全的原因。

局部性原理与磁盘预读

MySQL为何将节点大小设置为页的整数倍,这就需要理解磁盘的存储原理。磁盘本身存取就比主存慢很多,在加上机械运动损耗(特别是普通的机械硬盘),磁盘的存取速度往往是主存的几百万分之一,为了尽量减少磁盘I/O,磁盘往往不是严格按需读取,而是每次都会预读,即使只需要一个字节,磁盘也会从这个位置开始,顺序向后读取一定长度的数据放入内存,预读的长度一般为页的整数倍。

这样做的理论依据是计算机科学中著名的局部性原理:当一个数据被用到时,其附近的数据也通常会马上被使用;程序运行期间所需要的数据通常比较集中。由于磁盘顺序读取的效率很高(不需要寻道时间,只需很少的旋转时间),因此对于具有局部性的程序来说,预读可以提高I/O效率。

预读的长度一般为页(page)的整倍数,页是计算机管理存储器的逻辑块,硬件及OS往往将主存和磁盘存储区分割为连续的大小相等的块,每个存储块称为一页(许多OS中,
页的大小通常为4K)。主存和磁盘以页为单位交换数据。当程序要读取的数据不在主存中时,会触发一个缺页异常,此时系统会向磁盘发出读盘信号,磁盘会找到数据的起始
位置并向后连续读取一页或几页载入内存中,然后异常返回,程序继续运行。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值