[书]java并发编程的艺术笔记

第1章 并发编程的挑战1.多线程一定比单线程快? 不一定,如同在同时阅读两本书时的来回切换切换是会影响读书效率的,同样上下文切换也会影响多线程的执行速度. 并发执行的速度会比串行慢:这是因为线程有创建和上下文切换的开销.2.如何减少上下文切换 减少上下文切换的方法有无锁并发编程(避免使用锁)、CAS算法(Java的Atomic包使用CAS算法来更新数据,而不需要加锁)、使用最少线程...
摘要由CSDN通过智能技术生成

本文属于自己整理的读书笔记,便于回顾.内容绝大部分来自书籍:java并发编程的艺术,版权归原作者所有.


第1章 并发编程的挑战

1.多线程一定比单线程快?

不一定,如同在同时阅读两本书时的来回切换切换是会影响读书效率的,同样上下文切换也会影响多线程的执行速度.
并发执行的速度会比串行慢:这是因为线程有创建和上下文切换的开销.

2.如何减少上下文切换

减少上下文切换的方法有无锁并发编程(避免使用锁)、CAS算法(Java的Atomic包使用CAS算法来更新数据,而不需要加锁)、使用最少线程(避免创建不需要的线)和使用协程.

3.一段死锁的代码

public class DeadLockDemo {
   
    privat static String A = "A";
    private static String B = "B";
    public static void main(String[] args) {
    new DeadLockDemo().deadLock();
}
private void deadLock() {
    Thread t1 = new Thread(new Runnable() {
    @Override
    publicvoid run() {
        synchronized (A) {
            try { Thread.currentThread().sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }synchronized (B) {
            System.out.println("1");
        }
   }
}
});
    Thread t2 = new Thread(new Runnable() {
    @Override
        publicvoid run() {
        synchronized (B) {
            synchronized (A) {
            System.out.println("2");
            }
        }
    }
});
    t1.start();
    t2.start();
    }
}

4.避免死锁:

  • 避免一个线程同时获得多个锁
  • 避免一个线程在锁内占用多个资源,
  • 使用定时锁(尝试使用lock.tryLock(timeout))
  • 对于数据库锁,加锁和解锁必须在一个数据库连接中

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

5.volatile

在多处理器开发中保证了共享变量的“可见性“,一般它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度.

6.volatile的两条实现原则

  • Lock前缀指令(volatile变量相关操作转变成编程汇编代码,回带lock前缀)会引起处理器缓存回写到内存
  • 一个处理器的缓存回写到内存会导致其他处理器的缓存无效(迫使工作内存中的变量重新读取主内存的最新的共享变量的值)

7.synchronized

synchronized实现同步的基础:Java中的每一个对象都可以作为锁.

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

8 . Synchonized在JVM里的实现原理:

JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细不一样。代码块同步是使用monitorenter和monitorexit指令实现的,方法同步在JVM规范中没有指明.

monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处.任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态.

9.锁的升级与对比

Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,锁可以升级但不能降级(提供获得锁和释放锁的效率).
这里写图片描述

这里写图片描述

10.Java如何实现原子操作

原子操作:不可被中断的一个或一系列操作
Java如何实现原子操作:

  • 使用循环CAS实现原子操作
  • 使用锁机制实现原子操作

CAS :
CAS 操作 :cas操作需要两个值,一个旧值A(期望操作前的值),一个新值B,操作时,先比较旧值A有没有发生修改,没有发生变化,才交换成新值B,否则不做交换.

CAS仍然存在三大问题
ABA问题,循环时间长开销大,以及只能保证一个共享变量的原子操作.

  • ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A
  • 循环时间长开销大解决:如果JVM能支持处理器提供的pause指令,那么效率会有一定的提升
  • 当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作.

第3章 Java内存模型

11.java memory model:

Java的并发采用的是共享内存模型;
JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。
这里写图片描述

如果线程A与线程B之间要通信的话,必须要经历下面2个步骤。
1)线程A把本地内存A中更新过的共享变量刷新到主内存中去。
2)线程B到主内存中去读取线程A之前已更新过的共享变量。

12.重排序:

代码实际执行顺序和代码编写顺序并不是一样的:
重排序分为三种:
1)编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
2)指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level
Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
3)内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

