分类目录归档:Java语言

ThreadLocal 原理探究

ThreadLocal 的作用:

This class provides thread-local variables. 提供了线程级别的本地变量。

ThreadLocal的原理:

  1. 每一个线程Thread内部维护了两个package private级别的变量:
    • ThreadLocal.ThreadLocalMap threadLocals
    • ThreadLocal.ThreadLocalMap inheritableThreadLocals
  2. 每一个 ThreadLocal 对象有一个创建时生成唯一的 Id称为:threadLocalHashCode,访问一个 ThreadLocal 变量的值,就是用threadLocalHashCode去 本线程 的 ThreadLocalMap 中查找对应的值。
    private Entry getEntry(ThreadLocal<?> key) {
     int i = key.threadLocalHashCode 
     & (table.length - 1);
     Entry e = table[i];
     if (e != null && e.get() == key)
         return e;
     else
         return getEntryAfterMiss(key, i, e);
    }
  3. ThreadLocalMap 为一个静态内部类,包含了一个Entity数组(Entry[] table)来保持ThreadLocal的线程变量;

继续阅读

Advertisements

Task混用ThreadPool导致无限等待

现象:

生产环境商品打标异步任务提交任务后,任务没有被执行;查看日志,没有异常日志抛出。

初步猜测:

可能是队列出现了饱和或者死锁,但是如果出现了饱和,我们设置的线程池设置的饱和策略是通过主线程去执行,为什么主线程也没有执行呢?

具体分析:

我定义了一个线程池Pool-Z,core_size=5,max_size=20,queue_size=1000,第一个任务A提交后,占用一个线程,那么这个任务A又会被分解成多个异步任务去执行,比如分解为A1、A2、A3(这些子任务为耗时任务),这些异步任务使用了同一个线程池Pool-Z提供线程,但是任务A内部做了一个事情,通过CountDownLatch.await实现了当所有A的子任务都执行完毕后,才执行后续的一个扫尾工作,也就是说线程A会等待同一个线程池中的A1、A2、A3执行完毕后才能释放出线程A的资源;如果没有其它的主任务B或者C加进来,这些任务总会被执行完毕;但是如果主任务提交非常频繁,比如连续提交了几个包含非常多子任务的B、C、D进来,此时这样的不敢具体子任务只是等待子任务完成的主任务就占用了4个主要线程,只有一个子任务在执行工作,由于不会分配新的线程来处理子任务,所以B、C、D的子任务都会被扔到队列里面,知道队列满掉,当队列慢了后,会开辟新的线程来处理任务,但是我们知道由于很多主任务提交进来都处于等待子任务释放而处于等待状态,如果当某一刻20个核心线程都成为了主任务且都在等子任务执行完毕的时候,子任务就得不到线程,锁等待就可能发生了,因为当时配置的任务队列比较大,为3000,所以当时加入的任务都被加入到了队列中,由于总数没有达到3000个,所以没有触发线程池的饱和策略。

解决方案:

  • 方案一是去掉锁的使用,且需要保证相同业务含义;实现较困难;
  • 方案二把两个线程池拆开,不共用队列,避免了相互等待而产生死循环;

总结:

  • 尽量避免线程之间的依赖;处理“当子任务都完成后再执行主任务”的场景最容易产生这样的线程依赖问题;
  • 尽量避免使用wait、lock等操作;如果必须使用也切记加入超时时间;
  • 对线程池的使用时最好自定义线程池的名称,方便拍错和定位问题;
  • 如果需要使用内部类且内部类不需要应用外部类的属性,尽可能把内部类设置为static形态;

继续阅读

CAS 操作的实现原理

CAS原子操作原理:

(1)在x86 平台上,CPU提供了在指令执行期间对总线加锁的手段(多处理器)

