一些基础问题小记
基础
资源
资源的关闭
java.io.Closeable接口
这个接口里仅仅声明了一个close()方法 用于关闭资源. 不用又怎样呢? 其实是多态 如下
//第一段代码
static void copy(String src, String dest)throws IOException {
InputStream in = null;
OutputStream out = null;
try {
in = new FileInputStream(src);
out = new FileOutputStream(dest);
byte[] buf = new byte[1024];
int n;
while ((n = in.read(buf)) >= 0) {
out.write(buf, 0, n);
}
} finally {
if (in != null) in.close();
if (out != null) out.close();
}
}
这种代码的麻烦点在于 finally快中的close()方法可能也会抛出异常, 如果in.close()抛出异常了没有处理 那out.close就执行不了了 所以你要继续写trycatch 如果有了Closeable接口 就能像下面这样写(利用多态)
//第二段代码
// 对第一段代码中的finally语句改造如下
finally {
closeIgnoringIOException(in);
closeIgnoringIOException(out);
}
private static void closeIgnoringIOException(Closeable c) {
if (c != null) {
try {
c.close();
} catch (IOException ex) { }
}
}
然而这样写还是不够好(要自己手动关资源) 所以有了
try-with-resources
int a = 0;
String inputPath = null;
String outPath = "C:\\sb\\lastNew2.mp4";
try (FileInputStream fileInputStream = new FileInputStream(inputPath); FileOutputStream fileOutputStream = new FileOutputStream(outPath);)
{ //使用资源
while((a = fileInputStream.read()) != -1){
fileOutputStream.write(a);
}
} catch (IOException e) {
System.out.println("八嘎");
// e.printStackTrace();
}
在使用try-with-resources创建的资源无论是否抛出异常,JVM都会自动调用close 方法进行资源释放 无需手动关闭资源
注意:使用try-with-resources结构 只是保证你try()中的资源一定会关闭, 但如果你try()中的语句出现了异常 仍然是要走下面catch块中的内容的 比如上面的例子就会输出‘八嘎’。出现异常时资源关闭顺序是: 先执行所有资源(try的()中声明的)的close方法,然后在执行catch里面的代码,然后才是finally;
网络
网络层概述
五层模型以及功能
数据形式
TCP
HTTP & HTTPS
Socket
本地的进程该如何通信?
管道 消息队列 共享内存 同步机制(读写锁 信号量)
网络间的进程该如何通信呢?
我们进行网络通信的时候 (TCP为例) 我们需要指定对方的ip 端口号 还要表明我们自己的ip和端口号 然后发数据 . 如果每次都这样操作是不可能的, 因此 操作系统为我们封装好了这些操作 就是Socket, 他是一个操作系统内核维护的对象.
我们的Socket有两种:
一种是服务端监听的Socket:
他是用来建立连接的 我们new Socket()的时候 操作系统会为我们创建一个监听态的Socket 并返回一个标识(11111)给我们的进程, 我们调用bind()后 OS会把本机的Ip地址和端口号绑定到Socket里. 调用accept()就会拿出监听队列里的 连接Socket
这里的accept()就涉及到了IO模型:
- BIO: 我们可以server.accept()阻塞等待返回
- NIO: server.accpet()不阻塞 立刻返回结果(有无结果无所谓)
- 多路复用器: 查看连接队列里哪一个Socket有事件, 然后有效的去accept()
此Socket中有一个队列 这个队列是用来装那些需要建立连接的Socket对象的 这就意味着: 在一个监听Socket下 我们可以创建非常多的连接Socket(势必呀 一个服务器可以处理多个客户端的请求) 默认最大1024个连接?
所以如果出现Connection Refused错误: 可能就是等待队列满了 当然也可能是服务器端口没打开
一种是用于连接的Socket:
这种Socket是在三次握手后创建的, 此时他里面有四元组, 操作系统创建好后返回标识给进程使用 如上图(11112) 这个Socket里有两个队列
这两个队列是真正用来收发消息的
接收队列(客户端的消息发送过来会放在这里 应用程序可以从这里读消息)
发送队列(应用程序要发送的消息会放到这里 由网卡发出)
由此我们就知道了 如果应用程序要发送消息 只需要拿着操作系统给他的Socket标识符 然后执行Socket的操作(recv/send)即可
三次握手
四次挥手
NUMA架构
假如现在上面两个RAM内存都是32G, 如果现在有一个48G的程序要运行, 就会出现内存分配不均的情况 如下
这样就会产生swap 即现在在Node0里拿数据, 如果现在要去node1里拿数据 就会跨node读取。 速度会慢
怎么解决呢?
使用 interleaved=all
零拷贝
JAVA网络编程
计算机进行网络通信的时候会由封装出的socket对象(底层C++)来进行, 通信双方会保留彼此的信息.
java也封装了socket, 总的来说就是
服务端的ServerSocket
ServerSocket() server = new ServerSocket(8080); //服务端在8080端口创建一个server等待别人连接
Socket socket = server.accept(); //服务端等待连接..... 阻塞的
客户端的Socket
Socket Socket = new Socket("localhost", 8080); //连接本机的8080端口
通信就是通过这两个Socket来进行的 总的来说就是
对于这两个Socket(), 我们可以从他们中获取InputStream/OutputStream
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();
然后我们就可以通过这两个流来进行数据收发(通信) 我们向os流里写东西, 其实就是像socket里写 就是把数据发出去 同样 接收端从is流里读数据 就相当于从socket里读数据 这样就完成了通信.
文件收发如何进行呢?
IO流 & NIO
BIO
关于io操作 需要程序调用操作系统, 操作系统去硬件/内存拿资源. (系统调用)
而java的io是一个由低效到高效的 用装饰器模式修饰的系统.
效率从低到高依次为: OutputStream(读字节) --- OutputStreamWriter(读字符) --- BufferedWriter(实现一个缓冲区 一次读/写多一点内容再调用IO 有效减少IO次数)
Reader/Writer 用于读写字符. 计算机底层操作的都是字节, 所以Reader/Writer相当于构建了逻辑层和底层的桥梁. 适用于读写文本文件
分类
Buffered相当于最高级的实现 多了个缓冲区的功能
所以 这三者是一层套一层的 比如你要实现BufferedReader 就要
new BufferedReader(new InputStreamReader(new InputStream()));
**flush()操作: **
我们在调用完OutputStream的write()方法以后, 需要调用flush()操作来刷盘. 因为我们的写操作并不会直接把数据写到硬盘里, 而是写到内存里 在合适的时机会刷盘到硬盘(比如调用关闭资源的close()方法, close方法在关流之前就会强制刷盘)
因此我们每执行完wirte()方法后就进行一次刷盘 防止内存上的数据没写到硬盘去
来点例子吧
InputStream
//FileInputStream FileOutputStream 复制MP4文件
int a = 0;
String inputPath = "C:\\sb\\last.mp4";
String outPath = "C:\\sb\\lastNew2.mp4";
try (FileInputStream fileInputStream = new FileInputStream(inputPath); FileOutputStream fileOutputStream = new FileOutputStream(outPath);)
{
while((a = fileInputStream.read()) != -1){
fileOutputStream.write(a);
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
BufferedInputStream
//BufferedInputStream BufferedOutputStream复制MP4(比上面快很多)
try ( BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(inputPath));
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(outPath));)
{
byte[] b = new byte[1024];
int off = 0;
while((off = bufferedInputStream.read(b)) != -1){
bufferedOutputStream.write(b, 0, off);
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
Java IO是阻塞的,如果在一次读写数据调用时 数据还没有准备好,或者目前不可写,那么读写 操作就会 被阻塞直到数据准备好或目标可写为止。
而NIO则是非阻塞的, 每一次读/写调用 都会立即返回结果, 并将目前可读/可写的内容 从缓冲区输出/写入缓冲区. (即使当前没有数据 也会立即返回)
NIO核心(一) 缓冲区Buffer/
类似于数组
Buffer: 缓冲区的父类 里面包含 当前的位置(position)、容量 (capacity)、最大限制 (limit)、标记 (mark)等
IntBuffer:定义了存放数据的数组(只有堆缓冲区实现子类才会用到, 其实就是堆里的一个数组) , 是否只读等
HeapIntBuffer: 将父类定义的操作在这里具体实现了
缓冲区的 读/写 操作
写操作原理:
put(int i): 先写入数组, position指针后移(hb[position++] = i)
put(int index, int i): 写入数组指定位置 不动position指针.(hb[index] = i)
put(int[] src): 从position位置开始插入 将数组src中全部元素依次写入缓冲区
put(int[] src, int offset, int length): 从position位置开始插入 将数组src中 offest->length 中的元素依次写入缓冲区
flip()函数:
下面代码为何错?
public static void main(String[] args) {
IntBuffer src = IntBuffer.allocate(5);
for (int i = 0; i < 5; i++) src.put(i); //手动插入数据
IntBuffer buffer = IntBuffer.allocate(10);
buffer.put(src);
System.out.println(Arrays.toString(buffer.array()));
}
错因: 你在src中put了5个数 此时position处于5, 而limit也是5, 执行到第六6的时候, 要从src里取数据, 就发现position和limit指向一处 代表没数据可用了 就会报错
所以所以 一般我们在写入完成后需要进行读操作时 都要先进行一次filp()!!!!! flip()函数会刷新limit position mark的值!!!
public final Buffer flip() {
limit = position; //修改limit值,当前写到哪里,下次读的最终位置就是这里,limit的作用开始慢慢体现了
position = 0; //position归零
mark = -1; //标记还原为-1
return this;
}
读操作原理:
int get(); 直接获取position位置的数据, 然后position++(return hb[position++])
get(int index); 获取hb[index] 不会动position
get(int[] dst); 从position位置开始读取, 把数据写到dst中.(dst[i] = get())
get(int[] dst, int offset, int length); 同上, 但从是写入dst的offset到 length
mark() && reset():
mark()会记录当前的position: mark = position;
reset()会将position改为mark的值: position = mark;
public static void main(String[] args) {
IntBuffer buffer = IntBuffer.wrap(new int[]{1, 2, 3, 4, 5});
buffer.get();
buffer.mark();
buffer.get();
buffer.reset();
System.out.println(buffer.get());
//输出2
}
缓存区的其他操作
compact(): 压缩缓冲区
duplicate(): 复制缓冲区 (注意底层用的是同一个数组!!!!!)
public IntBuffer duplicate() {
return new HeapIntBuffer(hb, // 用的还是以前的hb数组
this.markValue(),
this.position(),
this.limit(),
this.capacity(),
offset);
}
slice(): 划分缓冲区
两点1. 划分的时候 用的还是以前的数组 2. 设置新的offset值 = 当前position + 当前offset, position = 0, mark = -1;
public IntBuffer slice() {
int pos = this.position(); //获取当前position
int lim = this.limit(); //获取position最大位置
int rem = (pos <= lim ? lim - pos : 0); //求得剩余空间
return new HeapIntBuffer(hb, //返回一个新的划分出的缓冲区,但是底层的数组用的还是同一个
-1,
0,
rem, //新的容量变成了剩余空间的大小
rem,
pos + offset); //可以看到offset的地址不再是0了,而是当前的position加上原有的offset值
}
举例:
public static void main(String[] args) {
IntBuffer buffer = IntBuffer.wrap(new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 0});
for (int i = 0; i < 4; i++) buffer.get();
IntBuffer slice = buffer.slice();
System.out.println("划分之后的情况:"+Arrays.toString(slice.array()));
System.out.println("划分之后的偏移地址:"+slice.arrayOffset());
System.out.println("当前position位置:"+slice.position());
System.out.println("当前limit位置:"+slice.limit());
while (slice.hasRemaining()) { //将所有的数据全部挨着打印出来
System.out.print(slice.get()+", ");
}
}
结果
rewind(): 重置缓冲区p&m
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
clear(): 整个缓冲区回归为最初
public final Buffer clear() {
position = 0; //同上
limit = capacity; //limit变回capacity
mark = -1;
return this;
}
equals() & compareTo()
equals比较的是剩下所有内容 只有一模一样才返回true
compareTo比较的是 Min(剩余)
asReadOnlyBuffer如何实现?
将只读参数isReadOnly设置为ture, put里直接返回异常即可
直接内存
public static void main(String[] args) {
//这里我们申请一个直接缓冲区
ByteBuffer buffer = ByteBuffer.allocateDirect(10);
//使用方式基本和之前是一样的
buffer.put((byte) 66);
buffer.flip();
System.out.println(buffer.get());
}
直接内存 其实就是调用Unsafe类来申请一个堆外内存使用, get put的时候都要调Unsafe的方法 速度比普通内存块. 关于直接内存的GC 我们在创建好相关类的时候 , 有一个线程会进行clear 他里面会一直循环 检查没有用的 直接内存并释放
NIO核心(二) 通道Channel
我们可以通过FileInputStream.getChannel()来获取通道 但这样的通道还是单向的
FileInputStream in = new FileInputStream("test.txt");
//但是这里的通道只支持读操作
FileChannel channel = in.getChannel();
FileOutputStream out = new FileOutputStream("test.txt");
//但是这里的通道只支持写操作
FileChannel channel = out.getChannel();
而我们通道的目的是即可以读 又可以写的双向通道 该如何获取呢?
RandomAccessFile类则是支持读写的 在创建时指定好即可
// r 以只读的方式使用
// rw 读操作和写操作都可以
// rws 每当进行写操作,同步的刷新到磁盘,刷新内容和元数据
RandomAccessFile f = new RandomAccessFile("test.txt", "rw");
FileChannel channel = f.getChannel()); //通过RandomAccessFile创建一个通道
// 此时这个channel就是一个既可以读 又可以写的channel
通过通道去复制文件是非常方便的
public static void main(String[] args) throws IOException {
try(FileOutputStream out = new FileOutputStream("test2.txt");
FileInputStream in = new FileInputStream("test.txt")){
FileChannel inChannel = in.getChannel(); //获取到test文件的通道
inChannel.transferTo(0, inChannel.size(), out.getChannel()); //直接将test文件通道中的数据转到test2文件的通道中
}
}
多线程
中断
Thread.interrupt 的作用其实也不是中断线程,而是「通知线程应该中断了」,
具体到底中断还是继续运行,应该由被通知的线程自己处理。
具体来说,当对一个线程,调用 interrupt() 时,
① 如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。仅此而已。(所以一些io操作函数中都可能会抛出InterruptedException异常防止阻塞)
② 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,仅此而已。被设置中断标志的线程将继续正常运行,不受影响。
interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行。
也就是说,一个线程如果有被中断的需求,那么就可以这样做。
① 在正常运行任务时,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程。
② 在调用阻塞方法时正确处理InterruptedException异常。(例如,catch异常后就结束线程。)
AQS
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
// 争锁
final void lock() {
acquire(1);
}
// 来自父类AQS,我直接贴过来这边,下面分析的时候同样会这样做,不会给读者带来阅读压力
// 我们看到,这个方法,如果tryAcquire(arg) 返回true, 也就结束了。
// 否则,acquireQueued方法会将线程压到队列中
public final void acquire(int arg) { // 此时 arg == 1
// 首先调用tryAcquire(1)一下,名字上就知道,这个只是试一试
// 因为有可能直接就成功了呢,也就不需要进队列排队了,
// 对于公平锁的语义就是:本来就没人持有锁,根本没必要进队列等待(又是挂起,又是等待被唤醒的)
if (!tryAcquire(arg) &&
// tryAcquire(arg)没有成功,这个时候需要把当前线程挂起,放到阻塞队列中。
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
selfInterrupt();
}
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
// 尝试直接获取锁,返回值是boolean,代表是否获取到锁
// 返回true:1.没有线程在等待锁;2.重入锁,线程本来就持有锁,也就可以理所当然可以直接获取
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// state == 0 此时此刻没有线程持有锁
if (c == 0) {
// 虽然此时此刻锁是可以用的,但是这是公平锁,既然是公平,就得讲究先来后到,
// 看看有没有别人在队列中等了半天了
if (!hasQueuedPredecessors() &&
// 如果没有线程在等待,那就用CAS尝试一下,成功了就获取到锁了,
// 不成功的话,只能说明一个问题,就在刚刚几乎同一时刻有个线程抢先了 =_=
// 因为刚刚还没人的,我判断过了
compareAndSetState(0, acquires)) {
// 到这里就是获取到锁了,标记一下,告诉大家,现在是我占用了锁
setExclusiveOwnerThread(current);
return true;
}
}
// 会进入这个else if分支,说明是重入了,需要操作:state=state+1
// 这里不存在并发问题
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
//超过int上限了。。。
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// 如果到这里,说明前面的if和else if都没有返回true,说明没有获取到锁
// 回到上面一个外层调用方法继续看:
// if (!tryAcquire(arg)
// && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// selfInterrupt();
return false;
}
// 假设tryAcquire(arg) 返回false,那么代码将执行:
// acquireQueued(addWaiter(Node.EXCLUSIVE), arg),
// 这个方法,首先需要执行:addWaiter(Node.EXCLUSIVE)
/**
* Creates and enqueues node for current thread and given mode.
*
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
// 此方法的作用是把线程包装成node,同时进入到队列中
// 参数mode此时是Node.EXCLUSIVE,代表独占模式
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
// 以下几行代码想把当前node加到链表的最后面去,也就是进到阻塞队列的最后
Node pred = tail;
// tail!=null => 队列不为空(tail==head的时候,其实队列是空的,不过不管这个吧)
if (pred != null) {
// 将当前的队尾节点,设置为自己的前驱
node.prev = pred;
// 用CAS把自己设置为队尾, 如果成功后,tail == node 了,这个节点成为阻塞队列新的尾巴
if (compareAndSetTail(pred, node)) {
// 进到这里说明设置成功,当前node==tail, 将自己与之前的队尾相连,
// 上面已经有 node.prev = pred,加上下面这句,也就实现了和之前的尾节点双向连接了
pred.next = node;
// 线程入队了,可以返回了
return node;
}
}
// 仔细看看上面的代码,如果会到这里,
// 说明 pred==null(队列是空的) 或者 CAS失败(有线程在竞争入队)
// 读者一定要跟上思路,如果没有跟上,建议先不要往下读了,往回仔细看,否则会浪费时间的
enq(node);
return node;
}
/**
* Inserts node into queue, initializing if necessary. See picture above.
* @param node the node to insert
* @return node's predecessor
*/
// 采用自旋的方式入队
// 之前说过,到这个方法只有两种可能:等待队列为空,或者有线程竞争入队,
// 自旋在这边的语义是:CAS设置tail过程中,竞争一次竞争不到,我就多次竞争,总会排到的
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 之前说过,队列为空也会进来这里
if (t == null) { // Must initialize
// 初始化head节点
// 细心的读者会知道原来 head 和 tail 初始化的时候都是 null 的
// 还是一步CAS,你懂的,现在可能是很多线程同时进来呢
if (compareAndSetHead(new Node()))
// 给后面用:这个时候head节点的waitStatus==0, 看new Node()构造方法就知道了
// 这个时候有了head,但是tail还是null,设置一下,
// 把tail指向head,放心,马上就有线程要来了,到时候tail就要被抢了
// 注意:这里只是设置了tail=head,这里可没return哦,没有return,没有return
// 所以,设置完了以后,继续for循环,下次就到下面的else分支了
tail = head;
} else {
// 下面几行,和上一个方法 addWaiter 是一样的,
// 只是这个套在无限循环里,反正就是将当前线程排到队尾,有线程竞争的话排不上重复排
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
// 现在,又回到这段代码了
// if (!tryAcquire(arg)
// && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// selfInterrupt();
// 下面这个方法,参数node,经过addWaiter(Node.EXCLUSIVE),此时已经进入阻塞队列
// 注意一下:如果acquireQueued(addWaiter(Node.EXCLUSIVE), arg))返回true的话,
// 意味着上面这段代码将进入selfInterrupt(),所以正常情况下,下面应该返回false
// 这个方法非常重要,应该说真正的线程挂起,然后被唤醒后去获取锁,都在这个方法里了
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
// p == head 说明当前节点虽然进到了阻塞队列,但是是阻塞队列的第一个,因为它的前驱是head
// 注意,阻塞队列不包含head节点,head一般指的是占有锁的线程,head后面的才称为阻塞队列
// 所以当前节点可以去试抢一下锁
// 这里我们说一下,为什么可以去试试:
// 首先,它是队头,这个是第一个条件,其次,当前的head有可能是刚刚初始化的node,
// enq(node) 方法里面有提到,head是延时初始化的,而且new Node()的时候没有设置任何线程
// 也就是说,当前的head不属于任何一个线程,所以作为队头,可以去试一试,
// tryAcquire已经分析过了, 忘记了请往前看一下,就是简单用CAS试操作一下state
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 到这里,说明上面的if分支没有成功,要么当前node本来就不是队头,
// 要么就是tryAcquire(arg)没有抢赢别人,继续往下看
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 什么时候 failed 会为 true???
// tryAcquire() 方法抛异常的情况
if (failed)
cancelAcquire(node);
}
}
/**
* Checks and updates status for a node that failed to acquire.
* Returns true if thread should block. This is the main signal
* control in all acquire loops. Requires that pred == node.prev
*
* @param pred node's predecessor holding status
* @param node the node
* @return {@code true} if thread should block
*/
// 刚刚说过,会到这里就是没有抢到锁呗,这个方法说的是:"当前线程没有抢到锁,是否需要挂起当前线程?"
// 第一个参数是前驱节点,第二个参数才是代表当前线程的节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
// 前驱节点的 waitStatus == -1 ,说明前驱节点状态正常,当前线程需要挂起,直接可以返回true
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
// 前驱节点 waitStatus大于0 ,之前说过,大于0 说明前驱节点取消了排队。
// 这里需要知道这点:进入阻塞队列排队的线程会被挂起,而唤醒的操作是由前驱节点完成的。
// 所以下面这块代码说的是将当前节点的prev指向waitStatus<=0的节点,
// 简单说,就是为了找个好爹,因为你还得依赖它来唤醒呢,如果前驱节点取消了排队,
// 找前驱节点的前驱节点做爹,往前遍历总能找到一个好爹的
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
// 仔细想想,如果进入到这个分支意味着什么
// 前驱节点的waitStatus不等于-1和1,那也就是只可能是0,-2,-3
// 在我们前面的源码中,都没有看到有设置waitStatus的,所以每个新的node入队时,waitStatu都是0
// 正常情况下,前驱节点是之前的 tail,那么它的 waitStatus 应该是 0
// 用CAS将前驱节点的waitStatus设置为Node.SIGNAL(也就是-1)
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
// 这个方法返回 false,那么会再走一次 for 循序,
// 然后再次进来此方法,此时会从第一个分支返回 true
return false;
}
// private static boolean shouldParkAfterFailedAcquire(Node pred, Node node)
// 这个方法结束根据返回值我们简单分析下:
// 如果返回true, 说明前驱节点的waitStatus==-1,是正常情况,那么当前线程需要被挂起,等待以后被唤醒
// 我们也说过,以后是被前驱节点唤醒,就等着前驱节点拿到锁,然后释放锁的时候叫你好了
// 如果返回false, 说明当前不需要被挂起,为什么呢?往后看
// 跳回到前面是这个方法
// if (shouldParkAfterFailedAcquire(p, node) &&
// parkAndCheckInterrupt())
// interrupted = true;
// 1. 如果shouldParkAfterFailedAcquire(p, node)返回true,
// 那么需要执行parkAndCheckInterrupt():
// 这个方法很简单,因为前面返回true,所以需要挂起线程,这个方法就是负责挂起线程的
// 这里用了LockSupport.park(this)来挂起线程,然后就停在这里了,等待被唤醒=======
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
// 2. 接下来说说如果shouldParkAfterFailedAcquire(p, node)返回false的情况
// 仔细看shouldParkAfterFailedAcquire(p, node),我们可以发现,其实第一次进来的时候,一般都不会返回true的,原因很简单,前驱节点的waitStatus=-1是依赖于后继节点设置的。也就是说,我都还没给前驱设置-1呢,怎么可能是true呢,但是要看到,这个方法是套在循环里的,所以第二次进来的时候状态就是-1了。
// 解释下为什么shouldParkAfterFailedAcquire(p, node)返回false的时候不直接挂起线程:
// => 是为了应对在经过这个方法后,node已经是head的直接后继节点了。剩下的读者自己想想吧。
}
总结一下吧。
在并发环境下,加锁和解锁需要以下三个部件的协调:
- 锁状态。我们要知道锁是不是被别的线程占有了,这个就是 state 的作用,它为 0 的时候代表没有线程占有锁,可以去争抢这个锁,用 CAS 将 state 设为 1,如果 CAS 成功,说明抢到了锁,这样其他线程就抢不到了,如果锁重入的话,state进行 +1 就可以,解锁就是减 1,直到 state 又变为 0,代表释放锁,所以 lock() 和 unlock() 必须要配对啊。然后唤醒等待队列中的第一个线程,让其来占有锁。
- 线程的阻塞和解除阻塞。AQS 中采用了 LockSupport.park(thread) 来挂起线程,用 unpark 来唤醒线程。
- 阻塞队列。因为争抢锁的线程可能很多,但是只能有一个线程拿到锁,其他的线程都必须等待,这个时候就需要一个 queue 来管理这些线程,AQS 用的是一个 FIFO 的队列,就是一个链表,每个 node 都持有后继节点的引用。AQS 采用了 CLH 锁的变体来实现,感兴趣的读者可以参考这篇文章关于CLH的介绍,写得简单明了。
Codition
一个锁底下可以new多个Condition对象, 某个线程只有获取了锁之后, 才可以调用Condition的await()/signal()方法. 此时线程会阻塞在当前的Condition对象的等待队列中
底层
ReentrantLock类中有一个方法 是newCondition 如下
//ReentrantLock类 实现了Lock接口
public class ReentrantLock implements Lock, java.io.Serializable {
//无参构造方法 默认是 非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
//传入boolean true则创建公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
//在锁的基础上创建一个Condition 用来执行wait()/singal()方法
public Condition newCondition() {
return sync.newCondition();
}
//Syn类 继承了AQS里的操作
abstract static class Sync extends AbstractQueuedSynchronizer {
//ReentrantLock.newCondition()方法时候会调用此方法
final ConditionObject newCondition() {
return new ConditionObject();
//这里的ConditionObject是在AQS类中实现的
}
}
//非公平锁的Sync
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
//公平锁的Sync
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
}
public abstract class AbstractQueuedSynchronizer{
//Condition的具体实现
public class ConditionObject implements Condition, java.io.Serializable {
private static final long serialVersionUID = 1173984872572414699L;
/**条件队列的头结点*/
private transient Node firstWaiter;
/**条件队列的尾结点*/
private transient Node lastWaiter;
public final void await() throws InterruptedException {
}
public final void signal() {
}
}
await方法的本质:释放当前线程持有的锁(),将线程包装为Node节点, 加入到wait队列中。然后 TODO
singan()的本质: 唤醒操作本质上是将条件队列中的结点直接丢进AQS等待队列中,让其参与到锁的竞争中
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
· 三行代码完成国际化适配,妙~啊~