13.happens-before:

在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系:(深入jvm虚拟机中描述的更为详细):

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

happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前(the first is visible to and ordered before the second)。

14.as-if-serial语义

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

15.顺序一致性

如果程序是正确同步的,程序的执行将具有顺序一致性(Sequentially Consistent)——即程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同.即经过正确同步,程序执行应该具有内存一致性.

从上面的示意图可以看出,在任意时间点最多只能有一个线程可以连接到内存。当多个线程并发执行时,图中的开关装置能把所有线程的所有内存读/写操作串行化(即在顺序一致性模型中,所有操作之间具有全序关系)。

16.volatile的内存语义

volatile变量自身特性:

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

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

下面对volatile写和volatile读的内存语义做个总结:

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

保守策略的JMM**内存屏障插入策略**:

  • 在每个volatile写操作的前面插入一个StoreStore屏障。
  • 在每个volatile写操作的后面插入一个StoreLoad屏障。
  • 在每个volatile读操作的前面插入一个LoadLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadStore屏障。

17 .锁的内存语义

对锁释放和锁获取的内存语义做个总结:

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

18.Java线程之间的通信

由于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变量。

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

19.happens-before

这里写图片描述

happens-before关系本质上和as-if-serial语义是一回事.

  • as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。
  • as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。
  • as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度

20.双重检查锁前后

非线程安全的初始化对象:

public class UnsafeLazyInitialization {
    private static Instance instance;
    public static Instance getInstance() {
        if (instance == null) // 1:A线程执行
        instance = new Instance(); // 2:B线程执行
        return instance;
    }
}

加synchronized :早期的JVM中,synchronized(甚至是无竞争的synchronized)存在巨大的性能开销

public class SafeLazyInitialization {
    private static Instance instance;
    public synchronized static Instance getInstance() {
        if (instance == null)
        instance = new Instance();
        return instance;
    }
}

双重检查锁定(Double-Checked Locking):

public class DoubleCheckedLocking { // 1
    private static Instance instance; // 2
    public static Instance getInstance() { // 3
        if (instance == null) { // 4:第一次检查
        synchronized (DoubleCheckedLocking.class) { // 5:加锁
            if (instance == null) // 6:第二次检查
            instance = new Instance(); // 7:问题的根源出在这里
            } // 8
        } // 9
        return instance; // 10
    } // 11
}

问题根源:
双重检查锁定看起来似乎很完美,但这是一个错误的优化!在线程执行到第4行,代码读取到instance不为null时,instance引用的对象有可能还没有完成初始化

前面的双重检查锁定示例代码的第7行(instance=new Singleton();)创建了一个对象。这一行代码可以分解为如下的3行伪代码。

memory = allocate(); // 1:分配对象的内存空间
ctorInstance(memory); // 2:初始化对象
instance = memory; // 3:设置instance指向刚分配的内存地址

上面3行伪代码中的2和3之间,可能会被重排序:

memory = allocate(); // 1:分配对象的内存空间
instance = memory; // 3:设置instance指向刚分配的内存地址
// 注意,此时对象还没有被初始化!
ctorInstance(memory); // 2:初始化对象

根据java语言规范,所有线程在执行Java程序时必须要遵守intra-thread semantics。intra-thread semantics保证重排序不会改变单线程内的程序执行结果。换句话说,intra-thread semantics允许那些在单线程内,不会改变单线程程序执行结果的重排序。上面3行伪代码的2和3之间虽然被重排序了,但这个重排序并不会违反intra-thread semantics。

单线程的执行示意图:
这里写图片描述
多线程执行示意图:
这里写图片描述

解决办法:
1.基于volatile的解决方案

public class SafeDoubleCheckedLocking {
    private volatile static Instance instance;
        public static Instance getInstance() {
            if (instance == null) {
            synchronized (SafeDoubleCheckedLocking.class) {
                if (instance == null)
                instance = new Instance(); // instance为volatile,现在没问题了
                }
            }
        return instance;
    }
}

加入volatile关键字之后,图3-38 的2和3之间的重排序将会被禁止,所以就不会再有问题了.

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

public class InstanceFactory {
    private static class InstanceHolder {
        public static Instance instance = new Instance();
    }
    public static Instance getInstance() {
        return InstanceHolder.instance ; // 这里将导致InstanceHolder类被初始化
    }
}

