JAVA并发编程的艺术 -- 收获

《第一章:并发编程的挑战》

2019.9.9

1.如果通过多线程执行任务来让程序运行的很快会遇到很多问题,比如上下文切换、死锁、以及受限于软硬件的资源限制问题。

2.单核处理器也支持多线程的执行代码,CPU通过给每个线程分配时间片来实现,因为时间片比较短(一般几十毫秒),所以CPU通过不断地切换线程,让我们感觉多个线程同时执行。

3.CPU在切换前,会保存上一个任务的状态,以便下次切回这个任务时,可以再加载这个任务的状态,所以任务从保存到再加载这个过程就是一次上下文切换。上下文切换会影响线程的执行速度。

2019.9.12

1.减少上下文切换的方法:

  1. 无锁并发编程:多线程竞争锁时会引起上下文切换,所以多线程处理数据时,可以使用一些方法来避免使用锁。比如将数据的id按照hash算法取模分段,不同的线程处理不同段的数据。
  2. CAS算法:java的Atomic包使用CAS算法来更新数据,而不需要加锁。
  3. 使用最少线程:避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样造成大量线程都处于等待状态。
  4. 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务的切换。

2.如何避免死锁:

  1. 避免一个线程同时获取多个锁。
  2. 避免一个线程在锁内占用多个资源,尽量保证一个锁只占用一个一个资源。
  3. 尝试使用定时锁,用lock.tryLock(timeout)来替代使用内部锁机制。
  4. 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。

3.什么是资源限制:资源限制指的是在进行并发编程时,程序的执行速度受限于计算机的硬件资源或软件资源。硬件资源有,带宽的上传、下载速度,磁盘的读写速度,CPU处理速度等;软件资源有,数据库的连接数,socket的连接数。

4.资源限制引发的问题:

  在并发编程中,将代码执行速度加快的原则是:将代码中串行执行的部分改为并发执行,但如果由于资源限制,代码仍在串行执行,这是程序不但不会加快执行速度,反而会更慢,因为增加了上下文切换和资源调度时间。

5.如何解决资源限制:

对于硬件资源限制,可以考虑使用集群并发执行程序。既然单机的资源有限,那么就让程序在多机上运行。比如使用ODPS、Hadoop或自己搭建服务器集群,不同的机器处理不同的数据。可以使用“数据id%机器数“,计算得到机器编号,然后由对应编号的机器去处理数据。

对于软件资源限制,可以考虑使用资源池将资源复用,比如使用连接池将数据库连接和socket连接复用,或者在调用对方WebService时,只建立一个连接。

6.如何在资源限制情况下进行并发编程:

根据不同的资源限制调整并发度,比如下载文件程序依赖与两个资源(带宽和硬盘的读写速度),有数据库连接时涉及到数据库连接数,如果SQL执行非常快,而线程的数量比数据库数量大很多,则某些线程会阻塞,等待数据库连接。

本章小结: 并发程序写的不严谨,如果出现问题,定位起来会比较棘手和耗时,所以建议并发编程的时候使用JDK并发包提供的并发容器和工具类来解决并发问题,因为这些类都是经过充分测试和优化的,均可解决本章提到的几个挑战。

《第二章:JAVA并发机制的底层实现原理》

2019.9.12

1.JAVA代码在编译后会变成JAVA字节码,类加载器将字节码加载到JVM中,JVM执行字节码,最终需要转为汇编指令在CPU上执行。JAVA中的并发编程依赖JVM的实现和CPU指令。

2.volatile的定义:

Java允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过拍他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁更加方便。如果一个字段被声明为volatile,Java线程内存模型确保所有线程看到的值是一致的。

3.volatile的作用:

volatile是轻量级的synchronized,它在多处理器开发中,保证共享变量的可见性,可见性是指当一个线程修改这个共享变量时,另外一个线程能读到这个修改后的值。volatile使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起上下文切换和调度。

2019.9.26

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

  • 使用总线锁定:所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞,那么该处理器将独占共享内存。在某一时刻,我们只需要保证对某个内存地址的操作是原子性即可,但总线锁定把CPU和内存直接的通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,目前处理器在某些场合下使用缓存锁定代替总线锁定来进行优化。
  • 缓存锁定:频繁使用的内存会缓存到处理器的L1、L2、L3高速缓存里,那么原子操作就可以直接在处理器内部缓存中进行,并不需要声明总线锁。缓存锁定是指,内存区域如果被缓存在处理器的缓存行中,并且在LOCK期间被锁定,那么当他执行锁操作回写到内存时,,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制保证操作的原子性,因为缓存一致性会阻止同时修改由两个处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行数据时,会使得缓存行无效。

2.但有以下两种情况下处理器不会使用缓存锁定:

  1. 当操作的数据不能被缓存在处理器内部,做操作的数据跨多个缓存行时,则处理器会调用总线锁定。
  2. 有些处理器不支持缓存锁定。对于Intel 486 和Petium处理器,就算锁定的内存区域在处理器的缓存行中也会调用总线锁定。

3.java中使用循环CAS(比较并替换)和锁的方式来实现原子操作。但循环CAS会有一些问题,比如ABA问题、循环时间长开销大、只能保证一个共享变量的原子操作等。目前JDK的Atomic包里提供了一些类可以解决这些问题。

4.锁机制保证了只有获得锁的线程才能操作锁定的内存区域。JVM内部实现了很多种锁机制,有偏向锁、轻量级锁和互斥锁。有意思的是除了偏向锁,JVM实现锁的方式都用了循环CAS,即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出出同步块的时候使用循环CAS释放锁。

《第三章:Java内存模型》

2019.9.26

1.在并发编程中,需要处理的两个关键问题:线程之间如何通信以及线程之间如何同步。

