简栈

拥抱AI,持续成长

Java中线程的状态分为6种。

  1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
  2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
    线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
  3. 阻塞(BLOCKED):表示线程阻塞于锁。
  4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
  5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
  6. 终止(TERMINATED):表示该线程已经执行完毕。

这6种状态定义在Thread类的State枚举中,可查看源码进行一一对应。

线程的状态图

http://static.cyblogs.com/20181120173640764.jpg

初始状态

实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态。

就绪状态

就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态。
调用线程的start()方法,此线程进入就绪状态。
当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。
当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。
锁池里的线程拿到对象锁后,进入就绪状态。

运行中状态
阅读全文 »

简介

ReentrantLocksynchronized都是提供了同步的功能,JDK1.6之后对synchronized性能进行了优化,所以两者的性能上几乎没什么区别,但是ReentrantLock提供了了一些高级功能。

  • 等待可中断:在synchronized中,如果一个线程在等待锁,他只用两种结果,要么获得锁执行完,要么一直保持等待。可中断的等待是通知正在等待的线程,告诉他没必要再等待后。

  • 实现公平锁:公平锁:会按照时间的先后顺序,保证先到先得。特点是它不会产生饥饿现象。而synchroized关键字进行所控制时,锁是非公平的。而重入锁可以设置为公平锁。 public ReetranLock(boolean fair)fairtrue时,表示锁是公平的。实现公平锁必然要求系统维护一个有序队列,因此公平锁的成本比较高,性能也非常低向。默认情况下锁是非公平的。

  • 绑定多个条件:类似于Object类的waitnotify方法,它是与ReentrantLock绑定的条件,可以绑定多个条件。

一个简单的例子

注意:退出临界区要释放锁,否则其他线程就没有机回访问临界区了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class TestReentrantLock implements Runnable {
public static ReentrantLock rlock = new ReentrantLock();
public static int i=0;
@Override
public void run(){
for(int j=0;j<1000000;j++){
rlock.lock();
try{
i++;
}finally {
rlock.unlock();
}
}
}

public static void main(String args[]) throws InterruptedException {
TestReentrantLock tl = new TestReentrantLock();
Thread t1 = new Thread(tl);
Thread t2 = new Thread(tl);
t1.start();
t2.start();
//表示当前线程等待t1执行完
t1.join();
t2.join();
System.out.println(i);
}
}

注意:退出临界区要释放锁,否则其他线程就没有机回访问临界区了。

Lock接口

Lock接口是JDK1.5新加的同步工具接口,它的实现类有ReentrantLockWriteLock等,接口中定义了通用的方法:

1
2
3
4
5
6
7
8
9
10
11
12
void lock();
void unlock();
// 可中断获取锁,与lock()不同之处在于可响应中断操作,即在获取锁的过程中可中断
// synchronized在获取锁时是不可中断的
void lockInterruptibly() throws InterruptedException;
//尝试非阻塞获取锁,调用该方法后立即返回结果,如果能够获取则返回true,否则返回false
boolean tryLock();
//根据传入的时间段获取锁,在指定时间内没有获取锁则返回false,如果在指定时间内当前线程未被中并断获取到锁则返回true
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//获取等待通知组件,该组件与当前锁绑定,当前线程只有获得了锁
//才能调用该组件的wait()方法,而调用后,当前线程将释放锁。
Condition newCondition();
阅读全文 »

前言

任务和线程的启动很容易。在大多数时候,我们都会让它们运行直到结束,或者让它们自行停止。然而,有时候我们希望提前结束任务或线程,或许是因为用户取消了操作,或者应用程序需要被快速关闭。

要使任务和线程能安全、快速、可靠地停止下来,并不是一件容易的事。Java 没有提供任何机制来安全的终止线程。但它提供了中断,这是一种协作机制,能够使一个线程终止另一个线程的当前工作。

这种协作式的方法是必要的,我们很少希望某个任务、线程或服务立即停止,因为这种立即停止会使共享的数据结构处于不一致的状态。相反,在编写任务和服务时可以使用一种协作的方式:当需要停止时,它们首先会清除当前正在执行的工作,然后再结束。这提供了更好的灵活性,因为任务本身的代码比发出取消请求的代码更清除如何执行清除工作。

正题

在开始文章前,有几个问题需要思考一下:

  • 取消任务的方式由哪几种?
  • 中断的策略是什么?
  • 如何响应中断?
取消任务的方式有哪几种

取消任务的方式大体上有一下两种:

  • 设置取消标志位
  • 中断
阅读全文 »

1、G1垃圾收集器介绍

