一、线程安全
线程在生命周期内的状态:
NEW(新建状态)、RUNNABLE(就绪状态)、RUNNING(运行状态)、BLOCKED(阻塞状态)、DEAD(终止状态)五种状态。
关于线程状态的具体介绍可以看以下博客,这里不再具体介绍。
线程的执行流程及各个阶段的状态
线程安全问题只在多线程环境下出现,为了保证高并发场景下的线程安全,可以从以下四个维度考量:
-
数据单线程内可见:单线程总是安全的,通过限制数据仅在单线程内可见,可以避免数据被其他线程篡改。
-
只读对象:只读对象总是安全的,他的特性是允许复制,拒绝写入。一个对象想要拒绝任何写入,必须满足以下条件:
使用final关键字修饰类,避免被继承。
使用private final 修饰属性,避免属性被中途修改。
没有任何更新方法 -
线程安全类:某些线程安全类内部又非常明确的线程同步机制。
-
同步与锁机制:合理利用JDK中的并发包。
线程同步类:Object中的wait()和notify()进行同步的方式逐渐被淘汰。主要的同步工具有:CountDownLatch,Semaphore,CyclicBarrier等。
并发集合类:最主要的有ConcurrentHashMap,其他的还有ConcurrentSkipListMap,CopyOnWriteArrayList,BlockingQueue等。
线程管理类:线程池,Executors线程工具类。
锁相关类:锁以Lock接口为核心,最有名的便是ReentrantLock。
二、什么是锁
Java中常用锁的实现方式有两种: -
在并发包类族中,Lock是顶级接口。以下是Lock的继承关系。
在他的实现逻辑中,并未用到synchronized,而是利用了volatile。对于AQS,它是JUC实现同步的基础工具。它其中的volatile int state 变量作为共享资源。以下为对于AQS的具体介绍:
AQS同步队列简单介绍 -
利用同步代码块。即synchronized关键字。
三、线程同步
1、线程同步的概念:线程之间按某种机制协调先后次序执行,当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,知道该线程完成操作。实现线程同步的方式有很多,同步方法,锁,阻塞队列等。
2、volatile
volatile解决的问题是多线程共享变量的可见性问题,类似于synchronized,但是不具备synchronized的互斥性。所以对volatile变量的操作并非都具有原子性, 比如变量的自增自减操作。
对于能够实现自增自减原子操作的类有AtomicLong和LongAddr。在JDK8中推荐使用LongAddr类,它的性能比AtomicLong更好,减少了乐观锁的重试次数。
3、信号量同步
信号量同步是指在不同线程之间,通过传递同步信号量来协调线程执行的先后次序。
基于时间维度和信号维度的两个类:CountDownLatch、Semaphore。
CountDownLatch使用说明:当在子线程中出现异常而无法执行countDown方法时,可以执行子线程的setUncaughtExceptionHandler() 方法捕获。
Semaphore使用说明:首先调用Semphore对象的acquire()成功后,才可以往下执行,完成后执行release()释放持有的信号量,下一个线程就可以获得这个空闲信号量进入执行。 以下为Semaphore方法实例
public class CustomCheckWindow {
public static void main(String[] args) {
//设定3个信号量,每次最多只能处理3个
Semaphore semaphore = new Semaphore(3);
//设置5个人
for (int i = 1; i <= 5; i ++) {
new Thread(new SecurityCheckThread(i, semaphore)).start();
}
}
private static class SecurityCheckThread implements Runnable {
private int seq;
private Semaphore semaphore;
public SecurityCheckThread(int seq, Semaphore semaphore) {
this.seq = seq;
this.semaphore = semaphore;
}
@Override
public void run() {
try {
semaphore.acquire();
System.out.println("No." + seq + "乘客正在查验中");
if(seq % 2 == 0) {
Thread.sleep(1000);
System.out.println("No." + seq + "乘客,身份可疑,不能出国!");
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release();
System.out.println("No." + seq + "乘客已完成服务。");
}
}
}
}
另外还有CyclicBarrier是基于同步到达某个点的信号量出发机制。
四、线程池
1、线程池的创建
ThreadPoolExecutor的构造方法如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
第一个参数:corePoolSize表示核心线程数。如果等于0,则任务执行完之后,没有任何请求进入时销毁线程池的线程;如果大于0,即使本地任务执行完毕,核心线程也不会被销毁。这个值的设置非常关键,设置过大会浪费资源,设置过小会导致线程频繁地创建或销毁。
第二个参数:maximumPoolSize表示线程池能够容纳同时执行的最大线程数。从上方示例代码中的第1处来看,必须大于或等于1。如果待执行的线程数大于此值,需要借助第5个参数的帮助,缓存在队列中。如果maximumPoolSize与corePoolSize 相等,即是固定大小线程池。
第三个参数:keepAliveTime表示线程池中的线程空闲时间, 当空闲时间达到keepAliveTime值时,线程会被销毁,直到只剩下corePoolSize个线程为止,避免浪费内存和句柄资源。但是当ThreadPoolExecutor的allowCoreThreadTimeOut 变量设置为true时,核心线程超时后也会被回收。
第四个参数:TimeUnit表示时间单位。keepAIiveTime的时间单位通常TimeUnit.SECONDS。
第五个参数:workQueue表示缓存队列。当有新任务产生时,如果核心线程未满,则会创建线程并执行任务;当核心线程数满之后,会将线程加入缓存队列中;当缓存队列也满之后,便会创建线程执行任务,但此时的线程数要小于等于maxmumPoolSize;当线程数等于maxmumPoolSize时,便会根据最后一个参数的策略进行相应处理。
第六个参数:threadFactory表示线程工厂。它用来生产一组相同任务的线程。线程池的命名是通过给这个factory增加组名前缀来实现的。在虚拟机栈分析时,就可以知道线程任务是由哪个线程工厂产生的。 以下为简单的ThreadFactory示例:
上述示例包括线程工厂和任务执行体的定义,通过newThread方法快速、统一的创建线程任务,强调线程一定要有特定意义的名称,方便出错时回溯。
第七个参数:**handler表示执行拒绝策略的对象。当超过第5个参数workQueue 的任务缓存区上限的时候,就可以通过该策略处理请求,这是一种简单的限流保护。**友好的拒绝策略可以是如下三种:
(1) 保存到数据库进行削嶝填谷。在空闲时再提取出来筑行。
(2) 转向某个提示页面。
(3) 打印日志。
在ThreadPoolExecutor中提供了四个公开的内部静态类:
- AbortPolicy : 丢弃任务并抛出RejectedExecutionException异常。
- DiscardPolicy: 丢弃任务,但是不抛出异常,这是不推荐的做法。
- DiscardOldestPolicy: 抛弃队列中等待最久的任务,然后把当前任务加入队列中。
- CallerRunsPolicy: 调用任务的run()方法,绕过线程池直接执行。
五、ThreadLocal
1、引用的类型
- 强引用 最为常见,如Object object = new Object();这样的变量声明和定义就会产生对该对象的强引用。只要对象有强引用指向,并且GCRoots 可达,那么Java内存回收时,即使濒临内存耗尽,也不会回收该对象。
- 软引用:引用力度弱于“强引用",是用在非必需对象的场景。在即将OOM之前,垃圾回收器会把这些软引用指向的对象加入回收范围,以获得更多的内存空间, 让程序能够继续健康运行。
- 弱引用:引用强度较前两者更弱,也是用来描述非必需对象的。如果弱引用指向的对象只存在弱引用这一条线路,则在下一次YGC时会被回收。调用WeakReference.get()可能返回null,要注意空指针异常。
- 虚引用:是极弱的一种引用关系,定义完成后,就无法通过该引用获取指向的对象。为一个对象设置虚引用的唯一目的就是希望能在这个对象被回收时收到一个系统通知。
对于四种引用的理解,可以结合以下例子。
在房产交易市场中,某个卖家有一套房子,成功出售给某个买家后引用置为null。这里有4个买家使用4种不同的引用关系指向这套房子。买家 buyerl是强引用,如果把seller引用赋值给它,则永久有效,系统不会因为seller=null 就触发对这套房子的回收,这是房屋交易市场最常见的交付方式。买家buyer2是软引用,只要不产生OOM,buyer2.get()就可以获取房子对象,就像房子是租来的一样。买家buyer3是弱引用,一旦过户后,seller置为null’ buyer3的房子持有时间估计只有几秒钟,卖家只是给买家做了一张假的房产证,买家高兴了几秒钟后,发现房子已经不是自己的了。buyer4是虚引用,定义完成后无法访问到房子对象,卖家只是虚构了房源,是空手套白狼的诈骗术。
2、ThreadLocal的价值
创建ThreadLocal的基本形式:
在ThreadLocal中覆写了initialValue()方法,他的执行是在当前线程执行ThreadLocal.get()方法的时候。
ThreadLocal是每一个线程单独持有的,每一个线程都有独立的变量副本,其他线程不能访问,所以不存在线程安全问题。ThreadLocal对象一般使用private static修饰。需要注意的是,在使用某个引用来操作共享对象时,依然需要进行线程同步, 如下所示:
public class InitValueInThreadLocal {
private static final StringBuilder INIT_VALUE = new StringBuilder("init");
private static final ThreadLocal<StringBuilder> builder = new ThreadLocal<StringBuilder>() {
@Override
protected StringBuilder initialValue() {
return INIT_VALUE;
}
};
private static class AppendStringThread implements Runnable {
@Override
public void run() {
StringBuilder inThread = builder.get();
for (int i = 0; i < 10; i ++) {
inThread.append(i);
}
System.out.println(inThread.toString());
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i ++) {
new Thread(new AppendStringThread()).start();
}
TimeUnit.SECONDS.sleep(10);
}
}
运行结果如图:
Thread和ThreadLocal的类图主要如下:
ThreadLocal有个静态内部类叫ThreadLocalMap,在ThreadLocalMap中有一个静态的Entry,它用来存储key,value,Entry的key存储的便是ThreadLocal本身,是弱引用,value则是强引用。
在ThreadLocal中当ThreadLocal对象引用被置为null时,Entry的key在下次YGC时必然会被垃圾回收。在ThreadLocal中,使用set和get时,会进行判断自动将key=null的value置为null,避免内存泄漏。但是,在使用时,ThreadLocal对象一般会设置为private static。这就延长了ThreadLocal的生命周期,容易造成内存泄漏。
PS:关于ThreadLocal的内存泄漏问题,可以参考以下几篇文章,我自己这里感觉也是比较模糊的。
ThreadLocal为什么会内存泄漏
static作用:静态变量的生存周期和作用域
ThreadLocal的三个重要方法:
(1) set():如果没有set操作的ThreadLocal,容易引起脏数据问题。
(2) get():始终没有get操作的ThreadLocal对象是没有意义的。
(3) remove():如果没有remove操作,容易引起内存泄漏。
3、ThreadLocal的副作用
- 脏数据:线程复用会产生脏数据。由于线程池会重用Thread对象,那么与Thread绑定的类的静态属性ThreadLocal变量也会被重用。如果在实现的线程run()方法体中不显式地调用remove()清理与线程相关的ThreadLocal信息,那么倘若下一个线程不调用 set()设置初始值,就可能get()到重用的线程信息,包括ThreadLocal所关联的线程对象的value值。
- 内存泄漏:当ThreadLocal被static关键字修饰时,寄希望于ThreadLocal对象失去引用后出发弱引用机制来回收Entry的Value就不现实了,如果不进行remove操作,ThreadLocal所持有的对象是不会进行释放的。所以在用完ThreadLocal时,必须要及时使用remove进行清理。