并发编程(三)

synchronized标记

synchronized 的范围标记

在 Java 中,synchronized 关键字可以用于标记代码块或方法,以实现线程同步。范围标记分为两种形式:

  1. 同步代码块

    通过指定一个对象作为锁,仅同步代码块内的逻辑。语法如下:

    java 复制代码
    synchronized (lockObject) {
        // 需要同步的代码
    }

    lockObject 可以是任意对象实例,通常使用共享资源或专用锁对象。

  2. 同步方法

    直接修饰方法,锁对象为当前实例(非静态方法)或类的 Class 对象(静态方法)。语法如下:

    java 复制代码
    public synchronized void method() { ... } // 实例锁
    public static synchronized void staticMethod() { ... } // 类锁

synchronized 的对象标记

synchronized 的行为依赖于锁对象的选择,不同锁对象影响同步范围:

  1. 实例对象锁

    非静态同步方法或代码块使用 this 作为锁时,锁的是当前对象实例。同一实例的多个同步方法会互斥。

  2. 类对象锁

    静态同步方法或代码块使用 Class 对象(如 MyClass.class)时,锁的是整个类,所有实例的同步静态方法会互斥。

  3. 自定义对象锁

    通过显式指定其他对象(如私有成员变量)作为锁,可以实现更细粒度的控制。例如:

    java 复制代码
    private final Object lock = new Object();
    public void method() {
        synchronized (lock) { ... }
    }

关键注意事项

  • 锁对象的唯一性:必须确保多个线程使用的是同一个锁对象才能实现同步。
  • 避免锁泄露:不要使用可能被外部修改的对象(如公共变量)作为锁。
  • 性能影响 :过度使用 synchronized 可能导致线程阻塞,降低并发性能。

通过合理选择锁对象和同步范围,可以平衡线程安全与性能需求。

synchronized锁的升级过程

Java中的synchronized锁会根据竞争情况从偏向锁升级到轻量级锁,最终升级到重量级锁。这种锁升级机制是为了在无竞争和多线程竞争环境下平衡性能。

偏向锁

偏向锁适用于只有一个线程访问同步块的场景。当线程第一次进入同步块时,会在对象头和栈帧中的锁记录中存储偏向的线程ID。后续该线程进入同步块时,只需检查对象头的Mark Word是否指向当前线程ID。

偏向锁的获取过程:

  • 检查对象头的Mark Word是否存储当前线程ID。
  • 如果是,直接进入同步块。
  • 如果不是,尝试通过CAS操作替换Mark Word中的线程ID。
  • 成功则获取锁,失败则升级为轻量级锁。

偏向锁的释放不会主动发生,只有当其他线程尝试获取锁时才会释放。

轻量级锁

当多个线程交替访问同步块但不存在竞争时,锁会升级为轻量级锁。轻量级锁通过CAS操作和自旋来实现同步。

轻量级锁的获取过程:

  • 在栈帧中创建锁记录空间(Lock Record)。
  • 将对象头的Mark Word复制到锁记录中(Displaced Mark Word)。
  • 尝试通过CAS将对象头的Mark Word替换为指向锁记录的指针。
  • 成功则获取锁,失败则自旋重试或升级为重量级锁。

轻量级锁的释放过程:

  • 使用CAS操作将Displaced Mark Word替换回对象头。
  • 成功则释放完成,失败则说明有竞争,锁已膨胀为重量级锁。

重量级锁

当多个线程同时竞争同一锁时,轻量级锁会升级为重量级锁。重量级锁依赖于操作系统提供的互斥量(mutex)实现,涉及线程的阻塞和唤醒,开销较大。

重量级锁的特点:

  • 线程竞争锁失败后会进入阻塞状态。
  • 锁释放时会唤醒等待线程。
  • 涉及用户态和内核态的切换,性能开销较大。
  • 适用于高并发竞争场景。

锁升级的条件

  • 偏向锁升级为轻量级锁:当有第二个线程尝试获取锁时。
  • 轻量级锁升级为重量级锁:当自旋次数超过阈值(默认10次)或等待线程数超过1个。
  • 重量级锁不会降级,除非发生GC(因为GC会清除所有锁状态)。

性能比较

  • 偏向锁:加锁和解锁不需要额外消耗,适合单线程场景。
  • 轻量级锁:线程不会阻塞,通过自旋消耗CPU时间,适合低竞争场景。
  • 重量级锁:线程会阻塞,避免CPU空转,适合高竞争场景。

Java对象头结构

Java对象头是JVM中每个对象实例的元数据部分,存储了对象的运行时信息。对象头通常包含以下两部分:

Mark Word

存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志等。在32位JVM中占32位,64位JVM中占64位(开启压缩指针时为32位)。

Klass Pointer

指向对象所属类的元数据的指针。32位JVM中占32位,64位JVM中占64位(开启压缩指针时为32位)。


Mark Word的详细内容

Mark Word的内容会根据对象状态动态变化,以下是HotSpot虚拟机中的典型布局(以64位系统为例):

无锁状态

复制代码
| 25bit哈希码 | 4bit分代年龄 | 1bit偏向锁标志(0) | 2bit锁标志(01) |

偏向锁状态

复制代码
| 23bit线程ID | 2bit Epoch | 4bit分代年龄 | 1bit偏向锁标志(1) | 2bit锁标志(01) |

