Java平台旨在支持并发编程中的基本并发支持Java编程语言和Java类库。在编写新代码或与团队进行代码评审时,我们经常考虑并发性吗?一个小bug可能会导致对生产问题进行没完没了的调试应用程序,而这个问题并不容易在本地复制。
为什么我们需要并发编程?我们所处的时代是用配备多核CPU的机器工作的.您交付的代码应该优化以在这样的机器上运行,充分利用硬件。在设计并发系统时,不应干扰在CPU上运行的其他进程。我们希望我们的应用程序在不影响其他进程的情况下在它自己的黑匣子中运行。现在我们有了多核CPU,可以很容易地处理这个问题,但是在使用单核的时候是如何做到的呢?
在单核CPU上,CPU使用了一种常用的调度技术--时间片.CPU时间或处理时间被划分为称为片的小块。调度程序负责分配CPU时间。进程将从调度器获得分配的时间以完成任务。调度程序侦听从进程到下一个进程的状态/事件。这种从一个任务切换到另一个任务的速度非常快。用户体验不受影响,对他来说,一切都是同时运行的。有许多算法和技术可以用来处理调度。
作为一个在并发系统上工作的开发人员,我们如何确保我们设计出最优的解决方案?要回答这个问题,让我们先看看线程。
螺纹是一堆我们想以某种方式执行的指令。在您的代码中,当另一个线程从数据库中写入数组时,您正在等待打印一个数组。在这里,读取器将编写一个不完整的数组,或者是空的,这取决于此时数组中存在的内容,这称为种族条件.
让我们以单例类为例,看看线程是如何工作的,并尝试研究引擎盖下的争用条件。
public class SingletonExample {
private static SingletonExample instance;
private SingletonExample() {}
public static SingletonExample getInstance() {
if(instance == null){
instance = new SingletonExample();
}
return instance;
}
}
CPU时间 | 线1 | 线程2 |
---|---|---|
Slice1-0毫秒 | 检查SingletonExsample的实例是否为NULL | 等待 |
Slice2-1ms | 实例此时为空,因此它输入中频块 | 等待 |
Slice3-2ms | 线程调度程序暂停线程1 | 处于可运行状态 |
Slice4-3ms | 等待 | 检查SingletonExsample的实例是否为NULL |
Slice5-4毫秒 | 等待 | 实例此时为空,因此它进入if块。 |
Slice6-5毫秒 | 等待 | 创建Singleton示例的实例 |
Slice7-6毫秒 | 处于可运行状态 | 线程调度程序暂停线程2 |
Slice8-7ms | 因为它已经被检查为NULL,所以将创建 Singleton的一个新实例--举例 |
例如,有2个线程线程1和线程2,它们都在调用SingletonExmen类的getInstance()。上表描述了每个线程在时间片中执行的操作。
在Slice 7之前,我们没有看到任何问题,代码按预期执行。但是,在切片8中,调度程序返回到Thread1,它跳过空检查,因为这已经在片1中执行了。因此,我们创建了Singleton示例的另一个实例,删除了Thread 2创建的实例。这是一个种族状况, 并发编程中引入的一个常见错误。
为了解决前面遇到的种族问题,我们可以使用同步的概念。用同步方法声明--我们可以克服这个争用条件,让我们看看示例代码。
public class SingletonExample {
private static SingletonExample instance;
private SingletonExample() {}
public static synchronized SingletonExample getInstance() {
if(instance == null){
instance = new SingletonExample();
}
return instance;
}
}
同步GetInstance() ,我们需要保护该方法不被线程访问。Java中的每个对象都有用于同步的锁和键,如果Thread 1试图使用受保护的代码块,它将请求密钥。LOCK对象将检查密钥是否可用,如果密钥可用,则将密钥交给Thread 1并执行代码。
现在,线程2请求密钥,因为对象没有密钥(键被线程1拥有),线程2将等待直到它可用。通过这种方式,我们可以确保getInstance()不会进入竞争条件。
现在,关于持有键并将其交给线程的对象,这是由JVM完成的。JVM使用SingletonExmen类对象来保存密钥。如果在非静态方法上使用同步关键字,则键由我们所在类的实例持有。更好的解决方案是在方法中创建一个专用的同步块,并将键作为参数传递。
public class SingletonExample {
private final Object lock = new Object();
public getInstance(){
synchroznied(lock) {
// logic goes here
}
}
}
这种技术在单个方法上运行良好,但是如果我们有多个同步块,我们该如何处理呢?让我展示一下,如果您开始跨多个方法使用同步块,那么可能会出错。
public class DeadlockTest {
public static Object Lock_Thread1 = new Object();
public static Object Lock_Thread2 = new Object();
private static class Thread1 extends Thread {
public void run() {
synchronized (Lock_Thread1) {
System.out.println("Thread 1 - Holding lock_thread1");
try {
Thread.sleep(30);
} catch (InterruptedException e) {
}
System.out.println("Thread 1 - Waiting for lock_thread2");
synchronized (Lock_Thread2) {
System.out.println("Thread 1 - Holding lock 1 & 2");
}
}
}
}
private static class Thread2 extends Thread {
public void run() {
synchronized (Lock_Thread2) {
System.out.println("Thread 2 - Holding lock_thread2");
try {
Thread.sleep(30);
} catch (InterruptedException e) {
}
System.out.println("Thread 2 - Waiting for lock_thread1");
synchronized (Lock_Thread1) {
System.out.println("Thread 2 - Holding lock 1 & 2");
}
}
}
}
public static void main(String args[]) {
Thread1 T1 = new Thread1();
Thread2 T2 = new Thread2();
T1.start();
T2.start();
}
}
产出:
Output:
Thread 1 - Holding lock_thread1
Thread 2 - Holding lock_thread2
Thread 1 - Waiting for lock_thread2
Thread 2 - Waiting for lock_thread1
esc to exit...
在上面的示例中,我们可以看到,T1保存Lock_Thread 1,T2保存Lock_Thread 2。T2在等待LOCK_TRART 1,T1正在等待锁Lock_Thread 2。这种情况称为死锁。这两个线程都处于阻塞状态,两个线程都不会结束,因为每个线程都在等待另一个线程退出。
这种情况的出现是因为我们获得锁的顺序。如果我们更改Lock_Thread1和Lock_Thread2的顺序,那么运行线程将不再永远等待的相同程序。
public static Object Lock_Thread1 = new Object();
public static Object Lock_Thread2 = new Object();
private static class Thread1 extends Thread {
public void run() {
synchronized (Lock_Thread1) {
System.out.println("Thread 1 - Holding lock_thread1");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
}
System.out.println("Thread 1 - Waiting for lock_thread2");
synchronized (Lock_Thread2) {
System.out.println("Thread 1 - Holding lock 1 & 2");
}
}
}
}
private static class Thread2 extends Thread {
public void run() {
//This is changed to use lock for Thread1
synchronized (Lock_Thread1) {
System.out.println("Thread 2 - Holding lock_thread1");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
}
System.out.println("Thread 2 - Waiting for lock_thread1");
synchronized (Lock_Thread2) {
System.out.println("Thread 2 - Holding lock 1 & 2");
}
}
}
}
public static void main(String args[]) {
Thread1 T1 = new Thread1();
Thread2 T2 = new Thread2();
T1.start();
T2.start();
}
}
Output:
Thread 1 - Holding lock_thread1
Thread 1 - Waiting for lock_thread2
Thread 1 - Holding lock 1 & 2
Thread 2 - Holding lock_thread1
Thread 2 - Waiting for lock_thread1
Thread 2 - Holding lock 1 & 2
正如我们所看到的,通过确保您可以获得应用程序所需的资源锁的顺序,可以避免死锁的情况。一个好的设计是使用最小的锁,如果它已经被分配给另一个线程,就不会共享锁。