发布于2021-06-14 10:38 阅读(961) 评论(0) 点赞(22) 收藏(4)
Java内存模型由Java虚拟机规范定义,用来屏蔽各个平台的硬件差异。简单总结成以下三点:
线程,主内存与工作内存的交互关系如下图所示:
内存间的交互操作有很多,和volatile有关的操作有:
volatile关键字是用来修饰共享变量的!主要有两个作用:
接下来通过一段代码深刻理解一下,假如线程1先执行,线程2后执行:
//线程1
boolean stop = false;
while(!stop){
doSomething();
}
//线程2
stop = true;
这段代码是很典型的一段代码,很多人在中断线程时可能都会采用这种标记办法。但是事实上,这段代码会完全运行正确么?即一定会将线程中断么?不一定,也许在大多数时候,这个代码能够把线程中断,但是也有可能会导致无法中断线程(虽然这个可能性很小,但是只要一旦发生这种情况就会造成死循环了)。
下面解释一下这段代码为何有可能导致无法中断线程。在前面已经解释过,每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将stop变量的 值拷贝一份放在自己的工作内存当中。那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。
但是用volatile修饰之后就变得不一样了:
第一:使用volatile关键字会强制将修改的值立即写入主存;
第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。
那么在线程2修改stop值时(当然这里包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。那么线程1读取到的就是最新的正确的值。
注意:
volatile关键字只保证可见性,无法保证对变量的任何操作都是原子性的,所以在以下情况中,需要使用锁来保证原子性:
(1)运算结果依赖变量的当前值,并且有不止一个线程在修改变量的值;
(2)变量需要与其他状态变量共同参与不变约束。
上面我们理解了volatile关键字如何保证线程操作的可见性,接下来我们了解一下什么是禁止指令重排序~~
什么是“单例模式”?
单例模式是设计模式的一种,对于一个类,其对外只实例化一个对象;
必要点:
有如下几种实现方式:
特点:线程安全,调用效率高,但是不能延时加载。
一上来在类加载的时候就把单例对象创建出来了,要用的时候直接返回即可,这种可以说是单例模式中最简单的一种实现方式。但是问题也比较明显。单例在还没有使用到的时候,初始化就已经完成了。也就是说,如果程序从头到位都没用使用这个单例的话,单例的对象还是会创建。这就造成了不必要的资源浪费。是一种“以空间换时间”的实现方式。而且由于只实例化一次,所以不存在什么并发问题,线程安全。
public class SingletonDemo{
//本类内部创建对象实例
private static SingletonDemo instance = new SingletonDemo;
//私有化构造方法
private SingletonDemo(){}
//提供一个公有的静态方法,返回实例对象
public static SingletonDemo getInstance(){
return instance;
}
}
特点:线程安全,调用效率不高,但是能延时加载。
类加载时,先不着急初始化对象,等到要去对获取对象实例时,才进行初始化操作,并且先判断存储实例的变量是否有值,若果没有,就创建一个对象实例,并把值赋值给存储实例的变量;如果此时存储实例的变量已经有值,那就直接使用。
public class Singleton {
//本类内部创建对象实例
private static Singleton instance = null;
//构造方法私有化,外部不能new
private Singleton() {
}
//提供一个公有的静态方法,返回实例对象
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
懒汉式体现了缓存的思想,延时加载就是一开始不要加载资源或者数据,一直等,等到马上就要使用这个资源的或者数据了,躲不过去了才去加载。懒汉式是典型的的“时间换空间”,不加同步的懒汉式是线程不安全的。
public class SingletonDemo2 {
//类初始化时,不初始化这个对象(延时加载,真正用的时候再创建)
private static SingletonDemo2 instance;
//构造器私有化
private SingletonDemo2(){}
//方法同步,调用效率低
public static synchronized SingletonDemo2 getInstance(){
if(instance==null){
instance=new SingletonDemo2();
}
return instance;
}
}
上面的代码使用的静态同步方法进行了优化后,虽然能够保证线程安全,但是在代码执行效率上性能低下,因为当对象初始化后,如果有多个线程同时操作,那么会多次进行多次调用getInstance()方法,使执行效率变低下。
双重校验锁机制
public class Singleton {
private static Singleton instance = null;
// 私有化构造方法
private Singleton() {
}
public static Singleton getInstance() {
1 if (instance == null) {
2 synchronized (Singleton.class) {
3 if (instance == null) {
4 instance = new Singleton();
}
5 }
}
6 return instance;
}
}
上面这段代码使用了“双重校验”,具体体现在用了两个 if 判断,并且在两个 if 判断之间加了锁机制!!!
结合上面代码,我们探讨一下”双重校验:背后的理论是:
在 //2 处的第二次检查使(如清单 3 中那样)创建两个不同的 Singleton 对象成为不可能。假设有下列事件序列:
- 线程 1 进入 getInstance() 方法;
- 由于//1处判断 instance 为 null,线程 1 在 //2 处进入 synchronized 块;
- 线程 1 被线程 2 预占;
- 线程 2 进入 getInstance() 方法;
- 由于 instance 仍旧为 null,线程 2 试图获取 //2 处的锁。然而,由于线程 1 此时持有该锁,线程 2 在 //2处阻塞;
- 线程 2 被线程 1 预占;
- 线程 1 执行,由于在 //3 处instance仍旧为 null,线程 1 便创建一个 Singleton 对象并将其引用赋值给 instance;
- 线程 1 退出 synchronized 块并从 getInstance() 方法返回实例;
- 线程 1 被线程 2 预占;
- 线程 2 获取 //2 处的锁并检查 instance 是否为 null;
- 由于 instance 是非 null 的,并没有创建第二个 Singleton 对象,将 线程 1 创建的对象被返回。
双重检查锁定背后的理论是完美的。不幸地是,现实完全不同。双重检查锁定的问题是:并不能保证它会在单处理器或多处理器计算机上顺利运行。
双重检查锁定失败的问题并不归咎于 JVM 中的实现 bug,而是归咎于 Java 平台内存模型。内存模型允许所谓的“无序写入”,这也是这些程序运行失败的一个主要原因。
我们再看一个线程序列,理解一下“无序写入”对线程安全造成的影响:
首先,我们先再认识一下“无序写入”,之前我们说了,JVM为了使运行效率提高会将代码的执行顺序打乱。这个时候我们要对“4 instance = new Singleton();”这句代码解读一下,这句代码是没有原子性的,它按顺序执行下来需要分成三个步骤:4.1 分配对象内存空间;4.2 初始化对象;4.3将值赋给引用。
现在,如果对于//4这句代码中的三个执行操作的顺序被打乱了,变成了4.1->4.3->4.2这样的顺序~~
且看下面的线程序列:
- 线程 1 进入 getInstance() 方法;
- 由于此时 instance 为 null,线程 1 在 //2 处进入 synchronized 块;
- 线程 1 前进到 //4 处,先后执行了4.1和4.3操作,即此时引用指向一个已经分配好的内存空间,但对象还没有初始化;
- 若此时,线程 1 被线程 2 预占;
- 线程 2 检查实例是否为 null,因为实例不为 null,线程 2 将 instance 引用返回给一个构造完整但部分初始化了的 Singleton对象。
- 这个时候线程 2 中如果调用对象的属性和方法就会报错,因为4.2操作并没有进行,对象初始化并不完整。
上面这个执行序列我们可以看出,指令重排序对于线程安全有着重要影响!!!所以我们要建立内存屏障,禁止指令重排序,这儿就需要用到“ volatile”关键字。
public class Singleton {
private volatile static Singleton instance = null;
// 私有化构造方法
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
用volatile修饰共享变量instance,对于“4 instance = new Singleton()”这句代码,它按顺序执行下来需要分成三个步骤:4.1 分配对象内存空间;4.2 初始化对象;4.3将值赋给引用。其中操作4.2是初始化创建了这个volatile修饰的对象,也就是说volatile对这个操作建立了内存屏障,该操作前的指令不能排序道屏障后面,该操作后面的指令不能排序到屏障前面。这样一来,我们就保障了操作的有序性,从而保障了线程的安全性!!!
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题 (体现在生产者和消费者都是同时操作一个数据,如库存数量)。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
public class MyBlockingQueue<E> {
private Object[] elements;
private int putIndex;//存放元素的索引
private int takeIndex;//取元素的索引
private int size;//存放元素的数量
public MyBlockingQueue(int capacity){
elements = new Object[capacity];
}
//线程安全的存放元素:如果超过最大容量,需要等待,否则就存放
public synchronized void put(E e) throws InterruptedException {
while(size==elements.length)
wait();
elements[putIndex] = e;
putIndex = (putIndex+1) % elements.length;
size++;
notifyAll();
}
//线程安全的取元素:如果队列中没有元素,需要等待,否则就可以取
public synchronized E take() throws InterruptedException {
while (size==0)
wait();
E e = (E) elements[takeIndex];
takeIndex = (takeIndex+1) % elements.length;
size--;
notifyAll();
return e;
}
public static void main(String[] args) {
MyBlockingQueue<Integer> queue = new MyBlockingQueue<>(10);
new Thread(new Runnable() {
@Override
public void run() {
try {
while (true) {
queue.put(3);
Thread.sleep(3000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
while(true) {
Integer n = queue.take();
System.out.println("消费");
Thread.sleep(300);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
线程池其实就是一种多线程处理形式,处理过程中可以将任务添加到队列中,然后在创建线程后自动启动这些任务。这里的线程就是我们前面学过的线程,这里的任务就是我们前面学过的实现了Runnable或Callable接口的实例对象。
面向对象编程中,对象创建和销毁是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是对一些很耗资源的对象创建和销毁。如何利用已有对象来服务就是一个需要解决的关键问题,其实这就是一些”池化资源”技术产生的原因。
多线程技术主要用于解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。但如果对多线程应用不当,会增加对单个任务的处理时间。
举一个简单的例子:
假设一台服务器完成一项任务的时间为T,其中, T1 创建线程的时间,T2 在线程中执行任务的时间,包括线程间同步所需时间,T3线程销毁的时间。
显然T = T1+T2+T3。注意这是一个极度简化的假设。可以看出T1,T3是多线程本身附加的开销,用户希望减少T1,T3所用的时间,从而减少T的时间。但一些线程的使用者并没有注意到这一点,所以在应用程序中频繁的创建或销毁线程,这导致T1和T3在T中占有非常大的比例。
线程池技术正是关注如何缩短或调整T1,T3时间的技术,从而提高服务器程序性能的。它把T1、T3分别安排在服务器程序的启动和结束的时间段或者一些空闲的时间段,这样在服务器程序处理客户请求时,不会有T1、T3的开销了,线程池不仅调整T1、T3产生的时间,而且它还显著减少了创建线程的数目。
再看一个例子:
假设一台服务器每天大约要处理100000个请求,并且每个请求需要一个单独的线程完成,这是一个很常用的场景。在线程池中,线程数量一般是固定的,所以产生线程总数不会超过线程池中线程的数目或者上限,而如果服务器不利用线程池来处理这些请求则线程总数为100000。一般线程池尺寸是远小于100000。所以利用线程池的服务器程序不会为了创建100000而在处理请求时浪费时间,从而提高效率。
线程池是一种多线程处理方法,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程,每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。如果某个线程处于空闲状态,则线程池将会调度一个任务给它,如果所有线程都始终保持繁忙,但将任务放入到一个队列中,则线程池将在一段时间后创建另一个辅助线程,但线程的数目永远不会超过最大值。超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。
需要大量的线程来完成任务,且完成任务的时间比较短。 如WEB服务器完成网页请求这样的任务。因为单个任务小,而任务数量巨大,比如一个热门网站的点击次数。 但对于长时间的任务,比如一个ftp连接请求,线程池的优点就不明显了,因为ftp会话时间相对于线程的创建时间长多了。
对性能要求苛刻的应用,比如要求服务器迅速相应客户请求。
接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。 突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限。如网购商品秒杀,12306购票系统等。
(1)降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
(2)提高响应速度。 由于线程池维护了一批 alive 状态的线程,当任务到达时,不需要再创建线程,而是直接由这些线程去执行任务,从而减少了任务的等待时间。
(3)提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资 源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
如下图:
从Java线程池Executor框架体系可以知道:线程池的真正实现类是ThreadPoolExecutor类,因此我们接下来重点研究这个类。在ThreadPoolExecutor类中有4个构造函数,最终调用的是如下函数:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
);
构造函数一共有7个参数(前5个参数必须存在,第6和第7个参数是非必须存在),如下:
corePoolSize
线程池中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize;
如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;
如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。
当线程数小于等于corePoolSize时,默认情况下线程会一直存活在线程池中,即时线程处于空闲状态。如果allowCoreThreadTimeOut被设置为true时,无论线程数多少,那么线程处于空闲状态超过一定时间就会被销毁掉。
maximumPoolSize
线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize;
keepAliveTime
线程空闲时的存活时间,即当线程没有任务执行时,继续存活的时间;默认情况下,该参数只在线程数大于corePoolSize时才有用;如果allowCoreThreadTimeOut被设置为true时,无论线程数多少,线程处于空闲状态超过一定时间就会被销毁掉。
unit
keepAliveTime的单位。TimeUnit是一个枚举类型,其包括:
NANOSECONDS :1微毫秒 = 1微秒 / 1000;
MICROSECONDS :1微秒 = 1毫秒 / 1000;
MILLISECONDS :1毫秒 = 1秒 /1000;
SECONDS :秒;
MINUTES :分;
HOURS :小时;
DAYS :天。
workQueue
用来保存等待被执行的任务的阻塞队列,且任务必须实现Runable接口,如下阻塞队列:
ArrayBlockingQueue:
基于数组结构的有界阻塞队列,按FIFO排序任务;
LinkedBlockingQuene:
基于链表结构的无界阻塞队列,按FIFO排序任务,吞吐量通常要高于ArrayBlockingQuene;
SynchronousQuene:
一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene;
threadFactory
创建线程的工厂,通过自定义的线程工厂可以给每个新建的线程设置一个具有识别度的线程名,也就是指定为线程池创建新线程的方式。
handler
线程池的拒绝策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:
AbortPolicy:直接抛出异常,默认策略;
CallerRunsPolicy:用调用者所在的线程来执行任务;
DiscardOldestPolicy:丢弃阻塞队列中被阻塞时间最长的任务,并执行当前任务;
DiscardPolicy:直接丢弃任务。
在Executors类中,为我们提供了常用线程池的创建方法。接下来我们就来了解常用的四种线程池的创建方法:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1,
1,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
从构造方法可以看出,它创建了一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。
有人可能会质疑:既然类似于单线程执行,那么这种线程池还有存在的必要吗?
需要强调的是:这里的单线程执行指的是线程池内部,从线程池外的角度看,主线程在提交任务到线程池时并没有阻塞,仍然是异步的。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads,
nThreads,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
从构造方法可以看出,它创建了一个固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大值nThreads。线程池的大小一旦达到最大值后,再有新的任务提交时则放入无界阻塞队列中,等到有线程空闲时,再从队列中取出任务继续执行。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0,
Integer.MAX_VALUE,
60L,
TimeUnit.SE没有核心线程,普通线程数量为Integer.MAX_VALUE(可以理解为无限)CONDS,
new SynchronousQueue<Runnable>());
}
从构造方法可以看出,它创建了一个可缓存的线程池,没有核心线程,最大线程数量为Integer.MAX_VALUE(可以理解为无限)。当有新的任务提交时,有空闲线程则直接处理任务,没有空闲线程则创建新的线程处理任务,队列中不储存任务。线程池不对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。如果线程空闲时间超过了60秒就会被回收。适用于任务量大但耗时低的场景。
private static final long DEFAULT_KEEPALIVE_MILLIS = 10L;
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}
public static ScheduledExecutorService newScheduledThreadPool(
int corePoolSize, ThreadFactory threadFactory) {
return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}
public ScheduledThreadPoolExecutor(int corePoolSize,ThreadFactory threadFactory) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue(), threadFactory);
}
这个方法创建了一个固定大小的线程池,支持定时及周期性任务执行。指定核心线程数量,普通线程数量无限,线程执行完任务立即回收,任务队列为延时阻塞队列。这是一个比较特别的线程池,适用于执行定时或周期性的任务。
Executors 的 4 个功能线程池虽然方便,但现在已经不建议使用了,而是建议直接通过使用 new ThreadPoolExecutor 的方式,这样的处理方式让编写的人更加明确线程池的运行规则,规避资源耗尽的风险。
参考代码:
public class MyThreadPool {
private MyBlockingQueue<Runnable> queue = new MyBlockingQueue<>(100);
//传入的核心线程数来创建工作线程,创建线程池就启动
public MyThreadPool(int coreSize){
for (int i = 0; i < coreSize; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
while (true){
Runnable task = queue.take();//从仓库取任务
task.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
public void execute(Runnable task){
try {
queue.put(task);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
MyThreadPool pool = new MyThreadPool(4);
for (int i = 0; i < 10; i++) {
pool.execute(new Runnable() {
@Override
public void run() {
try {
System.out.println(1);
Thread.sleep(99999999);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
}
其实 Executors 的 4 个功能线程有如下弊端:
ThreadPoolExecutor提供了两个方法,用于线程池的关闭:
原文链接:https://blog.csdn.net/qq_37453637/article/details/117669262
作者:小泽圈儿郎
链接:http://www.javaheidong.com/blog/article/222742/bd7ec7c181c43a15df45/
来源:java黑洞网
任何形式的转载都请注明出处,如有侵权 一经发现 必将追究其法律责任
昵称:
评论内容:(最多支持255个字符)
---无人问津也好,技不如人也罢,你都要试着安静下来,去做自己该做的事,而不是让内心的烦躁、焦虑,坏掉你本来就不多的热情和定力
Copyright © 2018-2021 java黑洞网 All Rights Reserved 版权所有,并保留所有权利。京ICP备18063182号-2
投诉与举报,广告合作请联系vgs_info@163.com或QQ3083709327
免责声明:网站文章均由用户上传,仅供读者学习交流使用,禁止用做商业用途。若文章涉及色情,反动,侵权等违法信息,请向我们举报,一经核实我们会立即删除!