《Java并发编程的艺术》读书笔记(1~4章)

148 阅读15分钟

并发编程的挑战

上下文切换的开销

无锁并发编程、CAS算法、使用最少线程(maxThread参数)和使用协程

死锁

常见方法:

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

资源限制

基础知识

CPU术语

  • 内存屏障:一组处理器指令,用于实现对内存操作的顺序限制
  • 缓存行:缓存中可以分配的最小存储单元,处理器填写缓存线时会加载整个缓存线,需要使用多个主内存读周期
  • 缓存行填充:当处理器识别到从内存中读取操作数可缓存时,处理器读取整个缓存行到适当的缓存
  • CPU流水线:几个不同功能的电路单元组成一条指令处理流水线,实现一个CPU时钟周期完成一条指令
  • 内存顺序冲突:由假共享引起,假共享指多个CPU同时修改一个缓存行。当发生内存顺序冲突时必须清空CPU流水线

操作系统知识点

锁住总线意味着其他CPU无法访问总线,即无法访问系统内存。

JVM相关知识点

Java对象头中包含着锁状态

Java并发机制的底层实现原理

依赖于JVM的实现和CPU的指令

volatile的应用

volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。当一个线程修改一个共享变量时,其他线程可以读到这个修改值。

恰当使用volatile会降低并发的上下文切换开销

volatile其实就是实现了将当前处理器的缓存写入内存且置其他处理器的缓存无效(Lock指令)

依靠LinkedTransferQueue类优化volatile性能

synchronized的原理和应用

锁的三种形式

  • 普通同步方法:锁当前实例对象
  • 静态同步方法:锁当前类的Class对象
  • 同步方法块:锁Synchonized括号配置的对象

JVM基于进入和退出Monitor对象来实现方法同步和代码块同步(细节略有不同)。

  • monitorenter指令:编译后插入到同步代码块的开始位置
  • monitorexit指令:插入到方法结束和异常处 任何对象都有一个monitor与之关联,当monitor被持有后,它将处于锁定状态。
    当线程执行到monitorenter指令时,它将尝试获取对应的monitor所有权

锁升级

无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态
随着竞争状况升级
只能升级不能降级(避免轻量级锁带来的无用自旋消耗CPU)

偏向锁

  • 引入原因 因为大多数情况下锁并不存在多线程竞争,而是被同一线程多次获得,引入偏向锁可以降低获得锁的代价(只要测试对象头中Mark Word里是否存储了指向当前线程的偏向锁,成功就获得锁,不成功就CAS竞争锁),加锁解锁不需要额外消耗
  • 偏向锁的撤销 当其他线程尝试竞争偏向锁时,需要等待全局安全点(在这个时间点上没有正在执行的字节码) 才能释放。撤销会消耗性能。
  • 使用场景 只有一个线程访问同步块场景

轻量级锁

  • 优点 竞争的线程不会引起阻塞
  • 缺点 自旋消耗CPU
  • 使用场景 追求响应速度,方法块执行速度快

重量级锁

  • 优点 不自旋不消耗CPU
  • 缺点 阻塞线程,响应慢
  • 使用场景 追求吞吐量,方法块执行时间长

实现原子操作

处理器

总线锁缓存锁

不使用缓存锁的情况

  • 操作数据不能被缓存或操作数据跨多个缓存行
  • 处理器不支持

Java

锁和CAS

CAS的三大问题

  • ABA 引入版本号
  • 自旋消耗 JVM引入pause指令
  • 只能保证一个共享变量的原子操作 1.直接用锁2.合并共享变量

Java内存模型(JMM)

采用共享内存并发模型
Java中所有实例域、静态域和数组元素都存储在堆内存中被线程共享(通过控制主内存与线程本地内存)

基础

线程间通信

  • 共享公共状态可通过写-读操作隐式通信
  • 发送消息显式通信

线程同步

同步指的是程序中用于控制不同线程间操作发生相对顺序的机制。

共享内存并发模型里同步是显式的,程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。

