Java并发理论基础

Java并发理论基础

Java并发理论基础

一、多线程的便利

  1. 更多的处理器核心

  2. 更快的响应速度


二、Java多线程并发不安全

指的是多个线程同时访问共享资源时,如果没有正确同步,可能会出现数据错误、程序异常或逻辑混乱的情况

并发不安全的核心问题:共享资源 + 缺乏同步

例如:

 public class Counter {
     private int count = 0;
 ​
     public void increment() {
         count++;
     }
 ​
     public int getCount() {
         return count;
     }
 }

如果多个线程同时调用increment()方法,预期结果是每次加1,但并发场景下会出现 加了多次结果却没变或者变少的 情况。

这是因为:

 count++ 实际上分为三步:
 1. 读取 count 的值
 2. +1
 3. 写回 count

多个线程可能都在几乎同时读取到相同的值,执行 +1 后写回,这就会造成“丢失更新”,即所谓的 线程不安全


如何避免并发不安全

  1. 加锁同步synchronizedReentrantLock

  2. 使用原子类:如 AtomicInteger

  3. 使用并发容器ConcurrentHashMapCopyOnWriteArrayList


三、Java多线程并发出现问题的根源

1. 可见性问题 —— 线程之间看不到彼此的最新数据

具有可见性:是指一个线程对共享变量的修改,另外一个线程能够立刻看到

现象:

一个线程对共享变量的修改,对另一个线程不可见。

举例:

 volatile boolean running = true;
 ​
 Thread t1 = new Thread(() -> {
     while (running) {
         // do something
     }
 });

如果没有volatile,另一个线程把running = false;改,但t1可能永远看不到这个修改

根源分析:

  • Java 的内存模型(JMM)中,每个线程有自己的工作内存(类似缓存),变量修改不会立即同步到主内存

  • 导致一个线程的变更,其他线程看不到


2. 原子性问题 —— 操作是不可分割的

原子性:即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行

现象:

i++ 这样的操作在多线程下会出现“丢失更新”问题

举例:

 count++; // 实际包含3步操作
 1. 读取 count 的值
 2. +1
 3. 写回 count

多个线程同时读写一个变量时,操作被中断或交叉执行,结果就会出错

根源分析:

  • Java 中的普通变量操作不是原子性的

  • 虽然某些语句只有一行,但背后可能包含多步,如读 => 改 => 写

  • 在没有同步机制时,同时操作同一个共享变量,且操作过程没有正确同步,最终导致彼此的数据读错、写乱、更新丢失


3. 有序性问题 —— 指令可能被重排

有序性:即程序执行的顺序按照代码的先后顺序执行

现象:

变量可能在尚未完成初始化时就被另一个线程访问。

举例:

 Room room = new Room();

这里的 room 是一个 引用变量,它并不直接存储对象本身,而是存储“指向堆内存中实际对象的地址”。

可以理解成:

  • room 是遥控器(引用)

  • new Room() 创建的是电视(对象)

什么叫“先把引用赋值”?

JVM 在执行 room = new Room(); 时,正常流程是:

 1. 在堆内存中分配空间
 2. 执行构造方法(初始化对象)
 3. 将地址赋值给变量 room(即引用)

但为了性能优化,JVM 可能会把“步骤3”提前到“步骤2”之前

 1. 分配内存
 2. 将地址赋值给变量 room(引用赋值)
 3. 执行构造方法(对象还没初始化 )

引用变量先获得了堆内存地址(即对象的引用),但这块内存中对应的对象还没有被完全初始化。

根源分析:

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


四、Java 解决并发问题的方式

synchronized(内置锁)

本质是一种互斥锁机制

  • 保证原子性可见性有序性。同一时刻,只能有一个线程进入同步代码块/方法

  • 对象或类作为锁对象,自动加锁解锁

1. 修饰同步方法(锁住当前对象)

