一、什么是线程安全?
简单来说,线程安全是多个线程访问同一段代码,不会造成不确定的结果。
线程安全就是多线程访问时,采用了加锁机制,同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再对共享数据进行操作,确保不会出现数据不一致或者数据污染。
线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据
二、如何保证线程安全?
先说一下造成线程不安全的三个原因,主要是:
原子性:一个或多个线程操作 CPU 执行的过程中被中断,互斥性称为操作的原子性。
可见性:一个线程对共享变量的修改,其他线程不能立刻看到。
有序性:程序执行的顺序没有按照代码的先后顺序执行。
可见性:必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(也就是其他线程获得锁时应获得最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本上继续操作,从而引起不一致。
这里解释一下有序性,为什么程序执行的顺序会和代码的执行顺序不一致。因为 java 平台的两种编译器:静态编译器(javac)和动态编译器(jit:just in time)。
- 静态编译器是将.java 文件编译成.class 文件,之后便可以解释执行。
- 动态编译器是将.class 文件编译成机器码,之后再由 jvm 运行。
有时候,动态编译器为了程序的整体性能会对指令进行重排序,虽然重排序可以提升程序的性能,但是重排序之后会导致源代码中指定的内存访问顺序与实际的执行顺序不一样,就会出现线程不安全的问题。
针对上述三个造成线程不安全的问题,java 程序如何保证线程安全呢?
1. 针对原子性:
JDK 里面提供了很多 atomic 类,比如 AtomicInteger、AtomicLong、AtomicBoolean 等等,这些类本身可以通过 CAS 来保证操作的原子性。另外 Java 也提供了各种锁机制,来保证锁内的代码块在同一时刻只能有一个线程执行,比如使用 synchronized 加锁,保证一个线程在对资源进行读、写时,其他线程不可对此资源进行操作,从而保证了线程的安全性。
2. 针对可见性:
同样可以通过 synchronized 关键字加锁来解决,与此同时,java 还提供了 volatile 关键字,要优于 synchronized 的性能,同样可以保证修改对其他线程的可见性。volatile 一般用于对变量的写操作不依赖于当前值的场景中,比如状态标记量等。
3. 针对重排序问题:
可以通过 synchronized 关键字定义同步代码块或者同步方法保障有序性,另外也可以通过 Lock 接口来保障有序性。