什么是线程安全
多线程执行某段代码,不对这段代码进行同步处理、线程间的协调,程序运行的结果仍与预期一致,这就是线程安全。
多线程编程的三个核心概念
-
原子性
: 同数据库事务的原子性,一些操作要么全部成功,要么全部失败,经典的例子就是银行转账。 -
可见性
:多线程并发访问共享变量时,某个线程对共享变量的更新,其他线程能立即看到这个更新。
现代的计算机都有几层缓存,一个变量在多个线程*享,每个线程都会对这个变量进行缓存,某个线程内部更新该变量会立即更新缓存,但不会立即更新主内存,这使得其他线程无法立即观察到这个更新变化。
-
顺序性
: 指程序顺序执行代码中的操作。
编译器、处理器会对代码的执行顺序进行优化,以提高处理速度。因此程序实际的执行顺序可能与代码中的不一样,但编译器、处理器保证结果是一样的。
如何保证原子性
使用synchronized、ReentrantLock作用于一段代码,同一时刻只能有一个线程能进入这段代码,保证了原子性、可见性、顺序性。使用synchronized、ReentrantLock的性能较差,因为一个线程占有锁,其他线程会阻塞,线程的挂起和唤醒会降低处理速度。
使用AtmoicInteger、AtmoicLong、AtmoicReference,底层使用CAS(compare and swap)。
如何保证可见性
使用synchronized、ReentrantLock作用于一段代码,同一时刻只能有一个线程能进入这段代码,保证了原子性、可见性、顺序性。使用synchronized、ReentrantLock的性能较差,因为一个线程占有锁,其他线程会阻塞,线程的挂起和唤醒会降低处理速度。
java使用volatile关键字来保证可见性。被volatile修饰的变量,当被某个线程更新时,会立即更新主内存,并将该变量的所有缓存设置为无效。其他线程想要获取该变量必须访问主内存。
如何保证顺序性
编译器、处理器会对代码的执行顺序进行优化,以提高处理速度。因此程序实际的执行顺序可能与代码中的不一样,但编译器、处理器保证结果是一样的。在单线程程序下结果是正确的,但在多线程下有可能不正确。
使用volatile可以一定程度上保证顺序性。
使用synchronized、ReentrantLock作用于一段代码,同一时刻只能有一个线程能进入这段代码,保证了原子性、可见性、顺序性。使用synchronized、ReentrantLock的性能较差,因为一个线程占有锁,其他线程会阻塞,线程的挂起和唤醒会降低处理速度。
如何实现线程安全
-
互斥同步
: 使用synchronized、ReentrantLock作用于一段代码,同一时刻只能有一个线程能进入这段代码,保证了原子性、可见性、顺序性。 -
非阻塞同步
: CAS(compare and swap)是非阻塞同步的计算机指令, 它有三个操作数,内存位置、旧的预期值、新值,当内存位置的值与旧的预期值相等时才将新值存入内存位置。对于ABA问题,可使用AtomicStampedReference,通过引入版本号解决。 -
无同步
: 使用ThreadLocal将共享变量的可见性限制在线程内部,每个线程维护一个共享变量的副本,线程间相互隔离,不再争用数据。