disruptor 介绍

一、背景


1.来源

Disruptor是英国外汇交易公司LMAX开发的一个高性能队列,研发的初衷是解决内部的内存队列的延迟问题,而不是分布式队列。基于Disruptor开发的系统单线程能支撑每秒600万订单,2010年在QCon演讲后,获得了业界关注。

2.应用背景和介绍

据目前资料显示:应用Disruptor的知名项目有如下的一些:Storm, Camel, Log4j2,还有目前的美团点评技术团队也有很多不少的应用,或者说有一些借鉴了它的设计机制。
Disruptor是一个高性能的线程间异步通信的框架,即在同一个JVM进程中的多线程间消息传递。

二、传统队列问题


首先这里说的队列也仅限于Java内部的消息队列

队列有界性结构队列类型
ArrayBlockingQueue有界加锁数组阻塞
LinkedBlockingQueue可选加锁链表阻塞
ConcurrentLinkedQueue无界无锁链表非阻塞
LinkedTransferQueue无界无锁链表阻塞
PriorityBlockingQueue无界加锁阻塞
DelayQueue无界加锁阻塞

队列的底层数据结构一般分成三种:数组、链表和堆。其中,堆这里是为了实现带有优先级特性的队列,暂且不考虑。
在稳定性和性能要求特别高的系统中,为了防止生产者速度过快,导致内存溢出,只能选择有界队列;同时,为了减少Java的垃圾回收对系统性能的影响,会尽量选择array/heap格式的数据结构。这样筛选下来,符合条件的队列就只有ArrayBlockingQueue。但是ArrayBlockingQueue是通过加锁的方式保证线程安全,而且ArrayBlockingQueue还存在伪共享问题,这两个问题严重影响了性能。
其中对于影响性能的两种方式:加锁和伪共享我们这里首先介绍下。加锁方式和不加锁的CAS方式这里不再进行介绍,我们这里首先对其中的伪共享问题讲解下。

1.伪共享概念

共享

计算机早就支持多核,软件也越来越多的支持多核运行,其实也可以叫做多处理运行。一个处理器对应一个物理插槽。其中一个插槽对应一个L3 Cache,一个槽包含多个cpu。一个cpu包含寄存器、L1 Cache、L2 Cache,如下图所示:
在这里插入图片描述

其中越靠近cpu则,速度越快,容量则越小。其中L1和L2是只能给一个cpu进行共享,但是L3是可以给同一个槽内的cpu共享,而主内存,是可以给所有的cpu共享,这就是内存的共享。
其中cpu执行运算的流程是这样:首先回去L1里面查找对应数据,如果没有则去L2、L3,如果都没有,则就会去主内存中去拿,走的路越长,则耗费时间越久,性能就会越低。
需要注意的是,当线程之间进行共享数据的,需要将数据写回到主内存中,而另一个线程通过访问主内存获得新的数据。
有人就会问了,多个线程之间不是会有一些非主内存的缓存进行共享么,那么另外一个线程会不会直接访问到修改之前的内存呢。答案是会的,但是有一点,就是这种数据我们可以通过设置缓存失效测试来进行保证缓存的最新,这个方式其实在cpu这里进行设置的,叫内存屏障(其实就是在cpu这里设置一条指令,这个指令就是禁止cpu重排序,这个屏障之前的不能出现在屏障之后,屏障之后的处理不能出现屏障之前,也就是屏障之后获取到的数据是最新的),对应到应用层面就是一个关键字volatile,下面会有一些进行介绍。

缓存行

刚刚说的缓存失效其实指的是Cache line的失效,也就是缓存行,Cache是由很多个Cache line 组成的,每个缓存行大小是32~128字节(通常是64字节)。我们这里假设缓存行是64字节,而java的一个Long类型是8字节,这样的话一个缓存行就可以存8个Long类型的变量,如下图所示:

在这里插入图片描述

 一个缓存对应的缓存行的结构图
  • 1

cpu 每次从主内存中获取数据的时候都会将相邻的数据存入到同一个缓存行中。假设我们访问一个Long内存对应的数组的时候,如果其中一个被加载到内存中,那么对应的后面的7个数据也会被加载到对应的缓存行中,这样就会非常快的访问数据。

伪共享

