java锁杂谈

关于java锁,内容蛮多的,这篇文章只谈一部分见解,所以取名为杂谈,没有大纲,等后面锁的体系建立起来后再整理一下,那么开始吧:
Java 锁有哪些?各种各样,网传15种有余,这些锁的底层大多是AQS实现的,比如:

ReentrantLock可重入锁是基于AQS(AbstractQueuedSynchronizer)实现的。
AQS是一个抽象类,它定义了同步队列的框架,提供了很多模板方法,子类可以根据这个框架来实现具体的同步方法。ReentrantLock就是基于AQS实现的,它分为公平锁和非公平锁。公平锁按照线程请求锁的顺序分配锁,非公平锁则允许后请求的线程优先获得锁

Java中的Semaphore(信号量)是基于AQS(AbstractQueuedSynchronizer)实现的。
Semaphore是一个计数器,可以用来限制对资源的访问。它有一个计数器,表示当前可用的资源数量。当一个线程需要访问一个资源时,它调用acquire()方法获取一个许可。如果可用资源数量大于0,那么该线程可以获取一个许可并继续执行。否则,该线程将被放入等待队列,直到有资源可用为止。
当一个线程完成对资源的访问后,它会调用release()方法释放一个许可。这会使计数器加1,并唤醒在等待队列中的一个线程(如果有的话)

CyclicBarrier 循环屏障 基于ReentrantLock的独占锁实现,本质其实也是基于AQS实现。
每当有线程调用await方法时,会将当前线程挂起放入到AQS的条件队列中,此时count会减少1,当count减少到0时,表示所有线程已经达到了屏障点,执行通过构造函数传递过来的任务

CountDownLatch 倒计时门闩是一种java.util.concurrent包下的一个同步工具类,它通过AQS(AbstractQueuedSynchronizer)的state字段来实现。
CountDownLatch的底层基于AQS实现,提供了一个int类型的构造方法new CountDownLatch(int count),表示计数器计数器的初始值为count,还有两个核心方法:
countDown()方法表示计数器的数字执行减1操作;
await()方法表示让当前线程进入等待状态,直到count的值为0才能继续执行;以及一个继承于AQS的内部类Sync来做具体的操作

ReentrantReadWriteLock 读写锁内部是通过队列同步器AQS实现的。
AQS(AbstractQueuedSynchronizer)是一个抽象类,它实现了线程安全的FIFO等待队列,通过维护一个共享资源状态(一个int型变量),并使用CAS操作来修改这个状态,从而实现线程的排队和唤醒。ReentrantReadWriteLock是一种可重入的读写锁,通过分离读锁和写锁,使得它比其他排他锁性能更好

当然还有几个特殊的:
StampedLock内部是基于CLH锁实现的,CLH是一种自旋锁,能够保证没有"饥饿现象"的发生,并且能够保证FIFO(先进先出)的服务顺序。
StampedLock的实现基于CLH锁。维护一个线程队列,申请不成功的记录在此,每个节点保存一个标记位(locked),用来判断当前线程是否释放锁。当线程试图获取锁,取得当前队列的尾部节点作为其前序节点,并使用类似while(pred.locked)判断前序节点是否已经成功释放锁。只要前序节点没有释放锁,则表示当前线程还不能继续执行,自旋等待。如果前序线程已经释放,则当前线程可以继续执行。释放锁时线程将自己的节点标记位置为false,那么后续等待的线程就能继续执行了。

synchronized是基于对象实现的。
无论你用的是类锁还是对象锁,这些的类和对象都会在堆内存中有一片空间去存储这些数据。就拿咱们new了这个一个Object来说,它会在堆内存中开辟一个空间,主要存储三块数据,一块是对象头、一块是实例数据还有一个是对象填充。咱们主要看到的是对象头里面包含的内容,其中有MarkWord还有ClassPoint。咱们核心要查看的就是MarkWord中存储了什么样的信息。
操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为"重量级锁"。
原子性:synchronized保证语句块内操作是原子的
同步方法
ACC_SYNCHRONIZED 这是一个同步标识,对应的 16 进制值是 0x0020
这 10 个线程进入这个方法时,都会判断是否有此标识,然后开始竞争 Monitor
对象。

