Java并发编程

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java并发编程相关的知识,希望对你有一定的参考价值。

Java并发编程(一)

之前看《Thinking In Java》时,并发讲解的挺多的,自己算是初步了解了并发。但是其讲解的不深入,自己感觉其讲解的不够好。后来自己想再学一学并发,买了《Java并发编程实战》,看了一下讲的好基础、好多的理论,而且自我感觉讲的逻辑性不强。最后,买了本《Java并发编程的艺术》看,这本书挺好的,逻辑性非常强。

1. 概述


本篇文章主要内容来自《Java并发编程的艺术》,其讲解的比较深入,自己也有许多不懂的地方,然后自己主要把它讲解的提炼出来。本章是Java并发编程的基础知识,了解后能够对Java并发有一些基本的了解,其中许多深入计算机系统层面的知识我都滤过。想要深入而具体的了解,请见《Java并发编程的艺术》。

2. 基本知识点


内存可见性:当一个线程修改一个共享变量(存放在堆中:如实例域、静态域、数组等)时,另一个线程总能读到修改后的值。

原子性(原子操作):不可以被中断的一个或一系列操作。

重排序:在执行程序时,为了提高性能,编译器和处理器常常对指令做重排序。

3. Java内存模型(JMM)


3.1 Java的并发编程采用共享内存模型
通信(指线程之间以何种机制交换信息):线程之间共享程序的公共状态,通过写-读内存中的公共状态进行隐式通信。
同步(指程序中用于控制不同线程间操作发生相对顺序的机制):程序员必须显示的指定某个方法或某段代码需要在线程之间互斥执行。

3.2 JMM定义了线程与主内存之间的抽象关系
线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存存储了该线程以读/写共享变量的副本。所以,JMM通过控制主内存与每个线程的本地内容之间的交互,来为Java程序提供内存可见性。
(注:本地内存是一个抽象概念,并不真实存在,它包含缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。)

3.3 JMM不保证对64位的long类型与double类型的写操作具有原子性,但读操作具有原子性
原因:JMM对64位的写操作会拆分为两个32位的写操作来执行。

4. happens-before


4.1 概念:Java中阐述操作之间的内存可见性
在JMM中,如果一个操作的结果需要对另一个操作可见,那么这两个操作之间存在happens-before关系。注:happens-before仅仅要对前一个操作(执行的结果)对后一操作可见,并不意味着操作顺序。

4.2 happens-before的规则:

  • 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。注:JMM中只要求对该线程的结果不会变化,所以只要最后结果没有变化,JMM容许操作系统的重排序。
  • 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  • volatile变量规则:对一个volatile域的写入,happens-before于任意后续对这个volatile域的读。
  • happens-before具有传递性。

5. volatile


5.1 volatile特性

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

5.2 volatile写-读建立的happens-before关系
volatile变量的写-读可以实现线程之间的通信(隐式)。
如下代码:

class VolatileExample {
    private int a = 0;
    private volatile boolean flag = false;

    void write() {
        this.a = 1;		// 1
        this.flag = true;	// 2
    }

    void read() {
        while (flag) {		// 3
            int i = a;		// 4
            ...
        }
    }
}

如果线程A执行write()方法后,线程B执行read()方法。根据happens-before规则:B线程总能够进入循环,并且获得的a的值为1。
程序顺序规则:1 happens-before 2;3 happens-before 4
volatile变量规则:2 happens-before 3
传递规则:1 happens-before 4

5.2 volatile写-读的内存意义

  • 写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中,并通知接下来要读取这个volatile变量的某个线程发出了(其对共享变量所做修改的)消息。
  • 读一个volatile变量,实质上该线程接受到之前某个线程发出的修改消息,JMM会把该线程对应的本地内存置为无效,线程接下来会从主内存中读取共享变量。

6. 锁


6.1 锁与volatile的内存意义基本相似(隐式通信)

  • 锁的释放与volatile变量的写有相同的内存意义。
  • 锁的获取与volatile变量的读有相同的内存意义。

6.2 锁能够让临界区互斥执行(显式同步)

6.3 锁与volatile变量的比较

  • volatile仅仅能够保证单个volatile变量的读/写具有原子性,而锁能够保证整个临界区具有原子性。
  • volatile性能比锁好,而功能不如锁。

注:ReentrantLock锁的实现使用到了volatile变量。自己不是太懂实现机制,就不写了,具体可以参见《Java并发编程的艺术》。

7. final域


7.1 内存意义:
只要final对象时正确构造的,那么不需要使用同步(锁与volatile)就可以保证任何线程都能够看都该对象的final域在构造函数中被初始化的值。

8.应用


8.1 单例模式中可以使用双重检查锁机制来创建单例
代码如下:

class Singleton {
    private volatile static Singleton instance;	// 1
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        if (instance == null) {				// 2
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();		// 3
                }
            }
        }
        return instance;
    }
}

注意:实例引用必须是volatile变量。如果不是,在运行到2时读取到instance不为null,但是可能该instance所指向的对象还没有完成初始化。原因:创建对象时的重排序,自己不太清楚,具体可以参见《Java并发编程的艺术》。

8.2 单例模式中基于类初始化的获取单例(内部类)
代码如下:

class Singleton {
    private Singleton() {}
    
    public static Singleton getInstance() {
        return SingletonHolder.instance;  // 这里才会导致SingletonHolder类的初始化
    }
    
    private static class SingletonHolder{
        static Singleton instance = new Singleton();
    }
}

优点:只有在第一次调用getInstance()方法时才会正式加载SingletonHolder类,也就是才会创建对象。

线程安全的原因:在类的初始化时期,JVM会获取一个锁,该锁可以同步多个线程对同一个类的初始化。















以上是关于Java并发编程的主要内容,如果未能解决你的问题,请参考以下文章

Java并发编程实战 04死锁了怎么办?

Java编程思想之二十 并发

全栈编程系列SpringBoot整合Shiro(含KickoutSessionControlFilter并发在线人数控制以及不生效问题配置启动异常No SecurityManager...)(代码片段

Java编程思想-并发

Java并发编程实战—–synchronized

JUC并发编程 共享模式之工具 JUC CountdownLatch(倒计时锁) -- CountdownLatch应用(等待多个线程准备完毕( 可以覆盖上次的打印内)等待多个远程调用结束)(代码片段