刚我们说了缓存的失效其实就是缓存行的失效,缓存行失效的原理是什么,这里又涉及到一个MESI协议(缓存一致性协议),我们这里不介绍这个,感兴趣的可以看下附录部分,首先我们用Disruptor中很经典的讲解伪共享的图来讲解下:

在这里插入图片描述

上图中显示的是一个槽的情况,里面是多个cpu, 如果cpu1上面的线程更新了变量X,根据MESI协议,那么变量X对应的所有缓存行都会失效,这个时候如果cpu2中的线程进行读取变量Y,发现缓存行失效,就会按照缓存查找策略,往上查找,如果cpu1对应的线程更新变量X后又访问了变量X,那么左侧的L1、L2和槽内的L3 缓存行都会得到生效。这个时候cpu2线程可以在L3 Cache 中得到生效的数据,否则的话(即cpu1对应的线程更新X后没有访问X)cpu2的线程就只能从主内存中获取数据,对性能就会造成很大的影响,这就是伪共享。
表面上 X 和 Y 都是被独立线程操作的,而且两操作之间也没有任何关系。只不过它们共享了一个缓存行,但所有竞争冲突都是来源于共享。

2.ArrayBlockingQueue 的伪共享问题

刚我们已经讲了伪共享的问题,那么ArrayBlockingQueue的这个伪共享问题存在于哪里呢,分析下核心的部分源码

    public void put(E e) throws InterruptedException {checkNotNull(e);final ReentrantLock lock = this.lock;//获取当前对象锁lock.lockInterruptibly();try {while (count == items.length)//阻塞并释放锁,等待notFull.signal()通知notFull.await();//将数据放入数组enqueue(e);} finally {lock.unlock();}}
    private void enqueue(E x) {final Object[] items = this.items;//putIndex 就是入队的下标items[putIndex] = x;if (++putIndex == items.length)putIndex = 0;count++;notEmpty.signal();}
public E take() throws InterruptedException {final ReentrantLock lock = this.lock;//加锁lock.lockInterruptibly();try {while (count == 0)//阻塞并释放对象锁,并等待notEmpty.signal()通知notEmpty.await();//在数据不为空的情况下return dequeue();} finally {lock.unlock();}}
private E dequeue() {final Object[] items = this.items;//takeIndex 是出队的下标E x = (E) items[takeIndex];items[takeIndex] = null;if (++takeIndex == items.length)takeIndex = 0;count--;if (itrs != null)itrs.elementDequeued();notFull.signal();return x;
}

其中最核心的三个成员变量为
putIndex:入队下标
takeIndex:出队下标
count:队列中元素的数量
而三个成员的位置如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传在这里插入图片描述

这三个变量很容易放到同一个缓存行中,为此专门用一个伪共享检测工具进行检测,目前检测伪共享的工具只有Intel的Intel Vtune 目前刚发现有mac os 版本,但是经过测试发现,该工具无法分析macOs 的处理器配置,用的时候发现如下错误“无法检测到支持的处理器配置”,这个可以遗留给其他同学,工具的安装和使用方式,可以查看附录中的另外的一个连接。

三、高性能原理


刚说了上面队列的两个性能问题:一个是加锁,一个是伪共享,那么disruptor是怎么解决这两个问题的,以及除了解决这两个问题之外,还引入了其他什么先进的东西提升性能的。
这里简单列举下:

  • 引入环形的数组结构:数组元素不会被回收,避免频繁的GC,
  • 无锁的设计:采用CAS无锁方式,保证线程的安全性
  • 属性填充:通过添加额外的无用信息,避免伪共享问题
  • 元素位置的定位:采用跟一致性哈希一样的方式,一个索引,进行自增

1.环形数组结构

环形数组结构是整个Disruptor的核心所在。
首先因为是数组,所以要比链表快,而且根据我们对上面缓存行的解释知道,数组中的一个元素加载,相邻的数组元素也是会被预加载的,因此在这样的结构中,cpu无需时不时去主存加载数组中的下一个元素。而且,你可以为数组预先分配内存,使得数组对象一直存在(除非程序终止)。这就意味着不需要花大量的时间用于垃圾回收。此外,不像链表那样,需要为每一个添加到其上面的对象创造节点对象—对应的,当删除节点时,需要执行相应的内存清理操作。环形数组中的元素采用覆盖方式,避免了jvm的GC。
其次结构作为环形,数组的大小为2的n次方,这样元素定位可以通过位运算效率会更高,这个跟一致性哈希中的环形策略有点像。在disruptor中,这个牛逼的环形结构就是RingBuffer,既然是数组,那么就有大小,而且这个大小必须是2的n次方,结构如下:
在这里插入图片描述

其实质只是一个普通的数组,只是当放置数据填充满队列(即到达2^n-1位置)之后,再填充数据,就会从0开始,覆盖之前的数据,于是就相当于一个环。

2.生产和消费模式

根据上面的环形结构,我们来具体分析一下Disruptor的工作原理。
Disruptor 不像传统的队列,分为一个队头指针和一个队尾指针,而是只有一个角标(上面的seq),那么这个是如何保证生产的消息不会覆盖没有消费掉的消息呢。
在Disruptor中生产者分为单生产者和多生产者,而消费者并没有区分。单生产者情况下,就是普通的生产者向RingBuffer中放置数据,消费者获取最大可消费的位置,并进行消费。而多生产者时候,又多出了一个跟RingBuffer同样大小的Buffer,称为AvailableBuffer。在多生产者中,每个生产者首先通过CAS竞争获取可以写的空间,然后再进行慢慢往里放数据,如果正好这个时候消费者要消费数据,那么每个消费者都需要获取最大可消费的下标,这个下标是在AvailableBuffer进行获取得到的最长连续的序列下标。

多生产者——生产

假设现在又两个生产者,开始写数据,通过CAS竞争,w1得到的34的空间,w2得到了78的空间,其中6是代表已被写入或者没有被消费的数据。

在这里插入图片描述

多生产者——消费

绿色代表已经写OK的数据
  • 1

假设三个生产者在写中,还没有置位AvailableBuffer,那么消费者可获取的消费下标只能获取到6,然后等生产者都写OK后,通知到消费者,消费者继续重复上面的步骤。如下图

在这里插入图片描述

消费者常见的等待

BusySpinWaitStrategy: 自旋等待,类似Linux Kernel使用的自旋锁。低延迟但同时对CPU资源的占用也多。
BlockingWaitStrategy: 使用锁和条件变量。CPU资源的占用少,延迟大,默认等待策略。
SleepingWaitStrategy: 在多次循环尝试不成功后,选择让出CPU,等待下次调度,多次调度后仍不成功,尝试前睡眠一个纳秒级别的时间再尝试。这种策略平衡了延迟和CPU资源占用,但延迟不均匀。
YieldingWaitStrategy: 在多次循环尝试不成功后,选择让出CPU,等待下次调。平衡了延迟和CPU资源占用,但延迟也比较均匀。
PhasedBackoffWaitStrategy: 上面多种策略的综合,CPU资源的占用少,延迟大

3.牛逼的下标指针

RingBuffer的指针(Sequence)属于一个volatile变量,同时也是我们能够不用锁操作就能实现Disruptor的原因之一,而且通过缓存行补充,避免伪共享问题。 该所谓指针是通过一直自增的方式来获取下一个可写或者可读数据,该数据是Long类型,不用担心会爆掉。有人计算过: long的范围最大可以达到9223372036854775807,一年365 * 24 * 60 * 60 = 31536000秒,每秒产生1W条数据,也可以使用292年。

class LhsPadding{//缓存行补齐, 提升cache缓存命中率protected long p1, p2, p3, p4, p5, p6, p7;
}class Value extends LhsPadding{protected volatile long value;
}class RhsPadding extends Value{//缓存行补齐, 提升cache缓存命中率protected long p9, p10, p11, p12, p13, p14, p15;
}public class Sequence extends RhsPadding{...
}

四、用法


用法很简单,一共三个角色:生产者,消费者,disruptor对象

1.简单用法

disruptor对象

disruptor 就两个构造方法

public Disruptor(final EventFactory<T> eventFactory,    // 数据实体构造工厂final int ringBufferSize,              // 队列大小,必须是2的次方final ThreadFactory threadFactory,     // 线程工厂final ProducerType producerType,       // 生产者类型,单个生产者还是多个final WaitStrategy waitStrategy){      // 消费者等待策略...
}public Disruptor(final EventFactory<T> eventFactory,     final int ringBufferSize, final ThreadFactory threadFactory){...
}

生产处理

生产者这里没有固定的对象,只是需要获取放置数据的位置,然后进行publish

public void send(String data){RingBuffer<MsgEvent> ringBuffer = this.disruptor.getRingBuffer();//获取下一个放置数据的位置long next = ringBuffer.next();try{MsgEvent event = ringBuffer.get(next);event.setValue(data);}finally {//发布事件ringBuffer.publish(next);}
}

消费处理

消费处理可以有如下几种
  • 1
public EventHandlerGroup<T> handleEventsWith(final EventHandler<? super T>... handlers){...
}
public EventHandlerGroup<T> handleEventsWith(final EventProcessor... processors){...
}
public EventHandlerGroup<T> handleEventsWith(final EventProcessorFactory<T>... eventProcessorFactories){...
}
public EventHandlerGroup<T> handleEventsWithWorkerPool(final WorkHandler<T>... workHandlers){...
}

简单用例

//消费者
public class MsgConsumer implements EventHandler<MsgEvent>{private String name;public MsgConsumer(String name){this.name = name;}@Overridepublic void onEvent(MsgEvent msgEvent, long l, boolean b) throws Exception {System.out.println(this.name+" -> 接收到信息: "+msgEvent.getValue());}
}//生产者处理
public class MsgProducer {private Disruptor disruptor;public MsgProducer(Disruptor disruptor){this.disruptor = disruptor;}public void send(String data){RingBuffer<MsgEvent> ringBuffer = this.disruptor.getRingBuffer();long next = ringBuffer.next();try{MsgEvent event = ringBuffer.get(next);event.setValue(data);}finally {ringBuffer.publish(next);}}public void send(List<String> dataList){dataList.stream().forEach(data -> this.send(data));}
}//触发测试
public class DisruptorDemo {@Testpublic void test(){Disruptor<MsgEvent> disruptor = new Disruptor<>(MsgEvent::new, 1024, Executors.defaultThreadFactory());//定义消费者MsgConsumer msg1 = new MsgConsumer("1");MsgConsumer msg2 = new MsgConsumer("2");MsgConsumer msg3 = new MsgConsumer("3");//绑定配置关系disruptor.handleEventsWith(msg1, msg2, msg3);disruptor.start();// 定义要发送的数据MsgProducer msgProducer = new MsgProducer(disruptor);msgProducer.send(Arrays.asList("nihao","hah"));}
}

输出(消费没有固定顺序):

1 -> 接收到信息: nihao
3 -> 接收到信息: nihao
3 -> 接收到信息: hah
2 -> 接收到信息: nihao
2 -> 接收到信息: hah
1 -> 接收到信息: hah

2.其他用法

上面主要介绍了多消费统一消费,但是在生产者模型中是有很多种,如下,一对一,一对多,多对多,多对一

在这里插入图片描述

1.单生产者生产数据,单消费者消费数据,一般用在后台处理的业务逻辑中。
2.单生产者生产数据,多个消费者消费数据(这里面有两种情况:同一个消息,可以被多个消费者分别消费。或者多个消费者组成一个组,一个消费者消费一个数据)。
3.多个生产者生产数据,单个消费者消费数据,可以用在限流或者排队等候单一资源处理的场景中。
4.多个生产者分别生产数据,多个消费者消费数据(这里面有两种情况:同一个消息,可以被多个消费者分别消费。或者多个消费者组成一个组,一个消费者消费一个数据)。

生产者配置

其中生产模式中的单生产者模式和多生产模式,这里主要是通过一个枚举:ProduceType来区分,建议,多个生产者用多生产者模式,性能会好点。

消费者配置

消费者模式这里分为两种:

统一消费:每个消费者都消费一份生产者生产的数据
分组消费:每个生产这生产的数据只被消费一次

统一消费像上面简单用法中运用即可,对于分组消费,用函数 handleEventsWithWorkerPool 即可

/*** 分组处理 handleEventWithWorkerPool*/
@Test
public void test1(){Disruptor<MsgEvent> disruptor = new Disruptor(MsgEvent::new, 1024, Executors.defaultThreadFactory());disruptor.handleEventsWithWorkerPool(new MyWorkHandler("work1"), new MyWorkHandler("work2"));disruptor.start();MsgProducer msgProducer = new MsgProducer(disruptor);msgProducer.send(Arrays.asList("aaa","bbb"));
}

输出:

work1 : MsgEvent(value=bbb)
work2 : MsgEvent(value=aaa)
work1 : MsgEvent(value=cc)
work2 : MsgEvent(value=dd)

消费顺序配置

在消费配置中,这里可以有很多种消费方式,比如:

1.消费者的顺序消费

/*** 测试顺序消费* 每一条消息的消费者1和3消费完毕后,消费者2再进行消费*/
@Test
public void test2(){MsgConsumer msg1 = new MsgConsumer("1");MsgConsumer msg2 = new MsgConsumer("2");MsgConsumer msg3 = new MsgConsumer("3");Disruptor<MsgEvent> disruptor = new Disruptor(MsgEvent::new, 1024, Executors.defaultThreadFactory());disruptor.handleEventsWith(msg1, msg3).then(msg2);disruptor.start();MsgProducer msgProducer = new MsgProducer(disruptor);msgProducer.send(Arrays.asList("aaa", "bbb", "ccc", "ddd"));
}
  •  

输出(里面的是根据每一条消息的消费者顺序):

1 -> 接收到信息: aaa
3 -> 接收到信息: aaa
1 -> 接收到信息: bbb
1 -> 接收到信息: ccc
2 -> 接收到信息: aaa
3 -> 接收到信息: bbb
3 -> 接收到信息: ccc
3 -> 接收到信息: ddd
1 -> 接收到信息: ddd
2 -> 接收到信息: bbb
2 -> 接收到信息: ccc
2 -> 接收到信息: ddd

2.消费分为多个支线,而且也有消费顺序问题

/*** 测试多支线消费* 消费者1和消费者3一个支线,消费者2和消费者4一个支线,消费者3和消费者4消费完毕后,消费者5再进行消费*/
@Test
public void test3(){MsgConsumer msg1 = new MsgConsumer("1");MsgConsumer msg2 = new MsgConsumer("2");MsgConsumer msg3 = new MsgConsumer("3");MsgConsumer msg4 = new MsgConsumer("4");MsgConsumer msg5 = new MsgConsumer("5");//支线:消费者1和消费者3disruptor.handleEventsWith(msg1, msg3);//支线:消费者2和消费者4disruptor.handleEventsWith(msg2, msg4);//消费者3和消费者4执行完之后,指向消费者5disruptor.after(msg3, msg4).handleEventsWith(msg5);disruptor.start();MsgProducer msgProducer = new MsgProducer(disruptor);msgProducer.send(Arrays.asList("aaa", "bbb", "ccc", "ddd"));
}

1 -> 接收到信息: aaa
2 -> 接收到信息: aaa
2 -> 接收到信息: bbb
3 -> 接收到信息: aaa
3 -> 接收到信息: bbb
4 -> 接收到信息: aaa
4 -> 接收到信息: bbb
5 -> 接收到信息: aaa
1 -> 接收到信息: bbb
5 -> 接收到信息: bbb

五、常见问题


下面介绍下一些常见问题。

1.disruptor应该如何用才能发挥最大功效?

disruptor原本就是事件驱动的设计,其整个架构跟普通的多线程很不一样。比如一种用法,将disruptor作为业务处理,中间带I/O处理,这种玩法比多线程还慢;相反,如果将disruptor做业务处理,需要I/O时采用nio异步调用,不阻塞disruptor消费者线程,等到I/O异步调用回来后在回调方法中将后续处理重新塞到disruptor队列中,可以看出来,这是典型的事件处理架构,确实能在时间上占据优势,加上ringBuffer固有的几项性能优化,能让disruptor发挥最大功效。

2.如果buffer常常是满的怎么办?

一种是把buffer变大,另一种是从源头解决producer和consumer速度差异太大问题,比如试着把producer分流,或者用多个disruptor,使每个disruptor的load变小。

3. 什么时候使用disruptor?

如果对延迟的需求很高,可以考虑使用。

六、参考:


官方git
https://github.com/LMAX-Exchange/disruptor
https://lmax-exchange.github.io/disruptor/
伪共享:
https://mechanical-sympathy.blogspot.com/2011/07/false-sharing.html
内存屏障:
http://in355hz.iteye.com/blog/1797829
MESI(缓存一致性协议)
https://www.cnblogs.com/cyfonly/p/5800758.html
ArrayBlockingQueue 伪共享问题
https://www.jianshu.com/p/71c9bc3bfe1a
jdk 本身针对伪共享做的处理
https://www.cnblogs.com/Binhua-Liu/p/5620339.html
intel vtune 分析伪共享案例
https://software.intel.com/zh-cn/vtune-amplifier-cookbook-false-sharing
RingBuffer工作原理
https://www.jianshu.com/p/71c9bc3bfe1a
http://wiki.jikexueyuan.com/project/disruptor-getting-started/the-framework.html
https://www.jianshu.com/p/d6375295fad4
http://colobu.com/2014/12/22/why-is-disruptor-faster-than-ArrayBlockingQueue/
https://blog.csdn.net/kobejayandy/article/details/18329583
https://blog.csdn.net/u014313492/article/details/42556341
Disruptor 是如何工作的
http://in355hz.iteye.com/blog/1797829
队列的各种场景
http://www.uml.org.cn/zjjs/2016060310.asp
消费顺序
http://357029540.iteye.com/blog/2395677

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/386534.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

算法题+JVM+自定义View,详细的Android学习指南

前言 想要成为一名优秀的Android开发&#xff0c;你需要一份完备的知识体系&#xff0c;在这里&#xff0c;让我们一起成长为自己所想的那样~。 学算法真的很痛苦&#xff0c;虽然大数据现在很火&#xff0c;但找到适合自己定位的职业也未尝不是一种合理选择。 投百度的经历非…

用过的前端插件合集

用过的前端插件合集 FontAwesome字体 Font Awesome详细用法参见上述站点的Examples。 SweetAlert系列 SweetAlertSweetAlert2SweetAlert 到 SweetAlert2 升级指南示例&#xff1a; 基本使用&#xff1a; swal("标题","内容","success);使用SweetAlert…

CAS和AQS

CAS 全称&#xff08;Compare And Swap&#xff09;,比较交换 Unsafe类是CAS的核心类&#xff0c;提供硬件级别的原子操作。 // 对象、对象的地址、预期值、修改值 public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);缺点&#xf…

系统盘点Android开发者必须掌握的知识点,全网疯传

最近在知乎上&#xff0c;有许多人在邀请我去回答“Android前景怎么样、是不是要凉了、是不是应该考虑要转行&#xff1f;”等一系列的问题。 想着可能有很多人都有这样的担心&#xff0c;于是就赶紧写篇文章&#xff0c;来跟你们谈下Android开发的前景到底怎么样&#xff1f;…

数据库操作DDL

show database; 查看所有数据库 drop database db_name; 删除数据库 create database db_name;创建数据库 一个数据库对应一个文件夹 create database if not exists db_name; show warnings; 查看所有警告 show create databae db_name;查看创建的数据库 create database if n…

细数Android开发者的艰辛历程,已拿offer附真题解析

笼统来说&#xff0c;中年程序员容易被淘汰的原因其实不外乎三点。 1、输出能力已到顶点。这个人奋斗十来年了&#xff0c;依旧碌碌无为&#xff0c;很明显这人的天花板就这样了&#xff0c;说白了&#xff0c;天赋就这样。 2、适应能力越来越差。年纪大&#xff0c;有家庭&…

原子操作类AtomicInteger详解

为什么需要AtomicInteger原子操作类&#xff1f; 对于Java中的运算操作&#xff0c;例如自增或自减&#xff0c;若没有进行额外的同步操作&#xff0c;在多线程环境下就是线程不安全的。num解析为numnum1&#xff0c;明显&#xff0c;这个操作不具备原子性&#xff0c;多线程并…

移动端Rem之讲解总结

日妈常说的H5页面&#xff0c;为啥叫H5页面嘛&#xff0c;不就是手机上展示的页面吗&#xff1f;那是因为啊手机兼容所有html5新特性&#xff0c;所以跑在手机上的页面也叫h5页面&#xff0c;跨平台&#xff08;安装ios),基于webview&#xff0c;它就是终端开发的一个组件&…

终于有人把安卓程序员必学知识点全整理出来了,送大厂面经一份!

除了Bug&#xff0c;最让你头疼的问题是什么&#xff1f;单身&#xff1f;秃头&#xff1f;996?面试造火箭&#xff0c;工作拧螺丝&#xff1f; 作为安卓开发者&#xff0c;除了Bug&#xff0c;经常会碰到下面这些问题&#xff1a; 应用卡顿&#xff0c;丢帧&#xff0c;屏幕画…

mq引入以后的缺点

系统可用性降低? 一旦mq不能使用以后,系统A不能发送消息到mq,系统BCD无法从mq中获取到消息.整个系统就崩溃了. 如何解决: 系统复杂程度增加? 加入mq以后,mq引入来的问题很多,然后导致系统的复杂程度增加. 如何解决 系统的一致性降低? 有人给系统A发送了一个请求,本来这个请求…

网易云的朋友给我这份339页的Android面经,成功入职阿里

IT行业的前景 近几年来&#xff0c;大数据、人工智能AI、物联网等一些技术不断发展&#xff0c;也让人们看到了IT行业的繁荣与良好的前景。越来越多的高校学府加大了对计算机的投入&#xff0c;设立相应的热门专业来吸引招生。当然也有越来越多的人选择从事这个行业&#xff0…

git介绍和常用操作

转载于:https://www.cnblogs.com/kesz/p/11124423.html

网易云的朋友给我这份339页的Android面经,满满干货指导

想要成为一名优秀的Android开发&#xff0c;你需要一份完备的知识体系&#xff0c;在这里&#xff0c;让我们一起成长为自己所想的那样~。 25%的面试官会在头5分钟内决定面试的结果60%的面试官会在头15分钟内决定面试的结果 一般来说&#xff0c;一场单面的时间在30分钟左右&…

synchronized 和Lock区别

synchronized实现原理 Java中每一个对象都可以作为锁&#xff0c;这是synchronized实现同步的基础&#xff1a; 普通同步方法&#xff0c;锁是当前实例对象静态同步方法&#xff0c;锁是当前类的class对象同步方法块&#xff0c;锁是括号里面的对象 当一个线程访问同步代码块…

美团安卓面试,难道Android真的凉了?快来收藏!

我所接触的Android开发者&#xff0c;百分之九十五以上 都遇到了以下几点致命弱点&#xff01; 如果这些问题也是阻止你升职加薪&#xff0c;跳槽大厂的阻碍。 那么我确信可以帮你突破瓶颈&#xff01; 1.开发者的门越来越高&#xff1a; 小厂的机会少了&#xff0c;大厂…

django -- 实现ORM登录

前戏 上篇文章写了一个简单的登录页面&#xff0c;那我们可不可以实现一个简单的登录功能呢&#xff1f;如果登录成功&#xff0c;给返回一个页面&#xff0c;失败给出错误的提示呢&#xff1f; 在之前学HTML的时候&#xff0c;我们知道&#xff0c;网页在往服务器提交数据的时…

美团点评APP在移动网络性能优化的实践,通用流行框架大全

" 对于程序员来说&#xff0c;如果哪一天开始他停止了学习&#xff0c;那么他的职业生涯便开始宣告消亡。” 高薪的IT行业是众多年轻人的职业梦想&#xff0c;然而&#xff0c;一旦身入其中却发觉没有想像中那么美好。被称为IT蓝领的编程员&#xff0c;工作强度大&#xf…

centos7.0利用yum快速安装mysql8.0

我这里直接使用MySQL Yum存储库的方式快速安装&#xff1a; 抽象 MySQL Yum存储库提供用于在Linux平台上安装MySQL服务器&#xff0c;客户端和其他组件的RPM包。这些软件包还可以升级和替换从Linux发行版本机软件存储库安装的任何第三方MySQL软件包&#xff0c;如果可以从MySQL…

腾讯3轮面试都问了Android事件分发,论程序员成长的正确姿势

前言 这些题目是网友去美团等一线互联网公司面试被问到的题目。笔者从自身面试经历、各大网络社交技术平台搜集整理而成&#xff0c;熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。 主要分为以下几部分&#xff1a; &#xff08;1&#xff09;Android面试题 …

happens-before规则和as-if-serial语义

概述 本文大部分整理自《Java并发编程的艺术》&#xff0c;温故而知新&#xff0c;加深对基础的理解程度。 指令序列的重排序 我们在编写代码的时候&#xff0c;通常自上而下编写&#xff0c;那么希望执行的顺序&#xff0c;理论上也是逐步串行执行&#xff0c;但是为了提高…