G1垃圾收集器针对具有大量内存的多处理器机器。它试图以很高的概率满足GC停顿时间目标,同时实现高吞吐量且几乎不需要配置。G1旨在在延迟和吞吐量之间提供最佳平衡,应用场景包括如下环境特征:

  • 堆大小可达10 GB或更大,超过50%的Java堆占用实时数据。
  • 随着时间的推移,对象分配速度和晋升(从新生代到老年代的晋升)速度会发生显著变化。
  • 堆中大量的碎片。
  • 可预测的时间停顿目标不超过几百毫秒,避免长时间垃圾收集停顿。

G1取了CMS,G1也是默认的收集器(JVM9、JVM10)。

G1收集器有很高的性能,并尝试通过以下几节所述的几种方式来满足停顿时间的目标。

2、启用G1收集器

G1是默认收集器,因此通常不需要执行任何其他操作。您可以通过在命令行上提供-XX:+ UseG1GC来显式启用它。

3、基本概念

G1是分代的、增量的、并行、大部分、并发的、stop-the-word、以及疏散(将活着的对象从一个区域(youngor young + old)拷贝到另一个区域)的垃圾收集器,用于监视每个stop-the-word停顿的停顿时间目标。与其他收集器类似,G1将堆分成(虚拟)新生代和老年代。空间回收的主要集中在年轻代,因为这样做最有效率,在老年代偶尔会有空间回收。

一些操作总是在stop-the-word停顿中执行以提高吞吐量。其他需要更多时间停止应用程序的操作(例如全局标记等全堆操作)将与应用程序同时并行并发执行。为了让空间回收stop-the-word停顿短,G1逐步地并行执行空间回收。G1通过跟踪以前的应用程序行为信息和垃圾收集停顿的信息来建立相关成本的模型,以此实现可预测性。它使用这些信息来调整在停顿中完成的工作。例如,G1首先在最有效率的区域回收空间(即大部分填充垃圾的区域,因此是名称)。

阅读全文 »

前言

BlockingQueue即阻塞队列,它算是一种将ReentrantLock用得非常精彩的一种表现,依据它的基本原理,我们可以实现Web中的长连接聊天功能,当然其最常用的还是用于实现生产者与消费者模式,大致如下图所示:

http://static.cyblogs.com/20161108212521456.png

在Java中,BlockingQueue是一个接口,它的实现类有ArrayBlockingQueueDelayQueueLinkedBlockingDequeLinkedBlockingQueuePriorityBlockingQueueSynchronousQueue等,它们的区别主要体现在存储结构上或对元素操作上的不同,但是对于take与put操作的原理,却是类似的。下面的源码以ArrayBlockingQueue为例。

分析

BlockingQueue内部有一个ReentrantLock,其生成了两个Condition,在ArrayBlockingQueue的属性声明中可以看见:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/** Main lock guarding all access */
final ReentrantLock lock;
/** Condition for waiting takes */
private final Condition notEmpty;
/** Condition for waiting puts */
private final Condition notFull;

...

public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}

而如果能把notEmptynotFullput线程、take线程拟人的话,那么我想puttake操作可能会是下面这种流程:

put(e)

http://static.cyblogs.com/20161108212418173.png

阅读全文 »

今天学习下ArrayList的源代码,不同于其他人写的博客,很多都是翻译源代码中的注释,然后直接贴到文章中去。小编打算换一种书写风格,带着问题看源码可能收获会更大,本文将围绕着下面几个问题展开讨论。

一、问题产生

  • 1、为什么ArrayList集合中存储元素的容器声明为transient Object[] elementData;
  • 2、既然ArrayList可以自动扩容,那么它的扩容机制是怎样实现的?
  • 3、调用ArrayListiterator()返回的迭代器是怎样的?
  • 4、采用ArrayList的迭代器遍历集合时,对集合执行相关修改操作时为什么会抛出ConcurrentModificationException,我们该如何避免?
  • 5、当集合扩容或者克隆时免不了对集合进行拷贝操作,那么ArrayList的数组拷贝是怎么实现的?
  • 6、ArrayList中的序列化机制

小编对ArrayList源码大概浏览了之后,总结出以上几个问题,带着这些问题,让我们一起翻开源码解决吧!

二、问题解答

1、为什么ArrayList集合中存储元素的容器声明为transient Object[] elementData;

ArrayList是一个集合容器,既然是一个容器,那么肯定需要存储某些东西,既然需要存储某些东西,那总得有一个存储的地方吧!就好比说你需要装一吨的水,总得有个池子给你装吧!或者说你想装几十毫升水,总得那个瓶子或者袋子给你装吧!区别就在于不同大小的水,我们需要的容器大小也不相同而已!

既然ArrayList已经支持泛型了,那么为什么ArrayList源码的容器定义为什么还要定义成下面的Object[]类型呢?

