Java并发编程的艺术总结笔记

第一章

减少上下文切换:

无锁并发编程,CAS,使用最少线程,协程

避免死锁:

避免一个线程同时获取多个锁,避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源,尝试使用定时锁,数据库锁加锁和解锁要在一个数据库连接里不然的话会解锁失败

 

第二章

volatile:

0x01a3de24:lock add1 $0x0,(esp):lock的前缀指令把当前处理器缓存行的数据写回到系统内存,然后使cpu缓存了该内存地址的数据无效

LOCK#一般锁缓存,一个处理器的缓存回写到内存中会导致其他处理器的缓存无效

一个对象的引用占4个字节

使用追加到64字节的方式来填满高速缓存区的缓存行,避免头结点和尾结点加载到同一个缓存行,使头尾节点在修改时不会互相锁定

普通同步方法:锁是当前实例对象

静态同步方法:锁是当前类的Class对象

同步方法块:锁是synchonized括号里配置的对象

JVM基于进入和退出Monitor对象(监视器锁)来实现方法同步和代码块同步,当且一个monitor对象被持有后,它将处于锁定状态,线程执行到monitorenter指令时,将会获取对象所对应的monitor的所有权,即尝试获取对象的锁

synchronized用的锁是存在Java对象头里的,32位的虚拟机一字宽等于4字节

四种锁的级别由低到高依次是:无锁,偏向锁,轻量级锁,重量级锁,会随着竞争情况逐渐升级,升级后就不能降级

出现竞争才会释放偏向锁

线程尝试使用CAS将对象头中的MarkWord替换成指向锁记录的指针,如果成功当前线程获得锁,失败表示其他线程竞争锁

2.3

缓存行:缓存的最小操作单位

处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性

Java通过锁和循环CAS的方式来实现原子操作

CAS实现原子操作的三个问题:ABA问题,循环时间长开销大,只能保证一个共享变量的原子操作

 

第三章

3.1

线程之间的通信机制:共享内存和消息传递

线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存(本地内存是抽象概念,并不真实存在)

源代码会依次进行编译器优化重排序,指令级并行重排序,内存系统重排序

内存屏障指令来禁止特定类型的处理器重排序

每个处理器上的写缓冲区只对它所在的处理器可见

如果一个操作执行的结果需要对另一个操作可见(可见是重点,顺序无所谓),那么这两个操作之间必须要存在happens-before关系

 

3.2

如果两个操作访问同一个变量,且这两个操作有一个为写操作,这两个操作之间就存在数据依赖性

编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在依赖关系的两个操作的执行顺序

as-if-serial:不管怎么重排序,程序的执行结果不能被改变,为了遵守这个语义,编译器和处理器不会对存在数据依赖关系的操作做重排序

 

3.3

在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见,在这个模型的任意时间点最多只能有一个线程可以连接到内存

JMM不允许临界区内的代码“逸出”到临界区之外,因为会破坏监视器的语义

JMM在不改变程序执行结果的前提下,尽可能地为编译器和处理器的优化打开方便之门

JMM保证线程读操作读取到的值不会无中生有

每次处理器和内存之间的数据传递都是通过一系列步骤来完成的,这些步骤称为总线事务,在一个处理器执行总线事务期间,总线会禁止其他的处理器和IO设备执行内存的读/写,任意时间点最多只能有一个处理器可以访问内存

锁的happens-before规则保证释放锁和获取锁的两个线程之间的内存可见性,意味着对一个volatile变量的读,总是能看到对这个volatile变量最后的写入

volatile的写/读与锁的释放/获取有相同的内存效果

写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存,读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,然后从主内存中读取共享变量

线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息

JMM在实现上的特点:先确保正确性,然后再去追求执行效率(平时做项目也是这样?)

 

3.5

锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息

监视器的作用:监视着临界区,不让其他线程进来

当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中

