网站导出链接查询,免费开源的cms,建设局主要负责什么,网站建设尾款催收函1.简介
Java 中的锁是用于控制多线程对共享资源访问的一种机制#xff0c;以防止数据的不一致性和脏读。Java 提供了多种锁机制#xff0c;包括内置的同步机制#xff08;synchronized#xff09;和在 java.util.concurrent.locks 包中提供的显式锁#xff08;如 Reentra…1.简介
Java 中的锁是用于控制多线程对共享资源访问的一种机制以防止数据的不一致性和脏读。Java 提供了多种锁机制包括内置的同步机制synchronized和在 java.util.concurrent.locks 包中提供的显式锁如 ReentrantLock等。
Synchronized synchronized 关键字是 Java 中最基本的同步机制。它可以用于修饰方法或代码块保证同一时间只有一个线程可以执行该方法或代码块内的代码。
方法同步将 synchronized 关键字加在方法上锁住的是调用该方法的对象。代码块同步通过 synchronized 关键字和一个锁对象来同步代码块锁住的是给定的对象。
ReentrantLock ReentrantLock 是 java.util.concurrent.locks 包中的一个类提供了比 synchronized 更灵活的锁定机制。它允许同一个线程多次获得锁并且支持公平锁和非公平锁。
公平锁按照线程请求锁的顺序来获取锁。非公平锁不按照请求锁的顺序来分配锁这可能导致某些线程永远获取不到锁。
使用 ReentrantLock 时需要显式地锁定和解锁
ReentrantLock lock new ReentrantLock();
lock.lock(); // 获取锁
try {// 访问或修改共享资源
} finally {lock.unlock(); // 释放锁
}ReadWriteLock ReadWriteLock 是一个读写锁它允许多个线程同时读取共享资源但只允许一个线程写入共享资源。这样可以提高并发性能特别是在读操作远远超过写操作的场景中。
ReadWriteLock 有两个锁一个读锁 (readLock()) 和一个写锁 (writeLock())。 Condition Condition 是与 ReentrantLock 配合使用的可以实现线程间的协调通信比如实现生产者-消费者模式。Condition 提供了类似 Object 的 wait、notify 和 notifyAll 方法。 StampedLock StampedLock 是 Java 8 引入的提供了一种乐观的读锁机制适用于读多写少的并发场景。它也支持读锁和写锁以及一种称为“乐观读”的锁模式。
总的来说Java 中的锁机制可以根据不同的应用场景和需求来选择以实现高效且安全的并发访问控制。
2.各种锁使用详情
2.1.Synchronized锁
Synchronized 关键字在 Java 中是实现同步的一种基本方式用于防止多个线程同时访问某个区域的代码。它可以应用于方法和代码块上根据应用的位置这两种方式有着不同的特点和用途。
2.1.1.Synchronized 方法锁
当你在方法声明上使用 synchronized 关键字时这个方法称为同步方法。对于实例方法锁定的是调用该方法的对象实例this对于静态同步方法锁定的是这个方法所在的Class对象本身因为静态方法是属于类的不属于任何对象实例。
优点
简单易用只需在方法前加上 synchronized 关键字。自动锁管理进入同步方法时自动加锁退出方法时自动解锁即使方法中途出现异常。
缺点
粒度较粗当一个线程访问同步方法时其他线程对同一对象中所有其他同步方法的访问将被阻塞可能会导致不必要的等待。灵活性较低无法对读操作和写操作采用不同的锁策略比如读写锁。
代码示例
class Counter {private int count 0;// 使用synchronized关键字修饰方法public synchronized void increment() {count; // 增加计数}public synchronized int getCount() {return count; // 返回当前计数}
}在这个例子中increment方法和getCount方法都被synchronized关键字修饰。这意味着在同一时刻只能有一个线程执行这些方法中的任何一个。如果有多个线程尝试同时访问这些方法它们将会排队直到前一个线程完成执行。
2.1.2.Synchronized 块锁
synchronized 也可以用来同步代码块而非整个方法。这时你需要指定一个锁对象。对于代码块同步可以选择任何对象作为锁对象监视器对象根据这个锁对象的不同同步代码块的作用范围也会有所不同。
优点
粒度更细可以减小锁的范围只在需要同步的代码区域上加锁提高了效率。灵活性高可以根据需要对不同的代码块使用不同的锁对象更加灵活地控制同步的范围和粒度。
缺点
使用复杂度较高需要手动指定锁对象且必须确保正确管理锁对象以避免死锁等问题。管理成本高相比于同步方法需要更仔细地考虑锁的选择和同步块的范围。
代码示例
class Counter {private int count 0;private final Object lock new Object();public void increment() {synchronized(lock) { // 在特定对象上加锁count; // 这部分代码是同步的}// lock释放后其他线程可以进入synchronized块}public int getCount() {synchronized(lock) {return count;}}
}在这个例子中我们只对修改count变量的部分代码进行同步。这是通过在一个特定的对象在这里是lock对象上使用synchronized代码块来实现的。这种方式提供了更细粒度的锁控制允许在同一个方法中的其他部分继续并发执行。
2.1.3.总结
使用 synchronized 方法时是在方法级别上加锁适用于简单的同步需求但可能导致较大范围的性能影响。使用 synchronized 块时可以更精确地控制同步的范围和锁对象提高程序的并发性能和灵活性但需要更谨慎地管理锁对象和同步块的范围。
选择哪种方式取决于具体的需求和场景。如果同步操作仅涉及方法内部的少数几行代码推荐使用 synchronized 块如果整个方法都需要同步使用 synchronized 方法更为简便。
2.2.ReentrantLock锁
ReentrantLock 是 Java 中 java.util.concurrent.locks 包提供的一种高级同步机制用于替代传统的同步方法和同步块synchronized。ReentrantLock 提供了更复杂的锁操作允许更灵活的线程互斥处理和更细粒度的锁控制。如其名所示“Reentrant” 意味着这种锁具有可重入的特性即同一个线程可以多次获得同一个锁而不会导致死锁。
2.2.1.特性
可重入性线程可以重复获取已经持有的锁这避免了死锁的发生。中断响应ReentrantLock 提供了一种能力允许在等待锁的过程中中断线程。尝试锁定提供了一个尝试获取锁的方法该方法无论锁是否可用都会立即返回获取结果。公平锁选项可以设置为公平锁确保等待时间最长的线程首先获得锁。锁绑定多个条件一个 ReentrantLock 可以同时绑定多个 Condition 实例。
2.2.2.使用方法
使用 ReentrantLock 需要显式地创建一个 ReentrantLock 实例然后在代码中显式地锁定和解锁。
ReentrantLock lock new ReentrantLock();public void method() {lock.lock(); // 获取锁try {// 执行临界区代码} finally {lock.unlock(); // 释放锁}
}2.2.3.比较 synchronized 和 ReentrantLock
灵活性ReentrantLock 提供了比 synchronized 更多的功能和灵活性例如尝试锁定、公平锁设置、以及处理中断等待的能力。性能在 Java 5 以后synchronized 的性能得到了显著提升使得两者的性能差异不再那么明显。性能差异主要取决于具体的JVM实现和应用场景。易用性synchronized 更易用因为它不需要显式地获取和释放锁而是自动管理。ReentrantLock 需要开发者手动管理锁的获取和释放增加了编程复杂度。功能丰富ReentrantLock 提供了一些 synchronized 无法提供的高级功能如条件变量Condition、可中断的锁获取等。
2.2.4.选择指南
当需要使用高级锁功能如尝试锁定、定时锁定、公平锁、或者多个条件变量等时应该使用 ReentrantLock。对于大多数基本的同步需求synchronized 是足够的它简单而且自动管理锁的获取和释放。
ReentrantLock 是一个强大的同步工具但它的使用应该根据具体场景和需求来决定考虑到额外的灵活性是否真正需要以及是否值得为此增加代码的复杂性。
2.2.5.特性代码
2.2.5.1.公平锁示例
公平锁是指线程获取锁的顺序与线程请求锁的顺序相同。
import java.util.concurrent.locks.ReentrantLock; public class FairLockExample { private final ReentrantLock lock new ReentrantLock(true); // 创建一个公平锁 public void printJob(Object document) { lock.lock(); try { // 模拟文档打印 System.out.println(Thread.currentThread().getName() : Printing a document); // 模拟打印时间 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public static void runDemo() { FairLockExample printer new FairLockExample(); Thread t1 new Thread(() - printer.printJob(new Object()), Thread 1); Thread t2 new Thread(() - printer.printJob(new Object()), Thread 2); Thread t3 new Thread(() - printer.printJob(new Object()), Thread 3); t1.start(); t2.start(); t3.start(); }
}2.2.5.2.尝试锁定tryLock示例
tryLock 方法尝试获取锁如果锁立即可用则获取锁并返回 true如果锁不可用则返回 false这允许程序在无法获取锁时不会无限期地等待。
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit; public class TryLockExample { private final ReentrantLock lock new ReentrantLock(); public void attemptLock() { try { // 尝试在1秒内获取锁 boolean lockAcquired lock.tryLock(1, TimeUnit.SECONDS); if (lockAcquired) { try { System.out.println(Thread.currentThread().getName() : Lock acquired, performing job.); // 模拟任务执行时间 Thread.sleep(2000); } finally { lock.unlock(); } } else { System.out.println(Thread.currentThread().getName() : Unable to acquire lock, doing something else.); } } catch (InterruptedException e) { e.printStackTrace(); } } public static void runDemo(){ TryLockExample example new TryLockExample(); Thread t1 new Thread(example::attemptLock, Thread 1); Thread t2 new Thread(example::attemptLock, Thread 2); t1.start(); t2.start(); }
}Thread 1: Lock acquired, performing job.
Thread 2: Unable to acquire lock, doing something else.2.2.5.3.结合条件变量Condition示例
条件变量允许一个或多个线程等待某些特定条件的发生并允许其他线程在条件发生时通知它们。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock; public class ConditionExample { private final ReentrantLock lock new ReentrantLock(); private final Condition condition lock.newCondition(); public void conditionWait() throws InterruptedException { lock.lock(); try { System.out.println(Thread.currentThread().getName() : Condition wait); condition.await(); // 等待 System.out.println(Thread.currentThread().getName() : Condition satisfied); } finally { lock.unlock(); } } public void conditionSignal() throws InterruptedException { lock.lock(); try { System.out.println(Thread.currentThread().getName() : Signal condition); condition.signal(); // 通知等待的线程 } finally { lock.unlock(); } } public static void runDemo() throws InterruptedException { ConditionExample example new ConditionExample(); Thread waiter new Thread(() - { try { example.conditionWait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }, Waiter Thread); Thread signaler new Thread(() - { try { // 确保waiter先执行等待 Thread.sleep(2000); example.conditionSignal(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }, Signaler Thread); waiter.start(); signaler.start(); }
}Waiter Thread: Condition wait
Signaler Thread: Signal condition
Waiter Thread: Condition satisfied2.2.5.4.公平锁复杂案例
公平锁选项可以设置为公平锁确保等待时间最长的线程首先获得锁。和 锁绑定多个条件一个 ReentrantLock 可以同时绑定多个 Condition 实例。
代码展示了如何在 ReentrantLock 中使用公平锁选项以及如何绑定多个 Condition 实例来实现一个简单的场景一个公平的生产者-消费者模型其中生产者和消费者线程根据等待时间的长短公平地获取锁同时利用两个条件实例分别控制生产和消费的逻辑。
package LockCode.ReentrantLocCode; import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock; public class FairProducerConsumerExample { private final QueueInteger queue new LinkedList(); private final int MAX_SIZE 5; private final ReentrantLock lock new ReentrantLock(true); // 使用公平锁 private final Condition notEmpty lock.newCondition(); private final Condition notFull lock.newCondition(); // 生产者方法 public void produce(int item) throws InterruptedException { lock.lock(); try { while (queue.size() MAX_SIZE) { System.out.println(Thread.currentThread().getName() waiting, queue full); notFull.await(); // 等待队列非满 } queue.add(item); System.out.println(Thread.currentThread().getName() produced item); notEmpty.signalAll(); // 通知消费者队列非空 } finally { lock.unlock(); } } // 消费者方法 public void consume() throws InterruptedException { lock.lock(); try { while (queue.isEmpty()) { System.out.println(Thread.currentThread().getName() waiting, queue empty); notEmpty.await(); // 等待队列非空 } int item queue.poll(); System.out.println(Thread.currentThread().getName() consumed item); notFull.signalAll(); // 通知生产者队列非满 } finally { lock.unlock(); } } public static void runDemo() { FairProducerConsumerExample example new FairProducerConsumerExample(); // 创建生产者线程 Thread producer new Thread(() - { try { for (int i 0; i 10; i) { example.produce(i); Thread.sleep(100); // 模拟生产时间 } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }, Producer); // 创建消费者线程 Thread consumer new Thread(() - { try { for (int i 0; i 10; i) { example.consume(); Thread.sleep(100); // 模拟消费时间 } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }, Consumer); producer.start(); consumer.start(); }
}2.2.5.4.1.代码解释
公平锁在创建 ReentrantLock 实例时通过传递 true 参数来启用公平锁。这确保了等待锁最长时间的线程将获得锁的优先权。多个条件变量使用两个 Condition 实例 notEmpty 和 notFull 来分别控制队列非空和非满的条件从而实现生产者和消费者之间的协调。生产者和消费者逻辑生产者在队列满时等待消费者在队列空时等待。当生产者生产一个元素后它会通过 notEmpty.signalAll() 唤醒等待的消费者同样消费者消费一个元素后通过 notFull.signalAll() 唤醒等待的生产者。公平的等待和唤醒由于使用了公平锁所以即使在多线程竞争激烈的情况下所有线程也将有机会按照请求锁的顺序获得锁。
2.2.5.4.2.好处
这种方式的好处是显而易见的
公平性确保长时间等待的线程不会被新到达的线程饿死。效率通过使用条件变量避免
2.3.公平锁和非公平锁
在并发编程中锁Lock的公平性是指当多个线程竞争锁时锁的分配机制是否考虑了线程等待的顺序。Java ReentrantLock 类在创建时可以指定锁是公平的Fair还是非公平的Nonfair。这个选择决定了等待相同锁的线程获得锁的顺序。
2.3.1.公平锁Fair Lock
公平锁意味着在分配锁时将考虑线程等待的顺序。具体来说如果锁是公平的那么在多个线程竞争锁时等待时间最长的线程将获得锁。这种方式保证了所有线程都将获得执行的机会避免了“饿死”某些线程永远获取不到锁的问题。
2.3.1.1.优点
公平性所有线程都有机会按顺序获得锁确保了长时间等待的线程不会被忽视。避免饥饿所有试图获取锁的线程最终都会按照请求的顺序获得锁没有线程会被无限期地阻塞。
2.3.1.2.缺点
性能公平锁通常会导致更大的延迟和更低的吞吐量。每次锁释放时都需要在等待的线程中找到等待时间最长的线程这可能会增加管理锁的开销。
2.3.2.非公平锁Nonfair Lock
非公平锁在分配锁时不考虑线程等待的顺序。如果锁是非公平的那么当锁变为可用时任何请求它的线程都有机会获得锁而不管其等待时间如何。这可能意味着某些线程可以立即重新获取锁而其他已经等待较长时间的线程则继续等待。
2.3.2.1.优点
性能非公平锁通常提供更好的性能。它减少了锁分配的管理开销因为不需要检查等待队列中的线程顺序只需判断锁是否可用即可尝试获取。吞吐量在高竞争的环境下非公平锁可能允许更多的线程在较短时间内完成工作从而提高吞吐量。
2.3.2.2.缺点
饥饿在极端情况下如果锁的请求非常高一些线程可能会遭遇饥饿即它们可能需要非常长的时间才能获得锁甚至可能永远得不到锁。不可预测性非公平锁的行为更不可预测难以确定哪个线程将获得锁。
2.3.3.总结
选择公平锁还是非公平锁取决于具体的应用场景和性能要求。如果对响应时间的公平性有严格要求或者想避免线程饥饿问题可以考虑使用公平锁。然而如果性能和吞吐量是主要关注点非公平锁可能是更好的选择。默认行为值得注意的是ReentrantLock 的默认行为是非公平的。如果需要公平性必须在创建锁时通过传递 true 给构造函数来明确请求公平锁。
2.3.4.代码样例说明
package LockCode.ReentrantLocCode; import java.util.concurrent.locks.ReentrantLock; public class FairAndNonfairLocks { public static void runDemo() throws InterruptedException { testLock(Fair Lock, true); Thread.sleep(1000); // 间隔1秒区分公平锁和非公平锁的输出 System.out.println(------------------------------------); testLock(Nonfair Lock, false); } private static void testLock(String testName, boolean isFair) { ReentrantLock lock new ReentrantLock(isFair); Runnable task () - { String threadName Thread.currentThread().getName(); System.out.println(threadName attempting to acquire testName); lock.lock(); try { System.out.println(threadName acquired testName); // 模拟任务执行时间 try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } finally { lock.unlock(); System.out.println(threadName released testName); } }; // 创建并启动5个线程 for (int i 1; i 5; i) { Thread thread new Thread(task, Thread i); thread.start(); } }
}Thread 1 attempting to acquire Fair Lock
Thread 2 attempting to acquire Fair Lock
Thread 1 acquired Fair Lock
Thread 3 attempting to acquire Fair Lock
Thread 4 attempting to acquire Fair Lock
Thread 5 attempting to acquire Fair Lock
Thread 1 released Fair Lock
Thread 2 acquired Fair Lock
Thread 2 released Fair Lock
Thread 3 acquired Fair Lock
Thread 4 acquired Fair Lock
Thread 3 released Fair Lock
Thread 4 released Fair Lock
Thread 5 acquired Fair Lock
Thread 5 released Fair Lock
------------------------------------
Thread 1 attempting to acquire Nonfair Lock
Thread 1 acquired Nonfair Lock
Thread 4 attempting to acquire Nonfair Lock
Thread 5 attempting to acquire Nonfair Lock
Thread 2 attempting to acquire Nonfair Lock
Thread 3 attempting to acquire Nonfair Lock
Thread 1 released Nonfair Lock
Thread 5 acquired Nonfair Lock
Thread 5 released Nonfair Lock
Thread 4 acquired Nonfair Lock
Thread 4 released Nonfair Lock
Thread 2 acquired Nonfair Lock
Thread 2 released Nonfair Lock
Thread 3 acquired Nonfair Lock
Thread 3 released Nonfair Lock2.3.4.1.代码解释
这个程序定义了一个名为 testLock 的方法它接收一个锁的名称和一个布尔值来决定使用公平锁还是非公平锁。ReentrantLock 对象根据 isFair 参数创建true 代表公平锁false 代表非公平锁。在 testLock 方法中创建了一个任务该任务尝试获取锁执行一段时间模拟通过 Thread.sleep(100)然后释放锁。对于每种锁类型我们启动5个线程来执行这个任务通过观察输出可以看到线程获取和释放锁的顺序。
2.3.4.2.预期观察
公平锁Fair Lock线程将按照请求锁的顺序获得锁。你应该能看到“Thread 1”、“Thread 2”依次获得锁显示锁分配的顺序性和公平性。非公平锁Nonfair Lock线程获取锁的顺序可能与它们请求锁的顺序不同。有可能看到后启动的线程比先启动的线程更早获取锁显示锁分配的非顺序性和非公平性。
2.3.4.3.注意事项
实际的运行结果可能因JVM调度、线程启动时间差异等因素而有所不同特别是在非公平锁的情况下。公平锁通常会有更高的开销因为维护线程顺序需要更多的管理和调度工作。在实际应用中选择使用公平锁还是非公平锁应根据具体需求和性能测试结果决定。
2.4.ReadWriteLock锁
ReadWriteLock 接口在 Java 中提供了一种高级的线程同步机制允许多个读操作并发进行而写操作则需要独占访问。这种锁是为了提高在读多写少的场景下的性能和并发性。ReadWriteLock 分为两部分读锁Read Lock和写锁Write Lock。这里我们重点介绍读锁。
2.4.1.读锁Read Lock
读锁是一种共享锁允许多个线程同时持有锁进行读操作。当一个线程持有读锁时其他请求读锁的线程也可以获取读锁并执行读操作但任何请求写锁的线程必须等待直到所有读锁都释放。这反映了一个基本原则只要没有线程在写入多个线程可以同时读取而不会引起冲突。
2.4.2.特性
共享访问多个线程可以同时持有读锁实现高效的并发读取。锁降级持有写锁的线程可以获取读锁然后释放写锁这是一种锁的降级策略用于保持数据的可见性。公平性选择ReentrantReadWriteLock 的实现提供了公平和非公平的选择。公平模式下线程将按照请求锁的顺序来获取锁而非公平模式则允许插队可能会提高性能但牺牲了公平性。
2.4.3.使用场景
读锁适用于数据读取远多于修改的场景例如缓存系统。在这些场景中读锁可以显著提高并发性能因为它允许多个线程同时读取数据而不阻塞彼此。
2.4.4.示例代码
下面是一个使用 ReadWriteLock 中读锁的简单示例
package LockCode.ReadLockCode; import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock; public class ReadLockExample { private final ReadWriteLock readWriteLock new ReentrantReadWriteLock(); private int value; // 使用读锁来保护读取操作 public int readValue() { readWriteLock.readLock().lock(); // 获取读锁 try { return value; } finally { readWriteLock.readLock().unlock(); // 释放读锁 } } // 使用写锁来保护写入操作 public void writeValue(int newValue) { readWriteLock.writeLock().lock(); // 获取写锁 try { this.value newValue; } finally { readWriteLock.writeLock().unlock(); // 释放写锁 } } public static void runDemo() { ReadLockExample example new ReadLockExample(); // 创建读线程 Thread readThread new Thread(() - { for (int i 0; i 5; i) { System.out.println(Read value: example.readValue()); } }); // 创建写线程 Thread writeThread new Thread(() - { for (int i 0; i 5; i) { example.writeValue(i); System.out.println(Write value: i); } }); readThread.start(); writeThread.start(); }
}Read value: 0
Write value: 0
Read value: 0
Write value: 1
Write value: 2
Write value: 3
Write value: 4
Read value: 1
Read value: 4
Read value: 42.4.5.注意事项
尽管多个线程可以同时持有读锁但如果有一个线程正在等待获取写锁则其他线程尝试获取读锁可能会被阻塞。这是为了避免写锁饥饿即确保请求写锁的线程最终能获取锁。锁的升级从读锁升级到写锁是不被允许的尝试这样做会导致死锁。
2.5.ReentrantReadWriteLock锁
下面的代码示例展示了ReentrantReadWriteLock的四个关键特性读读共享、写写互斥、读写互斥和锁降级。在这个示例中我们模拟了一个共享资源的访问以展示不同情况下锁的行为。
2.5.1.示例代码
package LockCode.ReadWriteLockCode; import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock; public class ReadWriteLockFeaturesDemo { private final ReadWriteLock rwLock new ReentrantReadWriteLock(); private int sharedResource 0; // 读操作展示读读共享 public void readOperation() { rwLock.readLock().lock(); try { System.out.println(Thread.currentThread().getName() starts reading.); Thread.sleep(1000); // 模拟读操作耗时 System.out.println(Thread.currentThread().getName() finished reading with value: sharedResource); } catch (InterruptedException e) { e.printStackTrace(); } finally { rwLock.readLock().unlock(); } } // 写操作展示写写互斥和读写互斥 public void writeOperation(int newValue) { rwLock.writeLock().lock(); try { System.out.println(Thread.currentThread().getName() starts writing.); Thread.sleep(1000); // 模拟写操作耗时 sharedResource newValue; System.out.println(Thread.currentThread().getName() finished writing.); } catch (InterruptedException e) { e.printStackTrace(); } finally { rwLock.writeLock().unlock(); } } // 锁降级从写锁降级到读锁 public void lockDowngrading(int newValue) { rwLock.writeLock().lock(); try { sharedResource newValue; System.out.println(Thread.currentThread().getName() updated value to: sharedResource); // 在释放写锁之前获取读锁 rwLock.readLock().lock(); } finally { rwLock.writeLock().unlock(); // 注意先释放写锁 } try { // 此时持有读锁可以安全地读取值 System.out.println(Thread.currentThread().getName() reads value after downgrading: sharedResource); } finally { rwLock.readLock().unlock(); } } public static void runDemo() throws InterruptedException { ReadWriteLockFeaturesDemo demo new ReadWriteLockFeaturesDemo(); // 启动两个读线程展示读读共享 new Thread(demo::readOperation, Reader1).start(); new Thread(demo::readOperation, Reader2).start(); Thread.sleep(100); // 确保读线程先启动 // 启动写线程展示写写互斥和读写互斥 new Thread(() - demo.writeOperation(100), Writer).start(); Thread.sleep(3000); // 等待上述操作完成 // 展示锁降级 new Thread(() - demo.lockDowngrading(200), Downgrader).start(); }
}Reader1 starts reading.
Reader2 starts reading.
Reader1 finished reading with value: 0
Reader2 finished reading with value: 0
Writer starts writing.
Writer finished writing.
Downgrader updated value to: 200
Downgrader reads value after downgrading: 2002.5.2.特性解释
读读共享readOperation 方法通过读锁展示了如果一个线程正在进行读操作其他线程也可以获得读锁进行读操作体现了共享性。写写互斥和读写互斥writeOperation 方法展示了写操作必须独占地进行无论是另一个写操作还是读操作都不能同时进行。锁降级lockDowngrading 方法展示了如何从写锁降级到读锁。线程首先获取写锁进行写操作然后在不释放写锁的情况下获取读锁接着释放写锁最后释放读锁。这样做可以保持对变量的读取权限即使写锁已经释放。
2.5.3.注意事项
锁的升级从读锁升级到写锁是不允许的尝试这样做会导致死锁。锁降级是一种高级特性通常用
2.6.StampedLock锁
StampedLock 是 Java 8 引入的一种新的锁机制位于 java.util.concurrent.locks 包中。与 ReentrantReadWriteLock 相比StampedLock 提供了更高的并发性。它是一种锁的实现支持读锁、写锁和乐观读。StampedLock 的主要特点是它的锁方法会返回一个表示锁状态的票据stamp这个票据在释放锁或检查锁是否有效时使用。
2.6.1.特性
读写锁支持传统的读写锁模式。乐观读一种非阻塞的读锁可以提高系统的吞吐量。乐观读锁在读取时不会阻塞写者但需要通过票据验证读取的数据是否在读取过程中被修改。不支持重入与 ReentrantLock 和 ReentrantReadWriteLock 不同StampedLock 不支持锁的重入。不支持条件变量StampedLock 不提供条件变量的支持不能使用 Condition 等待/通知模式。
2.6.2.使用场景
当数据的读取频率远高于修改时可以通过乐观读来提高并发性。在您可以容忍偶尔需要重新尝试读取的场景下使用乐观读以换取整体吞吐量的提升。对于更新操作较少读取操作非常频繁的数据结构。
2.6.3.示例代码
下面的代码示例展示了如何使用 StampedLock 实现一个简单的线程安全的点Point类包括使用写锁进行数据修改和使用乐观读取来提高读取操作的并发性。
import java.util.concurrent.locks.StampedLock;class Point {private double x, y;private final StampedLock sl new StampedLock();// 构造函数public Point(double x, double y) {this.x x;this.y y;}// 使用写锁修改点的坐标void move(double deltaX, double deltaY) {long stamp sl.writeLock(); // 获取写锁try {x deltaX;y deltaY;} finally {sl.unlockWrite(stamp); // 释放写锁}}// 使用乐观读锁读取点的坐标double distanceFromOrigin() {long stamp sl.tryOptimisticRead(); // 尝试获取乐观读锁double currentX x, currentY y;if (!sl.validate(stamp)) { // 检查在获取读锁后是否有其他写锁被获取stamp sl.readLock(); // 获取一个悲观读锁try {currentX x; // 重新读取变量currentY y;} finally {sl.unlockRead(stamp); // 释放读锁}}return Math.sqrt(currentX * currentX currentY * currentY);}
}public class StampedLockExample { public static void runDemo() {Point point new Point(1, 1);// 修改点的坐标point.move(2, 2);// 读取点的坐标System.out.println(point.distanceFromOrigin());}
}2.6.4.注意事项
使用 StampedLock 时特别需要注意乐观读锁的使用方法。乐观读可能需要在数据实际被读取后重新检查锁的有效性。StampedLock 不支持条件变量和锁重入这在某些情况下可能限制了它的使用场景。如果乐观读后发现锁已经不再有效就需要通过获取一个悲观读锁来保证数据的一致性。由于 StampedLock 不支持重入不当的锁管理可能导致死
2.7.LockSupport锁
LockSupport 是 Java 并发工具包 (java.util.concurrent.locks) 中的一个工具类提供了基本的线程阻塞和唤醒功能。它是创建锁和其他同步类的基础。与 Object 类的 wait() 和 notify() 方法相比LockSupport 提供的阻塞和唤醒操作更为灵活和简单因为它不要求线程持有任何锁。
主要方法
LockSupport 主要提供了以下几个静态方法
park(): 用于阻塞当前线程。如果调用 park() 时某个许可permit已经可用通过之前调用 unpark(Thread thread)那么 park() 会立即返回并且消耗掉这个许可否则它会使当前线程进入阻塞状态。unpark(Thread thread): 用于唤醒一个被 park() 阻塞的线程。它实际上是将一个许可permit提供给指定的线程如果这个线程已经被 park() 阻塞它将被唤醒如果还未被阻塞那么这个许可会被保存起来当这个线程将来调用 park() 时它将由于这个许可而立即返回。parkNanos(long nanos): 阻塞当前线程最多不超过给定的时间以纳秒为单位。parkUntil(long deadline): 阻塞当前线程直到某个时间以毫秒为单位的绝对时间。
许可Permit的概念
LockSupport 使用了一种名为“许可permit”的概念每个线程都有一个与之关联的许可虽然这些许可并没有实际的许可对象存在。许可的数量最多只有一个。调用 unpark 将使得线程的许可变为可用而调用 park 则会消耗掉这个许可使其不再可用如果许可已经不可用线程将阻塞。
示例代码
下面的示例演示了如何使用 LockSupport 来控制线程的阻塞与唤醒
import java.util.concurrent.locks.LockSupport; public class LockSupportExample { public static void runDemo() throws InterruptedException { Thread thread new Thread(() - { System.out.println(Thread.currentThread().getName() is parked.); // 阻塞当前线程 LockSupport.park(); // 当前线程被唤醒后执行 System.out.println(Thread.currentThread().getName() is unparked.); }); thread.start(); // 保证线程先执行 park Thread.sleep(1000); // 唤醒线程 System.out.println(main thread unparks thread.getName()); LockSupport.unpark(thread); }
}注意事项
LockSupport 不要求在 park 和 unpark 之间存在锁或同步块提供了比 Object 的 wait/notify 更加灵活的线程同步方式。park 方法可以无条件地返回因此通常需要循环检查条件来决定是否继续等待。相比于 Thread.suspend() 和 Thread.resume()LockSupport.park() 和 LockSupport.unpark(Thread thread) 不容易导致死锁因为许可的最大数量限制为一且 unpark 操作是安全的即使线程还未进入 park 状态。
LockSupport 类是实现高级同步机制的基础例如在 java.util.concurrent 包中的 Semaphore、ReentrantLock、
CountDownLatch等高级并发工具内部都有使用到LockSupport。
2.8.Volatile关键字
volatile 关键字在 Java 并发编程中扮演着重要的角色虽然它本身不是一种锁机制但它提供了变量的可见性和有序性保证从而成为实现轻量级同步的一种手段。使用 volatile 声明的变量可以确保所有线程都能看到共享变量的最新值。
2.8.1.可见性
在多线程环境中为了性能优化每个线程可能会把共享变量从主内存复制到线程的本地内存中。如果一个线程修改了这个变量的值而这个新值没有被及时写回主内存中那么其他线程可能就看不到这个修改。volatile 关键字可以保证被它修饰的变量不会被线程缓存所有的读写都直接操作主内存因此保证了变量修改的可见性。
2.8.2.有序性
在 Java 中编译器和处理器可能会对操作进行重排序以优化程序性能。但在某些情况下这种重排序可能会破坏多线程程序的正确性。volatile 变量具有“禁止指令重排序”语义对 volatile 变量的读写操作之前的所有操作都不会被重排序到读写操作之后。
2.8.3.不保证原子性
尽管 volatile 提供了变量操作的可见性和有序性保证但它并不保证操作的原子性。例如自增操作 count它包含读取 count、增加 count、写回 count 三个步骤就不是原子的即使 count 被声明为 volatile。
2.8.4.使用场景
状态标志volatile 常用于标志变量如线程运行状态标志。双重检查锁定Double-Check Locking用于实现单例模式时保证实例的唯一性和线程安全。一次性安全发布确保对象被安全地发布所有线程都能看到对象的初始化完成状态。
2.8.5.示例代码
使用 volatile 实现线程间的信号通信
public class VolatileExample {private static volatile boolean flag false;public static void main(String[] args) throws InterruptedException {new Thread(() - {while (!flag) { // 等待flag变为true// do something}System.out.println(Flag is true now!);}).start();Thread.sleep(1000); // 模拟主线程其他操作flag true; // 修改flag的值通知其他线程}
}2.8.6.注意事项
使用 volatile 时需要明确其目的是提供变量的可见性和有序性而非原子性。对于多个变量或复合操作的原子性要求应该使用锁如 synchronized 或 java.util.concurrent.locks 包中的锁或原子变量类如 AtomicInteger。
2.9.Semaphore信号量
Semaphore 是 Java 并发包 (java.util.concurrent) 中提供的一个同步工具类它用于控制对共享资源的访问数量。虽然 Semaphore 本质上不是锁但它可以用作一种特殊的锁机制用于限制对某段资源的同时访问数也就是说它可以限制同时执行一段代码的线程数。
2.9.1.工作原理
Semaphore 内部维护了一组许可permits许可的数量可以在 Semaphore 初始化时指定。线程在访问共享资源之前必须先从 Semaphore 获取许可如果 Semaphore 内的许可被其他线程全部取走那么尝试获取许可的线程将被阻塞直到其他线程释放许可或者被中断。
2.9.2.主要方法
Semaphore(int permits): 构造一个 Semaphore 实例初始化许可数量。void acquire(): 从 Semaphore 获取一个许可如果无可用许可则当前线程将被阻塞。void release(): 释放一个许可将其返回给 Semaphore。void acquire(int permits): 获取指定数量的许可。void release(int permits): 释放指定数量的许可。
2.9.3.使用场景
控制资源访问当资源有限时如数据库连接、IO通道等使用 Semaphore 可以限制资源的并发访问量。实现限流在高并发场景下限制任务的并发执行数防止过载。
2.9.4.示例代码
以下示例演示如何使用 Semaphore 控制对某个资源的并发访问
import java.util.concurrent.Semaphore;public class SemaphoreExample {// 假设只允许3个线程同时访问资源private static final Semaphore semaphore new Semaphore(3);static class Task implements Runnable {private String name;Task(String name) {this.name name;}Overridepublic void run() {try {// 获取许可semaphore.acquire();System.out.println(name is accessing the shared resource);Thread.sleep(1000); // 模拟资源访问需要的时间} catch (InterruptedException e) {e.printStackTrace();} finally {// 释放许可semaphore.release();System.out.println(name released the permit);}}}public static void main(String[] args) {// 创建并启动多个线程for (int i 0; i 10; i) {new Thread(new Task(Thread i)).start();}}
}2.9.5.注意事项
在使用 Semaphore 时需要确保每次成功获取许可的线程最终都会释放许可最好在 finally 代码块中释放许可以避免死锁。Semaphore 可以配置为公平fair模式通过构造函数 Semaphore(int permits, boolean fair) 来指定公平模式下线程获取许可的顺序将按照请求的顺序进行。
2.10.CyclicBarrier 和 CountDownLatch
CyclicBarrier 和 CountDownLatch 是 Java 并发包 java.util.concurrent 中提供的两种重要的同步辅助类。它们虽然不是锁但在多线程编程中被广泛用于协调线程间的同步或等待某些条件满足。
2.10.1.CountDownLatch
CountDownLatch 是一个同步辅助类用于让一个或多个线程等待一组事件的发生。它维护一个计数器该计数器在构造时被初始化只能设置一次每当一个事件发生时计数器的值就减一。调用 CountDownLatch 的 await() 方法的线程会阻塞直到计数器的值变为零这时所有等待的线程都会被释放继续执行。
2.10.1.1.使用场景
等待初始化完成在应用程序启动时等待必要的服务初始化完成。等待任务完成等待并行执行的任务全部完成。
2.10.1.2.示例代码
import java.util.concurrent.CountDownLatch;public class CountDownLatchExample {public static void main(String[] args) throws InterruptedException {int threads 3;CountDownLatch latch new CountDownLatch(threads);for (int i 0; i threads; i) {new Thread(() - {System.out.println(Thread.currentThread().getName() started.);// 模拟任务执行try {Thread.sleep(1000);} catch (InterruptedException e) {Thread.currentThread().interrupt();}latch.countDown(); // 完成任务计数器减一System.out.println(Thread.currentThread().getName() finished.);}).start();}latch.await(); // 等待所有线程完成System.out.println(All threads have finished.);}
}2.10.2.CyclicBarrier
CyclicBarrier 是另一个同步辅助类它允许一组线程相互等待直到所有线程都到达了一个共同的障碍点Barrier后才继续执行。与 CountDownLatch 不同CyclicBarrier 是可以重用的。
2.10.2.1.使用场景
同步任务在完成一组相互依赖的并行任务时确保所有任务在进行下一步之前都完成了当前步骤。
2.10.2.2.示例代码
import java.util.concurrent.CyclicBarrier;public class CyclicBarrierExample {public static void main(String[] args) {int threads 3;CyclicBarrier barrier new CyclicBarrier(threads, () - {// 当所有线程都到达barrier时执行System.out.println(All threads have reached the barrier.);});for (int i 0; i threads; i) {new Thread(() - {System.out.println(Thread.currentThread().getName() started.);try {Thread.sleep(1000);barrier.await(); // 等待其他线程System.out.println(Thread.currentThread().getName() crossed the barrier.);} catch (Exception e) {e.printStackTrace();}}).start();}}
}2.10.3.对比
初始化方式CountDownLatch 的计数器在对象创建时设置并且之后不能更改而 CyclicBarrier 的计数器可以通过重置来重用。用途CountDownLatch 适用于一个或多个线程等待其他线程完成操作的场景CyclicBarrier 适用于多个线程互相等待至某个公共点的场景。可重用性CyclicBarrier 可以重用而 CountDownLatch 不能。
3.具体细节整理汇总
3.1.死锁
死锁是指两个或多个线程在执行过程中因为互相等待对方持有的资源而无限期地阻塞的一种情况。简单来说就是每个线程持有一部分资源同时又等待其他线程释放它所需要的资源导致所有相关线程都进入等待状态无法继续执行。
死锁通常发生在以下四个条件同时满足时
互斥条件资源不能被多个线程同时占用。持有并等待条件一个线程至少持有一个资源并且正在等待获取额外的资源这些资源被其他线程持有。非抢占条件资源只能由持有它的线程自愿释放不能被强行抢占。循环等待条件存在一种线程资源的循环等待链每个线程都持有下一个线程所需要的资源。
解决死锁的方法通常包括预防、避免和检测与恢复
预防是指通过破坏死锁的四个必要条件中的至少一个来防止死锁的发生。避免涉及到系统的资源分配策略确保系统始终处于安全状态。检测与恢复是指系统定期检测死锁的存在并通过一些机制如资源抢占、线程重启来解除死锁。
3.2.竞态
竞态条件发生在两个或多个线程访问共享数据并且至少有一个线程对数据进行写操作时。如果没有适当的同步线程之间的交互可能导致数据的不一致性。简而言之竞态条件是由于多个线程以不可预知的顺序访问共享数据而导致的程序执行结果不正确的情况。
竞态条件的一个典型例子是检查后操作check-then-act即一个线程检查了某个条件如共享变量是否满足某个值然后在这个条件的基础上执行操作但在检查和操作之间另一个线程已经改变了这个条件。
解决竞态条件的主要方法是通过使用同步机制如锁、信号量等来确保对共享资源的访问是串行的或者设计无锁数据结构和算法来避免竞态条件。
总的来说死锁和竞态条件都是多线程编程中需要特别注意的问题合理地设计线程同步和资源分配策略是避免这些问题的关键。
3.3. Thread.currentThread().interrupt();
调用 Thread.currentThread().interrupt() 用于重新设置当前线程的中断状态。当线程中的一个阻塞操作如 Object.wait()、Thread.join() 或 Thread.sleep()因为中断interrupt被唤醒并抛出 InterruptedException 时该线程的中断状态会被清除即中断状态被重置为 false。如果你捕获到 InterruptedException通常意味着某个外部操作想要中断当前线程的执行。在捕获异常后调用 Thread.currentThread().interrupt() 可以再次设置中断状态这样做有几个目的
3.3.1. 保留中断请求
它保留了中断请求的信息。这对于那些可能不立即检查中断状态但稍后会检查它的代码路径来说是重要的。通过重新设置中断状态你确保后续的代码能够通过检查 Thread.interrupted() 或 Thread.isInterrupted() 来了解到当前线程已被请求中断。
3.3.2. 传递中断信号
在多层嵌套调用中最底层的方法可能会捕获到中断异常并不总是直接能够处理它比如它不知道如何适当地响应这个中断。通过重新设置中断状态它允许中断信号可以被传递给调用栈上的更高层方法这些方法可能更适合做出响应决定。
3.3.3. 支持清理操作和有序关闭
在捕获到 InterruptedException 后可能需要执行一些清理操作然后再结束线程。重新设置中断状态后你可以选择在完成必要的清理后再检查中断状态从而有序地关闭线程或者选择进一步的动作。
3.3.4.示例
public void run() {try {while (!Thread.currentThread().isInterrupted()) {// 线程正常执行任务Thread.sleep(1000); // 可能抛出 InterruptedException}} catch (InterruptedException e) {// 捕获异常重新设置中断状态允许线程退出或者在更高层处理中断Thread.currentThread().interrupt();}// 进行一些必要的清理操作
}在这个示例中如果 Thread.sleep() 抛出 InterruptedException我们捕获这个异常并重新设置中断状态然后线程可以继续到达清理代码或者通过检查中断状态来优雅地终止。
3.4.锁的重入
锁的重入或称为可重入锁Reentrant Lock是指同一个线程可以多次获得同一把锁。在多线程环境中如果一个线程已经持有了某个锁那么这个线程再次请求这个锁时可以再次获得这个锁而不会被锁阻塞。这种机制避免了一个线程在等待它自己持有的锁而产生死锁。
3.4.1.重入锁的特点
避免死锁允许线程再次获取已经持有的锁避免了线程等待自己持有的锁而产生死锁的情况。计数机制可重入锁通常通过计数机制来实现。当线程首次获取锁时计数器为1。每当这个线程再次获取到这把锁时计数器就增加1每当线程释放锁时计数器减1。当计数器回到0时锁被释放。保持公平性在实现时可以选择是否公平。公平的可重入锁意味着锁将按照线程请求的顺序来分配而非公平锁则允许“插队”。
3.4.2.使用场景
递归调用在递归函数中如果访问了同步资源线程可以再次获得已经持有的锁。扩展功能当一个已经获取锁的方法被另一个需要相同锁的方法调用时这种设计避免了死锁。调用链中的同步方法在调用链中一个同步方法直接或间接地调用另一个需要同一把锁的同步方法。
3.4.3.示例
以Java中的ReentrantLock为例演示可重入锁的基本使用
import java.util.concurrent.locks.ReentrantLock;public class ReentrantLockExample {private final ReentrantLock lock new ReentrantLock();public void outerMethod() {lock.lock();try {System.out.println(Lock acquired in outerMethod);innerMethod();} finally {lock.unlock();}}public void innerMethod() {lock.lock();try {System.out.println(Lock acquired in innerMethod);// perform some operations} finally {lock.unlock();}}public static void main(String[] args) {ReentrantLockExample example new ReentrantLockExample();example.outerMethod();}
}在这个例子中outerMethod 获取了锁然后调用了 innerMethod后者也尝试获取同一把锁。由于锁是可重入的所以 innerMethod 可以在不被阻塞的情况下获取锁并继续执行。如果锁不是可重入的那么 innerMethod 将会因为尝试获取已被自己持有的锁而被阻塞导致死锁。
3.4.5.结论
可重入锁是一种避免死锁的同步机制特别适用于递归调用、扩展功能和调用链中需要同步访问资源的场景。Java中的ReentrantLock和Synchronized关键字都实现了锁的重入性。