锁的简单分类
悲观锁
认为自己在使用数的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
synchronized关键字和Lock的实现类都是悲观锁
使用场景:适合写操作多的场景,先加索可以保证写操作时数据正确。显式的锁定之后在操作同步资源。定性为狼性锁
乐观锁
乐观锁是一种乐观思想的并发控制方式,它假设竞争不会经常发生,因此不会使用显式的锁进行资源保护。相反,乐观锁允许多个线程同时访问共享资源,但在更新资源时会检查其他线程是否同时修改了资源。常见的乐观锁实现方式是使用版本号(versioning
)或时间戳(timestamp
),通过比较版本号或时间戳来判断资源是否被修改。
在 Java 中,乐观锁常常使用CAS(Compare And Swap)
操作实现。CAS
是一种原子操作,用于实现多线程并发中的乐观锁。Java 中的 Atomic
类(如 AtomicInteger
、AtomicLong
等)就是基于 CAS
操作实现的。
任务自己在使用数据时不会有别的线程修改数据或资源,所以不会添加锁。
在Java中是通过使用无锁编程来实现,只是在更新数据的时候去判断,之前有没有别的线程更新了这个数据。
如果数据没有被更新,当前线程将自己修改的数据成功些入
如果这个数据已经被其他线程刚更新,则根据不同的实现方式执行不同的操作,比如放弃修改、重试抢锁等等。
判断规则:
1、版本号机制Version
2、最常采用的是CAS算法,Java原子类中的递增操作就是通过CAS自旋实现的。
下面是一个使用乐观锁的示例代码:
import java.util.concurrent.atomic.AtomicInteger;
public class OptimisticLockExample {
private AtomicInteger value = new AtomicInteger(0);
public void increment() {
int oldValue;
int newValue;
do {
// 读取当前值
oldValue = value.get();
// 计算新值
newValue = oldValue + 1;
// 尝试更新值,如果失败则重新尝试
} while (!value.compareAndSet(oldValue, newValue));
}
public int getValue() {
return value.get();
}
public static void main(String[] args) {
OptimisticLockExample example = new OptimisticLockExample();
// 创建多个线程并发执行增加操作
Thread[] threads = new Thread[10];
for (int i = 0; i {
for (int j = 0; j
在上面的示例中,AtomicInteger
类的 compareAndSet()
方法使用 CAS
操作实现了乐观锁。多个线程并发调用 increment()
方法增加 value
的值,但是由于使用了乐观锁,所以不需要使用显式的锁来保护 value
,也不会发生死锁等并发问题。
可重入锁(递归锁)
可重入锁(也称为递归锁)允许线程在持有锁的情况下再次获取同一个锁,而不会导致死锁。当一个线程尝试获取一个由它自己持有的锁保护的代码块时,这种情况就称为重入。
Java中的 ReentrantLock
就是一个可重入锁的实现,它提供了与synchronized
关键字类似的功能,并且更加灵活和强大。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private final Lock lock = new ReentrantLock();
public void method1() {
lock.lock(); // 获取锁
try {
System.out.println("Method 1: Critical section");
// 在method2中调用method1
method2();
} finally {
lock.unlock(); // 释放锁
}
}
public void method2() {
lock.lock(); // 获取锁
try {
System.out.println("Method 2: Critical section");
} finally {
lock.unlock(); // 释放锁
}
}
public static void main(String[] args) {
ReentrantLockExample example = new ReentrantLockExample();
Thread thread1 = new Thread(() -> example.method1());
Thread thread2 = new Thread(() -> example.method2());
thread1.start();
thread2.start();
}
}
在上面的示例中,ReentrantLock
实现了可重入锁。在 method1()
中先获取了锁,然后调用 method2()
,method2()
也成功获取了相同的锁。这两个方法在同一个线程中可以重复调用,因为线程已经持有了锁。
总结起来,可重入锁(递归锁)允许线程在持有锁的情况下再次获取同一个锁,而不会导致死锁。
死锁及排查
死锁是指两个或多个线程在执行过程中因争夺资源而造成的一种互相等待的现象,导致各个线程都无法继续执行下去。简单来说,死锁就是两个或多个线程无限期地阻塞等待彼此持有的资源。
死锁通常涉及两个或多个线程、两个或多个资源,以及两个或多个锁。当一个线程在等待另一个线程持有的锁时,同时另一个线程也在等待该线程持有的锁时,就会发生死锁。
排查死锁通常需要分析线程之间的相互依赖关系,以及每个线程持有的锁和需要获取的锁,从而找出导致死锁的原因。一般来说,通过工具、日志和代码审查等方式可以排查死锁。
下面是一个简单的死锁示例代码:
public class DeadlockExample {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread 1: Holding lock 1...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 1: Waiting for lock 2...");
synchronized (lock2) {
System.out.println("Thread 1: Holding lock 1 and lock 2...");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread 2: Holding lock 2...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 2: Waiting for lock 1...");
synchronized (lock1) {
System.out.println("Thread 2: Holding lock 2 and lock 1...");
}
}
});
thread1.start();
thread2.start();
}
}
在上面的示例中,thread1
线程先获取了 lock1
锁,然后尝试获取 lock2
锁;而 thread2 线程先获取了 lock2
锁,然后尝试获取 lock1
锁。这样就形成了死锁的情况,因为 thread1
持有 lock1
锁并等待 lock2
锁,而 thread2
持有 lock2
锁并等待 lock1
锁,彼此之间形成了相互等待的死锁状态。
要排查死锁,可以通过查看线程转储(Thread Dump
)信息、使用工具分析线程状态等方式来识别和解决死锁问题。
写锁(独占锁)/读锁(共享锁)
写锁(独占锁)和读锁(共享锁)是针对读写锁(ReadWriteLock)的两种不同类型的锁。
1. 写锁(独占锁):
- 写锁是一种独占锁,当一个线程持有写锁时,其他线程无法获取读锁或写锁,即独占资源。
- 写锁通常用于对共享资源进行写操作,保证在写操作时不被其他线程读取或写入。
2. 读锁(共享锁):
- 读锁是一种共享锁,当一个线程持有读锁时,其他线程仍然可以获取读锁,但是无法获取写锁。
- 读锁允许多个线程同时读取共享资源,但是不允许线程进行写操作,保证了共享资源在读操作时的线程安全性。
Java 中的 ReentrantReadWriteLock
提供了读写锁的实现,其中包括读锁和写锁。
下面是一个使用读写锁的示例代码:
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockExample {
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
private final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
private int value = 0;
public void read() {
readLock.lock();
try {
System.out.println("Read value: " + value);
} finally {
readLock.unlock();
}
}
public void write(int newValue) {
writeLock.lock();
try {
value = newValue;
System.out.println("Write value: " + value);
} finally {
writeLock.unlock();
}
}
public static void main(String[] args) {
ReadWriteLockExample example = new ReadWriteLockExample();
// 创建多个读线程和一个写线程
for (int i = 0; i example.read()).start();
}
new Thread(() -> example.write(100)).start();
}
}
在上面的示例中,ReadWriteLockExample
类使用了 ReentrantReadWriteLock
实现了读写锁。read()
方法获取了读锁并读取共享资源value
的值,而 write()
方法获取了写锁并修改共享资源 value
的值。多个线程可以同时获取读锁并读取共享资源的值,但是在有线程持有写锁时,其他线程无法获取写锁或读锁。
自旋锁
自旋锁(SpinLock
)是一种基于忙等待的锁,当线程尝试获取锁时,如果锁已被其他线程占用,它会一直处于忙等待状态,不断地尝试获取锁直到成功为止。
在 Java 中,可以使用 AtomicBoolean
或者 AtomicReference
来实现自旋锁。
下面是一个使用 AtomicBoolean
实现自旋锁的示例代码:
import java.util.concurrent.atomic.AtomicBoolean;
public class SpinLock {
private AtomicBoolean locked = new AtomicBoolean(false);
public void lock() {
// 使用CAS操作尝试获取锁
while (!locked.compareAndSet(false, true)) {
// 忙等待直到获取锁成功
}
}
public void unlock() {
// 释放锁
locked.set(false);
}
public static void main(String[] args) {
SpinLock spinLock = new SpinLock();
Thread thread1 = new Thread(() -> {
spinLock.lock();
try {
System.out.println("Thread 1 acquired the lock");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
spinLock.unlock();
System.out.println("Thread 1 released the lock");
}
});
Thread thread2 = new Thread(() -> {
spinLock.lock();
try {
System.out.println("Thread 2 acquired the lock");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
spinLock.unlock();
System.out.println("Thread 2 released the lock");
}
});
thread1.start();
thread2.start();
}
}
在上面的示例中,SpinLock
类使用了 AtomicBoolean
实现了自旋锁。lock()
方法使用CAS(Compare And Set)
操作尝试获取锁,如果获取失败则一直忙等待直到获取成功;unlock()
方法释放锁。在 main()
方法中创建了两个线程,并使用自旋锁保护临界区代码。
Synchronized
Synchronized同步代码块
作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁
synchronized void method() {
// 代码块
}
Synchronized普通同步方法
也就是给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁。因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管 new 了多少个对象,只有一份)。所以,如果一个线程 A 调用一个实例对象的非静态 synchronized
方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized
方法,是允许的,不会发生互斥现象,因为访问静态 synchronized
方法占用的锁是当前类的锁,而访问非静态 synchronized
方法占用的锁是当前实例对象锁。
synchronized void staic method() {
//业务代码
}
Synchronized静态同步方法
指定加锁对象,对给定对象/类加锁。synchronized(this|object)
表示进入同步代码库前要获得给定对象的锁。synchronized(类.class)
表示进入同步代码前要获得 当前 class 的锁
synchronized(this) {
//业务代码
}
ps:使用Synchronized的wait()和notify()让线程等待和唤醒
static Object objectLock = new Object();
public static void main(String[] args) {
new Thread(()->{
synchronized (objectLock){
System.out.println(Thread.currentThread().getName()+"t"+"----come in");
try {
objectLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"t"+"----被唤醒");
}
},"t1").start();
new Thread(()->{
synchronized (objectLock){
objectLock.notify();
System.out.println(Thread.currentThread().getName()+"t"+"----唤醒动作");
}
},"t2").start();
}
Lock
static Object objectLock = new Object();
static Lock lock = new ReentrantLock();
static Condition condition = lock.newCondition();
public static void main(String[] args) {
new Thread(()->{
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"t"+"----come in");
condition.await();
System.out.println(Thread.currentThread().getName()+"t"+"----被唤醒");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
},"t1").start();
new Thread(()->{
lock.lock();
try{
condition.signal();
System.out.println(Thread.currentThread().getName()+"t"+"----发出许可");
}finally{
lock.unlock();
}
},"t2").start();
}
LockSuppport
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
LockSupport中的park()和unpark()的作用分别是阻塞线程和解除阻塞线程。
3种让线程等待和唤醒的方法:
- 使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程(Synchronized)
- 使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程(Lock Condition)
- LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程(LockSupport)
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "t" + "----come in");
LockSupport.park();
System.out.println(Thread.currentThread().getName() + "t" + "----被唤醒");
}, "t1");
t1.start();
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
new Thread(() -> {
LockSupport.unpark(t1);
System.out.println(Thread.currentThread().getName() + "t" + "----唤醒动作");
}, "t2").start();
}
//先执行unpark(),依然可以被唤醒
public static void main(String[] args) {
Thread a = new Thread(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "t" + "----come in");
LockSupport.park();
System.out.println(Thread.currentThread().getName() + "t" + "----被唤醒");
}, "A");
a.start();
Thread b = new Thread(() -> {
LockSupport.unpark(a);
System.out.println(Thread.currentThread().getName() + "t" + "----唤醒动作");
}, "B");
b.start();
}
小结
LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。归根揭底,LockSupport调用的是Unsafe中的native代码。
LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit)。
permit只有两个值1和0,默认是0。
可以把许可看成是一种(0,1)信号量(Semaphore),但与Semaphore不同的是,许可的累加上限是1。
permit默认是0,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,park()方法会被唤醒,然后会将permit再次设置为0并返回。
问:为什么可以先唤醒线程后阻塞线程?
答:因为unpark获取了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞。类似于高速进站,已经办理了ETC,所以闸道不会拦截,直接放行。
问:为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?
答:因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证;而调用两次park却需要消费两个凭证,证不够,不能放行。
java内存模型 JMM
Q1:你知道什么是Java内存模型JMM吗?
A1:JMM(Java内存模型 Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在,它仅仅描述的是一组约定或规范,通过这组规范定义了程序中(尤其是多线程)各个变量的读写访问方式并决定一个线程对共享变量的何时写入及如何变成对另一个线程可见,关键技术点都是围绕多线程的原子性、可见性和有序性展开的。
JMM能干什么?
1、通过JMM来实现线程和主内存之间的抽象关系
2、屏蔽各个硬件平台和操作系统的内存访问差异以实现让Java程序在各个平台下都能达到一致的内存访问效果
Q2:JMM与volatile它们两个之间的关系?
JMM(Java Memory Model)和 volatile 关键字之间有着密切的关系,volatile 关键字是 JMM 的一部分,用于解决内存可见性和指令重排序问题。
1. 内存可见性(Memory Visibility):
- 在没有使用
volatile
关键字的情况下,一个线程对共享变量的修改可能对其他线程不可见,因为修改操作可能会被编译器优化或者处理器重排序所影响。使用volatile
关键字可以确保对volatile
变量的写操作对其他线程可见,从而解决了内存可见性问题。
2. 指令重排序(Instruction Reordering):
- 在没有使用
volatile
关键字的情况下,编译器和处理器可能会对指令进行重排序,从而导致程序的执行结果与预期不一致。使用volatile
关键字可以防止指令重排序,即对volatile
变量的写操作不会与其前面的操作重排序,对volatile
变量的读操作不会与其后面的操作重排序。
因此,volatile
关键字可以保证对 volatile
变量的写操作具有原子性、可见性和有序性,从而保证了多线程程序的正确性和稳定性。在 Java 中,volatile
关键字是一种轻量级的同步机制,常用于解决状态标志、双重检查锁定等场景下的内存可见性和指令重排序问题。
Q3:JMM有哪些特性or它的三大特性是什么?
1. 内存可见性(Memory Visibility):
- 当一个线程修改了共享变量的值后,其他线程能够立即看到修改后的值。JMM 通过定义 Happens-Before 关系来保证内存可见性,即对一个变量的写操作 Happens-Before 于对该变量的读操作。
2. 指令重排序(Instruction Reordering):也称有序性
- 在没有同步的情况下,编译器和处理器可能会对指令进行重排序,从而导致程序的执行结果与预期不一致。JMM 通过 Happens-Before 关系来防止指令重排序,保证了程序的执行顺序与代码的书写顺序一致。
3. 原子性(Atomicity):
- JMM 提供了一种机制来保证对共享变量的读写操作是原子性的。原子性指的是一个操作是不可中断的,要么全部执行成功,要么全部不执行。JMM 使用 synchronized 关键字、volatile 关键字、Atomic 类等来保证原子性。
Q4:happens-before先行发生原则你有了解过吗?
Happens-Before
原则是 Java 内存模型(Java Memory Model,JMM
)中定义的一种偏序关系,用于规定程序中各个操作之间的执行顺序。具体来说,如果一个操作 Happens-Before
另一个操作,那么第一个操作的执行结果对于第二个操作是可见的,即第一个操作的执行顺序先于第二个操作的执行顺序。Happens-Before
原则定义了 Java 程序中操作之间的执行顺序,通过这些规则,可以确保程序中的操作在多线程环境下的执行顺序是可预测和一致的,从而保证了多线程程序的正确性和稳定性。
【信息由网络或者个人提供,如有涉及版权请联系COOY资源网邮箱处理】
暂无评论内容