内存屏障

  • LoadLoad Barriers
    • 指令示例:Load1,LoadLoad;Load2
    • 确保Load1数据的装载先于Load2及所有后续装载指令的装载
  • StoreStore Barriers
    • 指令示例:Store1,StoreStore;Store2
    • 确保Store1数据对其他处理器可见(刷新到内存)先于Store2及所有后续存储指令的存储
  • LoadStore Barriers
    • 指令示例:Load1,LoadStore;Store2
    • 确保Load1数据的装载先于Store2及所有后续存储指令的存储
  • StoreLoad Barriers
    • 指令示例:Store1,StoreLoad;Load2
    • 确保Load1数据对其他处理器可见(刷新到内存)先于Load2及所有后续装载指令的装载。StoreLoad Barriers会使该屏障之前的所有内存访问指令(存储和装载指令)完成后,才执行该屏障之后的内存访问指令。

顺序一致性

总线事务:读事务、写事务

同步原语

happens-before

与程序员密切的规则:

  • 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作
  • 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁
  • volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读
  • 传递性:如果A happens-before BB happens-before C,那么A happens-before C

tip

happens-before并不意味着必须一个操作在另一个操作之前或之后执行,只是要求前一个操作的执行结果对后一个操作可见。(其实就暗含了as-if-serial

as-if-serial

不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。

volatile读写内存语义(JSR-133加强!)

  • 写内存语义:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
  • 读内存语义:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。 要限制前后读写操作重排序依靠屏障实现

锁的内存语义

  • 锁的释放-获取依靠happens-before关系
  • 其内存语义与volatile内存语义相同

公平锁和非公平锁的内存语义

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

concurrent包的实现

  • 声明共享变量为volatile
  • 使用CAS的原子条件更新来实现线程之间的同步
  • 配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信

final域的内存语义(JSR-133加强!)

final域的重排序规则

  • 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序
  • 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序

final域的重排序规则

  • JMM禁止编辑器把final域的写重排序到构造函数之外
  • 编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障。这个屏障禁止处理器把final域的写重排序到构造函数之外 即调用构造函数的时会执行final域写操作

final域的重排序规则

在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这两个操作(仅针对处理器)。编译器会在读final域之前插入一个LoadLoad屏障
这确保:在读一个对象的final域之前,一定会先读包含这个final域的对象的引用。如果该引用不为null则引用对象的final域一定已经被初始化了。

final域为引用类型

增加一个约束:在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数之外把这个构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。

双重检查锁定与延迟初始化

总而言之就是可能先指向对象内存空间再对对象初始化。此时对象不为null也就无法实现初始化。

  • 方案一:将instance声明为volatile对象(需要jdk5版本及以上)
public class SafeDoubleCheckedLocking {
    private volatile static Instance instance;

    public static Instance getInstance() {
        if(instance == null) {
            synchronized (SafeDoubleCheckedLocking.class) {//加锁
                if(instance == null) instance = new Instance();//原子操作
            }
        }
        return instance;
    }
}
  • 方案二:基于类初始化
public class InstanceFactory {
    private static class InstanceHolder {
        public static Instance instance = new Instance();
    }

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

实质是允许初始化指向对象内存空间重排序但不影响其他操作引用,即在引用前完成。

初始化一个类,包括执行这个类的静态初始化和初始化在这个类中声明的静态字段。根据Java语言规范,在首次发生下列任意一种情况下,一个类或接口类型T将立即被初始化: T是一个类,而且一个T类型的实例被创建。 T是一个类,且T中声明的一个静态方法被调用 T中声明的一个静态字段被赋值 T中声明的一个静态字段被使用,而且这个字段不是一个常量字段 T是一个顶级类(Object,该类不再继承别的类,是最上面一层的类),而且一个断言语句嵌套在T内部被执行。

  • 对比 虽然第二种代码更简洁,但第一种的静态字段和实例字段都实现了延迟初始化,第二种仅静态字段实现了延迟初始化

Java内存模型设计

处理器内存模型对读/写操作组合的执行顺序的放松

  • 放松程序中写-读操作顺序,由此产生了Total Store Ordering内存模型(简称TSO)
  • 在上面基础上还放松了写-写操作的顺序产生了Partial Store Order内存模型(简称PSO)
  • 在以上基础上还放松了读-写和读-读操作的顺序,由此产生了Relaxed Memory Order内存模型(简称RMO)和PowerPC内存模型

以上都是在满足as-if-serial语言的情况下!!!!!!!

JMM通过插入各种屏障对程序员们屏蔽了不同处理器内存模型的差异!!

JMM内存可见性保证

  • 单线程程序。不会出现内存可见性问题。编译器、runtime和处理器会共同确保单线程程序的执行结果与该程序在顺序一致性模型中的执行结果相同。
  • 正确同步的多线程程序。该执行具有顺序一致性(行结果与该程序在顺序一致性模型中的执行结果相同)。通过限制编译器和处理器的重排序为程序员提供内存可见性保证。
  • 未同步/未正确同步的多线程程序。提供最小安全性保障:线程执行时读取到的值是之前某个线程写入的值或默认值。(被使用前能被读到值,不保证值是正确的)

Java并发编程基础

线程

进程是程序的一次运行,线程是轻量级进程,是操作系统调度的最小单元。一个进程可以创建多个线程。线程私有各自的计数器、堆栈和局部变量等属性,之间有共享内存变量

JVM中的线程私有和线程共享:

  • 私有
    • 程序计数器
    • 虚拟机栈:局部变量表、操作数栈、动态链接、方法出口等信息
    • 本地方法区:类似虚拟机栈,区别是虚拟机栈为Java方法服务,本地方法区为Native方法服务
  • 共享
    • :也称运行时数据区,存储对象和产生的数据
    • 方法区:也被称为永久代,存储常量、静态变量、类信息、即时编译器编译后的机器码、运行时常量池 线程优先级决定被分配的时间片,线程优先级不保证程序的正确性

多线程的优点

  • 更多的处理器核心
  • 更快的响应时间
  • 更好的编程模型

线程状态

  • NEW:初始状态,线程被构建但未调用start()方法
  • RUNNABLE:运行状态,就绪和运行被笼统得称作“运行中”
  • BLOCKED:阻塞状态,表示线程阻塞于锁
  • WAITING:等待状态,需要等待其他线程作出通知或中断
  • TIME_WAITING:超时等待状态,与WAITING状态类似,但可在指定时间自行返回
  • TERMINATED:终止状态,表示当前线程已经执行完毕

Daemon线程

不一定会执行,当JVM不存在非Daemon线程时就退出了
通过Thread.setDaemon(true)设置为Daemon线程,需要在启动前(start()前设定)
不能依靠finally块中的内容确保执行关闭或清理资源

理解中断

中断可以理解为线程的一个标识位属性,表示一个运行中的线程是否被其他线程进行了中断操作。
线程通过检查自身是否被中断来进行响应,线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()对当前线程的中断标识位复位。
InterruptedException的方法抛出前,JVM会将该线程的中断标识位清除。此时isInterrupted()会返回false

过期的suspend()resume()stop()

暂停、恢复、停止,这些API已过期,不推荐使用。可用等待/通知机制替代

安全终止线程的实例

可以通过中断操作或一个boolean变量来控制是否需要停止任务并终止该线程

public class Shutdown {
    public static void main(String[] args) throws Exception {
        Runner one = new Runner();
        Thread countThread = new Thread(one, "CountThread");
        countThread.start();
        //睡眠1秒,main线程对CountThread进行中断,使CountThread能够感知中断而结束
        TimeUnit.SECONDS.sleep(1);
        countThread.interrupt();
        Runner two = new Runner();
        countThread = new Thread(two, "CountThread");
        countThread.start();
        //睡眠1秒,main线程对Runner two进行取消,使CountThread能够感知on为false而结束
        TimeUnit.SECONDS.sleep(1);
        two.cancel();
    }

    private static class Runner implements Runnable {
        private long i;
            private volatile boolean on = true;
            
            @Override
            public void run() {
                while (on && !Thread.currentThread().isInterrupted()) {
                    i++;
                }
                System.out.println("Count i =" + i);
            }

            public void cancel() {
                on = false;
            }
    }
}

这样线程终止时有机会去清理资源,更为安全和优雅。

线程间通信

volatilesynchronized关键字

等待/通知机制 notify()wait()方法

管道输入/输出流

使用输出流对象.connect(输入流对象)绑定

thread.join()

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

ThreadLocal

即线程变量,是一个以ThreadLocal对象为键,任意对象为值的存储结构。这个结构被附带在线程上,可以根据ThreadLocal对象查询到绑定在这个线程上的一个值。

set(T)方法来设置一个值,在当前线程下再通过get()方法获取到原先设置的值。

public class Profiler {

    private static final ThreadLocal<Long> TIME_THREADLOCAL = new ThreadLocal<Long> {
        proteceted Long initialValue() {
            return System.currentTimeMillis();
        }
    };

    public static final void begin() {
        TIME_THREADLOCAL.set(System.currentTimeMillis());
    }

    public static final long end() {
        return System.currentTimeMillis() - TIME_THREADLOCAL.get();
    }

    public static void main(String[] args) throws Exception {
        Profiler.begin();
        TimeUnit.SECONDS.sleep(1);
        System.out.println("Cost: " + Profiler.end() + "mills");
    }
}

以上实现一个可复用的计时器

实例

超时等待

public synchronized Object get(long mills) throws InterruptedException {
    long future = System.currentTimeMillis() + mills;
    long remaining = mills;
    while ((result == 0) && remaining > 0) {
        wait(remaining);
        remaining = future - System.currentTimeMillis();
    }
    return result;
}

一个基于线程池技术的简单Web服务器

Web服务器用main线程不断接收客户的Socket连接,将连接以及请求交给线程池处理,使Web服务器能够同时处理多个客户端请求

public class SimpleHttpServer {
    //处理HttpRequest的线程池
    static ThreadPool<HttpRequestHandler> threadPool = new DefaultThreadPool <HttpRequestHandler>(1);
    //SimpleHttpServer的根路径
    static String basePath;
    static ServerSocket serverSocket;
    //服务监听端口
    static int port = 8080;

    public static void setPort(int port) {
        if(port > 0) {
            SimpleHttpServer.port = port;
        }
    }

    public static void setBasePath(String basePath) {
        if(basePath != null && new File(basePath).exists() && new File(basePath).isDirectory()) {
            SimpleHttpServer.basePath = basePath;
        }
    }

    //启动SimpleHttpServer
    public static void start() throws Exception {
        serverSocket = new ServerSocket(port);
        Socket socket = null;
        while ((socket == serverSocket.accept()) != null) {
            //接收一个客户端Socket,生成一个HttpRequestHandler,放入线程池执行
            threadPool.execute(new HttpRequestHandler(socket));
        }
        serverSocket.close();
    }

    static class HttpRequestHandler implements Runnable {
        private Socket socket;
        public HttpRequestHandler(Socket socket) {
            this.socket = socket;
        }

        @Override
        public void run() {
            String line = null;
            BufferedReader br = null;
            BufferedReader reader = null;
            PrintWriter out = null;
            InputStream in = null;
            try {
                reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                String header = reader.readLine();
                //由相对路径计算出绝对路径
                String filePath = basePath + header.split(" ")[1];
                out = new PrintWriter(socket.getOutputStream());
                //若后缀为jpg或ico则读取资源并输出
                if(filePath.endsWith("jpg") || filePath.endsWith("ico")) {
                    in = new FileInputStream(filePath);
                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
                    int i = 0;
                    while ((i = in.read()) != -1) {
                        baos.write(i);
                    }
                    byte[] array = baos.toByteArray();
                    out.println("HTTP/1.1 200 OK");
                    out.println("Server:Molly");
                    out.println("Content-Type: image/jpeg");
                    out.println("Content-Length: " + array.length);
                    out.println("");
                    socket.getOutputStream().write(array, 0, array.length);
                } else {
                    br = new BufferedReader(new InputStreamReader(new FileInputStream(filePath)));
                    out = new PrintWriter(socket.getOutputStream());
                    out.println("HTTP/1.1 200 OK");
                    out.println("Server:Molly");
                    out.println("Content-Type: text/html; charset=UTF-8");
                    out.println("");
                    while ((line = br.readLine()) != null) {
                        out.println(line);
                    }
                }
                out.flush();
            } catch (Exception ex) {
                out.println("HTTP/1.1 500");
                out.println("");
                out.flush();
            } finally {
                close(br, in, reader, out, socket);
            }
        }
    }

    //关闭流或者Socket
    private static void close(Closeable... closeables) {
        if(closeables != null) {
            for(Closeable closeable : closeables) {
                try{
                    closeable.close();
                } catch (Exception ex) {
                }
            }
        }
    }
}