CPU芯片上有一条引线#HLOCK pin,如果汇编语言的程序中在一条指令前面加上前缀”LOCK”,经过汇编以后的机器代码就使CPU在执行这条指令的时候把#HLOCK pin的电位拉低,持续到这条指令结束时放开,从而把总线锁住,这样同一总线上别的CPU就暂时不能通过总线访问内存了,保证了这条指令在多处理器环境中的原子性。
inline bool CAS2(pointer_t *addr, pointer_t &old_value, pointer_t &new_value)
{
bool ret;
__asm__ __volatile__(
"lock cmpxchg16b %1;n"
"sete %0;n"
:"=m"(ret),"+m" (*(volatile pointer_t *) (addr))
:"a" (old_value.ptr), "d" (old_value.tag), "b" (new_value.ptr), "c" (new_value.tag));
return ret;
}
解锁来保证原子性只是保证原子性的其中一种手段而已,X86CPU保证原子性的手段有以下三种:
① 处理器自动保证基本内存操作的原子性。
一些基本的内存读写操作是本身已经被硬件提供了原子性保证(例如读写单个字节的操作);
②使用总线锁保证原子性。
一些需要保证原子性但是没有被第①条机制提供支持的操作(例如read-modify-write)可以通过使用”LOCK#”来锁定总线,从而保证操作的原子性。
③使用缓存锁保证原子性。
总线锁定把CPU和内存之间通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,最近的处理器在某些场合下使用缓存锁定代替总线锁定来进行优化。
因为很多内存数据是已经存放在L1/L2 cache中了,对这些数据的原子操作只需要与本地的cache打交道,而不需要与总线打交道,所以CPU就提供了cache coherency机制来保证其它的那些也cache了这些数据的processor能读到最新的值。
所谓“缓存锁定”就是如果缓存在处理器缓存行中内存区域在LOCK操作期间被锁定,当它执行锁操作回写内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时会起缓存行无效.
但是有两种情况下处理器不会使用缓存锁定。第一种情况是:当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行(cache line),则处理器会调用总线锁定。第二种情况是:有些处理器不支持缓存锁定。对于Inter486和奔腾处理器,就算锁定的内存区域在处理器的缓存行中也会调用总线锁定。

 

那么哪些操作是非原子的呢?
Accesses to cacheable memory that are split across bus widths, cache lines, and
page boundaries are not guaranteed to be atomic by the Intel Core 2 Duo, Intel®
Atom™, Intel Core Duo, Pentium M, Pentium 4, Intel Xeon, P6 family, Pentium, and
Intel486 processors.
(说点简单点,那些被总线带宽、cache line以及page大小给分隔开了的内存地址的访问不是原子的,你如果想保证这些操作是原子的,你就得求助于机制②,对总线发出相应的控制信号才行)。

(2)对于linux而言,内核提供了两组原子操作接口

一组是针对整数进行操作;另一组是针对单独的位进行操作。
①原子整数操作:针对整数的原子操作只能对atomic_t类型的数据处理,Linux支持的所有机器上的整型数据都是32位,但是使用atomic_t的代码只能将该类型的数据当作24位来使用。这个限制完全是因为在SPARC体系结构上,原子操作的实现不同于其它体系结构:32位int类型的低8位嵌入了一个锁,因为SPARC体系结构对原子操作缺乏指令级的支持,所以只能利用该锁来避免对原子类型数据的并发访问。
②原子位操作:原子位操作定义在文件中。令人感到奇怪的是位操作函数是对普通的内存地址进行操作的。原子位操作在多数情况下是对一个字长的内存访问,因而位号该位于0-31之间(在64位机器上是0-63之间),但是对位号的范围没有限制。

 

为什么关注原子操作?

1)在确认一个操作是原子的情况下,多线程环境里面,我们可以避免仅仅为保护这个操作在外围加上性能开销昂贵的锁。
2)借助于原子操作,我们可以实现互斥锁。
3)借助于互斥锁,我们可以把一些列操作变为原子操作。

 

CAS带来的问题:

1). ABA问题

