什么是线程安全
多线程执行某段代码,不对这段代码进行同步处理、线程间的协调,程序运行的结果仍与预期一致,这就是线程安全。
多线程编程的三个核心概念
原子性 : 同数据库事务的原子性,一些操作要么全部成功,要么全部失败,经典的例子就是银行转账。可见性 :多线程并发访问共享变量时,某个线程对共享变量的更新,其他线程能立即看到这个更新。
在java中,对象储存在主内存。每个线程都有自己的工作内存,线程从主内存读取对象到工作内存,执行更新对象操作,立即更新工作内存,但没有立即刷新到主内存,故其他线程仍然读到旧值。
编译器、处理器会进行指令重排序,优化代码,以提高处理速度。程序实际的执行顺序可能与代码中的不一样,但编译器、处理器保证结果是一样的。这不会影响单线程的正确性,但会影响多线程的正确性。
要保证多线程程序正确执行,必需保证原子性、可见性、顺序性。
synchronized、ReentrantLock
使用synchronized、ReentrantLock作用于一段代码,同一时刻只能有一个线程能进入这段代码,保证了原子性、顺序性。使用synchronized、ReentrantLock获取到锁,线程更新共享变量后会立即刷新到主内存,其他线程获取到同一个锁时会将缓存失效并从主内存读取新值,保证了可见性。注意是同一个锁才能保证可见性。synchronized、ReentrantLock的性能较差,因为一个线程占有锁,其他线程会阻塞,线程的挂起和唤醒会降低处理速度。
原子操作类
使用AtmoicInteger、AtmoicLong、AtmoicReference来保证原子性,底层使用CAS(compare and swap)。CAS(compare and swap)是非阻塞同步的计算机指令, 它有三个操作数,内存位置、旧的预期值、新值,当内存位置的值与旧的预期值相等时才将新值存入内存位置。对于ABA问题,可使用AtomicStampedReference,通过引入版本号解决。
volatile
volatile能保证可见性和一定程度的顺序性。 变量被volatile修饰时,线程对变量进行写操作时jvm会向处理器发送lock前缀指令,lock前缀指令相当于内存屏障。 内存屏障的功能
- 写操作修改的值会立即刷新到主内存,并设置其他线程的缓存无效,线程读取变量必需从主内存读取新值,保证了可见性。
- 禁止指令重排序,后面的指令不能再内存屏障之前,前面的指令不能再内存屏障之后,保证一定程度的顺序性。
ThreadLocal
每个线程Thread都有一个ThreadLocalMap的变量threadLocals。 ThreadLocal不存储对象,对象存储于每个线程的ThreadLocalMap。ThreadLocal就是对当前线程的ThreadLocalMap进行增删改查。
ThreadLocal中的内存泄露问题
ThreadLocalMap的key是弱引用,这个key就是ThreadLocal对象。 若ThreadLocal变量被设置为null后,且没有强引用指向这个ThreadLocal对象,根据垃圾回收的可达性分析算法,该ThreadLocal对象将被回收,ThreadLocalMap中某个Entry的key就会变为null,不能再使用的value无法释放内存,造成内存泄露。 ThreadLocalMap的补救措施,调用getEntry()、set()、remove()方法会清除key为null的entry。但不调用这三个方法仍然会有内存泄露问题,因此当ThreadLocal使用完时应当remove掉。
如何实现线程安全
互斥同步 : 使用synchronized、ReentrantLock作用于一段代码,同一时刻只能有一个线程能进入这段代码,保证了原子性、可见性、顺序性。非阻塞同步 : CAS(compare and swap)是非阻塞同步的计算机指令, 它有三个操作数,内存位置、旧的预期值、新值,当内存位置的值与旧的预期值相等时才将新值存入内存位置。对于ABA问题,可使用AtomicStampedReference,通过引入版本号解决。无同步 : 使用ThreadLocal将共享变量的可见性限制在线程内部,每个线程维护一个共享变量的副本,线程间相互隔离,不再争用数据。
|