1
transient Object[] elementData;

其实无论你采用transient E[] elementData;的方式声明,或者是采用transient Object[] elementData;声明,都是允许的,差别在于前者要求我们我们在具体实例化elementData时需要做一次类型转换,而这次类型转换要求我们程序员保证这种转换不会出现任何错误。为了提醒程序员关注可能出现的类型转换异常,编译器会发出一个Type safety: Unchecked cast from String[] to E[]警告,这样讲不知道会不会很懵比,下面的代码告诉你:

阅读全文 »

也许大家对这个问题都不陌生,实际装过系统用过电脑的朋友可能都有这样的经历:自己电脑配的是4G的内存条,可是装完系统之后发现电脑上显示的只有3.2G左右可用内存,其它的内存跑到哪去了?网上也有很多朋友给出了一些解释,大部分我觉得都没有解释得很清楚,今天我们就来看一下其中的具体缘由。

在此之前先来了解一些计算机系统结构和PC硬件方面的一些知识。

总线结构和主板的构成

  说起总线大家肯定不陌生,而且大家平时肯定跟它打过交道,我们在用U盘拷贝数据的时候先要把U盘通过USB接口与电脑相连才能拷贝。USB接口实际上就是一种总线,一般称这种总线为USB总线(也叫做通用串行总线)。在很久之前是没有USB总线的,那个时候每个外设各自采用自己的接口标准,举个最简单的例子:鼠标生产厂商采用鼠标特有的接口,键盘生产厂商用键盘特有的接口,这样一来的话,PC机上就必须提供很多接口,这样一来增加了硬件设计难度和成本,直到后来USB接口的出现,它统一了很多外设接口的标准,不仅使得用户可以很方便地连接一些外设,更增强了PC的可扩展性。所以现在大家看到的鼠标、键盘、U盘、打印机等等这些外设都可以直接通过USB接口直接插到电脑上的。

  在计算机系统中总线是非常重要的一个概念,正是因为有了总线,所有的组成部件才能一起正常协同分工合作。在很久以前的PC机中,采用的是三总线结构,即:数据总线、地址总线、控制总线。它们分别用来传输不同类型的数据,数据总线用来传输数据,地址总线用来传输地址,控制总线用来传输一些控制信号。下面这幅图很清楚地展示了三总线结构:

  img

随着时代的发展,这种简单的总线结构逐渐被淘汰。下面这幅图是现代计算采用的结构:

  img

  事实上这也是现代主板所采用的结构,当然可能部分地方有略微不同(大体结构是差不多的),仔细观察过主板构成的朋友可能对上面一幅图很熟悉。在主板上主要有两大主要部分:北桥(North Bridge也称Host Bridge)和南桥(South Bridge)。北桥主要负责CPU和内存、显卡这些部件的数据传送,而南桥主要负责I/O设备、外部存储设备以及BIOS之间的通信。现在有些主板已经没有北桥了,因为芯片厂商已经把北桥所负责的功能直接集成到CPU中了(不过暂且我们以上副图的模型来讨论)。

  在上副图中,我没有画出 数据总线和地址总线等,因为在某些总线标准中它们被集成到一起了,比如在PCI总线中,地址总线和数据总线总是分时复用的(也就是说假如PCI总线有32位数据总线,这32位总线在某个时刻可以充当数据总线的作用,在下一时刻可以充当地址总线的作用)。有的总线同时提供了数据总线和地址总线。

阅读全文 »

堆外内存的优势在于IO操作,相比堆内存可以减少一次copy和gc的次数。下面通过源码去了解堆外内存的分配和回收。一般分配堆外内存通过ByteBuffer allocateDirect(int capacity)方法,其内部是通过如下构造函数来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
DirectByteBuffer(int cap) {               
super(-1, 0, cap, cap);// mark, pos, lim, cap
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);
long base = 0;
try {
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) { // 修改内存起始地址
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}

首先调用父类的构造方法初始化ByteBuffer的四个基本属性,接下来reserveMemory方法是判断堆外剩余内存是否满足。这里的剩余并不是系统真是的剩余内存,参数-XX:MaxDirectMemorySize指定JVM最多可用的堆外内存。

如果堆外内存不足,则触发System.gc,这里有些难已理解,明明是堆外内存不足,System.gc的作用是建议VM进行full gc,再怎么说也是堆内存的回收。这里先保留这个疑问,继续往下看。

根据VM参数判断是否内存页对齐计算真实分配内存的大小,由-XX:+PageAlignDirectMemory控制,默认为false。allocateMemory是真正分配内存如果失败则回收内存。setMemory为填充内存。