CAS的核心思想是通过比对内存值与预期值是否一样而判断内存值是否被改过,但这个判断逻辑不严谨,假如内存值原来是A,后来被一条线程改为B,最后又被改成了A,则CAS认为此内存值并没有发生改变,但实际上是有被其他线程改过的,这种情况对依赖过程值的情景的运算结果影响很大。解决的思路是引入版本号,每次变量更新都把版本号加一。在Java中提供了AtomicStampedReference解决ABA问题。

2). 循环时间长,开销大;

AtomicInteger-spin-cas
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

3). 只能保证一个共享变量的原子操作;

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

 

参考连接:

用线程池解决批量任务执行

今天遇到一个场景:用于有一批数据需要批量处理,每一个任务的处理都会花费300~500ms的时间,如果列表为30个时,直接导致http请求超时了(设置的Http请求时间为10s)。所以期望服务端能够多线程执行任务,所有执行完毕后返回最终结果。

解决方案大家比较明确了,在外部接口无法被优化的场景下,只能将顺序执行的任务变为并行执行,Java提供了线程模型和线程池模型。但是要做到合理的利用线程池,必须对其原理了如指掌。

所以问题的解决方案是:

1. 定义线程池对象:

    protected BlockingQueue queue = new LinkedBlockingQueue(100);
    protected ExecutorService taskExecutor = new ThreadPoolExecutor(5, 20, 10, TimeUnit.MILLISECONDS, queue);

为什么不使用Executors工厂类提供的集中固定的线程池对象呢?

Executors工厂提供了以下集中常见的线程池(基本上都是通过不同参数实例化ThreadPoolExecutor得到):
  • Executors.newFixedThreadPool 创建固定数量线程的线程池,线程空闲多久都不会自动关闭,直到线程池主动关闭。当我们线程数不均匀的时候,无法做到很好的扩容。
  • Executors.newSingleThreadExecutor 创建单线程的线程池。
  • Executors.newCachedThreadPool 创建无线程上限(Integer.MAX_VALUE)的线程池,默认线程空闲60秒后自动回收,防止资源浪费,因为无上限,所以就没必要使用阻塞队列来存储任务了,因为原则上线程池不会满,这里采用的是SynchronousQueue,这个阻塞队列要求每个插入操作必须等待另一个线程的对应移除操作,相当于一个数量为1的缓冲区,既然线程无上限,那SynchronousQueue的作用是什么呢,我猜想应该是线程池初始化线程是要时间,采用这样一个缓冲区就可以等待线程池中新的线程创建完毕后直接使用,而不是任务当代线程池中线程的创建。一般生产环境线程数受限于Jvm、Linux等限制不可能做到无限大,为了更高的可控性,不会直接使用此方法。
  • Executors.newScheduledThreadPool 创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。

浮点数 为什么不能精确

一、float、double 在计算机中的存储方式:

我们知道计算机存储数据都是以二进制方式进行存储的,那么浮点数是怎么存储的呢?
比如二进制数101.11 就等于 1 * 2^2 +0 *2^1 + 1*2^0 + 1*2^-1 + 1*2^-2  = 4+0+1+1/2+1/4 = 5.75
下面的图展示了一个二进制小数的表达形式:
float-double-store
计算机对float与double类型都是将十进制数转换为二进制数再小数部分与指数部分分开存储,两者均使用自己独立的符号位,且由于二进制只为1或0,而一个二进制数必然可以设置成为1.XXX,其中X为0或1,所以该数可以默认为1.XXX,使用指数为控制整数部分,整数后面的自然就转换为了小数部分。
根据国际标准IEEE 754,任意一个二进制浮点数V可以表示成下面的形式:
IEEE-v-sme
 (1)(-1)^s表示符号位,当s=0,V为正数;当s=1,V为负数。
 (2)M表示有效数字,大于等于1,小于2。
 (3)2^E表示指数位。

继续阅读