多线程篇-9--锁的使用及分类(可重入锁,读写锁,锁降级,死锁,LockSupport,乐观锁,悲观锁,分段锁等)
1、锁的概述
Java 中,锁是一种用于控制多线程并发访问共享资源的机制。合理的锁机制可以确保线程安全,避免数据竞争和不一致的问题。 Java 提供了多种锁机制,包括内置锁(即 synchronized 关键字)、显式锁(如 ReentrantLock)、读写锁(如 ReentrantReadWriteLock)等。
2、Lock锁和synchronized的区别
1、synchronized是一个关键字,可以直接应用于方法或代码块。Lock 是一个接口,提供了比 synchronized更丰富的锁操作。
2、synchronized当同步代码块或方法执行完毕或抛出异常时,锁会自动释放。Lock需要手动获取和释放锁,通常在 try-finally
块中使用,确保锁在任何情况下都能被释放。
3、synchronized锁是非公平的,即等待时间最长的线程不一定最先获得锁。ReentrantLock可以选择是否使用公平锁。公平锁确保等待时间最长的线程最先获得锁。
Lock lock = new ReentrantLock(true); // 公平锁
4、synchronized锁的粒度是对象级别的,即一个对象的多个同步方法之间会相互阻塞。Lock可以更细粒度地控制锁,允许多个锁实例,从而减少不必要的阻塞。
5、条件变量不一样,synchronized内使用Object类的wait和notify方法;Lock提供了Condition接口,通过await和signal方法实现线程等待唤醒机制。
Lock lock = new ReentrantLock();Condition condition = lock.newCondition();try {lock.lock();// 等待条件condition.await();// 通知条件condition.signal();} catch (InterruptedException e) {// 处理中断异常} finally {lock.unlock();}
6、synchronized而言,获取锁的线程和等待获取锁的线程都是不可中断的;Lock可以通过灵活的机制控制是否可被中断。
Lock可中断获取锁示例:
如下的代码中,通过lock.lockInterruptibly()可中断的获取锁,那么被中断时会直接中断抛出异常;如果是lock.lock()获取锁,那么就和synchronized一样,任然会继续执行。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class LockExample {private final Lock lock = new ReentrantLock();public void method() throws InterruptedException {try {System.out.println("Thread " + Thread.currentThread().getName() + " is trying to acquire the lock...");lock.lockInterruptibly(); // 可中断地获取锁,被中断时直接抛出中断异常System.out.println("Thread " + Thread.currentThread().getName() + " got the lock.");Thread.sleep(10000); // 模拟长时间操作} catch (InterruptedException e) {System.out.println("Thread " + Thread.currentThread().getName() + " was interrupted.");throw e;} finally {lock.unlock();}}public static void main(String[] args) {LockExample example = new LockExample();Thread t1 = new Thread(() -> {try {example.method();} catch (InterruptedException e) {System.out.println("Thread " + Thread.currentThread().getName() + " was interrupted.");}});Thread t2 = new Thread(() -> {try {example.method();} catch (InterruptedException e) {System.out.println("Thread " + Thread.currentThread().getName() + " was interrupted.");}});t1.start();t2.start();// 让主线程等待一段时间,确保t1已经进入同步代码块try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}// 中断t2线程t2.interrupt();}
}
3、常用锁
ReentrantLock 和 ReentrantReadWriteLock 是 Java 中提供的两种显式锁机制,它们提供了比 synchronized 更灵活的锁控制。
(1)、ReentrantLock可重入锁
ReentrantLock是一个可重入的互斥锁,允许多个线程在不同的时间点上持有同一个锁,但同一时间只有一个线程可以持有锁。
它提供了比 synchronized
更多的功能,例如公平锁、非公平锁、尝试加锁等。
1、主要特性
可重入:一个线程可以多次获取同一个锁,而不会被自己阻塞。(注意:可重入即多次获取同一个锁上锁,获取多少次就要解锁多少次,不然锁就无法释放。)
公平锁和非公平锁:可以选择是否按照请求锁的顺序来分配锁。(构造时指定)
尝试加锁:可以尝试获取锁,如果不能立即获取到锁,可以选择不阻塞而是返回一个布尔值。(如果返回ture标识抢到了锁)
锁中断:可以中断正在等待锁的线程。(lockInterruptibly上锁方式)
2、主要方法
lock()
:获取锁。unlock()
:释放锁。tryLock()
:尝试获取锁,如果立即可用则返回true
(已经抢到了锁,无需在用lock方法),否则返回false
。tryLock(long time, TimeUnit unit)
:尝试在指定时间内获取锁,如果超时则返回false
。lockInterruptibly()
:尝试获取锁,如果不能立即获取到锁并且当前线程被中断,则抛出InterruptedException
。newCondition()
:创建一个与锁绑定的条件对象。
注意:
(1)、lock() 方法:会阻塞当前线程,一直抢锁,直到锁可用为止。
(2)、tryLock() 方法:非阻塞尝试,如果锁当前可用,则立即返回 true
;如果锁不可用,则立即返回 false
,不会阻塞当前线程。
(3)、tryLock(long time, TimeUnit unit) 方法:带尝试在指定的时间内获取锁,如果在指定时间内锁可用,则返回 true
;如果超时仍未获取到锁,则返回 false
。
(4)、lockInterruptibly()方法:可中断式的抢锁,这种方式抢到锁,被其他线程中断后会抛出中断异常。上面其他方式的抢锁,被中断时不会抛出异常。
3、普通使用锁代码示例
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class ReentrantLockExample {private final Lock lock = new ReentrantLock();public void method() {lock.lock(); // 上锁try {// 只有一个线程可以进入这个代码块System.out.println("Method is running by " + Thread.currentThread().getName());} finally {lock.unlock(); // 解锁,建议放到finally中,一定要解锁。}}public static void main(String[] args) {ReentrantLockExample example = new ReentrantLockExample();Thread t1 = new Thread(() -> example.method(), "Thread 1");Thread t2 = new Thread(() -> example.method(), "Thread 2");t1.start();t2.start();}
}
4、可重入特性代码示例
利用可重入的特性处理业务
import java.util.concurrent.locks.ReentrantLock;public class LockOptimizationExample {private final ReentrantLock lock = new ReentrantLock();private int count = 0;public void increment() {lock.lock();try {count++;} finally {lock.unlock();}}public static void main(String[] args) {LockOptimizationExample example = new LockOptimizationExample();Thread thread1 = new Thread(() -> {for (int i = 0; i < 1000; i++) {example.increment(); // thread1线程会执行1000次increment方法,即会获取1000次锁资源,同时完全释放也需要解锁1000次}});Thread thread2 = new Thread(() -> {for (int i = 0; i < 1000; i++) {example.increment();}});thread1.start();thread2.start();try {thread1.join();thread2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Final count: " + example.count);}
}
(2)、ReentrantReadWriteLock读写锁
ReentrantReadWriteLock是读写锁,允许多个读线程同时访问资源,但写线程独占资源。这种锁机制适用于读多写少的场景,可以显著提高并发性能。
定义一个ReentrantReadWriteLock读写锁对象,实际上可以使用两把锁,分别是这个对象的读锁和写锁。
1、主要特性
读锁:允许多个读线程同时访问资源。
写锁:只允许一个写线程访问资源,写锁优先于读锁。
可重入:读锁和写锁都是可重入的,一个线程可以多次获取同一个锁。
公平锁和非公平锁:可以选择是否按照请求锁的顺序来分配锁。
理解一下:
读写锁中有有读锁和写锁两种锁,都是可重入的,但是两种锁是不允许同时存在的。读锁允许多个线程同时占用,但是写锁只能允许一个线程占有。
简单说,读锁存在时,可以存在多个线程共有,新的线程想抢占写锁,必须所有的读锁线程都释放读锁后,新的写锁的线程才能抢到锁开始工作。反之一样,写锁(只会有一个线程)存在时,新的读锁线程是无法获取到读锁的,只有当写锁的线程释放锁后,读锁的线程才能开始抢到读锁,读取数据。
但是在同一个线程中,可以先获取到写锁,在获取读锁,可以同时拥有两把锁。通常都是锁降级情况下才会这么使用。
2、主要方法
- readLock():获取读锁。
- writeLock():获取写锁。
- newCondition():创建一个与锁绑定的条件对象。
示例代码
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;public class ReadWriteLockExample {private final ReadWriteLock lock = new ReentrantReadWriteLock(); // 定义一个读写锁对象private final int[] data = new int[100];public void readData() {lock.readLock().lock(); // 读锁上锁,允许其他的读线程持有,但会阻塞写线程,直到所有的读锁都释放后,写锁可被持有使用try {for (int i = 0; i < data.length; i++) {System.out.println("Reading data[" + i + "] = " + data[i]);}} finally {lock.readLock().unlock(); // 释放读锁}}public void writeData(int index, int value) {lock.writeLock().lock(); // 写锁,仅允许一个线程持有,会阻塞读线程持有。写锁释放后,读线程才能抢占读锁进行数据读取。try {data[index] = value;System.out.println("Writing data[" + index + "] = " + value);} finally {lock.writeLock().unlock(); // 释放写锁}}public static void main(String[] args) {ReadWriteLockExample example = new ReadWriteLockExample();Thread reader1 = new Thread(() -> example.readData(), "Reader 1"); // reader1和reader2都能抢到锁都能执行Thread reader2 = new Thread(() -> example.readData(), "Reader 2");Thread writer = new Thread(() -> example.writeData(0, 10), "Writer"); // writer需要等待两个读线程都释放读锁后才能抢到写锁,开始执行reader1.start();reader2.start();writer.start();}
}
3、锁降级(写锁降级为读锁)
锁降级,实际上是在读写锁使用中的一个概念。把写锁降级为读锁的过程。关键在于利用 ReentrantReadWriteLock 的可重入特性。可以在保持线程安全的前提下,将写锁降级为读锁,确保在多线程环境下的数据一致性。
具体步骤如下:
1、获取写锁:线程首先获取写锁,确保在写操作期间没有其他线程可以读取或写入数据。
2、获取读锁:在保持写锁的情况下,在获取读锁。由于同一个线程可以多次获取同一个锁,这里不会出现问题。
3、释放写锁:在保持读锁的情况下,先释放写锁。此时,写锁被释放,但读锁仍然被持有,允许其他读线程访问数据。
4、继续读操作:在释放写锁后,当前线程可以继续执行读操作,确保数据的一致性和完整性。
5、释放读锁:最后释放读锁,完成整个过程。
锁降级代码示例:
import java.util.concurrent.locks.ReentrantReadWriteLock;public class WriteToReadLockDowngradeExample {private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); // 定义读写锁private final int[] data = new int[100];public void writeAndDowngradeToRead(int index, int value) {ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); // 写锁,控制写线程执行ReentrantReadWriteLock.ReadLock readLock = lock.readLock(); // 读锁,控制读线程执行// 获取写锁writeLock.lock(); // 先获取写锁try {// 修改数据data[index] = value;System.out.println("Writing data[" + index + "] = " + value);// 在保持写锁的情况下获取读锁readLock.lock(); // 在获取读锁} finally {// 释放写锁,但保持读锁writeLock.unlock(); // 持有读锁时,释放写锁,实现锁降级}try {// 继续执行读操作System.out.println("Reading data[" + index + "] = " + data[index]);} finally {// 最后释放读锁readLock.unlock(); // 最后释放读锁}}public void readData(int index) {ReentrantReadWriteLock.ReadLock readLock = lock.readLock();readLock.lock();try {System.out.println("Reading data[" + index + "] = " + data[index]);} finally {readLock.unlock();}}public static void main(String[] args) {WriteToReadLockDowngradeExample example = new WriteToReadLockDowngradeExample();Thread writer = new Thread(() -> example.writeAndDowngradeToRead(0, 10), "Writer");Thread reader1 = new Thread(() -> example.readData(0), "Reader 1");Thread reader2 = new Thread(() -> example.readData(0), "Reader 2");writer.start();reader1.start();reader2.start();}
}
(3)可重入锁和读写锁对比
4、条件变量Condition(监视器)
Condition 是 java.util.concurrent.locks 包中的接口,用于在锁的基础上实现更复杂的线程同步机制。
(如实现synchronsized中使用wait和notify方法实现等待/通知机制,Condition是Lock接口提供的接口,也可以实现等待/同步机制)
1、主要方法
- await():使当前线程等待,释放锁。
- signal():唤醒一个等待的线程。
- signalAll():唤醒所有等待的线程。
2、示例代码
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class ConditionExample {private final Lock lock = new ReentrantLock();private final Condition condition = lock.newCondition();private boolean ready = false;public void prepare() {lock.lock();try {ready = true;condition.signalAll();} finally {lock.unlock();}}public void consume() {lock.lock();try {while (!ready) {condition.await();}System.out.println("Resource is ready, consuming...");} catch (InterruptedException e) {Thread.currentThread().interrupt();} finally {lock.unlock();}}
}
5、LockSupport工具
(1)、概述
LockSupport 是 Java 并发工具类中的一个重要工具,位于 java.util.concurrent.locks 包中。
它提供了一系列静态方法,用于精确地控制线程的挂起和唤醒操作。LockSupport
是许多高级并发工具(如 ReentrantLock、Semaphore、CountDownLatch 等)的基础。
(2)、主要方法
1、park(Object blocker)
- 挂起当前线程,直到其他线程调用 unpark 方法或当前线程被中断。
- blocker参数是一个对象,用于标识阻塞当前线程的原因,主要用于调试目的。
2.、parkNanos(Object blocker, long nanos)
- 挂起当前线程最多
nanos
纳秒,或者直到其他线程调用unpark
方法或当前线程被中断。(超时后会自动唤醒) blocker
参数是一个对象,用于标识阻塞当前线程的原因,主要用于调试目的。
3、parkUntil(Object blocker, long deadline)
- 挂起当前线程直到指定的绝对时间
deadline
,或者直到其他线程调用unpark
方法或当前线程被中断。 blocker
参数是一个对象,用于标识阻塞当前线程的原因,主要用于调试目的。
4、unpark(Thread thread)
- 唤醒指定的线程。如果该线程已经被唤醒或未被挂起,则此方法没有任何效果。
5、getBlockedThread(Object blocker)
- 返回当前被
blocker
阻塞的线程,如果没有线程被阻塞则返回null
。
6、currentThread()
- 返回当前正在执行的线程。
(3)、代码示例:
示例 1:基本的挂起和唤醒
import java.util.concurrent.locks.LockSupport;public class LockSupportExample {public static void main(String[] args) {Thread t1 = new Thread(() -> {System.out.println("Thread T1 is running");LockSupport.park("Blocker"); // t1线程挂起System.out.println("Thread T1 is unblocked");}, "T1");t1.start();try {Thread.sleep(1000); // 让主线程等待1秒} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Unparking T1");LockSupport.unpark(t1); // t1线程唤醒}
}
在这个示例中,线程 T1
在启动后会调用 LockSupport.park("Blocker")
挂起自己。主线程等待1秒后,调用 LockSupport.unpark(t1)
唤醒 T1
。
示例 2:带有超时的挂起
import java.util.concurrent.locks.LockSupport;public class LockSupportTimeoutExample {public static void main(String[] args) {Thread t1 = new Thread(() -> {System.out.println("Thread T1 is running");LockSupport.parkNanos("Blocker", 2_000_000_000L); // t1线程挂起,最多2秒后唤醒,或者期间被unpark唤醒System.out.println("Thread T1 is unblocked");}, "T1");t1.start();try {Thread.sleep(1000); // 让主线程等待1秒} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Unparking T1");LockSupport.unpark(t1); // 唤醒t1线程,实际上t1仅挂起1秒}
}
在这个示例中,线程 T1
在启动后会调用 LockSupport.parkNanos("Blocker", 2_000_000_000L)
挂起自己最多2秒。主线程等待1秒后,调用 LockSupport.unpark(t1)
唤醒 T1
。如果 T1
在2秒内没有被唤醒,它也会自动解除挂起状态。
示例 3:带有绝对时间的挂起
import java.util.concurrent.locks.LockSupport;public class LockSupportAbsoluteTimeExample {public static void main(String[] args) {Thread t1 = new Thread(() -> {System.out.println("Thread T1 is running");long deadline = System.currentTimeMillis() + 2000; // 2秒后LockSupport.parkUntil("Blocker", deadline); // 给定绝对时间的挂起System.out.println("Thread T1 is unblocked");}, "T1");t1.start();try {Thread.sleep(1000); // 让主线程等待1秒} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Unparking T1");LockSupport.unpark(t1);}
}
6、死锁的原因及常用避免方法
(1)、什么是死锁
死锁是指两个或多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象,导致这些线程都无法继续执行。在计算机科学中,死锁是一种常见的并发问题,特别是在多线程编程中。
(2)、产生原因
死锁的发生通常满足以下四个必要条件,称为 Coffman 条件:
1、互斥条件
资源不能被共享,只能由一个线程占用。即,一个线程占用了资源后,其他线程就会被阻塞。
2、请求与保持条件
一个线程已经持有了某些资源,但又申请新的资源,而这些新资源被其他线程持有。即:多个线程都要多把锁的情况,如:A线程抢到了锁1,需要锁2,锁2已被A线程持有,需要锁1,两者相互等待。
3、不剥夺条件
已经分配给线程的资源不能被强制回收,只能在该线程使用完后主动释放。
4、循环等待条件
存在一个线程等待环路,即每个线程都在等待下一个线程持有的资源。
(3)、综述
死锁是的一种常见的并发问题,因为锁的特性(抢占锁的线程必须主动释放锁,一个锁不能被两个线程同时持有),在遇到一个线程需要多个锁的条件时,就容易发生多路线程持有锁又要抢占其他锁的情况,就容易发生死锁。
死锁示例:
public class DeadlockExample {private final Object lockA = new Object();private final Object lockB = new Object();public void thread1() {synchronized (lockA) { // 先要A锁System.out.println("Thread 1: Holding lock A...");try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Thread 1: Waiting for lock B...");synchronized (lockB) { // 再要B锁System.out.println("Thread 1: Holding lock A & B...");}}}public void thread2() {synchronized (lockB) { // 先要B锁System.out.println("Thread 2: Holding lock B...");try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Thread 2: Waiting for lock A...");synchronized (lockA) { // 再要A锁System.out.println("Thread 2: Holding lock A & B...");}}}public static void main(String[] args) {DeadlockExample example = new DeadlockExample();Thread t1 = new Thread(() -> example.thread1(), "Thread 1");Thread t2 = new Thread(() -> example.thread2(), "Thread 2");t1.start(); // t1和t2多路线程都需要A和B锁才能完成工作t2.start();}
}
(4)、避免方法
1、锁排序
确保所有线程在获取多个锁时,总是按照相同的顺序获取。这样可以避免循环等待条件。
代码示例
public class LockOrderingExample {private final Object lock1 = new Object();private final Object lock2 = new Object();public void method1() {synchronized (lock1) {synchronized (lock2) {System.out.println("Method 1 holding both locks");}}}public void method2() {synchronized (lock1) {synchronized (lock2) {System.out.println("Method 2 holding both locks");}}}public static void main(String[] args) {LockOrderingExample example = new LockOrderingExample();Thread t1 = new Thread(() -> example.method1(), "Thread 1");Thread t2 = new Thread(() -> example.method2(), "Thread 2");t1.start();t2.start();}}
2、锁超时
使用 tryLock方法尝试获取锁,并设置超时时间。如果在指定时间内无法获取锁,则放弃尝试。(tryLock方法会返回true或false标识是否抢到锁资源,false时代码应该按照未抢到锁的情况编码)
代码示例
import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;import java.util.concurrent.TimeUnit;public class LockTimeoutExample {private final Lock lock1 = new ReentrantLock();private final Lock lock2 = new ReentrantLock();public void method1() {boolean locked1 = false;boolean locked2 = false;try {locked1 = lock1.tryLock(1, TimeUnit.SECONDS);locked2 = lock2.tryLock(1, TimeUnit.SECONDS);if (locked1 && locked2) { // 都抢到了锁,主要业务System.out.println("Method 1 holding both locks");} else {// 未抢到锁,一般直接返回给出提示System.out.println("Method 1 failed to acquire both locks");}} catch (InterruptedException e) {e.printStackTrace();} finally {if (locked1) { // 都要判断和释放锁lock1.unlock();}if (locked2) {lock2.unlock();}}}public void method2() {boolean locked1 = false;boolean locked2 = false;try {locked1 = lock2.tryLock(1, TimeUnit.SECONDS);locked2 = lock1.tryLock(1, TimeUnit.SECONDS);if (locked1 && locked2) {System.out.println("Method 2 holding both locks");} else {System.out.println("Method 2 failed to acquire both locks");}} catch (InterruptedException e) {e.printStackTrace();} finally {if (locked1) {lock2.unlock();}if (locked2) {lock1.unlock();}}}public static void main(String[] args) {LockTimeoutExample example = new LockTimeoutExample();Thread t1 = new Thread(() -> example.method1(), "Thread 1");Thread t2 = new Thread(() -> example.method2(), "Thread 2");t1.start();t2.start();}}
3、使用 tryLock 方法抢锁
尝试获取锁,如果无法获取则立即返回,而不是无限期等待。
注:
(1)、lock() 方法:会阻塞当前线程,一直抢锁,直到锁可用为止。
(2)、tryLock() 方法:非阻塞尝试,如果锁当前可用,则立即返回 true
;如果锁不可用,则立即返回 false
,不会阻塞当前线程。
(3)、tryLock(long time, TimeUnit unit) 方法:带尝试在指定的时间内获取锁,如果在指定时间内锁可用,则返回 true
;如果超时仍未获取到锁,则返回 false
。
4、死锁检测和恢复
通过定期检测系统中的死锁情况,并采取措施恢复,例如中断一个或多个线程,释放部分资源。
5、减少锁的范围
尽量减少锁的作用范围,只在必要的地方使用锁,减少锁的竞争。
7、锁分类
(1)、内置锁和显式锁
在 Java 中,锁分为两种主要类型:内置锁(也称为同步锁或内置同步)和显式锁(也称为显示锁)。
1、内置锁
(1)、概述
内置锁是通过 synchronized 关键字实现的(它提供了方法级和代码块级的锁)。Java 虚拟机(JVM)提供了内置的锁机制,使得多线程编程更加简单和直观。
(2)、特点
1、自动释放:当同步代码块或方法执行完毕后,内置锁会自动释放。
2、可重入:同一个线程可以多次获取同一个内置锁,不会导致死锁。(即synchronized 方法中可以在使用synchronized 代码块)
3、阻塞等待:如果一个线程尝试获取已被其他线程持有的内置锁,它会阻塞,直到锁可用。
4、不可中断:等待获取内置锁的线程不能被中断。
5、无超时:没有提供超时机制,等待线程会一直等待,直到锁可用。
示例代码
public class SynchronizedExample {private final Object lock = new Object();public void synchronizedMethod() {synchronized (lock) {// 同步代码块System.out.println("Synchronized block executed by " + Thread.currentThread().getName());}}public synchronized void synchronizedMethod2() {// 同步方法System.out.println("Synchronized method executed by " + Thread.currentThread().getName());}public static void main(String[] args) {SynchronizedExample example = new SynchronizedExample();Thread t1 = new Thread(() -> example.synchronizedMethod(), "Thread 1");Thread t2 = new Thread(() -> example.synchronizedMethod2(), "Thread 2");t1.start();t2.start();}
}
2、显示锁
(1)、概述
显式锁是通过 java.util.concurrent.locks.Lock
接口及其实现类(如 ReentrantLock
)提供的。显式锁提供了比内置锁更多的功能和灵活性。
(2)、特点
1、手动释放:需要手动调用 lock()
方法获取锁,调用 unlock()
方法释放锁。
2、可重入:支持可重入,同一个线程可以多次获取同一个锁。
3、可中断:等待获取锁的线程可以被中断。
4、超时机制:提供 tryLock(long time, TimeUnit unit)
方法,允许在指定时间内尝试获取锁。
5、公平锁:可以选择是否使用公平锁,公平锁确保线程按顺序获取锁。
6、条件变量:支持条件变量,可以实现更复杂的同步机制。
(3)、使用方式
1、获取锁
lock.lock();
2、释放锁
lock.unlock();
3、尝试获取锁
if (lock.tryLock()) {try {// 执行临界区代码} finally {lock.unlock();}}
4、带超时的尝试获取锁
try {if (lock.tryLock(2, TimeUnit.SECONDS)) {try {// 执行临界区代码} finally {lock.unlock();}} else {// 超时未能获取到锁}} catch (InterruptedException e) {// 处理中断异常}
3、内置锁和显式锁对比
总结
内置锁:即使用synchronized方式,简单易用,适合大多数基本的同步需求。
显式锁:即使用Lock接口的锁,功能强大,灵活性高,适合复杂的同步需求,如中断等待、超时机制、公平锁等。
(2)、乐观锁和悲观锁
乐观锁和悲观锁是两种不同的并发控制策略,它们在处理多线程环境下的数据竞争问题时采用了不同的方法
1、乐观锁
(1)、概述
乐观锁假设最好的情况,认为每次访问数据时不会有其他线程在修改数据,因此在访问数据时不加锁。如果在更新数据时发现数据已经被其他线程修改,则重新尝试更新操作,直到成功为止。
通常使用版本号和重试机制来实现乐观锁。
(2)、特点
非阻塞性:在读取数据时不加锁,提高了并发性能。
版本号:通常使用版本号或时间戳来判断数据是否被修改。
重试机制:如果数据被修改,需要重新读取数据并重试更新操作。
(3)、应用场景
读多写少:当读操作远多于写操作时,乐观锁可以显著提高并发性能。
低冲突:当数据冲突概率较低时,乐观锁是更好的选择。
(4)、示例代码
import java.util.concurrent.atomic.AtomicInteger;public class OptimisticLockExample {private int counter = 0;private AtomicInteger version = new AtomicInteger(0);public boolean increment() {int currentVersion = version.get();while (true) { // 重试机制int currentValue = counter;int newValue = currentValue + 1;if (version.compareAndSet(currentVersion, currentVersion + 1)) {counter = newValue;System.out.println("Counter incremented to " + counter + " by " + Thread.currentThread().getName());return true; // 设置成功就结束} else {// 版本号不匹配,数据已被其他线程修改,重试currentVersion = version.get();}}}public static void main(String[] args) {OptimisticLockExample example = new OptimisticLockExample();Thread t1 = new Thread(() -> example.increment(), "Thread 1");Thread t2 = new Thread(() -> example.increment(), "Thread 2");t1.start();t2.start();}
}
(5)、乐观锁的问题
1、读取脏数据
如果一个线程读取了数据,而在该线程准备更新数据之前,另一个线程已经修改了数据,那么第一个线程可能会基于过时的数据进行操作,导致数据不一致。
读取脏数据代码示例
import java.util.concurrent.atomic.AtomicInteger;public class OptimisticLockExample {private int counter = 0; // 普通int类型private AtomicInteger version = new AtomicInteger(0);public int read() {return counter;}public boolean increment() {while (true) {int currentValue = counter.get();int newValue = currentValue + 1;if (counter.compareAndSet(currentValue, newValue)) {System.out.println("Counter incremented to " + newValue + " by " + Thread.currentThread().getName());return true;}}}public static void main(String[] args) {OptimisticLockExample example = new OptimisticLockExample();Thread t1 = new Thread(() -> {int oldValue = example.read(); // t1先读取了值,直接睡眠1stry {Thread.sleep(1000); // 模拟长时间操作} catch (InterruptedException e) {e.printStackTrace();}example.increment(); // 直接睡眠1s后再去更新(但是这睡眠的1秒内有其他线程更新了值可能造成问题)}, "Thread 1");Thread t2 = new Thread(() -> example.increment(), "Thread 2");t1.start();t2.start();}
}
在这个示例中,Thread 1
读取了初始值 0
,然后休眠1秒钟。在这期间,Thread 2
将 counter
增加到 1
。当 Thread 1
唤醒并尝试增加 counter
时,它仍然基于过时的数据 0
进行操作,这可能导致数据不一致。
改进示例:
使用 Atomic类避免读取脏数据
import java.util.concurrent.atomic.AtomicInteger;public class OptimisticLockExample {private AtomicInteger counter = new AtomicInteger(0); // 使用原子类public int read() {return counter.get();}public boolean increment() {while (true) {int currentValue = counter.get();int newValue = currentValue + 1;if (counter.compareAndSet(currentValue, newValue)) {System.out.println("Counter incremented to " + newValue + " by " + Thread.currentThread().getName());return true;}}}public static void main(String[] args) {OptimisticLockExample example = new OptimisticLockExample();Thread t1 = new Thread(() -> {int oldValue = example.read();try {Thread.sleep(1000); // 模拟长时间操作} catch (InterruptedException e) {e.printStackTrace();}example.increment(); // 基于最新的数据进行更新}, "Thread 1");Thread t2 = new Thread(() -> example.increment(), "Thread 2");t1.start();t2.start();}
}
在这个改进的示例中,counter
使用 AtomicInteger
类型,确保读取和更新操作的原子性。即使 Thread 1
在读取数据后休眠一段时间,它在更新时仍然会基于最新的数据进行操作,避免了读取脏数据的问题。
2、重试次数过多
在高并发环境下,如果多个线程频繁尝试更新同一个数据,可能会导致大量的重试操作,从而降低系统的整体性能。
3、ABA问题
乐观锁通常使用版本号或时间戳来判断数据是否被修改。然而,如果一个数据从A变为B,再变回A,版本号或时间戳可能不会发生变化,这会导致乐观锁误判数据未被修改。
(6)、乐观锁问题常用的解决方案:
1、使用悲观锁:在高并发或数据敏感的场景下,可以考虑使用悲观锁来确保数据的一致性。
2、减少读取和更新之间的延迟:尽量减少读取和更新之间的间隔,以减少数据被其他线程修改的可能性。
3、使用 CAS 操作:使用 Atomic
类提供的 compareAndSet
方法来确保原子性操作,避免中间数据被修改。
4、版本号或时间戳:使用版本号或时间戳来记录数据的修改次数,确保在更新时检查版本号或时间戳的一致性。
5、事务管理:在数据库操作中,可以使用事务管理来确保数据的一致性。事务具有 ACID(原子性、一致性、隔离性、持久性)特性,可以有效防止数据不一致的问题。
2、悲观锁
(1)、概述
悲观锁假设最坏的情况,认为每次访问数据时都可能有其他线程在修改数据,因此在访问数据前总是先加锁。
这种方式确保了数据的一致性和完整性,但可能会导致性能下降,因为频繁的加锁和解锁操作会增加开销。
(2)、特点
互斥性:同一时间只有一个线程可以访问数据。
阻塞等待:如果一个线程已经持有锁,其他线程必须等待。
线程安全:确保数据的一致性和完整性。
(3)、应用场景
写多读少:当写操作频繁且并发度较高时,悲观锁可以确保数据的一致性。
数据敏感:当数据对一致性要求非常高时,悲观锁是更好的选择。
(4)、示例代码
使用 ReentrantLock 实现悲观锁:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class PessimisticLockExample {private final Lock lock = new ReentrantLock();private int counter = 0;public void increment() {lock.lock();try {counter++;System.out.println("Counter incremented to " + counter + " by " + Thread.currentThread().getName());} finally {lock.unlock();}}public static void main(String[] args) {PessimisticLockExample example = new PessimisticLockExample();Thread t1 = new Thread(() -> example.increment(), "Thread 1");Thread t2 = new Thread(() -> example.increment(), "Thread 2");t1.start();t2.start();}
}
3、对比总结
悲观锁:假设最坏的情况,每次访问数据时都加锁,确保数据的一致性和完整性。适用于写多读少的场景。
乐观锁:假设最好的情况,每次访问数据时不加锁,使用版本号或时间戳来判断数据是否被修改。适用于读多写少的场景。但是在高并发或数据敏感的场景下,可能会导致读写不一致的问题,这种情况下还是使用悲观锁比较好,毕竟数据安全才是最重要的。
(3)、独享锁和共享锁
1、独享锁
独享锁(Exclusive Lock)也称为排他锁或写锁。当一个线程获取了独享锁后,其他线程不能获取该锁,直到当前线程释放锁。独享锁确保在同一时间只有一个线程可以访问资源,常用于写操作。
如:ReentrantLock 或ReentrantReadWriteLock.WriteLock(读写锁的写锁)
2、共享锁
共享锁(Shared Lock)也称为读锁。当一个线程获取了共享锁后,其他线程也可以获取该锁,但不能获取独享锁。共享锁允许多个线程同时读取资源,常用于读操作。
如:ReentrantReadWriteLock.ReadLock(读写锁的读锁)
(4)、可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法可以再次获取锁。简单来说就是同一个线程可以重复加锁,每次加锁的时候count值加1,每次释放锁的时候count减1,直到count为0,其他的线程才可以再次获取。
(5)、公平锁和非公平锁
1、公平锁
按照请求锁的顺序来分配锁,确保每个线程都能按顺序获得锁。
2、非公平锁
不保证锁的获取顺序,允许插队,可能提高吞吐量但可能导致某些线程长期等待。
示例代码
import java.util.concurrent.locks.ReentrantLock;public class FairLockExample {private final ReentrantLock fairLock = new ReentrantLock(true); // true为公平锁public void method() {fairLock.lock();try {System.out.println("Method is running by " + Thread.currentThread().getName());} finally {fairLock.unlock();}}
}
(6)、分段锁
1、概述
分段锁(Segmented Locking)是一种优化锁机制的技术,通过将数据分成多个段(segments),每个段使用独立的锁来减少锁的竞争,从而提高并发性能。如并发容器ConcurrentHashMap实际就是通过分段锁的形式来实现高效的并发操作。
分段锁特别适用于大型数据结构,如哈希表或集合,其中多个线程可以同时访问不同的段而不相互干扰。
2、实现原理
(1)、当创建一个ConcurrentHashMap对象时(如map),其内部实际上创建了多个Segment(简单理解为HashMap)对象。
(2)、在操作map对象时,会根据操作的key获取其中的一个Segment对象(可以是hashCode在求余,保证这个key一定会存到这个分段中)
(3)、操作时实际上只对获取到的这个Segment上锁,并没有对整个map上锁,从而实现了可并发又保障了数据安全。
分段锁实现哈希表代码示例
下面是一个简单的分段锁实现哈希表的示例。我们将哈希表分成多个段,每个段有一个独立的锁。
import java.util.concurrent.locks.ReentrantLock;public class SegmentedHashTable<K, V> {private static final int DEFAULT_SEGMENT_SIZE = 16; // 默认分段数量private final Segment<K, V>[] segments; // 分段数组public SegmentedHashTable(int segmentSize) {this.segments = new Segment[segmentSize]; // 创建分段数组for (int i = 0; i < segmentSize; i++) {segments[i] = new Segment<>();}}public V get(K key) {int hash = key.hashCode();int segmentIndex = hash % segments.length; // 根据key找到对应的分段return segments[segmentIndex].get(key);}public void put(K key, V value) {int hash = key.hashCode();int segmentIndex = hash % segments.length;segments[segmentIndex].put(key, value);}public V remove(K key) {int hash = key.hashCode();int segmentIndex = hash % segments.length;return segments[segmentIndex].remove(key);}private static class Segment<K, V> {private final ReentrantLock lock = new ReentrantLock();private final java.util.Map<K, V> map = new java.util.HashMap<>();public V get(K key) {lock.lock(); // 分段内上锁try {return map.get(key);} finally {lock.unlock(); // 分段内解锁}}public void put(K key, V value) {lock.lock();try {map.put(key, value);} finally {lock.unlock();}}public V remove(K key) {lock.lock();try {return map.remove(key);} finally {lock.unlock();}}}public static void main(String[] args) {SegmentedHashTable<String, String> table = new SegmentedHashTable<>(DEFAULT_SEGMENT_SIZE);Thread t1 = new Thread(() -> table.put("key1", "value1"), "Thread 1");Thread t2 = new Thread(() -> table.put("key2", "value2"), "Thread 2");Thread t3 = new Thread(() -> table.put("key3", "value3"), "Thread 3");t1.start();t2.start();t3.start();try {t1.join();t2.join();t3.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(table.get("key1"));System.out.println(table.get("key2"));System.out.println(table.get("key3"));}
}
解释下:
1、分段初始化
SegmentedHashTable
构造函数初始化多个Segment
对象,每个Segment
对象包含一个ReentrantLock
和一个HashMap
。segments
数组的大小可以通过参数segmentSize
指定,默认为16。
2、获取段索引
get
、put
和remove
方法通过计算键的哈希码来确定对应的段索引。hash % segments.length
计算出键应该属于哪个段。
3、段操作
Segment
类中的get
、put
和remove
方法在操作HashMap
时都会先获取段的锁,操作完成后释放锁。
(7)偏向锁、轻量级锁和重量级锁
在Java的并发编程中,锁的实现经历了从重量级锁到轻量级锁再到偏向锁的演进过程。这些锁机制的设计目的是为了在不同的并发场景下提供更高的性能和更低的开销。
在Java中,可以通过 synchronized
关键字或 ReentrantLock
类来实现。JVM会根据实际情况决定使用哪一种机制,无需我们在代码上处理。
1、重量级锁(Heavyweight Lock)
重量级锁是传统的锁机制,通常使用操作系统提供的互斥锁(Mutex)来实现。
特点
(1)、阻塞等待:如果一个线程已经持有锁,其他线程必须等待,直到锁被释放。
(2)、上下文切换:重量级锁会导致线程的上下文切换,开销较大。
(3)、互斥性:同一时间只有一个线程可以持有锁。
2、轻量级锁(Lightweight Lock)
轻量级锁是重量级锁的一种优化机制。
轻量级锁在多线程竞争不激烈的情况下,可以避免重量级锁的开销。轻量级锁通过自旋锁(Spin Lock)来实现,线程在等待锁时不会立即阻塞,而是通过自旋(循环等待)来尝试获取锁。
特点
(1)、自旋等待:线程在等待锁时不会立即阻塞,而是通过自旋(循环等待)来尝试获取锁。
(2)、减少上下文切换:轻量级锁减少了线程的上下文切换,提高了性能。
(3)、适用场景:适用于多线程竞争不激烈的场景。
3、偏向锁(Biased Locking)
偏向锁是进一步优化轻量级锁的一种机制。
偏向锁假设在多线程竞争不激烈的场景下,大部分情况下只有一个线程访问某个对象。因此,偏向锁会将锁偏向于第一个访问该对象的线程,减少不必要的锁操作。
特点
(1)、偏向于一个线程:偏向锁会将锁偏向于第一个访问对象的线程,减少锁的开销。
(2)、减少锁的开销:偏向锁减少了锁的获取和释放的开销,提高了性能。
(3)、适用场景:适用于多线程竞争不激烈的场景,特别是大部分情况下只有一个线程访问对象的场景。
理解一下:
以过安检示例,看门大爷对于新员工会喊停下来并检查证件后才放行。之后熟悉了,看到该员工脸就直接放行了。
Jvm实现重量级锁,轻量级锁,偏向锁也是类似的过程。第一次都是以重量级锁方式处理;频繁该线程请求该资源后就变成轻量级锁,直到变为偏向锁。
4、锁的升级过程
(1)、无锁状态:对象没有任何锁。
(2)、偏向锁:当一个线程第一次访问对象时,JVM会尝试将锁偏向于该线程。
(3)、轻量级锁:如果多个线程竞争同一个对象的锁,偏向锁会升级为轻量级锁。
(4)、重量级锁:如果竞争激烈,轻量级锁会升级为重量级锁。
5、总结
(1)、重量级锁:传统的锁机制,通过操作系统提供的互斥锁实现,适用于高并发写操作和数据敏感的场景。
(2)、轻量级锁:通过自旋锁实现,适用于多线程竞争不激烈的场景,减少上下文切换的开销。
(3)、偏向锁:进一步优化轻量级锁,适用于多线程竞争不激烈的场景,特别是大部分情况下只有一个线程访问对象的场景。
实际上作为程序员我们要做的就是正常上锁解锁就行,无需关心这三种状态机制的切换,这个是JVM会自动处理的。
(8)、自旋锁
1、概述
自旋锁(Spin Lock)是一种同步机制,当一个线程试图获取已经被其他线程持有的锁时,它不会立即进入阻塞状态,而是通过循环不断地尝试获取锁。如果在短时间内能够获取到锁,自旋锁可以避免线程的上下文切换开销,从而提高性能。缺点是循环会消耗CPU。
2、自旋锁和tryLock方法
自旋锁是一种锁机制,通过让线程在等待锁时不断循环(自旋)来尝试获取锁。
tryLock
方法本身不是自旋锁,但它可以被用来实现自旋锁的行为。如带有参数调用tryLock(时间)方法,就类似实现了自旋锁的功能。
示例代码
下面是一个简单的自旋锁实现示例,使用 volatile
关键字确保可见性和 Thread.yield()
方法让出CPU时间片。
public class SpinLock {private volatile int state = 0; // 0 表示未锁定,1 表示已锁定public void lock() {while (true) {if (state == 0 && compareAndSet(0, 1)) {// 成功获取锁就结束break;}// 自旋等待Thread.yield(); // 让出CPU时间片,避免CPU占用过高}}public void unlock() {state = 0; // 释放锁}private boolean compareAndSet(int expect, int update) {// 模拟 CAS 操作if (state == expect) {state = update;return true;}return false;}public static void main(String[] args) {SpinLock spinLock = new SpinLock();Thread t1 = new Thread(() -> {spinLock.lock();try {Thread.sleep(1000); // 模拟耗时操作} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Thread 1 released the lock");spinLock.unlock();}, "Thread 1");Thread t2 = new Thread(() -> {spinLock.lock();System.out.println("Thread 2 acquired the lock");spinLock.unlock();}, "Thread 2");t1.start();t2.start();}
}
学海无涯苦作舟!!!
相关文章:
多线程篇-9--锁的使用及分类(可重入锁,读写锁,锁降级,死锁,LockSupport,乐观锁,悲观锁,分段锁等)
1、锁的概述 Java 中,锁是一种用于控制多线程并发访问共享资源的机制。合理的锁机制可以确保线程安全,避免数据竞争和不一致的问题。 Java 提供了多种锁机制,包括内置锁(即 synchronized 关键字)、显式锁(…...
提升阅读体验,Balabolka让文字跃然“声”上
作为一个专业的语音合成工具,Balabolka为用户提供了全方位的文本朗读解决方案。这款软件不仅可以将各类文本实时转换为清晰的语音输出,还能将转换后的音频内容导出为多种主流格式。它强大的兼容性使其能够处理各类电子书和文档格式,让用户可以…...
【汇编语言】call 和 ret 指令(三) —— 深度解析汇编语言中的批量数据传递与寄存器冲突
文章目录 前言1. 批量数据的传递1.1 存在的问题1.2 如何解决这个问题1.3 示例演示1.3.1 问题说明1.3.2 程序实现 2. 寄存器冲突问题的引入2.1 问题引入2.2 分析与解决问题2.2.1 字符串定义方式2.2.2 分析子程序功能2.2.3 得到子程序代码 2.3 子程序的应用2.3.1 示例12.3.2 示例…...
嵌入式C编程:宏定义与typedef的深入对比与应用
目录 一、宏定义(Macro Definition) 1.1. 特点与应用 1.1.1 定义常量 1.1.2 定义函数式宏 1.1.3 条件编译 1.2. 作用范围和生命周期方面 1.3. 应用注意事项 二、typedef 2.1. 特点与应用 2.1.1 简化类型声明 2.1.2 提高代码可读性 2.1.3 实现…...
算法复杂度
目录: 算法的效率时间复杂度 1.算法的效率 1.1旋转数组习题分析 如何衡量一个算法的好坏呢? 案例:旋转数组(189. 轮转数组 - 力扣(LeetCode)) 思路:循环k次将所有元素向后移动一…...
时序约束进阶六:Set_Clock_Groups详解
目录 一、前言 二、时钟间关系 2.1 时钟关系分类 2.2 时钟关系查看 三、set_clock_groups设置 3.1 使用格式 3.2 优先级 3.3 约束设置示例 3.4 约束效果查看 四、Exclusive差异说明 4.1 Asynchronous 4.2 Logically_exclusive与Physically_exclusive 4.3 logical…...
《运放秘籍》第二部:仪表放大器专项知识点总结
一、差分放大器与仪表放大器的讨论 1.1. 仪放的前世今生——差分放大器原理? 1.2. 差分放大的原理 1.3. 差分放大器检测电流 1.4. 差分放大器端一:输入阻抗 1.5. 差分放大器端二:共模抑制比 1.6. 为什么关注输入阻抗?共模抑…...
JavaSE——异常
一、异常的概念 在Java中,将程序执行中发生的不正常行为称为"异常",开发过程中的语法错误和逻辑错误不是异常。 主要分为以下两大类: Error(错误):Java虚拟机无法解决的严重问题,是严重错误,程序…...
HormonyOS: 图形变换之Rotate
官网地址:rotate 1. 概述 rotate是鸿蒙系统为组件提供的旋转属性,通过rotate属性,可实现组件的旋转效果 2. rotate属性 2.1. 语法参数 rotate(value: RotateOptions) 参数: 参数名 类型 必填 说明 value RotateOptions…...
【Solidity】入门指南:智能合约开发基础
🌈个人主页: 鑫宝Code 🔥热门专栏: 闲话杂谈| 炫酷HTML | JavaScript基础 💫个人格言: "如无必要,勿增实体" 文章目录 Solidity入门指南:智能合约开发基础引言1. 开发环境搭建1.1 Remix I…...
HTMLHTML5革命:构建现代网页的终极指南 - 3. 开发工具选择
HTML&HTML5革命:构建现代网页的终极指南 3. 开发工具选择 大家好,我是莫离老师。 前两节课我们学习了 HTML 的基础概念和 HTML5 的主要特性,为接下来的实际开发奠定了理论基础。 今天,我们将讨论开发工具的选择问题。 选择合…...
智能设备安全隐患:五项关键解决措施
目前,我们的智能设备越来越多,而背后的物联网(IoT)安全像一面隐形的盾牌,默默地守护着我们周围那些复杂连网的设备。 为了让大家更加明白这些安全协议和操作是如何高效运作的,我们有必要深入探讨物联网安全…...
Android NDK开发 JNI 基础
在android 系统开发中 ndk开发是很重要的内容。ndk开发中 jni 是基础。 目录 一.什么是JNI 二. 如何使用JNI 1.Java 代码如何调用 c/c 代码 2. c/c如何调用 java 函数 一.什么是JNI JNI——Java Native Interface,它是Java平台的一个特…...
嵌入式linux之文件系统管理
嵌入式Linux文件系统的管理涉及多个方面,包括文件系统的创建、配置、维护以及优化。以下是一些关键点和实践技巧: 文件系统的创建与配置 选择合适的文件系统:根据应用需求(如读写频率、存储介质类型等)选择最合适的文…...
Y20030029 微信+SPRINGBOOT+MYSQL+LW+传统文化展示小程序的设计与实现 源代码 配置 文档
传统文化展示小程序 1.项目描述2. 课题开发的背景与意义3.项目功能4.界面展示5.源码获取 1.项目描述 基于微信小程序的传统文化展示小程序是一个集合了多种传统文化元素与现代化技术的创新平台。它充分利用了微信小程序的便捷性和普及性,为广大用户提供了一个深入了…...
【LC】3232. 判断是否可以赢得数字游戏
题目描述: 给你一个 正整数 数组 nums。 Alice 和 Bob 正在玩游戏。在游戏中,Alice 可以从 nums 中选择所有个位数 或 所有两位数,剩余的数字归 Bob 所有。如果 Alice 所选数字之和 严格大于 Bob 的数字之和,则 Alice 获胜。如果…...
【人工智能基础03】机器学习(练习题)
文章目录 课本习题监督学习的例子过拟合和欠拟合常见损失函数,判断一个损失函数的好坏无监督分类:kmeans无监督分类,Kmeans 三分类问题变换距离函数选择不同的起始点 重点回顾1. 监督学习、半监督学习和无监督学习的定义2. 判断学习场景3. 监…...
C/C++每日一练:合并K个有序链表
本篇博客将探讨如何 “合并K个有序链表” 这一经典问题。本文将从题目要求、解题思路、过程解析和相关知识点逐步展开,同时提供详细注释的代码示例。 链表(Linked List) 链表是一种线性数据结构,由一系列节点(Node&…...
jmeter基础07_组件的层级
课程大纲 1. 优先级/执行顺序(一般情况) 同级组件:按组件先后顺序执行。如:同一层的线程组、同一层的http请求。 上下级组件:先执行外层(上级),再执行内层(下级ÿ…...
【QNX+Android虚拟化方案】125 - 如何创建android-spare镜像
【QNX+Android虚拟化方案】125 - 如何创建android-spare镜像 1. Android侧创建 (ext4 / sparse) test_img.img 镜像 方法一2. Android侧创建 (ext4 / sparse) test_img.img 镜像 方法二3. qnx 侧 分区透传Android 配置3.1 配置分区透传3.2 Android 侧分区 rename3.3 创建挂载目…...
大R玩家流失预测在休闲社交游戏中的应用
摘要 预测玩家何时会离开游戏为延长玩家生命周期和增加收入贡献创造了独特的机会。玩家可以被激励留下来,战略性地与公司组合中的其他游戏交叉链接,或者作为最后的手段,通过游戏内广告传递给其他公司。本文重点预测休闲社交游戏中高价值玩家…...
使用Postman搞定各种接口token实战
现在许多项目都使用jwt来实现用户登录和数据权限,校验过用户的用户名和密码后,会向用户响应一段经过加密的token,在这段token中可能储存了数据权限等,在后期的访问中,需要携带这段token,后台解析这段token才…...
【C++】printf 函数详解与格式化输出控制
博客主页: [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: C 文章目录 💯前言💯printf 基础用法1.1 printf 的常见占位符1.2 占位符与参数的对应关系1.3 换行控制示例: 💯格式化输出控制2.1 输出宽度控制2.1.1 指定最小宽度 2.2 …...
在21世纪的我用C语言探寻世界本质——字符函数和字符串函数(1)
人无完人,持之以恒,方能见真我!!! 共同进步!! 文章目录 一、字符分类函数二、字符转换函数三、strlen的使用和模拟实现四、strcpy的使用和模拟实现五、strcat的使用和模拟实现六、strcmp的使用和…...
【专题】存储器管理
1. 存储器的层次结构 在计算机执行时,几乎每一条指令都涉及对存储器的访问,因此要求对存储器的访问速度能跟得上处理机的运行速度。 存储器的速度必须非常快,能与处理机的速度相匹配,否则会明显地影响到处理机的运行。 此外还要求…...
python股票数据分析(Pandas)练习
需求: 使用pandas读取一个CSV文件,文件内容包括股票名称、价格和交易量。完成以下任务: 找出价格最高的股票; 计算总交易量; 绘制价格折线图。 代码实现: import pandas as pd import matplotlib.pyplot …...
Hadoop生态圈框架部署(八)- Hadoop高可用(HA)集群部署
文章目录 前言一、部署规划二、Hadoop HA集群部署(手动部署)1. 下载hadoop2. 上传安装包2. 解压hadoop安装包3. 配置hadoop配置文件3.1 虚拟机hadoop1修改hadoop配置文件3.1.1 修改 hadoop-env.sh 配置文件3.3.2 修改 core-site.xml 配置文件3.3.3 修改 …...
抗干扰设计的检查细则
抗干扰设计是确保电子系统或设备在复杂电磁环境中稳定运行的重要环节,涉及多个方面的设计和实施。以下是对抗干扰设计的检查细则的详细归纳: 一、电源线与地线设计 电源线设计:选择合适的电源,尽量加宽电源线,保证电源…...
[Redis#12] 常用类型接口学习 | string | list
目录 0.准备 1.string get | set set_with_timeout_test.cpp set_nx_xx_test.cpp mset_test.cpp mget_test.cpp getrange_setrange_test.cpp incr_decr_test.cpp 2.list lpush_lrange_test.cpp rpush_test.cpp lpop_rpop_test.cpp blpop_test.cpp llen_test.cpp…...
React的ts文件中通过createElement拼接一段内容出来
比如接口返回一个值 const values [23.00, 40.00/kg];想做到如下效果, 如果单纯的用render渲染会很简单, 但是在ts文件中处理,所以采用了createElement拼接 代码如下: format: (values: string[]) > {if (!values || !val…...
【Git系列】Git 提交历史分析:深入理解`git log`命令
💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…...
深度学习笔记——生成对抗网络GAN
本文详细介绍早期生成式AI的代表性模型:生成对抗网络GAN。 文章目录 一、基本结构生成器判别器 二、损失函数判别器生成器交替优化目标函数 三、GAN 的训练过程训练流程概述训练流程步骤1. 初始化参数和超参数2. 定义损失函数3. 训练过程的迭代判别器训练步骤生成器…...
《地球科学与环境学报》
《地球科学与环境学报》报道范围涵盖基础地质、矿产地质、水资源与环境、工程地质、地球物理、地球信息科学等领域,刊载国内外未公开发表的有创新性或意义重大的研究论文和综述文章。 来稿必须包括以下项目:题名(尽可能不要超过20字&…...
k8s 1.28 聚合层部署信息记录
–requestheader-client-ca-file –requestheader-allowed-namesfront-proxy-client –requestheader-extra-headers-prefixX-Remote-Extra- –requestheader-group-headersX-Remote-Group –requestheader-username-headersX-Remote-User –proxy-client-cert-file –proxy-cl…...
组件化设计的意义
鸿蒙操作系统(HarmonyOS)是华为公司开发的一款面向未来、面向全场景的分布式操作系统。它不仅能够支持多种智能终端设备,还能够实现跨设备之间的协同工作。为了满足不同设备的资源能力和业务需求,鸿蒙操作系统采用了组件化的设计方…...
2025 年河北省职业院校大数据应用与 服务(中职组)赛项样题
— 1 — 2025 年河北省职业院校大数据应用与 服务(中职组)赛项样题 一、背景描述 近年来,随着旅游业的快速发展和社交媒体的普及,一 些目的地因其独特的魅力或者事件而迅速走红,吸引了大量 游客涌入,使得当…...
宏集eXware物联网网关在水务管理系统上的应用
一、前言 水务管理系统涵盖了对城市水网、供水、排水、污水处理等多个环节的监控与管理。随着物联网(IoT)技术的快速发展,物联网网关逐渐成为水务管理系统中的关键组成部分。 宏集物联网网关以其高效的数据采集、传输和管理功能,…...
【看海的算法日记✨优选篇✨】第三回:二分之妙,寻径中道
🎬 个人主页:谁在夜里看海. 📖 个人专栏:《C系列》《Linux系列》《算法系列》 ⛰️ 一念既出,万山无阻 目录 📖一、算法思想 细节问题 📚左右临界 📚中点选择 📚…...
yolov5 解决:export GIT_PYTHON_REFRESH=quiet
当我们在第一次运行YOLOv5中的train.py程序时:可能会出现以下报错: This initial warning can be silenced or aggravated in the future by setting the $GIT_PYTHON_REFRESH environment variable. Use one of the following values: - quiet|q|silen…...
MongoDB聚合操作
1.聚合操作 聚合操作处理数据记录并返回计算结果。聚合操作组值来自多个文档,可以对分组数据执行各种操作以返回单个结果。聚合操作包含三类:单一作用聚合、聚合管道、MapReduce。 单一作用聚合:提供了对常见聚合过程的简单访问,…...
Apple雷电5到底有多快?
在科技日新月异的今天,苹果公司始终走在技术创新的前沿。2023年9月12日,随着英特尔发布雷电5(Thunderbolt 5)规范,苹果迅速跟进,将其应用于自家的产品中。雷电5接口以其卓越的性能,彻底颠覆了我…...
项目快过:知识蒸馏 | 目标检测 |FGD | Focal and Global Knowledge Distillation for Detectors
公开时间:2022年3月9号 项目地址:https://github.com/yzd-v/FGD 论文地址:https://arxiv.org/pdf/2111.11837 知识蒸馏已成功地应用于图像分类。然而,目标检测要复杂得多,大多数知识蒸馏方法都失败了。本文指出&#…...
Spring Boot日志总结
文章目录 1.我们的日志2.日志的作用3.使用日志对象打印日志4.日志框架介绍5.深入理解门面模式(外观模式)6.日志格式的说明7.日志级别7.1日志级别分类7.2配置文件添加日志级别 8.日志持久化9.日志文件的拆分9.1官方文档9.2IDEA演示文件分割 10.日志格式的配置11.更简单的日志输入…...
PostgreSQL最常用数据类型-重点说明自增主键处理
简介 PostgreSQL提供了非常丰富的数据类型,我们平常使用最多的基本就3类: 数字类型字符类型时间类型 这篇文章重点介绍这3中类型,因为对于高并发项目还是推荐:尽量使用简单类型,把运算和逻辑放在应用中,…...
androidstudio 最新继承 proto kts 方式
在Android Studio中,如果你使用的是Kotlin DSL(.kts文件)来配置你的Gradle项目,并且你想集成Protocol Buffers(Proto),你需要稍微调整你的配置方式。以下是如何在Kotlin DSL中配置Proto集成的步…...
【STM32学习】TB6612FNG驱动芯片的学习,驱动电路的学习
目录 1、TB6612电机驱动芯片 1.1如下是芯片的引脚图: 1.2如下图是电机的控制逻辑: 1.3MOS管运转逻辑 1.3典型应用电路 2、H桥驱动电路 2.1、单极模式 2.2、双极模式 2.3、高低端MOS管导通条件 2.4、H桥电路设计 2.5、自举电路 3、电气特性 3…...
【AI战略思考13】克服懒惰,保持专注,提升效率,不再焦虑
【AI论文解读】【AI知识点】【AI小项目】【AI战略思考】【AI日记】 引言 我发现自己最近非常懒惰,浪费了很多时间,也容易分心,不够专注,效率低下,且每天都有点焦虑,因此制定了下面的要求和作息时间表。 目…...
基于Vue3+Element Plus 实现多表单校验
使用场景 表单校验在日常的开发需求中是一种很常见的需求,通常在提交表单发起请求前校验用户输入是否符合规则,通常只需formRef.value.validate()即可校验,但是,例如一些多步骤表单、动态表单、以及不同的用户角色可能看到不同的表…...
“岗位复合化、技能层次化” 高职大数据技术专业人才培养实践
在全球数字化浪潮的推动下,大数据技术已经成为引领社会进步和经济发展的核心动力。随着《关于深化现代职业教育体系建设改革的意见》等系列指导问文件的发布,我国高职大数据技术专业的教育正迎来全新机遇与挑战。这些政策不仅明确了职业教育改革的方向&a…...
Day2 生信新手笔记: Linux基础
一、基础知识 1.1 服务器 super computer 或 server 1.2 组学数据分析 组学数据:如基因组学、转录组学、蛋白质组学等; 上游分析:主要涉及原始数据的获取和初步处理,计算量大,消耗的资源较多,在服务器完…...