示意图:
这里写图片描述
这个方案的实质是:允许3.8.2节中的3行伪代码中的2和3重排序,但不允许非构造线程(这里指线程B)“看到”这个重排序.

结论:
通过对比基于volatile的双重检查锁定的方案和基于类初始化的方案,我们会发现基于类初始化的方案的实现代码更简洁。但基于volatile的双重检查锁定的方案有一个额外的优势:除了可以对静态字段实现延迟初始化外,还可以对实例字段实现延迟初始化
字段延迟初始化降低了初始化类或创建实例的开销,但增加了访问被延迟初始化的字段的开销。在大多数时候,正常的初始化要优于延迟初始化。如果需要对实例字段使用线程安全的延迟初始化,请使用基于volatile的延迟初始化的方案;如果确实需要对静态字段使用线程安全的延迟初始化,请使用上面介绍的基于类初始化的方案。

PS :java类初始化的同步机制:(看书)


第4章 Java并发编程基础

21.线程的状态

这里写图片描述

22.Java线程状态变迁

这里写图片描述

Thread.join()源码:
等待Thread执行结束之后,才继续其他线程的工作,(但是这样会使多线程变单线程):
当线程终止时,会调用线程自身的notifyAll()方法,会通知所有等待在该线程对象上的线程。

// 加锁当前线程对象
public final synchronized void join() throws InterruptedException {
    // 条件不满足,继续等待
    while (isAlive()) {
        wait(0);
    } // 条件符合,方法返回
}

23.ThreadLocal的使用

ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。
可以通过set(T)方法来设置一个值,在当前线程下再通过get()方法获取到原先设置的值。
在代码4-15所示的例子中,构建了一个常用的Profiler类,它具有begin()和end()两个方法,而end()方法返回从begin()方法调用开始到end()方法被调用时的时间差,单位是毫秒。
list 4-15:

package loveStudy.core.listener;

import java.util.concurrent.TimeUnit;

public class Profiler {
   
    // 第一次get()方法调用时会进行初始化(如果set方法没有调用),每个线程会调用一次
    private static final ThreadLocal<Long> TIME_THREADLOCAL = new ThreadLocal<Long>() {
        protected 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");
    }
}

24:等待超时模式

等待超时模式的伪代码:

// 对当前对象加锁
    public synchronized Object get(long mills) throws InterruptedException {
        long future = System.currentTimeMillis() + mills;
        long remaining = mills;
        // 当超时大于0并且result返回值不满足要求
        while ((result == null) && remaining > 0) {
            wait(remaining);
            remaining = future - System.currentTimeMillis();
        }
        return result;
    }

25.一个简单的数据库连接池示例

我们使用等待超时模式来构造一个简单的数据库连接池,在示例中模拟从连接池中获取、使用和释放连接的过程,而客户端获取连接的过程被设定为等待超时的模式,也就是在1000毫秒内如果无法获取到可用连接,将会返回给客户端一个null。设定连接池的大小为10个,然后通过调节客户端的线程数来模拟无法获取连接的场景。
首先看一下连接池的定义。它通过构造函数初始化连接的最大上限,通过一个双向队列来维护连接,调用方需要先调用fetchConnection(long)方法来指定在多少毫秒内超时获取连接,当连接使用完成后,需要调用releaseConnection(Connection)方法将连接放回线程池,示例如代码清单4-16所示。

import java.sql.Connection;
import java.util.LinkedList;

public class ConnectionPool {
   
    private LinkedList<Connection> pool = new LinkedList<Connection>();

    public ConnectionPool(int initialSize) {
        if (initialSize > 0) {
            for (int i = 0; i < initialSize; i++) {
                pool.addLast(ConnectionDriver.createConnection());
            }
        }
    }

    public void releaseConnection(Connection connection) {
        if (connection != null) {
            synchronized (pool) {
                // 连接释放后需要进行通知,这样其他消费者能够感知到连接池中已经归还了一个连接
                pool.addLast(connection);
                pool.notifyAll();
            }
        }
    }// 在mills内无法获取到连接,将会返回null

    public Connection fetchConnection(long mills) throws InterruptedException {
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值