同步代码
 monitorenter,在判断拥有同步标识 ACC_SYNCHRONIZED 抢先进入此方法
的线程会优先拥有 Monitor 的 owner ,此时计数器 +1。
 monitorexit,当执行完退出后,计数器 -1,归 0 后被其他进入的线程获得。
volatile 的可见性都是通过内存屏障(Memnory Barrier)来实现的。
synchronized 靠操作系统内核的Mutex Lock(互斥锁)实现,相当于 JMM 中的 lock、unlock。退出代码块时刷新变量到主内存。
synchronized保证有序性(通过“一个变量在同一时刻只允许一条线程对其进行lock操作”)
as-if-serial,保证不管编译器和处理器为了性能优化会如何进行指令重排序,
都需要保证单线程下的运行结果的正确性。也就是常说的:如果在本线程内观察,
所有的操作都是有序的;如果在一个线程观察另一个线程,所有的操作都是无序
的。
volidate是多线程环境下,可以保证不同的线程之间,操作同一个变量能互相通讯可见的。但是不能保证操作原子性,禁止指令重拍,系统为了优化性能会发送指令重排。

至于公平锁/非公平锁 独享锁/共享锁,乐观锁/悲观锁都是根据锁的性质分类的
公平锁和互斥锁是两种不同的锁机制,它们在处理线程间的竞争和同步时有所不同。
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的先后顺序来一次获得锁。公平锁的好处是等待锁的线程不会饿死,但是整体效率相对低一些。Java中的公平锁有ReentrantLock。
互斥锁是独享锁,是指锁一次只能被一个线程持有。Java中的互斥锁有synchronized。
此外,还有可重入锁、读写锁、乐观锁、悲观锁等。
公平锁(FairLock):公平锁保证线程获取锁的顺序与线程请求锁的顺序相同。如果存在一个等待队列,那么等待时间最长的线程将获得锁。

互斥锁(Mutex):互斥锁是一种最简单的锁,它通过对共享资源加锁来确保同一时间只有一个线程可以访问该资源。

偏向锁/轻量级锁/重量级锁
这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
  偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。

轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。

重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让他申请的线程进入阻塞,性能降低。

自旋锁
  在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
  乐观锁/悲观锁
乐观锁与悲观锁并不是指具体的某一类实现的锁,而是一种实现的思想或者是构想,主要是从看待并发的角度区分

乐观锁:乐观锁总是乐观的认为在修改数据的时候,没有其他线程对数据进行修改,只是在更改数据时进行检查数据有没有在此之前被修改过。可以通过版本号等机制实现,乐观锁适用于读操作量比较大的情况。

悲观锁:悲观锁总是悲观的认为在操作数据时会有其他线程对数据进行操作,所以每次会对操作的对象加锁,这样别人想同时修改数据就会阻塞,直到锁被释放,其他线程可以进行操作,加锁会降低性能。

以版本号方式为例:

如在操作的数据上加上版本version属性,每次操作版本递增,在取得数据的同时获取版本属性,在操作完数据后保存前,将之前的版本号与当前的版本号进行对比,如果一致则进行更新操作,否则代表数据以被修改,重试更新操作。
锁案例

  1. 重入锁

重入锁(ReentrantLock)是 Java 中的一种锁机制,它可以重复进入同一个锁保护的代码块而不会死锁,同时还提供了更多的高级特性,比如可中断锁、超时锁、公平锁等。下面是一个简单的重入锁的使用示例:

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo {

private final ReentrantLock lock = new ReentrantLock();

public void foo() {

lock.lock(); // 获取锁

try {

// 这里是被锁保护的代码块

// 可以重复进入该代码块,因为是同一个锁

// …

} finally {

lock.unlock(); // 释放锁

}

}

}

在上面的代码中,我们使用了一个 ReentrantLock 对象来保护一个代码块。在 foo() 方法中,我们首先调用了 lock() 方法来获取锁,然后在被锁保护的代码块中执行需要同步的操作,最后调用 unlock() 方法来释放锁。

值得注意的是,当使用重入锁时,需要在 finally 块中调用 unlock() 方法来确保锁一定会被释放,避免出现死锁等问题。