2.通信是指线程之间以何种机制来交换信息,在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递。

  1. 在共享内存的并发模型里,线程之间共享程序的公共状态,通过读写内存中的公共状态进行隐式通信。
  2. 在消息传递的并发模型中,线程之间没有公共状态,线程之间必须通过发送消息来显示的进行通信。

3.同步是指程序中用于控制不同线程间操作发生相对顺序的机制。在共享内存并发模型里,同步是显式进行的,程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行;在消息传递的并发模型里,由于消息的发送方必须要在消息的接收之前,因此同步是隐式进行的。

4.Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对程序员透明。如果编写多线程程序的Java程序员不理解隐式进行的线程之间通信的工作机制,很可能会遇到各种奇怪的内存可见性问题。

5.在Java中,所有实例域、静态域和数组元素都存在堆内存中,堆内存在线程之间共享。局部变量、方法定义参数和异常处理器参数不会在线程之间共享,他们不会有内存可见性问题,也不受内存模型影响。

6.Java线程之间的通信有JAVA内存模型(JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程与主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程可以读写共享变量的副本。本地内存是JMM抽象出来的一个概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。

2019.10.9

1.volatile自身具有以下特性:

  • 可见性:对一个volatile变量的读,总是能看到任意线程对这个volatile变量最后的写。
  • 原子性:对任意单个volatile变量的读/写具有原子性,但对于类似volatile++这种复合操作不具有原子性。

2.volatile写的内存语意:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新的到主内存中。

3.volatile读的内存语意:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

4.总结:线程A写一个volatile变量,实际上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所做修改的)消息;线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个vlatile变量之前对共享变量所做的修改的)消息。线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。

2019.10.15

1.锁是Java并发编程中最重要的同步机制。锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息。

2.锁的内存语意:

  1. 线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A对共享变量所做的修改的)消息。
  2. 线程B获取一个锁,实质上是线程B接受了之前某个线程发出的(在释放这个锁之前对共享变量所做的修改的)消息。
  3. 线程A释放锁,随后线程B获得这个锁,这个过程实质上是线程A通过主内存向线程B发送消息。

3.ReentrantLock又分为公平锁和非公平锁,现对其内存语意做个总结:

  1. 公平锁和非公平锁释放时,最后都要写一个volatile变量status。
  2. 公平锁获取时,都会先去读volatile变量。
  3. 非公平锁获取时,首先会用CAS更新volatile变量,这个操作同时具有volatile读和volatile写的内存语义。

4.从ReentrantLock的分析可以看出,锁释放-获取的内存语义至少有以下两种方式:

  1. 利用volatile变量的写-读所具有的内存语义。
  2. 利用CAS所附带的volatile读和volatile写的内存语义。

5.由于Java的CAS同时具有volatile读和volatile写的内存语义,因此Java线程之间的通信现在有了下面4种方式:

  1. A线程写volatile变量,随后B线程读这个volatile变量。
  2. A线程写volatile变量,随后B线程利用CAS更新这个volatile变量。
  3. A线程利用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量。
  4. A线程利用CAS更新一个volatile变量,随后B线程读这个volatile变量。

6.Java的CAS会使用现代处理器上提供的高效机器级别的原子指令,这些原子指令以原子形式对内存执行读-写操作,这是在多处理器中实现同步的关键。同时volatile变量的读写和CAS可以实现线程见的通信。

2019.10.17

1.happens-before规则定义如下:

  1. 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
  2. 监视器规则:对于一个锁的解锁,happens-before于随后对这个锁的加锁。
  3. volatile变量规则:对一个volitile域的写,happens-before于任意后续对这个volatile域的读。
  4. 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
  5. start()规则:如果线程A 执行操作ThreadB.start(),那么线程A的ThreadB.start()操作happens-before于线程B的任意操作。
  6. join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从TheadB.join()操作成功返回。

2.双重检查锁定的由来:

/**
 * 懒汉式单例,是线程不安全的
 */
public class Singleton{
    
    private static Singleton instance;

    private Singleton(){}

    public static Singleton getInstance(){
        
        if(instance == null){
            instance = new Singleton();
        }

        return instance;
        
    }

}

/**
 * 加同步锁,保证线程安全,但synchronized将导致内存开销,如果getInstance()被多个 
 * 线程频繁调用,将会导致程序的执行性能下降。
 */
public class Singleton{
    
    private static Singleton instance;

    private Singleton(){}

    public synchronized static Singleton getInstance(){
        
        if(instance == null){
            instance = new Singleton();
        }

        return instance;
        
    }

}

/**
 * 这种双重检查方式看似完美,但实际有可能出问题。
 * 第4步创建对象的过程实际可拆分为3步伪代码:1、分配内存对象;2、初始化对象;3、设置 
 * instance指向刚分配的内存地址。这三部中的2和3有可能发生重排序,如果这个时候另一个线程B调用的getInstance()发现instance不为空,但实际上对象还没有初始化完成。
 */
public class Singleton{
    
    private static Singleton instance;

    private Singleton(){}

    public static Singleton getInstance(){
        
        if(instance == null){                    //1 第一次检查
            synchronized (Singleton.class){     //2  加锁
                if(instance == null){            //3 第二次检查
                    instance = new Singleton();  //4 问题的根源在这里
                }
            }
        }

        return instance;
        
    }

}

//我们可以有2种方式解决上述问题:1、不允许2和3重排序;2、允许2和3重排序,但不允许其他线程“看到”这个重排序。

/**
 * 基于volatile的解决方案(也即不允许重排序)
 * volatile是的一个线程对其变量的写一定happens-before另一个线程对该变量的读。
 */
public class Singleton{
    