代码示例:

 public class SyncInstanceMethodDemo {
     public synchronized void syncMethod() {
         System.out.println(Thread.currentThread().getName() + " 进入实例同步方法");
         try { Thread.sleep(500); } catch (InterruptedException ignored) {}
         System.out.println(Thread.currentThread().getName() + " 退出实例同步方法");
     }
 ​
     public static void main(String[] args) {
         SyncInstanceMethodDemo demo = new SyncInstanceMethodDemo();
         new Thread(() -> demo.syncMethod(), "T1").start();
         new Thread(() -> demo.syncMethod(), "T2").start();
     }
 }

运行结果:

 T1 进入实例同步方法
 T1 退出实例同步方法
 T2 进入实例同步方法
 T2 退出实例同步方法
  • 锁对象是当前实例对象(this)。

  • 多个线程调用同一个实例的同步方法时会被串行执行。


可能遇到的问题

 public class AccountingSyncBad implements Runnable {
     //共享资源(临界资源)
     static int i = 0;
     // synchronized 同步方法
     public synchronized void increase() {
         i ++;
     }
 ​
     @Override
     public void run() {
         for(int j=0;j<1000000;j++){
             increase();
         }
     }
 ​
     public static void main(String args[]) throws InterruptedException {
         // new 两个AccountingSync新实例
         Thread t1 = new Thread(new AccountingSyncBad());
         Thread t2 = new Thread(new AccountingSyncBad());
         t1.start();
         t2.start();
         t1.join();
         t2.join();
         System.out.println("static, i output:" + i);
     }
 }

上述代码创建了两个对象 AccountingSyncBad,然后启动两个不同的线程对共享变量 i 进行操作,操作结果是 1224617 而不是期望的结果 2000000

因为上述代码犯了严重的错误,虽然使用了 synchronized 同步 increase 方法,但却 new 了两个不同的对象,这也就意味着存在着两个不同的对象锁,因此 t1 和 t2 都会进入各自的对象锁,也就是说 t1 和 t2 线程使用的是不同的锁,因此线程安全是无法保证的。

每个对象都有一个对象锁,不同的对象,他们的锁不会互相影响

解决方案:

解决这种问题的的方式是将 synchronized 作用于静态的 increase 方法,这样的话,对象锁就锁的是当前的类,由于无论创建多少个对象,类永远只有一个,所有在这样的情况下对象锁就是唯一的


2. 修饰同步代码块

某些情况下,我们编写的方法代码量比较多,存在一些比较耗时的操作,而需要同步的代码块只有一小部分如果直接对整个方法进行同步,可能会得不偿失,此时我们可以使用同步代码块的方式对需要同步的代码进行包裹

代码示例:

 public class AccountingSync2 implements Runnable {
     static AccountingSync2 instance = new AccountingSync2(); // 饿汉单例模式
 ​
     static int i=0;
 ​
     @Override
     public void run() {
         //省略其他耗时操作....
         //使用同步代码块对变量i进行同步操作,锁对象为instance
         synchronized(instance){
             for(int j=0;j<1000000;j++){
                 i++;
             }
         }
     }
 ​
     public static void main(String[] args) throws InterruptedException {
         Thread t1=new Thread(instance);
         Thread t2=new Thread(instance);
         t1.start();t2.start();
         t1.join();t2.join();
         System.out.println(i);
     }
 }

运行结果:

 /**
  * 输出结果:
  * 2000000
  */
  • 将 synchronized 作用于一个给定的实例对象 instance,即当前实例对象就是锁的对象,当线程进入 synchronized 包裹的代码块时就会要求当前线程持有 instance 实例对象的锁,如果当前有其他线程正持有该对象锁,那么新的线程就必须等待,这样就保证了每次只有一个线程执行 i++ 操作。

当然除了用 instance 作为对象外,我们还可以使用 this 对象(代表当前实例)或者当前类的 Class 对象作为锁,如下代码:

 synchronized(this) {
     for (int j = 0; j < 1000000; j++) {
         i++;
     }
 }

