mysql 死锁监视器
本文是我们名为Java Concurrency Essentials的学院课程的一部分。
在本课程中,您将深入探讨并发的魔力。 将向您介绍并发和并发代码的基础知识,并学习诸如原子性,同步和线程安全性的概念。 在这里查看 !
目录
1.活泼
在开发使用并发实现目标的应用程序时,您可能会遇到不同线程可能相互阻塞的情况。 由于整个应用程序的运行速度比预期的慢,因此我们可以说应用程序无法按预期的时间完成。 在本节中,我们将仔细研究可能危害多线程应用程序正常运行的问题。
僵局
术语“死锁”对于软件开发人员来说是众所周知的,即使是大多数普通计算机用户也经常会使用“死锁”这个术语,尽管它并非总是以正确的含义使用。 严格说来,这意味着两个(或更多)线程分别在另一个线程上等待以释放其已锁定的资源,而线程本身已锁定另一个线程在等待的资源:
Thread 1: locks resource A, waits for resource B
Thread 2: locks resource B, waits for resource A
为了更好地理解该问题,让我们看一下以下源代码:
public class Deadlock implements Runnable {
private static final Object resource1 = new Object();
private static final Object resource2 = new Object();
private final Random random = new Random(System.currentTimeMillis());
public static void main(String[] args) {
Thread myThread1 = new Thread(new Deadlock(), "thread-1");
Thread myThread2 = new Thread(new Deadlock(), "thread-2");
myThread1.start();
myThread2.start();
}
public void run() {
for (int i = 0; i < 10000; i++) {
boolean b = random.nextBoolean();
if (b) {
System.out.println("[" + Thread.currentThread().getName() + "] Trying to lock resource 1.");
synchronized (resource1) {
System.out.println("[" + Thread.currentThread().getName() + "] Locked resource 1.");
System.out.println("[" + Thread.currentThread().getName() + "] Trying to lock resource 2.");
synchronized (resource2) {
System.out.println("[" + Thread.currentThread().getName() + "] Locked resource 2.");
}
}
} else {
System.out.println("[" + Thread.currentThread().getName() + "] Trying to lock resource 2.");
synchronized (resource2) {
System.out.println("[" + Thread.currentThread().getName() + "] Locked resource 2.");
System.out.println("[" + Thread.currentThread().getName() + "] Trying to lock resource 1.");
synchronized (resource1) {
System.out.println("[" + Thread.currentThread().getName() + "] Locked resource 1.");
}
}
}
}
}
}
从上面的代码可以看出,启动了两个线程并尝试锁定两个静态资源。 但是对于死锁,两个线程需要不同的顺序,因此我们利用Random实例选择线程首先要锁定的资源。 如果布尔变量b为true,则首先锁定resource1,然后线程尝试获取对资源2的锁定。如果b为false,则线程首先锁定resource2,然后尝试锁定resource1。 在我们到达第一个死锁之前,该程序不必运行很长时间,即,如果我们不终止它,该程序将永远挂起:
[thread-1] Trying to lock resource 1.
[thread-1] Locked resource 1.
[thread-1] Trying to lock resource 2.
[thread-1] Locked resource 2.
[thread-2] Trying to lock resource 1.
[thread-2] Locked resource 1.
[thread-1] Trying to lock resource 2.
[thread-1] Locked resource 2.
[thread-2] Trying to lock resource 2.
[thread-1] Trying to lock resource 1.
在此执行中,线程1持有资源2的锁,并等待对resource1的锁,而线程2持有资源1的锁,并等待resource2。
如果将上面示例代码中的布尔变量b设置为true,则不会遇到任何死锁,因为线程1和线程2请求锁的顺序始终相同。 因此,两个线程中的一个首先获取锁,然后请求第二个锁,因为其他线程都在等待第一个锁,所以第二个锁仍然可用。
通常,可以确定以下死锁要求:
- 互斥:有一种资源在任何时间点只能由一个线程访问。
- 资源持有:锁定一个资源后,线程尝试获取某个其他排他资源上的另一个锁定。
- 无抢占:没有机制,如果一个线程在特定时间段内持有锁,则该机制可以释放资源。
- 循环等待:在运行时发生一个星座,其中两个(或更多)线程分别在另一个线程上等待以释放已锁定的资源。
尽管要求清单看起来很长,但更高级的多线程应用程序存在死锁问题并不罕见。 但是,如果您能够放宽上述要求之一,则可以尝试避免死锁:
- 互斥:这是一项通常不能放宽的要求,因为必须专门使用资源。 但这并非总是如此。 使用DBMS系统时,可以使用一种称为Optimistic Locking的技术,而不是在必须更新的某些表行上使用悲观锁,这是一种可能的解决方案。
- 在等待另一个排他资源时避免资源持有的可能解决方案是在算法开始时锁定所有必要的资源,并在不可能获得所有锁定的情况下释放所有资源。 当然,这并非总是可能的,锁定的资源可能并不事先知道,或者就像浪费资源一样。
- 如果无法立即获得锁定,则避免超时的可能解决方案是引入超时。 例如,SDK类ReentrantLock提供了指定锁定超时的可能性。
- 从上面的示例代码可以看出,如果不同线程之间的锁定请求顺序没有不同,则不会出现死锁。 如果您能够将所有锁定代码放入所有线程都必须通过的一种方法中,则可以轻松地控制它。
在更高级的应用程序中,您甚至可以考虑实现死锁检测系统。 在这里,您将必须实现某种类型的线程监视,其中每个线程都报告已成功获取锁以及其尝试获取锁的尝试。 如果将线程和锁建模为有向图,则可以检测到两个不同的线程何时拥有资源,同时请求另一个阻塞的资源。 然后,如果您可以强制阻塞线程释放获得的资源,则可以自动解决死锁情况。
饥饿
调度程序决定下一步应该在状态RUNNABLE中执行的线程 。 该决定基于线程的优先级; 因此,具有较低优先级的线程比具有较高优先级的线程获得的CPU时间更少。 听起来很合理的功能在滥用时也会引起问题。 如果大多数时间都执行高优先级的线程,则低优先级的线程似乎“饿死了”,因为它们没有足够的时间正确执行其工作。 因此,建议仅在有充分理由的情况下设置线程的优先级。
线程匮乏的一个复杂示例是例如finalize()方法。 Java语言的此功能可用于在对象被垃圾回收之前执行代码。 但是,当您查看终结器线程的优先级时,您可能会发现它的运行优先级最高。 因此,如果与其他代码相比,对象的finalize()方法花费太多时间,则可能导致线程不足。
执行时间的另一个问题是问题,即未定义线程传递同步块的顺序。 当许多并行线程必须传递某些封装在同步块中的代码时,某些线程可能要比其他线程等待更长的时间,直到它们可以进入该块为止。 从理论上讲,它们可能永远不会进入障碍。
后一种问题的解决方案是所谓的“公平”锁定。 当选择下一个要传递的线程时,公平锁会考虑线程的等待时间。 Java SDK提供了一个公平锁的示例实现:java.util.concurrent.locks.ReentrantLock。 如果使用布尔标志设置为true的构造函数,则ReentrantLock授予对最长等待线程的访问权限。 这保证了没有饥饿,但是同时引入了以下问题:没有考虑线程优先级,因此可能会更频繁地执行经常在此屏障处等待的优先级较低的线程。 最后但并非最不重要的一点是,ReentrantLock类当然只能考虑正在等待锁的线程,即,执行频率足以达到锁的线程。 如果线程优先级太低,则可能不会经常发生这种情况,因此,具有更高优先级的线程仍会更频繁地通过锁。
2.使用wait()和notify()进行对象监控
多线程计算中的一个常见任务是让一些工作线程正在等待其生产者为其创建工作。 但是,据我们了解,就CPU时间而言,在循环中忙于等待并检查某些值并不是一个好的选择。 在此用例中,Thread.sleep()方法也没有太大价值,因为我们希望在提交后立即开始工作。
因此,Java编程语言具有另一种可在这种情况下使用的构造:wait()和notify()。 每个对象都从java.lang.Object类继承的wait()方法可用于暂停当前线程执行,并等待直到另一个线程使用notify()方法将我们唤醒。 为了正常工作,调用wait()方法的线程必须持有它已获得的锁,然后才能使用synced关键字。 当调用wait()时,锁被释放,线程等待直到拥有该锁的另一个线程在同一对象实例上调用notify()。
在多线程应用程序中,当然可能有多个线程在等待某个对象的通知。 因此,有两种不同的唤醒线程的方法:notify()和notifyAll()。 第一种方法仅唤醒其中一个等待线程,而notifyAll()方法将其全部唤醒。 但是请注意,与synced关键字类似,没有规则指定在调用notify()时接下来唤醒哪个线程。 在简单的生产者和消费者示例中,这无关紧要,因为我们对哪个线程完全唤醒的事实不感兴趣。
下面的代码演示了如何使用wait()和notify()机制来使使用者线程等待从某个生产者线程推送到队列中的新工作:
package a2;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
public class ConsumerProducer {
private static final Queue queue = new ConcurrentLinkedQueue();
private static final long startMillis = System.currentTimeMillis();
public static class Consumer implements Runnable {
public void run() {
while (System.currentTimeMillis() < (startMillis + 10000)) {
synchronized (queue) {
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (!queue.isEmpty()) {
Integer integer = queue.poll();
System.out.println("[" + Thread.currentThread().getName() + "]: " + integer);
}
}
}
}
public static class Producer implements Runnable {
public void run() {
int i = 0;
while (System.currentTimeMillis() < (startMillis + 10000)) {
queue.add(i++);
synchronized (queue) {
queue.notify();
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized (queue) {
queue.notifyAll();
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread[] consumerThreads = new Thread[5];
for (int i = 0; i < consumerThreads.length; i++) {
consumerThreads[i] = new Thread(new Consumer(), "consumer-" + i);
consumerThreads[i].start();
}
Thread producerThread = new Thread(new Producer(), "producer");
producerThread.start();
for (int i = 0; i < consumerThreads.length; i++) {
consumerThreads[i].join();
}
producerThread.join();
}
}
main()方法启动五个使用者线程和一个生产者线程,然后等待它们完成。 然后,生产者线程将新值插入队列,然后通知所有等待线程发生了某些事情。 使用者线程获取队列锁,然后进入睡眠状态,以便稍后再次填充队列时被唤醒。 生产者线程完成工作后,会通知所有消费者线程唤醒。 如果我们不做最后一步,那么消费者线程将永远等待下一个通知,因为我们没有为等待指定任何超时。 取而代之的是,我们至少可以在经过一定时间后使用wait(long timeout)方法来唤醒它。
带wait()和notify()的嵌套同步块
如上一节所述,在对象监视器上调用wait()仅释放该对象监视器上的锁。 由同一线程持有的其他锁不会被释放。 因为这很容易理解,所以在日常工作中,调用wait()的线程可能会进一步锁定。 而且,如果其他线程也在等待这些锁,则会发生死锁情况。 让我们看下面的示例代码:
public class SynchronizedAndWait {
private static final Queue queue = new ConcurrentLinkedQueue();
public synchronized Integer getNextInt() {
Integer retVal = null;
while (retVal == null) {
synchronized (queue) {
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
retVal = queue.poll();
}
}
return retVal;
}
public synchronized void putInt(Integer value) {
synchronized (queue) {
queue.add(value);
queue.notify();
}
}
public static void main(String[] args) throws InterruptedException {
final SynchronizedAndWait queue = new SynchronizedAndWait();
Thread thread1 = new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 10; i++) {
queue.putInt(i);
}
}
});
Thread thread2 = new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 10; i++) {
Integer nextInt = queue.getNextInt();
System.out.println("Next int: " + nextInt);
}
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
}
}
正如我们之前所了解的 ,将同步添加到方法签名等于创建一个synced(this){}块。 在上面的示例中,我们意外地向该方法添加了synced关键字,然后在对象监视器队列上进行了同步,以便在等待队列中的下一个值时将当前线程置于睡眠状态。 然后,当前线程释放队列上的锁保持,但不释放对此的锁保持。 putInt()方法通知睡眠线程已添加新值。 但是,偶然地,我们还向该方法添加了关键字sync。 现在,当第二个线程进入睡眠状态时,它仍然持有该锁。 然后,第一个线程无法进入方法putInt(),因为此锁由第一个线程持有。 因此,我们陷入僵局,程序挂起。 如果执行上面的代码,则在程序开始后立即发生。
在日常生活中,情况可能不像上面那样清楚。 线程持有的锁可能取决于运行时参数和条件,而导致问题的同步块可能与代码中我们放置wait()调用的位置不太接近。 这使得很难找到此类问题,并且可能是这些问题仅在一段时间后或在高负载下才会出现。
同步块中的条件
在对同步对象执行某些操作之前,通常您必须检查是否满足某些条件。 例如,当您有一个队列时,您要等待直到该队列被填满。 因此,您可以编写一种检查队列是否已满的方法。 如果没有,则在唤醒当前线程之前使其处于睡眠状态:
public Integer getNextInt() {
Integer retVal = null;
synchronized (queue) {
try {
while (queue.isEmpty()) {
queue.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized (queue) {
retVal = queue.poll();
if (retVal == null) {
System.err.println("retVal is null");
throw new IllegalStateException();
}
}
return retVal;
}
上面的代码在调用wait()之前在队列上进行同步,然后在while循环内等待,直到队列中至少有一个条目。 第二个同步块再次将队列用作对象监视器。 它轮询()队列中的内部值。 为了演示起见,当poll()返回null时,抛出IllegalStateException。 当队列中没有要轮询的值时,就是这种情况。
运行此示例时,您将看到IllegalStateException很快就会抛出。 尽管我们已经在队列监视器上正确地同步了,但是会抛出异常。 原因是我们有两个单独的同步块。 假设我们有两个线程到达了第一个同步块。 第一个线程进入该块并由于队列为空而进入睡眠状态。 第二个线程也是如此。 现在,当两个线程都唤醒时(通过另一个在监视器上调用notifyAll()的线程),它们都在队列中看到了一个值(生产者添加的值。然后,两个线程到达第二个屏障。轮询队列中的值,当第二个线程进入时,队列已为空,因此它从poll()调用返回的值作为null并引发异常。
为避免出现上述情况,您将必须在同一同步块中执行所有取决于监视器状态的操作:
public Integer getNextInt() {
Integer retVal = null;
synchronized (queue) {
try {
while (queue.isEmpty()) {
queue.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
retVal = queue.poll();
}
return retVal;
}
在这里,我们在与isEmpty()方法相同的同步块中执行方法poll()。 通过同步块,我们可以确保在给定的时间点上只有一个线程正在此监视器上执行方法。 因此,没有其他线程可以从isEmpty()和poll()调用之间的队列中删除元素。
3.多线程设计
正如我们在上一节中所看到的,实现多线程应用程序有时比乍一看要复杂。 因此,在启动项目时务必牢记清晰的设计。
不变的对象
在这种情况下,非常重要的一种设计规则是不变性。 如果在不同线程之间共享对象实例,则必须注意两个线程不会同时修改同一对象。 但是在无法更改的情况下,不可修改的对象很容易处理。 要修改数据时,始终必须构造一个新实例。 基本类java.lang.String是不可变类的示例。 每次您要更改字符串时,都会得到一个新实例:
String str = "abc";
String substr = str.substring(1);
尽管创建对象的操作并非没有成本,但是这些成本经常被高估。 但是,如果具有不可变对象的简单设计胜过不使用不可变对象,则总要权衡一下,因为存在存在并发错误的风险,而该错误可能会在项目后期出现。
在下面的内容中,您将找到一组要使类不可变的规则:
- 所有字段均应为最终字段和私有字段。
- 不应使用setter方法。
- 为了防止子类违反不变性原则,应将类本身声明为final。
- 如果字段不是原始类型,而是对另一个对象的引用:
- 不应有将引用直接暴露给调用方的getter方法。
下列类的实例表示一条消息,其中包含主题,消息正文和一些键/值对:
public final class ImmutableMessage {
private final String subject;
private final String message;
private final Map<String,String> header;
public ImmutableMessage(Map<String,String> header, String subject, String message) {
this.header = new HashMap<String,String>(header);
this.subject = subject;
this.message = message;
}
public String getSubject() {
return subject;
}
public String getMessage() {
return message;
}
public String getHeader(String key) {
return this.header.get(key);
}
public Map<String,String> getHeaders() {
return Collections.unmodifiableMap(this.header);
}
}
该类是不可变的,因为它的所有字段都是final和private。 在构造实例之后,没有任何方法可以修改实例的状态。 返回对主题和消息的引用是安全的,因为String本身是一个不变的类。 例如,获得消息引用的呼叫者无法直接对其进行修改。 对于标题映射,我们必须更加注意。 只要返回对Map的引用,调用者就可以更改其内容。 因此,我们必须返回通过调用Collections.unmodifiableMap()获得的不可修改的Map。 这将返回Map上的一个视图,该视图允许调用者读取值(再次为字符串),但不允许修改。 尝试修改Map实例时,将引发UnsupportedOperationException。 在此示例中,返回特定键的值也是安全的,就像在getHeader(String key)中完成操作一样,因为返回的String再次是不可变的。 如果Map包含本身不可变的对象,则此操作将不是线程安全的。
API设计
在设计类的公共方法(即此类的API)时,您也可以尝试将其设计用于多线程使用。 当对象处于特定状态时,您可能有不应执行的方法。 克服这种情况的一种简单解决方案是拥有一个私有标志,该标志指示我们处于哪种状态,并在不应该调用特定方法的情况下抛出例如IllegalStateException:
public class Job {
private boolean running = false;
private final String filename;
public Job(String filename) {
this.filename = filename;
}
public synchronized void start() {
if(running) {
throw new IllegalStateException("...");
}
...
}
public synchronized List getResults() {
if(!running) {
throw new IllegalStateException("...");
}
...
}
}
上面的模式通常也被称为“禁止模式”,因为该方法一旦在错误的状态下执行便会失败。 但是您可以使用静态工厂方法设计相同的功能,而无需在每个方法中检查对象的状态:
public class Job {
private final String filename;
private Job(String filename) {
this.filename = filename;
}
public static Job createAndStart(String filename) {
Job job = new Job(filename);
job.start();
return job;
}
private void start() {
...
}
public synchronized List getResults() {
...
}
}
静态工厂方法使用私有构造函数创建Job的新实例,并已在实例上调用start()。 返回的Job引用已经处于可以使用的正确状态,因此getResults()方法仅需要同步,而不必检查对象的状态。
线程本地存储
到目前为止,我们已经看到线程共享相同的内存。 就性能而言,这是在线程之间共享数据的好方法。 如果我们将使用单独的进程来并行执行代码,那么我们将拥有更繁重的数据交换方法,例如远程过程调用或文件系统或网络级别的同步。 但是,如果同步不正确,则在不同线程之间共享内存也将难以处理。
Java中通过java.lang.ThreadLocal类提供了仅由我们自己的线程而不是其他线程使用的专用内存:
private static final ThreadLocal myThreadLocalInteger = new ThreadLocal();
通用模板参数T给出了应存储在ThreadLocal中的数据类型。在上面的示例中,我们仅使用了Integer,但在这里我们也可以使用任何其他数据类型。 以下代码演示了ThreadLocal的用法:
public class ThreadLocalExample implements Runnable {
private static final ThreadLocal threadLocal = new ThreadLocal();
private final int value;
public ThreadLocalExample(int value) {
this.value = value;
}
@Override
public void run() {
threadLocal.set(value);
Integer integer = threadLocal.get();
System.out.println("[" + Thread.currentThread().getName() + "]: " + integer);
}
public static void main(String[] args) throws InterruptedException {
Thread threads[] = new Thread[5];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(new ThreadLocalExample(i), "thread-" + i);
threads[i].start();
}
for (int i = 0; i < threads.length; i++) {
threads[i].join();
}
}
}
您可能想知道,尽管变量threadLocal被声明为静态的,但每个线程输出的正是它通过构造函数获得的值。 ThreadLocal的内部实现确保每次调用set()时,给定值都存储在仅当前线程有权访问的内存区域中。 因此,当您事后调用get()时,尽管存在其他线程可能已调用set()的事实,但您仍会检索之前设置的值。
Java EE世界中的应用程序服务器大量使用ThreadLocal功能,因为您有许多并行线程,但是每个线程都有自己的事务或安全上下文。 由于您不想在每次方法调用中传递这些对象,因此只需将其存储在线程自己的内存中,并在以后需要时访问它。
翻译自: https://www.javacodegeeks.com/2015/09/concurrency-fundamentals-deadlocks-and-object-monitors.html
mysql 死锁监视器