1、java线程的几种状态
JDK从1.5开始在Thread类中增添了State枚举
NEW(新建)、RUNNABLE(当线程正在运行或者已经就绪正等待CPU时间片)、BLOCKED(阻塞)、WAITING(无期限等待)、TIMED_WAITING(限期等待)、TERMINATED(死亡)六种
阻塞:线程在等待获取对象同步锁时
Waiting:调用不带超时的wait()或者不带超时的join()
TIMED_WAITING:调用sleep(xx)、或者带超时的wait、join
CountDownLatch是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程的操作执行完后再执行。

2、线程安全
线程安全:当多个线程访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或在调用方法时进行任何其他协调,调用对象都能得到正确结果,那么就成这个对象是线程安全的。(这属于绝对线程安全)
线程安全可以分为5类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立
不可变:永远也不会改变,根本不存在线程安全问题。
自定义不可变对象:这个类是final的保证不能被扩展,把所有可能影响对象状态的字段都声明为fianl的,不提供任何会修改对象状态的方法,把所有的域声明为private的(避免客户端访问引用的可变域),保证对象能被正确的创建出来(在构造方法内不会有this引用逃逸)
Jdk中不可变对象:String、Long/Double等包装类、BigInteger、BigDecimal等大数据类型。AtomicInteger和AtomicLong不是不可变对象
相对线程安全:Vector、HashTable、Collections.synchronizedCollection()
线程兼容:对象本身并非线程安全,但是可以通过外部调用的同步保证线程安全:ArrayList、HashMap等大部分类
线程对立:调用端无论是否采取同步措施都不是线程安全的System.setIn和System.setOut
五个特性:共享性、互斥性、原子性、可见性、有序性
线程安全的实现方法:
互斥/阻塞同步:(悲观锁)
synchronized:编译后在同步代码段的前后加上monitorenter、monitorexit两个指令;对同一线程来说是可重入的。因为java线程是映射到操作系统的原生线程之上的,阻塞或唤醒一个线程都需要从用户态切换到核心态,状态转换的时间可能比执行同步代码的时间还要长,所以synchronized是一个重量级的操作。
ReentrantLock(重入锁):也是一个重量级的操作,在语义上与synchronized相同,但是可以进行更精细的控制:等待可中断(可以尝试获取锁)、公平锁(当多个线程都等待同一个锁时必须按照时间顺序获得锁)、锁绑定多个条件(可以同时绑定多个Condition对象)
非阻塞同步:(乐观锁)
先进性操作,如果没有其他线程争用共享数据就操作成功了,如果有就产生了冲突,然后在采取其他措施补偿(最常见的就是不断尝试,直到成功)
CAS指令,例如:AtomicInteger.incrementAndGet()方法的原子性
无同步方案:
可重入代码:不依赖存储在堆上的数据和公用的系统资源,用到的状态量都由参数传入、不调用非可重入的方法
3、ThreadLocal的设计理念与作用
线程局部变量,是一种维护线程限制的方法。通常定义为private static类型。它提供了get与set访问器,为每个使用该变量的线程维护一份单独的拷贝,get总是返回当前线程通过set设置的最新的值。
当我们在一个类的多个方法中想使用某个变量,但这个变量仅反映当前线程的状态时可以通过方法传参的形式,但是ThreadLocal更优雅。比如DAO的单例实现,它的属性Connection就不是线程安全的,这种情况就可以使用ThreadLocal,这样每次返回的都是同一个连接的副本。
1 | public void set(T value) { |
实现机制:
每个Thread对象内部都有一个ThreadLocalMap,可以存放若干个以ThreadLocal为key的Entry对象;ThreadLocal对象就是当前线程ThreadLocalMap的访问入口
当我们调用get方法时,先获取当前线程,然后获取当前线程的ThreadLocalMap,ThreadLocalMap是使用ThreadLocal的弱引用作为Key的,如果不为空就以当前ThreadLocal对象为key获取对应的value,如果为空就调用setInitialValue()方法(内部调用了initialValue方法产生value)
ThreadLocal可能产生内存泄漏吗?
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统GC的时候,这个ThreadLocal势必会被回收。这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:ThreadRef->Thread->ThreaLocalMap->Entry->value永远无法回收,造成内存泄漏。Jdk已经考虑到这点,它在每次get和set的时候都会清除Map中key为null的entry。但是当我们结合线程池使用的时候就不能避免了。因为线程在结束之后不会被GC回收而是放回池内待重用。
4、共享对象
- 指令重排:CPU可能会对输入的代码进行乱序执行优化,处理器会在计算之后将乱序执行的结果重组以确保其与顺序执行的结果是一致的。JVM的即时编译期中也有类似的指令重排优化。使用volatile可以禁止指令重排。
- 使用同步可以保证原子性和可见性
- volatile语义:仅保证了可见性不能保证原子性、禁止指令重排(通过添加内存屏障)
- 线程封闭技术(不共享变量)也可以保证线程安全,可以通过栈限制或ThreadLocal两种方式实现。
- 创建后状态就不能变的对象是不可变对象,不可变对象天生线程安全
5、可见性是什么?实现方式
可见性:一个线程修改了对象的状态后,其它线程能够真正看到改变。Synchronized和volatile和final可以实现内存可见性。
- synchronized是通过“在unlock之前必须把变量值同步会主内存”这条规则实现
- final是指被final修饰的字段在构造器中一旦初始化完成,并且构造器中没有this引用逃逸,那么在其他线程中就能看见final字段的值。
- volatile的特殊规则:新值立即同步到主内存中,每次使用前都从主内存中刷新。
一个类只有get/set方法都设为synchronized才能保证这个类是线程安全的,
只将set方法设为synchronized并不能保证可见性,因为在调用非synchronized
的get方法时并不能保证工作内存从主内存刷新数据,所以可能读到脏数据。
6、volatile的作用及实现方式
作用:内存可见性、禁止指令重排
实现:内存可见性是通过新值立即同步到主内存,以及每次使用前立即从主内存刷新;禁止指令重排是通过添加内存屏障的方式,即重排序时不能把后面的指令放到内存屏障之前。
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
- 禁止进行指令重排序
- 没办法保证对变量操作的原子性
7、this引用逃逸
是指在构造函数返回之前其他线程就持有该对象的引用。通常发生在以内部类的形式在构造函数中启动一个线程或者注册监听器。
1 | public class ThisEscape { |
改进:我们可以在构造方法中创建一个线程但是不要启动它。所以我们可以创建一个private的构造方法,然后创建一个工厂方法