锁释放与volatile写有相同的内存语义,锁获取与volatile读有相同的内存语义

AQS使用一个整型的volatile变量(state)来维护同步状态,这个变量时ReentrantLock内存语义实现的关键

ReentrantLock公平锁加锁方法lock()调用轨迹:ReentrantLock:lock(),FairSync:lock(),AQS:acquire(),ReentrantLock:tryAcquire(),最后一步才真正加锁

编译器不能对CAS和CAS前面和后面的任意内存操作重排序,CAS同时具有volatile读和volatile写的内存语义

concurrent包源代码实现的一个通用化实现模式:首先,声明共享变量为volatile,然后使用CAS的原子条件更新来实现线程之间的同步,同时配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信

 

3.6

写final域的重排序规则禁止把final域的写重排序到构造函数之外,这个规则可以保证在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了,而普通域不具有这个保障

读final域的重排序规则禁止重排序初次读对象引用和初次读该对象包含的final域,这个规则保证了读一个对象的final域之前,一定会先读包含这个final域的对象的引用

在构造函数内部,不能让这个被构造对象对象的引用为其他线程所见,即对象引用不能在构造函数中逸出,如果逸出了会被其他线程看到还没初始化的final域

this相当于构造对象的引用

在构造函数返回前,被构造对象的引用不能为其他线程所见,因为final域可能还没有被初始化,构造函数返回后,任意线程都将保证能看到final域正确初始化之后的值

同步一般指的是在两个或多个数据库、文件、模块、线程之间用来保持数据内容一致性的机制

只要对象是正确构造的(被构造对象的引用在构造函数中没有“逸出”),那么不需要同步就可以保证任意线程都能看到这个final域在构造函数中被初始化之后的值

 

3.7

JMM要找到的平衡点:一方面为程序员提供足够强的内存可见性保证,另一方面对编译器和处理器的限制要尽可能地放松

只要不改变程序的执行结果,编译器和处理器怎么优化都行

as-if-serial保证单线程内程序的执行结果不被改变,happen-before保证多线程内程序的执行结果不被改变

  • 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。

  • 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。

  • volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。

  • start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。

  • join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。

  • 程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。 对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始。

Thread.join()的作用是等待这个线程终结

需要对实例字段使用线程安全的延迟初始化,使用基于volatile的延时初始化方案,需要对静态字段使用线程安全的延迟初始化,使用基于类初始化的方案

 

3.9

写缓存区可能导致写-读操作重排序

同处理器内存模型一样,越是追求执行性能的语言,内存模型设计得会越弱

最小安全性保证线程读取到的值,要么是之前某个线程写入的值,要么是默认值,最小安全性保证线程读取到的值不会无中生有地冒出来,但不保证线程读取到的值一定是正确的

 

第四章

4.1

[6]Monitor Ctrl-Break [5]Attach Listener [4]Signal Dispatcher //分发处理发送给JVM信号的线程 [3]Finalizer //调用对象finallize方法的线程 [2]Reference Handler //清除Reference的线程 [1]main //main线程入口

使用多线程的原因:更多的处理器核心,更快的响应时间,更好的编程模型

一个线程在一个时刻只能运行在一个处理器核心上

操作系统会分出一个个时间片,线程会分配到若干时间片,当线程的时间片用完了就会发生线程调度,并等待着下次分配

Java程序设置的线程优先级会被操作系统忽略

Thread.yield()的作用是把当前线程使用的处理器核心让给其他线程

 

Daemon线程主要被用作程序中后台调度以及支持性工作。这 意味着,当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出