    private volatile static Singleton instance;

    private Singleton(){}

    public static Singleton getInstance(){
        
        if(instance == null){                    //1 第一次检查
            synchronized (Singleton.class){     //2  加锁
                if(instance == null){            //3 第二次检查
                    instance = new Singleton();  //4 声明为volatile,ok了
                }
            }
        }

        return instance;
        
    }

}


/**
 * 基于类初始化的解决方案
 * JVM在类初始化阶段(即在Class被加载后,且被线程使用前),会执行类的初始化。只执行 
 * 类的初始化期间,JVM会获取一个锁,这个锁可以同步多个线程对同一个类的初始化。
 */

public class Singleton{

    private static class InstanceHolder{

        public static Instance instance = new Instance();
    }

    public static Instance getInstance(){
        return InstanceHolder.instance;
    }

    private Singleton(){}

}

 

3.初始化一个类,包括执行这个类的静态初始化和初始化这个类中声明的静态字段。根据java语言规范,在首次发生下列任意一种情况时,一个类或接口类型T将立即被初始化:

  1. T是一个类,而且一个T类型的实例将被创建。
  2. T是一个类,且T中声明的一个静态方法被调用。
  3. T中声明的一个静态字段被赋值。
  4. T中声明的一个静态字段被使用,而且这个字段不是一个常量字段。
  5. T是一个顶级类,而且一个断言语句嵌套在T内部被执行。

4.Java语言规定,对于一个类或接口C,都有一个唯一的初始化锁LC与之对应。从C到LC的映射,由JVM的具体实现去自由实现。JVm在类初始化期间会获取这个初始化锁,并且每个线程至少获取锁来确保这个类已经被初始化过了。

5.通过对比基于volatile的双重检查锁定的方案和基于类初始化的方案,我们会发现基于类初始化的方案实现代码更简洁。但基于volatile的双重检查锁定的方案有一个额外的优势:除了可以对静态字段实现延迟初始化外,还可以对实例字段实现延迟初始化。

6.字段延迟初始化降低了初始化类或创建实例的开销,但增加了访问被延迟初始化的字段的开销。在大多数时候,正常的初始化要优于延迟初始化。如果确实需要对实例字段使用线程安全的延迟初始化,请使用上面介绍的基于volatile的延迟初始化方案;如果确实需要对静态字段使用线程安全的延迟初始化,请使用上面介绍的基于类出丝滑的方案。

 

《第四章:Java并发编程基础》

2019.10.21

1.线程是操作系统调度的最小单元,多个线程同时执行能显著提升程序性能,在多核环境下表现更加明显。每个线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上高速切换,让使用者感觉到这些线程在同时执行。

2.使用多线程的原因有以下几点:

  1. 更多的处理器核心:使用多线程,将计算逻辑分配到多个处理器核心上,就会显著减少程序的处理时间,并且随着更多处理器核心的加入而变得更有效率。
  2. 更快的响应时间:使用多线程技术,将数据一致性不强的操作派发给其他线程处理(也可以使用消息队列),这样,响应用户请求的线程能够尽可能快的处理完成,缩短了响应时间,提升用户体验。
  3. 更好的编程模型:Java为多线程编程提供了良好、考究并且一致的编程模型,是开发人员能够更加专注于问题的解决,即为所遇到的问题建立合适的模型。

3.在Java线程中,通过一个整型变量priority来控制优先级,范围从1~10,在线程构建的时候可以通过setPriority(int)来修改优先级,默认优先级为5,优先级高的线程分配时间片的数量要多余优先级的线程。设置优先级时,针对频繁阻塞(休眠或者I/O操作)的线程需要设置较高的优先级,而偏重计算(需要较多CPU时间或者偏运算)的线程则设置较低的优先级,确保处理器不会被独占。但在不同的JVM以及操作系统上,线程的规划会存在差异,有些操作系统甚至会忽略对线程优先级的设定。

4.Java线程所处的几种状态:

  1. NEW:初始状态,线程被构建,但还没有调用start()方法。
  2. RUNNABLE:运行状态,Java线程将操作系统中的就绪和运行两种状态笼统地称作“运行中”。
  3. BLOCK:阻塞状态,表示线程阻塞于锁(比如同步锁)。
  4. WAITING:等待状态,当线程执行wait()方法后,线程进入等待状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态。也即进入该状态表示当前线程需要等待其他线程作出一些特定动作(通知或中断)。
  5. TIME_WAITING:超时等待状态,该状态不同于WAITING,它是可以在指定的时间自行返回的。
  6. TERMINATED:终止状态:表示当前线程已经执行完毕。

5.线程对象的构造:线程对象在构造的时候需要提供线程所需要的属性,如线程所属的线程组、线程优先级、是否是Daemon线程等信息。其实child线程是继承了parent是否Deamon、优先级和加载资源的contextClassLoader以及可继承的ThreadLocal,同时还会分配一个唯一的ID来标识这个child线程。到这里,一个能够运行的线程对象就初始化完毕了,在堆内存中等待着运行。

6.启动线程:线程对象初始化完毕后,调用start方法就可以启动这个线程。该线程的父线程会同步告知Java虚拟机,只要线程规划器空闲,应立即启动调用start()方法的线程。注:启动一个线程前,最好为这个线程设置线程名称,因为这样使用jstack分析程序或者进行问题排查时,就会给开发人员提供一些提示,自定义的线程最好能够起一个名字。

7.安全的地终止线程:我们可以利用中断操作来取消或停止任务,我们还可以定义一个boolean变量来控制是否需要停止任务并终止该线程。这种停止线程的方式更加安全、优雅。

