Java课程笔记

本文详细探讨了Java并发编程中的基础知识,包括共享资源的理解、并发编程的难点——原子性、可见性和有序性,以及JMM内存模型的作用。重点讲解了volatile关键字的特性和实现机制,以及锁的内存语义和常见锁的比较。深入剖析了Atomic类的原子操作和 Unsafe 类的 CAS 方法在并发中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Java并发

参考书目:
《Java并发编程的艺术》
《Java并发实现原理》

一.并发编程的基础

二.原子类

三.锁

四.线程池

五.并发工具

01/什么是共享资源

一.并发编程的基础

是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象。
实例、Java中几乎所有的对象实例都在这里分配内存。
方法区与堆一样,也是各个线程共享的一块内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
哪些数据区的数据可以被共享?
答:堆和方法区中的数据是共享的,堆里面的数据是被栈中的变量所引用、所持有的。栈是线程隔离的,栈是持有堆中数据的。每个线程会私有一个栈,栈中的数据是私有的。


例:假设线程1、线程2调用了方法A,又通过方法A调用了方法B,通过方法B调用了方法C(有多少线程就有多少栈,栈帧体现了线程调用方法的顺序),则内存的变化如下:
在这里插入图片描述

栈中的数据、方法内部的局部变量是线程私有的,是不被共享的,没有必要加锁。堆中的共享变量才有可能需考虑并发的问题、考虑要不要加锁。

02/并发编程的难点

2.1 原子性问题

  • 操作系统做任务切换,可以发生在任何一条CPU指令执行完成之后。

  • CPU能保证的原子操作是指令级别的,而不是高级语言的操作符。
    例:两个线程对共享数据n执行n++,但效果却只执行了一次:
    在这里插入图片描述
    2.2可见性问题

  • 可见性是指一个线程对共享变量的修改,另外一个线程能够立即看到。

  • 可见性问题是由CPU的缓存导致的,多核CPU的每一核均有各自的缓存,这些缓存均要与内存进行同步。

例:线程A、线程B将内存中变量加载到CPU1、CPU2的缓存并做修改,修改后写回内存,于是产生了可见性问题:
在这里插入图片描述
2.3 有序性问题

  • 在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。
  • 重排序不会影响单线程的执行结果,但是在并发情况下,可能会出现诡异的BUG。

例:如指令If(instance==null){instance=new Singleton();}经过编译器指令重排后被线程A、线程B调用,可能会出现如下BUG:
在这里插入图片描述

03/JMM(Java内存模型)

3.1并发编程的关键目标
并发编程需要处理两个关键问题,即线程之间如何通信和同步。

  • 通信:指线程之间以何种机制来交换信息。
  • 同步:指程序中用于控制不同线程之间的操作发生的相对顺序的机制。
    3.2并发编程的内存模型
    共有两种并发编程模型:共享内存模型、消息传递模型,Java采用的是前者。
  • 在共享内存模型下,线程之间共享程序的公共状态,通过写-读内存中的公共状态进行隐式通信。
  • 在共享内存模型下,同步是显示进行的,程序员必须显示指定某段代码需要在线程之间互斥执行。
    3.3这个内存模型叫JMM
    JMM是lava Memory Model的缩写,Java线程之间的通信由JMM控制,即JMM决定一个线程对共享
    变量的写入何时对另一个线程可见。JMM定义了线程和主内存之间的抽象关系,通过控制主内存与每个
    本地内存(抽象概念)之间的交互,]MM为]ava程序员提供了内存可见性的保证。
    例:JMM控制线程之间的通信:
    在这里插入图片描述
    3.4源代码与指令间的重排序
    为了提高性能,编译器和处理器常常会对指令做重排序。重排序有3种类型,其中后2种都是处理
    器重排序。这些重排序可能会导致多线程程序出现内存可见性问题。
    1.编译器优化重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
    2.指令级并行重排序:现代处理器采用了指令级并行技术来将多条指令重叠执行,如果不存在数据依
    赖性,处理器可以改变语句对应机器指令的执行顺序。
    3.内存系统的重排序:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱
    序执行。
编译器
编译器优化重排序
指令级并行重排序
内存系统重排序
最终执行的指令序列

3.5重排序对可见性的影响
参考下表,虽然处理器执行的顺序是A1->A2,但是从内存角度来看,实际发生的顺序是A2->A1。
这里的关键是,由于写缓冲区仅对自己的处理器可见,它会导致处理器执行内存操作的顺序可能会与实
际的操作执行顺序不一致。由于现代的处理器都会使用写缓冲区,因此它们都会允许对写读操作执行
重排序。
在这里插入图片描述
3.6如何解决重排序带来的问题
对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序。对于处理器重排序,JMM的处理器重排序规则会要求编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barries/Memory Fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序。
由于常见的处理器内存模型比JMM要弱,Java编译器在生成字节码时,会在执行指令序列的适当位置插入内存屏障来限制处理器的重排序。同时,由于各种处理器内存模型的强弱不同,为了在不同的处理器平台向程序员展示一个一致的内存模型,JMM在不同的处理器中需要插入的内存屏障的数量和种类也不同。

  • CPU内存屏障
    1.LoadLoad:禁止读和读的重排序
    2.StoreStore:禁止写和写的重排序
    3.LoadStore:禁止读和写的重排序
    4.StoreLoad:禁止写和读的重排序。
  • Java内存屏障:
