Skip to content

Latest commit

 

History

History
242 lines (188 loc) · 15.4 KB

README.md

File metadata and controls

242 lines (188 loc) · 15.4 KB

Java 并发编程

allst-thread

并发三要素

1、可见性: CPU缓存引起

可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到。
例:
    若变量T1=0,在多核CPU中,一个线程在CPU1上执行操作修改变量T1,这时CPU的操作是先将T1加载到高速缓存中,将T1=100,但是并没有写入到主存中。
    这时线程2在CPU2上执行操作也修改变量T1,同样是加载到高速缓存中,但是初始值还是0,这时候线程2修改的值是不正确的。
    这就是典型的可见性问题。

2、原子性: 分时复用引起

原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
以下代码解读:
这里需要注意的是:i += 1需要三条 CPU 指令
1、将变量 i 从内存读取到 CPU寄存器; 
2、在CPU寄存器中执行 i + 1 操作; 
3、将最后的结果i写入内存(缓存机制导致可能写入的是 CPU 缓存而不是内存)。

由于CPU分时复用(线程切换)的存在,线程1执行了第一条指令后,就切换到线程2执行,假如线程2执行了这三条指令后,再切换会线程1执行后续两条指令,
将造成最后写到内存中的i值是2而不是3。
int i = 1;
// 线程1执行
i += 1;
// 线程2执行
i += 1;

3、有序性: 重排序引起

有序性:即程序执行的顺序按照代码的先后顺序执行。
int i = 0;              
boolean flag = false;
i = 1;                // 语句1  
flag = true;          // 语句2
解读上述代码:
定义了两个不同类型的变量,然后对这两个变量赋值操作,从代码的顺序上看 语句1 是在 语句2前面的,
那么JVM在执行这段代码的时候能保证 语句1 在 语句2 前面执行吗? 答案是:不一定, 因为这里有可能发生指令重排序(Instruction Reorder)

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