8.线程间通信:

  1. volitale和synchronized关键字:volitale关键字告知程序,任何对该变量的读均需要从内存中获取,任何对该变量的写必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。关键字synchronized主要用来修饰方法或者以代码块的形式来进行使用,它主要保证多个线程,在同一时刻,只能有一个线程在同步代码块或者方法中,它保证了线程对变量访问的可见性和排他性。
  2. 等待/通知机制:等待/通知机制是指,一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。上述两个线程通过对象O来完成交互,而wait()、notify()、notifyAll()方法是被定义在所有对象的超类上。
  3. 管道输入/输出流:管道输入/输出流和普通的文件输入、输出流不同之处在于,它主要用于线程之间的数据传输,而传输的媒介为内存。管道输入/输出流主要包括了如下4种具体实现:PipedOutputStream、PipedInputStream、PipedWriter、PipedReader,前面两个面向字节,后面两个面向字符。注:对Piped类型的流,必须要先进行绑定,也就是调用connect()方法(out.connect(in)),如果没有将输入/输出流绑定起来,对于该流的访问将抛出异常。
  4. Thread.join()的使用:如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才从thread.join()返回。它还提供了join(long millis)和join(long millis,int namos)两个具备超时特性的方法,他们表示线程thread在给定的超时时间里没有终止,那么将会从该超时方法中返回。其实join()方法内部也是采用了等待/通知机制,即加锁、循环和处理逻辑3个步骤。
  5. ThreadLocal的使用:即线程变量,是一个以ThreadLocal对象为键、任意对象为值得存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。

2019.10.30

1.设计一个线程池:

public class DefaultThreadPool<Job extends Runnable> implements ThreadPool<Job>{

    //线程池最大限制数
    private static final int MAX_WORKER_NUMBERS = 10;
    //线程池默认的数量
    private static final int DEFAULT_WORKER_NUMBERS = 5;
    //线程池最小的数量
    private static final int MIN_WORKED_NUMBERS = 1;
    //这是一个工作列表,将会向里面插入工作
    private final LinkedList<Job> jobs = new LinkedList<Job>();
    //工作者列表
    private final List<Worker> workers = Collections.synchronizedList(new ArrayList<Worker>());
    //工作者线程数量
    private int workerNum =    DEFAULT_WORKER_NUMBERS;
    //线程编号生成
    private AtomicLong threadNum = new AtomicLong();
    
    public DefaultThreadPool(){
        initializeWorker(DEFAULT_WORKER_NUMBERS);
    }  

    public DefaultThreadPool(int num){
        workerNum = num>MAX_WORKER_NUMBERS?MAX_WORKER_NUMBERS:num<MIN_WORKED_NUMBERS?MIN_WORKED_NUMBERS:num:
        initializeWorkers(workerNum);
    }

    
    public void execute(Job job){
        if(job != null){
            //添加一个工作,然后进行通知
            synchronized(jobs){
                jobs.addList(job);
                jobs.notify();
            }
        }
    }

    public void shutdown(){
        for(Worker worker :workers){
            worker.shutdown();
        }
    }

    public void addWorkers(int num){
        synchronized(jobs){
            //限制新增的worker数量不能超过最大值
            if(num + this.workNum > MAX_WORKER_NUMBERS){
                num = MAX_WORKER_NUMBERS - this.workNum;
            }
            initializeWorker(num);
            this.workNum += num;
        }
    }


    public void removeWorker(int num){
        synchronized(jobs){
        
            if(num>=this.workerNum){
                throw new IllegalArgumentException("beyond workNum");
            }

            //按照给定的数量停止worker
            int count = 0;
            while(count < num){
                Worker worker = works.get(count);
                if(workers.remove(worker){
                    worker.shutdown();
                    count++;
                }
            }
            this.workerNum -=count;
        }
    }

    public int getJobSize(){
        return jobs.size();
    }
    
    private void initializeWorkers(init num){
        for(int i=0; i< num ;i++){
            Worker worker = new Worker();
            workers.add(worker);
            Thread thread = new Thread(worker,"ThreadPool-Worker-"+threadNum.incrementAndGet());
            thread.start();
        }
    }

    //工作者,负责消费任务
    class Worker implements Runnable{
        
        //是否工作
        private volatile boolean running = true;

        public void run(){
            while(running){
                Job job = null;
                synchronized(jobs){
                    //如果工作者列表是空的,那么久wait
                    while(jobs.isEmpty()){
                        try{
                            jobs.wait();
                        }catch(InterruptedException ex){
                            //感知到外部WorkerThread的中断操作,返回
                            Thread.currentThread().interrupt();
                            return;
                        }
                     }
                    //取出一个Job
                    job = jobs.removeFirst();
                 }
                 
                 if(job != null){
        
                     try{
                        job.run();
                     }catch(Exception ex){
                        //忽略Job执行中的Exception
                     }
                 }
                
             }
        }


        public void shutdown(){

            running = false;
        }
        
    }


}

《第四章:Java中的锁》

2019.11.20

1.锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源(但有些锁可以允许多个线程并发的访问共享资源,比如读写锁)。在Lock接口出现之前,Java程序是靠sychronized关键字实现锁功能,而JavaSE 5之后,并发包中新增了Lock接口(以及相关实现类)用来实现锁功能,它提供了与synchronized关键字类似的同步功能,只是在使用时需要显示的获取和释放。虽然它缺少了(通过synchronized块或者方法所提供的)隐式获取和释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。

2.比如有个场景:先获得锁A,然后再获得锁B,当锁B获得后,释放锁A同时获取锁C,当锁C获得后,再释放锁B获得锁D。这种场景,synchronized关键字就不那么容易实现了,而使用Lock却容易许多。

3.在finally块中释放锁,目的是保证在获取到锁之后,最终能够被释放。但不要将获取锁的过程写在try块中,因为如果在获取锁(自定义锁)时放生了异常,异常抛出的同时,也会导致锁无故释放。

Lock lock = new ReentrantLock();
lock.lock();
try{
}finally{
    lock.unlock();
}

4.Lock和synchronized的不同点:

  1. Lock是一个接口,而synchronized是Java中的一个关键字。
  2. 当synchronized块结束时,会自动释放锁,Lock一般需要在finally中自己释放。synchronized在发生异常,会自动释放线程线程占有的锁,因为不会导致死锁现象发生;而lock在发生异常时,如果没有通过主动unlock()去释放占有的锁,则很可能造成死锁现象,因此,使用Lock时需要在finally块中释放锁。
  3. lock等待锁的过程中可以用interrupt中断等待,而synchronized只能等待锁的释放,不能响应中断。
  4. lock可以通过tryLock()来知道有没有获取锁,而synchronized不能。
  5. 当synchronized块执行时,只能使用非公平锁,无法实现公平锁,而Lock可以通过new ReentrantLock(true)设置为公平锁,从而在某些场景下提高效率。
  6. Lock可以提高多个线程进行读写操作的效率(可以通过readWriteLock实现读写分离)。
  7. synchronized锁类型 可重入、不可中断、非公平;而lock是可重入、可中断、可公平、可非公平。
  8. 在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。

5.Lock接口提供的synchronized关键字不具备的主要特性:

  1. 尝试非阻塞地获取锁:当前线程尝试获取锁,如果这一时间没有被其他线程获取到,则成功获取并持有锁。
  2. 能被中断地获取锁:与synchronized不同,获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常将会抛出,同时锁会被释放。
  3. 超时获取锁:在指定的截止时间之前获取锁,如果截止时间到了仍旧无法获取锁,则返回。

6.Lock接口常用方法:

  1. void lock():获取锁,调用该方法,当前线程将会获取锁,当锁获得后,才从该方法返回(是阻塞性的)。
  2. lockInterruptibly()throws InterruptedException :可中断地获取锁,跟lock()不同的时,该方法会响应中断,即在锁的获取中可以中断当前线程。
  3. boolean tryLock():尝试非阻塞地获取锁,调用该方法后立刻返回,如果能够获取则返回true,否则返回false。
  4. boolean tryLock(long time,TimeUnit unit):超时的获取锁,如果在超时时间内获取到锁,就返回true,否则返回false。
  5. unlock():释放锁。
  6. new Condition():获取等待通知组件,该组件和当前的锁绑定,当前线程只有获得了锁,才能调用该组件的wait方法,而调用后,当前线程将释放锁。

7.线程调用了wait()或者join()方法就会进入WAITING状态,执行了wait(long timeout)、sleep(long millis)、join(long millis),就会进入TIME_WAITING状态。在这些状态下,对线程对象调用interrupt()方法,会使得该线程抛出InteruptedException,需要注意的是,抛出该异常后,中断标记位会被清空,而不是被设置。

8.如果线程在等待锁,也即是处于BLOCK(阻塞)状态下,synchronized锁的线程并不会响应中断,线程仍然处于BLOCK状态,要想响应中断,可以使用显示锁(Lock锁)。

2019.11.21

1.队列同步器(AbstractQueuedSynchronizer):是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。

2.同步器自身并没有实现任何同步接口,它仅仅是定义了若干个同步状态获取和释放的方法来供自定义组件的使用,同步器既可以支持独占式获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock、ReentrantReadWriteLock和CountDownLatch)。

3.同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。可以这样理解二者之间的关系:锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并发访问)隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者合实现者所需关注的领域。

2019.12.3

1.重入锁(ReentrantLock):就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。除此之外,该锁还支持获取锁时公平和非公平选择。

2.当一个线程调用Mutex的lock()方法获取锁之后,如果再次调用lock()方法,则该线程将会被自己阻塞,原因是Mutex在实现tryAcquire(int acquires)方法时没有考虑占有锁的线程再次获取锁的场景,而在调用tryAcquire(int acquires)方法时返回了false,导致该线程被阻塞。简单的说,Mutex是一个不支持冲进入的锁。而synchronized关键字隐式的支持重进入,比如一个synchronized修饰的递归方法,在方法执行时,执行线程在获取了锁之后,仍能连续多次的获得该锁,而不像Mutex由于锁获取了锁,而在下一次获取锁时出现自己阻塞自己的情况。

3.ReentrantLock虽然没有像synchronized关键字一样支持隐式的重进入,但是在调用lock()方法时,已经获取到锁的线程,能够再次调用lock()方法获取锁而不被阻塞。

4.这里提到一个锁获取的公平性问题,如果在绝对时间上,先对锁进行获取的请求一定先被满足,那么这个锁是公平的,反之,是不公平的。也即是,锁的获取是顺序,这种即为公平锁。ReentrantLock提供了一个构造函数,能够控制锁是否是公平的。

5.事实上,公平的锁机制往往没有非公平的效率高,但是,并不是任何场景都是以TPS作为唯一的指标,公平锁能够减少“饥饿”发生的概率,等待越久的请求是能过得到优先满足。

2019.12.4

1.如何实现重进入?重进入是指任意线程在获取到锁之后能够再次获取到该锁而不会被锁所阻塞,实现该特性需要解决一下两个问题:

  1. 线程再次获取锁:锁需要去识别获取锁的线程是否为当前占用锁的线程,如果是,则再次成功获取。
  2. 锁的最终释放:线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数为0时表示锁已经成功释放。

2.成功获取锁的线程再次获取锁,只是增加了同步状态,这就要求ReentrantLock在释放同步状态时减少同步状态值。

