Java并发之线程安全
线程不安全
首先我们来看一段代码:
public class SynchronizedDemo implements Runnable {
private static int data = 0;
@Override
public void run() {
for (int i = 0; i < 10; i++) {
data++;
}
}
public static void main(String[] args) throws InterruptedException {
SynchronizedDemo demo = new SynchronizedDemo();
for (int i = 0; i < 1000; i++) {
new Thread(demo).start();
}
Thread.sleep(5000);
System.out.println(data);
}
}
我们想程序输出10000,但是很遗憾实际运行时很多情况下都不是10000而是小于10000,原因是data++并不是原子操作,而是线程从内存中读取data值,完成自增,然后刷新内存值,在单线程的情况下,这并没有什么问题,然而在多线程的情况下,当data值完成自增后还没来得及刷新内存,这个时候另外一个线程将内存中的data完成了读取自增刷新的操作,这个时候前一个线程才将内存的数据刷新,这两个线程的两次自增只自增了一次。这就造成了线程的安全问题,线程安全问题的本质是共享资源被多个线程访问,造成了数据不一致的情况。
实现线程安全的方法
要使上面的demo变成线程安全的程序很简单,这里介绍两种方法。
synchronized
通过对run方法添加synchronized关键字即可实现线程安全:
@Override
public synchronized void run() {
for (int i = 0; i < 10; i++) {
data++;
}
}
synchronized可以修饰方法和代码块,被synchronized修饰的方法和代码块同一时间下只允许一个线程访问相当于给这部分添加了一个锁,任何线程在没拿到这个锁的情况下是不能访问这部分的。
lock
我们还可以通过显式lock的方式为代码加锁。
通过lock的方式加锁,上面的代码可改为:
private final Lock lock = new ReentrantLock(true);
@Override
public void run() {
try {
lock.lock();
for (int i = 0; i < 10; i++) {
data++;
}
} finally {
lock.unlock();
}
}
实例化一个lock,然后为有共享资源访问的代码显式的加锁。Java中的lock有ReentrantLock和ReentrantReadWriteLock两种。
区别:
- Lock使用起来比较灵活,但需要手动释放和开启;采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;
- Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;
- 在并发量比较小的情况下,使用synchronized是个不错的选择,但是在并发量比较高的情况下,其性能下降很严重,此时Lock是个不错的方案。
- 使用Lock的时候,等待/通知 是使用的Condition对象的await()/signal()/signalAll() ,而使用synchronized的时候,则是对象的wait()/notify()/notifyAll();由此可以看出,使用Lock的时候,粒度更细了,一个Lock可以对应多个Condition。
- 虽然Lock缺少了synchronized隐式获取释放锁的便捷性,但是却拥有了锁获取与是释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized所不具备的同步特性;
Java提供的线程工具
lock:ReentrantLock、ReentrantReadWriteLock、StampedLock(JDK8新增);
原子类:AtomicXXXXXX;
信号量:CountDownLatch、CyclicBarrier、Semaphore;
其他的还有线程池工具,ForkJoin框架;
我们的多线程系列文章就是围绕这些个类和工具展开分析。