private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {
    if (name == null) {
        throw new NullPointerException("name cannot be null");
    }
​
    this.name = name;
    //当前线程就是该线程的父线程
    Thread parent = currentThread();
    SecurityManager security = System.getSecurityManager();
    if (g == null) {
        /* Determine if it's an applet or not *//* If there is a security manager, ask the security manager
           what to do. */
        if (security != null) {
            g = security.getThreadGroup();
        }
​
        /* If the security doesn't have a strong opinion of the matter
           use the parent thread group. */
        if (g == null) {
            g = parent.getThreadGroup();
        }
    }
​
    /* checkAccess regardless of whether or not threadgroup is
       explicitly passed in. */
    g.checkAccess();
​
    /*
     * Do we have the required permissions?
     */
    if (security != null) {
        if (isCCLOverridden(getClass())) {
            security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
        }
    }
​
    g.addUnstarted();
​
    this.group = g;
    // 将daemon、priority属性设置为父线程的对应属性
    this.daemon = parent.isDaemon();
    this.priority = parent.getPriority();
    if (security == null || isCCLOverridden(parent.getClass()))
        this.contextClassLoader = parent.getContextClassLoader();
    else
        this.contextClassLoader = parent.contextClassLoader;
    this.inheritedAccessControlContext =
            acc != null ? acc : AccessController.getContext();
    this.target = target;
    setPriority(priority);
    // 将父线程的InheritableThreadLocal复制过来
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    /* Stash the specified stack size in case the VM cares */
    this.stackSize = stackSize;
​
    /* Set thread ID */
    tid = nextThreadID();
}

 

根据父线程来构造一个新的线程

线程start()方法的含义 是:当前线程(即parent线程)同步告知Java虚拟机,只要线程规划器空闲,应立即启动调用 start()方法的线程。

中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行 了中断操作。中断好比其他线程对该线程打了个招呼,其他线程通过调用该线程的interrupt() 方法对其进行中断操作。

线程通过检查自身是否被中断来进行响应

方法在抛出InterruptedException之前,Java虚拟机会先将该线程的中断标识位 清除,然后抛出InterruptedException

一直忙碌运行的线程的中断标识位不会被清除,不停睡眠的线程的中断标识位抛出InterruptedException会被清除

网络协议是通信计算机双方必须共同遵从的一组约定。如怎么样建立连接、怎么样互相识别等。只有遵守这个约定,计算机之间才能相互通信交流

main线程通过中断操作和cancel()方法均可使CountThread得以终止。 这种通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断地 将线程停止,因此这种终止线程的做法显得更加安全和优雅。

Thread.interrupt()的作用是中断当前线程

 

4.3

虽然对象以及成员变量分配的内存是在共享内存中的,但是每个执行的线程还是 可以拥有一份拷贝,这样做的目的是加速程序的执行,这是现代多核处理器的一个显著特

任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用 时,执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法,而没有获 取到监视器(执行该方法)的线程将会被阻塞在同步块和同步方法的入口处,进入BLOCKED 状态

 

 

1)使用wait()、notify()和notifyAll()时需要先对调用对象加锁。 2)调用wait()方法后,线程状态由RUNNING变为WAITING,并将当前线程放置到对象的 等待队列。 3)notify()或notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用notify()或 notifAll()的线程释放锁之后,等待线程才有机会从wait()返回。 4)notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll() 方法则是将等待队列中所有的线程全部移到同步队列,被移动的线程状态由WAITING变为 BLOCKED。 5)从wait()方法返回的前提是获得了调用对象的锁

 

在图4-3中,WaitThread首先获取了对象的锁-ra调用对象的wait()方法,从而放弃了锁 并进入了对象的等待队列WaitQueue中,进入等待状态。由于WaitThread释放了对象的锁, NotifyThread随后获取了对象的锁,并调用对象的notify()方法,将WaitThread从WaitQueue移到 SynchronizedQueue中,此时WaitThread的状态变为阻塞状态。NotifyThread释放了锁之后, WaitThread再次获取到锁并从wait()方法返回继续执行。

等待方遵循如下原则。 1)获取对象的锁。 2)如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件。 3)条件满足则执行对应的逻辑。 对应的伪代码如下。