3.锁的公平性与否是针对锁获取而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO。公平锁每次都是从同步队列中的第一个节点获取到锁,而非公平锁会出现一个线程连续获取锁的情况。为什么会出现连续获取锁的情况呢?因为,当一个线程请求锁时,只要获取了同步状态即成功获取锁,在这个前提下,刚释放的线程再次获取同步状态的几率非常大,使得其他线程只能在同步队列中等待。

4.非公平性锁可能使线程“饥饿”,但它又被设定为默认的实现,为什么呢?从测试例子可以看出,非公平锁虽然可能造成线程“饥饿”,但极少的线程切换,开销较小,保证了其更大的吞吐量。

2019.12.16

1.队列同步器实现分析:接下来,从实现角度分析同步器是如何完成线程同步的,主要包括:同步队列、独占是同步状态获取与释放、共享式同步状态获取与释放以及超时获取同步状态等同步器的核心数据结构与模板方法。

2.同步队列:同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构成为一个节点(Node)并将其加入同步队列,同时会阻塞该线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。

3.同步队列中的节点用来保存获取同步状态失败的线程引用、等待状态、前驱及后继节点等。

4.独占式同步状态获取是释放:通过调用同步器的acquire(int arg)方法可以获取同步状态,该方法对中断不敏感,也就是,由于线程获取同步状态失败后,进入同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移出。同一时刻,只能有一个线程成功获取同步状态,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。

5.共享式同步状态获取与释放:共享式与独占式获取的最主要区别在于同一时刻能否有多个线程同时获取到同步状态。

6.独占式超时获取同步状态:即在超时时间范围内独占式的获取同步状态,如果时间范围内没有成功,则返回失败。

2019.12.17

1.分析ReentrantReadWriteLock的实现,主要包括:读写状态的设计、写锁的获取与释放、读锁的获取与释放以及锁降级。

2.读写锁同样是依赖自定义同步器来实现同步功能,而读写状态就是其同步器的同步状态。回想RenntrantLock中自定义同步器的实现,同步状态表示锁被一个线程重复获取的次数,而读写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状态,使得该状态的设计成为读写锁实现的关键。

3.写锁的获取与释放:写锁是一个支持重进入的排它锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取或者该线程不是已经获取写锁的线程,则当前线程进入等待状态。原因在于:读写锁要确保写锁的操作对读锁可见,如果允许读锁在已获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到写线程的操作。因此,只有等待其他读线程都释放了读锁,写锁才能被当前线程获取,而写锁一旦被获取,则其他读写线程的后续访问均被阻塞。

4.写锁的释放与ReentrantLock的释放过程类似,每次释放均减少写状态,当写状态为0时表示写锁已被释放,从而等待的读写线程能够继续访问读写锁,同时前次写线程的修改对后续读写线程可见。

5.读锁的获取与释放:读锁是一个支持重进入的共享锁,它能够被多个线程同时获取,在没有其他写线程访问时,读锁总会被成功获取,而所做的也只是增加读状态。

6.读锁的每次释放(线程安全的,可能有多个线程同时释放读锁)均减少读状态。

7.锁降级:锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获得读锁,这种分段完成的过程不能称之为锁降级,锁降级是指把持住写锁,再获取到读锁,随后释放写锁的过程。

8.锁降级中读锁的获取是否有必要?是有必要的。主要是为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(记做线程T)获取了写锁并修改了数据,那么当前线程无法感知到线程T的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能进行数据更新。

9.ReentrantReadWriteLock不支持锁降级(把持读锁,获取写锁,最后释放读锁的过程)。目的是保证数据可见性,如果读锁已经被多个线程获取,其中任意线程获取了写锁并更新了数据,则其更新对其他获取的到读锁的线程是不可见的。

2019.12.17

1.LockSupport工具:当需要阻塞或唤醒一个线程的时候,都会使用LockSupport工具来完成相应的工作。其定义了一组公共静态方法,,提供最基本的线程阻塞和唤醒功能,而LockSupport也成为构建同步组件的基础工具。

2.LockSupport提供的常用方法:

  1. void park():阻塞当前线程,如果调用unpark(Thread Thread)方法或者当前线程被中断,才能从park方法返回。
  2. void parkNanos(long nanos):阻塞当前线程,最长不超过nanos纳秒,返回条件在park()的基础上增加了超时返回。
  3. void parkUntil(long deadline):阻塞当前线程,直到deadline时间(从1970年开始到deadline时间的毫秒数)
  4. void unpark(Thread thread):唤醒处于阻塞状态的thread。

《第五章:Java并发容器和框架》

2019.12.19

1.ConcurrentHashMap是线程安全且高效的HashMap,接下来会探究该容器是如何保证线程安全的同时又能保证高效的操作。

2.为什么要使用ConcurrentHashMap?原因有三:

线程不安全的HashMap:在多线程环境下,使用HashMap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap。例如,执行以下代码会引起死循环。

final HashMap<String,String> map = new HashMap<String,String>(2);
Thread t = new Thread(new Runnable(){
    
    @Override
    public void run(){

        for(int i=0;i<10000;i++){
            new Thread(new Runnable(){
                @Override
                public void run(){
                    map.put(UUID.randomUUID().toString(),"");
                }
            },"ftf"+i).start();
        }
        
    }

});

t.start();
t.join();

2.效率低下的HashTable:HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低。因为当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同步方法,会进入阻塞或轮询状态。如线程1使用put进行添加元素,线程2不但不能使用put方法添加元素,也不能使用get方法来获取元素,所以竞争越激烈效率越低。

