Java中的多线程编程牵涉到了并发访问,同时访问共享资源可能会造成数据竞争导致程序出现异常。为了解决这个问题,Java提供了两个主要的同步控制手段,即synchronized和ReentrantLock。然而,在使用这两种手段进行并发控制时也可能出现错误,下面就具体说明其出现的原因及如何解决。
Java synchronized的错误处理
问题引出
在Java中synchronized关键字是用来实现线程同步的重要手段,可以保证在同一时刻只有一个线程执行代码块,避免线程间的数据竞争。但是如果使用不当也会出现一些问题。
考虑下面一个例子,创建了一个共享变量counter,同时定义了两个线程A和B,都对counter进行累加操作,如下:
public class Counter {
private int counter = 0;
public synchronized void add() {
for (int i = 0; i < 10000; i++) {
counter++;
}
}
public int getCounter() {
return counter;
}
}
public class Main {
public static void main(String[] args) {
Counter counter = new Counter();
Thread thread1 = new Thread(counter::add);
Thread thread2 = new Thread(counter::add);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(counter.getCounter());
}
}
在该例子中,共享变量counter被定义为私有变量,并在add方法加上了synchronized修饰符,保证了在线程同步执行时只有一个线程能够访问add方法,进而保证数据的正确性。
问题解决
而令人惊奇的是,该程序运行结果并不是10000 * 2 = 20000,而是一个小于20000的整数,这是由于synchronized并不能保证原子性操作,在方法结束后仍然可能存在一些竞争条件。一个修正错误的办法是使用AtomicInteger类,该类提供了一种原子更新单个整数的方法,具体示例代码如下:
import java.util.concurrent.atomic.AtomicInteger;
public class Counter {
private AtomicInteger counter = new AtomicInteger(0);
public void add() {
for (int i = 0; i < 10000; i++) {
counter.incrementAndGet();
}
}
public int getCounter() {
return counter.get();
}
}
这里使用了AtomicInteger类来代替之前的int类型计数器。这样的话,每个增量操作都是原子的,线程在执行时只需等到其它线程执行完毕即可安全地执行操作。
ReentrantLock的错误处理
问题引出
与synchronized类似,ReentrantLock也提供了线程同步的重要手段,并且相较于synchronized有更为灵活的控制方式,但是当使用不当时也会出现一些问题。
考虑下面的例子,创建了一个共享变量counter,定义两个线程A和B,都对counter进行累加操作,如下:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private int counter = 0;
private final Lock lock = new ReentrantLock();
public void add() {
lock.lock();
try {
for (int i = 0; i < 10000; i++) {
counter++;
}
} finally {
lock.unlock();
}
}
public int getCounter() {
return counter;
}
}
public class Main {
public static void main(String[] args) {
Counter counter = new Counter();
Thread thread1 = new Thread(counter::add);
Thread thread2 = new Thread(counter::add);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(counter.getCounter());
}
}
在该例子中,共享变量counter被定义为私有变量,创建了ReentrantLock对象lock,在add方法中,首先使用lock.lock()获取锁,保证了在线程同步执行时只有一个线程能够访问add方法,以及锁机制的可重入性,其函数调用了两次lock()方法,但没有解锁,如果不加解锁,就意味着这段代码只有一个线程能够获得锁并执行,而另外一个线程将陷入了一直等待状态。这样的话,是非常危险的。
问题解决
正确的方式应该使用try-finally语句块来保证正常执行时try中的代码能够正确运行,并在try块结尾处正确地释放锁,避免死锁现象发生,示例代码如下:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private int counter = 0;
private final Lock lock = new ReentrantLock();
public void add() {
lock.lock();
try {
for (int i = 0; i < 10000; i++) {
counter++;
}
} finally {
lock.unlock();
}
}
public int getCounter() {
return counter;
}
}
总结
本文简单介绍了synchronized和ReentrantLock同步控制手段,及其使用时可能出现的问题,以及如何处理这些问题。正确使用同步控制手段是Java多线程编写的重要步骤,需要在编写过程中进行仔细的分析和实践。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:Java synchornized与ReentrantLock处理并发出现的错误 - Python技术站