在本文中,我们将分两部分介绍Java中的线程同步,以更好地理解Java的内存模型。抽丝剥茧 细说架构那些事——【优锐课】
介绍
Java线程同步和并发是复杂应用程序各个设计阶段中讨论最多的主题。线程,同步技术有很多方面,它们可以在应用程序中实现高并发性。多年来,CPU(多核处理器,寄存器,高速缓存存储器和主内存(RAM))的发展已导致通常是开发人员往往忽略的某些领域—例如线程上下文,上下文切换,变量可见性,JVM内存 型号与CPU内存型号。
在本系列中,我们将讨论Java内存模型的各个方面,包括它如何影响线程上下文,Java中实现并发性的同步技术,竞争条件等。在本文中,我们将重点介绍线程,同步的概念。技术以及Java和我们的CPU的内存模型。
概括
在深入研究线程和同步这一主题之前,让我们快速回顾一下一些与线程相关的术语和概念。
- 锁—锁是线程同步机制。
- Java中的每个对象都有一个与之关联的固有锁。线程使用对象的监视器进行锁定或解锁。锁可以视为逻辑上是内存中对象标头的一部分的数据。有关监视器无法实现的扩展功能,请参见ReentrantLock。
- Java中的每个对象都有同步方法
wait()
和notify()
[alsonotifyAll()
]。任何调用这些方法的线程都使用其监视器获得该对象的锁。必须使用synced关键字来调用此方法,否则将抛出IllegealMonitorStateException。 - 信号是一种通知线程应继续执行的方法。这是使用对象方法
wait()
,notify()
和notifyAll()
实现的。调用方法notify()
或notifyAll()
可以使线程单独唤醒后台(通过调用方法wait()
)。 - 信号丢失—方法
notify()
和notifyAll()
不保存方法调用,也不知道其他线程是否调用过wait()
。如果线程在要被信号通知的线程调用wait()
之前调用notify()
,则等待线程将丢失该信号。这可能导致线程无休止地等待,因为它错过了信号。 Runnable
是一个功能接口,可以由应用程序中的任何类实现,以便线程可以执行它。volatile
是分配给变量以使类成为线程安全的另一个关键字。要了解此关键字的用法,必须了解CPU体系结构和JVM内存模型。我们稍后再讨论。ThreadLocal
允许创建只能由所有者线程读取/写入的变量。 这用于使代码安全。- 线程池是线程的集合,线程将在其中执行任务。线程的创建和维护非常受服务控制。在Java中,线程池由ExecutorService的实例表示。
10. ThreadGroup
是该类提供一种用于将多个线程收集到单个对象中的机制,并允许我们一次操纵/控制那些线程。
11.
Daemon thread
—这些线程在后台运行。守护程序线程的一个很好的例子是Java Garbage Collector。JVM在退出以完成其执行之前不等待守护程序线程(而JVM在等待非守护程序线程或用户线程完成其执行之前)。
12. synchronized —当多个线程必须在并发模式下执行同一功能时,用于控制单个线程执行代码的关键字。此关键字可用于方法和代码块以实现线程安全。请注意,此关键字没有超时,因此有可能发生死锁情况。
13. Dead-lock—一种或多种线程正在等待另一线程释放对象锁的情况。导致死锁的可能情况是线程正在互相等待释放锁!
14. 虚假唤醒—出于无法解释的原因,即使未调用notify()和notifyAll(),线程也可能会唤醒。这是一个虚假的唤醒。 为了解决此问题,唤醒的线程围绕自旋锁中的条件自旋。
public synchronized doWait() { while(!wasSignalled) { // spin-lock check to avoid spurious wake up calls wait(); } // do something } public synchronized doNotify() { wasSignalled = true; notify(); }
线程饥饿死锁
当没有为某个线程分配CPU时间(因为其他线程占用了所有线程)时,就会发生线程饥饿。(例如,在某个对象上等待的线程(已调用wait()
)保持无限期等待,因为其他线程会不断唤醒(通过调用notify()
)。
为了缓解这种情况,我们可以使用Thread.setPriority(int priority)方法为线程设置优先级。优先级参数必须在Thread.MIN_PRIORITY到Thread.MAX_PRIORITY之间的设置范围内。检查官方线程文档以获取有关线程优先级的更多信息。
锁定界面与同步关键字
- 在同步块或方法中无法超时。这可能会在应用程序似乎挂起,死锁等情况下结束。同步块必须仅包含在单个方法中。
- Lock接口的实例可以在单独的方法中调用
lock()
和unlock()
。此外,锁也可以具有超时。与synced关键字相比,这是两个很大的好处。
以下是使用本机的wait()
和notify()
方法的自定义锁类的简单实现。请阅读下面的代码块中的注释,其中提供了更多关于wait()
和notify()
方法的信息。
class CustomLock { private boolean isLocked = false; public synchronized void lock() throws InterruptedException { isLocked = true; while(isLocked) { // calling thread releases the lock it holds on the monitor // object. Multiple threads can call wait() as the monitor is released. wait(); } } public synchronized void unlock() { isLocked = false; notify(); // only after the lock is released in this block, the wait() block // above can re-acquire the lock on this object's monitor. } }
线程执行
我们可以通过两种方式在Java中执行线程。他们是:
- 扩展Thread类并调用start()方法。(这不是从Thread子类化类的首选方式,因为它减少了添加该类更多功能的范围。)
- 实施
Runnable
或Callable
接口。这两个接口都是功能性接口,这意味着它们都只定义了一个抽象方法。(将来也可以通过实现其他接口来扩展作为类的首选方法。)
可运行的界面
这是用于通过线程执行特定任务的基本接口。此接口仅描述一种方法,称为run()
,返回类型为void
。如果必须在线程中执行任何功能,但不期望返回类型,请实现此接口。基本上,在失败的情况下,无法检索线程的结果或任何异常或错误。
通话界面
这是一个接口,除了获得执行结果之外,还用于通过线程执行特定任务。该接口遵循泛型。对于实现此接口的类,它仅描述一种称为call()
的方法,并描述了返回类型。如果必须在线程中执行任何功能并且必须捕获执行结果,请实现此接口。
同步技术
如上所述,可以使用synchronized
关键字或通过Lock的实例来同步线程。Lock接口的基本实现是ReentrantLock
类。同样,用于读/写操作的Lock接口也有所不同。
当线程试图读取或写入资源时,这有助于应用程序实现更高的并发性。此实现称为ReentrantReadWriteLock。这两个类之间的主要区别如下所示:
请参阅下面的ReentrantReadWriteLock
示例,以了解如何在仅允许一个线程更新资源的同时实现对资源的并发读取。
注意:资源可以是应用程序中各种线程尝试同时访问的任何数据。
public class ConcurrentReadWriteResourceExample { private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); private ReentrantReadWriteLock.ReadLock readLock = lock.readLock(); private ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); private void readResource() { readLock.lock(); // read the resource from a file, cache, database or from memory // this block can be accessed by 'N' threads concurrently for reading readLock.unlock(); } private void writeResource(String value) { writeLock.lock(); // write or update value to either a file, cache, database or from memory // this block can be accessed by at-most '1' thread at a time for writing writeLock.unlock(); } }
创建上述类的一个实例,并将其传递给多个线程;将处理以下内容:
readLock
由'N'个线程使用,或者writeLock
由最多一个线程使用。- 切勿同时进行读取或写入。
Java内存模型和CPU
有关Java和CPU内存模型的说明将帮助我们更好地了解对象和变量在Java堆/线程堆栈中的存储方式以及实际CPU内存的存储方式。现代的CPU由寄存器组成,这些寄存器充当处理器本身的直接存储器,高速缓存存储器—每个处理器都有一个高速缓存层来存储数据,最后是存在应用程序数据的RAM或主存储器。
在硬件或CPU上,线程堆栈和堆都位于主内存中。线程堆栈和堆的某些部分有时可能会出现在CPU缓存和内部寄存器中。以下是由于上述体系结构而可能发生的问题:
- 并非所有访问该变量的线程都会立即看到对共享变量的线程更新(写入)的可见性。
- 读取,检查和更新共享变量的数据时的竞争条件。
Volatile关键字
volatile
关键字是Java 5中引入的,在实现线程安全方面有重要的用途。此关键字可用于基元和对象。在变量上使用volatile关键字可确保给定的变量在更新后直接从主存储器读取并写回到主存储器。
ThreadLocal类别
锁同步之后的最后一个主题是Java类ThreadLocal
。此类可创建只能由同一线程读取/写入的变量。这为我们提供了一种通过定义线程局部变量来实现线程安全的简单方法。ThreadLocal
在线程池或ExecutorService
中具有重要用途,因此每个线程都使用自己的某些资源或对象的实例。
例如,对于每个线程,都需要一个单独的数据库连接,或者一个单独的计数器。在这种情况下,ThreadLocal
可以提供帮助。这在Spring
Boot应用程序中也使用,其中为每个传入呼叫设置了用户上下文(Spring
Security),并且将通过各种实例在线程流之间共享用户上下文。 在以下情况下,请使用ThreadLocal
:
- 线程限制。
- 每个线程的数据以提高性能。
- 每个线程上下文。
/** * This is a demo class only. The ThreadLocal snippet can be applied * to any number of threads and you can see that each thread gets it's * own instance of the ThreadLocal. This achieves thread safety. */ public class ThreadLocalDemo { public static void main(String...args) { ThreadLocal<String> threadLocal = new ThreadLocal<String>() { protected String initialValue() { return "Hello World!"; } }; // below line prints "Hello World!" System.out.println(threadLocal.get()); // below line sets new data into ThreadLocal instance threadLocal.set("Good bye!!!"); // below line prints "Good bye!!!" System.out.println(threadLocal.get()); // below line removes the previously set message threadLocal.remove(); // below line prints "Hello World!" as the initial value will be // applied again System.out.println(threadLocal.get()); } }
线程同步和相关概念就是这样。并发将在本文的第2部分中介绍。