【其他】专业问题 - hippowc/hippowc.github.io GitHub Wiki
Java面试题,总有一天会用到,不断更新中。。
String类的内部实现是
private final char value[];
private int hash;
char数组,final类型的,表示不能再次被赋值;hash用来保存String的hash值;
关于String的hash值的计算方法深入研究也很有名堂:每个char的ascii值乘以31的n-i次方,然后相加,为什么是乘以31,目前只能说最好选择一个素数,这样相乘的到的结果值更不容易相同。
StringBuffer是线程安全的,StringBuilder是线程不安全的,内部char数组的初始容量是16,对于内部实现使用数组的,都会有扩容的问题,扩容都是新建一个合理大小的数组,把原来数组copy进去。
Map.entrySet(), Iterator<Map.Entry<Integer, Integer>> entries = map.entrySet().iterator()等等
HashMap是线程不安全的,key-value可以为nul,HashTable和ConcurrentHashMap都是线程安全的,key-value都不可以为null,HashTable与另外两个父类不同是的Dictionary,每个方法都是同步的,但是这样并不能保证使用的时候完全同步。ConcurrentHashMap对并发做了专门的优化,官方建议无需并发时使用HashMap,需要并发时使用ConcurrentHashMap。
源码时间: HashMap的主要成员变量
transient Node<K,V>[] table
transient Set<Map.Entry<K,V>> entrySet;
transient int size;
transient int modCount;
int threshold;
final float loadFactor;
Node类型的数组,也就是一个外层数组,内层链表的结构。其中Node是Entry的实现。这个数组中的每一个位置被成为bucket(桶),HashMap有两个重要的参数:
- Capacity Capacity就是buckets的数目,就是数组的长度
- Load factor Load factor就是buckets填满程度的最大比例,默认是0.75
几个问题:
- 如何根据key,将该节点放入相应的Node数组中?或者说如何根据key计算对应的数组下标
先获取key的hashCode,hashCode高位不变,低位与高位异或,然后与数组长度进行与运算,获取数组下标;解释:高位与低位异或是为了将高位的hash变相保留下来,但是具体是否更加散列了呢,道理上没办法解释,结果上来看是更散列了。然后与capacity进行与,其实是只保留后面几位,很好的替代了取模。这也是为什么建议容量是2的次方,这样才能更好的保证hash值后面几位被原封不动的保留下来。
- JDK8后新增来红黑树?是干什么用的?
在一个bucket中Node数量超过8个之后,就会进行桶的树形化 treeifyBin,这是为了提高链表的查询效率。红黑树是一种自平衡二叉查找树,查找效率高于链表(O(Logn))
ConcurrentHashMap如何保证同步?
与HashTable不同,HashTable简单的将每个方法进行同步,也就是说执行每个方法的时候都会锁住整个对象。执行效率地下。ConcurrentHashMap在具体场景下,对并发进行了优化处理,这个例子也告诉我们优化要基于实际场景;散列表一般的应用场景是:除了少数插入操作和删除操作外,绝大多数都是读取操作,而且读操作在大多数时候都是成功的。正是基于这个前提,ConcurrentHashMap 针对读操作做了大量的优化。
ConcurrentHashMap实现特点:
jdk1.8取消了Segemnt分段锁,
- synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发
- 首先使用无锁操作CAS(Compare and Swap)插入头结点,如果插入失败,说明已经有别的线程插入头结点了,再次循环进行操作。如果头结点已经存在,则通过synchronized获得头结点锁,进行后续的操作。性能比segment分段锁又再次提升。
- 使用volatile修饰Node字段,使得一个线程对共享变量修改的状态对另一个线程是可见的;保证并发修改时,其他线程看到时最新修改的值
ps:volatile
- 共享变量指的是可以同时被多个线程同时访问的变量,共享变量都被存放在堆内存中,volatile只作用于共享变量。
- 重排序;重排序值得指的是为了提高代码执行性能,指令的执行顺序被编译器或者运行时环境调整顺序的现象
- 内存的可见性,是指在线程之间的可见性,一个线程对共享变量修改的状态对另一个线程是可见的
- volatile修饰的变量不允许线程内部缓存和重排序,线程读取数据的时候直接读写内存,同时volatile不会对变量加锁,因此性能会比synchronized好
- 常用的:ExcutorService(并发调用框架),ConcurrentHashMap,BlockingQueue
- 同步:synchronize,Lock()
- 工具:Latch(闭锁),CyclicBarrier(栅栏),Exchanger(交换机),Semaphore(信号量)
- 其他:原子变量, ThreadLocal
后三种还需要学习下
编译器会对java源码进行解析并生成class文件,而注解也是在编译时由编译器进行处理,编译器会对注解符号处理并附加到class结构中 ;注解被编译后的本质就是一个继承Annotation接口的接口
- Lock能完成synchronized所实现的所有功能
- synchronized会自动释放锁,而Lock一定要求程序员手工释放,并且必须在finally从句中释放
- Lock还有更强大的功能,例如,它的tryLock方法可以非阻塞方式去拿锁。
- 新生:程序中用构造方法(new操作符)创建一个新线程时,如new Thread(r);此时它已经有了相应的内存空间和其它资源,但是还没有开始执行
- 就绪:调用该线程的 start()方法就可以启动线程,当线程启动时,线程进入就绪状态
- 运行:当就绪状态的线程被调用并获得处理器资源时,线程就进入了运行状态。
- 阻塞:在可执行状态下,如果调用 sleep()、 suspend()、 wait()等方法,线程都将进入堵塞状态。堵塞时,线程不能进入排队队列,只有当引起堵塞的原因被消除后,线程转入就绪状态。重新到就绪队列中排队等待,这时被CPU调度选中后会从原来停止的位置开始继续执行。
- 死亡:线程调用 stop()方法、destory()方法或 run()方法执行结束后,线程即处于死亡状态
细分大概有这么几个状态:
- 就绪(runnable)
- 阻塞(Blocked):等待锁
- 阻塞(Waiting):Object#wait() Thread#join() LockSupport#park() 且没有超时参数
- 阻塞(Time-Waiting 限时等待):Thread#sleep() Object#wait() Thread#join()且有超时参数 LockSupport#parkNanos()等
- sleep()是线程线程类(Thread)的方法,调用会暂停此线程指定的时间,只让出了CPU,不会释放对象锁;wait()是Object的方法,调用会放弃对象锁,进入等待队列,待调用notify()/notifyAll()唤醒指定的线程或者所有线程,才会进入锁池,不再次获得对象锁才会进入运行状态;
- sleep()方法可以在任何地方使用;wait()方法则只能在同步方法或同步块中使用;
<< : 左移运算符,num << 1,相当于num乘以2
: 右移运算符,num >> 1,相当于num除以2
: 无符号右移,忽略符号位,空位都以0补齐 & : 按照位做与运算 | : 按照位做或运算 ^ : 按照位做异或,位异或:位不同为真,位相同为假;场景:可以快速比较两个值是否相同等,和0异或不变,和1异或取反。
可以使用Integer.toXXString方法,打印二进制、八进制、十进制
对于ThreadLocal使用后一定要记得remove;否则下一条线程重用这个Thread的时候,很可能get到的是上条线程set的数据而不是自己想要的内容
- JVM通过类的全限定名(包命+类名)找到类的.class文件
- 从下往上查找类是否已经加载,如果找到,直接返回已加载的类,如果没找着接着往上找。
- 链接:链接就是要把二进制的.class文件转换成可以被jvm执行的Class对象的过程。这个过程又分为:检验、准备、解析。
- 初始化:执行类或接口中的静态初始化函数(块),将静态变量初始化。这就是我们平时理解的对静态变量赋值。
Java中的内存泄露,广义并通俗的说,就是:不再会被使用的对象的内存不能被回收,就是内存泄露。对象都是有生命周期的,有的长,有的短,如果长生命周期的对象持有短生命周期的引用,就很可能会出现内存泄露。
决的原则就是尽量减小对象的作用域以及手动设置null值。
数据分片:数据分片就是把数据均匀分散到多个实例中。数据分片可以采用以下几种规则:区间分片、hash分片、 slot分片。但总归还是会有一个前置代理用于分发请求,但是这个代理可以是客户端,也可以是服务器,一般是服务器,那么这个时候这个服务器还是会有单点问题。
一致性哈希算法在1997年由麻省理工学院提出的一种分布式哈希(DHT)实现算法,设计目标是为了解决因特网中的热点(Hot spot)问题,对于分布式缓存,不同机器上存储不同对象的数据。为了实现这些缓存机器的负载均衡,可以使用式子来定位对象缓存的存储机器:m = hash(o) mod n;o为对象的名称,n为机器的数量,m为机器的编号,hash为一hash函数;
在大部分时候都可以工作得很好,然而,当机器需要扩容或者机器出现宕机的情况下,事情就比较棘手了。 当机器扩容,需要增加一台缓存机器时,通过这种hash取模的方式,可能使大部分数据无法命中。
一致性hash算法通过一个叫作一致性hash环的数据结构实现。然后将对象和机器通过hash函数都放置到同一个hash环后,在hash环上顺时针查找距离这个对象的hash值最近的机器,即是这个对象所属的机器。
可用性:线上使用过程中,如果出现某些缓存实例不可用,大量请求穿透会给DB带来巨大的压力,极端情况会导致雪崩场景,这需要有更好的方式保证缓存的高可用。于是我们采用用主从(Master/Slave)的架构,实际就是备份机
和分布式事务相比,幂等设计的优势在于它的轻量级,容易适应异构环境,以及性能和可用性方面。在某些性能要求比较高的应用,幂等设计往往是唯一的选择。
如果一个操作不涉及数据更改,那么肯定是幂等的;设计到数据更改的操作,讨论幂等性更有意义。
对数据的操作无非增删改查:查询、更改和删除是天然幂等的,最需要注意的是新增数据,解决方式是增加唯一性索引;或者增加时进行查询(这个时候要考虑加锁)。
看到并发控制首先要想到并发的几个策略:
- 乐观锁
- 悲观锁
- 幂等控制
从加锁的角度来考虑:单个JVM可以使用java提供的并发工具来控制。对于多个JVM,可以理解为多个应用,或者分布式应用。并发控制的关键是:如何在一个统一的地方获得锁。譬如使用统一的缓存作为锁:
- 在需要并发控制的代码外层增加锁操作
- 调用MemcachedClient.get("lock1"),如果为空或者等于0,则表示没有其它server争用这把锁
- 调用MemcachedClient.put("lock1","1"),表示有人获取到锁并正在使用
- 使用完毕后,释放锁调用MemcachedClient.put("lock1","0")
- 要注意put的时候,对于memcache来说也是并发问题,需要进行同步控制
- 首先肯定要又一个独立的登陆认证服务器
- http协议是无状态的,要区分用户需要记录用户信息,最简单的方案就是cookie
- 问题是,浏览器是根据访问的域名,决定携带那个cookie,所谓单点要解决的一个关键问题是:含有登陆信息的cookie如何被不同域名所接受
- 做cookie同步;单点登陆后,需要携带cookie信息302到pass.xxx.com服务器,这个服务器就是验证cookie并同步cookie到这个域名,返回需要set-cookie,然后在302到目标页面
当我们的一个线程需要针对数据库做一系列操作时,每次都会去调用getConnection()方法获取数据库连接,然后执行完后再释放或归还数据库连接(SqlSessionTemplate就是这么做的),那么很明显,我们需要能够保证每次调用Dao层方法时都能动态的切换数据源,这就需要Spring的AOP:我们定义一个切面,当我们调用Dao层方法时,执行我们的逻辑来判断这次调用的数据源
HTTP中的GET,POST,PUT,DELETE就对应着对这个资源的查,改,增,删4个操作;其实get和post只是语义和用法上有区别,实质并没有什么区别。
forward是服务器行为,redirect是客户端行为
关于防止重复提交,常常会有这样的情况:由于网络延迟或其他因素,用户点击了多次提交按钮。如何防止这种重复提交呢?
- 前端控制。譬如设置标识位,点击后就不能再次点击,但是前端控制一般都是防君子不防小人
- 后端控制:
在返回给客户端页面时,增加一个随机token,放置在页面的隐藏域中
客户端提交数据时,同时将token提交上来;服务端在缓存中查询是否有token的信息,如果有,则表明是重复提交,如果没有则记录token信息,并提交数据
事务的特点?采用什么技术实现分布式事务,如何实现,讲讲原理,为什么需要两阶段提交,能解决哪些问题,哪些问题不能解决。spring 提供了#### 哪两种事物处理方式,采用了java的什么技术?-- 6 7
spring bean初始化的过程?static 代码块、构造函数、init方法、afterPropertiesSet顺序?单例?spring的单例和设计模式中单例模式区别?设计模式中的单例模式使用过程中的注意点?如何实现?(多线程、序列化) --6 7
主从数据库。
数据拆分方案: 单点问题:nginx单点,数据库写库也是单点
- 性能瓶颈
- 非高可用
单机mysql无法满足存储数据的容量要求或性能要求时,就要考虑进行数据库拆分;
- 垂直拆分:多个数据表拆分到多个数据库中。按照业务拆分;
- 水平拆分:一个表的数据拆分到多个表,或者多个数据库的多个表;主要问题是拆完后怎么合起来?
对于分布式事务是没有办法绝对完成事务一致性的问题,我们只能做到更好。
一些常见的解决方案:
- 2阶段提交:多一个事务管理器TM,用来管理多个系统的事务,第一个阶段:TM向各个系统(资源管理器RM)发送prepare消息,RM接受到消息后,执行本地事务,要么返回失败,要么执行成功停留在commit;第二个阶段是:commit/rollback,如果TM收到失败的消息或者超时,那么TM发送rollback消息,一起回滚;否则发送commit消息,一起提交。
可以发现,其实2阶段提交也是会发生问题:譬如发送commit后,有的RM没有接收到,就导致有些commit有些没有,数据不一致。
互联网业务发展过程中用户增长是永恒的主题(联想到自己负责的应用,感觉很悲凉,我负责的就不是)。我们做了一些功能的改进,如何判断我们的功能是否有用,最根本是通过用户是否增长,那么有哪些指标可以用来判断用户是否增长呢?
产品页面类型:
- 流量分发页面:首页,导航页。注重分发效率
- 流量承接页面:1、功能性页面。2、攻略性页面
关注指标类型分类:
- 规模类指标:反应页面的规模体量,pv uv DAU等
- 黏性指标:反应用户忠诚度:次日复访率、7日复访率、活跃率(DAU/MAU)
- 页面承接指标:反应用户在页面的行为:点击PV/点击UV、PV/UV点击率、人均点击次数、人均停留时长等