Java并发理论基础
一、多线程的便利
- 更多的处理器核心
- 更快的响应速度
二、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 后写回,这就会造成“丢失更新”,即所谓的 线程不安全“
如何避免并发不安全
-
加锁同步:
synchronized
、ReentrantLock
-
使用原子类:如
AtomicInteger
-
使用并发容器:
ConcurrentHashMap
、CopyOnWriteArrayList
三、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
)。 -
多个线程调用同一个实例的同步方法时会被串行执行。
2. 修饰同步代码块
代码示例:
public class SyncBlockDemo {
private final Object lock = new Object();
public void syncBlock() {
synchronized (lock) {
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) {
SyncBlockDemo demo = new SyncBlockDemo();
new Thread(() -> demo.syncBlock(), "T3").start();
new Thread(() -> demo.syncBlock(), "T4").start();
}
}
运行结果:
T3 进入同步代码块
T3 退出同步代码块
T4 进入同步代码块
T4 退出同步代码块
解释运行过程:
- 主线程中创建了一个
SyncBlockDemo
对象,只有 一个lock
对象。 - 启动了两个线程:T3 和 T4,都调用
demo.syncBlock()
。 - 两个线程几乎同时执行,在执行
synchronized (lock)
时会尝试获取 lock 的锁。 - 哪个线程先抢到,哪个线程就会先执行临界区的内容(打印+睡眠500ms)。
- 另一个线程会进入阻塞状态,直到锁释放为止。
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 多少个对象,这个锁都是一样的
volatile 关键字
- 保证可见性、禁止指令重排序
- 不保证原子性(适用于状态标志)
可见性的实现
代码示例:
import java.util.concurrent.TimeUnit;
public class VolatileExample1 {
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();
}
}
有序性体现在哪里?
a = 1
在flag = true
之前执行;flag = true
是volatile
写;- 在另一个线程中读取
flag
(volatile
读)为 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<>(); // 报错