synchronized(对象) { 
  while(条件不满足) {
  对象.wait();      
}      
  对应的处理逻辑
}

通知方遵循如下原则。 1)获得对象的锁。 2)改变条件。 3)通知所有等待在对象上的线程。 对应的伪代码如下。

synchronized(对象) {  
改变条件      
对象.notifyAll();
}

对于Piped类型的流,必须先要进行绑定,也就是调用connect()方法,如果没有将输入/输 出流绑定起来,对于该流的访问将会抛出异常。

如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才 从thread.join()返回

常用的Java Web服务器, 如Tomcat、Jetty,在其处理请求的过程中都使用到了线程池技术。

 

SimpleHttpServer在建立了与客户端的连接之后,并不会处理客户端的请求, 而是将其包装成HttpRequestHandler并交由线程池处理。在线程池中的Worker处理客户端请求 的同时,SimpleHttpServer能够继续完成后续客户端连接的建立,不会阻塞后续客户端的请求。

 

第5章

不要将获取锁的过程写在try块中,因为如果在获取锁(自定义锁的实现)时发生了异常, 异常抛出的同时,也会导致锁无故释放。

 

Lock接口的实现基本都是 通过聚合了一个同步器的子类来完成线程访问控制的

 

5.2

同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状 态

锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域。

继承同步器然后实现同步器的模板方法,模板方法调用同步器自带的方法

当前线程获取 同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态

 

释放同步状态并且后继结点获取同步状态成功才出队=前驱结点为头结点且获取同步状态成功是出队,获取同步状态成功的是首结点

只有前驱节点是头节点才能够尝试获取同步状态,因为: 第一,头节点是成功获取到同步状态的节点,而头节点的线程释放了同步状态之后,将会 唤醒其后继节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点。 第二,维护同步队列的FIFO原则

由于非首节点线程前驱节点出队或者被中断而从等待状态返回,随后检查自 己的前驱是否是头节点,如果是则尝试获取同步状态

 

共享式和独占式主要区别在于tryReleaseShared(int arg) 方法必须确保同步状态(或者资源数)线程安全释放,一般是通过循环和CAS来保证的,因为 释放同步状态的操作会同时来自多个线程

超时获取同步状态过程可以被视作响应中断获取同步状态过程的“增强版”, doAcquireNanos(int arg,long nanosTimeout)方法在支持响应中断的基础上,增加了超时获取的 特性

private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (nanosTimeout <= 0L)
        return false;
    final long deadline = System.nanoTime() + nanosTimeout;//总等待时间
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return true;
            }
            nanosTimeout = deadline - System.nanoTime();//还可以等待的时间
            //超时,获取失败
            if (nanosTimeout <= 0L)
                return false;
            //获取失败后判断是否超时,不超时就继续等待
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > spinForTimeoutThreshold)
                LockSupport.parkNanos(this, nanosTimeout);
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

 

nanosTimeout非常短的场景下,同步器会进入无条件的快速自旋

park是休眠,unpark是唤醒

 

5.3

如果在绝对时间上,先对锁进行获取的请求一定先被满足,那么这个锁是公平的,反之,是不公平的,公平的获取锁,也就是等待时间最长的线 程最优先获取锁,也可以说锁获取是顺序的

锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取

计数表示当前锁被重复获取的次数,而锁 被释放时,计数自减,当计数等于0时表示锁已经成功释放

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        //判断当前线程是否为获取锁的线程来决定获取操作是否成功,如果是获取锁的线程再次请求,则将同步状态值进行增加并返回 true,表示获取同步状态成功
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}
​
protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
    //同步状态是否为0作为最终释放的条 件,当同步状态为0时,将占有线程设置为null,并返回true,表示释放成功。
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }
​
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;
        }

 

对于非公平锁,只要CAS设置 同步状态成功,则表示当前线程获取了锁

公平性锁每次都是从同步队列中的第一个节点获取到锁,而非公平性锁出现了一个线程连续获取锁的情况

