线程安全本质是什么,Java 如何保证线程安全,callable,runnable 有什么区别,线程不正常终止会发生什么,线程占用的空间具体是哪,是寄存器还是内存还是什么
线程安全的本质就是由于多个线程在争抢资源,可能使得资源的一致性、完整性发生问题,导致程序的不正确执行。
Java 是怎么保证线程安全的?
提供一些关键字以及 API 尽量的保证线程安全:
关键字:
volatile:能保证可见性、有序性、不能保证原子性,需要加上 Synchornized,底层的原理是内存屏障,内存屏障就是一个硬件指令,保证程序的可见性、有序性。
synchornized(底层会加锁,表面上是锁上了一段代码):保证代码块的可见性、有序性、原子性
API(就是一些锁和保证线程安全的工具类)
Lock 接口:显式的加锁,解锁,保证代码块的指向的线程安全
ThreadLocal 类,将数据放入到 ThradLocal 中,可以保证多线程下的数据安全
API : 原子类的API ,底层是 Unsafe 类的 CAS 操作,直接操作内存,使用 CPU 的原子指令
Callable 和 Runnable 都是用于创建线程的接口:
区别是:
@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
Callable 的 call 方法是允许有返回值的,run() 方法是没有返回值的
Callable 是一个泛型接口,可以指定返回数据的数据类型
Callable 的 call() 方法是允许抛出异常的,但是 run() 方法是不允许抛出异常的
线程的不正确退出可能导致内存泄露,服务器宕机,影响客户的使用。JVM 中线程存储在栈内存空间中,因为线程就是一个个指令的集合,当然,线程也会使用到 JVM 的堆内存空间保存中间创建出来的对象,也会使用程序计数器保存下一条需要执行的指令。
Java 的线程和 Linux 的线程有什么区别,为什么需要 Java 的线程
1、调度方式不一样,一个是 JVM 调度,一个是 Linux 系统内核调度
2、创建销毁成本不一样, Java 的线程创建销毁的成本是更加高的
需要 Java 线程的原因就是需要使用 Java 线程实现代码的跨平台,JVM 本质上就是运行的一个个 Java 线程组成的,JVM 的可跨平台 Java 线程是有帮助的
volatile 具体实现原理,内存重排序都会发生在哪,为什么要内存重排序
volatile 用于修饰变量,保证多个线程对同一个 volatile 修饰的变量的可见性以及有序性;
实现的核心原理就是在线程读取 volatile 变量之前加一个读屏障,保证读取最新的数据,在写入数据之后加一个写屏障,保证数据被其他线程看到。
内存重排序可能发生在创建对象的时候,创建对象,变量指向内存地址的顺序或许可以发生变化。
内存重排序目的是为了对 Java 代码进行优化,提升代码的运行效率
使用过哪些 Java 并发包
Java 保证线程安全的工具类或者接口而已
原子类
各种锁
简述 BIO,NIO 的具体使用及原理
一个是同步阻塞IO(BIO),一个是同步非阻塞IO(NIO)
使用场景用客户端请求服务器来说,对于 BIO 来说,一个客户端连接到服务器,服务器会给其单独开辟一个线程用于处理这个请求,客户端的请求一直在阻塞,直到获取到了服务器准备好的数据。
BIO 适用于连接比较少的场景,因为多了,服务器创建线程吃不消
对于 NIO 来说,是一个同步非阻塞的模型,当客户端在请求服务器的时候,服务器只是需要将这个请求放到多路复用器上面,使用一个线程维护所有的请求即可,此时客户端是不需要阻塞等待的,其可以执行其他的操作。
NIO 适用于连接数比较多的情况,其允许单个线程处理多个请求,减少了线程的上下文切换带来的性能损耗。
concurrentHashMap1.8 和之前版本有什么区别,HashMap 的具体实现,红黑树和平衡二叉树的区别,为什么不用 B+ 树
ConcurrentHashMap 在 1.8 的时候,实现的数据结构是:数组 + 链表 + 红黑树,本质上就是对于单个 Hash 桶进行加锁,使得锁的粒度降低,提高了并发度。
在 1.7 的版本中,锁的粒度比较大,其核心思想是是将一个大的哈希表分为多个小的哈希表,每个哈希表也就是 sergement ,对于每个 sergement 加锁,比 HashTable 整表加锁的效率提高了一点点。
HashMap 的具体实现的数据结构就是数组 + 链表 + 红黑树,是线程不安全的。
红黑树和平衡二叉树的区别就是:红黑树本身对于左子树和右子树之间的高度差的要求没有那么的严格,平衡二叉树为了维护左右子树的高度差不能高于 1 ,这样可能损失大量的性能,
HashMap 没有使用 B+ 树,是因为,B+ 树常用于数据库中的数据检索,并且如果 B+ 树不按照自增主键的话,HashMap 没有顺序,导致了大面积的数据移动,导致了性能下降,可能会发生大量的数据移动,导致效率下降,相比之下,红黑树,的旋转、染色操作可能更简单些,对于计算机来讲,执行的步骤更少一些。
Java 的 GC 整体过程,垃圾收集算法,流程,垃圾收集器,强引用,弱引用,虚引用等概念等,如果我要设置一个内存缓冲区,让垃圾收集器不对其进行操作怎么办
常见的垃圾回收算法就是:标记清除,标记整理,标记复制;
垃圾清除的核心原理就是,创建对象,内存满了,使用一点手段把垃圾回收掉。
说一下常用的 G1 垃圾回收器,使用的比较多,思想都差不多,没必要背那么多没有用的东西。
G1 的核心思想就是:没有必要只是简单的将堆内存区域划分为新生代和老年代,可以把堆内存空间划分为很多个小块,在更低的粒度上,有的是 Eden 区域块,有的是 Survivor 区域块、有的是老年代。这样在垃圾回收的时候,更加的灵活。可以根据系统的运行状况,动态对 G1 的内存区域进行划分。
所谓的强软弱虚的概念,无非就是为了更好的回收垃圾,可以合理的控制对象的回收时机,做到对对象生命周期的管理。
强引用:JVM 内存不足也不会回收这个对象
软引用:JVM 内存不足并且空间不足以分配给后面的对象会回收软引用指向的对象
弱引用:下一次的垃圾回收的时候,直接收集掉释放内存
虚引用:在任何时候可能都会被收集掉,生命周期飘忽不定
如果想要一个内存区域是不接受 JVM 内存管理,那么把这个缓存放到堆的外面,可以考虑使用 ByteBuffer 实现
public class Test {
public static void main(String[] args) {
// 创建了 ByteBuffer 这个内存空间是直接内存,这个内存直接被宝座系统管理,你就回收不了了
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
System.out.println(buffer);
}
}
JVM 内存分区。假设 Java 的 GC 时间过长,简述应该如何做来排查解决这个问题,jamp 的 dump 命令比较重,有什么代替的方法。
dump 操作生成了 Java 进程的内存快照,包含了 Java 进程在某个时间点上面的所有对象,、类信息等状态信息。
同时还会冻结暂停 Java 进程,一直到 dump 命令完成为止。
可以使用 jstat 命令代替,查看 JVM 状态
使用 jstat 步骤
1、获取 Java 进程 Id
jps
2、使用 jstat
jstat [option] [vmid] [interval] [count]
option 参数是 jstat 命令的操作选项,例如 gcutil 表示查看垃圾回收情况,class 表示查看类加载情况等。vmid 参数是要监控的 Java 进程的进程 ID 或者进程名,interval 参数表示每次输出数据的时间间隔(单位是毫秒),count 参数表示输出数据的次数。
下面是一些常用的 jstat 命令选项:
jstat -gcutil [vmid]:查看垃圾回收情况,包括 GC 时间、堆内存使用情况等。
jstat -class [vmid]:查看类加载情况,包括已加载类数量、未加载类数量等。
jstat -compiler [vmid]:查看 JIT 编译器的情况,包括编译任务数量、编译失败数量等。
jstat -gc [vmid]:查看垃圾回收器的详细信息,包括新生代和老年代的情况、每个 GC 阶段的时间等。
需要注意的是,jstat 命令输出的数据都是数字,需要结合具体的文档或者工具来解读这些数字所代表的含义。
输出内容
(base) yimeng@MacBook-Pro MyBlog % jps
46608 nacos-server.jar
62906 RemoteMavenServer36
62860
64285 Jps
(base) yimeng@MacBook-Pro MyBlog % jstat -gcutil 62906
S0 S1 E O M CCS YGC YGCT FGC FGCT CGC CGCT GCT
0.00 74.78 48.73 5.15 98.52 93.70 2 0.005 0 0.000 0 0.000 0.005
参数含义
S0: 第一个幸存区的使用占比,单位是%
S1: 第二个幸存区的使用占比,单位是%
E: Eden 区的使用占比,单位是%
O: 老年代的使用占比,单位是%
M: 元空间的使用占比,单位是%
CCS: 压缩类空间的使用占比,单位是%
YGC: Young GC 次数
YGCT: Young GC 时间,单位是秒
FGC: Full GC 次数
FGCT: Full GC 时间,单位是秒
CGC: Concurrent GC 次数
CGCT: Concurrent GC 时间,单位是秒
GCT: 所有 GC 时间,单位是秒
注:Full GC 针对的是新生代和老年代的对象回收
代码实现每五分钟一次 Minor GC,如果要 FullGC 呢
Minor GC ,Eden 和 一个 Survivor 满了,复制对象到另外一个 Survivor 的过程
Young GC ,指的是 Eden 和两个 Survivor 都满了才会触发,进行年轻代的对象回收
想要让系统实现按照一定的时间就执行一次垃圾回收,可以考虑使用 System.gc() 方法,使用 ScheduledExecutorService 类创建对象,按照一定的时间执行 System.gc() ;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class MinorGCExample {
public static void main(String[] args) {
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
// 每五分钟执行一次垃圾回收
executor.scheduleAtFixedRate(() -> {
System.out.println("Performing minor GC...");
System.gc();
}, 0, 5, TimeUnit.MINUTES);
}
}
ThreadLocal 的具体是怎样的,为什么会有内存泄漏问题,怎样避免
什么是 ThreadLocal
创建一个 ThreadLocal 变量,那么每个线程使用这个变量的时候,都会对这个变量进行一个拷贝,使得每一个线程中的 ThreadLocal 变量都是相互隔离的,这样就会避免掉线程安全的问题,保证了一定的安全性。
//创建一个ThreadLocal变量
static ThreadLocal<String> localVariable = new ThreadLocal<>();
为什么需要 ThreadLocal 用空间换取时间
传统的多线程操作一个变量的时候,会使用到加锁,这样节省了空间,但是由于加锁排队,导致程序在时间上消耗的时间就会多一些,使用 ThreadLocal 可以给每一个线程一个变量的拷贝,增加了空间的浪费,但是节省了时间,
使用 ThreadLocal 和不使用 ThreadLocal 的区别
如果多线程操作一个变量,创建这个变量的类本身不是线程安全的,那么就会发生线程安全的问题,比如数据没有一致性了。
但是将变量放到了 ThreadLocal 中,会避免线程安全问题,程序正常执行
Thread 类中有 ThreadLocalMap 变量,专门用于存储 ThreadLocal 变量
Thread类中,有个ThreadLocal.ThreadLocalMap 的成员变量。
ThreadLocalMap内部维护了Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型对象值。
ThreadLocal 为什么会可能会发生内存泄露的问题
每一个线程的内部有一个 ThreadLocalMap 对象,里面有一个个的 Entry 结构,Entry 里面是 key , value 结构,如果一个 ThreadLocal 变量被清除掉了(ThreadLocal == null),那么 Entry 就是 Key = null,value = value, 此时因为 value 是强引用,本应该 Entry 一起被释放掉内存的,但是由于 value 的存在,Entry 没有被释放掉,所以,造成了内存泄露。
ThreadLocalMap 为什么 key 设置成为弱引用?
为了尽可能的避免内存泄露,因为弱引用的对象在下一次垃圾回收的时候就会被释放内存,但是得等到下一次的垃圾回收,在垃圾回收之前,还是存在内存泄露的。
ThreadLocal 正常使用时候的图示
ThreadLocal 被回收,value 回收不掉,发生内存泄露的图示
注
虽然使用了一系列的方式尽可能的避免内存泄露的发生,但是开发人员在使用结束 ThreadLocal 变量之后,使用 remove() 方法将 Entry 内存释放掉,避免当前线程的内存泄漏。
ThreadLocal 变量创建、使用、删除代码实例
// 创建
ThreadLocal<String> myThreadLocal = new ThreadLocal<>();
// 使用
myThreadLocal.set("Hello, ThreadLocal!");
String value = myThreadLocal.get();
// 销毁
myThreadLocal.remove();
Java 的异常种类有哪些,平时自己是怎么处理异常的
受检异常:编译期间必须要处理的异常,ClassNotFoundExceptin
非受检异常:运行期间可能发生的异常,数组索引超出异常
在异常中使用 try catch 语句记性异常的逻辑控制,打印异常信息,或者其他的逻辑。
Spring 的 IOC 和 AOP 及 MVC 机制,Sping 中的单例 bean 是否可以依赖多例 bean
IOC
AOP
MVC:软件设计模式,控制器、模型、视图
Springboot 起步依赖有什么好处
Spring Boot 起步依赖的好处就是可以使用尽量少的配置来开发应用程序,使得开发人员可以将大量的精力放到业务逻辑上面,而不是应用程序的配置上面。
tomcat 如果有两个项目,两个项目里面如果有相同的 class,那么 tomcat 是如何对其进行区别
Tomcat 的每一个 web 项目都是都自己的类加载器的,所以两个 web 应用程序拥有相同类的时候,因为使用了不同的类加载器所以是不会冲突的。
简述 MySQL 的 ACID 性质及实现,ACID 有哪些一致性种类,乐观锁怎么实现的,简述 MySQL 的隔离机制及实现,简述 MySQL 索引结构及实现
ACID(MySQL 事务的特性):原子性、一致性、隔离性、持久性,只有在说事务的时候,事务具有上面的这几个特点。
乐观锁基于 MVCC 实现
核心就是给每一行数据的后面,MySQL 隐式的给一个版本号,通过这个版本号来控制线程并发的访问数据库。
事务 A 修改记录行 1 ,获取到版本号并且对版本号 + 1;
修改结束,查看版本号是不是自己修改时候拿到的版本号 + 1,如果版本号对的话,那么在事务 A 访问记录行 1 期间,没有其他事务对这个记录进行修改,此时这个事务就可以成功更新数据。否则就要进行事务的回滚操作,更新失败。
简单来说就是做一个标记,如果标记不是事务自己留下来的,那么就说明数据有问题,回滚就行。
MySQL 的四个隔离级别:读未提交、读已提交、可重复度、串行化,隔离机制是通过 MVCC 实现的,也就是记录事务访问行记录的版本号或者时间戳,只有版本号或者时间戳对得上才能完成行记录的修改,否则需要事务的回滚。
一个事务写入MySQL 行数据,获取版本号,比较版本号的顺序写出来
1、开始事务
2、读取行数据的当前版本号(version),记为version1
3、将version1加1得到version2,将要写入的新数据的版本号设置为version2,版本号比较,否则回滚
4、执行数据修改操作
5、提交事务
流程就是:读取数据以及版本号,在修改数据之前再次比较数据版本号,如果版本号能对的上那么就是同一个事务在操作这个行数据,直接修改即可,否则需要进行事务的回滚操作。核心就是在同一个事务操作同一个数据期间,其他的事务不能操作数据,为了保证数据的线程安全性。
第三步中会将事务T2读取到的version2与当前最新的行版本进行比较,以此来判断在T2读取数据后,是否有其他事务对这行数据进行了修改。
如果当前最新的行版本号大于T2读取的version2,则说明在T2读取数据后,有其他事务对这行数据进行了修改,此时需要回滚事务T2;反之,则说明在T2读取数据后,这行数据没有被其他事务修改,T2可以继续执行写操作。
MySQL 使用 B+ 树(聚簇索引和非聚簇索引都是B+ 树)
如果 MySQL 要插入十万条数据,有什么较好的方法
JDBC批处理,开启事务,平均每 1.9 秒插入 十万 条数据;
使用 JDBC 的方法、并且开启事务,这样可以使得插入数据的效率变高。
Redis 支持集群的几种方式,Redis 的跳表要实现快速查找第 k 小的元素怎么做
在集群下面,主数据库可能是多个结点的集合,多个结点共同组成了主数据库。
Redis 支持集群的几种方式
Redis 支持三种集群模式:
主从复制模式
集群中设置两类数据库,设置一类主数据库,另外一类是从数据库,由于数据库的备份导致数据的安全性得到了提升。
Sentinel(哨兵)模式:
在上面的主从复制的模式下,当主数据库宕机之后,需要手动的将从数据库切换为主数据库,一定程度上面是比较费时费力的。
哨兵模式存在的前提是在存在主从数据库。
这个时候,开启一个哨兵线程,向所有的数据库服务器发送信息,如果一旦发现主数据库宕机,那么将从数据库变为主数据库,重点是对所有的数据库进行监视,主数据库宕机的时候,选举后一个代替品即可。当然在 Redis 集群中不可能只有一个哨兵,当多个哨兵线程发现主服务器宕机之后,才会将原来的主服务器替换掉,确保一个哨兵可能带来的误判问题。
Cluster 模式
哨兵模式解决了主从复制自动选举新的主服务器,也就是完成了故障的自动迁移,但是不能够自动扩容,使用 Cluster 模式可以轻松的将数据存储在不同的结点上, 方便数据的扩容。
将 Redis 集群存储的数据分为多个结点,每个结点中存储不同的数据,里面使用哈希槽来存储数据,这种分区的结构方便增加以及删除结点。方便了结点的扩容。
注:哨兵模式依赖于主从复制模式,但是Redis Cluster 模式不依赖于主从复制模式和哨兵模式。是一个独立的分区机制,每个分区中存储不同的数据,使得数据的扩展能力得到了提升。
Redis 的跳表要实现快速查找第 k 小的元素怎么做
首先看一下调表这种数据结构,调表的最后一层可以看做是一个链表,在链表上面,有多级索引,通过在多级索引上面查询数据,看起来像是跳跃的查询数据,所以叫做跳表,查询第 k 的小元素,日如果跳表底层数据按照从小到大排序,本质上是查询链表中第 k 个元素。
拆包粘包的实质,了解 zero copy 嘛
拆包粘包的实质
在网络中,客户端想服务端传送的是一个个包,服务器接收到包之后将会进行解析,如果包没有被正确的拆分,那么数据就不能被正确的解析出来,这就发生了粘包现象
零拷贝
谈到零拷贝、需要提到用户访问操作系统磁盘数据时的三层结构:
最下面是硬件层,中间的应用程序的内核态,最上面是应用程序的用户态。
在传统的 IO 中,客户端访问服务器的数据,假设请求服务端的磁盘数据,正常情况下的数据会先从磁盘中读取到应用程序的内核态中缓冲区中,然后内核态中的缓存区数据经过 CPU 拷贝到达用户态的内存缓冲区中,然后在从应用程序缓冲区到内核态的套接字缓冲区中,然后套接字缓冲区再到网卡中把数据发送出去。在上面的描述中,出现了内核态和用户态的多次数据拷贝,造成了数据传输效率的下降。
零拷贝就是想着尽量的减少 CPU 拷贝次数,直接从磁盘到内核的缓冲区到内核的套接字缓冲区,就不去用户态的缓冲区了,这样一来,减少了 CPU 拷贝,使得传输的效率增加了。
减少了中间商,使得数据效传输效率明显提高;
CPU 拷贝,自己将数据从磁盘搬到内存中(用户态和内核态都是在内存中运行的)。
DMA 拷贝,将 CPU 释放出来,使用 DMA 控制器实现,相当于雇人搬运数据到内存中。
DMA
拷贝指的是直接内存访问(Direct Memory Access),是一种无需 CPU 参与的数据传输方式。在 DMA 拷贝中,数据是从磁盘中通过 DMA 控制器直接传输到目标设备的内存中,而 CPU 只需要启动 DMA 控制器即可
简述 TCP 三次握手四次挥手的状态转移,ip 层的 mtu 是什么,如何探测机器之间的 mtu
TCP 三次握手:
客户端向服务器发送 SYN 报文,表示请求建立连接,并设置一个随机的初始序列号 seq。
服务器收到 SYN 报文后,返回 SYN+ACK 报文确认收到请求,并将自己的初始序列号设为 ack=seq+1,同时设置一个随机的序列号 seq1。
客户端收到 SYN+ACK 报文后,发送 ACK 报文确认连接建立,同时将自己的初始序列号设为 ack=seq1+1。
TCP 四次挥手:
客户端向服务器发送 FIN 报文,表示请求关闭连接。
服务器收到 FIN 报文后,返回 ACK 报文确认收到请求,但是还没有准备好关闭连接。
服务器完成所有数据传输后,向客户端发送 FIN 报文,表示可以关闭连接了。
客户端收到 FIN 报文后,返回 ACK 报文确认收到请求,等待一段时间后才真正关闭连接。
需要注意的是,在 TCP 连接过程中可能会出现超时、丢包等情况,需要根据具体情况进行超时重传等操作来保证连接的可靠性和稳定性。
进程和线程的区别,进程的通信方式,线程空间中的内容,进程中打开文件其他线程可以直接读写那个文件嘛。用户态和内核态对于读写文件的操作有什么区别
资源占用不一样
切换消耗资源不一样
通信成本不一样
进程之间是独立的,不需要对共享自己进行并发访问控制,但是线程需要对共享资源进行并发控制
进程是操作系统中分配资源的最小单元
线程是进程中的执行单元
进程
进程之间的通信方式有:
管道:
一个进程在管道中写入数据,另外一个进程从管道中读取数据
消息队列
一个进程向消息队列中放入数据、另外一个进程从消息队列中读取消息
共享内存
允许多个进程访问同一块内存区域,达到进程之间的通讯
内存为什么分页,虚拟内存有什么用,内存中的脏页是由硬件完成还是软件完成
内存分页:一块大的内存是方便管理的,但是分为一个个的数据页,那么就会方便管理一些。
虚拟内存的作用就是允许运行比实际物理内存中更大的软件,一个软件,也就是一个进程,没有必要全部资源加载到内存中,这样同一时间可以打开的软件数量有限,比如一次 100 G 的游戏,不可能全部加载到内存,使用了虚拟内存,可以将有用的部分加载到真实的内存中,一部分放到固态硬盘中,按照需要加载。
脏页的概念
使用虚拟内存时候,因为要将虚拟内存中的数据,也就是磁盘中的数据加载到内存中,为了提升加载数据的效率,一次加载一块数据,这一块数据也叫做一页数据,一般是 4kb 或者 8kb。
当一个进程对一个页块的数据进行操作的时候,如果这个页数据没有被写到磁盘中,那么这个页数据就是脏数据,脏页是由于软件完成的。
怎样用单线程实现 2k QPS 对于服务器压测,1 s 均匀发 2 k 条消息,可以异步返回
在 Java 中可以考虑使用同步非阻塞实现,也就是使用多路复用模式,否则使用 BIO 的话,创建 2k 个线程的成本太高了。
Nginx 和 Redis 能同时处理很多条请求,万级别,是不是都是通过 Linux 中的 IO 模型的的类似信号的机制.像这样的 C10 K问题,也就是单机 10000 条并发连接应该如何去处理
设计高并发秒杀系统,里面的阻塞队列是如何实现的
高并发就是系统能够接受在同一时间大量的请求打到服务器上,服务器能够不宕机。
在高并发,在系统短时间内接收到大量的请求时候,可以考虑使用阻塞队列,当队列中满的时候,让其他的请求先等待,防止直接打到服务器上,保护服务器的正常运行。
使用 Java 封装好的阻塞队列即可,ArrayBlockingQueue 是 Java 中的阻塞队列实现之一。它有多个构造函数,其中一个是
ArrayBlockingQueue(int capacity, boolean fair, Collection<? extends E> c)。其中:
capacity 是队列的容量;
fair 表示是否使用公平策略,即是否采用先进先出(FIFO)的原则来选择等待时间最长的线程,如果是 true 表示使用公平策略,否则表示使用非公平策略;
c 是一个集合,可以在构造函数中将集合中的元素添加到队列中。
这个构造函数的作用是创建一个容量为 capacity 的 ArrayBlockingQueue 对象,并根据 fair 参数选择使用公平或非公平策略,如果 c 不为空,则将集合中的元素添加到队列中。
需要注意的是,当 fair 参数为 true 时,线程获取锁的顺序按照线程进入等待队列的顺序,而当 fair 参数为 false 时,线程获取锁的顺序是不确定的。
多线程访问一个队列,如果是公平的,那么每个线程访问到队列中数据的概率是相同的,反之,不公平的话,有的线程可能一直能获取到数据,但是有的线程可能一直获取不到数据。
如何实现 cookie 和 session 机制
Cookie 和 Session 是用于在浏览器和服务器之间共享数据使用的,主要是用于共享会话数据。
Cookie 是在发送 http 请求的时候跟着 http 请求头一起发送到服务器的,在请求头中有一个 Cookie 字段,里面存储了浏览器要发送到服务器的 Cookie 数据。
Session 是基于Cookie 实现的:
使用 Cookie 第一次访问服务器的时候,服务器会创建一个会话的唯一标识,以后的客户端访问服务器的使用唯一的会话标识符即可。
Cookie 和 Session 的核心功能就是浏览器和服务器之间进行数据传输,维持用户的会话信息
token 在分布式服务器上的应用,用什么框架来验证 token
token 是一种用于身份验证和授权的字符串,里面可以包含一些特殊的信息,比如用户 ID、过期时间等信息,
Cookie 和 Session 是需要再浏览器或者服务器端保存一定的信息的,但是 token 不需要,
在分布式系统中,只要有 token 那么在任何一个结点都是可以身份认成功的。
可以使用 Spring security 进行认证,Spring 结合 JWT 可以提供更加灵活的身份认证服务。
Token 通常分为三个部分:Header、Payload 和 Signature
Header:包含了 Token 的基本信息,例如类型和加密算法等。
Payload:存储了 Token 中要传输的信息,例如用户 ID、过期时间等。
Signature:对 Header 和 Payload 进行签名,保证 Token 的完整性和可信度。
IO 多路复用
多路复用体现在一个监听进程可以同时监听多个 socket ,当某个 socket 中有事件发生的时候作出合理的响应。
http 请求到 socket,一个进程监控多个socket(监控多个服务器端口) socket 调用程序员写的代码,返回数据,是这个操作流程吗
是的,这是一个简化的操作流程。具体来说,当客户端发送一个 HTTP 请求时,它会通过网络到达服务器,服务器上的操作系统内核会将请求分发给监听相应端口的进程或线程。这个进程或线程会接收到请求并处理它,然后生成一个 HTTP 响应并将其发送回客户端的 socket 连接。这个过程中,socket 连接扮演了数据传输的角色,而进程或线程则负责处理请求并生成响应。
socket 中只是有 ip + 端口,已连接 socket 中既有 服务器的 ip + 端口,也有客户端的 ip + 端口
对的,socket 中存储的是一个地址(IP 地址 + 端口号),用来标识一个网络连接端点。而已连接的 socket 是指在 TCP 建立连接之后,操作系统内核为这个连接创建的一个数据结构,包含了本地服务器的地址(IP 地址 + 端口号)以及远程客户端的地址(IP 地址 + 端口号)。
注意,监听的 Socket 和真正用来传数据的 Socket 是两个:
一个叫作监听 Socket;
一个叫作已连接 Socket;
连接建立后,客户端和服务端就开始相互传输数据了,双方都可以通过 read() 和 write() 函数来读写数据。
监听 socket 就是普通 ip + 端口
已连接 socket 是客户端的 ip + 端口和服务器端的 ip + 端口
生活中的例子说明 IO 多路复用
餐厅前台的一个点菜员对接多个客户点菜,客户拿上自己的号,菜好了把菜给对应的人,这个本身就是一种 IO 多路复用思想?
从某种程度上来说,可以把这个情景看作是一种简单的 IO 多路复用思想。点菜员可以看作是选择器,多个客户点菜可以看作是多个 I/O 请求,而菜好了后,对应的客户拿到自己的菜就可以看作是 I/O 操作。通过点菜员这个“选择器”,点菜员可以同时对接多个客户的点菜请求,并将菜品发送给对应的客户,从而实现高效的管理和操作。