大学电子系的建设网站的方案,网站建设 用英文怎么说,企业微信公众平台,长沙全市停课目录
前言#xff1a;
引入#xff1a;
锁机制#xff1a;
CAS算法#xff1a;
乐观锁与悲观锁#xff1a;
总结#xff1a; 前言#xff1a; 在多线程编程中#xff0c;线程之间的协作和资源共享是一个重要的话题。当多个线程同时操作共享数…目录
前言
引入
锁机制
CAS算法
乐观锁与悲观锁
总结 前言 在多线程编程中线程之间的协作和资源共享是一个重要的话题。当多个线程同时操作共享数据时就可能引发数据不一致或竞态条件等问题。为了解决这些问题Java提供了强大的锁机制使得多线程程序能够安全地共享资源、实现线程间的同步。 Java锁机制允许我们控制多个线程对共享资源的访问确保在任何时刻只有一个线程可以访问公共数据或执行特定的代码块。这种机制既可以用于保护共享变量的一致性也可以用于实现对临界区的互斥访问。 引入
在锁机制没有出现以前多线程往往会出现以下两个问题
1.数据不一致当多个线程同时读写共享数据时可能会出现数据不一致的情况。比如一个线程正在对某个变量执行修改操作而另一个线程同时读取该变量的值如果没有锁机制的保护可能会读取到未被修改之前的旧值导致数据出现不一致的情况。 假设有两个线程同时对共享的变量进行读取和修改操作
int sharedVariable 0;// 线程1的代码
sharedVariable 10;// 线程2的代码
int value sharedVariable;
System.out.println(value);在上述代码中线程1将共享变量 sharedVariable 的值修改为10。同时线程2读取共享变量 sharedVariable 的值并打印出来。如果没有适当的同步机制线程2可能会读取到修改之前的旧值0导致数据不一致的情况
2.竞态条件当多个线程同时对共享资源进行修改操作时由于线程之间执行顺序的不确定性可能导致执行结果依赖于线程执行时的相对顺序。这种不确定性可能引发竞态条件导致程序出现错误的行为。例如多个线程同时对同一个计数器进行自增操作如果没有适当的同步机制就可能出现计数器值不正确的情况。 假设有两个线程同时对一个计数器进行自增操作
int counter 0;// 线程1的代码
counter;// 线程2的代码
counter;在上述代码中如果没有适当的同步机制两个线程在执行 counter 操作时可能会发生线程切换导致线程1和线程2之间的执行顺序不确定。这种情况下如果线程1先执行自增操作然后线程2执行自增操作最终计数器的值可能只增加了1而不是期望的2。这就是典型的竞态条件导致了程序行为的错误。 因此我们需要一种方法可以在其他线程被调用的时候对调用数据进行保护所以我们创造出了锁机制通过使用锁机制可以解决这些问题。锁机制可以确保在某个线程修改共享数据时其他线程无法同时进行读取或修改操作从而避免了数据不一致和竞态条件的发生。 在正式介绍锁机制之前我们还是先来认识一下JVM运行时的内存结构 在这张图中我们需要知道
绿色部分是所有线程共享的数据区域黄色部分是每一个线程单独享有的数据区域
那让我们开始正式的介绍锁
锁机制
在JAVA中每一个对象都有一把锁这把锁被存放在对象头中锁中记录了当前对象被哪个线程占用。 对象的组成部分 对象头Object Header对象头包含了一些用于存储对象元数据的信息如对象的哈希码、锁的信息、GC垃圾回收相关的标记等。对象头的大小在不同的Java虚拟机实现中会有所差异。 实例数据Instance Data实例数据是对象的成员变量的实际存储空间。它们是对象的状态的一部分也就是我们定义在类中的成员变量。实例数据的大小取决于对象的成员变量数量和类型。 对齐填充Alignment Padding为了提高内存访问的效率Java虚拟机要求对象的起始地址必须是某个特定值的倍数。如果实例数据的大小不是这个特定值的倍数就需要通过填充字节来对齐。 OK那我们开始讲解我们学习中遇到的第一个锁
synchronized
在Java中当使用synchronized关键字修饰方法或代码块时编译后会生成两条字节码指令monitorenter和monitorexit。这两条指令用于获取锁和释放锁实现线程同步。 monitorenter指令该指令用于获取对象的锁内置锁。当线程执行到被synchronized修饰的代码块或方法时它首先会尝试获取对象的锁。如果该锁没有被其他线程持有该线程就会成功获取锁并继续执行下面的指令。如果锁被其他线程持有则当前线程会进入阻塞状态直到锁被释放。 monitorexit指令该指令用于释放对象的锁。当线程执行完synchronized修饰的代码块或方法后或者发生了异常退出时该线程会释放对象的锁。这样可以确保其他线程能够获取锁并执行相关的代码。
示例
public class MyClass {private final Object lock new Object();public void synchronizedMethod() {synchronized (lock) {// 被synchronized修饰的代码块}}
}对应的编译后的字节码指令如下
0: aload_0 ; 将当前对象加载到操作数栈
1: getfield #1 ; 加载对象的字段锁对象
4: dup ; 复制栈顶元素锁对象
5: astore_1 ; 将锁对象存储到局部变量
6: monitorenter ; 进入同步块获取锁
7: /* 同步代码块 */ ; 执行同步块的代码
8: aload_1 ; 加载局部变量锁对象
9: monitorexit ; 退出同步块释放锁这些字节码指令确保了在synchronized修饰的代码块中只能有一个线程执行并保证了线程之间的互斥性和正确的内存同步。这样可以确保多个线程安全地访问共享资源避免并发问题。
而这就是synchronized的运行机制通过两个字节码来实现对线程的同步机制。
但遗憾的是synchronized存在性能问题因为他被编译后实际上就是两个字节码指令而这两个字节码文件都是依赖于操作系统的mutex lock进行的而JAVA线程本质上就是对操作系统线程的映射。因此每当操作或挂起一个线程都要对操作系统内核态进行切换而这种操作太费时间了在一切情况下甚至切换的时间都超过了应用的时间。
而从JAVA6开始就对synchronized进行了优化引入了偏向锁和轻量级锁。
此时锁就一共有四种了
无锁偏向锁轻量级锁重量级锁 无锁Lock-Free无锁是一种并发控制机制允许多个线程同时修改共享资源而不需要显式地使用锁。无锁的算法通常通过使用原子操作如CASCompare and Swap来保证多线程操作的原子性和线程安全性。无锁的目标是通过无竞争的方式实现最大的并发性能。 偏向锁Biased Locking偏向锁是JVM针对没有竞争的场景进行的一种锁优化机制。它的目标是减少无竞争情况下的锁操作开销。在偏向锁状态下当一个线程访问锁时JVM会将锁对象的标记置为偏向线程ID之后该线程再次访问锁时就不会再进行同步操作从而提高性能。 轻量级锁Lightweight Locking轻量级锁是针对竞争不激烈的情况下的一种锁优化机制。它通过使用CAS操作来进行锁定和释放而不需要进行互斥的内核态操作。当一个线程尝试获取轻量级锁时它会使用CAS操作将对象头中的标志位更新为锁记录Lock Record指向的线程ID。如果操作成功这个线程就可以继续执行临界区代码如果操作失败说明存在竞争需要升级为重量级锁。 重量级锁Heavyweight Locking重量级锁是传统的锁机制也是默认的锁实现。当多个线程竞争一个锁时JVM会将该锁从轻量级锁升级为重量级锁。重量级锁会在操作系统层面进行互斥的内核态操作如使用互斥量等。它能确保多个线程之间的互斥性但也会带来更多的开销。
这四个状态是递增的无锁-偏向锁-轻量级锁-重量级锁。而这种状态可以升级也可以降级。
在我们学习了互斥锁的底层机制互斥锁的四种状态之后 我们在来介绍一下
CAS算法
CASCompare and Swap是一种用于实现无锁算法的同步原语。它主要用于多线程环境下对共享数据的原子操作提供了一种线程安全的方式来进行数据的更新。
CAS 算法涉及三个操作数内存地址V、旧的预期值A和新的值B。CAS 算法的执行过程如下 首先线程读取内存地址 V 中的值记为当前值 currentV。 然后线程检查当前值 currentV 是否等于预期值 A。如果相等说明没有其他线程修改过该值线程可以进行更新操作。 如果当前值 currentV 不等于预期值 A说明有其他线程修改过该值线程不进行更新操作。可以选择重试或采取其他策略来处理。 如果当前值 currentV 等于预期值 A线程将新的值 B 写入到内存地址 V 中。 最后线程判断写入操作是否成功。如果成功说明更新操作完成如果不成功说明有其他线程在该线程之前执行了更新操作需要重新执行整个 CAS 算法。
CAS 算法的核心思想是通过比较当前值和预期值是否相等来判断共享数据是否被修改过。如果没有被修改过就进行更新操作如果被修改过说明有其他线程先一步修改了数据需要重试。因此CAS 算法可以避免传统锁所带来的线程阻塞和上下文切换的开销增加了并发性能。
然而CAS 算法也存在一些问题例如ABA 问题两次读取的值是一样的但是中间过程发生了变化以及循环时间长开销大等。为了解决这些问题Java 提供了 Atomic 包下的一些原子类如 AtomicReference 和 AtomicStampedReference可以解决 CAS 算法中的ABA 问题并提供了更高级的封装和功能。
我们用图片来演示一下CAS算法
A和B就代表两条线程而C就代表此时A和B争抢的资源文件如果A线程运气好抢到了资源C他将会把自身的old value 与C进行对比如果一致就把C的状态改为1并且获得对C的操作权。
而此时B再与C进行比较01因此B就会放弃swap操作但是在实际操作中B并不会就直接放弃而是让其进行自旋所谓的自旋就是不断进行CAS操作假如C的状态后面变为0B就又会重新进行比较和交换操作 下面我们用一段代码来展示一下CAS函数
int cas(long * addr ,long oldvalue,long newvalue)
{if(*addr ! old)return 0;*addr new ;return 1;
}
其实这段代码还是有问题的CAS分为两部分compare 和 swap ,那么既然这个方法没有进行任何同步操作那如果A线程获得时间片但对C状态修改的时候B线程又获得了时间片此时不就是两个线程AB同时获得了对资源数据的操作权力吗
但好在CAS早就已经通过底层设计将赋予了其原子性。
最后我们再介绍一下乐观锁与悲观锁
乐观锁与悲观锁
乐观锁和悲观锁是两种并发控制的思想主要应用于多线程环境下对共享数据的访问控制。它们的主要区别在于对于并发冲突的处理策略和机制。
悲观锁 悲观锁的思想是假设在整个数据操作过程中其他线程可能会修改数据因此在对数据进行操作时默认认为会发生冲突所以采取阻塞等待的方式。悲观锁主要通过线程阻塞、锁定共享资源等方式来保证同一时间只有一个线程能够访问共享数据。
常见的悲观锁实现包括
互斥锁如 Java 中的 synchronized 关键字、ReentrantLock使用互斥锁来保证对共享资源的独占访问其他线程需要等待锁释放才能访问。读写锁如 Java 中的 ReentrantReadWriteLock通过区分读操作和写操作允许多个线程同时读取共享资源但只允许单个线程进行写操作。
乐观锁 乐观锁的思想是假设在整个数据操作过程中不会发生并发冲突因此不采取阻塞等待的方式而是在更新数据时检查是否发生冲突。如果发现冲突则采取相应的策略如重试或放弃更新。乐观锁通常使用无锁算法如 CAS来实现。 乐观锁在大多数情况下用的是CAS无锁算法因此不要看见锁这个字就认为乐观锁是锁 常见的乐观锁实现包括
版本号Versioning在数据记录中加入版本号字段每次更新时通过比较版本号判断是否发生冲突。时间戳Timestamp在数据记录中加入时间戳字段每次更新时通过比较时间戳判断是否发生冲突。CASCompare and Swap使用原子操作的方式进行数据的更新通过比较当前值和预期值是否一致来判断是否发生冲突。
乐观锁适合于读操作非常频繁但写操作相对较少的场景可以提高并发性能。然而乐观锁需要保证数据不会被并发修改的假设成立否则会引发数据不一致问题。如果冲突频率较高乐观锁可能会引起大量的重试降低性能。
在实际应用中选择悲观锁还是乐观锁要根据具体的场景考虑并发冲突的频率、数据一致性要求以及性能需求等因素。有时也可以结合两者的优点使用适当的锁机制来满足需求。
总结 锁的底层确实很复杂我们也不是一篇两篇文章就可以讲清楚的因此我写这篇文章更多的还是为了吸引大家的兴趣如果有兴趣了可以再去深入的了解一下各种锁。
如果我的内容对你有帮助请点赞评论收藏。创作不易大家的支持就是我坚持下去的动力