多线程
简述线程,程序、进程的基本概念。以及他们之间关系是什么
线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线 程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程, 或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。
进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是 一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个 指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如 CPU 时间,内存空间,文件,文 件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程是进 程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定, 因为同一进程中的线程极有可能会相互影响。从另一角度来说,进程属于操作系统的范畴,主要是同一 段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。
线程有哪些基本状态?
Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态。 线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。Java 线程状态 变迁如下图所示
如何理解内存泄漏问题?有哪些情况会导致内存泄露?如何解决?
一直以来 java 都占据着语言排行榜的头把交椅。这是与 java 的设计密不可分的,其中最令大家喜欢的不 是面向对象,而是垃圾回收机制。你只需要简单的创建对象而不需要负责释放空间,因为 Java 的垃圾回 收器会负责内存的回收。然而,情况并不是这样简单,内存泄露还是经常会在 Java 应用程序中出现。 下面我们将详细的学习什么是内存泄露,为什么会发生,以及怎样阻止内存泄露。
什么是内存泄露
内存泄露的定义:对于应用程序来说,当对象已经不再被使用,但是 Java 的垃圾回收器不能回收它们的 时候,就产生了内存泄露。
要理解这个定义,我们需要理解对象在内存中的状态。如下图所示,展示了哪些对象是无用对象,哪些 是未被引用的对象;
上图中包含了未引用对象和引用对象。未引用对象将会被垃圾回收器回收,而引用对象却不会。未引用 对象很显然是无用的对象。然而,无用的对象并不都是未引用对象,有一些无用对象也有可能是引用对 象,这部分对象正是内存泄露的来源。
为什么内存泄露会发生
让我们用下面的例子来看看为什么会发生内存泄露。如下图所示,对象 A 引用对象 B,A 的生命周期(t1- t4)比 B 的生命周期(t2-t3)要长,当 B 在程序中不再被使用的时候,A 仍然引用着 B。在这种情况下, 垃圾回收器是不会回收 B 对象的,这就可能造成了内存不足问题,因为 A 可能不止引用着 B 对象,还可能 引用其它生命周期比 A 短的对象,这就造成了大量无用对象不能被回收,且占据了昂贵的内存资源
同样的,B 对象也可能引用着一大堆对象,这些被 B 对象引用着的对象也不能被垃圾回收器回收,所有的 这些无用对象消耗了大量内存资源。
怎样阻止内存泄露
- 1.使用 List、Map 等集合时,在使用完成后赋值为 null
- 2.使用大对象时,在用完后赋值为 null
- 3.目前已知的 jdk1.6 的 substring()方法会导致内存泄露
- 4.避免一些死循环等重复创建或对集合添加元素,撑爆内存
- 5.简洁数据结构、少用静态集合等
- 6.及时的关闭打开的文件,socket 句柄等
- 7.多关注事件监听(listeners)和回调(callbacks),比如注册了一个 listener,当它不再被使用的时候,忘 了注销该 listener,可能就会产生内存泄露
线程池的原理,为什么要创建线程池?创建线程池的方式;
- 原理: 线程池的优点
- 1、线程是稀缺资源,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以重复使用。
- 2、可以根据系统的承受能力,调整线程池中工作线程的数量,防止因为消耗过多内存导致服务器崩溃。
- 线程池的创建
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler)
- corePoolSize:线程池核心线程数量
- maximumPoolSize:线程池最大线程数量
- keepAliverTime:当活跃线程数大于核心线程数时,空闲的多余线程最大存活时间
- unit:存活时间的单位
- workQueue:存放任务的队列
- handler:超出线程范围和队列容量的任务的处理程序
线程池的实现原理
提交一个任务到线程池中,线程池的处理流程如下:
1、判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创 建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。
2、线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如 果工作队列满了,则进入下个流程。
3、判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已 经满了,则交给饱和策略来处理这个任务。
我们通过一个程序来观察线程池的工作原理:
1 public class ThreadPoolTest implements Runnable
2 {
3 @Override
4 public void run()
5 {
6 try
7 {
8 Thread.sleep(300);
9 }
10 catch (InterruptedException e)
11 {
12 e.printStackTrace();
13 }
14 }
15 }
1 public static void main(String[] args)
2 {
3 LinkedBlockingQueue<Runnable> queue =
4 new LinkedBlockingQueue<Runnable>(5);
5 ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 10, 60,
TimeUnit.SECONDS, queue);
6 for (int i = 0; i < 16 ; i++)
7 {
8 threadPool.execute(
9 new Thread(new ThreadPoolTest(), "Thread".concat(i + "")));
10 System.out.println("线程池中活跃的线程数: " +
threadPool.getPoolSize());
11 if (queue.size() > 0)
12 {
13 System.out.println("----------------队列中阻塞的线程数" +
queue.size());
14 }
15 }
16 threadPool.shutdown();
17 }
线程池中活跃的线程数: 1
线程池中活跃的线程数: 2
线程池中活跃的线程数: 3
线程池中活跃的线程数: 4
线程池中活跃的线程数: 5
线程池中活跃的线程数: 5
----------------队列中阻塞的线程数1
线程池中活跃的线程数: 5
----------------队列中阻塞的线程数2
线程池中活跃的线程数: 5
----------------队列中阻塞的线程数3
线程池中活跃的线程数: 5
----------------队列中阻塞的线程数4
线程池中活跃的线程数: 5
----------------队列中阻塞的线程数5
线程池中活跃的线程数: 6
----------------队列中阻塞的线程数5
线程池中活跃的线程数: 7
----------------队列中阻塞的线程数5
线程池中活跃的线程数: 8
----------------队列中阻塞的线程数5
线程池中活跃的线程数: 9
----------------队列中阻塞的线程数5
线程池中活跃的线程数: 10
----------------队列中阻塞的线程数5
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task
Thread[Thread15,5,main] rejected from
java.util.concurrent.ThreadPoolExecutor@232204a1[Running, pool size = 10, active
threads = 10, queued tasks = 5, completed tasks = 0]
at
java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPool
Executor.java:2047)
at
java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
at
java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
at test.ThreadTest.main(ThreadTest.java:17)
从结果可以观察出:
1、创建的线程池具体配置为:核心线程数量为 5 个;全部线程数量为 10 个;工作队列的长度为 5。
2、我们通过 queue.size()的方法来获取工作队列中的任务数。
3、运行原理:
刚开始都是在创建新的线程,达到核心线程数量 5 个后,新的任务进来后不再创建新的线程,而是将任 务加入工作队列,任务队列到达上线 5 个后,新的任务又会创建新的普通线程,直到达到线程池最大的线 程数量 10 个,后面的任务则根据配置的饱和策略来处理。我们这里没有具体配置,使用的是默认的配置 AbortPolicy:直接抛出异常。
当然,为了达到我需要的效果,上述线程处理的任务都是利用休眠导致线程没有释放!!! RejetedExecutionHandler:饱和策略
当队列和线程池都满了,说明线程池处于饱和状态,那么必须对新提交的任务采用一种特殊的策略来进 行处理。这个策略默认配置是 AbortPolicy,表示无法处理新的任务而抛出异常。JAVA 提供了 4 中策略:
- 1、AbortPolicy:直接抛出异常
- 2、CallerRunsPolicy:只用调用所在的线程运行任务
- 3、DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
- 4、DiscardPolicy:不处理,丢弃掉。
我们现在用第四种策略来处理上面的程序:
1 public static void main(String[] args)
2 {
3 LinkedBlockingQueue<Runnable> queue =
4 new LinkedBlockingQueue<Runnable>(3);
5 RejectedExecutionHandler handler = new
ThreadPoolExecutor.DiscardPolicy();
6 7
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, 5, 60,
TimeUnit.SECONDS, queue,handler);
8 for (int i = 0; i < 9 ; i++)
9 {
10 threadPool.execute(
11 new Thread(new ThreadPoolTest(), "Thread".concat(i + "")));
12 System.out.println("线程池中活跃的线程数: " +
threadPool.getPoolSize());
13 if (queue.size() > 0)
14 {
15 System.out.println("----------------队列中阻塞的线程数" +
queue.size());
16 }
17 }
18 threadPool.shutdown();
19 }
这里采用了丢弃策略后,就没有再抛出异常,而是直接丢弃。在某些重要的场景下,可以采用记录日志 或者存储到数据库中,而不应该直接丢弃。
设置策略有两种方式:
RejectedExecutionHandler handler = new ThreadPoolExecutor.DiscardPolicy();
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, 5, 60,
TimeUnit.SECONDS, queue,handler);
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, 5, 60,
TimeUnit.SECONDS, queue);
threadPool.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
JAVA 线程池原理详解二
Executor 框架的两级调度模型
在 HotSpot VM 的模型中,JAVA 线程被一对一映射为本地操作系统线程。JAVA 线程启动时会创建一个本 地操作系统线程,当 JAVA 线程终止时,对应的操作系统线程也被销毁回收,而操作系统会调度所有线程 并将它们分配给可用的 CPU。
在上层,JAVA 程序会将应用分解为多个任务,然后使用应用级的调度器(Executor)将这些任务映射成 固定数量的线程;在底层,操作系统内核将这些线程映射到硬件处理器上。 Executor 框架类图
在前面介绍的 JAVA 线程既是工作单元,也是执行机制。而在 Executor 框架中,我们将工作单元与执行 机制分离开来。Runnable 和 Callable 是工作单元(也就是俗称的任务),而执行机制由 Executor 来提 供。这样一来 Executor 是基于生产者消费者模式的,提交任务的操作相当于生成者,执行任务的线程相 当于消费者。
1、从类图上看,Executor 接口是异步任务执行框架的基础,该框架能够支持多种不同类型的任务执行 策略。
Executor 接口就提供了一个执行方法,任务是 Runnbale 类型,不支持 Callable 类型。
2、ExecutorService 接口实现了 Executor 接口,主要提供了关闭线程池和 submit 方法:
另外该接口有两个重要的实现类:ThreadPoolExecutor 与 ScheduledThreadPoolExecutor。
其中 ThreadPoolExecutor 是线程池的核心实现类,用来执行被提交的任务;而 ScheduledThreadPoolExecutor 是一个实现类,可以在给定的延迟后运行任务,或者定期执行命令。
在上一篇文章中,我是使用 ThreadPoolExecutor 来通过给定不同的参数从而创建自己所需的线程池,但 是在后面的工作中不建议这种方式,推荐使用 Exectuors 工厂方法来创建线程池
这里先来区别线程池和线程组(ThreadGroup 与 ThreadPoolExecutor)这两个概念:
- a、线程组就表示一个线程的集合。
- b、线程池是为线程的生命周期开销问题和资源不足问题提供解决方案,主要是用来管理线程。
Executors 可以创建 3 种类型的 ThreadPoolExecutor:SingleThreadExecutor、FixedThreadExecutor 和 CachedThreadPool
a、SingleThreadExecutor:单线程线程池
我们从源码来看可以知道,单线程线程池的创建也是通过 ThreadPoolExecutor,里面的核心线程数和线 程数都是 1,并且工作队列使用的是无界队列。由于是单线程工作,每次只能处理一个任务,所以后面所 有的任务都被阻塞在工作队列中,只能一个个任务执行。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
b、FixedThreadExecutor:固定大小线程池
ExecutorService threadPool = Executors.newFixedThreadPool(5);
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
这个与单线程类似,只是创建了固定大小的线程数量。
c、CachedThreadPool:无界线程池
ExecutorService threadPool = Executors.newCachedThreadPool();
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
无界线程池意味着没有工作队列,任务进来就执行,线程数量不够就创建,与前面两个的区别是:空闲 的线程会被回收掉,空闲的时间是 60s。这个适用于执行很多短期异步的小程序或者负载较轻的服务 器
Callable、Future、FutureTash 详解
Callable 与 Future 是在 JAVA 的后续版本中引入进来的,Callable 类似于 Runnable 接口,实现 Callable 接 口的类与实现 Runnable 的类都是可以被线程执行的任务。
三者之间的关系:
- Callable 是 Runnable 封装的异步运算任务。
- Future 用来保存 Callable 异步运算的结果
- FutureTask 封装 Future 的实体类
1、Callable 与 Runnbale 的区别
- a、Callable 定义的方法是 call,而 Runnable 定义的方法是 run。
- b、call 方法有返回值,而 run 方法是没有返回值的。
- c、call 方法可以抛出异常,而 run 方法不能抛出异常。
2、Future
Future 表示异步计算的结果,提供了以下方法,主要是判断任务是否完成、中断任务、获取任务执行结 果
1 public interface Future<V> {
2 3
boolean cancel(boolean mayInterruptIfRunning);
4 5
boolean isCancelled();
6 7
boolean isDone();
8 9
V get() throws InterruptedException, ExecutionException;
10
11 V get(long timeout, TimeUnit unit)
12 throws InterruptedException, ExecutionException, TimeoutException;
13 }
3、FutureTask
可取消的异步计算,此类提供了对 Future 的基本实现,仅在计算完成时才能获取结果,如果计算尚未完 成,则阻塞 get 方法。
FutureTask 不仅实现了 Future 接口,还实现了 Runnable 接口,所以不仅可以将 FutureTask 当成一个 任务交给 Executor 来执行,还可以通过 Thread 来创建一个线程。
Callable 与 FutureTask
定义一个 callable 的任务:
1 public class MyCallableTask implements Callable<Integer>
2 {
3 @Override
4 public Integer call()
5 throws Exception
6 {
7 System.out.println("callable do somothing");
8 Thread.sleep(5000);
9 return new Random().nextInt(100);
10 }
11 }
1 public class CallableTest
2 {
3 public static void main(String[] args) throws Exception
4 {
5 Callable<Integer> callable = new MyCallableTask();
6 FutureTask<Integer> future = new FutureTask<Integer>(callable);
7 Thread thread = new Thread(future);
8 thread.start();
9 Thread.sleep(100);
10 //尝试取消对此任务的执行
11 future.cancel(true);
12 //判断是否在任务正常完成前取消
13 System.out.println("future is cancel:" + future.isCancelled());
14 if(!future.isCancelled())
15 {
16 System.out.println("future is cancelled");
17 }
18 //判断任务是否已完成
19 System.out.println("future is done:" + future.isDone());
20 if(!future.isDone())
21 {
22 System.out.println("future get=" + future.get());
23 }
24 else
25 {
26 //任务已完成
27 System.out.println("task is done");
28 }
29 }
30 }
callable do somothing
future is cancel:true
future is done:true
task is done
a、第11行,尝试取消对任务的执行,该方法如果由于任务已完成、已取消则返回false,如果能够取消
还未完成的任务,则返回true,该DEMO中由于任务还在休眠状态,所以可以取消成功。
b、第13行,判断任务取消是否成功:如果在任务正常完成前将其取消,则返回true
c、第19行,判断任务是否完成:如果任务完成,则返回true,以下几种情况都属于任务完成:正常终
止、异常或者取消而完成。
我们的DEMO中,任务是由于取消而导致完成。
d、在第22行,获取异步线程执行的结果,我这个DEMO中没有执行到这里,需要注意的是,
future.get方法会阻塞当前线程, 直到任务执行完成返回结果为止。
Callable 与 Future
public class CallableThread implements Callable<String>
{
@Override
public String call()
throws Exception
{
System.out.println("进入Call方法,开始休眠,休眠时间为:" +
System.currentTimeMillis());
Thread.sleep(10000);
return "今天停电";
}
public static void main(String[] args) throws Exception
{
ExecutorService es = Executors.newSingleThreadExecutor();
Callable<String> call = new CallableThread();
Future<String> fu = es.submit(call);
es.shutdown();
Thread.sleep(5000);
System.out.println("主线程休眠5秒,当前时间" + System.currentTimeMillis());
String str = fu.get();
System.out.println("Future已拿到数据,str=" + str + ";当前时间为:" +
System.currentTimeMillis());
}
}
进入Call方法,开始休眠,休眠时间为:1478606602676
主线程休眠5秒,当前时间1478606608676
Future已拿到数据,str=今天停电;当前时间为:147860661267
这里的 future 是直接扔到线程池里面去执行的。由于要打印任务的执行结果,所以从执行结果来看,主 线程虽然休眠了 5s,但是从 Call 方法执行到拿到任务的结果,这中间的时间差正好是 10s,说明 get 方法 会阻塞当前线程直到任务完成。
通过 FutureTask 也可以达到同样的效果:
public static void main(String[] args) throws Exception
{
ExecutorService es = Executors.newSingleThreadExecutor();
Callable<String> call = new CallableThread();
FutureTask<String> task = new FutureTask<String>(call);
es.submit(task);
es.shutdown();
Thread.sleep(5000);
System.out.println("主线程等待5秒,当前时间为:" + System.currentTimeMillis());
String str = task.get();
System.out.println("Future已拿到数据,str=" + str + ";当前时间为:" +
System.currentTimeMillis());
}
以上的组合可以给我们带来这样的一些变化:
如有一种场景中,方法 A 返回一个数据需要 10s,A 方法后面的代码运行需要 20s,但是这 20s 的执行过程 中,只有后面 10s 依赖于方法 A 执行的结果。如果与以往一样采用同步的方式,势必会有 10s 的时间被浪 费,如果采用前面两种组合,则效率会提高:
- 1、先把 A 方法的内容放到 Callable 实现类的 call()方法中
- 2、在主线程中通过线程池执行 A 任务
- 3、执行后面方法中 10 秒不依赖方法 A 运行结果的代码
- 4、获取方法 A 的运行结果,执行后面方法中 10 秒依赖方法 A 运行结果的代码 这样代码执行效率一下子就提高了,程序不必卡在 A 方法处。
创建线程池的几种方式:
ThreadPoolExecutor、ThreadScheduledExecutor、ForkJoinPool
线程的生命周期,什么时候会出现僵死进程
- 1.新建(Thread t = new Thread(。。。))===t.start()
- 2.可运行状态(Runnable)====获取时间片
- 3.运行状态《====时间片用完 ====》1.run(),main()方法退出;2.异常退出
- 4.阻塞状态
- 4.死亡
僵死进程是指子进程退出时,父进程并未对其发出的 SIGCHLD 信号进行适当处理,导致子 进程停留在僵 死状态等待其父进程为其收尸,这个状态下的子进程就是僵死进程。
说说线程安全问题,什么是线程安全,如何实现线程安全;
- 线程安全 - 如果线程执行过程中不会产生共享资源的冲突,则线程安全。
- 线程不安全 - 如果有多个线程同时在操作主内存中的变量,则线程不安全
实现线程安全的三种方式
- 1)互斥同步
- 临界区:syncronized、ReentrantLock
- 信号量 semaphore
- 互斥量 mutex
- 2)非阻塞同步 CAS(Compare And Swap)
- 3)无同步方案
- 可重入代码
- 使用 Threadlocal 类来包装共享变量,做到每个线程有自己的 copy
- 线程本地存储
Java 多线程安全机制
在开始讨论 java 多线程安全机制之前,首先从内存模型来了解一下什么是多线程的安全性。
我们都知道 java 的内存模型中有主内存和线程的工作内存之分,主内存上存放的是线程共享的变量(实 例字段,静态字段和构成数组的元素),线程的工作内存是线程私有的空间,存放的是线程私有的变量 (方法参数与局部变量)。线程在工作的时候如果要操作主内存上的共享变量,为了获得更好的执行性 能并不是直接去修改主内存而是会在线程私有的工作内存中创建一份变量的拷贝(缓存),在工作内存 上对变量的拷贝修改之后再把修改的值刷回到主内存的变量中去,JVM 提供了 8 中原子操作来完成这一过 程:lock, unlock, read, load, use, assign, store, write。深入理解 java 虚拟机-jvm 最高特性与实践这本 书中有一个图很好的表示了线程,主内存和工作内存之间的关系:
如果只有一个线程当然不会有什么问题,但是如果有多个线程同时在操作主内存中的变量,因为 8 种操作 的非连续性和线程抢占 cpu 执行的机制就会带来冲突的问题,也就是多线程的安全问题。线程安全的定 义就是:如果线程执行过程中不会产生共享资源的冲突就是线程安全的。
Java 里面一般用以下几种机制保证线程安全:
1.互斥同步锁(悲观锁)
- 1)Synchorized
- 2)ReentrantLock
互斥同步锁也叫做阻塞同步锁,特征是会对没有获取锁的线程进行阻塞。
要理解互斥同步锁,首选要明白什么是互斥什么是同步。简单的说互斥就是非你即我,同步就是顺序访 问。互斥同步锁就是以互斥的手段达到顺序访问的目的。操作系统提供了很多互斥机制比如信号量,互 斥量,临界区资源等来控制在某一个时刻只能有一个或者一组线程访问同一个资源。
Java 里面的互斥同步锁就是 Synchorized 和 ReentrantLock,前者是由语言级别实现的互斥同步锁,理解 和写法简单但是机制笨拙,在 JDK6 之后性能优化大幅提升,即使在竞争激烈的情况下也能保持一个和 ReentrantLock 相差不多的性能,所以 JDK6 之后的程序选择不应该再因为性能问题而放弃 synchorized。 ReentrantLock 是 API 层面的互斥同步锁,需要程序自己打开并在 finally 中关闭锁,和 synchorized 相比 更加的灵活,体现在三个方面:等待可中断,公平锁以及绑定多个条件。但是如果程序猿对 ReentrantLock 理解不够深刻,或者忘记释放 lock,那么不仅不会提升性能反而会带来额外的问题。另 外 synchorized 是 JVM 实现的,可以通过监控工具来监控锁的状态,遇到异常 JVM 会自动释放掉锁。而 ReentrantLock 必须由程序主动的释放锁。
互斥同步锁都是可重入锁,好处是可以保证不会死锁。但是因为涉及到核心态和用户态的切换,因此比 较消耗性能。JVM 开发团队在 JDK5-JDK6 升级过程中采用了很多锁优化机制来优化同步无竞争情况下锁的 性能。比如:自旋锁和适应性自旋锁,轻量级锁,偏向锁,锁粗化和锁消除。
2.非阻塞同步锁
- 原子类(CAS)
非阻塞同步锁也叫乐观锁,相比悲观锁来说,它会先进行资源在工作内存中的更新,然后根据与主存中 旧值的对比来确定在此期间是否有其他线程对共享资源进行了更新,如果旧值与期望值相同,就认为没 有更新,可以把新值写回内存,否则就一直重试直到成功。它的实现方式依赖于处理器的机器指令: CAS(Compare And Swap)
JUC 中提供了几个 Automic 类以及每个类上的原子操作就是乐观锁机制
不激烈情况下,性能比 synchronized 略逊,而激烈的时候,也能维持常态。激烈的时候,Atomic 的性能 会优于 ReentrantLock 一倍左右。但是其有一个缺点,就是只能同步一个值,一段代码中只能出现一个 Atomic 的变量,多于一个同步无效。因为他不能在多个 Atomic 之间同步。
非阻塞锁是不可重入的,否则会造成死锁。
3.无同步方案
1)可重入代码
在执行的任何时刻都可以中断-重入执行而不会产生冲突。特点就是不会依赖堆上的共享资源
2)ThreadLocal/Volaitile
线程本地的变量,每个线程获取一份共享变量的拷贝,单独进行处理。
- 线程本地存储
如果一个共享资源一定要被多线程共享,可以尽量让一个线程完成所有的处理操作,比如生产者消费者 模式中,一般会让一个消费者完成对队列上资源的消费。典型的应用是基于请求-应答模式的 web 服务器 的设计
创建线程池有哪几个核心参数? 如何合理配置线程池的大小
1)核心参数
public ThreadPoolExecutor(int corePoolSize, // 核心线程数量大小
int maximumPoolSize, // 线程池最大容纳线程数
long keepAliveTime, // 线程空闲后的存活时长
TimeUnit unit,
//缓存异步任务的队列 //用来构造线程池里的worker线程
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
//线程池任务满载后采取的任务拒绝策略
RejectedExecutionHandler handler)
- 核心说明
- 当线程池中线程数量小于 corePoolSize 则创建线程,并处理请求。
- 当线程池中线程数量大于等于 corePoolSize 时,则把请求放入 workQueue 中,随着线程池 中的核 心线程们不断执行任务,只要线程池中有空闲的核心线程,线程池就从 workQueue 中取 任务并处 理
- 当 workQueue 已存满,放不下新任务时则新建非核心线程入池,并处理请求直到线程数目 达到 maximumPoolSize(最大线程数量设置值)。
- 如果线程池中线程数大于 maximumPoolSize 则使用 RejectedExecutionHandler 来进行任 务拒绝 处理。
3)线程池大小分配
线程池究竟设置多大要看你的线程池执行的什么任务了,CPU 密集型、IO 密集型、混合型,任 务类型不 同,设置的方式也不一样。
任务一般分为:CPU 密集型、IO 密集型、混合型,对于不同类型的任务需要分配不同大小的线 程池。
- 3.1)CPU 密集型 尽量使用较小的线程池,一般 Cpu 核心数+1
- 3.2)IO 密集型
- 方法一:可以使用较大的线程池,一般 CPU 核心数 * 2
- 方法二:(线程等待时间与线程 CPU 时间之比 + 1)* CPU 数目
- 3.3)混合型 可以将任务分为 CPU 密集型和 IO 密集型,然后分别使用不同的线程池去处理,按情况而定
关于线程池的执行原则及配置参数详解
在软件开发中,池一直都是一种非常优秀的设计思想,通过建立池可以有效的利用系统资源,节约系统 性能。Java 中的线程池就是一种非常好的实现,从 JDK 1.5 开始 Java 提供了一个线程工厂 Executors 用 来生成线程池,通过 Executors 可以方便的生成不同类型的线程池。但是要更好的理解使用线程池,就 需要了解线程池的配置参数意义以及线程池的具体工作机制。
下面先介绍一下线程池的好处以及创建方式,接着会着重介绍关于线程池的执行原则以及构造方法的参 数详解。
线程池的好处
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统 的稳定性,使用线程池可以进行统一的分配,调优和监控。
创建线程池
//参数初始化
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
//核心线程数量大小
private static final int corePoolSize = Math.max(2, Math.min(CPU_COUNT - 1, 4));
//线程池最大容纳线程数
private static final int maximumPoolSize = CPU_COUNT * 2 + 1;
//线程空闲后的存活时长
private static final int keepAliveTime = 30;
//任务过多后,存储任务的一个阻塞队列
BlockingQueue<Runnable> workQueue = new SynchronousQueue<>();
//线程的创建工厂
ThreadFactory threadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread newThread(Runnable r) {
return new Thread(r, "AdvacnedAsyncTask #" + mCount.getAndIncrement());
}
};
//线程池任务满载后采取的任务拒绝策略
RejectedExecutionHandler rejectHandler = new
ThreadPoolExecutor.DiscardOldestPolicy();
//线程池对象,创建线程
ThreadPoolExecutor mExecute = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
TimeUnit.SECONDS,
workQueue,
threadFactory,
rejectHandler
);
具体参数介绍
- corePoolSize 线程池的核心线程数。在没有设置 allowCoreThreadTimeOut 为 true 的情况下,核心线程会在线 程池中一直存活,即使处于闲置状态。
- maximumPoolSize 线程池所能容纳的最大线程数。当活动线程(核心线程+非核心线程)达到这个数值后,后续任务将 会根据 RejectedExecutionHandler 来进行拒绝策略处理。
- keepAliveTime 非核心线程 闲置时的超时时长。超过该时长,非核心线程就会被回收。若线程池通设置核心线程也 允许 timeOut,即 allowCoreThreadTimeOut 为 true,则该时长同样会作用于核心线程,在超过 aliveTime 时,核心线程也会被回收,AsyncTask 配置的线程池就是这样设置的。
- unit keepAliveTime 时长对应的单位。
- workQueue 线程池中的任务队列,通过线程池的 execute() 方法提交的 Runnable 对象会存储在该队列中。
- ThreadFactory 线程工厂,功能很简单,就是为线程池提供创建新线程的功能。这是一个接口,可以通过自定义, 做一些自定义线程名的操作。
- RejectedExecutionHandler 当任务无法被执行时(超过线程最大容量 maximum 并且 workQueue 已经被排满了)的处理策略, 这里有四种任务拒绝类型。
线程池工作原则
- 1、当线程池中线程数量小于 corePoolSize 则创建线程,并处理请求。
- 2、当线程池中线程数量大于等于 corePoolSize 时,则把请求放入 workQueue 中,随着线程池中的 核心线程们不断执行任务,只要线程池中有空闲的核心线程,线程池就从 workQueue 中取任务并 处理。
- 3 、当 workQueue 已存满,放不下新任务时则新建非核心线程入池,并处理请求直到线程数目达 到 maximumPoolSize(最大线程数量设置值)。
- 4、如果线程池中线程数大于 maximumPoolSize 则使用 RejectedExecutionHandler 来进行任务 拒绝处理。
任务队列 BlockingQueue
任务队列 workQueue 是用于存放不能被及时处理掉的任务的一个队列,它是 一个 BlockingQueue 类 型。
- 关于 BlockingQueue,虽然它是 Queue 的子接口,但是它的主要作用并不是容器,而是作为线程 同步的工具,他有一个特征,当生产者试图向 BlockingQueue 放入(put)元素,如果队列已满,则 该线程被阻塞;当消费者试图从 BlockingQueue 取出(take)元素,如果队列已空,则该线程被阻 塞。(From 疯狂 Java 讲义)
任务拒绝类型
- ThreadPoolExecutor.AbortPolicy: 当线程池中的数量等于最大线程数时抛 java.util.concurrent.RejectedExecutionException 异常, 涉及到该异常的任务也不会被执行,线程池默认的拒绝策略就是该策略。
- ThreadPoolExecutor.DiscardPolicy(): 当线程池中的数量等于最大线程数时,默默丢弃不能执行的新加任务,不报任何异常。
- ThreadPoolExecutor.CallerRunsPolicy(): 当线程池中的数量等于最大线程数时,重试添加当前的任务;它会自动重复调用 execute()方法。
- ThreadPoolExecutor.DiscardOldestPolicy(): 当线程池中的数量等于最大线程数时,抛弃线程池中工作队列头部的任务(即等待时间最久的任务), 并执行新传入的任务。
java 线程池如何合理的设置大小
线程池究竟设置多大要看你的线程池执行的什么任务了,CPU 密集型、IO 密集型、混合型,任务类型不 同,设置的方式也不一样 任务一般分为:CPU 密集型、IO 密集型、混合型,对于不同类型的任务需要分配不同大小的线程池
1、CPU 密集型
尽量使用较小的线程池,一般 Cpu 核心数+1 因为 CPU 密集型任务 CPU 的使用率很高,若开过多的线程,只能增加线程上下文的切换次数,带来额外 的开销
2、IO 密集型
方法一:可以使用较大的线程池,一般 CPU 核心数 * 2 IO 密集型 CPU 使用率不高,可以让 CPU 等待 IO 的时候处理别的任务,充分利用 cpu 时间
方法二:线程等待时间所占比例越高,需要越多线程。线程 CPU 时间所占比例越高,需要越少线程。
下面举个例子:
比如平均每个线程 CPU 运行时间为 0.5s,而线程等待时间(非 CPU 运行时间,比如 IO)为 1.5s,CPU 核 心数为 8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32。这个公式进一步转化为: 最佳线程数目 = (线程等待时间与线程 CPU 时间之比 + 1)*CPU 数目
3、混合型
可以将任务分为 CPU 密集型和 IO 密集型,然后分别使用不同的线程池去处理,按情况而定
volatile、ThreadLocal 的使用场景和原理;
volatile 原理
volatile 变量进行写操作时,JVM 会向处理器发送一条 Lock 前缀的指令,将这个变量所在缓 存行的数据 写会到系统内存。
Lock 前缀指令实际上相当于一个内存屏障(也成内存栅栏),它确保指令重排序时不会把其 后面的指 令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内 存屏障这句指令 时,在它前面的操作已经全部完成。
volatile 的适用场景
- 1)状态标志,如:初始化或请求停机
- 2)一次性安全发布,如:单列模式
- 3)独立观察,如:定期更新某个值
- 4)“volatile bean” 模式
- 开销较低的“读-写锁”策略,如:计数器
ThreadLocal 原理
ThreadLocal 是用来维护本线程的变量的,并不能解决共享变量的并发问题。ThreadLocal 是 各线程将 值存入该线程的 map 中,以 ThreadLocal 自身作为 key,需要用时获得的是该线程之前 存入的值。如果 存入的是共享变量,那取出的也是共享变量,并发问题还是存在的。
ThreadLocal 的适用场景
场景:数据库连接、Session 管理
volatile 的适用场景
模式 #1:状态标志
也许实现 volatile 变量的规范使用仅仅是使用一个布尔状态标志,用于指示发生了一个重要的一次性事 件,例如完成初始化或请求停机。
1. volatile boolean shutdownRequested;
2.
3. ...
4.
5. public void shutdown() {
6. shutdownRequested = true;
7. }
8.
9. public void doWork() {
10. while (!shutdownRequested) {
11. // do stuff
12. }
13. }
线程 1 执行 doWork()的过程中,可能有另外的线程 2 调用了 shutdown,所以 boolean 变量必须是 volatile。
而如果使用 synchronized 块编写循环要比使用 volatile 状态标志编写麻烦很多。由于 volatile 简化了 编码,并且状态标志并不依赖于程序内任何其他状态,因此此处非常适合使用 volatile。
这种类型的状态标记的一个公共特性是:通常只有一种状态转换; shutdownRequested 标志从 false 转换为 true ,然后程序停止。这种模式可以扩展到来回转换的状态标志,但是只有在转换周期不被察 觉的情况下才能扩展(从 false 到 true ,再转换到 false )。此外,还需要某些原子状态转换机制, 例如原子变量。
模式 #2:一次性安全发布(one-time safe publication)
在缺乏同步的情况下,可能会遇到某个对象引用的更新值(由另一个线程写入)和该对象状态的旧值同 时存在。
这就是造成著名的双重检查锁定(double-checked-locking)问题的根源,其中对象引用在没有同步的 情况下进行读操作,产生的问题是您可能会看到一个更新的引用,但是仍然会通过该引用看到不完全构 造的对象。
//注意volatile!!!!!!!!!!!!!!!!!
2. private volatile static Singleton instace;
3.
4. public static Singleton getInstance(){
5. //第一次null检查
6. if(instance == null){
7. synchronized(Singleton.class) { //1
8. //第二次null检查
9. if(instance == null){ //2
10. instance = new Singleton();//3
11. }
12. }
13. }
14. return instance;
如果不用 volatile,则因为内存模型允许所谓的“无序写入”,可能导致失败。——某个线程可能会获得一 个未完全初始化的实例。
考察上述代码中的 //3 行。此行代码创建了一个 Singleton 对象并初始化变量 instance 来引用此对象。 这行代码的问题是:在 Singleton 构造函数体执行之前,变量 instance 可能成为非 null 的!
什么?这一说法可能让您始料未及,但事实确实如此。
在解释这个现象如何发生前,请先暂时接受这一事实,我们先来考察一下双重检查锁定是如何被破坏 的。假设上述代码执行以下事件序列:
- 线程 1 进入 getInstance() 方法。
- 由于 instance 为 null,线程 1 在 //1 处进入 synchronized 块。
- 线程 1 前进到 //3 处,但在构造函数执行之前,使实例成为非 null。
- 线程 1 被线程 2 预占。
- 线程 2 检查实例是否为 null。因为实例不为 null,线程 2 将 instance 引用返回,返回一个构造完 整但部分初始化了的 Singleton 对象。
- 线程 2 被线程 1 预占。
- 线程 1 通过运行 Singleton 对象的构造函数并将引用返回给它,来完成对该对象的初始化。
模式 #3:独立观察(independent observation)
安全使用 volatile 的另一种简单模式是:定期 “发布” 观察结果供程序内部使用。【例如】假设有一种环 境传感器能够感觉环境温度。一个后台线程可能会每隔几秒读取一次该传感器,并更新包含当前文档的 volatile 变量。然后,其他线程可以读取这个变量,从而随时能够看到最新的温度值。
使用该模式的另一种应用程序就是收集程序的统计信息。【例】如下代码展示了身份验证机制如何记忆 最近一次登录的用户的名字。将反复使用 lastUser 引用来发布值,以供程序的其他部分使用。
1. public class UserManager {
2. public volatile String lastUser; //发布的信息
3.
4. public boolean authenticate(String user, String password) {
5. boolean valid = passwordIsValid(user, password);
6. if (valid) {
7. User u = new User();
8. activeUsers.add(u);
9. lastUser = user;
10. }
11. return valid;
12. }
13. }
模式 #4:“volatile bean” 模式
volatile bean 模式的基本原理是:很多框架为易变数据的持有者(例如 HttpSession )提供了容器, 但是放入这些容器中的对象必须是线程安全的。
在 volatile bean 模式中,JavaBean 的所有数据成员都是 volatile 类型的,并且 getter 和 setter 方法必 须非常普通——即不包含约束!
1. @ThreadSafe
2. public class Person {
3. private volatile String firstName;
4. private volatile String lastName;
5. private volatile int age;
6.
7. public String getFirstName() { return firstName; }
8. public String getLastName() { return lastName; }
9. public int getAge() { return age; }
10.
11. public void setFirstName(String firstName) {
12. this.firstName = firstName;
13. }
14.
15. public void setLastName(String lastName) {
16. this.lastName = lastName;
17. }
18.
19. public void setAge(int age) {
20. this.age = age;
21. }
22.
模式 #5:开销较低的“读-写锁”策略
如果读操作远远超过写操作,您可以结合使用内部锁和 volatile 变量来减少公共代码路径的开销。 如下显示的线程安全的计数器,使用 synchronized 确保增量操作是原子的,并使用 volatile 保证 当前结果的可见性。如果更新不频繁的话,该方法可实现更好的性能,因为读路径的开销仅仅涉及 volatile 读操作,这通常要优于一个无竞争的锁获取的开销。
1. @ThreadSafe
2. public class CheesyCounter {
3. // Employs the cheap read-write lock trick
4. // All mutative operations MUST be done with the 'this' lock held
5. @GuardedBy("this") private volatile int value;
6.
7. //读操作,没有synchronized,提高性能
8. public int getValue() {
9. return value;
10. }
11.
12. //写操作,必须synchronized。因为x++不是原子操作
13. public synchronized int increment() {
14. return value++;
15. }
使用锁进行所有变化的操作,使用 volatile 进行只读操作。 其中,锁一次只允许一个线程访问值,volatile 允许多个线程执行读操作
正确使用 Volatile 变量
volatile 变量使用指南
Java 语言中的 volatile 变量可以被看作是一种 “程度较轻的 synchronized ”;与 synchronized 块相 比,volatile 变量所需的编码较少,并且运行时开销也较少,但是它所能实现的功能也仅是 synchronized 的一部分。本文介绍了几种有效使用 volatile 变量的模式,并强调了几种不适合使用 volatile 变量的情形。
锁提供了两种主要特性:互斥(mutual exclusion) 和可见性(visibility)。互斥即一次只允许一个线程 持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能 够使用该共享数据。可见性要更加复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获 得该锁的另一个线程是可见的 —— 如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能 是修改前的值或不一致的值,这将引发许多严重问题。
Volatile 变量
Volatile 变量具有 synchronized 的可见性特性,但是不具备原子特性。这就是说线程能够自动发现 volatile 变量的最新值。Volatile 变量可用于提供线程安全,但是只能应用于非常有限的一组用例:多个 变量之间或者某个变量的当前值与修改后值之间没有约束。因此,单独使用 volatile 还不足以实现计数 器、互斥锁或任何具有与多个变量相关的不变式(Invariants)的类(例如 “start <=end”)。
出于简易性或可伸缩性的考虑,您可能倾向于使用 volatile 变量而不是锁。当使用 volatile 变量而非锁 时,某些习惯用法(idiom)更加易于编码和阅读。此外,volatile 变量不会像锁那样造成线程阻塞,因 此也很少造成可伸缩性问题。在某些情况下,如果读操作远远大于写操作,volatile 变量还可以提供优于 锁的性能优势。
正确使用 volatile 变量的条件
您只能在有限的一些情形下使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同 时满足下面两个条件:
- 对变量的写操作不依赖于当前值。
- 该变量没有包含在具有其他变量的不变式中。
实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当 前状态。
第一个条件的限制使 volatile 变量不能用作线程安全计数器。虽然增量操作( x++ )看上去类似一个单 独操作,实际上它是一个由读取-修改-写入操作序列组成的组合操作,必须以原子方式执行,而 volatile 不能提供必须的原子特性。实现正确的操作需要使 x 的值在操作期间保持不变,而 volatile 变 量无法实现这点。(然而,如果将值调整为只从单个线程写入,那么可以忽略第一个条件。)
大多数编程情形都会与这两个条件的其中之一冲突,使得 volatile 变量不能像 synchronized 那样普遍 适用于实现线程安全。清单 1 显示了一个非线程安全的数值范围类。它包含了一个不变式 —— 下界总是 小于或等于上界。
清单 1. 非线程安全的数值范围类
@NotThreadSafe
public class NumberRange {
private int lower, upper;
public int getLower() { return lower; }
public int getUpper() { return upper; }
public void setLower(int value) {
if (value > upper)
throw new IllegalArgumentException(...);
lower = value;
} p
ublic void setUpper(int value) {
if (value < lower)
throw new IllegalArgumentException(...);
upper = value;
}
}
这种方式限制了范围的状态变量,因此将 lower 和 upper 字段定义为 volatile 类型不能够充分实现类 的线程安全;从而仍然需要使用同步。否则,如果凑巧两个线程在同一时间使用不一致的值执行 setLower 和 setUpper 的话,则会使范围处于不一致的状态。例如,如果初始状态是 (0, 5) ,同一 时间内,线程 A 调用 setLower(4) 并且线程 B 调用 setUpper(3) ,显然这两个操作交叉存入的值是 不符合条件的,那么两个线程都会通过用于保护不变式的检查,使得最后的范围值是 (4, 3) —— 一个 无效值。至于针对范围的其他操作,我们需要使 setLower() 和 setUpper() 操作原子化 —— 而将字 段定义为 volatile 类型是无法实现这一目的的。
性能考虑
使用 volatile 变量的主要原因是其简易性:在某些情形下,使用 volatile 变量要比使用相应的锁简单得 多。使用 volatile 变量次要原因是其性能:某些情况下,volatile 变量同步机制的性能要优于锁。
很难做出准确、全面的评价,例如 “X 总是比 Y 快”,尤其是对 JVM 内在的操作而言。(例如,某些情况 下 VM 也许能够完全删除锁机制,这使得我们难以抽象地比较 volatile 和 synchronized 的开 销。)就是说,在目前大多数的处理器架构上,volatile 读操作开销非常低 —— 几乎和非 volatile 读操 作一样。而 volatile 写操作的开销要比非 volatile 写操作多很多,因为要保证可见性需要实现内存界定 (Memory Fence),即便如此,volatile 的总开销仍然要比锁获取低。
volatile 操作不会像锁一样造成阻塞,因此,在能够安全使用 volatile 的情况下,volatile 可以提供一些 优于锁的可伸缩特性。如果读操作的次数要远远超过写操作,与锁相比,volatile 变量通常能够减少同步 的性能开销。
正确使用 volatile 的模式
很多并发性专家事实上往往引导用户远离 volatile 变量,因为使用它们要比使用锁更加容易出错。然 而,如果谨慎地遵循一些良好定义的模式,就能够在很多场合内安全地使用 volatile 变量。要始终牢记 使用 volatile 的限制 —— 只有在状态真正独立于程序内其他内容时才能使用 volatile —— 这条规则能够 避免将这些模式扩展到不安全的用例。
模式 #1:状态标志
也许实现 volatile 变量的规范使用仅仅是使用一个布尔状态标志,用于指示发生了一个重要的一次性事 件,例如完成初始化或请求停机。
很多应用程序包含了一种控制结构,形式为 “在还没有准备好停止程序时再执行一些工作”,如清单 2 所 示 清单 2. 将 volatile 变量作为状态标志使用
volatile boolean shutdownRequested;
...
public void shutdown() { shutdownRequested = true; }
public void doWork() {
while (!shutdownRequested) {
// do stuff
}
}
很可能会从循环外部调用 shutdown() 方法 —— 即在另一个线程中 —— 因此,需要执行某种同步来 确保正确实现 shutdownRequested 变量的可见性。(可能会从 JMX 侦听程序、GUI 事件线程中的操作 侦听程序、通过 RMI 、通过一个 Web 服务等调用)。然而,使用 synchronized 块编写循环要比使 用清单 2 所示的 volatile 状态标志编写麻烦很多。由于 volatile 简化了编码,并且状态标志并不依赖于 程序内任何其他状态,因此此处非常适合使用 volatile。
这种类型的状态标记的一个公共特性是:通常只有一种状态转换; shutdownRequested 标志从 false 转换为 true ,然后程序停止。这种模式可以扩展到来回转换的状态标志,但是只有在转换周期不被察 觉的情况下才能扩展(从 false 到 true ,再转换到 false )。此外,还需要某些原子状态转换机 制,例如原子变量。
模式 #2:一次性安全发布(one-time safe publication)
缺乏同步会导致无法实现可见性,这使得确定何时写入对象引用而不是原语值变得更加困难。在缺乏同 步的情况下,可能会遇到某个对象引用的更新值(由另一个线程写入)和该对象状态的旧值同时存在。 (这就是造成著名的双重检查锁定(double-checked-locking)问题的根源,其中对象引用在没有同步 的情况下进行读操作,产生的问题是您可能会看到一个更新的引用,但是仍然会通过该引用看到不完全 构造的对象)。
实现安全发布对象的一种技术就是将对象引用定义为 volatile 类型。清单 3 展示了一个示例,其中后台 线程在启动阶段从数据库加载一些数据。其他代码在能够利用这些数据时,在使用之前将检查这些数据 是否曾经发布过。
清单 3. 将 volatile 变量用于一次性安全发布
public class BackgroundFloobleLoader {
public volatile Flooble theFlooble;
public void initInBackground() {
// do lots of stuff
theFlooble = new Flooble(); // this is the only write to theFlooble
}
}
public class SomeOtherClass {
public void doWork() {
while (true) {
// do some stuff...
// use the Flooble, but only if it is ready
if (floobleLoader.theFlooble != null)
doSomething(floobleLoader.theFlooble);
}
}
}
如果 theFlooble 引用不是 volatile 类型, doWork() 中的代码在解除对 theFlooble 的引用时,将 会得到一个不完全构造的 Flooble 。
该模式的一个必要条件是:被发布的对象必须是线程安全的,或者是有效的不可变对象(有效不可变意 味着对象的状态在发布之后永远不会被修改)。volatile 类型的引用可以确保对象的发布形式的可见性, 但是如果对象的状态在发布后将发生更改,那么就需要额外的同步。
模式 #3:独立观察(independent observation)
安全使用 volatile 的另一种简单模式是:定期 “发布” 观察结果供程序内部使用。例如,假设有一种环境 传感器能够感觉环境温度。一个后台线程可能会每隔几秒读取一次该传感器,并更新包含当前文档的 volatile 变量。然后,其他线程可以读取这个变量,从而随时能够看到最新的温度值。
使用该模式的另一种应用程序就是收集程序的统计信息。清单 4 展示了身份验证机制如何记忆最近一次 登录的用户的名字。将反复使用 lastUser 引用来发布值,以供程序的其他部分使用。
清单 4. 将 volatile 变量用于多个独立观察结果的发布
public class UserManager {
public volatile String lastUser;
public boolean authenticate(String user, String password) {
boolean valid = passwordIsValid(user, password);
if (valid) {
User u = new User();
activeUsers.add(u);
lastUser = user;
} return valid;
}
}
该模式是前面模式的扩展;将某个值发布以在程序内的其他地方使用,但是与一次性事件的发布不同, 这是一系列独立事件。这个模式要求被发布的值是有效不可变的 —— 即值的状态在发布后不会更改。使 用该值的代码需要清楚该值可能随时发生变化。
模式 #4:“volatile bean” 模式
volatile bean 模式适用于将 JavaBeans 作为“荣誉结构”使用的框架。在 volatile bean 模式中, JavaBean 被用作一组具有 getter 和/或 setter 方法 的独立属性的容器。volatile bean 模式的基本原理 是:很多框架为易变数据的持有者(例如 HttpSession )提供了容器,但是放入这些容器中的对象必 须是线程安全的。
在 volatile bean 模式中,JavaBean 的所有数据成员都是 volatile 类型的,并且 getter 和 setter 方法必 须非常普通 —— 除了获取或设置相应的属性外,不能包含任何逻辑。此外,对于对象引用的数据成员, 引用的对象必须是有效不可变的。(这将禁止具有数组值的属性,因为当数组引用被声明为 volatile 时,只有引用而不是数组本身具有 volatile 语义)。对于任何 volatile 变量,不变式或约束都不能包含 JavaBean 属性。清单 5 中的示例展示了遵守 volatile bean 模式的 JavaBean:
清单 5. 遵守 volatile bean 模式的 Person 对象
@
ThreadSafe
public class Person {
private volatile String firstName;
private volatile String lastName;
private volatile int age;
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public int getAge() { return age; }
public void setFirstName(String firstName) {
this.firstName = firstName;} p
ublic void setLastName(String lastName) {
this.lastName = lastName;
} p
ublic void setAge(int age) {
this.age = age;
}
}
volatile 的高级模式
前面几节介绍的模式涵盖了大部分的基本用例,在这些模式中使用 volatile 非常有用并且简单。这一节 将介绍一种更加高级的模式,在该模式中,volatile 将提供性能或可伸缩性优势。
volatile 应用的的高级模式非常脆弱。因此,必须对假设的条件仔细证明,并且这些模式被严格地封装了 起来,因为即使非常小的更改也会损坏您的代码!同样,使用更高级的 volatile 用例的原因是它能够提 升性能,确保在开始应用高级模式之前,真正确定需要实现这种性能获益。需要对这些模式进行权衡, 放弃可读性或可维护性来换取可能的性能收益 —— 如果您不需要提升性能(或者不能够通过一个严格的 测试程序证明您需要它),那么这很可能是一次糟糕的交易,因为您很可能会得不偿失,换来的东西要 比放弃的东西价值更低。
模式 #5:开销较低的读-写锁策略
目前为止,您应该了解了 volatile 的功能还不足以实现计数器。因为 ++x 实际上是三种操作(读、添 加、存储)的简单组合,如果多个线程凑巧试图同时对 volatile 计数器执行增量操作,那么它的更新值 有可能会丢失。
然而,如果读操作远远超过写操作,您可以结合使用内部锁和 volatile 变量来减少公共代码路径的开 销。清单 6 中显示的线程安全的计数器使用 synchronized 确保增量操作是原子的,并使用 volatile 保证当前结果的可见性。如果更新不频繁的话,该方法可实现更好的性能,因为读路径的开 销仅仅涉及 volatile 读操作,这通常要优于一个无竞争的锁获取的开销。
清单 6. 结合使用 volatile 和 synchronized 实现 “开销较低的读-写锁”
@
ThreadSafe
public class CheesyCounter {
// Employs the cheap read-write lock trick
// All mutative operations MUST be done with the 'this' lock held
@GuardedBy("this") private volatile int value;
public int getValue() { return value; }
public synchronized int increment() {
return value++;
}
}
之所以将这种技术称之为 “开销较低的读-写锁” 是因为您使用了不同的同步机制进行读写操作。因为本 例中的写操作违反了使用 volatile 的第一个条件,因此不能使用 volatile 安全地实现计数器 —— 您必须 使用锁。然而,您可以在读操作中使用 volatile 确保当前值的可见性,因此可以使用锁进行所有变化的 操作,使用 volatile 进行只读操作。其中,锁一次只允许一个线程访问值,volatile 允许多个线程执行读 操作,因此当使用 volatile 保证读代码路径时,要比使用锁执行全部代码路径获得更高的共享度 —— 就 像读-写操作一样。然而,要随时牢记这种模式的弱点:如果超越了该模式的最基本应用,结合这两个 竞争的同步机制将变得非常困难。
结束语
与锁相比,Volatile 变量是一种非常简单但同时又非常脆弱的同步机制,它在某些情况下将提供优于锁 的性能和伸缩性。如果严格遵循 volatile 的使用条件 —— 即变量真正独立于其他变量和自己以前的值 —— 在某些情况下可以使用 volatile 代替 synchronized 来简化代码。然而,使用 volatile 的代 码往往比使用锁的代码更加容易出错。本文介绍的模式涵盖了可以使用 volatile 代替 synchronized 的最常见的一些用例。遵循这些模式(注意使用时不要超过各自的限制)可以帮助您安全地实现大多数 用例,使用 volatile 变量获得更佳性能。
ThreadLocal 什么时候会出现 OOM 的情况?为什么?
ThreadLocal 变量是维护在 Thread 内部的,这样的话只要我们的线程不退出,对象的引用就会 一直存 在。当线程退出时,Thread 类会进行一些清理工作,其中就包含 ThreadLocalMap, Thread 调用 exit 方 法如下:
ThreadLocal 在没有线程池使用的情况下,正常情况下不会存在内存泄露,但是如果使用了线程 池的 话,就依赖于线程池的实现,如果线程池不销毁线程的话,那么就会存在内存泄露。
ThreadLocal 可以说是笔试面试的常客,每逢面试基本都会问到,关于 ThreadLocal 的原理以及不正当的 使用造成的 OOM 内存溢出的问题,值得花时间仔细研究一下其原理。这一篇主要学习一下 ThreadLocal 的原理,在下一篇会深入理解一下 OOM 内存溢出的原理和最佳实践。
ThreadLocal 很容易让人望文生义,想当然地认为是一个“本地线程”。其实,ThreadLocal 并不是一个 Thread,而是 Thread 的一个局部变量,也许把它命名为 ThreadLocalVariable 更容易让人理解一些。 当使用 ThreadLocal 维护变量时,ThreadLocal 为每个使用该变量的线程提供独立的变量副本,所以每一 个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
ThreadLocal 的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程 内多个函数或者组件之间一些公共变量的传递的复杂度。
从线程的角度看,目标变量就像是线程的本地变量,这也是类名中“Local”所要表达的意思。
ThreadLocal 全部方法和内部类
ThreadLocal 全部方法和内部类结构如下:
ThreadLocal 公有的方法就四个,分别为:get、set、remove、intiValue:
也就是说我们平时使用的时候关心的是这四个方法。
ThreadLocal 是如何做到为每一个线程维护变量的副本的呢?
其实实现的思路很简单:在 ThreadLocal 类中有一个 static 声明的 Map,用于存储每一个线程的变量副 本,Map 中元素的键为线程对象,而值对应线程的变量副本。我们自己就可以提供一个简单的实现版 本
public class SimpleThreadLocal<T> {
/**
* Key为线程对象,Value为传入的值对象
*/
private static Map<Thread, T> valueMap = Collections.synchronizedMap(new
HashMap<Thread, T>());
/**
* 设值
* @param value Map键值对的value
*/
public void set(T value) {
valueMap.put(Thread.currentThread(), value);
} /
**
* 取值
* @return
*/
public T get() {
Thread currentThread = Thread.currentThread();
//返回当前线程对应的变量
T t = valueMap.get(currentThread);
//如果当前线程在Map中不存在,则将当前线程存储到Map中
if (t == null && !valueMap.containsKey(currentThread)) {
t = initialValue();
valueMap.put(currentThread, t);
} r
eturn t;
} p
ublic void remove() {
valueMap.remove(Thread.currentThread());
} p
ublic T initialValue() {return null;
} p
ublic static void main(String[] args) {
SimpleThreadLocal<List<String>> threadLocal = new SimpleThreadLocal<>();
new Thread(() -> {
List<String> params = new ArrayList<>(3);
params.add("张三");
params.add("李四");
params.add("王五");
threadLocal.set(params);
System.out.println(Thread.currentThread().getName());
threadLocal.get().forEach(param -> System.out.println(param));
}).start();
new Thread(() -> {
try {
Thread.sleep(1000);
List<String> params = new ArrayList<>(2);
params.add("Chinese");
params.add("English");
threadLocal.set(params);
System.out.println(Thread.currentThread().getName());
threadLocal.get().forEach(param -> System.out.println(param));
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
ThreadLocal 源码分析
1、线程局部变量在 Thread 中的位置
既然是线程局部变量,那么理所当然就应该存储在自己的线程对象中,我们可以从 Thread 的源码中找 到线程局部变量存储的地方:
我们可以看到线程局部变量是存储在 Thread 对象的 threadLocals 属性中,而 threadLocals 属性是 一个 ThreadLocal.ThreadLocalMap 对象。 ThreadLocalMap 为 ThreadLocal 的静态内部类,如下图所示:
2、Thread 和 ThreadLocalMap 的关系
Thread 和 ThreadLocalMap 的关系,先看下边这个简单的图,可以看出 Thread 中的 threadLocals 就是 ThreadLocal 中的 ThreadLocalMap:
可以看出每个 thread 实例都有一个 ThreadLocalMap 。在上图中的一个 Thread 的这个 ThreadLocalMap 中分别存放了 3 个 Entry,默认一个 ThreadLocalMap 初始化了 16 个 Entry,每一个 Entry 对象存放的是一个 ThreadLocal 变量对象。
再简单一点的说就是:一个 Thread 中只有一个 ThreadLocalMap,一个 ThreadLocalMap 中可以有多个 ThreadLocal 对象,其中一个 ThreadLocal 对象对应一个 ThreadLocalMap 中的一个 Entry(也就是说: 一个 Thread 可以依附有多个 ThreadLocal 对象)。
再看一张图片,应该可以更好的理解,如下图:
这里的 Map 其实是 ThreadLocalMap。
3、ThreadLocalMap 与 WeakReference
ThreadLocalMap 从字面上就可以看出这是一个保存 ThreadLocal 对象的 map(其实是以它为 Key),不 过是经过了两层包装的 ThreadLocal 对象:
(1)第一层包装是使用 WeakReference<ThreadLocal<?>> 将 ThreadLocal 对象变成一个弱引用的 对象;
(2)第二层包装是定义了一个专门的类 Entry 来扩展 WeakReference<ThreadLocal<?>> :
类 Entry 很显然是一个保存 map 键值对的实体, ThreadLocal<?> 为 key, 要保存的线程局部变量的值为 value 。 super(k) 调用的 WeakReference 的构造函数,表示将 ThreadLocal<?> 对象转换成弱引用对 象,用做 key。
4、ThreadLocalMap 的构造函数
可以看出,ThreadLocalMap 这个 map 的实现是使用一个数组 private Entry[] table 来保存键值对 的实体,初始大小为 16, ThreadLocalMap 自己实现了如何从 key 到 value 的映射:
1、为什么要用 ThreadLocalMap 来保存线程局部对象呢?
原因是一个线程拥有的的局部对象可能有很多,这样实现的话,那么不管你一个线程拥有多少个局部变 量,都是使用同一个 ThreadLocalMap 来保存的,ThreadLocalMap 中 private Entry[] table 的 初始大小是 16。超过容量的 2/3 时,会扩容。
ThreadLocal 可能引起的 OOM 内存溢出问题简要分析
我们知道 ThreadLocal 变量是维护在 Thread 内部的,这样的话只要我们的线程不退出,对象的引用就会 一直存在。当线程退出时,Thread 类会进行一些清理工作,其中就包含 ThreadLocalMap,Thread 调用 exit 方法如下:
但是,当我们使用线程池的时候,就意味着当前线程未必会退出(比如固定大小的线程池,线程总是存 在的)。如果这样的话,将一些很大的对象设置到 ThreadLocal 中(这个很大的对象实际保存在 Thread 的 threadLocals 属性中),这样的话就可能会出现内存溢出的情况。
一种场景就是说如果使用了线程池并且设置了固定的线程,处理一次业务的时候存放到 ThreadLocalMap 中一个大对象,处理另一个业务的时候,又一个线程存放到 ThreadLocalMap 中一个大 对象,但是这个线程由于是线程池创建的他会一直存在,不会被销毁,这样的话,以前执行业务的时候 存放到 ThreadLocalMap 中的对象可能不会被再次使用,但是由于线程不会被关闭,因此无法释放
Thread 中的 ThreadLocalMap 对象,造成内存溢出。 也就是说,ThreadLocal 在没有线程池使用的情况下,正常情况下不会存在内存泄露,但是如果使用了 线程池的话,就依赖于线程池的实现,如果线程池不销毁线程的话,那么就会存在内存泄露。所以我们 在使用线程池的时候,使用 ThreadLocal 要格外小心!
Java 并发和并行
- 并发 : 是指两个或多个事件在同一时间间隔发生,在一台处理器上“同时”处理多个任务;
- 并行 : 是指两个或者多个事件在同一时刻发生,在多台处理器上同时处理多个任务。
怎么提高并发量,请列举你所知道的方案?
1、HTML 静态化
2、图片服务器分离
3、数据库集群、库表散列
4、缓存
5、镜像
6、负载均衡
7、最新:CDN 加速技术
什么是 CDN? CDN 的全称是内容分发网络。其目的是通过在现有的 Internet 中增加一层新的网络架构,将网站的 内容发布到最接近用户的网络“边缘”,使用户可以就近取得所需的内容,提高用户访问网站的响应速 度。
CDN 有别于镜像,因为它比镜像更智能,或者可以做这样一个比喻:CDN=更智能的镜像+缓存+流 量导流。因而,CDN 可以明显提高 Internet 网络中信息流动的效率。从技术上全面解决由于网络带宽 小、用户访问量大、网点分布不均等问题,提高用户访问网站的响应速度。
大型网站是怎样解决多用户高并发访问的
为了解决大型网站的访问量大、并发量高、海量数据的问题,我们一般会考虑业务拆分和分布式部署。 我们可以把那些关联不太大的业务独立出来,部署到不同的机器上,从而实现大规模的分布式系统。但 这之中也有一个问题,那就是用户如何选择相应的机器的问题,这也被称为访问统一入口问题,而解决 的方法是我们可以在集群机器的前面增加负载均衡设备,实现流量分发(总图如下)。
这里得先解释一下何为“负载均衡”,负载均衡就是将负载(工作任务、访问请求等)进行平衡、分摊到 多个操作单元(服务器、组件等)上进行执行,是解决高性能,单点故障(高可用,如果你是单机版网 络,一旦服务器挂掉了,那么用户就无法请求了,但对于集群来说,一台服务器挂掉了,负载均衡器会 把用户的请求发送给其他的服务器进行处理),扩展性(这里主要是指水平伸缩)的终极解决方案。
为什么要用线程池
在 Java 中,如果每当一个请求到达就创建一个新线程,开销是相当大的。在实际使用中,每个请求创建 新线程的服务器在创建和销毁线程上花费的时间和消耗的系统资源,甚至可能要比花在处理实际的用户 请求的时间和资源要多得多。除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。如果 在一个 JVM 里创建太多的线程,可能会导致系统由于过度消耗内存或“切换过度”而导致系统资源不足。为 了防止资源不足,服务器应用程序需要一些办法来限制任何给定时刻处理的请求数目,尽可能减少创建 和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务, 这就是“池化资源”技术产生的原因。
线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个任务重用线程,线程创建的开 销就被分摊到了多个任务上了,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延 迟。这样,就可以立即为请求服务,使应用程序响应更快。另外,通过适当地调整线程池中的线程数目 可以防止出现资源不足的情况。
线程的几种状态
线程在一定条件下,状态会发生变化。线程一共有以下几种状态:
- 新建状态(New):新创建了一个线程对象。
- 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的 start()方法。该状态的线程位于 “可运行线程池”中,变得可运行,只等待获取 CPU 的使用权。即在就绪状态的进程除 CPU 之外,其 它的运行所需资源都已全部获得。
- 运行状态(Running):就绪状态的线程获取了 CPU,执行程序代码。
- 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃 CPU 使用权,暂时停止运行。直到线程进入 就绪状态,才有机会转到运行状态。 阻塞的情况分三种:
- 等待阻塞:运行的线程执行 wait()方法,该线程会释放占用的所有资源,JVM 会把该线程放入 “等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用 notify()或 notifyAll()方法才能被唤醒,
- 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线 程放入“锁池”中。
- 其他阻塞:运行的线程执行 sleep()或 join()方法,或者发出了 I/O 请求时,JVM 会把该线程置为 阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新 转入就绪状态。
- 死亡状态(Dead):线程执行完了或者因异常退出了 run()方法,该线程结束生命周期。
常用的线程池模式以及不同线程池的使用场景
强烈建议程序员使用较为方便的 Executors 工厂方法 Executors.newCachedThreadPool()(无界线 程池,可以进行自动线程回收)、Executors.newFixedThreadPool(int)(固定大小线程池) Executors.newSingleThreadExecutor()(单个后台线程)它们均为大多数使用场景预定义了设 置
- 4.1 newCachedThreadPoo
- 4.2 newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等 待
- 4.3 newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
- 4.4 newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务, 保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
- 4.5 newWorkStealingPool 创建一个拥有多个任务队列(以便减少连接数)的线程池。