前言
我们在开发中有时需要处理多任务、多线程的场景,如果多个线程同时访问和修改共享数据,很容易出现线程不安全问题。从而导致意外或者错误行为。这个时候我们可以通过锁,确保线程的安全。通过下面例子希望你对锁有一定了解。
线程不安全的例子
下面创建了一个Counter对象,increment()执行递增操作。
public class Counter {
private int count = 0;
public void increment() {
this.count ++;
}
public int getCount() {
return this.count;
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
// 创建两个线程,同时对计数器进行递增操作
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter.increment();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("counter:" + counter.getCount());
}
}
上面我们可以看到创建了两个线程,每个线程执行increment方法一万次,最后输出counter的值。
我们期望最终值是20000,但是通常不会出现20000,而是出现一个低于20000的随机数。
为什么会出现线程不安全
原因:
- 读取count当前的值
- 将count的值进行加1,写回count
再多线程环境下,两个线程同时读取count的当前的值,分别进行加1操作,在将值写会。由于这两个线程的操作是并发执行的,可能导致其中一个线程的结果被覆盖。因此,最终的计数值会小于预期。
使用 synchronized 关键字可以解决
只需要在increment
方法中 加入synchronized
关键字,即可
// 使用 synchronized 确保线程安全
public synchronized void increment() {
count++;
}
这个时候不管怎么运行,都会最终数都是:20000
虽然 synchronized 是一种简单而有效的锁机制,但在某些场景下,我们可能需要更加灵活的锁控制。例如,synchronized 锁的释放是自动的,而有时候我们希望更明确地控制锁的获取和释放。这时候可以使用 Lock 接口及其实现类ReentrantLock。
使用ReentrantLock
使用ReentrantLock 来显式地控制锁的获取和释放。lock.lock() 方法用于获取锁,而 lock.unlock() 方法确保锁在操作完成后释放,确保线程间对 balance 的修改是安全的。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private int count = 0;
private final Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
this.count ++;
} finally {
lock.unlock();
}
}
public int getCount() {
return this.count;
}
}
运行结果和我们期望的一样都是 20000
死锁
两个或多个线程互相等待对方释放锁,导致它们永远无法继续执行
出现了死锁怎么办
每个方法循环两次:为更好的看到效果
制造死锁,两个线程分别锁定其中一个资源,然后试图得到另一个资源。这时如果两个线程互相等待释放资源,死锁就会发生。
在Counter类中,新增两个方法incrementWithDeadlock、anodtherIncrementWithDeadlock方法。
private final Lock lock1 = new ReentrantLock();
private final Lock lock2 = new ReentrantLock();
public void incrementWithDeadlock() {
System.out.println("incrementWithDeadlock方法");
lock1.lock();
System.out.println(Thread.currentThread().getName() + "\tincrement占用锁:lock1");
try {
// 模拟一些操作
Thread.sleep(50);
lock2.lock();
System.out.println(Thread.currentThread().getName() + "\tincrement占用锁:lock2");
try {
this.count++;
} finally {
lock2.unlock();
System.out.println(Thread.currentThread().getName() + "\tincrement释放锁: lock2");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock1.unlock();
System.out.println(Thread.currentThread().getName() + "\tincrement释放锁: lock1");
}
}
public void anotherIncrementWithDeadlock() {
System.out.println("anotherIncrementWithDeadlock方法");
lock2.lock();
System.out.println(Thread.currentThread().getName() + "\tanother占用锁:lock2");
try {
// 模拟一些操作
Thread.sleep(50);
lock1.lock();
System.out.println(Thread.currentThread().getName() + "another占用锁:lock1");
try {
this.count++;
} finally {
lock1.unlock();
System.out.println(Thread.currentThread().getName() + "another释放锁: lock1");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock2.unlock();
System.out.println(Thread.currentThread().getName() + "\tanother释放锁: lock2");
}
}
这个时候就会造成死锁:
解决死锁
1,规划好的获取锁的顺序去获取资源。
2,超时释放,当超时的时候就放弃当前操作,而不是无限期等待。
当前采用tryLock方法获取线程:
在方法中定义了boolean类型两个变量,用来是否获取到了进程。
public void incrementWithDeadlock() {
boolean locked1 = false;
boolean locked2 = false;
System.out.println("incrementWithDeadlock方法");
try {
locked1 = lock1.tryLock(100, TimeUnit.MILLISECONDS);
if (locked1) {
System.out.println(Thread.currentThread().getName() + "\tincrement占用锁:lock1");
// 模拟一些操作
try {
Thread.sleep(50);
locked2 = lock2.tryLock(100, TimeUnit.MILLISECONDS);
if (locked2) {
System.out.println(Thread.currentThread().getName() + "\tincrement占用锁:lock2");
count++;
lock2.unlock();
System.out.println(Thread.currentThread().getName() + "\tincrement释放锁: lock2");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock1.unlock();
System.out.println(Thread.currentThread().getName() + "\tincrement释放锁: lock1");
}
}
public void anotherIncrementWithDeadlock() {
boolean locked1 = false;
boolean locked2 = false;
System.out.println("anotherIncrementWithDeadlock方法");
try {
locked1 = lock2.tryLock(100, TimeUnit.MILLISECONDS);
if (locked1) {
System.out.println(Thread.currentThread().getName() + "\tanother占用锁:lock2");
try {
// 模拟一些操作
Thread.sleep(50);
locked2 = lock1.tryLock(100, TimeUnit.MILLISECONDS);
if (locked2) {
System.out.println(Thread.currentThread().getName() + "\t another占用锁: lock1");
this.count ++;
lock2.unlock();
System.out.println(Thread.currentThread().getName() + "\tanother释放锁:lock2");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock2.unlock();
System.out.println(Thread.currentThread().getName() + "\tanother释放锁: lock2");
}
}
可以看到虽然解决了死锁的问题,但是预期的结果和我们的不一样,我么期望的是4,但是最中是1,其实每次的运行的过程都不一样,导致结果也不一样,所以我们在开发的时候应该要避免死锁,而不是等死锁产生了,在去解决死锁。
总结
synchronized和lockReentrantLock都可以锁住资源,但是synchronized没有ReentrantLock灵。
避免死锁!!!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。