3.ConcurrentHashMap的锁分段技术可有效提升并发访问率:HashTable容器在激烈的并发环境下表现出效率低的原因是所有访问HashTable的线程都必须竞争同一把锁,加入容器里有多把锁,每一把锁用于锁容器中的一部分数据,那么多线程访问容器里不同数据段的时候,线程就不会存在锁竞争,从而可以有效提升并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术。首先将数据分成一段一段地存储,然后给每一段数据分配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

2019.12.24

1.ConcurrentHashMap的结构:ConcurrentHashMap是由Segment数据结构和HashEntry数据结构组成。Segment是一种可重入锁(ReentrantLock),在ConcurrentHashMap中扮演锁的角色;HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,一个Segment包含一个HashEntry数组,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得与它对应的Segment锁。

2.ConcurrentHashMap的操作:

  • get操作:Segment的get操作实现非常简单和高效。先经过一次再散列,然后使用这个散列值通过散列运算定位到Segment,再通过散列算法定位到元素。其之所以高效,是因为整个get过程不需要加锁,除非读到的值为空才会加锁重读。我们知道HashTable的get方法是需要加锁的,那么ConcurrentHashMap的get操作是如何做到不加锁呢?原因是它的get方法里将要使用的共享变量都定位为volatile类型,能够在线程间保持可见性,能够被多线程同时读,并且保证读到的值不过期,但是只能被单线程写,而get操作不需要写共享变量,所以不用加锁。之所以不会读到过期的值,是因为Java内存模型的happen before原则,对volatile字段的写入操作优先于读操作,即使两个线程同时修改和获取volatile变量,get操作也能拿到最新的值。
  • put操作:由于put操作需要对共享变量进行写入操作,为了线程安全,操作共享变量必须加锁。put方法首先定位到Segment,然后在Segment里进行插入操作。插入操作需要经历两个步骤,第一步判断是否需要对Segment里的HashEntry数组进行扩容,第二步定位添加元素的位置,然后将其放在HashEntry数组里。
  • size操作:要想统计ConcurrentHashMap里的元素的大小,就必须统计所有的Segment里元素的大小后求和。虽然Segment里的全局变量count是一个volatile变量,相加的时候可以获取每个Segment的最新值,但是可能累加前使用的count发生的变化,那么统计结果就不准了。解决办法是,使用一个叫modCount的变量,在put、remove、clean方法里操作元素都会讲变量加1,那么在统计size前后比较modCount是否发生了变化,从而得知容器的大小是否发生了变化。

《第5章:Java中的并发工具类》

1.CountDownLatch:允许一个或多个线程等待其他线程完成操作。

       我们知道join也可以用于让当前线程等待join线程执行结束,他的原理是不停的检查join线程是否存活,如果join线程存活则让当前线程永远等待,直到join线程中止后,线程的this.notifyAll()方法会被调用,调用notifyAll()方法是在JVM里实现的,所以在JDK里大家看不到,可以查看JVM源码。

       并发包里提供的CountDownLatch也可以实现join的功能,并且比join的功能更多:

public class CountDownLatchTest{

   static CountDownLatch c = new CountDownLatch(2);

   
    public static void main(String[] args) throws InterruptedException{


        new Thread(new Runnable(


            @Override
            public void run(){

                System.Out.println(1);
                c.countDown();
                System.Out.println(2);
                c.countDown();
                
            }
        )).start();
        
        c.await();
        System.Out.println(3);
    
    }


}

         CountDownLatch的构造函数接收一个int类型的参数作为计数器,如果想等待N个点完成,这里就传入N。当我们调用其countDown方法时,N就会减1,CountDownLatch的await方法会阻塞当前线程,直到N变成0。这里所说的N个点,可以是N个线程,也可以是1个线程里的N个执行步骤,用在多线程时,只需要把这个CountDownLatch的引用传递到线程即可。

《第9章:Java中的线程池》

2020.1.3

1.Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池,使用线程池能够带来3个好处:

  1. 降低资源消耗:通过重复利用已创建的线程,降低线程创建和销毁造成的消耗。
  2. 提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行。
  3. 提高线程的可管理性:线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以统一分配、调优和监控。

2.线程池的工作原理(也即是ThreadPoolExecutor执行execute()方法的过程):

  1. 如果当前运行的线程数少于corePoolSize,则创建新线程来执行任务(注意,执行该步骤需要获取全局锁)
  2. 如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。
  3. 如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务(注意,执行该步骤也需要获取全局锁)。
  4. 如果创建新线程将使得当前运行的线程数超出maximumPoolSize,任务将被拒绝,并调用RejectedExceptionHandler.rejectedException()方法。

3.ThreadPoolExecutor采取的总体思路,是为了在执行exccute()方法时,尽可能地避免获取全局锁,在ThreadPoolExecutor完成预热之后(当前运行的线程数大于等于corePoolSize),几乎所有的execute()方法调用都是执行步骤2,而步骤2不需要获取全局锁。

2020.1.10

1.线程池的创建:我们可以通过ThreadPoolExecutor来创建一个线程池。

