多线程---再次认识volatile,Synchronize,lock

本文介绍了多线程中保证共享变量一致性的方法之一:Volatile。解释了Volatile的工作原理,包括如何确保所有线程看到一致的变量值,并探讨了Volatile与Synchronize、Lock的区别,以及JVM中锁的不同状态。
    在多线程中我们常用的保证共享变量的方法有很多,现在我们介绍其中的一种,volatile,也是效率最高的一种。
   一 、volatile的意义:
            为了确保共享变量能被正确和一致的更新,字段被声明称volatile,java线程内存模型确保所有线程看到这个变量的值是一致的。volatile变量修饰符如果使用恰当的话,它比synchronize的使用和执行成本更低,因此它不会引起线程上下文的切换和调度。

   二 、如何保证所有线程看到的这个变量的值是一致的?
 源码中有这样一句, instance = new Singleton();说明这是一个单例,任何线程请求的都是这一个对象。生成汇编代码:

 在汇编代码中会出现 一个lock前缀,有这个前缀会发生两件事:
1.当前处理器的缓存行的数据都会写回到系统内存。
2.这个写回内存的操作会引起其他CPU缓存的该内存地址的数据无效。
为什么好多的都设64个字节,或是hash的一些默认空间设置成16个,原因与处理器结构有关,一般处理器缓存的额告诉缓存行时64个字节宽,换句话说他可以放16个对象,所以追加字节能优化性能。一些我们常见的java集合都是默认的设置空间为16。
  三 、 与Synchronize、lock的区别
       1.synchronize是一种机制,可加在方法上,也可以加载语句块上;lock是一个对象,多线程需要一个ReentrantLock才能实现锁机制。
       2.synchronize是托管给JVM管理的,lock是java代码写的控制锁的实现。
       3.synchronized采用的是悲观锁机制,线程获得的是独占锁,只能依靠阻塞来等待线程释放锁。而且CPU转换线程时上下文切换回引起新城竞争锁,使得效率降低。lock 采用的是乐观锁机制,乐观锁其实就是不加锁,一直去操作,失败就重新操作,直到成功为止,效率高很多。
4.当竞争不激烈的时候synchronize的性能好一点儿,但是当竞争激烈的时候lock的性能要很多。
   四 、在JVM中锁是如何工作的:
         1.java对象头:hashcode,锁,分代年龄。在运行期间MarkWord 里存储的数据会随着锁标志位的变化而变化,可能变化为下面的数据:
       
        2锁的状态:无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态。
        3 锁状态比较:
优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距 如果宣布从个别个驻华吧创业板在锁竞争,会带来额外的锁撤销的消耗 适用于只有一个线程访问同步块场景
轻量级锁 竞争的线程不会阻塞,提高了程序的效应速度 如果始终得不到锁竞争的线程使用自旋会消耗CPU 追求相应时间,同步块儿执行速度非常快
重量级锁 线程竞争不适用自旋,不会消耗CPU,但是枷锁解锁过程会消耗CPU, 线程阻塞,相应时间缓慢 追求吞吐量,同步块执行速度较长

多线程环境下修改全局变量时,确实存在 **可见性问题(Visibility Problem)** 和 **竞争条件(Race Condition)**,这会导致程序行为不可预测或数据不一致。以下是详细分析: --- ### **1. 核心问题** #### **(1) 竞争条件(Race Condition)** - **定义**:多个线程同时读写同一全局变量,且至少有一个线程执行写操作,最终结果取决于线程执行的随机顺序。 - **示例**: ```c int counter = 0; // 全局变量 void* increment(void* arg) { for (int i = 0; i < 100000; i++) { counter++; // 非原子操作(可能被中断) } return NULL; } int main() { pthread_t t1, t2; pthread_create(&t1, NULL, increment, NULL); pthread_create(&t2, NULL, increment, NULL); pthread_join(t1, NULL); pthread_join(t2, NULL); printf("Counter: %d\n", counter); // 可能输出小于 200000 } ``` - **原因**:`counter++` 并非原子操作(实际包含读取-修改-写入三步),线程可能交替执行导致部分修改丢失。 #### **(2) 可见性问题(Visibility Problem)** - **定义**:一个线程对全局变量的修改可能不会立即被其他线程看到,导致读取到过时数据。 - **原因**:CPU 缓存一致性协议(如 MESI)的延迟或编译器/CPU 的指令重排序优化。 --- ### **2. 解决方案** #### **(1) 互斥锁(Mutex)** ```c pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; void* increment(void* arg) { for (int i = 0; i < 100000; i++) { pthread_mutex_lock(&lock); counter++; pthread_mutex_unlock(&lock); } return NULL; } ``` - **优点**:简单通用,保证原子性和可见性。 - **缺点**:性能开销较大(锁竞争)。 #### **(2) 原子操作(Atomic Operations)** - 使用 C11 的 `<stdatomic.h>` 或编译器内置原子指令(如 GCC 的 `__atomic_fetch_add`): ```c #include <stdatomic.h> atomic_int counter = 0; // 原子变量 void* increment(void* arg) { for (int i = 0; i < 100000; i++) { atomic_fetch_add(&counter, 1); // 原子递增 } return NULL; } ``` - **优点**:无锁设计,高性能。 - **缺点**:仅适用于简单操作(如加减、位运算)。 #### **(3) 线程本地存储(Thread-Local Storage, TLS)** - 使用 `_Thread_local`(C11)或 `__thread`(GCC 扩展)避免共享: ```c _Thread_local int local_counter = 0; // 每个线程独享副本 ``` --- ### **3. 其他注意事项** - **编译器优化屏障**: 使用 `volatile` 可防止编译器过度优化(如缓存变量到寄存器),但 **不能解决原子性问题**。 ```c volatile int flag = 0; // 仅保证每次从内存读取,不保证原子性 ``` - **内存屏障(Memory Barrier)**: 在多核 CPU 中,需插入屏障指令(如 `__sync_synchronize()`)保证写入对其他线程可见。 --- ### **4. 总结:全局变量多线程修改的风险** | **问题类型** | **表现** | **解决方案** | |--------------------|-----------------------------|---------------------------| | 竞争条件 | 数据丢失、计算结果错误 | 互斥锁、原子操作 | | 可见性问题 | 读取到过时数据 | 内存屏障、`volatile` | | 指令重排序 | 代码执行顺序与预期不符 | 内存屏障 | --- **
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值