了解并发
并发是指计算机可以同时处理多个任务。它涉及管理多个任务的执行,以提高软件应用程序的性能和响应能力。
🧵什么是线程
线程就像程序中的一个单独的执行路径。当程序运行时,它可以有多个线程并发运行,每个线程独立地执行自己的指令集。
💾线程缓存的问题
在下面的代码中,我们将Clock
类创建为Runnable
类,并通过从主类生成新的Thread
来运行它。一旦启动,Clock
类的run方法将继续运行while循环。为了阻止这种情况,我们需要从主线程调用cancel
方法,这将:
- 将
isStopped
值更改为false。 - 把它写在记忆里。
然而,运行while循环的线程可能没有更新的值isStopped
,并且可能仍然具有旧值isStopped
(false)的本地缓存。
java
public class Main {
public static void main(String[] args) throws InterruptedException {
Clock clock = new Clock();
Thread thread = new Thread(clock);
thread.start();
Thread.sleep(10000);
clock.cancel();
}
}
class Clock implements Runnable {
private volatile boolean isStopped = false;
public void cancel() {
this.isStopped = true;
}
public void run() {
while (!this.isStopped) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("Tick");
}
}
}
解决方案:Volatile关键字
volatile
变量的值永远不会被缓存,所有的写入和读取都将在主存中完成。因此,上面的thread
运行while
循环将始终具有来自内存的最新读取值。
竞争问题:竞争条件
让我们考虑这样一个场景:我们创建Counter
类,并尝试从两个单独的线程更新counter
值。
csharp
public class Main {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(counter);
Thread t2 = new Thread(counter);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("expect to be 2000 but value is : " + counter.getCounter());
}
}
public class Counter implements Runnable{
private int counter=0;
@Override
public void run() {
for(int i=0;i<1000;i++){
increament();
}
}
public void increament(){
int i =counter;
counter = i+1;
}
public int getCounter(){
return this.counter;
}
}
你能猜到输出吗?
kotlin
expect to be 2000 but value is: 1746
但为什么会这样呢?让我们来理解一下:
看看increment
方法:
- 首先,我们将计数器的值存储到局部变量
i
中。 - 然后,我们将
i
递增1。 - 最后,我们将
i
写回counter
。
两个线程同时执行此操作。
同步解决方案:同步关键字
我们可以使方法同步,这样每次只有一个线程可以运行该方法。其他线程必须等待轮到它们执行该方法。这导致了一致的数据。
我们使用synchronized
关键字定义一个要同步的方法。
csharp
public class Counter implements Runnable{
private int counter=0;
@Override
public void run() {
for(int i=0;i<1000;i++){
increament();
}
}
public synchronized void increament(){
int i =counter;
counter = i+1;
}
public int getCounter(){
return this.counter;
}
}
🧱密码块
如果两个方法处理两个不同的变量,我们可以使用synchronized代码块只锁定每个方法的关键部分,而不是使整个方法同步并锁定整个对象。这种方法允许我们确保没有两个线程可以同时访问同一个方法,从而提供更好的并发性。
java
public class ExampleClass {
private final Object lock1 = new Object();
private final Object lock2 = new Object();
private int value1 = 0;
private int value2 = 0;
public void incrementValue1() {
synchronized (lock1) {
value1++;
System.out.println("Value 1 incremented to: " + value1);
}
}
public void decrementValue2() {
synchronized (lock2) {
value2--;
System.out.println("Value 2 decremented to: " + value2);
}
}
}
例如,假设我们有incrementValue1()
和decrementValue2()
方法,每个方法修改单独的变量value1
和value2
。我们已经声明了两个私有的final对象lock1和lock2。这些对象用作同步块的锁,以确保访问相应变量时的线程安全。通过在每个方法中使用同步代码块,我们可以只锁定修改共享变量的部分,允许并发访问其他方法,同时保持线程安全。
🔐锁定
并发中的锁用于通过启用对共享资源的同步访问来实现线程安全。
- 内在锁
在Java中,每个对象都有一个内置的锁,称为内部锁。当一个线程想要访问一个共享对象时,它必须首先获得这个锁。这确保了一次只有一个线程可以访问对象。当您将一个方法声明为synchronized
时,调用该方法的线程将获取该方法对象的内部锁,并在该方法退出时释放该锁
例如,考虑一个Counter
类,其中每个线程递增一个共享计数器变量。在这个类中,increment()
方法被声明为synchronized,这意味着对于给定的Counter
对象,一次只有一个线程可以执行它。当一个线程调用increment()
方法时,它会自动获取与该方法的对象关联的内部锁,确保没有其他线程可以并发修改计数器。一旦方法完成执行,锁就会被释放,从而允许其他线程调用该方法。