从 java 源代码到最终实际执行的指令序列,会分别经历下面三种重排序:
查看图片:images/java-jmm-指令重排序.png
上述的 1 属于编译器重排序,2 和 3 属于处理器重排序。这些重排序都可能会导致多线程程序出现内存可见性问题。
对于编译器,JMM 的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。
对于处理器重排序,JMM 的处理器重排序规则会要求 java 编译器在生成指令序列时,插入特定类型的内存屏障(memory barriers,intel 称之为 memory fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序(不是所有的处理器重排序都要禁止)。

Java是怎么解决并发问题的:JMM

JMM相关 (查看)[allst-note/note/book/README-JMM.md]

JMM本质上可以理解为,Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法。
这些方法包括(在JMM相关有详解):
    volatile、synchronized 和 final 三个关键字
    Happens-Before 规则

可见性,有序性,原子性
【可见性】
Java提供了volatile关键字来保证可见性。 
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。 
而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,
并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

【有序性】
在Java里面,可以通过volatile关键字来保证一定的“有序性”。
另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
当然JMM是通过 Happens-Before 规则来保证有序性的

【原子性】
在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。
x = 10;        //语句1: 直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中
y = x;         //语句2: 包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及 将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。
x++;           //语句3: x++包括3个操作:读取x的值,进行加1操作,写入新的值。
x = x + 1;     //语句4: 同语句3
上面4个语句只有语句1的操作具备原子性。
也就是说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。
从上面可以看出,Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。
由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。

Happens-Before 规则

上面提到了可以用 volatile 和 synchronized 来保证有序性。除此之外,JVM 还规定了先行发生原则,让一个操作无需控制就能先于另一个操作完成.

单一线程原则
    在一个线程内,在程序前面的操作先行发生于后面的操作。

管程锁定规则
    一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。

volatile 变量规则
    对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。

线程启动规则
    Thread 对象的 start() 方法调用先行发生于此线程的每一个动作。

线程加入规则
    Thread 对象的结束先行发生于 join() 方法返回。

线程中断规则
    对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 interrupted() 方法检测到是否有中断发生。

对象终结规则
    一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。
    
传递性
    如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C。



线程安全

    一个类在可以被多个线程安全调用时就是线程安全的。
    线程安全不是一个非真即假的命题,可以将共享数据按照安全程度的强弱顺序分成以下五类: 不可变、绝对线程安全、相对线程安全、线程兼容和线程对立

    🍎不可变:
    不可变(Immutable)的对象一定是线程安全的,不需要再采取任何的线程安全保障措施。只要一个不可变的对象被正确地构建出来,永远也不会看到它在多个线程之中处于不一致的状态。
    多线程环境下,应当尽量使对象成为不可变,来满足线程安全。
    不可变类型:    
        1、final 关键字修饰的基本数据类型 
        2、String 
        3、枚举类型 
        4、Number 部分子类,如 Long 和 Double 等数值包装类型,BigInteger 和 BigDecimal 等大数据类型。但同为 Number 的原子类 AtomicInteger 和 AtomicLong 则是可变的。

    对于集合类型,可以使用 Collections.unmodifiableXXX() 方法来获取一个不可变的集合。

    🍎绝对线程安全:
    不管运行时环境如何,调用者都不需要任何额外的同步措施。
    
    🍎相对线程安全:
    相对线程安全需要保证对这个对象单独的操作是线程安全的,在调用的时候不需要做额外的保障措施。但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。
    在 Java 语言中,大部分的线程安全类都属于这种类型,例如 Vector、HashTable、Collections 的 synchronizedCollection() 方法包装的集合等。 
    对于下面的代码,如果删除元素的线程删除了 Vector 的一个元素,而获取元素的线程试图访问一个已经被删除的元素,那么就会抛出 ArrayIndexOutOfBoundsException。

    🍎线程兼容:    
    线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用,我们平常说一个类不是线程安全的,绝大多数时候指的是这一种情况。
    Java API 中大部分的类都是属于线程兼容的,如与前面的 Vector 和 HashTable 相对应的集合类 ArrayList 和 HashMap 等。

    🍎线程对立:
    线程对立是指无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码。由于 Java 语言天生就具备多线程特性,线程对立这种排斥多线程的代码是很少出现的,而且通常都是有害的,应当尽量避免。

线程安全的实现方法

1、互斥同步
    synchronized 和 ReentrantLock。

2、非阻塞同步
    互斥同步最主要的问题就是线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。
    
    互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,那就肯定会出现问题。
    无论共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。

    CAS:
    随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略: 先进行操作,如果没有其它线程争用共享数据,那操作就成功了,否则采取补偿措施(不断地重试,直到成功为止)。
    这种乐观的并发策略的许多实现都不需要将线程阻塞,因此这种同步操作称为非阻塞同步。

    乐观锁需要操作和冲突检测这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。硬件支持的原子性操作最典型的是: 比较并交换(Compare-and-Swap,CAS)。
    CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B。

Java原子类

Java中的原子类位于java.util.concurrent.atomic包下,它们提供了一种线程安全的、能够确保原子性操作的数据类型。以下是一些主要的原子类:

  1. 基本类型原子类

    • AtomicInteger:用于对整数进行原子操作。
    • AtomicLong:用于对长整型(64位)进行原子操作。
    • AtomicBoolean:用于对布尔值进行原子操作。
  2. 数组类型原子类

    • AtomicIntegerArray:用于对整数数组进行原子操作。
    • AtomicLongArray:用于对长整型数组进行原子操作。
    • AtomicReferenceArray<E>:用于对引用类型数组进行原子操作。
  3. 引用类型原子类

    • AtomicReference<V>:用于对任意引用类型对象进行原子更新。
    • AtomicStampedReference<V>:带有版本号或“戳记”的原子引用,可以解决ABA问题。
    • AtomicMarkableReference<V>:带有标记位的原子引用,可用于多状态同步场景。
  4. 字段更新原子类

    • AtomicIntegerFieldUpdater<T>:用于原子地更新指定类中声明的整型字段。
    • AtomicLongFieldUpdater<T>:用于原子地更新指定类中声明的长整型字段。
    • AtomicReferenceFieldUpdater<T, V>:用于原子地更新指定类中声明的引用类型字段。

这些原子类均利用了CAS(Compare and Swap / Compare and Set)机制来实现无锁编程,从而在高并发环境下能有效地执行非阻塞式的原子操作。

Java线程相关的类

Java中涉及线程相关的类主要包括以下几种:

  1. java.lang.Thread

    • 这是Java中最基础的线程实现类,通过继承Thread类或实现Runnable接口创建线程。它提供了构造函数、start()方法用于启动线程,以及一些控制线程的方法如join(), sleep(), setName(), setPriority()等。
  2. java.lang.Runnable

    • Runnable是一个接口,它只包含一个run()方法,实现了Runnable接口的类可以被Thread类包装并在新的线程中运行。这种方式允许用户定义的任务类同时继承其他类。
  3. java.util.concurrent.atomic包下的原子类

    • 包括AtomicInteger, AtomicLong, AtomicIntegerArray, AtomicLongArray等,这些类提供了一种在高并发环境下进行原子操作的数据类型,确保了多线程环境下的数据一致性。
  4. java.util.concurrent.locks包下的锁与同步器

    • ReentrantLock:可重入锁,提供了比内置synchronized关键字更灵活的锁定机制。
    • Condition:与ReentrantLock配合使用的条件队列,用于线程间的协作和通知机制。
    • Semaphore(信号量):用于控制同时访问特定资源的线程数量。
    • CountDownLatch:计数器门闩,等待多个线程完成后再执行后续动作。
    • CyclicBarrier:循环栅栏,让一组线程等待至所有线程都到达某个屏障点再继续执行。
  5. java.util.concurrent.ExecutorService 和 Executors

    • ExecutorService 是一个接口,它是线程池的核心接口,用于管理和控制异步任务的执行。
    • Executors 类提供了工厂方法来创建不同类型的ExecutorService,例如单线程、固定大小线程池、可缓存线程池等。
  6. java.util.concurrent.Future 和 FutureTask

    • Future 接口表示异步计算的结果,可用于获取线程执行完毕后的结果。
    • FutureTask 是Future接口的实现类,同时扩展了Runnable接口,因此可以作为线程的任务并能返回结果。
  7. java.util.concurrent.ConcurrentHashMap

    • 线程安全的哈希表实现,适合于并发环境中使用。
  8. java.util.concurrent.ConcurrentLinkedQueue

    • 高效、线程安全的无界链表队列。
  9. ThreadLocal

    • 提供线程局部变量,每个线程都有该变量的一个副本,不会影响其他线程中的副本值,常用于解决多线程间的数据隔离问题。

以上都是Java编程语言中与线程密切相关的类,它们共同构成了Java强大的多线程支持框架。