另外,重入锁还提供了一些高级特性,如公平锁、可中断锁、超时锁等,可以根据实际需要选择不同的特性来满足不同的应用场景。

  1. 信号量锁

信号量(Semaphore)是 Java 中的一种同步机制,它可以控制多个线程同时访问某个共享资源,从而避免资源竞争问题。下面是一个使用信号量锁的简单示例:

import java.util.concurrent.Semaphore;

public class SemaphoreDemo {

private final Semaphore semaphore = new Semaphore(2); // 初始化信号量的数量为 2

public void foo() throws InterruptedException {

semaphore.acquire(); // 获取信号量,如果当前信号量数量为 0,则阻塞线程

try {

// 这里是被信号量保护的代码块

// …

} finally {

semaphore.release(); // 释放信号量

}

}

}

在上面的代码中,我们使用 Semaphore 对象来保护一个代码块,初始信号量的数量为 2。在 foo() 方法中,我们首先调用 acquire() 方法来获取信号量,如果当前信号量的数量为 0,则会阻塞线程,直到有可用的信号量。在被信号量保护的代码块中执行需要同步的操作,最后调用 release() 方法来释放信号量。

信号量可以用于限制同时访问某个共享资源的线程数量,它可以控制同时访问的线程数,并且支持公平和非公平两种方式。需要注意的是,在使用信号量时,也需要在 finally 块中释放信号量,以确保信号量一定会被释放。

  1. 读写锁

读写锁(ReadWriteLock)是 Java 中的一种锁机制,它允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。下面是一个使用读写锁的简单示例:

import java.util.concurrent.locks.ReadWriteLock;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockDemo {

private final ReadWriteLock lock = new ReentrantReadWriteLock();

public void read() {

lock.readLock().lock(); // 获取读锁

try {

// 这里是被读锁保护的代码块

// 可以允许多个线程同时读取该代码块

// …

} finally {

lock.readLock().unlock(); // 释放读锁

}

}

public void write() {

lock.writeLock().lock(); // 获取写锁

try {

// 这里是被写锁保护的代码块

// 只允许一个线程写入该代码块

// …

} finally {

lock.writeLock().unlock(); // 释放写锁

}

}

}

在上面的代码中,我们使用了一个 ReadWriteLock 对象来保护一个代码块。在 read() 方法中,我们首先调用 readLock() 方法来获取读锁,允许多个线程同时读取被锁保护的代码块,最后在 finally 块中调用 unlock() 方法来释放读锁。在 write() 方法中,我们使用 writeLock() 方法来获取写锁,只允许一个线程写入被锁保护的代码块,最后同样需要在 finally 块中调用 unlock() 方法来释放写锁。

读写锁可以提高读操作的并发性能,从而提高程序的效率,适用于读多写少的场景。但是需要注意的是,在使用读写锁时,需要考虑锁的粒度和性能问题,避免因为锁的过多或者过少导致程序的性能下降或者数据不一致。

  1. 偏向锁

偏向锁(Biased Locking)是 Java 中的一种锁机制,它可以在只有一个线程访问同步块时,通过将对象头信息标记为偏向锁来避免线程之间的竞争。下面是一个使用偏向锁的简单示例:

public class BiasedLockDemo {

private static Object lock = new Object(); // 创建一个对象

public void foo() {

synchronized (lock) { // 同步块

// 这里是被锁保护的代码块

// 只允许一个线程访问该代码块

// …

}

}

}

在上面的代码中,我们使用了一个 synchronized 块来保护一个代码块,这个锁是偏向锁。在 foo() 方法中,我们使用 synchronized 关键字来获取锁,如果只有一个线程访问同步块,JVM 会自动将锁的状态标记为偏向锁,避免了线程之间的竞争。

偏向锁可以提高单线程程序的性能,避免线程之间的竞争。但是需要注意的是,在多线程环境下,偏向锁可能会失效,需要重新获取锁,因此需要根据具体的场景来选择使用偏向锁还是其他锁机制。

各种锁有各自适合使用的场景,不同的场景下要选择合适的锁来解决问题成了一个程序员必修的课程。
待续。。。

你可能感兴趣的:(java,java,开发语言,锁)