那么 synchronized(this) 实际上和 synchronized(instance) 效果完全相同

  • 因为 t1 和 t2 运行的都是同一个 Runnable 实例 → this 是同一个对象。

  • 所以它们还是会竞争同一把锁,最终结果依然是 2000000

但是,t1 和 t2 分别 new 不同的 Runnable 实例 时:

 Thread t1 = new Thread(new AccountingSync2());
 Thread t2 = new Thread(new AccountingSync2());
  • t1 里的 this 是一个对象,t2 里的 this 是另一个对象。

  • 两个线程进入 synchronized(this) 时,各自拿到的是不同的锁 → 锁不共享

  • 结果就会出错(小于 2000000)。


3. 修饰静态同步方法

代码示例:

 public class StaticSyncMethodDemo {
     public static synchronized void staticSyncMethod() {
         System.out.println(Thread.currentThread().getName() + " 进入静态同步方法");
         try { Thread.sleep(500); } catch (InterruptedException ignored) {}
         System.out.println(Thread.currentThread().getName() + " 退出静态同步方法");
     }
 ​
     public static void main(String[] args) {
         new Thread(() -> StaticSyncMethodDemo.staticSyncMethod(), "T5").start();
         new Thread(() -> StaticSyncMethodDemo.staticSyncMethod(), "T6").start();
     }
 }

运行结果:

 T5 进入静态同步方法
 T5 退出静态同步方法
 T6 进入静态同步方法
 T6 退出静态同步方法

说明:

  • 静态同步方法锁住类的 Class对象,两个线程顺序执行。

  • 静态同步方法锁的是“类级别的锁”,也就是 JVM 为当前类创建的唯一 Class 对象,如 Example.class,不论你 new 多少个对象,这个锁都是一样的

4. 原理分析

4.1 加锁释放锁原理

同步块的实现使用了monitorentermonitorexit指令

同步方法则是依靠方法修饰符上的ACC_SYNCHRONIZED来完成

本质是对一个对象的监视器进行获取,而这个过程是排他的,也就是同一时刻只能有一个线程获取到由synchronized所保护对象的监视器。

注:synchronized 是非公平锁


4.2 可重入的实现原理



volatile 关键字

  • 保证可见性禁止指令重排序

  • 不保证原子性(适用于状态标志)

可见性的实现

