Monthly Archives: 七月 2016

CountDownLatch使用原理

先看一个例子,来说明下CountDownLatch运用场景

public class SpiderDemo {
	private static Logger logger = LoggerFactory.getLogger(SpiderDemo.class);
	private static ExecutorService spiderPool = Executors.newFixedThreadPool(5);

	public static void main(String[] args) {

		CountDownLatch countDown = new CountDownLatch(5);
		for (int i=0;i<5;i++) {
			spiderPool.execute(new SpiderThread(countDown, i));
		}

		try {
			countDown.await();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

		logger.info("spider end");
		spiderPool.shutdown();
	}

	static class SpiderThread implements Runnable{
		private CountDownLatch countDown;
		private int index;

		public SpiderThread(CountDownLatch countDown, int index) {
			this.countDown = countDown;
			this.index = index;
		}

		@Override
		public void run() {
			// TODO Auto-generated method stub
			logger.info("spider"+index+" data begin.. ");
			try {
				Thread.sleep(3000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}

			logger.info("spider"+index+" data complete.. ");

			countDown.countDown();
		}

	}

}

我们来看看上面的例子,这里创建了一个CountDownLatch对象,传入一个数值,使用5个线程去执行SpiderThread,结束后使用countDown()方法将计数器递减,最后使用await()方法堵塞,直到所有的线程执行完成。

所以CountDownLatch的使用场景是可以异步去执行多个任务,主线程可以等待所有任务执行完成后,再继续执行。

下面从源码方面分析下CountDownLatch的实现原理

首先查看构造方法CountDownLatch(int count )

    public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }

	Sync(int count) {
            setState(count);
        }

当count小于0的时候,直接抛出异常,大于0则创建了一个Sync对象。Sync构造函数则是直接设置了一个变量值state来保存count数组

再看看countDown()方法,很显然它的目的是将count数组-1

    public void countDown() {
        sync.releaseShared(1);
    }

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

它里面是直接调用了releaseShared(1)方法,而releaseShared又调用了tryReleaseShared方法,如果尝试执行成功,则再调用doReleaseShared()方法

	protected boolean tryReleaseShared(int releases) {
        // Decrement count; signal when transition to zero
        for (;;) {
            int c = getState();
            if (c == 0)
                return false;
            int nextc = c-1;
            if (compareAndSetState(c, nextc))
                return nextc == 0;
        }
    }

tryReleaseShared是干什么的呢?从上面的例子看它的作用就是将count值-1,如果当前count值就是0了,那就啥都不干,直接返回false了,否则使用CAS算法尝试将count值-1,最后判断当前的count值是否为0,若为0则返回true,返回true就需要执行doReleaseShared方法了

    private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

doReleaseShared又干了些啥?doReleaseShared的作用就是唤醒主线程继续执行,这里调用了unparkSuccessor方法

最后来看下await()方法

    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }
    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }

    private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

可以看到最终会调用doAcquireSharedInterruptibly(若中途线程有被打断,则抛出异常了)

首先会添加一个Node节点(Node.SHARED看源码实质就是new Node()),这个节点就是保存所有调用await()方法的线程(因为可能会有多个线程调用了await()方法),所以这里每调用一次就保存下来一个节点,最后一个一个的释放,如果这里判断count不为0,则会使用park方法将线程挂起。

 

JAVA垃圾收集器(二)

上篇文章说了JAVA垃圾收集器的一些算法,这次讲讲JVM中用到的一些垃圾收集器,这些垃圾收集器也是运用这些算法来回收内存的,如下图:

141513122384006

新生代收集器有:Serial、ParNew、Parallel Scavenge

老年代收集器有:CMS、Serial Old Parallel Old

Serial收集器(复制算法)

新生代单线程收集器,使用标记清理算法,且标记、清理都是单线程,优点:运行高效,缺点:界面会产生卡顿的现象

Serial Old收集器(标记-整理)

老年代单线程收集器,Serial老年代收集版本

ParNew收集器(停止-复制算法)

新生代收集器,Serial的多线程版本,并发标记、清理,在多CPU下有比Serial更好的性能

Parallel Scavenge收集器(停止-复制算法)

新生代收集器,也是使用多线程并行收集,但关注的是高吞吐量(最高效率的利用CPU时间),吞吐量=用户线程时间/(用户线程时间+GC线程时间),适用于后台对交互性不高的应用

Parallel Old收集器(停止-复制算法)

老年代收集器,并行收集,吞吐量优先

CMS(Concurrent Mark Sweep)收集器(标记-清除算法)

以获取最短停顿时间为目标的收集器,应用于如(B/S网站),并发收集,占用CPU资源较高。

 对象分配

1)对象优先会在Eden上分配,如果空间不够,则会发起一次Young GC,清除非存活的对象,把存活的对象移动到Survivor中

2)大对象直接进入老年代,大对象指的是需要大量连续内存空间的Java对象,比如很长的字符串或数组,要避免这种大的对象的频繁创建与销毁

