一些基础问题小记

基础

资源

资源的关闭

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;

网络

网络层概述

五层模型以及功能
image.png
数据形式
image.png
image.png

TCP

HTTP & HTTPS

Socket

本地的进程该如何通信?
管道 消息队列 共享内存 同步机制(读写锁 信号量)
网络间的进程该如何通信呢?
我们进行网络通信的时候 (TCP为例) 我们需要指定对方的ip 端口号 还要表明我们自己的ip和端口号 然后发数据 . 如果每次都这样操作是不可能的, 因此 操作系统为我们封装好了这些操作 就是Socket, 他是一个操作系统内核维护的对象.
我们的Socket有两种:
image.png
一种是服务端监听的Socket:
他是用来建立连接的 我们new Socket()的时候 操作系统会为我们创建一个监听态的Socket 并返回一个标识(11111)给我们的进程, 我们调用bind()后 OS会把本机的Ip地址和端口号绑定到Socket里. 调用accept()就会拿出监听队列里的 连接Socket
这里的accept()就涉及到了IO模型:

  1. BIO: 我们可以server.accept()阻塞等待返回
  2. NIO: server.accpet()不阻塞 立刻返回结果(有无结果无所谓)
  3. 多路复用器: 查看连接队列里哪一个Socket有事件, 然后有效的去accept()

此Socket中有一个队列 这个队列是用来装那些需要建立连接的Socket对象的 这就意味着: 在一个监听Socket下 我们可以创建非常多的连接Socket(势必呀 一个服务器可以处理多个客户端的请求) 默认最大1024个连接?
所以如果出现Connection Refused错误: 可能就是等待队列满了 当然也可能是服务器端口没打开
image.png
一种是用于连接的Socket:

这种Socket是在三次握手后创建的, 此时他里面有四元组, 操作系统创建好后返回标识给进程使用 如上图(11112) 这个Socket里有两个队列
这两个队列是真正用来收发消息的
接收队列(客户端的消息发送过来会放在这里 应用程序可以从这里读消息)
发送队列(应用程序要发送的消息会放到这里 由网卡发出)
由此我们就知道了 如果应用程序要发送消息 只需要拿着操作系统给他的Socket标识符 然后执行Socket的操作(recv/send)即可

三次握手

image.png

四次挥手

图如上
为什么需要三次握手 四次挥手呢 少了不行吗?

NUMA架构

image.png

假如现在上面两个RAM内存都是32G, 如果现在有一个48G的程序要运行, 就会出现内存分配不均的情况 如下
image.png
这样就会产生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()方法后就进行一次刷盘 防止内存上的数据没写到硬盘去
image.png
来点例子吧
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)
image.png
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(): 压缩缓冲区
image.png
image.png
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()+", ");
    }
}

结果
image.png
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的直接后继节点了。剩下的读者自己想想吧。
}

总结一下吧。
在并发环境下,加锁和解锁需要以下三个部件的协调:

  1. 锁状态。我们要知道锁是不是被别的线程占有了,这个就是 state 的作用,它为 0 的时候代表没有线程占有锁,可以去争抢这个锁,用 CAS 将 state 设为 1,如果 CAS 成功,说明抢到了锁,这样其他线程就抢不到了,如果锁重入的话,state进行 +1 就可以,解锁就是减 1,直到 state 又变为 0,代表释放锁,所以 lock() 和 unlock() 必须要配对啊。然后唤醒等待队列中的第一个线程,让其来占有锁。
  2. 线程的阻塞和解除阻塞。AQS 中采用了 LockSupport.park(thread) 来挂起线程,用 unpark 来唤醒线程。
  3. 阻塞队列。因为争抢锁的线程可能很多,但是只能有一个线程拿到锁,其他的线程都必须等待,这个时候就需要一个 queue 来管理这些线程,AQS 用的是一个 FIFO 的队列,就是一个链表,每个 node 都持有后继节点的引用。AQS 采用了 CLH 锁的变体来实现,感兴趣的读者可以参考这篇文章关于CLH的介绍,写得简单明了。

Codition


一个锁底下可以new多个Condition对象, 某个线程只有获取了锁之后, 才可以调用Condition的await()/signal()方法. 此时线程会阻塞在当前的Condition对象的等待队列中

底层

image.png
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等待队列中,让其参与到锁的竞争中
image.png

posted @   rocketmmm  阅读(16)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
· 三行代码完成国际化适配,妙~啊~
点击右上角即可分享
微信分享提示