提到线程安全想必大家并不陌生,但是为什么真正项目中遇到多线程问题的时候却很棘手? 很大一部分原因是我们只学理论走马观花看了几篇博客背了几个面试点就以为自己掌握了,Talk is Cheap, Show me the code! 因此这篇文章我会结合具体的代码介绍几种线程安全的方法和注意事项,希望能给你一个清晰地印象。
我们以火车售票场景为例,假如有两个窗口同时卖票,代码如下
public class Test {
static class SellTickets implements Runnable {
private int tickets = 10;
@Override
public void run() {
while (tickets > 0) {
String name = Thread.currentThread().getName();
System.out.println(name + "售票成功! 还剩 " + (--tickets) + " 张票~");
delay1s();
}
}
private void delay1s() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
SellTickets sellTickets = new SellTickets();
Thread one = new Thread(sellTickets, "1号窗口");
Thread two = new Thread(sellTickets, "2号窗口");
one.start();
two.start();
}
}
这段代码没有做线程安全的处理, 我们看下运行结果 (如果你运行的结果没有异常可以多试几次)
1号窗口售票成功! 还剩 9 张票~
2号窗口售票成功! 还剩 8 张票~
2号窗口售票成功! 还剩 7 张票~
1号窗口售票成功! 还剩 7 张票~ // error 数据错误 只剩6张票但是说剩7张
1号窗口售票成功! 还剩 6 张票~
2号窗口售票成功! 还剩 6 张票~ // error 数据错误
2号窗口售票成功! 还剩 4 张票~
1号窗口售票成功! 还剩 5 张票~
2号窗口售票成功! 还剩 3 张票~
1号窗口售票成功! 还剩 2 张票~
2号窗口售票成功! 还剩 1 张票~
1号窗口售票成功! 还剩 0 张票~
这就是多线程带来的数据不一致问题: A线程对共享变量的修改没有同步到B线程,B线程读取到错误的变量值
synchronized (this){
System.out.println(name + "售票成功! 还剩 " + (--tickets) + " 张票~");
}
synchronized 可用于修饰普通方法、静态方法和代码块
class A {
// 同步方法
public synchronized void method1() {
}
// 非同步方法
public void method2() {
}
}
当 synchronized 修饰普通方法时,被修饰的方法被称为同步方法,其作用范围是整个方法,作用的对象是调用这个方法的对象。
class B {
public static synchronized void method1() {
}
public static void method2() {
}
}
当 synchronized 修饰静态方法时,其作用范围是整个程序,这个锁对于所有调用这个锁的对象都是互斥的。
class C {
public static void method2() {
// 加锁某个类
synchronized (SynchronizedUsage.class) {
// ......
}
// 加锁当前类对象
synchronized (this) {
// ......
}
// 加锁某个对象,如果害怕多线程修改这个对象可以锁住这个对象
synchronized (object) {
object = xxxx
}
}
}
代码块加锁这种方式可以放两种元素: 对象 和 class, 对象的话作用和普通方法类似,class的话作用和静态方法类似。
ReentrantLock 是 Java 提供的一个可重入锁,它和 synchronized 类似,但是更加灵活,可以支持多个条件变量和公平锁等特性
需要注意的是: lock 和 unlock需要配合使用,如果不调用unlock会造成死锁,一般会结合try finally使用
private int tickets = 10;
ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (tickets > 0) {
String name = Thread.currentThread().getName();
try {
lock.lock();
System.out.println(name + "售票成功! 还剩 " + (--tickets) + " 张票~");
} finally {
lock.unlock();
}
delay1s();
}
}
虽然也有一些其他的方法实现线程安全,但是这两个方法应该可以解决99%以上的问题,大家可以看完自己敲一下理解一下。