3)长期存活的对象将进入老年代,虚拟机会给每个对象定义一个对象年龄计数器,如果对象在Eden出生并经过第一次Young GC后仍然存活将被移动到Survivor中,并对对象年龄设为1,对象在Survivor区中每熬过一次Young GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁)时,就会被晋升到老年代中

4)动态对象年龄判定:虚拟机并不总是要求对象必须到达年龄才能晋升到老年代,如果在Survivor中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入到老年代。

5)触发Full GC:当老年代的剩余空间不足以容纳从Survivor区移动过来的对象,则会触发一次Full GC,另外在触发Young GC时,JVM会进行空间分配担保,如果新生代晋升了平均大小大于老年代的剩余大小也会触发Full GC;调用System.gc()也会触发Full GC,Full GC会对整个堆进行回收,会照成整个应用停止,所以要尽量减少Full GC的次数。

参考资料:

http://www.cnblogs.com/sunniest/p/4575144.html

 

JAVA垃圾收集器(一)

在JAVA中,JVM虚拟机已经帮我们实现了Java垃圾内存的回收,在说JAVA垃圾收集器之前,先思考下面几个问题:

1)哪些内存需要回收?

2)什么时候回收?

3)如何回收?

闭着眼睛思考1分钟…

好,下面依次来说下:

1,哪些内存需要回收?

我们知道JAVA内存模型包括:Java堆,方法区,线程栈(分为:程序计数器,虚拟机栈,本地方法栈)

线程栈(程序计数器,虚拟机栈,本地方法栈)这三个是随着线程而生,线程而灭。栈中的栈针随着方法的进入和退出有条不紊的执行者出栈和入栈的操作,每一个栈帧中分配多少内存基本上是在类结构确定下来就已知的,所以方法结束或线程结束后,内存自然就跟着回收了,所以这部分不用过多考虑内存回收的问题

Java堆和方法区,这些里面都是用来存一些静态的类,或者动态代理生成的类,创建的对象,这些都是在程序运行时动态创建的,所需要的内存我们就不确定了,如果创建的对象使用完后,一直没有被回收,就导致内存溢出了,所以这部分就是垃圾回收器所要关心的,他要识别出那些无效的对象回收掉

2,什么时候回收?

其实第一点已经说了,就是那些无效的对象,何为无效的对象?即不再可能被任何途径使用的对象

如何判断这个对象为无效对象呢?垃圾回收提供了2种算法:引用计数法和根搜索算法,

引用计数法:即判断某个对象不在被其它对象引用,当某个对象被一个对象引用时,计数器+1,当引用失效时,则-1,直到减为0时,表示可以回收了,但这不能解决A,B之间相互引用的问题,A中有一个变量指向B,同时B中有一个变量指向A,所以即使A,B最后都不会再用了,由于他们的引用关系还在,仍然不能被回收。

JAVA中使用的就是根搜索算法来判断对象是否存活的

根搜索算法:JAVA中定义了一个名为“GC Roots” 的对象来作为起始点,从这个节点开始找,如果某个对象到这个“GC Roots”对象没有引用连接,那么表示这个对象就可以被回收了。

哪些对象可以作为GC Roots呢?Java中定义了这些对象:

1)虚拟机栈(栈帧中的本地变量表)中引用的对象。

2)方法区中类静态属性引用的对象。

3)方法区中常量引用的对象

4)本地方法栈中(一般说的本地方法)应用的对象

如下图,object1,object2,object3,object4不能被回收,object5,object6,object7虽然有互连,但是可以被回收

75B97DA3-0F8F-43CB-9BDD-2B310EF88B90

3,如何回收?

关于回收有3中算法:

1)标记-清除算法

javaneicunbiaojiqingchu

如上图,先标记出无效的对象,再进行清理,它有两个缺点:

效率问题:标记和清除过程的效率都不高

空间问题:如上图,清除后会产生大量不连续的内存碎片

2)复制算法

javaneicunfuzhi

如上图,先将可用内存划为为大小相等的两块,每次只使用其中一块,当这一块内存用完了,将存活的对象复制到另一份上,并将原先那块一次性清理掉

缺点:以空间换时间,这样,可用内存就缩小为原来的一半了,当然JVM做了也做了优化

在使用这种算法回收新生代时,不用要求内存分为1:1等量的两块,因为新生代一般都是朝生夕死,所以只会有少部分对象存活下来,JVM将新生代分为了Eden和两块较小的Survivor,Eden和Survivor内存为8:1,当Survivor内存不够时,就需要其他内存(老年代)来担保为其承担部分内存。

3)标记-整理算法

javaneicunbiaojizhengli

如上图,类似于“标记-清理”,也是先标记处存活的对象,但之后是先将存活的对象都向一端移动,然后清理掉边界之外的内存,这种算法一般适用于JAVA老年代的回收

说明:JAVA堆分为了新生代和老年代

新生代每次回收都会有大批对象死去,只有少量存活,所以采用复制算法

而老年代每个对象的存活率很高,且没有额外的空间对其担保,所以可以采用“标记-清理”或“标记-整理”算法来回收