轻量级锁状态

复制代码
| 62bit指向栈中锁记录的指针 | 2bit锁标志(00) |

重量级锁状态

复制代码
| 62bit指向互斥量的指针 | 2bit锁标志(10) |

GC标记状态

复制代码
| 空 | 2bit锁标志(11) |

对象头大小计算

  • 32位JVM

    对象头总大小 = 8字节(Mark Word 4字节 + Klass Pointer 4字节)

  • 64位JVM(未开启压缩指针)

    对象头总大小 = 16字节(Mark Word 8字节 + Klass Pointer 8字节)

  • 64位JVM(开启压缩指针)

    对象头总大小 = 12字节(Mark Word 8字节 + Klass Pointer 4字节)


查看对象头的方法

使用JOL(Java Object Layout)工具可以分析对象头:

java 复制代码
// 添加Maven依赖
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.16</version>
</dependency>

// 示例代码
System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());

输出示例:

复制代码
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4        (object header: class)    0xf80001e5
 12   4        (object alignment gap)    
Instance size: 16 bytes

对象头与锁升级

对象头中的Mark Word会随着锁竞争升级而变化:

  1. 无锁:初始状态,未发生同步。
  2. 偏向锁:当同一线程多次访问时,Mark Word记录线程ID。
  3. 轻量级锁:发生竞争时,Mark Word转换为指向线程栈中锁记录的指针。
  4. 重量级锁:竞争加剧时,Mark Word指向互斥量(Monitor)。

注意事项

  • 数组对象会在对象头中额外存储数组长度(4字节)。
  • 对象头布局是JVM实现相关的,不同虚拟机可能有差异。
  • 压缩指针(-XX:+UseCompressedOops)可减少64位系统中的内存占用。

原子操作

原子操作的定义

原子操作指在多线程或并发环境中不可分割的操作,执行过程中不会被其他线程中断,保证操作的完整性和一致性。原子操作是并发编程的基础,常用于实现锁、计数器等同步机制。

原子操作的特性

  • 不可分割性:操作要么完全执行,要么完全不执行,不会出现中间状态。
  • 可见性:操作完成后,结果对其他线程立即可见。
  • 有序性:操作不会被编译器或处理器重排序破坏逻辑。

原子操作的实现方式

硬件支持的原子指令

现代处理器提供原子指令(如x86的LOCK前缀、ARM的LDREX/STREX),直接通过CPU指令实现原子操作。例如:

  • CAS(Compare-And-Swap) :比较并交换,若当前值等于预期值,则更新为新值。

    cpp 复制代码
    bool atomic_compare_exchange(int* ptr, int expected, int new_val) {
        return __sync_val_compare_and_swap(ptr, expected, new_val);
    }
编程语言提供的原子类型

C++11、Java等语言内置原子类型(如std::atomicAtomicInteger),封装底层硬件指令:

cpp 复制代码
#include <atomic>
std::atomic<int> counter(0);
counter.fetch_add(1); // 原子递增
操作系统提供的原子API

如Linux的atomic_t类型或Windows的Interlocked系列函数:

c 复制代码
// Windows示例
LONG InterlockedIncrement(LONG volatile* Addend);

常见原子操作场景

  • 计数器:无锁统计,避免多线程竞争。
  • 标志位控制:如线程安全的开关状态切换。
  • 无锁数据结构:如无锁队列、栈的实现。

原子操作的局限性

  • 性能开销:原子指令可能比普通操作慢,频繁使用会影响性能。
  • ABA问题:CAS操作中,值从A变为B又变回A,可能导致误判。解决方案如版本号标记。

示例:无锁队列的原子操作

cpp 复制代码
template<typename T>
class LockFreeQueue {
    std::atomic<Node*> head;
    std::atomic<Node*> tail;
public:
    void push(T val) {
        Node* new_node = new Node(val);
        Node* old_tail = tail.exchange(new_node);
        old_tail->next = new_node;
    }
};

通过合理使用原子操作,可以高效解决并发问题,但需结合场景权衡性能与复杂度。

相关推荐
在放️5 小时前
Python 爬虫 · 第三方代理接入与合规使用
开发语言·爬虫·python
KANGBboy5 小时前
java知识五(继承)
java·开发语言
c++之路5 小时前
Bazel C++ 构建系列文档(三):构建第一个 C++ 项目
开发语言·c++
AI人工智能+电脑小能手5 小时前
【大白话说Java面试题 第117题】【并发篇】第17题:线程有几种状态,之间如何转换?
java·开发语言·面试
聚名网7 小时前
域名net,com,cn有区别吗?有哪些不同呢?
服务器·开发语言·php
牛油果子哥q7 小时前
STL set与map底层精讲,红黑树适配原理、有序去重特性、迭代器遍历、API实战与面试核心考点全解
开发语言·数据结构·c++·面试
foundbug9997 小时前
直流电机 PID 速度控制 MATLAB 仿真程序
开发语言·matlab
Tian_Hang8 小时前
C++原型模式(Protype)
开发语言·c++·算法
天天讯通8 小时前
OKCC 呼叫中心安全性能全解析:技术防护与管理措施指南
大数据·开发语言·网络·人工智能·安全·语音识别
xufengzhu8 小时前
第三方 Python 库 redis-py + hiredis 的使用
开发语言·redis·python