public final class Unsafe{
public native void loadFence();
//LoadLoad+LoadStore
public native void storeFence();   //StoreStore+LoadStore
public native void fullFence():
//loadFence()+storeFence()+StoreLoad
}

3.7 happens-before
JMM使用happens-before规则来阐述操作之间的内存可见性,以及什么时候不能重排序。在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。
换个角度来说如果A happens-before B,则意味着A的执行结果必须对B可见,也就是保证跨线程的内存可见性。其中,前4条规则与程序员密切相关。
1.程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
2.volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
3.synchronized规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
4.传递性:若A happens-before B,且B happens-before C.则A happens-before C。
5.start()规则:若线程A执行Thread.start(),则线程A的start()操作happens-before于线程B中的任意操作。
6.join()规则:若线程A执行ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.joinOE的成功返回。

举个例子
假设线程A执行writer()方法之后,线程B执行reader()方法。根据happens-before规则,这个过程建立的happens-before关系可以分为3类
1.根据顺序规则,1 happens-before 2, 3 happens-before 4
2.根据volatile规则,2 happens-before 3
3.根据happens-before的传递性规则,1 happens-before4。

public class VolatileExample{
int a=0;
volatile boolean flag false;
public void writer(){
a=1;               //1
flag=true;         //2
}
public void reader(){
if (flag){       //3
int i = a;        //4
        }
      }
  }  

04/volatile

4.1 volatile的基本特性

  • 可见性:对一个volatile变量的读,总是能看到对这个volatile变量最后的写入。
  • 原子性:对任意单个volatile变量的读/写具有原子性,但类似volatile++这种复合操作不具有原子性。
    4.2 volatile的内存语义
  • 写内存语义:当写一个volatile变量时,JMM会把该线程本地内存中的共享变量的值刷新到主内存。
  • 读内存语义:当读一个volatile变量时,JMM会把该线程本地内存置为无效,使其从主内存中读取共享变量。

4.3 volatilel的实现机制
为了实现volatilel的内存语义、编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。内存屏障插入策略非常保守,但它可以保证在任意处理器平台,任意的程序中都能得到正确的volatile内存语义。

  • 在每个volatile写操作的前面插入一个StoreStore屏障。
  • 在每个volatile写操作的后面插入一个StoreLoad屏障。
  • 在每个volatile读操作的后面插入一一个LoadLoad屏障。
    在每个volatile读操作的后面播入一个LoadStore屏障。
    4.4 volatile与锁的对比
    volatile仅仅保证对单个volatile变量的读/写具有原子性,而锁的互斥执行的特性可以确保对整个临界区代码的执行具有原子性。在功能上锁比volatile更强大,在可伸缩性和执行性能上volatile更有优势。
05/锁

5.1锁的内存语义

  • 当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。
  • 当线程获取锁时,JMM会把该线程对应的本地内存置为无效。
    5.2锁的实现机制
  • synchronized:采用“CAS+Mark Word"实现,存在锁升级的情况。
  • Lock:采用“CAS+volatile”实现,存在锁降级的情况,核心是AQS。

为什么需要Lock?
1.Lock支持响应中断。
2.Lock支持超时机制。
3.Lock支持以非阻塞的方式获取锁。
4.Lock支持多个条件变量(阻塞队列)。

二.原子类

-/简介
Java从JDK1.5开始提供了java.util.concurrent.atomic包,这个包中的原子操作类提供了一种用法简单、性能高效、线程安全地更新一个变量的方式。在atomic包里一共提供了17个类,属于4种类型的原子更新方式,分别是原子更新基本类型、原子更新引用类型、原子更新属性、原子更新数组。
atomic包里的类基本都是使用Unsafe实现的包装类,而Unsafe类提供了如下3个CAS方法。这三个方法都带有4个参数,这些参数依次指代:对象、成员变量、期望的值、更新的值。

  • compareAndSwaplnt(Object var1,long var2,int var4,int var5)
  • compareAndSwapLong(Object var1,long var2,long var4,long var6)
  • compareAndSwapObject(Object var1,long var2,Object var4,Object var5)

01/原子更新基本类型
包括3个类:Atomiclnteger、AtomicLong、AtomicBoolean,这3个类提供的方法几乎一模一样。

  • Atomiclntegeri源码:
    以Atomicinteger为例,它包含如下常用的方法:getAndAdd(),addAndGet(),getAndincrement(),incrementAndGet(),compareAndSet()等。
  • Unsafe类的实现源码:
    上述方法底层均利用Unsafe来类实现,涉及到的方法主要有getIntVolatile(),compareAndSwapint()等、这些方法均为本地实现代码,通过本地操作系统底层的API实现原子操作。
  • 其他基本类型的原子操作:
    其他基本类型的变量,如char、float.、double,可以先转换为整型,然后再进行原子操作。例如
    AtomicBoolean就是先把Boolean转换成整型,再使用compareAndSwapInti进行CAS操作。

要求:读源码Atomicinteger、Unsafe、AtomicBoolean。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值