new ThreadPoolExecutor(corePoolSize,maximumPoolSize,keepAliveTime,milliseconds,runnableTaskQueue,handler);
  • corePoolSize(线程池基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果线程调用了prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程。
  • runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列。
  1. ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,按FIFO(先进先出)原则对元素进行排序。
  2. LinkedBlockingQueue:是一个基于链表结构的阻塞队列,仍然是按FIFO排序元素,吞吐量要高于ArrayBlockingQueue。静态工程方法Executors.newFixedThreadPool()使用了这个队列。
  3. SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须要等到另一个线程调用移出操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
  4. PriorityBlockQueue:一个具有优先级的无限阻塞队列。
  • maximumPoolSize(线程池最大数量):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会创建新的线程来执行任务,如果使用了无界的任务队列,这个参数就没有什么效果了。
  • ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。
  • RejectedExceptionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认是AbortPolicy,表示无法处理新任务时抛出异常。Java线程池框架提供了以下4中策略。
  1. AbortPolicy:直接抛异常。
  2. CallerRunsPolicy:只用调用者所在线程来运行任务。
  3. DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
  4. DiscardPolicy:不处理,丢弃掉。(也可以根据场景需要来实现RejectedExceptionHandler接口自定义策略)
  • keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活时间。所以如果任务很多,并且每个任务执行时间比较短,可以调大时间,提高线程的利用率。
  • TimeUnit(线程活动保持时间的单位):可选单位有天、小时、分钟、毫秒、微秒和纳秒。

2.向线程池提交任务:可以使用两种方法向线程池提交任务,分别是execute()和submit()方法。

  • execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。通过以下代码可知execute()方法输入的任务是一个Runnable类的实例。
threadsPool.execute(new Runable(){
    @Override
    public void run(){
        //TODO 
    }
});
  • submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get方法来获取返回值,get()方法会阻塞当前线程直到任务完成。而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时有可能任务没有执行完。
Future<Object> future = executor.submit(harReturnValueTask);
try{
    Object s = future.get();
}catch (InterruptException e){
    //处理中断异常
}catch(ExceptionException e){
    //处理无法执行任务异常
}finally{
    //关闭线程池
    executor.shutdown();
}

3.关闭线程池:可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂定任务的线程,并返回等待执行任务的列表,而shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。

4.只要调用了这两个关闭方法中的任意一个,isShutDown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminated方法会返回true。至于应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown方法来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow方法。

5.合理地配置线程池:要合理地配置线程池,就必须首先分析任务特性,可以从以下几个角度来分析:

  1. 任务的性质:CPU密集型任务、IO密集型任务和混合型任务。
  2. 任务的优先级:高、中、和低。
  3. 任务的执行时间:长、中和短。
  4. 任务的依赖性:是否依赖其他系统资源,如数据库连接。
  • 性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务应配置尽可能小的线程,如配置N(cpu个数)+1个线程的线程池。由于IO密集型任务线程并不是一直在执行任务,则应配备尽可能多的线程。
  • 优先级不同的任务可以使用优先级队列PriorityBlockingQueueQueue来处理。它可以让优先级高的任务先执行。
  • 执行时间不同的任务可以交给不同规模的线程池来处理,或者可以使用优先级队列,让执行时间短的任务先执行。
  • 依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,等待的时间越长,则CPU空闲的时间就越长,那么线程数应该设置的越大,这样才能更好地利用CPU。

6.建议使用有界队列。有界队列能增加系统的稳定性和预警能力,可以根据需要设置大一些。

7.线程池的监控:如果在系统中大量使用线程池,则有必要对线程池进行监控,方便在出现问题时,可以根据线程池的使用状态快速定位问题。可以通过线程池提供的参数来进行监控,在监控线程池的时候可以使用以下参数:

  1. taskCount:线程池需要执行的任务数量。
  2. completedTaskCount:线程池在运行过程中已完成的任务数量,小于或等于taskCount。
  3. largestPoolSize:线程池曾经创建过的最大线程数量。通过这个值可以知道线程池是否曾经满过。
  4. getPoolSize:线程池的线程数。如果线程池不销毁的话,线程池里的线程不会自动销毁,所以这个大小只增不减。
  5. getActiveCount:获取活动的线程数。

8.可以通过扩展线程进行监控,通过继承线程池来自定义线程池,重写线程池的beforeExecute、afterExecute和terninated方法,也可以在任务执行前、执行后和线程池关闭前执行一些代码来进行监控。例如,监控任务的平均执行时间、最大执行时间和最小执行时间等。

第10章 Executor框架

2020.1.13

1.Java的线程既是工作单元,也是执行机制。从JDK5开始,把工作单元与执行机制分离开来,工作单元包括Runable和Callable,而执行机制由Executor框架提供。

 

第11章 Java并发编程实践

2020.2.24

1.在并发编程找那个使用生产者和消费者模式能够解决绝大多数并发问题。该模式通过平衡生产线程和消费线程的工作能力来提高程序整体处理数据的速度。

2.什么是生产者和消费者模式:生产者和消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通信,而是通过阻塞队列来进行通信,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。

3.这个阻塞队列用来给生产者和消费者解耦的,就相当于一个缓冲区,平衡了生产者和消费者的处理能力。

4.Java中的线程池其实就是一种生产者和消费者模式的实现方式,但其更加高明。生产者把任务丢给线程池,线程池创建线程并处理任务,如果将要运行的任务数大于线程池的基本线程数就把任务扔到阻塞队列里,这种做法比只使用一个阻塞队列来实现生产者和消费者模式显然要高明很多,因为消费者能够处理直接就处理掉了,这样速度更快,而生产者先存,消费者再取这种方式显然要慢些。

5.线上问题点位:有很多问题只有在线上或者预发布环境才能发现,而线上又不能调试代码,所以线上问题定位就只能看日志、系统状态和dump线程。

6.简单地介绍一些常用的工具,以帮助大家定位线上问题:

  1. 在Linux命令下使用TOP命令查看每个进程的使用情况。
  2. 再使用top的交互命令数字1查看每个CPU的性能数据。
  3. 使用top的交互命令H查看每个线程的性能信息。

7.性能测试中使用的命令:

  1. 查看网络流量:cat  /proc/net/dev
  2. 查看系统平均负载:cat  /proc/loadavg
  3. 查看系统内存情况:cat  /proc/meminfo
  4. 查看CPU的使用率:cat   /proc/stat

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值