在40岁老架构师尼恩的(50+)读者社区中,经常有小伙伴,需要面试京东、阿里、 百度、头条、美团等大厂。
下面是一个小伙伴成功拿到通过了京东一次技术面试,最终,小伙伴通过后几面技术拷问、灵魂拷问,最终拿到offer。
从这些题目来看:京东的面试,偏重底层知识和原理,大家来看看吧。
现在把面试真题和参考答案收入咱们的宝典,大家看看,收个京东Offer需要学点啥?
当然对于中高级开发来说,这些面试题,也有参考意义。
这里把题目以及参考答案,收入咱们的《尼恩Java面试宝典》 V83版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请到公号【技术自由圈】取
IO 全程 Input/Output,即数据的读取(接收)或写入(发送)操作,针对不同的数据存储媒介,大致可以分为网络 IO 和磁盘 IO 两种。
而在 Linux 系统中,为了保证系统安全,操作系统将虚拟内存划分为内核空间和用户空间两部分。因此用户进程无法直接操作IO设备资源,需要通过系统调用完成对应的IO操作。
即此时一个完整的 IO 操作将经历一下两个阶段:用户空间 <-> 内核空间 <-> 设备空间
。
同步阻塞IO(Blocking IO) 指的是用户进程(或线程)主动发起,需要等待内核 IO 操作彻底完成后才返回到用户空间的 IO 操作。在 IO 操作过程中,发起 IO 请求的用户进程处于阻塞状态。
同步非阻塞IO(Non-Blocking IO,NIO) 指的是用户进程主动发起,不需要等待内核 IO 操作彻底完成就能立即返回用户空间的 IO 操作。在 IO 操作过程中,发起 IO 请求的用户进程处于非阻塞状态。
1)当数据 Ready 之后,用户线程仍然会进入阻塞状态,直到数据复制完成,并不是绝对的非阻塞。
2)NIO 实时性好,内核态数据没有 Ready 会立即返回,但频繁的轮询内核,会占用大量的 CPU 资源,降低效率。
IO 多路复用(IO Multiplexing) 实际上就解决了 NIO 中的频繁轮询 CPU 的问题,并且引入一种新的 select 系统调用。
复用 IO 的基本思路就是通过 slect 调用来监控多 fd(文件描述符),来达到不必为每个 fd 创建一个对应的监控线程的目的,从而减少线程资源创建的开销。一旦某个描述符就绪(一般是内核缓冲区可读/可写),内核就能够将文件描述符的就绪状态返回给用户进程(或者线程),用户空间可以根据文件描述符的就绪状态进行相应的 IO 系统调用。
IO 多路复用(IO Multiplexing)属于一种经典的 Reactor 模式实现,有时也称为异步阻塞 IO。
异步IO(Asynchronous IO,AIO) 指的是用户空间的线程变成被动接收者,而内核空间成为主动调用者。在异步 IO 模型中,当用户线程收到通知时,数据已经被内核读取完毕并放在用户缓冲区内,内核在 IO 完成后通知用户线程直接使用即可。而此处说的 AIO 通常是一种异步非阻塞 IO。
当进程发起一个 IO 操作,会向内核注册一个信号处理函数,然后进程返回不阻塞;当内核数据就绪时会发送一个信号给进程,进程便在信号处理函数中调用 IO 读取数据。
信号驱动 IO 不同于 AIO 的是依旧存在阻塞状态,即用户进程获取到数据就绪信号后阻塞进行 IO 操作。
我们在不同的地方见到过一致性的概念,总结大概分为以下几类:
1.最常见的定义是:事务的一致性是指系统从一个正确的状态,迁移到另一个正确的状态。
指的是事务前后的正确性是一致的。
Consistency ensures that a transaction can only bring the database from one valid state to another, maintaining database invariants: any data written to the database must be valid according to all defined rules, including constraints, cascades,triggers, and any combination thereof. This prevents database corruption by an illegal transaction, but does not guarantee that a transaction is correct.
2.“ensuring the consistency is the responsibility of user, not DBMS.”, "DBMS assumes that consistency holds for each transaction。
指的是对业务中和数据库中约束的检查,业务上的合理性,只靠AID手段不容易检查出逻辑性的问题来。比如转账操作中,账户金额不能为负数,这是业务逻辑上的要求,用一致性来保证。
3.This(Consistency)does not guarantee correctness of the transaction in all ways the application programmer might have wanted (that is the responsibility of application-level code) but merely that any programming errors cannot result in the violation of any defined database constraints.[1]
某些数据保存了多个副本,所有副本内容相同。
整个分布式系统在对外的反馈上,与一台单机完全一样,不会因为分布式导致对外行为的前后冲突。
一种哈希算法,指将存储节点和数据都映射到一个首尾相连的哈希环上,如果增加或者移除一个节点,仅影响该节点在哈希环上顺时针相邻的后继节点,其它数据也不会受到影响。
在移除或者添加一个服务器时,能够尽可能小地改变已存在的服务请求与处理请求服务器之间的映射关系。一致性哈希解决了简单哈希算法在分布式哈希表( Distributed Hash Table,DHT) 中存在的动态伸缩等问题。
对比非一致性哈希(普通的哈希),映射时如果节点发生变化,需要重新计算所有数据的映射值。
这三个都是分布式系统的一致性级别
线性一致性:强一致性,侧重于单个key的场景
外部一致性:事务在数据库内的执行序列不能违背外部观察到的顺序,更侧重于对比传统数据库系统的内部一致性。
最终一致性:弱一致性。
40岁老架构师尼恩提示:分布式事务是既是面试的绝对重点,也是面试的绝对难点,建议大家有一个深入和详细的掌握,具体的内容请参见《尼恩Java面试宝典-专题17:分布式事务面试题》PDF,该专题对分布式事务有一个系统化、体系化、全面化的介绍。
如果要把分布式事务写入简历,可以找尼恩指导
原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,这和前面两篇博客介绍事务的功能是一样的概念,因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响。
一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。
拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。
隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
即要达到这么一种效果:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。
关于事务的隔离性数据库提供了多种隔离级别,稍后会介绍到。
持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。
多个线程都开启事务操作数据库中的数据时,数据库系统要能进行隔离操作,以保证各个线程获取数据的准确性,在介绍数据库提供的各种隔离级别之前,我们先看看如果不考虑事务的隔离性,会发生的几种问题:
脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据。
当一个事务正在多次修改某个数据,而在这个事务中这多次的修改都还未提交,这时一个并发的事务来访问该数据,就会造成两个事务得到的数据不一致。例如:用户A向用户B转账100元,对应SQL命令如下
update account set money=money+100 where name=’B’; (此时A通知B)
update account set money=money - 100 where name=’A’;
当只执行第一条SQL时,A通知B查看账户,B发现确实钱已到账(此时即发生了脏读),而之后无论第二条SQL是否执行,只要该事务不提交,则所有操作都将回滚,那么当B以后再次查看账户时就会发现钱其实并没有转。
不可重复读是指在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。
例如事务T1在读取某一数据,而事务T2立马修改了这个数据并且提交事务给数据库,事务T1再次读取该数据就得到了不同的结果,发送了不可重复读。
不可重复读和脏读的区别是,脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。
在某些情况下,不可重复读并不是问题,比如我们多次查询某个数据当然以最后查询得到的结果为主。但在另一些情况下就有可能发生问题,例如对于同一个数据A和B依次查询就可能不同,A和B就可能打起来了……
幻读是事务非独立执行时发生的一种现象。例如事务T1对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是为“1”并且提交给数据库。而操作事务T1的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务T2中添加的,就好像产生幻觉一样,这就是发生了幻读。
幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数)。
现在来看看MySQL数据库为我们提供的四种隔离级别:
① Serializable (串行化):可避免脏读、不可重复读、幻读的发生。
② Repeatable read (可重复读):可避免脏读、不可重复读的发生。
③ Read committed (读已提交):可避免脏读的发生。
④ Read uncommitted (读未提交):最低级别,任何情况都无法保证。
以上四种隔离级别最高的是Serializable级别,最低的是Read uncommitted级别,当然级别越高,执行效率就越低。像Serializable这样的级别,就是以锁表的方式(类似于Java多线程中的锁)使得其他的线程只能在锁外等待,所以平时选用何种隔离级别应该根据实际情况。在MySQL数据库中默认的隔离级别为Repeatable read (可重复读)。
在MySQL数据库中,支持上面四种隔离级别,默认的为Repeatable read (可重复读);而在Oracle数据库中,只支持Serializable (串行化)级别和Read committed (读已提交)这两种级别,其中默认的为Read committed级别。
40岁老架构师尼恩提示:分布式事务是既是面试的绝对重点,也是面试的绝对难点,建议大家有一个深入和详细的掌握,具体的内容请参见《尼恩Java面试宝典-专题17:分布式事务面试题》PDF,该专题对分布式事务有一个系统化、体系化、全面化的介绍。
如果要把分布式事务写入简历,可以找尼恩指导。
在SQL标准中定义了四种隔离级别,包括了一些具体规则,用来限定事务内外的哪些改变是可见的,哪些是不可见的。低级别的隔离级一般支持更高的并发处理,并拥有更低的系统开销。
1)Read Uncommitted(读取未提交内容)
在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)。
2)Read Committed(读取提交内容)
这是大多数数据库系统的默认隔离级别(但不是mysql默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别也支持所谓的不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的commit,所以同一条select语句可能返回不同结果。
3)Repeatable Read(可重读)
这是mysql的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读 (Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题。
4)Serializable(可串行化)
这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争,因此使用该隔离级别会造成数据库性能的显著下降。
在数据库中,事务隔离级别确定了事务中读取和写入操作的执行顺序。下面是每个隔离级别是如何解决数据冲突的。
总之,隔离级别越高,数据的一致性越高,但是性能越差。在实际应用中,需要根据不同的业务需求选择合适的隔离级别。
在MySQL中,要使用next-key锁,可以使用以下语句示例:
SELECT * FROM table_name WHERE column_name = 'value' FOR UPDATE
在上述示例中,你需要将table_name
替换为你要查询的表名,column_name
替换为你要查询的列名,value
替换为你要匹配的值。
使用FOR UPDATE
语句可以确保在查询期间对所选行应用next-key锁,以防止其他事务对这些行进行修改。
请注意,next-key锁是在InnoDB存储引擎中实现的,默认情况下它是开启的。确保你的表使用了InnoDB引擎,以便使用next-key锁。
JDK8的内存模型
在Java中所有的垃圾收集问题几乎都是针对堆内存空间完成的,但是要想充分理解垃圾的收集流程,必须首先掌握Java堆内存的最初内存模型组成。如图1所示:
内存模型的变化
JDK1.8以前提共用永久代,而从JDK1.8后永久代被替换为元空间(MetaSpace)。在JDK1.8之前,HotSpot都在努力改变永久代的存储位置,例如,在JDK1.6时提供有永久代,到了JDK1.7时又将永久代的部分操作移交给了堆内存,而在JDK1.8时使用元空间代替了永久代。
JDK 1.8之前的内存模型如图2所示。
可以发现,在JDK1.8之前都会提供有永久代,此部分内存是不受GC控制的。在最初的设计中,都将方法区保存在了永久代,所以一旦方法执行中出现了内存不足的情况,将会抛出:“OutOfMemoryError:PermGen space”错误。同时Oracle也在考虑将HotSpot与JRockit(此虚拟机不存在永久代)两个虚拟机合二为一,所以此内存空间被元空间所替代。
在整个Java內存模型中,主要有3块内存区:年轻代(Young)、老年代(Tenured)、元空间(MetaSpace),同时还会有几块动态调整的内存伸缩区(当几个内存区空间不足时动态扩充)。而JVM的内存回收就是对这几块空间的回收处理操作,对于内存分配与GC的执行流程如图3所示。
上图中的垃圾回收主要是针对年轻代(Eden+Survivor)与老年代(Tenured)完成的。
具体流程如下:
可见,Java在每次创建对象时如果发现内存不足都会自动向其他区域延伸。为了提高性能,在实际应用中可能会开辟尽量大的内存空间,以实现更加合理的GC控制。
a.争抢锁,只有一个人能获得锁
b.获得锁,客户端出现问题,临时节点(session)
c.锁被释放,删除,如何通知其他客户端
c-1: 主动轮询,心跳:弊端:延迟,压力
c-2: watch: 解决延迟问题。 弊端:压力
c-3: sequence+watch:watch 前一个,最小的获得锁,一旦最小的释放了锁,成本:zk只需要给第二个发时间回调
a.使用setnx()方法,获取锁信息
b.设置过期时间,防止客户端down机,造成死锁
c.多线程(守护线程),监控锁,业务还未处理完,锁过期,自动延期
3.1 从获得锁的速度上,redis的速度优于zookeeper
3.2 从方案实现的角度,zookeeper实现相对redis简单,zookeeper只管获取锁和回调,redis还要增加线程对锁进行监控。
public class ZKUtils {
private static ZooKeeper zk;
private static String address = "192.168.7.230:2181,192.168.7.240:2180,192.168.7.71:2181/testDistributeLock";
private static DefaultWatch watch = new DefaultWatch();
private static CountDownLatch init = new CountDownLatch(1);
public static ZooKeeper getZK(){
try {
zk = new ZooKeeper(address,1000,watch);
watch.setCc(init);
init.await();
} catch (Exception e) {
e.printStackTrace();
}
return zk;
}
}
public class WatchCallBack implements Watcher, AsyncCallback.StringCallback ,AsyncCallback.Children2Callback ,AsyncCallback.StatCallback {
ZooKeeper zk ;
String threadName;
CountDownLatch cc = new CountDownLatch(1);
String pathName;
public String getPathName() {
return pathName;
}
public void setPathName(String pathName) {
this.pathName = pathName;
}
public String getThreadName() {
return threadName;
}
public void setThreadName(String threadName) {
this.threadName = threadName;
}
public ZooKeeper getZk() {
return zk;
}
public void setZk(ZooKeeper zk) {
this.zk = zk;
}
public void tryLock(){
try {
System.out.println(threadName + " create....");
// if(zk.getData("/"))
zk.create("/lock",threadName.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL,this,"abc");
cc.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void unLock(){
try {
zk.delete(pathName,-1);
System.out.println(threadName + " over work....");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
@Override
public void process(WatchedEvent event) {
//如果第一个哥们,那个锁释放了,其实只有第二个收到了回调事件!!
//如果,不是第一个哥们,某一个,挂了,也能造成他后边的收到这个通知,从而让他后边那个跟去watch挂掉这个哥们前边的。。。
switch (event.getType()) {
case None:
break;
case NodeCreated:
break;
case NodeDeleted:
zk.getChildren("/",false,this ,"sdf");
break;
case NodeDataChanged:
break;
case NodeChildrenChanged:
break;
}
}
@Override
public void processResult(int rc, String path, Object ctx, String name) {
if(name != null ){
System.out.println(threadName +" create node : " + name );
pathName = name ;
zk.getChildren("/",false,this ,"sdf");
}
}
//getChildren call back
@Override
public void processResult(int rc, String path, Object ctx, List<String> children, Stat stat) {
//一定能看到自己前边的。。
// System.out.println(threadName+"look locks.....");
// for (String child : children) {
// System.out.println(child);
// }
Collections.sort(children);
int i = children.indexOf(pathName.substring(1));
//是不是第一个
if(i == 0){
//yes
System.out.println(threadName +" i am first....");
try {
zk.setData("/",threadName.getBytes(),-1);
cc.countDown();
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
//no
zk.exists("/"+children.get(i-1),this,this,"sdf");
}
}
@Override
public void processResult(int rc, String path, Object ctx, Stat stat) {
//偷懒
}
}
public class TestDistributeLock {
ZooKeeper zk ;
@Before
public void conn (){
zk = ZKUtils.getZK();
}
@After
public void close (){
try {
zk.close();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Test
public void lock(){
for (int i = 0; i < 10; i++) {
new Thread(){
@Override
public void run() {
WatchCallBack watchCallBack = new WatchCallBack();
watchCallBack.setZk(zk);
String threadName = Thread.currentThread().getName();
watchCallBack.setThreadName(threadName);
//每一个线程:
//抢锁
watchCallBack.tryLock();
//干活
System.out.println(threadName+" working...");
// try {
// Thread.sleep(1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
//释放锁
watchCallBack.unLock();
}
}.start();
}
while(true){
}
}
}
40岁老架构师尼恩提示:分布式锁是既是面试的绝对重点,也是面试的绝对难点,建议大家有一个深入和详细的掌握,具体的内容请参见《尼恩Java面试宝典-专题15:分布式锁面试题》PDF,该专题对分布式锁有一个系统化、体系化、全面化的介绍。
如果要把分布式锁写入简历,可以找尼恩指导。
在一个进程中,多线程去竞争资源时,可以通过synchronized或者Lock锁进行同步执行,保证多线程情况下,资源的调用是安全的,那么多进程中或者多节点机器中如何去保证对相同资源的调用是安全的,此时就引出了分布式锁解决方案。分布式锁就是用来保证在分布式系统中对共享资源调用时保证其一致性。
在实现分布式锁过程中需要考虑如下几点:
需要实现分布式锁,就得借助于第三方软件,比如数据库、Redis、ZooKeeper等,本文就分别从这三种软件着手,来看看是怎样的实现过程。
1、基于Mysql的分布式锁
数据库中我们可以通过主键唯一性特点来进行加锁和解锁过程,主键唯一性就表示当前节点中只能有一个节点创建成功,其余的都是得到创建异常,那么创建成功的节点就表示获得了锁,其余节点只能等待获得锁。此时保证了第一步和第二步,那么怎么实现锁的可重入性呢?此时我们可以增加一列来存储当前节点的当前线程信息(可以用节点的应用名称+ip+线程名),重入次数加上一个计数器,因为数据库没有一个过期时间的设置,所以需要开启一个定时任务去判断当前锁是不是已经过期。所以我们还需要一个更新时间。
表如下:
DROP TABLE IF EXISTS `locks`;
CREATE TABLE `locks` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`lock_key` varchar(255) NOT NULL COMMENT '需要锁定的资源',
`repeat_key` varchar(255) NOT NULL COMMENT '可重入标识',
`repeat_time` int(11) DEFAULT NULL COMMENT '重入次数',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `lock_key` (`lock_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
代码实现如下,
import javax.sql.DataSource;
import java.io.Closeable;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.concurrent.TimeUnit;
/**
* Created by Administrator on 2022/1/13.
*/
public class MyLockFromMysql implements MyLock{
private DataSource dataSource;
public MyLockFromMysql(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public void lock(String key) {
if (!tryLock(key)){
throw new RuntimeException();
}
}
@Override
public void unlock(String key) {
String repeatKey= getRepeatKey();
if (hasRepeatLock(key,repeatKey)){
updateLock(key,repeatKey,-1);
}else if (!deleteLock(key,repeatKey)){
throw new RuntimeException();
}
}
@Override
public boolean tryLock(String key) {
String repeatKey= getRepeatKey();
if (hasLock(key,repeatKey)){
return updateLock(key,repeatKey,1);
}
for (;;){
if (addLock(key,repeatKey)){
break;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
return true;
}
private String getRepeatKey() {
String host= "";
try {
host = InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException e) {
e.printStackTrace();
}
return host+Thread.currentThread().getName();//采用节点ip+线程名来做重入判断
}
/**
* 根据key和repeatKey判断当前是否已经获得锁
* @param key 锁定的资源
* @param repeatKey 可重入标识
* @return
*/
private boolean hasLock(String key,String repeatKey){
Connection connection=null;
PreparedStatement statement=null;
ResultSet rs=null;
try {
connection=dataSource.getConnection();
statement=connection.prepareStatement("SELECT repeat_time FROM locks WHERE lock_key=? AND repeat_key=?");
statement.setString(1,key);
statement.setString(2,repeatKey);
rs=statement.executeQuery();
return rs.next();
} catch (Exception e) {
return false;
}finally {
close(rs);
close(statement);
close(connection);
}
}
/**
* 判断当前是否有重入锁
* @param key
* @param repeatKey
* @return
*/
private boolean hasRepeatLock(String key,String repeatKey){
Connection connection=null;
PreparedStatement statement=null;
ResultSet rs=null;
try {
connection=dataSource.getConnection();
statement=connection.prepareStatement("SELECT repeat_time FROM locks WHERE lock_key=? AND repeat_key=? AND repeat_time>1 ");
statement.setString(1,key);
statement.setString(2,repeatKey);
rs=statement.executeQuery();
return rs.next();
} catch (Exception e) {
return false;
}finally {
close(rs);
close(statement);
close(connection);
}
}
/**
* 如果当前线程没有获得锁直接添加一条数据去竞争锁
* @param key 锁定的资源
* @param repeatKey 可重入标识
* @return
*/
private boolean addLock(String key,String repeatKey){
Connection connection=null;
PreparedStatement statement=null;
try {
connection=dataSource.getConnection();
statement=connection.prepareStatement("INSERT INTO locks (lock_key,repeat_key,repeat_time,update_time) VALUES (?,?,1,now())");
statement.setString(1,key);
statement.setString(2,repeatKey);
return statement.executeUpdate()>0;
} catch (Exception e) {
return false;
}finally {
close(statement);
close(connection);
}
}
private boolean updateLock(String key,String repeatKey,int upDown){
Connection connection=null;
PreparedStatement statement=null;
try {
connection=dataSource.getConnection();
statement=connection.prepareStatement("UPDATE locks set repeat_time=repeat_time+?,update_time=now() WHERE lock_key=? AND repeat_key=?");
statement.setInt(1,upDown);
statement.setString(2,key);
statement.setString(3,repeatKey);
return statement.executeUpdate()>0;
} catch (Exception e) {
return false;
}finally {
close(statement);
close(connection);
}
}
private boolean deleteLock(String key,String repeatKey){
Connection connection=null;
PreparedStatement statement=null;
try {
connection=dataSource.getConnection();
statement=connection.prepareStatement("DELETE FROM locks WHERE lock_key=?");
statement.setString(1,key);
return statement.executeUpdate()>0;
} catch (Exception e) {
e.printStackTrace();
return false;
}finally {
close(statement);
close(connection);
}
}
private void close(AutoCloseable close){
if (close!=null){
try {
close.close();
} catch (Exception e) {
}
}
}
}
以上代码只是简单的去实现了用mysql来做分布式锁的过程(性能不是太友好,也会存在很多问题),逻辑就是通过设定需要锁定的资源为主键,加锁的时候往数据库中添加数据,此时只会有添加成功的节点会获得锁,当调用完成之后删掉数据,也就是表明释放锁。针对重入锁,采用了一个重入key和重入次数两个字段来实现重入,如果当前节点已经获得锁,需要再次获得锁的时候,直接对次数+1操作,释放锁的时候做-1操作。当为1的时候再释放就需要删除节点。
其中的缺点也可想而知:
2、基于Redis实现分布式锁
Redis中有一个setnx命令,这个命令key不存在添加成功放回1,存在返回0,所以采用redis实现分布式锁就是基于这个命令来实现,也就是只有在创建成功放回1的节点就是获得锁的节点,释放锁的时候,删除该节点即可,redis中可以采用过期时间策略来保证,客户端释放锁失败后,也能在规定时间内释放锁,锁的可重入性在于value的设计过程,value我们可以保存当前节点的唯一标识和可重入次数。
简单代码如下所示:
import com.alibaba.fastjson.JSONObject;
import redis.clients.jedis.Jedis;
import java.net.InetAddress;
import java.net.UnknownHostException;
/**
* Created by Administrator on 2022/1/14.
*/
public class MyLockForRedis implements MyLock {
private final long TIME_WAIT=50L;
@Override
public void lock(String key) {
tryLock(key);
}
@Override
public void unlock(String key) {
Jedis redis=new Jedis("127.0.0.1",6379);
String json=redis.get(key);
LockValue lockValue= JSONObject.parseObject(json,LockValue.class);
if (getRepeatKeyV().equals(lockValue.getRepeatKey())){
if (lockValue.getTime()>1){
lockValue.setTime(lockValue.getTime()-1);
redis.set(key,JSONObject.toJSONString(lockValue));
}else {
redis.del(key);
}
}
redis.close();
}
@Override
public boolean tryLock(String key) {
Jedis redis=new Jedis("127.0.0.1",6379);
try {
for (;;){
if ("OK".equals(redis.set(key,JSONObject.toJSONString(new LockValue()),"NX","EX",200000))){
return true;
}else {
String json=redis.get(key);
LockValue lockValue= JSONObject.parseObject(json,LockValue.class);
if (lockValue!=null&&getRepeatKeyV().equals(lockValue.getRepeatKey())){
lockValue.setTime(lockValue.getTime()+1);
redis.set(key,JSONObject.toJSONString(lockValue));
return true;
}
}
try {
Thread.sleep(TIME_WAIT);
} catch (InterruptedException e) {
}
}
}finally {
redis.close();
}
}
private static class LockValue{
private int time=1;
private String repeatKey=getRepeatKeyV();
public int getTime() {
return time;
}
public void setTime(int time) {
this.time = time;
}
public String getRepeatKey() {
return repeatKey;
}
public void setRepeatKey(String repeatKey) {
this.repeatKey = repeatKey;
}
@Override
public String toString() {
return "{time=" + time +", repeatKey=\"" + repeatKey+ "\"}";
}
}
private static String getRepeatKeyV() {
String host= "";
try {
host = InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException e) {
e.printStackTrace();
}
return host+Thread.currentThread().getName();//采用节点ip+线程名来做重入判断
}
}
以上只是简单的去实现了基于Redis的分布式锁功能(肯定存在很多问题),其中value保存的是json格式的数据来实现锁的可重入性,这里就会存在一个性能的开销(不断的解析json格式),这里存在一个问题是过期时间的设置,如果我们设置的是10,如果某节点获得锁之后,执行的很慢超过了十秒,此时别的节点就可以获得锁,一种解决方案就是在获得锁的节点中开启一个线程去更新锁的过期时间,当节点执行完成之后关掉这个线程,如果获取锁的节点宕机之后,也可以通过过期时间来解决。
3、基于Zookeeper实现分布式锁
ZooKeeper中实现分布式锁方式有两种方案:
基于这种方式实现的分布式锁,可以查看Curator客户端连接中的InterProcessMutex实现,它的实现逻辑就是通过创建临时顺序子节点,具体实现过程如下:
40岁老架构师尼恩提示:分布式锁是既是面试的绝对重点,也是面试的绝对难点,建议大家有一个深入和详细的掌握,具体的内容请参见《尼恩Java面试宝典-专题15:分布式锁面试题》PDF,该专题对分布式锁有一个系统化、体系化、全面化的介绍。
如果要把分布式锁写入简历,可以找尼恩指导。
下面的图片显示了一个请求在hystrix中的流程图。
1.构造一个HystrixCommand或者HystrixObservableCommand对象
第一步是创建一个HystrixCommand或者HystrixObservableCommand对象来执行依赖请求。创建时需要传递相应的参数。
如果请求只返回一个单一值,使用HystrixCommand。
HystrixCommand command = new HystrixCommand(arg1, arg2);
如果希望返回一个Observable来监听多个值,使用HystrixObservableCommand。
HystrixObservableCommand command = new HystrixObservableCommand(arg1, arg2);
2.执行命令
有四种方法来执行命令(前面两种只对HystrixCommand有用,HystrixObservableCommand没有相应的方法)。
K value = command.execute();
Future<K> fValue = command.queue();
Observable<K> ohValue = command.observe(); //hot observable
Observable<K> ocValue = command.toObservable(); //cold observab
同步调用execute本质是调用了queue().get().queue() ,而queue本质上调用了toObservable().toBlocking().toFuture().本质上都是通过rxjava的Observable实现。
3.是否使用缓存
如果开启缓存,请求首先会返回缓存中的结果。
4.是否开启熔断
当运行hystrix命令时,会判断是否熔断,如果已经熔断,hystrix将不会执行命令,而是直接执行fallback。等熔断关闭了,在执行命令。
5.线程/队列/信号量是否已满
如果线程或队列(非线程池模式下是信号量)已满,将不会执行命令,而是直接执行fallback。
6.HystrixObservableCommand.construct() or HystrixCommand.run()
hystrix通过一下两种方式来调用依赖:
HystrixObservableCommand.construct() :返回一个Observable,发射多个值。
HystrixCommand.run():返回一个单一值,或抛异常。
如果HystrixCommand.run()或HystrixObservableCommand.construct() 发送超时,则执行的相应线程将会抛出TimeoutException的异常。然后执行fallback流程。并且丢弃run或construst的返回值。
注意,hystrix没有办法强制停止线程执行,hysrix能做的最好方式是抛出InterruptedException。如果hystrix执行的方法没有相应InterruptedException,那么它会继续执行,但是用户的已经收到TimeoutException异常。大多是的http client不回响应InterruptedException,确保正确配置了链接的timeout时间。
如果执行方法成功,hystrix会记录日志、上报metrics,然后返回执行结果。
7.熔断器计算
hystrix在成功、失败、拒绝、timeout时会上报到熔断器模块,熔断器会计算当前的熔断状态。熔断器使用一个状态来表示当前是否被熔断,一旦熔断所有的请求将不回执行命令直到熔断恢复。
8.执行fallback
当命令执行失败时,hystrix会执行fallback:当run或construct方法抛出异常;当熔断器被熔断;当线程池/队列/信号量使用完;当timeout。
通过fallback可以优雅降级,通过静态逻辑返回一个结果。如果你想要在fallback中执行依赖调用,那么必须把这个依赖封装成一个HystrixCommand或者HystrixObservableCommand。HystrixCommand中通过fallback方法来返回一个单一值,HystrixObservableCommand通过resumeWithFallback来返回一个 Observable来返回一个或多个值。hystrix将会把返回值返回给调用方。
如果不实现fallback方法,或者执行fallback方法抛出异常,hystrix仍然会返回一个Observable,但该Observable不会发射数据,而是直接执行error。通过onerror通知,告诉调用方失败结果。fallback不存在或者fallback执行失败,不同的方法将会有不同的表现:
execute,抛出一个异常。
queue,返回一个future,但是调用get方法时,将会抛出异常。
observe,返回一个Observable,一旦被监听会立即调用监听者的onError方法。
toObservable,返回一个Observable,一旦被监听会立即调用监听者的onError方法。
9.返回成功结果
如果hystrix命令执行成功,它将会返回一个Observable,根据调用的方法,Observable将会被转换成响应结果:
下面的图表展示了HystrixCommand和HystrixObservableCommand与HystrixCircuitBreaker交互的流程,以及HystrixCircuitBreaker的原理。
熔断器开关条件:
hystrix使用了舱壁隔离模式来隔离和限制各个请求。
线程和线程池
第三方依赖在独立的线程池中执行,这样可以隔离调用线程(tomcat线程池)实现异步调用。
hystrix为每个依赖服务调用使用独立池。
在依赖调用能够快速失败或者可以一直运行良好的情况下,也可以不使用线程池来执行调用。
hystrix选择使用线程池有一下原因:
HystrixCommand和HystrixObservableCommand实现了缓存机制,通过指定一个cache key,它们能够在一个请求范围内对运行结果进行缓存以便下次使用。下面是在一个请求中两个线程执行同一个请求的流程:
使用缓存的好处
总之,通过线程池来隔离依赖服务可以很优雅的隔离那些经常发生变化的依赖服务从而保护整个系统的运行。
线程池的缺点
线程池的主要缺点就是增加了额外的计算资源,每一个命令的执行都需要系统进行调度。netflix基于线程池不会耗费大量计算资源而决定使用这样的设计。
线程池花费
hystrix计算了通过线程池执行construct和run的延时。Netflix API 每天使用线程池方式处理上百亿的请求,每一个API都有40多个线程池,每个线程池中有5~20个线程。线图显示了一个QPS为60的HystrixCommand在线程池模式下的执行性能
平均请求,线程池几乎没有什么花费。
90th%,线程池花费3ms
99th%,线程池花费达到了9ms。但是线程池的增长远远小于整个请求的延时增长。超过90th%的花费对于大多数的Netflix使用场景来说是可接受的。
信号量
也可以使用信号量来限制每个依赖的并发数量。他可以对依赖服务降级,但不能监听timeout,如果我们对依赖服务的调用确认不会出现timeout情况,我们也可以使用这中方式。
HystrixCommand和HystrixObservableCommand在下面两个地方支持使用信号量。
可以通过动态的配置来设置并发数。尽可能设置合适的并发数,不可设置过大的并发数,这样将导致无法起保护作用。
通过使用HystrixCollapser可以实现合并多个请求批量执行。下面的图标显示了使用请求合并和不是请求合并,他们的线程迟和连接情况:
使用请求合并可以减少线程数和并发连接数,并且不需要使用这额外的工作。请求合并有两种作用域,全局作用域会合并全局内的同一个HystrixCommand请求,请求作用域只会合并同一个请求内的同一个HystrixCommand请求。但是请求合并会增加请求的延时。
一个请求对同一个Hystrix Command调用可以避免重复执行。
对于一些延时比较低的,不需要检测timeout的依赖,我们也可以使用信号量控制方式来做隔离,减少额外的花费。
这个功能对于多人协作开发的大型的项目非常有用。据一个例子,在一个请求中,多个地方需要使用用户的Account对象。
Account account = new UserGetAccount(accountId).execute();
//or
Observable<Account> accountObservable = new UserGetAccount(accountId).observe();
Hystrix将会执行run方法一次,两个线程都会接收到各自内容相同的Account对象。当执行第一次run方法后,结果将会被缓存起来,当在同一个请求执行同一个命令时,会直接使用缓存值。
减少线程重复执行。
因为缓存是在执行run或者construct方法前进行判断的,所以可以减少run和construct的调用。如果Hystrix没有实现缓存功能,那么每个调用都需要执行construct或者run方法。
NIO即New IO,这个库是在JDK1.4中才引入的。NIO和IO有相同的作用和目的,但实现方式不同,NIO主要用到的是块,所以NIO的效率要比IO高很多。在Java API中提供了两套NIO,一套是针对标准输入输出NIO,另一套就是网络编程NIO。
NIO和IO的主要区别,下表总结了Java IO和NIO之间的主要区别:
类型 | IO | NIO |
---|---|---|
流类型 | 面向流 | 面向缓冲 |
是否阻塞 | 阻塞IO | 非阻塞IO |
有无选择器 | 无 | 选择器 |
真正的了解NIO一定要看Netty,Netty是NIO里用的最好的框架。
Java IO和NIO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,因为它们没有被缓存在任何地方,所以,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。
Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write() 时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道channel。
Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。
NIO是为弥补传统IO的不足而诞生的,但是尺有所短寸有所长,NIO也有缺点,因为NIO是面向缓冲区的操作,每一次的数据处理都是对缓冲区进行的,那么就会有一个问题,在数据处理之前必须要判断缓冲区的数据是否完整或者已经读取完毕,如果没有,假设数据只读取了一部分,那么对不完整的数据处理没有任何意义。所以每次数据处理之前都要检测缓冲区数据。
如果需要管理同时打开的成千上万个连接,这些连接每次只是发送少量的数据,例如聊天服务器,这时候用NIO处理数据可能是个很好的选择。
而如果只有少量的连接,而这些连接每次要发送大量的数据,这时候传统的IO更合适。使用哪种处理数据,需要在数据的响应等待时间和检查缓冲区数据的时间上作比较来权衡选择。
Java NIO的三个核心基础组件:Channels、Buffers、Selectors。其余的诸如Pipe,FileLcok都是在使用以上三个核心组件时帮助更好使用的工具类。
NIO(New IO)和AIO(Asynchronous I/O)都是基于内存缓存的通道与传输机制,可以直接读写本地文件和网络通道,不需要经过文件系统和网络协议栈。两者的主要区别如下:
NIO基于通道和传输机制,IO基于文件和网络协议栈。在NIO中,通道可以是TCP通道、UDP通道等,而IO只能是TCP通道。
NIO对内存进行了缓存和重用,使得NIO程序可以高效地读写内存中的数据。IO没有使用内存缓存机制,每次读写都需要使用文件和网络协议栈。
NIO采用了事件驱动模型,程序可以根据IO事件进行读写操作。IO则采用了同步阻塞模型,需要等待操作完成才能返回结果。
NIO适用于需要高并发、高吞吐量的场景,例如Web服务器、IM应用等。IO适用于需要低延迟、高可靠性的场景,例如数据库、文件系统等。
因此,虽然NIO和AIO都是基于内存缓存的通道与传输机制,但是两者的数据通信方式、对内存的使用、事件驱动模型和应用场景有很大的不同。对于需要高并发和高吞吐量的应用,使用NIO是一个不错的选择;而对于需要低延迟和高可靠性的应用,则可以使用AIO。
AOP(Aspect Oriented Programming)面向切面编程,通过预编译方式和运行期动态代理实现程序功能的横向多模块统一控制的一种技术。通俗点,就是在不改变系统原本业务功能的前提下,对系统的功能进行横向扩展。
创建UserService接口,包含4个方法,并创建UserServiceImpl类实现该接口。
public interface UserService {
public void add();
public void delete();
public void update();
public void select();
}
123456
public class UserServiceImpl implements UserService {
public void add() {
System.out.println("增加了一个用户");
}
public void delete() {
System.out.println("删除了一个用户");
}
public void select() {
System.out.println("查询了一个用户");
}
public void update() {
System.out.println("修改了一个用户");
}
}
创建Log类,实现MethodBeforeAdvice接口,在这里可以进行一些自定义操作,比如向控制台输出一句话,还可以利用反射机制获得该方法的一些信息,必须方法名等,这是另外两大方法所不具备的优点,该切面方法会放在切入点方法调用之前调用。
import org.springframework.aop.MethodBeforeAdvice;
import java.lang.reflect.Method;
public class Log implements MethodBeforeAdvice {
//method:要执行的目标对象的方法
//args:参数
//target:目标对象
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println(target.getClass().getName()+"的"+method.getName()+"被执行了");
}
}
创建AfterLog类,实现AfterReturningAdvice接口,在这里可以进行一些自定义操作,比如向控制台输出一句话,还可以利用反射机制获得该方法的一些信息,必须方法名等,这是另外两大方法所不具备的优点,该切面方法会放在切入点方法调用之前调用。
import org.springframework.aop.AfterReturningAdvice;
import java.lang.reflect.Method;
public class AfterLog implements AfterReturningAdvice {
//returnValue:返回值
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.println("执行了"+method.getName()+"方法,返回结果为:"+returnValue);
}
}
在xml配置文件中的标签中应加上对应的aop地址,创建相应bean,对aop进行配置,并导入aop的约束。
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="userService" class="com.kuang.service.UserServiceImpl"/>
<bean id="log" class="com.kuang.log.Log"/>
<bean id="afterLog" class="com.kuang.log.AfterLog"/>
<aop:config>
<aop:pointcut id="poinicut" expression="execution(* com.kuang.service.UserServiceImpl.*(..))"/>
<aop:advisor advice-ref="log" pointcut-ref="poinicut"/>
<aop:advisor advice-ref="afterLog" pointcut-ref="poinicut"/>
aop:config>
com.kuang.service.UserServiceImpl.(…)指切入点为UserServiceImpl类中的所有方法。
运行结果:
com.kuang.service.UserServiceImpl的add被执行了
增加了一个用户
执行了add方法,返回结果为:null
创建自定义切面类DiyPointCut,里面包含一些自定义的切面方法。
public class DiyPointCut {
public void before(){
System.out.println("===========方法执行前==========");
}
public void after(){
System.out.println("===========方法执行后==========");
}
}
在applicationContext.xml文件中的aop配置为:
<bean id="diy" class="com.kuang.diy.DiyPointCut"/>
<aop:config>
<aop:aspect ref="diy">
<aop:pointcut id="point" expression="execution(* com.kuang.service.UserServiceImpl.*(..))"/>
<aop:before method="before" pointcut-ref="point"/>
<aop:after method="after" pointcut-ref="point"/>
aop:aspect>
aop:config>
运行结果
=========== 方法执行前 ==========
增加了一个用户
=========== 方法执行后==========
建立一个切面方法类(AnnotationPointCut),里面包含一些切面方法,比如before、after、aroud等。
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
//方式三:使用注解方式实现AOP
@Aspect//标注这个类是一个切面
public class AnnotationPointCut {
@Before("execution(* com.kuang.service.UserServiceImpl.*(..))")
public void before(){
System.out.println("==========方法执行前==========");
}
@After("execution(* com.kuang.service.UserServiceImpl.*(..))")
public void after(){
System.out.println("==========方法执行后==========");
}
@Around("execution(* com.kuang.service.UserServiceImpl.*(..))")
public void around(ProceedingJoinPoint jp) throws Throwable{
System.out.println("环绕前");
Object proceed=jp.proceed();
System.out.println("环绕后");
}
}
在applicationContext.xml配置文件中对aop进行配置
<bean id="annotationPointCut" class="com.kuang.diy.AnnotationPointCut"/>
<aop:aspectj-autoproxy/>
运行结果
环绕前
========== 方法执行前==========
增加了一个用户
环绕后
========== 方法执行后==========
这里注意aroud与before、after方法的执行顺序:
环绕前—>方法执行前—>增加了一个用户—>环绕后—>方法执行后
动态代理是一种在运行时创建代理对象的方式,它可以在不修改原始类的情况下,为其添加额外的功能。在Java中,有两种常见的动态代理实现方式:基于接口的动态代理和基于类的动态代理。
java.lang.reflect.Proxy
类实现的。该类提供了一个newProxyInstance()
方法,通过传入目标类的接口、一个InvocationHandler
对象和类加载器来创建代理对象。InvocationHandler
对象的invoke()
方法中。在invoke()
方法中,可以执行一些前置或后置操作,并最终调用目标对象的方法。区别:
总体而言,基于接口的动态代理更加灵活,并且是Java官方支持的方式;而基于类的动态代理在某些场景下更加方便,尤其是对于没有实现接口的类。
索引在以下场景下可能会失效:
WHERE UPPER(column_name) = 'VALUE'
,索引可能无法起作用,因为函数或表达式的结果无法直接匹配索引中的值。WHERE CAST(column_name AS VARCHAR) = 'value'
,索引可能无法起作用,因为类型转换后的值无法直接匹配索引中的数据类型。LIKE
)进行搜索时,如果搜索模式以通配符开头(例如LIKE '%value'
),则索引可能无法起作用,因为通配符开头的模式无法利用索引的有序性。BETWEEN
、>、<
等),索引可能会失效,因为范围查询需要扫描多个索引节点,而不是单个等值匹配。要确保索引的有效使用,需要根据具体的查询场景和数据特点来设计和优化索引。
一种简单的方法是通过命令 “uptime” 来查看系统负载。这个命令会输出系统启动以来的总时间和系统当前的负载状态。
例如,如果系统负载较高,你可以看到类似下面的输出:
load average: 1.24 1.24 1.24
这个输出表示在过去1分钟、5分钟和15分钟内,系统的负载状态分别为1.24、1.24和1.24。
另外,一些图形化界面的系统管理工具也可以提供系统负载的图表,让你更直观地了解系统的负载状态。例如,在Linux系统中,你可以使用 “top” 命令来查看系统负载,如下所示:
top
这个命令会以字符界面的形式显示系统当前的进程和系统资源的使用情况,包括CPU、内存、磁盘等。你可以根据需要使用 “Ctrl+C” 退出命令。
分布式ID生成方案是在分布式系统中生成唯一ID的一种解决方案,常见的分布式ID生成方案包括:
雪花算法是Twitter开源的一种分布式ID生成算法,它是基于Snowflake算法实现的。雪花算法使用了一个64位的整数,其中包含了时间戳、机器ID、数据中心ID和序列号。具体来说:
通过雪花算法生成的ID既能保证在分布式系统中的唯一性,又能保证ID的有序性(按照时间戳递增)。雪花算法非常适合高并发环境下的分布式ID生成需求。
40岁老架构师尼恩提示:分布式id是既是面试的绝对重点,也是面试的绝对难点,建议大家有一个深入和详细的掌握,具体的内容请参见尼恩的《10Wqps推送中台实操》,该专题对分布式id有一个系统化、体系化、全面化的介绍。
如果要把推送中台写入简历,可以找尼恩指导。
在 Linux 中,可以使用 du
命令来查找磁盘上最大的文件。具体命令如下:
du -sh * | sort -rh | head -n 1
这个命令的含义是:
du
命令用于显示指定目录或文件的磁盘使用情况;-s
选项表示只显示指定目录或文件的总大小;-h
选项表示以人类可读的方式显示大小(例如,将字节转换为 KB、MB、GB 等);*
表示查找当前目录下的所有文件和目录;|
符号用于将前面的命令输出作为后面命令的输入;sort
命令用于对输入进行排序;-r
选项表示按照降序排序;-h
选项表示按照人类可读的方式排序;head
命令用于显示前几行结果,这里设置为只显示一行结果;-n n
选项表示显示前 n 行结果。在 Linux 中,可以使用 cat
、less
、tail
、grep
等命令来查看系统日志文件。
以下是一些常用的命令:
cat /var/log/messages
:显示系统消息日志文件的内容;less /var/log/messages
:分页显示系统消息日志文件的内容;tail -f /var/log/messages
:实时显示系统消息日志文件的最后几行内容;grep "error" /var/log/messages
:查找包含 “error” 字符串的系统消息日志文件的内容;sudo journalctl -u sshd.service
:查看 SSHD 服务的日志文件。40岁老架构师尼恩提示:Linux是既是面试的绝对重点,也是面试的绝对难点,建议大家有一个深入和详细的掌握,具体的内容请参见《尼恩Java面试宝典-专题22:Linux面试题》PDF,该专题对Linux有一个系统化、体系化、全面化的介绍。
如果要把Linux相关实战写入简历,可以找尼恩指导
事务隔离级别是数据库管理系统中用来控制并发访问数据的一种机制。常见的事务隔离级别包括读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable)。
在可重复读隔离级别下,事务在执行期间看到的数据是一致的,即使其他事务对数据进行了修改。这是通过多版本并发控制(MVCC)实现的。下面是可重复读实现原理的简要说明:
事务开始时,会为每个读取的数据行创建一个快照(Snapshot),该快照会记录当前数据行的状态。
在事务执行期间,其他事务对数据行进行修改不会影响当前事务的快照数据。
当前事务读取数据时,会使用快照数据而不是实时数据。
当前事务对数据进行修改时,会在修改的数据行上创建一个新的版本,并更新快照数据。
如果其他事务在当前事务执行期间对数据行进行修改,并且修改的数据行版本晚于当前事务的快照版本,则当前事务会回滚并重新执行。
通过使用快照和版本控制,可重复读隔离级别可以保证事务在执行期间看到一致的数据。
需要注意的是,不同的数据库管理系统可能会有不同的实现方式,但基本原理是相似的。同时,可重复读隔离级别也可能导致幻读问题,即在同一事务中多次执行相同的查询,但结果集不一致。为了解决幻读问题,可以使用更高级别的隔离级别,如串行化。
在操作系统中,进程间通信(Inter-Process Communication,IPC)是指不同进程之间进行数据交换和共享资源的过程。常见的进程间通信方式包括以下几种:
操作系统中常用的数据结构包括:
除了以上常见的数据结构,操作系统中还使用了其他一些特殊的数据结构,如散列表、红黑树、图等。这些数据结构在操作系统的实现中发挥着重要的作用。
Linux是一种操作系统内核,它是由一些志愿者开发的,具有开放源代码的特点。Linux系统在全球范围内被广泛应用于各种不同的领域,例如服务器、工作站、嵌入式系统等。
Linux系统的优势包括:
应用场景:
注意事项:
TCP:
传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793 定义。
UDP:
Internet 协议集支持一个无连接的传输协议,该协议称为用户数据报协议(UDP,User Datagram Protocol)。UDP 为应用程序提供了一种无需建立连接就可以发送封装的 IP 数据包的方法。RFC 768 描述了 UDP。
TCP与UDP区别总结:
1)TCP三次握手
三次握手是TCP用来确保连接可靠建立的方式:
在三次握手之后,A和B都能确定这么一件事: 双方的通信可以流畅的进行。 这样,双方就可以开始进行正常的对话了。
2)TCP四次挥手
四次挥手是TCP用来确保连接可靠关闭的方式:
在四次挥手之后,A和B都能确定这么一件事: 双方的通信可以正常关闭。 这样,双方就可以确定对方已经完全知晓自己确认要关闭连接。
1)UDP 使用场景:
因此UDP不提供复杂的控制机制,利用IP提供面向无连接的通信服务,随时都可以发送数据,处理简单且高效。
所以主要使用在以下场景:
主要是一切追求速度的场景上
2)TCP 使用场景:
TCP 使用场景:相对于 UDP,TCP 实现了数据传输过程中的各种控制,可以进行丢包时的重发控制,还可以对次序乱掉的分包进行顺序控制。在对可靠性要求较高的情况下,可以使用 TCP,即不考虑 UDP 的时候,都可以选择 TCP。
特别是需要可靠连接,比如付费、加密数据等等方向都需要依靠TCP
面向连接的TCP与无连接的UDP将是网络协议中不可或缺的重要知识点,TCP 和 UDP是TCP/IP 中有两个具有代表性的传输层协议,也是常年常考题型。
40岁老架构师尼恩提示:TCP/IP协议是既是面试的绝对重点,也是面试的绝对难点,建议大家有一个深入和详细的掌握,具体的内容请参见《尼恩Java面试宝典-专题10:TCPIP协议》PDF,该专题对TCP/IP协议有一个系统化、体系化、全面化的介绍。
如果要把TCP/IP实战写入简历,可以找尼恩指导
超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议。所有的WWW文件都必须遵守这个标准。HTTP是客户端浏览器或其他程序与Web服务器之间的应用层通信协议。
HTTPS(全称:Hyper Text Transfer Protocol over Secure Socket Layer 或 Hypertext Transfer Protocol Secure,超文本传输安全协议),是以安全为目标的HTTP通道,简单讲是HTTP的安全版。即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。它是一个URI scheme(抽象标识符体系),句法类同http体系。用于安全的HTTP数据传输。
HTTP协议传输的数据都是未加密的,也就是明文的,因此使用HTTP协议传输隐私信息非常不安全,为了保证这些隐私数据能加密传输,于是网景公司设计了SSL(Secure Sockets Layer)协议用于对HTTP协议传输的数据进行加密,从而就诞生了HTTPS。简单来说,HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全。
HTTPS和HTTP的区别主要如下:
在描述HTTPS有什么用之前,先来了解什么是HTTPS证书(即SSL证书)。
简单来说,SSL证书 = HTTPS +证书 +域名。HTTPS证书(即SSL证书)。HTTPS证书是颁发给标识互联网域名的数字证书,证书作用为建立SSL加密通道。结合HTTPS证书,来描述HTTPS,更容易理解HTTPS的作用。将SSL证书安装在网站服务器上,可实现网站身份验证和数据加密传输双重功能。
A:安全上的作用
1)实现加密传输
用户通过http协议访问网站时,浏览器和服务器之间是明文传输,安装SSL证书后,使用Https协议加密访问网站,可激活客户端浏览器到网站服务器之间的"SSL加密通道"(SSL协议),实现高强度双向加密传输,防止传输数据被泄露或篡改。
2)认证服务器真实身份(防钓鱼)
钓鱼欺诈网站泛滥,用户如何识别网站是钓鱼网站还是安全网站?网站部署全球信任的SSL证书后,浏览器内置安全机制,实时查验证书状态,通过浏览器向用户展示网站认证信息,让用户轻松识别网站真实身份,防止钓鱼网站仿冒。
B:提升企业形象
谷歌在2014年宣布,他们将考虑将https作为一个轻量级的因素,以鼓励网络安全。即使在Google的建议之外,那些切换到SSL的站点也经常发现客户认为他们的站点更真实。该网站还受到更多保护,免受第三方可能造成的损害。最近,谷歌警告Chrome用户,网络浏览器将开始重新标记仍在http上的网站:“从2017年10月开始,Chrome将在另外两种情况下显示‘不安全’警告。”
C:提升SEO搜索权重
重要的是要注意,从切换到HTTPS,谷歌,百度搜索引擎已经提示了其权重。百度已明确表示https配置作为排名因素,而且是出于网站安全的角度,那么我们也应该引起足够的重视,这可能并不需要花费太多的精力去解决这个事情但是能为SEO优化带来很好的效果!
我们知道,https就是http+ssl/tls,而http又是建立在tcp/ip之上的,所以浏览器和服务器使用https协议传输时,先用tcp/ip协议三次握手建立连接,再用ssl/tls协议握手确定算法密钥等,然后才是加密传输应用数据,最后tcp/ip四次挥手断开连接。
TLS协议分为TLS记录协议以及TLS握手协议。TLS记录协议负责对消息的压缩、加密以及数据认证。TSL握手协议则是生成共享密钥以及交换证书,其中共享密钥是为了支持TLS记录协议的加密传输,而交换证书是通信双方进行认证。
TLS握手协议的握手过程如下图所示:
具体步骤:
可以看到,握手协议完成了以下操作:
以上握手协议是TLS握手协议的一部分,TLS握手协议还包括:密码变更协议、警告协议、应用数据协议。
密码变更协议:客户端和服务器修改密码前分别发出 ChangeCipherSpec 消息表明要切换密码,并发送Finished 消息 确认切换密码结束。
警告协议:发生错误时通知通信对象,握手协议过程中异常、消息认证码错误等,都会使用该协议。
应用数据协议:用于通信对象之间传输应用数据,当TLS加密http时,请求和响应就会通过TLS的应用数据协议和TLS记录协议来进行传送。
一、定义
GC(Garbage Collection)是Java虚拟机(JVM)垃圾回收器提供的一种用于在空闲时间不定时回收无任何对象引用的对象占据的内存空间的一种机制。
二、作用
垃圾回收机制的引入可以有效的防止内存泄露、保证内存的有效使用,也减轻了 Java 程序员的对内存管理的工作量。
三、回收什么?
在JVM内存模型中,有三个是不需要进行垃圾回收的:程序计数器、JVM栈、本地方法栈。因为它们的生命周期是和线程同步的,随着线程的销毁,它们占用的内存会自动释放,所以只有方法区和堆需要进行GC
垃圾收集器在对垃圾进行回收前,确定有哪些对象是“死亡”状态,在对他们进行回收
四、判断方法
引用计数算法(Reference Counting):堆中每个对象实例都有一个引用计数。当一个对象被创建时,就将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。
可达性算法(根搜索算法GC Roots Tracing):从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点,无用的节点将会被判定为是可回收的对象。
GC ROOT对象:a) 虚拟机栈中引用的对象(栈帧中的本地变量表);b) 方法区中类静态属性引用的对象;c) 方法区中常量引用的对象;d) 本地方法栈中JNI(Native方法)引用的对象。
对象死亡(被回收)前的最后一次挣扎:通过可达性分析,那些不可达的对象并不是立即被销毁,他们还有被拯救的机会。如果要回收一个不可达的对象,要经历两次标记过程。首先是第一次标记,并判断对象是否覆写了 finalize 方法,如果没有覆写,则直接进行第二次标记并被回收。如果对象在finalize()方法中重新与引用链建立了关联关系,那么将会逃离本次回收,继续存活。
五、什么时候进行回收?
六、怎么回收?
垃圾收集算法、垃圾收集器
在介绍收集算法之前先给出一个术语:Stop-the-world(STW).意味着 **JVM由于要执行GC而停止了应用程序的执行,并且这种情形会在任何一种GC算法中发生。**当Stop-the-world发生时,除了GC所需的线程以外,所有线程都处于等待状态直到GC任务完成。
七、垃圾收集算法
在文章开头,已经说了垃圾回收机制主要负责回收方法区和堆的垃圾。方法区也叫做永久代。Java堆就包括新生代和老年代两部分
注意:在JDK 1.8中已经移除了永久代,改为元空间
工作流程如下:
八、垃圾收集器
新生代收集器:Serial,ParNew,Parallel Scavenge
老年代收集器:CMS,Serial Old,Parallel Old
Serial收集器:新生代单线程收集器,标记和清理都是单线程,优点是简单高效。是client级别默认的GC方式,可以通过-XX:+UseSerialGC
来强制指定。
Serial Old收集器:老年代单线程收集器,Serial收集器的老年代版本。
ParNew收集器:新生代收集器,可以认为是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现。
Parallel Scavenge收集器:并行收集器,追求高吞吐量,高效利用CPU。吞吐量一般为99%, 吞吐量= 用户线程时间/(用户线程时间+GC线程时间)。适合后台应用等对交互相应要求不高的场景。是server级别默认采用的GC方式,可用-XX:+UseParallelGC
来强制指定,用-XX:ParallelGCThreads=4
来指定线程数。
Parallel Old收集器:Parallel Scavenge收集器的老年代版本,并行收集器,吞吐量优先。
CMS(Concurrent Mark Sweep)收集器:高并发、低停顿,追求最短GC回收停顿时间,cpu占用比较高,响应时间快,停顿时间短,多核cpu 追求高响应时间的选择。
悲观锁:当前线程去操作数据的时候,总是认为别的线程会去修改数据,所以每次操作数据的时候都会上锁,别的线程去操作数据的时候就会阻塞,比如synchronized;
乐观锁:当前线程每次去操作数据的时候都认为别人不会修改,更新的时候会判断别人是否会去更新数据,通过版本来判断,如果数据被修改了就拒绝更新,例如cas是乐观锁,但是严格来说并不是锁,通过原子性来保证数据的同步,例如数据库的乐观锁,通过版本控制来实现,cas不会保证线程同步,乐观的认为在数据更新期间没有其他线程影响
总结:悲观锁适合写操作多的场景,乐观锁适合读操作多的场景,乐观锁的吞吐量会比悲观锁高
公平锁:有多个线程按照申请锁的顺序来获取锁,就是说,如果一个线程组里面,能够保证每个线程都能拿到锁,例如:ReentrantLock(使用的同步队列FIFO)
非公平锁:获取锁的方式是随机的,保证不了每个线程都能拿到锁,会存在有的线程饿死,一直拿不到锁,例如:synchronized,ReentrantLock
总结:非公平锁性能高于公平锁,更能重复利用CPU的时间
可重入锁:也叫递归锁,在外层使用锁之后,在内层仍然可以使用,并且不会产生死锁
不可重入锁:在当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞
总结:可重入锁能一定程度的避免死锁,例如:synchronized,ReentrantLock
自旋锁:一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环,任何时刻最多只能有一个执行单元获得锁
总结:不会发生线程状态的切换,一直处于用户态,减少了线程上下文切换的消耗,缺点是循环会消耗CPU
共享锁:也叫读锁,可以查看数据,但是不能修改和删除的一种数据锁,加锁后其他的用户可以并发读取,但不能修改、增加、删除数据,该锁可被多个线程持有,用于资源数据共享
独享锁:也叫排它锁、写锁、独占锁、独享锁,该锁每一次只能被一个线程所持有,加锁后任何线程试图再次加锁都会被阻塞,直到当前线程解锁。例如:线程A对data加上排它锁后,则其他线程不能再对data加任何类型的锁,获得互斥锁的线程既能读数据又能修改数据
一、作用
lock
和 synchronized
都是 Java
中去用来解决线程安全问题的一个工具,是Java
中两种用来实现线程同步的方式。
二、来源
sychronized
是 Java
中的一个关键字,它可以修饰方法和代码块。当一个线程访问一个对象的同步方法或同步代码块时,其他线程不能访问这个对象的其他同步方法或同步代码块。
lock
是 java.util.concurrent.locks
包里的一个接口,这个接口有很多实现类,其中就包括我们最常用的 ReentrantLock
(可重入锁)。它提供了更多的灵活性,比如可以尝试获取锁而不会阻塞线程、可以重试获取锁的次数以及可以提供公平锁和非公平锁。
三、锁的力度
sychronized
可以通过两种方式去控制锁的力度:
sychronized
关键字修饰在方法层面。锁对象的不同:
锁对象为静态对象或者是class
对象,那这个锁属于全局锁。
锁对象为普通实例对象,那这个锁的范围取决于这个实例的生命周期。
lock
锁的力度是通过lock()
与unlock()
两个方法决定的。在两个方法之间的代码能保证其线程安全。lock
的作用域取决于lock
实例的生命周期。
四、灵活性
lock
锁比sychronized
的灵活性更高。
lock
可以自主的去决定什么时候加锁与释放锁。只需要调用lock
的lock()
和unlock()
这两个方法就可以。
sychronized
由于是一个关键字,所以他无法实现非阻塞竞争锁的方法,一个线程获取锁之后,其他锁只能等待那个线程释放之后才能有获取锁的机会。
五、公平锁与非公平锁
(1)公平锁:
多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
(2)非公平锁:
多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
lock
提供了公平锁和非公平锁两种机制(默认非公平锁)。PS:sychronized
是非公平锁。
六、异常是否释放锁
synchronized
锁的释放是被动的,当sychronized
同步代码块执行结束或者出现异常的时候才会被释放。
lock
锁发生异常的时候,不会主动释放占有的锁,必须手动unlock()
来释放,所以我们一般都是将同步代码块放进try-catch
里面,finally
中写入unlock()
方法,避免死锁发生。
七、判断是否能获取锁
synchronized
不能。
lock
提供了非阻塞竞争锁的方法trylock()
,返回值是Boolean
类型。它表示的是用来尝试获取锁:成功获取则返回true
;获取失败则返回false
,这个方法无论如何都会立即返回。
八、调度方式
synchronized
使用的是object
对象本身的wait
、notify
、notifyAll
方法,而lock
使用的是Condition
进行线程之间的调度。
九、是否能中断
synchronized
只能等待锁的释放,不能响应中断。
lock
等待锁过程中可以用interrupt()
来中断。
十、性能
如果竞争不激烈,性能差不多;竞争激烈时,lock
的性能会更好。
lock
锁还能使用readwritelock
实现读写分离,提高多线程的读操作效率。
十一、sychronized锁升级
synchronized
代码块是由一对 monitorenter/monitorexit
指令实现的。Monitor
的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作。
所以现在JVM提供了三种不同的锁:偏向锁、轻量级锁、重量级锁。
(1)偏向锁:
当没有竞争出现时,默认使用偏向锁。线程会利用 CAS 操作在对象头上设置线程 ID ,以表示对象偏向当前线程。
目的:在很多应用场景中,大部分对象生命周期最多会被一个线程锁定,使用偏向锁可以降低无竞争时的开销。
(2)轻量级锁:
JVM
比较当前线程的 threadID
和 Java
对象头中的threadID
是否一致,如果不一致(比如线程2要竞争锁对象),那么需要查看 Java
对象头中记录的线程1是否存活(偏向锁不会主动释放因此还是存储的线程1的 threadID
):
threadID
为线程2的);当有其他线程想访问加了轻量级锁的资源时,会使用 自旋锁 优化,来进行资源访问。
目的:竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,开销大,如果刚刚阻塞不久这个锁就被释放了,就得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。
(3)重量级锁:
如果自旋失败,很大概率再进行一次自旋,如果也是失败,因此直接升级成 重量级锁 ,进行线程阻塞,减少cpu消耗。
当锁升级为重量级锁后,未抢到锁的线程都会被阻塞,进入阻塞队列。
synchronized
关键字并不一定会阻塞线程。synchronized
关键字用于实现Java中的同步机制,它可以应用于方法或代码块。当一个线程获得了某个对象的锁时(通过synchronized
关键字),其他线程如果想要获取该对象的锁,就会被阻塞,直到持有锁的线程释放锁。
但是,如果一个线程尝试获取一个对象的锁时,如果锁没有被其他线程持有,那么该线程会立即获得锁,而不会被阻塞。因此,synchronized
关键字只会在获取锁时可能导致线程阻塞,而不是一定会阻塞线程。
另外需要注意的是,synchronized
关键字还可以用于静态方法和类级别的锁定,这时锁定的是整个类而不是对象。在这种情况下,如果一个线程获取了类级别的锁,其他线程也会被阻塞,直到持有锁的线程释放锁。
总结起来,synchronized
关键字的阻塞行为取决于锁的可用性,如果锁可用,线程会立即获取锁而不被阻塞;如果锁不可用,线程会被阻塞直到锁可用。
在Java中,锁是用于实现线程同步的机制。为了提高多线程程序的性能,Java引入了偏向锁、轻量级锁和重量级锁等不同级别的锁。
总的来说,偏向锁和轻量级锁都是为了在竞争不激烈的情况下减少锁的开销,提高程序性能。只有在竞争激烈的情况下,锁才会膨胀为重量级锁,使用操作系统的互斥量来实现线程同步。
锁状态的变化结论
偏向锁延迟偏向
//关闭延迟开启偏向锁
-XX:BiasedLockingStartupDelay=0
//禁止偏向锁
-XX:-UseBiasedLocking
偏向锁在无竞争的时候一直是偏向锁
public static void main(String[] args) throws InterruptedException {
log.debug(Thread.currentThread().getName() + "最开始的状态。。。\n"
+ ClassLayout.parseInstance(new Object()).toPrintable());
// HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式
Thread.sleep(4000);
Object obj = new Object();
new Thread(new Runnable() {
@Override
public void run() {
log.debug(
Thread.currentThread().getName() + "开始执行准备获取锁。。。\n" + ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj) {
log.debug(Thread.currentThread().getName() + "获取锁执行中。。。\n"
+ ClassLayout.parseInstance(obj).toPrintable());
}
log.debug(Thread.currentThread().getName() + "释放锁。。。\n" + ClassLayout.parseInstance(obj).toPrintable());
}
}, "thread1").start();
Thread.sleep(5000);
log.debug(Thread.currentThread().getName() + "结束状态。。。\n" + ClassLayout.parseInstance(obj).toPrintable());
}
在同步代码块外调用hashCode()方法
在同步代码块内调用hashCode()方法
偏向锁撤销:自己验证wait和notify
模拟竞争不激烈的场景
@Slf4j
public class TestMemory {
public static void main(String[] args) throws InterruptedException {
log.debug(Thread.currentThread().getName() + "最开始的状态。。。\n"
+ ClassLayout.parseInstance(new Object()).toPrintable());
// HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式
Thread.sleep(4000);
Object obj = new Object();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
log.debug(Thread.currentThread().getName() + "开始执行thread1。。。\n"
+ ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj) {
log.debug(Thread.currentThread().getName() + "获取锁执行中thread1。。。\n"
+ ClassLayout.parseInstance(obj).toPrintable());
}
log.debug(Thread.currentThread().getName() + "释放锁thread1。。。\n"
+ ClassLayout.parseInstance(obj).toPrintable());
}
}, "thread1");
thread1.start();
// 控制线程竞争时机
Thread.sleep(1);
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
log.debug(Thread.currentThread().getName() + "开始执行thread2。。。\n"
+ ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj) {
log.debug(Thread.currentThread().getName() + "获取锁执行中thread2。。。\n"
+ ClassLayout.parseInstance(obj).toPrintable());
}
log.debug(Thread.currentThread().getName() + "释放锁thread2。。。\n"
+ ClassLayout.parseInstance(obj).toPrintable());
}
}, "thread2");
thread2.start();
Thread.sleep(5000);
log.debug(Thread.currentThread().getName() + "结束状态。。。\n" + ClassLayout.parseInstance(obj).toPrintable());
}
}
竞争不激烈的场景的运行结果
模拟竞争激烈的场景
// 控制线程竞争时机
Thread.sleep(1);
竞争激烈的场景的运行结果
线程池的7大参数包括:corePoolSize、maximumPoolSize、keepAliveTime、TimeUnit、workQueue、threadFactory和handler。
在使用 ThreadPoolExecutor 创建线程池时所设置的 7 个参数,如以下源码所示:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
//...
}
核心线程数:是指线程池中长期存活的线程数。
这就好比古代大户人家,会长期雇佣一些“长工”来给他们干活,这些人一般比较稳定,无论这一年的活多活少,这些人都不会被辞退,都是长期生活在大户人家的。
最大线程数:线程池允许创建的最大线程数量,当线程池的任务队列满了之后,可以创建的最大线程数。
这是古代大户人家最多可以雇佣的人数,比如某个节日或大户人家有人过寿时,因为活太多,仅靠“长工”是完不成任务,这时就会再招聘一些“短工”一起来干活,这个最大线程数就是“长工”+“短工”的总人数,也就是招聘的人数不能超过 maximumPoolSize。
注意事项
最大线程数 maximumPoolSize 的值不能小于核心线程数 corePoolSize,否则在程序运行时会报 IllegalArgumentException 非法参数异常,如下图所示:
空闲线程存活时间,当线程池中没有任务时,会销毁一些线程,销毁的线程数=maximumPoolSize(最大线程数)-corePoolSize(核心线程数)。
还是以大户人家为例,当大户人家比较忙的时候就会雇佣一些“短工”来干活,但等干完活之后,不忙了,就会将这些“短工”辞退掉,而 keepAliveTime 就是用来描述没活之后,短工可以在大户人家待的(最长)时间。
时间单位:空闲线程存活时间的描述单位,此参数是配合参数 3 使用的。
参数 3 是一个 long 类型的值,比如参数 3 传递的是 1,那么这个 1 表示的是 1 天?还是 1 小时?还是 1 秒钟?是由参数 4 说了算的。
TimeUnit 有以下 7 个值:
阻塞队列:线程池存放任务的队列,用来存储线程池的所有待执行任务。
它可以设置以下几个值:
比较常用的是 LinkedBlockingQueue,线程池的排队策略和 BlockingQueue 息息相关。
线程工厂:线程池创建线程时调用的工厂方法,通过此方法可以设置线程的优先级、线程命名规则以及线程类型(用户线程还是守护线程)等。
线程工厂的使用示例如下:
public static void main(String[] args) {
// 创建线程工厂
ThreadFactory threadFactory = new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
// 创建线程池中的线程
Thread thread = new Thread(r);
// 设置线程名称
thread.setName("Thread-" + r.hashCode());
// 设置线程优先级(最大值:10)
thread.setPriority(Thread.MAX_PRIORITY);
//......
return thread;
}
};
// 创建线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 10, 0,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(),
threadFactory); // 使用自定义的线程工厂
threadPoolExecutor.submit(new Runnable() {
@Override
public void run() {
Thread thread = Thread.currentThread();
System.out.println(String.format("线程:%s,线程优先级:%d",
thread.getName(), thread.getPriority()));
}
});
}
以上程序的执行结果如下:
从上述执行结果可以看出,自定义线程工厂起作用了,线程的名称和线程的优先级都是通过线程工厂设置的。
拒绝策略:当线程池的任务超出线程池队列可以存储的最大值之后,执行的策略。
默认的拒绝策略有以下 4 种:
线程池的默认策略是 AbortPolicy 拒绝并抛出异常。
RocketMQ 消息生产消费过程如下:
Message
对象中,并放入到 topic
的 queue
中。producerGroup
向 Broker 发送消息,Broker 会将消息发送到所属的 topic
的 queue
中。Message
对象中,并从 topic
的 queue
中取出。consumerGroup
从 Broker 中拉取消息,并在消费者自己的 messageListener
中处理消息。下面是用 Java 实现的 RocketMQ 消息生产消费过程的示例代码:
public class RocketMQProducer {
public static void main(String[] args) throws Exception {
// 创建生产者
Producer producer = new DefaultMQProducer("my-project-namespace");
// 创建消息
Message message = new Message("TopicTest", "TagA", "Hello World".getBytes());
// 发送消息到消息队列
producer.send(message);
// 关闭生产者
producer.close();
}
}
public class RocketMQConsumer {
public static void main(String[] args) throws Exception {
// 创建消费者
Consumer consumer = new DefaultMQPushConsumer("my-project-namespace");
// 设置消费者组名称
consumer.setConsumerGroup("ConsumerGroup");
// 创建消息监听器
MessageListenerOrderly messageListener = new MessageListenerOrderly();
consumer.subscribe("TopicTest", "*");
consumer.setMessageListener(messageListener);
// 启动消费者
consumer.start();
// 关闭消费者
consumer.shutdown();
}
static class MessageListenerOrderly implements MessageListener {
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
// 处理消息
System.out.println("Received Message: " + new String(msgs.get(0).getBody()));
// 返回消费者消费状态
return ConsumeOrderlyStatus.SUCCESS;
}
}
}
在以上示例代码中,我们使用 DefaultMQProducer
和 DefaultMQPushConsumer
来创建生产者和消费者。我们使用 send()
方法将消息发送到消息队列中,使用 subscribe()
方法订阅消息主题,使用 setMessageListener()
方法设置消费者监听器,在监听器中实现 consumeMessage()
方法来处理消息。在 consumeMessage()
方法中,我们可以对消息进行业务处理,并将处理结果返回给消费者。
在以上示例代码中,我们还使用了 MessageExt
类来封装消息,它包含了消息的主题、消息内容、消息类型等信息。在使用 consumeMessage()
方法处理消息时,我们需要实现 MessageListener
接口,并实现 consumeMessage()
方法来处理消息。
在实际应用中,我们还需要考虑消息的发送顺序、消息的重试等问题,可以使用 RocketMQ 提供的一些高级特性来实现,例如:发送消息时设置 Priority
,使用 TransactionManager
进行事务管理,使用 BrokerSelector
选择合适的 Broker 等。
40岁老架构师尼恩提示:RocketMQ是既是面试的绝对重点,也是面试的绝对难点,建议大家有一个深入和详细的掌握,具体的内容请参见《尼恩RocketMQ四部曲》,该专题对RocketMQ有一个系统化、体系化、全面化的介绍。
如果要把RocketMQ实战写入简历,可以找尼恩指导
生产者的负载均衡是指在多个Broker之间分配消息,以确保每个Broker处理的消息量大致相等。RocketMQ提供了多种负载均衡策略,包括以下几种:
在RocketMQ中,可以通过设置ProducerConfig对象的loadBalancePolicy属性来选择负载均衡策略。例如,使用轮询策略可以这样设置:
DefaultMQProducer producer = new DefaultMQProducer("producer_group");
producer.setNamesrvAddr("localhost:9876");
producer.setLoadBalancePolicy("roundrobin"); // 设置负载均衡策略为轮询
producer.start();
在RocketMQ中,消息的reput过程是指对消息的可靠性和一致性进行保障的过程。RocketMQ是一个分布式消息队列系统,用于实现高可靠性、高吞吐量的消息传递。
消息的reput过程在RocketMQ中包括以下几个步骤:
需要注意的是,RocketMQ的消息reput过程是基于分布式架构的,通过主从同步和消息复制机制保证消息的可靠性和一致性。同时,RocketMQ还提供了丰富的配置选项和监控工具,以便对消息的reput过程进行监控和调优。
在RocketMQ中,ConsumeQueue是用于存储消息消费进度的数据结构。它是基于文件的存储方式,每个主题(Topic)都有一个对应的ConsumeQueue文件。
ConsumeQueue文件由多个逻辑队列(Logical Queue)组成,每个逻辑队列对应一个消息队列(Message Queue)。逻辑队列中存储了消息的索引信息,包括消息在CommitLog文件中的偏移量(offset)和消息的大小。
ConsumeQueue文件由两部分组成:索引文件(Index File)和位图文件(BitMap File)。
索引文件用于记录每个消息队列的消息索引信息。它包含了每个消息的偏移量和消息的存储时间戳。索引文件按照消息的存储时间戳进行排序,方便快速查找特定时间范围内的消息。
位图文件用于记录消息队列中每个消息的消费状态。每个消息占用一个位,0表示未消费,1表示已消费。通过位图文件,可以快速判断消息是否已经被消费。
ConsumeQueue中的消息格式如下:
+---------------------+---------------------+
| Offset | CommitLogOffset |
| (8字节) | (8字节) |
+---------------------+---------------------+
| Size | TagsCode |
| (4字节) | (8字节) |
+---------------------+---------------------+
| StoreTimestamp | ConsumeTimestamp |
| (8字节) | (8字节) |
+---------------------+---------------------+
通过解析ConsumeQueue文件,RocketMQ可以快速定位消息的位置,提高消息的消费效率。
CAP原理主要是针对分布式系统中的一致性、可用性和分区容忍性进行的讨论。在分布式系统中,这三个属性是相互制约的,必须根据实际情况进行权衡。
RocketMQ是一个开源的分布式消息中间件,它的设计目标是提供高吞吐量、低延迟、高可用性和可伸缩性的消息传递解决方案。
在分布式系统中,一般会使用CAP原则来描述系统的特性。CAP原则指的是一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),它表明在分布式系统中,无法同时满足一致性、可用性和分区容错性这三个特性,只能在其中选择两个。
根据CAP原则,RocketMQ被归类为AP(可用性和分区容错性)系统。这意味着在RocketMQ中,当网络分区发生时,它会保证可用性和分区容错性,即消息仍然可以进行传递和消费,但在某些情况下可能会出现数据不一致的情况。
RocketMQ通过主从复制和消息冗余机制来保证高可用性和可靠性。它使用了主题(Topic)和队列(Queue)的概念来组织消息,消息被写入到主题中,然后根据配置的队列数量进行分布式存储。当一个消息被发送到RocketMQ集群时,它会被复制到多个Broker节点上的队列中,以确保消息的可靠性和高可用性。
总结起来,RocketMQ是一个AP系统,它在可用性和分区容错性方面提供了强大的支持,但在某些情况下可能会出现数据不一致的情况。
当RocketMQ的Broker宕机时,会对消息传递的可用性和可靠性产生一定的影响。下面是宕机时的一些可能情况和处理方式:
为了减少Broker宕机对消息传递的影响,可以采取以下措施:
总的来说,RocketMQ对Broker宕机情况有一定的容错和恢复机制,可以保证消息传递的可用性和可靠性。然而,对于关键业务场景,建议采取适当的高可用性和容灾措施,以确保系统的稳定性和可靠性。
RocketMQ是一个开源项目,您可以在Apache RocketMQ的官方GitHub仓库中找到完整的源代码。
以下是选主过程的源码简要描述:
org.apache.RocketMQ.broker.BrokerController
类中的initialize()
方法,该方法用于初始化Broker节点的各个组件。initialize()
方法中,会执行org.apache.RocketMQ.broker.BrokerController
类中的registerBrokerAll()
方法,该方法用于向NameServer注册Broker节点的相关信息。registerBrokerAll()
方法中,会执行org.apache.RocketMQ.namesrv.processor.RegisterBrokerProcessor
类中的processRequest()
方法,该方法用于处理Broker节点注册的请求。processRequest()
方法中,会执行org.apache.RocketMQ.namesrv.routeinfo.RouteInfoManager
类中的registerBroker()
方法,该方法用于将Broker节点的信息存储在内存中,并定期将这些信息持久化到磁盘上的文件中。org.apache.RocketMQ.client.impl.factory.MQClientInstance
类中的updateTopicRouteInfoFromNameServer()
方法中找到。org.apache.RocketMQ.broker.BrokerController
类中的slaveSynchronize()
方法中找到。org.apache.RocketMQ.store.DefaultMessageStore
类中的doDispatch()
方法中找到。org.apache.RocketMQ.broker.BrokerController
类中的rebalanceService()
方法中找到。在尼恩的(50+)读者社群中,很多、很多小伙伴需要进大厂、拿高薪。
尼恩团队,会持续结合一些大厂的面试真题,给大家梳理一下学习路径,看看大家需要学点啥?
前面用多篇文章,给大家介绍阿里、百度、字节、滴滴的真题:
《问麻了…阿里一面索命27问,过了就60W+》
《百度狂问3小时,大厂offer到手,小伙真狠!》
《饿了么太狠:面个高级Java,抖这多硬活、狠活》
《字节狂问一小时,小伙offer到手,太狠了!》
《收个滴滴Offer:从小伙三面经历,看看需要学点啥?》
这些真题,都会收入到 史上最全、持续升级的 PDF电子书 《尼恩Java面试宝典》。
本文收录于 《尼恩Java面试宝典》 V83版。
基本上,把尼恩的 《尼恩Java面试宝典》吃透,大厂offer很容易拿到滴。另外,下一期的 大厂面经大家有啥需求,可以发消息给尼恩。
《从0开始,手写Redis》
《从0开始,手写MySQL事务管理器TM》
《从0开始,手写MySQL数据管理器DM》
《腾讯太狠:40亿QQ号,给1G内存,怎么去重?》
《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》PDF,请到下面公号【技术自由圈】取↓↓↓