接下来根据是否内存页对齐来计算内存的起始地址。我们知道HeapByteBuffer是基于byte数组来实现,不需要我们去考虑回收由JVM去处理。但是堆外内存JVM无法想堆内存那样回收,因此就有了Cleaner和Deallocator的存在。

每一个DirectBytebuffer都对应一个Deallocator和Cleaner对象,而Deallocator是Cleaner的一个属性。Deallocator继承了Runnable接口,当然run方法内部是释放内存的逻辑。

1
2
3
4
5
6
7
public void run() {
// 释放
unsafe.freeMemory(address);
address = 0;
// 修改堆外内存的占用大小
Bits.unreserveMemory(size, capacity);
}

在分析Cleaner之前我们先复习下PhantomReference(虚引用)

虚引用,正如其名,对一个对象而言,这个引用形同虚设,有和没有一样。如果一个对象与GC Roots之间仅存在虚引用,则称这个对象为虚可达(phantom reachable)对象。
当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在垃圾回收后,将这个虚引用加入引用队列,可以通过检查引用队列中是否有相应的虚引用来判断对象是否已经被回收了。

阅读全文 »

Feign简介

​ 在上一篇文章中分析了Eureka的注册、续约、服务剔除、服务自我保护等机制,地址在https://blog.csdn.net/lgq2626/article/details/80288992 。这篇分析SpringCloudfeignSpringCloud微服务项目之间调用是通过httprest请求来进行服务调用的,之前我们会用到HttpClient等工具来进行服务请求,Spring对这种请求进行了处理,封装成了可声明式的web客户端,使得编写web客户端更容易,feign还支持可插拔的编码器和解码器,Spring在用的时候增加了对@requestMapping的处理,同时,SpringCloud还对feign集成了注册中心(eureka)和客户端负载均衡(ribbon),使得我们拥有一个客户端负载均衡的web请求客户端。

Feign在项目中的配置和使用

​ 在Springcloud中使用feign的时候,需要在配置类中加入一个@EnableFeignClients注解。代码如下:

1
2
3
4
5
6
7
8
@SpringBootApplication//springboot 启动类
@EnableFeignClients//开启eureka扫描
@EnableDiscoveryClient//开启eureka客户端
public class Application {
public static void main( String[] args ) throws ClassNotFoundException {
SpringApplication.run(Application.class, args);
}
}

配置feign调用客户端

1
2
3
4
5
@FeignClient(value = "xxx-server",configuration = FeignConfiguration.class)
public interface ConsumerSmsService extends SMSService{
@RequestMapping(value = "/sms/smsMessage", method = RequestMethod.POST)
RespSMSDto sendSms(ReqSMSDto smsReqDto);
}

经过上面的配置,直接在项目里面注入容器调用接口就可以了。

Feign源码分析

​ 在@EnableFeignClients标签中,import了一个FeignClientsRegistrar类,那么这个FeignClientsRegistrar#registerBeanDefinitions()在什么时候调用的呢?跟着Spring的源码走下去,看过源码的人都会直接看到AbstractApplicationContext#refresh()方法,整体整理一下代码:

阅读全文 »

前文已经讲过,CMS是老年代垃圾收集器,在收集过程中可以与用户线程并发操作。它可以与Serial收集器和Parallel New收集器搭配使用。CMS牺牲了系统的吞吐量来追求收集速度,适合追求垃圾收集速度的服务器上。

CMS相关参数

参数 类型 默认值 作用
-XX:+UseConcMarkSweepGC boolean false 老年代采用CMS收集器收集
–XX:ParallelGCThreads=n int (ncpus <= 8) ? ncpus : 3 + ((ncpus * 5) / 8) 老年代采用CMS收集器收集
-XX:CMSInitiatingOccupancyFraction int 92 年代堆空间的使用率。比如value=75意味着第一次CMS垃圾收集会在老年代被占用75%时被触发。
-XX:+UseCMSInitiatingOccupancyOnly boolean false 只用设定的回收阈值(上面指定的70%),如果不指定,JVM仅在第一次使用设定值,后续则自动调整

触发条件

周期性GC

由后台线程ConcurrentMarkSweepThread循环判断(默认2s)是否需要触发。

如果没有设置-XX:+UseCMSInitiatingOccupancyOnly,虚拟机会根据收集的数据决定是否触发
老年代使用率达到阈值 CMSInitiatingOccupancyFraction,默认92%。
永久代的使用率达到阈值 CMSInitiatingPermOccupancyFraction,默认92%,前提是开启 CMSClassUnloadingEnabled
新生代的晋升担保失败。

主动触发

YGC过程发生Promotion Failed,进而对老年代进行回收
比如执行了System.gc(),前提是没有参数ExplicitGCInvokesConcurrent

收集过程

阅读全文 »
0%