代码示例:

 import java.util.concurrent.TimeUnit;
 ​
 public class VolatileExample1 {
     /**
      * volatile写-读的内存语义:
      * 1. 当写一个volatile变量时,JMM会把该线程对应本地内存中的共享变量值刷新到主内存
      * 2. 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主存中读取共享变量
 ​
      * 1.stop变量增加volatile关键字之后,主线程修改stop=true,此时并不会只修改本地内存。JMM还会触发本地
      * 内存到主内存的回写,所以主内存中stop就会变成true。
      * 2.当线程A尝试读取volatile关键字下的stop变量的时候,就不会从再从本地内存读了,转而去读取主内存中的
      * stop值,此时读取到的stop =true。两个线程一致了。
      */
 ​
     // 加 volatile,保证可见性
     // 如果一个字段被声明为volatile,Java内存模型(JMM)确保所有线程看到这个变量的值是一样的
     private static volatile boolean stop = false;
 ​
     public static void main(String[] args) {
         // Thread-A
         new Thread(() -> {
             while (!stop) {
                 // 模拟一些工作
             }
             System.out.println("3: " + Thread.currentThread().getName() + " 停止了");
         }, "Thread A").start();
 ​
         // Thread-main
         try {
             TimeUnit.SECONDS.sleep(1);
             System.out.println("1: 主线程等待一秒...");
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
 ​
         System.out.println("2: 将 stop 变量设置为 true");
         stop = true;
     }
 }

运行结果:

 1: 主线程等待一秒...
 2: 将 stop 变量设置为 true
 3: Thread A 停止了

有序性实现

代码示例:

 public class VolatileOrderExample {
 ​
     int a = 0;
     volatile boolean flag = false;
 ​
     public void writer() {
         a = 1;          // 写共享变量a
         flag = true;    // 写 volatile变量
     }
 ​
     public void reader() {
         if (flag) {     // 读 volatile变量
             System.out.println(a); // 有序性保证下,这里一定会输出1
         }
     }
 ​
     public static void main(String[] args) {
         VolatileOrderExample example = new VolatileOrderExample();
 ​
         Thread t1 = new Thread(example::writer);
         Thread t2 = new Thread(example::reader);
 ​
         t1.start();
         t2.start();
     }
 }
有序性体现在哪里?

volatile 关键字修饰共享变量会禁止重排序,执行到 volatile 变量,其前面的所有语句必须执行完毕,其后面语句都未执行

当我们使用 volatile 关键字来修饰一个变量时,Java 内存模型会插入内存屏障(一个处理器指令,可以对 CPU 或编译器重排序做出约束)来确保以下两点:

  • 写屏障(Write Barrier):当一个 volatile 变量被写入时,写屏障确保在该屏障之前的所有变量的写入操作都提交到主内存

  • 读屏障(Read Barrier):当读取一个 volatile 变量时,读屏障确保在该屏障之后的所有读操作都从主内存中读取

换句话说:

  • 当程序执行到 volatile 变量的读操作或者写操作时,在其前面操作的更改肯定已经全部进行,且结果对后面的操作可见;在其后面的操作肯定还没有进行;

  • 在进行指令优化时,不能将 volatile 变量的语句放在其后面执行,也不能把 volatile 变量后面的语句放到其前面执行。

  • a = 1flag = true 之前执行;

  • flag = truevolatile 写;

  • 在另一个线程中读取 flagvolatile 读)为 true 后,根据 happens-before 原则,a = 1 的写入对该线程可见

如果没有 volatile

可能会出现 flag == true,但 a == 0 的情况(由于指令重排序或可见性问题)。


final 关键字(不可变性)

final 是 Java 的一个修饰符,用于 修饰类、方法和变量,表示不可更改的含义:

使用位置

表示含义

不能被继承

方法

不能被重写(可以重载)

变量

值不能被修改(常量)


修饰类(final class)

作用:

final 修饰的类不能被继承。

示例:
 final class Animal {
     public void run() {
         System.out.println("Animal is running.");
     }
 }
 ​
 // 错误示例:
 // class Dog extends Animal {} // 报错:不能继承 final 类

修饰方法(final method)

作用:

final 修饰的方法不能被子类重写(Override)。

示例:
 class Parent {
     public final void sayHello() {
         System.out.println("Hello from parent.");
     }
 }
 ​
 class Child extends Parent {
     // 报错:不能重写 final 方法
     // public void sayHello() {}
 }

修饰变量

作用:

变量值一旦赋值之后不能再更改,即表示常量。

分类:

类型

示例

特点

final 局部变量

方法内声明的 final 变量

赋值后不可改,常用于 lambda

final 成员变量

类中的 final 字段

一般和构造器搭配赋值

final 静态变量

static final 修饰

全局常量,命名全大写 MAX_VALUE

示例:
 public class Demo {
     final int age = 18;
     static final String SCHOOL_NAME = "OpenAI Academy";
  
     public void test() {
         final int x = 100;
         // x = 200; // 报错:不能再赋值
     }
 }
注意:
  • final 成员变量 必须在定义时或构造函数中初始化

  • final 引用类型变量:引用不能变,但对象内容可以变

 final List<String> list = new ArrayList<>();
 list.add("hello"); // 合法
 // list = new ArrayList<>(); // 报错

五、Java内存模型(JMM)

定义

Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入时,能对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。

个人第一个网站 2025-12-28

评论区