非公平性锁虽然可能造成线程“饥饿”,但极少的线程切换,保证了其更大的吞吐量

获取了读锁,其他获取写锁的线程会被阻塞

读写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状 态,高16位代表读状态,低16位代表写状态

假设当前同步状态 值为S,写状态等于S&0x0000FFFF(将高16位全部抹去),读状态等于S>>>16

S不等于0时,当写状态(S&0x0000FFFF)等于0时,则读 状态(S>>>16)大于0,即读锁已被获取

写状态增加1时,等于S+1,当读状态增加1时,等于S+(1<<16),也就是 S+0x00010000

protected final boolean tryAcquire(int acquires) {
    /*
     * Walkthrough:
     * 1. If read count nonzero or write count nonzero
     *    and owner is a different thread, fail.
     * 2. If count would saturate, fail. (This can only
     *    happen if count is already nonzero.)
     * 3. Otherwise, this thread is eligible for lock if
     *    it is either a reentrant acquire or
     *    queue policy allows it. If so, update state
     *    and set owner.
     */
    Thread current = Thread.currentThread();
    int c = getState();
    int w = exclusiveCount(c);
    if (c != 0) {
        // (Note: if c != 0 and w == 0 then shared count != 0)
        //存在读锁或者当前获取线程不是已经获取写锁的线程
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // Reentrant acquire
        setState(c + acquires);
        return true;
    }
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;
    setExclusiveOwnerThread(current);
    return true;
}

 

在没有其他写线程访问 (或者写状态为0)时,读锁总会被成功地获取

读状态是所有线 程获取读锁次数的总和,而每个线程各自获取读锁的次数只能选择保存在ThreadLocal中,由线程自身维护

锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程

 

Thread.currentThread表示当前代码段正在被哪个线程调用的相关信息

 

5.6

 

Condition对象是由Lock对象(调用Lock对象的newCondition()方法)创 建出来的,换句话说,Condition是依赖Lock对象的。

尾节点引用更新的过程并没有使用CAS保证,原因在于调用 await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的

Lock(更确切地说是同步器)拥有一个同步队列和多个等待队列,两个队列的结点是同一种

private Node addConditionWaiter() {
    Node t = lastWaiter;
    // If lastWaiter is cancelled, clean out.
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;//原有的尾结点nextWaiter指向新增结点
    lastWaiter = node;//更新尾结点
    return node;
}
​
//调用该方法的线程是成功获取了锁的线程,也就是同步队列中的首节点,该方法会将当前
//线程构造成节点并加入等待队列中,然后释放同步状态,唤醒同步队列中的后继节点,然后当
//前线程会进入等待状态。
 public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
        //当前线程加入等待队列
            Node node = addConditionWaiter();
     //释放同步状态,也就是解锁
            int savedState = fullyRelease(node);
            int interruptMode = 0;
     //被唤醒后退出while循环,因为isOnSyncQueue返回true,节点已经在同步队列中
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }
当从await()方法返回时,当前线程一定获取了Condition相关联的锁

当等待队列中的节点被唤醒,则这个被唤醒的节点的线程开始尝试获取同步状态

同步队列的首节点并不会直接加入等待队列,而是通过addConditionWaiter()方 法把当前线程构造成一个新的节点并将其加入等待队列中

调用Condition的signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在 唤醒节点之前,会将节点移到同步队列中

//要获得锁才能调用该方法,没锁的话会抛出IllegalMonitorStateException
public final void signal() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

 

获取等待队列的首节点,将其移动到同步队列并使用LockSupport唤醒节点中的线程,然后这个线程加入到获取同步状态的竞争中,如果获取同步状态成功则从await()方法返回

 

第六章

HashMap在并发执行put操作时会引起死循环,是因为多线程会导致HashMap的Entry链表 形成环形数据结构,一旦形成环形数据结构,Entry的next节点永远不为空,就会产生死循环获 取Entry。

每个HashEntry是一个链表结构的元 素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时, 必须首先获得与它对应的Segment锁

65535转换成二进制是16个1

之所以进行再散列,目的是减少散列冲突,使元素能够均匀地分布在不同的Segment上,

入队主要做两件事情:第一是 将入队节点设置成当前队列尾节点的下一个节点;第二是更新tail节点,如果tail节点的next节 点不为空,则将入队节点设置成tail节点,如果tail节点的next节点为空,则将入队节点设置成 tail的next节点,所以tail节点不总是尾节点

 

第一种情况就是tail结点的next结点为空

public boolean offer(E e) {
    checkNotNull(e);
    //入队前创建一个入队节点
    final Node<E> newNode = new Node<E>(e);
   //创建一个指向tail结点的引用t并进行死循环,入队不成功反复入队
    //p用来表示队列的尾结点,默认情况下等于tail结点
    for (Node<E> t = tail, p = t;;) {
        Node<E> q = p.next;//获取p的下一个节点
        //next结点为空则p是尾结点然后进行插入
        if (q == null) {
            //p是尾结点则cas设置p的next结点为入队节点
            //null表示cas希望看到的值是null,是null的话就把入队节点设置为p的next结点,入队节点变成尾结点
            if (p.casNext(null, newNode)) {
                //p不等于t表示tail节点不是最新的尾结点需要更新,设置新加入的节点为tail节点
                if (p != t) // hop two nodes at a time
                    casTail(t, newNode);  // 更新tail结点允许失败
                return true;
            }
            // Lost CAS race to another thread; re-read next
        }
        //p=q即p结点等于p的next结点,这种情况是两个结点都为空表示队列刚初始化或只添加了第一个         //节点
        //准备添加结点,所以需要返回head节点
        else if (p == q)
          //多线程操作时候,由于poll操作移除元素后有可能会把head变为自引用,然后head的next变为新head,所以这里需要重新找新的head,因为新的head后面的节点才是正常的节点。
            p = (t != (t = tail)) ? t : head;
        //不为空则p不是尾结点
        else
            // Check for tail updates after two hops.
            //p有next结点,表示p的next结点是尾结点,则重新设置p结点
            p = (p != t && t != (t = tail)) ? t : q;
    }
}
入队方法永远返回true,所以不要通过返回值判断入队是否成功

public E poll() {
    restartFromHead:
    for (;;) {
        //p表示头节点,需要出队的节点
        for (Node<E> h = head, p = h, q;;) {
            E item = p.item;//获取p节点的元素
           // 如果p节点的元素不为空,使用CAS设置p节点引用的元素为null,                             // 如果成功则返回p节点的元素。 不成功表示其他线程更新了head节点,要重新获取头节点
            if (item != null && p.casItem(item, null)) {
                // Successful CAS is the linearization point
                // for item to be removed from this queue.
                //p不等于h即p不是头节点
                      if (p != h) // hop two nodes at a time
                    //将p节点的下一个节点设置成head节点
                    updateHead(h, ((q = p.next) != null) ? q : p);
                return item;
            }
             // 如果p的下一个节点也为空,说明这个队列已经空了 
            else if ((q = p.next) == null) {
                updateHead(h, p);//h=p才更新头节点为p
                return null;
            }
            //这种情况头节点被另一个线程修改了然后变成了自引用,即next节点指向了自己,重新获取头节点
            else if (p == q)
                continue restartFromHead;
            // 如果下一个元素不为空,则将头节点的下一个节点设置成头节点 
            else
                p = q;
        }
    }
}
 

 

6.3

队 列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。 只有在延迟期满时才能从队列中提取元素

SynchronousQueue可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费 者线程。队列本身并不存储任何元素,非常适合传递性场景。

JDK使用通知模式实现阻塞队列

pthread_cond_timedwait的使用大致分为几个流程:

  1. 加mutex锁(在pthread_cond_timedwait调用前)

  2. 加cond锁

  3. 释放mutex锁

  4. 修改cond数据

  5. 释放cond锁

  6. 执行futex_wait休眠,如果不释放mutex锁就开始休眠,那其他线程就永远无法调用signal方法将休眠线程唤醒(因为调用signal方法前需要获得mutex锁)

  7. 重新获得cond锁

  8. 比较cond的数据,判断当前线程是被正常唤醒的还是timeout唤醒的,需不需要重新wait

  9. 修改cond数据

  10. 是否cond锁

  11. 重新获得mutex锁

  12. 释放mutex锁(在pthread_cond_timedwait调用后)

    加 mutex锁的原因:保证wait和其wait条件的原子性,保证进行线程休眠时,条件变量是没有被篡改的

    cond锁的作用: 保证对象cond->data的线程安全

6.4

工作窃取(work-stealing)算法是指某个线程从其他队列里窃取任务来执行,主要是为了不让线程空闲下来充分利用资源

为了减少窃取任务线程和被 窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿 任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行

子任务执行完的结果都统一放在一个队列里,启动一个线程从队列里拿数据,然后合并这些数据

 

第九章

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
​
    int c = ctl.get();
    //线程数小于核心线程数则创建一个worker线程
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    else if (!addWorker(command, false))
        reject(command);
}

 

 

第十章

从JDK 5开始,把工作单元与执行机制分离开 来。工作单元包括Runnable和Callable,而执行机制由Executor框架提供

 

如果执行ExecutorService.submit(…),ExecutorService将返回一个实现Future接口的对象

SingleThreadExecutor适用于需要保证顺序地执行各个任务

FixThreadPool使用的是无界队列LinkedBlockingQueue,所以maxPoolSize和keepAliveTime参数没用

CachedThreadPool使用没有容量的SynchronousQueue作为线程池的工作队列

ScheduledThreadPoolExecutor使用DelayQueue

DelayQueue封装了一个PriorityQueue,这个PriorityQueue会对队列中的ScheduledFutureTask进行排序。排序时,time小的排在前面(时间早的任务将被先执行)。如果两个 ScheduledFutureTask的time相同,就比较sequenceNumber,sequenceNumber小的排在前面(也就 是说,如果两个任务的执行时间相同,那么先提交的任务将被先执行)。

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    //获取锁
    lock.lockInterruptibly();
    try {
        for (;;) {
            E first = q.peek();
            //如果Priority为空,当前线程到Condition等待
            if (first == null)
                available.await();
            else {
                long delay = first.getDelay(NANOSECONDS);
                //等够了就出队
                if (delay <= 0)
                    return q.poll();
                first = null; // don't retain ref while waiting
                if (leader != null)
                    available.await();
                else {
                    Thread thisThread = Thread.currentThread();
                    leader = thisThread;
                    try {
                        available.awaitNanos(delay);
                    } finally {
                        if (leader == thisThread)
                            leader = null;
                    }
                }
            }
        }
    } finally {
        //Priority不为空则唤醒在Condition中等待的线程
        if (leader == null && q.peek() != null)
            available.signal();
        lock.unlock();
    }
}

 

 

FutureTask所有的的公有方法都直接委托给了内部私有的Sync

当 某个线程执行FutureTask.run()方法或FutureTask.cancel(...)方法时,会唤醒线程等待队列的第一 个线程

 

第十一章

·第一种情况,某个线程CPU利用率一直100%,则说明是这个线程有可能有死循环,那么 请记住这个PID。 ·第二种情况,某个线程一直在TOP 10的位置,这说明这个线程可能有性能问题。 ·第三种情况,CPU利用率高的几个线程在不停变化,说明并不是由某一个线程导致CPU 偏高。

posted @ 2021-03-31 22:37  hanabivvv  阅读(119)  评论(0编辑  收藏  举报