从源码的角度再学「Thread」

原创: zhangshaolin 张少林同学
微信号: zhangshaolin_tonxue

功能介绍 分享

前言

Java中的线程是使用Thread类实现的,Thread在初学Java的时候就学过了,也在实践中用过,不过一直没从源码的角度去看过它的实现,今天从源码的角度出发,再次学习Java Thread,愿此后对Thread的实践更加得心应手。

从注释开始

相信阅读过JDK源码的同学都能感受到JDK源码中有非常详尽的注释,阅读某个类的源码应当先看看注释对它的介绍,注释原文就不贴了,以下是我对它的总结:

Thread是程序中执行的线程,Java虚拟机允许应用程序同时允许多个执行线程

每个线程都有优先级的概念,具有较高优先级的线程优先于优先级较低的线程执行

每个线程都可以被设置为守护线程

当在某个线程中运行的代码创建一个新的Thread对象时,新的线程优先级跟创建线程一致

当Java虚拟机启动的时候都会启动一个叫做main的线程,它没有守护线程,main线程会继续执行,直到以下情况发送

Runtime 类的退出方法exit被调用并且安全管理器允许进行退出操作

所有非守护线程均已死亡,或者run方法执行结束正常返回结果,或者run方法抛出异常

创建线程第一种方式:继承Thread类,重写run方法

1 //定义线程类 2 class PrimeThread extends Thread { 3 long minPrime; 4 PrimeThread(long minPrime) { 5 this.minPrime = minPrime; 6 } 7 public void run() { 8 // compute primes larger than minPrime 9  . . . 10 } 11 } 12 //启动线程 13 PrimeThread p = new PrimeThread(143); 14 p.start();

创建线程第二种方式:实现Runnable接口,重写run方法,因为Java的单继承限制,通常使用这种方式创建线程更加灵活

1 //定义线程 2 class PrimeRun implements Runnable { 3 long minPrime; 4 PrimeRun(long minPrime) { 5 this.minPrime = minPrime; 6 } 7 public void run() { 8 // compute primes larger than minPrime 9  . . . 10 } 11 } 12 //启动线程 13 PrimeRun p = new PrimeRun(143); 14 new Thread(p).start();

创建线程时可以给线程指定名字,如果没有指定,会自动为它生成名字

除非另有说明,否则将null参数传递给Thread类中的构造函数或方法将导致抛出 NullPointerException

Thread 常用属性

阅读一个Java类,先从它拥有哪些属性入手:

1 //线程名称,创建线程时可以指定线程的名称 2 private volatile String name; 3 4 //线程优先级,可以设置线程的优先级 5 private int priority; 6 7 //可以配置线程是否为守护线程,默认为false 8 private boolean daemon = false; 9 10 //最终执行线程任务的`Runnable` 11 private Runnable target; 12 13 //描述线程组的类 14 private ThreadGroup group; 15 16 //此线程的上下文ClassLoader 17 private ClassLoader contextClassLoader; 18 19 //所有初始化线程的数目,用于自动编号匿名线程,当没有指定线程名称时,会自动为其编号 20 private static int threadInitNumber; 21 22 //此线程请求的堆栈大小,如果创建者没有指定堆栈大小,则为0。, 虚拟机可以用这个数字做任何喜欢的事情。, 一些虚拟机会忽略它。 23 private long stackSize; 24 25 //线程id 26 private long tid; 27 28 //用于生成线程ID 29 private static long threadSeqNumber; 30 31 //线程状态 32 private volatile int threadStatus = 0; 33 34 //线程可以拥有的最低优先级 35 public final static int MIN_PRIORITY = 1; 36 37 //分配给线程的默认优先级。 38 public final static int NORM_PRIORITY = 5; 39 40 //线程可以拥有的最大优先级 41 public final static int MAX_PRIORITY = 10; Thread 构造方法

了解了属性之后,看看Thread实例是怎么构造的?先预览下它大致有多少个构造方法:

查看每个构造方法内部源码,发现均调用的是名为init的私有方法,再看init方法有两个重载,而其核心方法如下:

1 /** 2 * Initializes a Thread. 3 * 4 * @param g 线程组 5 * @param target 最终执行任务的 `run()` 方法的对象 6 * @param name 新线程的名称 7 * @param stackSize 新线程所需的堆栈大小,或者 0 表示要忽略此参数 8 * @param acc 要继承的AccessControlContext,如果为null,则为 AccessController.getContext() 9 * @param inheritThreadLocals 如果为 true,从构造线程继承可继承的线程局部的初始值 10 */ 11 private void init(ThreadGroup g, Runnable target, String name, 12 long stackSize, AccessControlContext acc, 13 boolean inheritThreadLocals) { 14 //线程名称为空,直接抛出空指针异常 15 if (name == null) { 16 throw new NullPointerException("name cannot be null"); 17 } 18 //初始化当前线程对象的线程名称 19 this.name = name; 20 //获取当前正在执行的线程为父线程 21 Thread parent = currentThread(); 22 //获取系统安全管理器 23 SecurityManager security = System.getSecurityManager(); 24 //如果线程组为空 25 if (g == null) { 26 //如果安全管理器不为空 27 if (security != null) { 28 //获取SecurityManager中的线程组 29 g = security.getThreadGroup(); 30 } 31 //如果获取的线程组还是为空 32 if (g == null) { 33 //则使用父线程的线程组 34 g = parent.getThreadGroup(); 35 } 36 } 37 38 //检查安全权限 39 g.checkAccess(); 40 41 //使用安全管理器检查是否有权限 42 if (security != null) { 43 if (isCCLOverridden(getClass())) { 44 security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION); 45 } 46 } 47 48 //线程组中标记未启动的线程数+1,这里方法是同步的,防止出现线程安全问题 49 g.addUnstarted(); 50 51 //初始化当前线程对象的线程组 52 this.group = g; 53 //初始化当前线程对象的是否守护线程属性,注意到这里初始化时跟父线程一致 54 this.daemon = parent.isDaemon(); 55 //初始化当前线程对象的线程优先级属性,注意到这里初始化时跟父线程一致 56 this.priority = parent.getPriority(); 57 //这里初始化类加载器 58 if (security == null || isCCLOverridden(parent.getClass())) 59 this.contextClassLoader = parent.getContextClassLoader(); 60 else 61 this.contextClassLoader = parent.contextClassLoader; 62 this.inheritedAccessControlContext = 63 acc != null ? acc : AccessController.getContext(); 64 //初始化当前线程对象的最终执行任务对象 65 this.target = target; 66 //这里再对线程的优先级字段进行处理 67 setPriority(priority); 68 if (inheritThreadLocals && parent.inheritableThreadLocals != null) 69 this.inheritableThreadLocals = 70 ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); 71 //初始化当前线程对象的堆栈大小 72 this.stackSize = stackSize; 73 74 //初始化当前线程对象的线程ID,该方法是同步的,内部实际上是threadSeqNumber++ 75 tid = nextThreadID(); 76 }

另一个重载init私有方法如下,实际上内部调用的是上述init方法:

1 private void init(ThreadGroup g, Runnable target, String name, 2 long stackSize) { 3 init(g, target, name, stackSize, null, true); 4 }

接下来看看所有构造方法:

空构造方法

1 public Thread() { 2 init(null, null, "Thread-" + nextThreadNum(), 0); 3 }

内部调用的是init第二个重载方法,参数基本都是默认值,线程名称写死为"Thread-" + nextThreadNum()格式,nextThreadNum()为一个同步方法,内部维护一个静态属性表示线程的初始化数量+1:

1 private static int threadInitNumber; 2 private static synchronized int nextThreadNum() { 3 return threadInitNumber++; 4 }

与第一个构造方法区别在于可以自定义Runnable对象

自定义执行任务Runnable对象的构造方法

1 private static int threadInitNumber; 2 private static synchronized int nextThreadNum() { 3 return threadInitNumber++; 4 }

自定义执行任务Runnable对象和AccessControlContext对象的构造方法

1 Thread(Runnable target, AccessControlContext acc) { 2 init(null, target, "Thread-" + nextThreadNum(), 0, acc, false); 3 }

自定义线程组ThreadGroup和执行任务Runnable对象的构造方法

1 public Thread(ThreadGroup group, Runnable target) { 2 init(group, target, "Thread-" + nextThreadNum(), 0); 3 }

自定义线程名称name的构造方法

1 public Thread(String name) { 2 init(null, null, name, 0); 3 }

自定义线程组ThreadGroup和线程名称name的构造方法

1 public Thread(String name) { 2 init(null, null, name, 0); 3 }

自定义执行任务Runnable对象和线程名称name的构造方法

1 public Thread(Runnable target, String name) { 2 init(null, target, name, 0); 3 }

自定义线程组ThreadGroup和线程名称name和执行任务Runnable对象的构造方法

1 public Thread(ThreadGroup group, Runnable target, String name) { 2 init(group, target, name, 0); 3 }

全部属性都是自定义的构造方法

1 public Thread(ThreadGroup group, Runnable target, String name, 2 long stackSize) { 3 init(group, target, name, stackSize); 4 }

Thread提供了非常灵活的重载构造方法,方便开发者自定义各种参数的Thread对象。

常用方法

这里记录一些比较常见的方法吧,对于Thread中存在的一些本地方法,我们暂且不用管它~

设置线程名称

设置线程名称,该方法为同步方法,为了防止出现线程安全问题,可以手动调用Thread的实例方法设置名称,也可以在构造Thread时在构造方法中传入线程名称,我们通常都是在构造参数时设置

1 public final synchronized void setName(String name) { 2   //检查安全权限 3 checkAccess(); 4   //如果形参为空,抛出空指针异常 5 if (name == null) { 6 throw new NullPointerException("name cannot be null"); 7 } 8 //给当前线程对象设置名称 9 this.name = name; 10 if (threadStatus != 0) { 11 setNativeName(name); 12 } 13 } 获取线程名称

内部直接返回当前线程对象的名称属性

1 public final String getName() { 2 return name; 3 } 启动线程 1 public synchronized void start() { 2 //如果不是刚创建的线程,抛出异常 3 if (threadStatus != 0) 4 throw new IllegalThreadStateException(); 5 6 //通知线程组,当前线程即将启动,线程组当前启动线程数+1,未启动线程数-1 7 group.add(this); 8 9 //启动标识 10 boolean started = false; 11 try { 12 //直接调用本地方法启动线程 13 start0(); 14 //设置启动标识为启动成功 15 started = true; 16 } finally { 17 try { 18 //如果启动呢失败 19 if (!started) { 20 //线程组内部移除当前启动的线程数量-1,同时启动失败的线程数量+1 21 group.threadStartFailed(this); 22 } 23 } catch (Throwable ignore) { 24 /* do nothing. If start0 threw a Throwable then 25 it will be passed up the call stack */ 26 } 27 } 28 }

我们正常的启动线程都是调用Thread的start()方法,然后Java虚拟机内部会去调用Thred的run方法,可以看到Thread类也是实现Runnable接口,重写了run方法的:

1 @Override 2 public void run() { 3 //当前执行任务的Runnable对象不为空,则调用其run方法 4 if (target != null) { 5 target.run(); 6 } 7 }

Thread的两种使用方式:

继承Thread类,重写run方法,那么此时是直接执行run方法的逻辑,不会使用target.run();

实现Runnable接口,重写run方法,因为Java的单继承限制,通常使用这种方式创建线程更加灵活,这里真正的执行逻辑就会交给自定义Runnable去实现

设置守护线程

本质操作是设置daemon属性

1 public final void setDaemon(boolean on) { 2 //检查是否有安全权限 3 checkAccess(); 4 //本地方法,测试此线程是否存活。, 如果一个线程已经启动并且尚未死亡,则该线程处于活动状态 5 if (isAlive()) { 6 //如果线程先启动后再设置守护线程,将抛出异常 7 throw new IllegalThreadStateException(); 8 } 9 //设置当前守护线程属性 10 daemon = on; 11 } 判断线程是否为守护线程 1 public final boolean isDaemon() { 2 //直接返回当前对象的守护线程属性 3 return daemon; 4 } 线程状态

先来个线程状态图:

获取线程状态:

1 public State getState() { 2 //由虚拟机实现,获取当前线程的状态 3 return sun.misc.VM.toThreadState(threadStatus); 4 }

线程状态主要由内部枚举类State组成:

1 public enum State { 2 3 NEW, 4 5 6 RUNNABLE, 7 8 9 BLOCKED, 10 11 12 WAITING, 13 14 15 TIMED_WAITING, 16 17 18 TERMINATED; 19 }

NEW:刚刚创建,尚未启动的线程处于此状态

RUNNABLE:在Java虚拟机中执行的线程处于此状态

BLOCKED:被阻塞等待监视器锁的线程处于此状态,比如线程在执行过程中遇到synchronized同步块,就会进入此状态,此时线程暂停执行,直到获得请求的锁

WAITING:无限期等待另一个线程执行特定操作的线程处于此状态

通过 wait() 方法等待的线程在等待 notify() 方法

通过 join() 方法等待的线程则会等待目标线程的终止

TIMED_WAITING:正在等待另一个线程执行动作,直到指定等待时间的线程处于此状态

通过 wait() 方法,携带超时时间,等待的线程在等待 notify() 方法

通过 join() 方法,携带超时时间,等待的线程则会等待目标线程的终止

TERMINATED:已退出的线程处于此状态,此时线程无法再回到 RUNNABLE 状态

线程休眠

这是一个静态的本地方法,使当前执行的线程休眠暂停执行 millis 毫秒,当休眠被中断时会抛出InterruptedException中断异常

1 /** 2 * Causes the currently executing thread to sleep (temporarily cease 3 * execution) for the specified number of milliseconds, subject to 4 * the precision and accuracy of system timers and schedulers. The thread 5 * does not lose ownership of any monitors. 6 * 7 * @param millis 8 * the length of time to sleep in milliseconds 9 * 10 * @throws IllegalArgumentException 11 * if the value of {@code millis} is negative 12 * 13 * @throws InterruptedException 14 * if any thread has interrupted the current thread. The 15 * <i>interrupted status</i> of the current thread is 16 * cleared when this exception is thrown. 17 */ 18 public static native void sleep(long millis) throws InterruptedException; 检查线程是否存活

本地方法,测试此线程是否存活。 如果一个线程已经启动并且尚未死亡,则该线程处于活动状态。

1 /** 2 * Tests if this thread is alive. A thread is alive if it has 3 * been started and has not yet died. 4 * 5 * @return <code>true</code> if this thread is alive; 6 * <code>false</code> otherwise. 7 */ 8 public final native boolean isAlive(); 线程优先级

设置线程优先级

1 /** 2 * Changes the priority of this thread. 3 * <p> 4 * First the <code>checkAccess</code> method of this thread is called 5 * with no arguments. This may result in throwing a 6 * <code>SecurityException</code>. 7 * <p> 8 * Otherwise, the priority of this thread is set to the smaller of 9 * the specified <code>newPriority</code> and the maximum permitted 10 * priority of the thread's thread group. 11 * 12 * @param newPriority priority to set this thread to 13 * @exception IllegalArgumentException If the priority is not in the 14 * range <code>MIN_PRIORITY</code> to 15 * <code>MAX_PRIORITY</code>. 16 * @exception SecurityException if the current thread cannot modify 17 * this thread. 18 * @see #getPriority 19 * @see #checkAccess() 20 * @see #getThreadGroup() 21 * @see #MAX_PRIORITY 22 * @see #MIN_PRIORITY 23 * @see ThreadGroup#getMaxPriority() 24 */ 25 public final void setPriority(int newPriority) { 26 //线程组 27 ThreadGroup g; 28 //检查安全权限 29 checkAccess(); 30 //检查优先级形参范围 31 if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) { 32 throw new IllegalArgumentException(); 33 } 34 if((g = getThreadGroup()) != null) { 35 //如果优先级形参大于线程组最大线程最大优先级 36 if (newPriority > g.getMaxPriority()) { 37 //则使用线程组的优先级数据 38 newPriority = g.getMaxPriority(); 39 } 40 //调用本地设置线程优先级方法 41 setPriority0(priority = newPriority); 42 } 43 } 线程中断

有一个stop()实例方法可以强制终止线程,不过这个方法因为太过于暴力,已经被标记为过时方法,不建议程序员再使用,因为强制终止线程会导致数据不一致的问题。

这里关于线程中断的方法涉及三个:

1 //实例方法,通知线程中断,设置标志位 2 public void interrupt(){} 3 //静态方法,检查当前线程的中断状态,同时会清除当前线程的中断标志位状态 4 public static boolean interrupted(){} 5 //实例方法,检查当前线程是否被中断,其实是检查中断标志位 6 public boolean isInterrupted(){}

interrupt() 方法解析

1 /** 2 * Interrupts this thread. 3 * 4 * <p> Unless the current thread is interrupting itself, which is 5 * always permitted, the {@link #checkAccess() checkAccess} method 6 * of this thread is invoked, which may cause a {@link 7 * SecurityException} to be thrown. 8 * 9 * <p> If this thread is blocked in an invocation of the {@link 10 * Object#wait() wait()}, {@link Object#wait(long) wait(long)}, or {@link 11 * Object#wait(long, int) wait(long, int)} methods of the {@link Object} 12 * class, or of the {@link #join()}, {@link #join(long)}, {@link 13 * #join(long, int)}, {@link #sleep(long)}, or {@link #sleep(long, int)}, 14 * methods of this class, then its interrupt status will be cleared and it 15 * will receive an {@link InterruptedException}. 16 * 17 * <p> If this thread is blocked in an I/O operation upon an {@link 18 * java.nio.channels.InterruptibleChannel InterruptibleChannel} 19 * then the channel will be closed, the thread's interrupt 20 * status will be set, and the thread will receive a {@link 21 * java.nio.channels.ClosedByInterruptException}. 22 * 23 * <p> If this thread is blocked in a {@link java.nio.channels.Selector} 24 * then the thread's interrupt status will be set and it will return 25 * immediately from the selection operation, possibly with a non-zero 26 * value, just as if the selector's {@link 27 * java.nio.channels.Selector#wakeup wakeup} method were invoked. 28 * 29 * <p> If none of the previous conditions hold then this thread's interrupt 30 * status will be set. </p> 31 * 32 * <p> Interrupting a thread that is not alive need not have any effect. 33 * 34 * @throws SecurityException 35 * if the current thread cannot modify this thread 36 * 37 * @revised 6.0 38 * @spec JSR-51 39 */ 40 public void interrupt() { 41 //检查是否是自身调用 42 if (this != Thread.currentThread()) 43 //检查安全权限,这可能导致抛出{@link * SecurityException}。 44 checkAccess(); 45 46 //同步代码块 47 synchronized (blockerLock) { 48 Interruptible b = blocker; 49 //检查是否是阻塞线程调用 50 if (b != null) { 51 //设置线程中断标志位 52 interrupt0(); 53 //此时抛出异常,将中断标志位设置为false,此时我们正常会捕获该异常,重新设置中断标志位 54 b.interrupt(this); 55 return; 56 } 57 } 58 //如无意外,则正常设置中断标志位 59 interrupt0(); 60 }

线程中断方法不会使线程立即退出,而是给线程发送一个通知,告知目标线程,有人希望你退出啦~

只能由自身调用,否则可能会抛出 SecurityException

调用中断方法是由目标线程自己决定是否中断,而如果同时调用了wait,join,sleep等方法,会使当前线程进入阻塞状态,此时有可能发生InterruptedException异常

被阻塞的线程再调用中断方法是不合理的

中断不活动的线程不会产生任何影响

检查线程是否被中断:

1 /** 2 * Tests whether this thread has been interrupted. The <i>interrupted 3 * status</i> of the thread is unaffected by this method. 4 5 测试此线程是否已被中断。, 线程的<i>中断*状态</ i>不受此方法的影响。 6 * 7 * <p>A thread interruption ignored because a thread was not alive 8 * at the time of the interrupt will be reflected by this method 9 * returning false. 10 * 11 * @return <code>true</code> if this thread has been interrupted; 12 * <code>false</code> otherwise. 13 * @see #interrupted() 14 * @revised 6.0 15 */ 16 public boolean isInterrupted() { 17 return isInterrupted(false); 18 }

静态方法,会清空当前线程的中断标志位:

1 /** 2 *测试当前线程是否已被中断。, 此方法清除线程的* <i>中断状态</ i>。, 换句话说,如果要连续两次调用此方法,则* second调用将返回false(除非当前线程再次被中断,在第一次调用已清除其中断的*状态 之后且在第二次调用已检查之前), 它) 3 * 4 * <p>A thread interruption ignored because a thread was not alive 5 * at the time of the interrupt will be reflected by this method 6 * returning false. 7 * 8 * @return <code>true</code> if the current thread has been interrupted; 9 * <code>false</code> otherwise. 10 * @see #isInterrupted() 11 * @revised 6.0 12 */ 13 public static boolean interrupted() { 14 return currentThread().isInterrupted(true); 15 } 总结

记录自己阅读Thread类源码的一些思考,不过对于其中用到的很多本地方法只能望而却步,还有一些代码没有看明白,暂且先这样吧,如果有不足之处,请留言告知我,谢谢!后续会在实践中对Thread做出更多总结记录。

read more
深入理解JVM-内存模型(jmm)和GC

转载:https://www.jianshu.com/p/76959115d486

1 CPU和内存的交互

了解jvm内存模型前,了解下cpu和计算机内存的交互情况。【因为Java虚拟机内存模型定义的访问操作与计算机十分相似】

有篇很棒的文章,从cpu讲到内存模型:什么是java内存模型

在计算机中,cpu和内存的交互最为频繁,相比内存,磁盘读写太慢,内存相当于高速的缓冲区。

但是随着cpu的发展,内存的读写速度也远远赶不上cpu。因此cpu厂商在每颗cpu上加上高速缓存,用于缓解这种情况。现在cpu和内存的交互大致如下。

cpu上加入了高速缓存这样做解决了处理器和内存的矛盾(一快一慢),但是引来的新的问题

缓存一致性

在多核cpu中,每个处理器都有各自的高速缓存(L1,L2,L3),而主内存确只有一个 。

以我的pc为例,因为cpu成本高,缓存区一般也很小。

CPU要读取一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有就从三级缓存或内存中查找,每个cpu有且只有一套自己的缓存。

如何保证多个处理器运算涉及到同一个内存区域时,多线程场景下会存在缓存一致性问题,那么运行时保证数据一致性?

为了解决这个问题,各个处理器需遵循一些协议保证一致性。【如MSI,MESI啥啥的协议。。】

大概如下

cpu与内存

在CPU层面,内存屏障提供了个充分必要条件

1.1.1 内存屏障(Memory Barrier)

CPU中,每个CPU又有多级缓存【上图统一定义为高速缓存】,一般分为L1,L2,L3,因为这些缓存的出现,提高了数据访问性能,避免每次都向内存索取,但是弊端也很明显,不能实时的和内存发生信息交换,分在不同CPU执行的不同线程对同一个变量的缓存值不同。

硬件层的内存屏障分为两种:Load Barrier 和 Store Barrier即读屏障和写屏障。【内存屏障是硬件层的】 为什么需要内存屏障 由于现代操作系统都是多处理器操作系统,每个处理器都会有自己的缓存,可能存再不同处理器缓存不一致的问题,而且由于操作系统可能存在重排序,导致读取到错误的数据,因此,操作系统提供了一些内存屏障以解决这种问题. 简单来说:1.在不同CPU执行的不同线程对同一个变量的缓存值不同,为了解决这个问题。 2.用volatile可以解决上面的问题,不同硬件对内存屏障的实现方式不一样。java屏蔽掉这些差异,通过jvm生成内存屏障的指令。 对于读屏障:在指令前插入读屏障,可以让高速缓存中的数据失效,强制从主内存取。 内存屏障的作用 cpu执行指令可能是无序的,它有两个比较重要的作用 1.阻止屏障两侧指令重排序 2.强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。 volatile型变量

当我们声明某个变量为volatile修饰时,这个变量就有了线程可见性,volatile通过在读写操作前后添加内存屏障。

用代码可以这么理解

//相当于读写时加锁,保证及时可见性,并发时不被随意修改。publicclassSynchronizedInteger{privatelong value;publicsynchronizedintget(){return value;}publicsynchronizedvoidset(long value){this.value = value;}}

volatile型变量拥有如下特性

可见性,对于一个该变量的读,一定能看到读之前最后的写入。 防止指令重排序,执行代码时,为了提高执行效率,会在不影响最后结果的前提下对指令进行重新排序,使用volatile可以防止,比如单例模式双重校验锁的创建中有使用到,如(https://www.jianshu.com/p/b30a4d568be4) 注意的是volatile不具有原子性,如volatile++这样的复合操作,这里感谢大家的指正。

至于volatile底层是怎么实现保证不同线程可见性的,这里涉及到的就是硬件上的,被volatile修饰的变量在进行写操作时,会生成一个特殊的汇编指令,该指令会触发mesi协议,会存在一个总线嗅探机制的东西,简单来说就是这个cpu会不停检测总线中该变量的变化,如果该变量一旦变化了,由于这个嗅探机制,其它cpu会立马将该变量的cpu缓存数据清空掉,重新的去从主内存拿到这个数据。简单画了个图。

2. Java内存区域

前提:本文讲的基本都是以Sun HotSpot虚拟机为基础的,Oracle收购了Sun后目前得到了两个【Sun的HotSpot和JRockit(以后可能合并这两个),还有一个是IBM的IBMJVM】

之所以扯了那么多计算机内存模型,是因为java内存模型的设定符合了计算机的规范。

Java程序内存的分配是在JVM虚拟机内存分配机制下完成

Java内存模型(Java Memory Model ,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。

简要言之,jmm是jvm的一种规范,定义了jvm的内存模型。它屏蔽了各种硬件和操作系统的访问差异,不像c那样直接访问硬件内存,相对安全很多,它的主要目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。可以保证并发编程场景中的原子性、可见性和有序性。

从下面这张图可以看出来,Java数据区域分为五大数据区域。这些区域各有各的用途,创建及销毁时间。

其中方法区和堆是所有线程共享的,栈,本地方法栈和程序虚拟机则为线程私有的。

根据java虚拟机规范,java虚拟机管理的内存将分为下面五大区域。

jmm

2.1 五大内存区域 2.1.1 程序计数器 程序计数器是一块很小的内存空间,它是线程私有的,可以认作为当前线程的行号指示器。

为什么需要程序计数器

我们知道对于一个处理器(如果是多核cpu那就是一核),在一个确定的时刻都只会执行一条线程中的指令,一条线程中有多个指令,为了线程切换可以恢复到正确执行位置,每个线程都需有独立的一个程序计数器,不同线程之间的程序计数器互不影响,独立存储。

注意:如果线程执行的是个java方法,那么计数器记录虚拟机字节码指令的地址。如果为native【底层方法】,那么计数器为空。这块内存区域是虚拟机规范中唯一没有OutOfMemoryError的区域

2.1.2 Java栈(虚拟机栈)

同计数器也为线程私有,生命周期与相同,就是我们平时说的栈,栈描述的是Java方法执行的内存模型

每个方法被执行的时候都会创建一个栈帧用于存储局部变量表,操作栈,动态链接,方法出口等信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。【栈先进后出,下图栈1先进最后出来】

对于栈帧的解释参考 Java虚拟机运行时栈帧结构

栈帧: 是用来存储数据和部分过程结果的数据结构。 栈帧的位置: 内存 -> 运行时数据区 -> 某个线程对应的虚拟机栈 -> here[在这里] 栈帧大小确定时间: 编译期确定,不受运行期数据影响。

通常有人将java内存区分为栈和堆,实际上java内存比这复杂,这么区分可能是因为我们最关注,与对象内存分配关系最密切的是这两个。

平时说的栈一般指局部变量表部分。

局部变量表:一片连续的内存空间,用来存放方法参数,以及方法内定义的局部变量,存放着编译期间已知的数据类型(八大基本类型和对象引用(reference类型),returnAddress类型。它的最小的局部变量表空间单位为Slot,虚拟机没有指明Slot的大小,但在jvm中,long和double类型数据明确规定为64位,这两个类型占2个Slot,其它基本类型固定占用1个Slot。

reference类型:与基本类型不同的是它不等同本身,即使是String,内部也是char数组组成,它可能是指向一个对象起始位置指针,也可能指向一个代表对象的句柄或其他与该对象有关的位置。

returnAddress类型:指向一条字节码指令的地址【深入理解Java虚拟机】怎么理解returnAddress

需要注意的是,局部变量表所需要的内存空间在编译期完成分配,当进入一个方法时,这个方法在栈中需要分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表大小。

Java虚拟机栈可能出现两种类型的异常:

线程请求的栈深度大于虚拟机允许的栈深度,将抛出StackOverflowError。 虚拟机栈空间可以动态扩展,当动态扩展是无法申请到足够的空间时,抛出OutOfMemory异常。
2.1.3 本地方法栈

本地方法栈是与虚拟机栈发挥的作用十分相似,区别是虚拟机栈执行的是Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务,可能底层调用的c或者c++,我们打开jdk安装目录可以看到也有很多用c编写的文件,可能就是native方法所调用的c代码。

2.1.4 堆

对于大多数应用来说,堆是java虚拟机管理内存最大的一块内存区域,因为堆存放的对象是线程共享的,所以多线程的时候也需要同步机制。因此需要重点了解下。

java虚拟机规范对这块的描述是:所有对象实例及数组都要在堆上分配内存,但随着JIT编译器的发展和逃逸分析技术的成熟,这个说法也不是那么绝对,但是大多数情况都是这样的。

即时编译器:可以把把Java的字节码,包括需要被解释的指令的程序)转换成可以直接发送给处理器的指令的程序)

逃逸分析:通过逃逸分析来决定某些实例或者变量是否要在堆中进行分配,如果开启了逃逸分析,即可将这些变量直接在栈上进行分配,而非堆上进行分配。这些变量的指针可以被全局所引用,或者其其它线程所引用。

参考逃逸分析

注意:它是所有线程共享的,它的目的是存放对象实例。同时它也是GC所管理的主要区域,因此常被称为GC堆,又由于现在收集器常使用分代算法,Java堆中还可以细分为新生代和老年代,再细致点还有Eden(伊甸园)空间之类的不做深究。

根据虚拟机规范,Java堆可以存在物理上不连续的内存空间,就像磁盘空间只要逻辑是连续的即可。它的内存大小可以设为固定大小,也可以扩展。

当前主流的虚拟机如HotPot都能按扩展实现(通过设置 -Xmx和-Xms),如果堆中没有内存内存完成实例分配,而且堆无法扩展将报OOM错误(OutOfMemoryError)

2.1.5 方法区

方法区同堆一样,是所有线程共享的内存区域,为了区分堆,又被称为非堆。

用于存储已被虚拟机加载的类信息、常量、静态变量,如static修饰的变量加载类的时候就被加载到方法区中。

运行时常量池

是方法区的一部分,class文件除了有类的字段、接口、方法等描述信息之外,还有常量池用于存放编译期间生成的各种字面量和符号引用。

在老版jdk,方法区也被称为永久代【因为没有强制要求方法区必须实现垃圾回收,HotSpot虚拟机以永久代来实现方法区,从而JVM的垃圾收集器可以像管理堆区一样管理这部分区域,从而不需要专门为这部分设计垃圾回收机制。不过自从JDK7之后,Hotspot虚拟机便将运行时常量池从永久代移除了。】

jdk1.7开始逐步去永久代。从String.interns()方法可以看出来 String.interns()native方法:作用是如果字符串常量池已经包含一个等于这个String对象的字符串,则返回代表池中的这个字符串的String对象,在jdk1.6及以前常量池分配在永久代中。可通过 -XX:PermSize和-XX:MaxPermSize限制方法区大小。 publicclassStringIntern{//运行如下代码探究运行时常量池的位置publicstaticvoidmain(String[] args)throwsThrowable{//用list保持着引用 防止full gc回收常量池List<String> list =newArrayList<String>();int i =0;while(true){ list.add(String.valueOf(i++).intern());}}}//如果在jdk1.6环境下运行 同时限制方法区大小 将报OOM后面跟着PermGen space说明方法区OOM,即常量池在永久代//如果是jdk1.7或1.8环境下运行 同时限制堆的大小 将报heap space 即常量池在堆中

idea设置相关内存大小设置

这边不用全局的方式,设置main方法的vm参数。

做相关设置,比如说这边设定堆大小。(-Xmx5m -Xms5m -XX:-UseGCOverheadLimit)

这边如果不设置UseGCOverheadLimit将报java.lang.OutOfMemoryError: GC overhead limit exceeded, 这个错是因为GC占用了多余98%(默认值)的CPU时间却只回收了少于2%(默认值)的堆空间。目的是为了让应用终止,给开发者机会去诊断问题。一般是应用程序在有限的内存上创建了大量的临时对象或者弱引用对象,从而导致该异常。虽然加大内存可以暂时解决这个问题,但是还是强烈建议去优化代码,后者更加有效,也可通过UseGCOverheadLimit避免[不推荐,这里是因为测试用,并不能解决根本问题]

jdk8真正开始废弃永久代,而使用元空间(Metaspace)

java虚拟机对方法区比较宽松,除了跟堆一样可以不存在连续的内存空间,定义空间和可扩展空间,还可以选择不实现垃圾收集。

2.2 对象的内存布局

在HotSpot虚拟机中。对象在内存中存储的布局分为

1.对象头 2.实例数据 3.对齐填充 2.2.1 对象头【markword】

在32位系统下,对象头8字节,64位则是16个字节【未开启压缩指针,开启后12字节】。

markword很像网络协议报文头,划分为多个区间,并且会根据对象的状态复用自己的存储空间。 为什么这么做:省空间,对象需要存储的数据很多,32bit/64bit是不够的,它被设计成非固定的数据结构以便在极小的空间存储更多的信息, 假设当前为32bit,在对象未被锁定情况下。25bit为存储对象的哈希码、4bit用于存储分代年龄,2bit用于存储锁标志位,1bit固定为0。

不同状态下存放数据

这其中锁标识位需要特别关注下。锁标志位与是否为偏向锁对应到唯一的锁状态

锁的状态分为四种无锁状态、偏向锁、轻量级锁和重量级锁

不同状态时对象头的区间含义,如图所示。

对象头

HotSpot底层通过markOop实现Mark Word,具体实现位于markOop.hpp文件。

markOop中提供了大量方法用于查看当前对象头的状态,以及更新对象头的数据,为synchronized锁的实现提供了基础。[比如说我们知道synchronized锁的是对象而不是代码,而锁的状态保存在对象头中,进而实现锁住对象]。

关于对象头和锁之间的转换,网上大神总结

偏向锁轻量级锁重量级锁

2.2.2 实例数据 存放对象程序中各种类型的字段类型,不管是从父类中继承下来的还是在子类中定义的。 分配策略:相同宽度的字段总是放在一起,比如double和long 2.2.3 对齐填充

这部分没有特殊的含义,仅仅起到占位符的作用满足JVM要求。

由于HotSpot规定对象的大小必须是8的整数倍,对象头刚好是整数倍,如果实例数据不是的话,就需要占位符对齐填充。 2.3 对象的访问定位

java程序需要通过引用(ref)数据来操作堆上面的对象,那么如何通过引用定位、访问到对象的具体位置。

对象的访问方式由虚拟机决定,java虚拟机提供两种主流的方式 1.句柄访问对象 2.直接指针访问对象。(Sun HotSpot使用这种方式)

参考Java对象访问定位

2.3.1 句柄访问

简单来说就是java堆划出一块内存作为句柄池,引用中存储对象的句柄地址,句柄中包含对象实例数据、类型数据的地址信息。

优点:引用中存储的是稳定的句柄地址,在对象被移动【垃圾收集时移动对象是常态】只需改变句柄中实例数据的指针,不需要改动引用【ref】本身。

访问方式2

2.3.2 直接指针

与句柄访问不同的是,ref中直接存储的就是对象的实例数据,但是类型数据跟句柄访问方式一样。

优点:优势很明显,就是速度快,相比于句柄访问少了一次指针定位的开销时间。【可能是出于Java中对象的访问时十分频繁的,平时我们常用的JVM HotSpot采用此种方式】

访问方式1

3.内存溢出 两种内存溢出异常[注意内存溢出是error级别的] 1.StackOverFlowError:当请求的栈深度大于虚拟机所允许的最大深度 2.OutOfMemoryError:虚拟机在扩展栈时无法申请到足够的内存空间[一般都能设置扩大]

java -verbose:class -version 可以查看刚开始加载的类,可以发现这两个类并不是异常出现的时候才去加载,而是jvm启动的时候就已经加载。这么做的原因是在vm启动过程中我们把类加载起来,并创建几个没有堆栈的对象缓存起来,只需要设置下不同的提示信息即可,当需要抛出特定类型的OutOfMemoryError异常的时候,就直接拿出缓存里的这几个对象就可以了。

比如说OutOfMemoryError对象,jvm预留出4个对象【固定常量】,这就为什么最多出现4次有堆栈的OutOfMemoryError异常及大部分情况下都将看到没有堆栈的OutOfMemoryError对象的原因。

参考OutOfMemoryError解读

Snip20180904_8.png

两个基本的例子

publicclassMemErrorTest{publicstaticvoidmain(String[] args){try{List<Object> list =newArrayList<Object>();for(;;){ list.add(newObject());//创建对象速度可能高于jvm回收速度}}catch(OutOfMemoryError e){ e.printStackTrace();}try{hi();//递归造成StackOverflowError 这边因为每运行一个方法将创建一个栈帧,栈帧创建太多无法继续申请到内存扩展}catch(StackOverflowError e){ e.printStackTrace();}}publicstaticvoidhi(){hi();}}

4.GC简介

GC(Garbage Collection):即垃圾回收器,诞生于1960年MIT的Lisp语言,主要是用来回收,释放垃圾占用的空间。

java GC泛指java的垃圾回收机制,该机制是java与C/C++的主要区别之一,我们在日常写java代码的时候,一般都不需要编写内存回收或者垃圾清理的代码,也不需要像C/C++那样做类似delete/free的操作。

4.1.为什么需要学习GC

对象的内存分配在java虚拟机的自动内存分配机制下,一般不容易出现内存泄漏问题。但是写代码难免会遇到一些特殊情况,比如OOM神马的。。尽管虚拟机内存的动态分配与内存回收技术很成熟,可万一出现了这样那样的内存溢出问题,那么将难以定位错误的原因所在。

对于本人来说,由于水平有限,而且作为小开发,并没必要深入到GC的底层实现,但至少想要说学会看懂gc及定位一些内存泄漏问题。

从三个角度切入来学习GC

1.哪些内存要回收

2.什么时候回收

3.怎么回收

哪些内存要回收

java内存模型中分为五大区域已经有所了解。我们知道程序计数器、虚拟机栈、本地方法栈,由线程而生,随线程而灭,其中栈中的栈帧随着方法的进入顺序的执行的入栈和出栈的操作,一个栈帧需要分配多少内存取决于具体的虚拟机实现并且在编译期间即确定下来【忽略JIT编译器做的优化,基本当成编译期间可知】,当方法或线程执行完毕后,内存就随着回收,因此无需关心。

而Java堆、方法区则不一样。方法区存放着类加载信息,但是一个接口中多个实现类需要的内存可能不太一样,一个方法中多个分支需要的内存也可能不一样【只有在运行期间才可知道这个方法创建了哪些对象没需要多少内存】,这部分内存的分配和回收都是动态的,gc关注的也正是这部分的内存。

Java堆是GC回收的“重点区域”。堆中基本存放着所有对象实例,gc进行回收前,第一件事就是确认哪些对象存活,哪些死去[即不可能再被引用] 4.2 堆的回收区域 为了高效的回收,jvm将堆分为三个区域 1.新生代(Young Generation)NewSize和MaxNewSize分别可以控制年轻代的初始大小和最大的大小 2.老年代(Old Generation) 3.永久代(Permanent Generation)【1.8以后采用元空间,就不在堆中了】

GC为什么要分代-R大的回答

关于元空间

5 判断对象是否存活算法 1.引用计数算法 早期判断对象是否存活大多都是以这种算法,这种算法判断很简单,简单来说就是给对象添加一个引用计数器,每当对象被引用一次就加1,引用失效时就减1。当为0的时候就判断对象不会再被引用。 优点:实现简单效率高,被广泛使用与如python何游戏脚本语言上。 缺点:难以解决循环引用的问题,就是假如两个对象互相引用已经不会再被其它其它引用,导致一直不会为0就无法进行回收。 2.可达性分析算法 目前主流的商用语言[如java、c#]采用的是可达性分析算法判断对象是否存活。这个算法有效解决了循环利用的弊端。 它的基本思路是通过一个称为“GC Roots”的对象为起始点,搜索所经过的路径称为引用链,当一个对象到GC Roots没有任何引用跟它连接则证明对象是不可用的。

gc

可作为GC Roots的对象有四种

①虚拟机栈(栈桢中的本地变量表)中的引用的对象,就是平时所指的java对象,存放在堆中。 ②方法区中的类静态属性引用的对象,一般指被static修饰引用的对象,加载类的时候就加载到内存中。 ③方法区中的常量引用的对象, ④本地方法栈中JNI(native方法)引用的对象

即使可达性算法中不可达的对象,也不是一定要马上被回收,还有可能被抢救一下。网上例子很多,基本上和深入理解JVM一书讲的一样对象的生存还是死亡

要真正宣告对象死亡需经过两个过程。 1.可达性分析后没有发现引用链 2.查看对象是否有finalize方法,如果有重写且在方法内完成自救[比如再建立引用],还是可以抢救一下,注意这边一个类的finalize只执行一次,这就会出现一样的代码第一次自救成功第二次失败的情况。[如果类重写finalize且还没调用过,会将这个对象放到一个叫做F-Queue的序列里,这边finalize不承诺一定会执行,这么做是因为如果里面死循环的话可能会时F-Queue队列处于等待,严重会导致内存崩溃,这是我们不希望看到的。]

HotSpot虚拟机如何实现可达性算法

5 垃圾收集算法

jvm中,可达性分析算法帮我们解决了哪些对象可以回收的问题,垃圾收集算法则关心怎么回收。

5.1 三大垃圾收集算法 1.标记/清除算法【最基础】 2.复制算法 3.标记/整理算法 jvm采用`分代收集算法`对不同区域采用不同的回收算法。

参考GC算法深度解析

新生代采用复制算法

新生代中因为对象都是"朝生夕死的",【深入理解JVM虚拟机上说98%的对象,不知道是不是这么多,总之就是存活率很低】,适用于复制算法【复制算法比较适合用于存活率低的内存区域】。它优化了标记/清除算法的效率和内存碎片问题,且JVM不以5:5分配内存【由于存活率低,不需要复制保留那么大的区域造成空间上的浪费,因此不需要按1:1【原有区域:保留空间】划分内存区域,而是将内存分为一块Eden空间和From Survivor、To Survivor【保留空间】,三者默认比例为8:1:1,优先使用Eden区,若Eden区满,则将对象复制到第二块内存区上。但是不能保证每次回收都只有不多于10%的对象存货,所以Survivor区不够的话,则会依赖老年代年存进行分配】。

GC开始时,对象只会存于Eden和From Survivor区域,To Survivor【保留空间】为空。

GC进行时,Eden区所有存活的对象都被复制到To Survivor区,而From Survivor区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阈值(默认15是因为对象头中年龄战4bit,新生代每熬过一次垃圾回收,年龄+1),则移到老年代,没有达到则复制到To Survivor。

老年代采用标记/清除算法或标记/整理算法

由于老年代存活率高,没有额外空间给他做担保,必须使用这两种算法。

5.2 枚举根节点算法

GC Roots 被虚拟机用来判断对象是否存活

可作为GC Roos的节点主要是在一些全局引用【如常量或静态属性】、执行上下文【如栈帧中本地变量表】中。那么如何在这么多全局变量和本地变量表找到【枚举】根节点将是个问题。

可达性分析算法需考虑

1.如果方法区几百兆,一个个检查里面的引用,将耗费大量资源。

2.在分析时,需保证这个对象引用关系不再变化,否则结果将不准确。【因此GC进行时需停掉其它所有java执行线程(Sun把这种行为称为‘Stop the World’),即使是号称几乎不会停顿的CMS收集器,枚举根节点时也需停掉线程】

解决办法:实际上当系统停下来后JVM不需要一个个检查引用,而是通过OopMap数据结构【HotSpot的叫法】来标记对象引用。

虚拟机先得知哪些地方存放对象的引用,在类加载完时。HotSpot把对象内什么偏移量什么类型的数据算出来,在jit编译过程中,也会在特定位置记录下栈和寄存器哪些位置是引用,这样GC在扫描时就可以知道这些信息。【目前主流JVM使用准确式GC】

OopMap可以帮助HotSpot快速且准确完成GC Roots枚举以及确定相关信息。但是也存在一个问题,可能导致引用关系变化。

这个时候有个safepoint(安全点)的概念。

HotSpot中GC不是在任意位置都可以进入,而只能在safepoint处进入。 GC时对一个Java线程来说,它要么处在safepoint,要么不在safepoint。

safepoint不能太少,否则GC等待的时间会很久

safepoint不能太多,否则将增加运行GC的负担

安全点主要存放的位置

1:循环的末尾 2:方法临返回前/调用方法的call指令后 3:可能抛异常的位置

参考:关于安全点safepoint

6.垃圾收集器 如果说垃圾回收算法是内存回收的方法论,那么垃圾收集器就是具体实现。jvm会结合针对不同的场景及用户的配置使用不同的收集器。 年轻代收集器 Serial、ParNew、Parallel Scavenge 老年代收集器 Serial Old、Parallel Old、CMS收集器 特殊收集器 G1收集器[新型,不在年轻、老年代范畴内]

收集器,连线代表可结合使用

新生代收集器 6.1 Serial

最基本、发展最久的收集器,在jdk3以前是gc收集器的唯一选择,Serial是单线程收集器,Serial收集器只能使用一条线程进行收集工作,在收集的时候必须得停掉其它线程,等待收集工作完成其它线程才可以继续工作。

虽然Serial看起来很坑,需停掉别的线程以完成自己的gc工作,但是也不是完全没用的,比如说Serial在运行在Client模式下优于其它收集器[简单高效,不过一般都是用Server模式,64bit的jvm甚至没Client模式]

JVM的Client模式与Server模式

优点:对于Client模式下的jvm来说是个好的选择。适用于单核CPU【现在基本都是多核了】

缺点:收集时要暂停其它线程,有点浪费资源,多核下显得。

6.2 ParNew收集器

可以认为是Serial的升级版,因为它支持多线程[GC线程],而且收集算法、Stop The World、回收策略和Serial一样,就是可以有多个GC线程并发运行,它是HotSpot第一个真正意义实现并发的收集器。默认开启线程数和当前cpu数量相同【几核就是几个,超线程cpu的话就不清楚了 - -】,如果cpu核数很多不想用那么多,可以通过*-XX:ParallelGCThreads*来控制垃圾收集线程的数量。

优点:1.支持多线程,多核CPU下可以充分的利用CPU资源 2.运行在Server模式下新生代首选的收集器【重点是因为新生代的这几个收集器只有它和Serial可以配合CMS收集器一起使用】 缺点: 在单核下表现不会比Serial好,由于在单核能利用多核的优势,在线程收集过程中可能会出现频繁上下文切换,导致额外的开销。 6.3 Parallel Scavenge

采用复制算法的收集器,和ParNew一样支持多线程。

但是该收集器重点关心的是吞吐量【吞吐量 = 代码运行时间 / (代码运行时间 + 垃圾收集时间) 如果代码运行100min垃圾收集1min,则为99%】

对于用户界面,适合使用GC停顿时间短,不然因为卡顿导致交互界面卡顿将很影响用户体验。

对于后台

高吞吐量可以高效率的利用cpu尽快完成程序运算任务,适合后台运算

Parallel Scavenge注重吞吐量,所以也成为"吞吐量优先"收集器。

老年代收集器 6.4 Serial Old

和新生代的Serial一样为单线程,Serial的老年代版本,不过它采用"标记-整理算法",这个模式主要是给Client模式下的JVM使用。

如果是Server模式有两大用途

1.jdk5前和Parallel Scavenge搭配使用,jdk5前也只有这个老年代收集器可以和它搭配。

2.作为CMS收集器的后备。

6.5 Parallel Old

支持多线程,Parallel Scavenge的老年版本,jdk6开始出现, 采用"标记-整理算法"【老年代的收集器大都采用此算法】

在jdk6以前,新生代的Parallel Scavenge只能和Serial Old配合使用【根据图,没有这个的话只剩Serial Old,而Parallel Scavenge又不能和CMS配合使用】,而且Serial Old为单线程Server模式下会拖后腿【多核cpu下无法充分利用】,这种结合并不能让应用的吞吐量最大化。

Parallel Old的出现结合Parallel Scavenge,真正的形成“吞吐量优先”的收集器组合。

6.6 CMS

CMS收集器(Concurrent Mark Sweep)是以一种获取最短回收停顿时间为目标的收集器。【重视响应,可以带来好的用户体验,被sun称为并发低停顿收集器】

启用CMS:-XX:+UseConcMarkSweepGC

正如其名,CMS采用的是"标记-清除"(Mark Sweep)算法,而且是支持并发(Concurrent)的

它的运作分为4个阶段

1.初始标记:标记一下GC Roots能直接关联到的对象,速度很快 2.并发标记:GC Roots Tarcing过程,即可达性分析 3.重新标记:为了修正因并发标记期间用户程序运作而产生变动的那一部分对象的标记记录,会有些许停顿,时间上一般 初始标记 < 重新标记 < 并发标记 4.并发清除

以上初始标记和重新标记需要stw(停掉其它运行java线程)

之所以说CMS的用户体验好,是因为CMS收集器的内存回收工作是可以和用户线程一起并发执行。

总体上CMS是款优秀的收集器,但是它也有些缺点。

1.cms堆cpu特别敏感,cms运行线程和应用程序并发执行需要多核cpu,如果cpu核数多的话可以发挥它并发执行的优势,但是cms默认配置启动的时候垃圾线程数为 (cpu数量+3)/4,它的性能很容易受cpu核数影响,当cpu的数目少的时候比如说为为2核,如果这个时候cpu运算压力比较大,还要分一半给cms运作,这可能会很大程度的影响到计算机性能。

2.cms无法处理浮动垃圾,可能导致Concurrent Mode Failure(并发模式故障)而触发full GC

3.由于cms是采用"标记-清除“算法,因此就会存在垃圾碎片的问题,为了解决这个问题cms提供了 -XX:+UseCMSCompactAtFullCollection选项,这个选项相当于一个开关【默认开启】,用于CMS顶不住要进行full GC时开启内存碎片合并,内存整理的过程是无法并发的,且开启这个选项会影响性能(比如停顿时间变长)

浮动垃圾:由于cms支持运行的时候用户线程也在运行,程序运行的时候会产生新的垃圾,这里产生的垃圾就是浮动垃圾,cms无法当次处理,得等下次才可以。 6.7 G1收集器

G1(garbage first:尽可能多收垃圾,避免full gc)收集器是当前最为前沿的收集器之一(1.7以后才开始有),同cms一样也是关注降低延迟,是用于替代cms功能更为强大的新型收集器,因为它解决了cms产生空间碎片等一系列缺陷。

摘自甲骨文:适用于 Java HotSpot VM 的低暂停、服务器风格的分代式垃圾回收器。G1 GC 使用并发和并行阶段实现其目标暂停时间,并保持良好的吞吐量。当 G1 GC 确定有必要进行垃圾回收时,它会先收集存活数据最少的区域(垃圾优先)

g1的特别之处在于它强化了分区,弱化了分代的概念,是区域化、增量式的收集器,它不属于新生代也不属于老年代收集器。

用到的算法为标记-清理、复制算法

jdk1.7,1.8的都是默认关闭的,更高版本的还不知道 开启选项 -XX:+UseG1GC 比如在tomcat的catania.sh启动参数加上

g1是区域化的,它将java堆内存划分为若干个大小相同的区域【region】,jvm可以设置每个region的大小(1-32m,大小得看堆内存大小,必须是2的幂),它会根据当前的堆内存分配合理的region大小。

jdk7中计算region的源码,这边博主看了下也看不怎么懂,也翻了下openjdk8的看了下关于region的处理似乎不太一样。。

g1通过并发(并行)标记阶段查找老年代存活对象,通过并行复制压缩存活对象【这样可以省出连续空间供大对象使用】。

g1将一组或多组区域中存活对象以增量并行的方式复制到不同区域进行压缩,从而减少堆碎片,目标是尽可能多回收堆空间【垃圾优先】,且尽可能不超出暂停目标以达到低延迟的目的。

g1提供三种垃圾回收模式 young gc、mixed gc 和 full gc,不像其它的收集器,根据区域而不是分代,新生代老年代的对象它都能回收。

几个重要的默认值,更多的查看官方文档oracle官方g1中文文档

g1是自适应的回收器,提供了若干个默认值,无需修改就可高效运作 -XX:G1HeapRegionSize=n 设置g1 region大小,不设置的话自己会根据堆大小算,目标是根据最小堆内存划分2048个区域 -XX:MaxGCPauseMillis=200 最大停顿时间 默认200毫秒 7 Minor GC、Major GC、FULL GC、mixed gc 7.1 Minor GC

在年轻代Young space(包括Eden区和Survivor区)中的垃圾回收称之为 Minor GC,Minor GC只会清理年轻代.

7.2 Major GC

Major GC清理老年代(old GC),但是通常也可以指和Full GC是等价,因为收集老年代的时候往往也会伴随着升级年轻代,收集整个Java堆。所以有人问的时候需问清楚它指的是full GC还是old GC。

7.3 Full GC

full gc是对新生代、老年代、永久代【jdk1.8后没有这个概念了】统一的回收。

【知乎R大的回答:收集整个堆,包括young gen、old gen、perm gen(如果存在的话)、元空间(1.8及以上)等所有部分的模式】

7.4 mixed GC【g1特有】

混合GC

收集整个young gen以及部分old gen的GC。只有G1有这个模式

8 查看GC日志 8.1 简单日志查看

要看得懂并理解GC,需要看懂GC日志。

这边我在idea上试了个小例子,需要在idea配置参数(-XX:+PrintGCDetails)。

publicclassGCtest{publicstaticvoidmain(String[] args){for(int i =0; i <10000; i++){List<String> list =newArrayList<>(); list.add("aaaaaaaaaaaaa");}System.gc();}} [GC (System.gc()) [PSYoungGen: 3998K->688K(38400K)] 3998K->696K(125952K), 0.0016551 secs[本次回收时间]] [Times: user=0.01 sys=0.00, real=0.00 secs] [Full GC (System.gc()) [PSYoungGen: 688K->0K(38400K)] [ParOldGen: 8K->603K(87552K)] 696K->603K(125952K), [Metaspace: 3210K->3210K(1056768K)], 0.0121034 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] Heap PSYoungGen[年轻代] total 38400K, used 333K [0x0000000795580000, 0x0000000798000000, 0x00000007c0000000) eden space 33280K, 1% used [0x0000000795580000,0x00000007955d34a8,0x0000000797600000) from space 5120K, 0% used [0x0000000797600000,0x0000000797600000,0x0000000797b00000) to space 5120K, 0% used [0x0000000797b00000,0x0000000797b00000,0x0000000798000000) ParOldGen[老年代] total 87552K, used 603K [0x0000000740000000, 0x0000000745580000, 0x0000000795580000) object space 87552K, 0% used [0x0000000740000000,0x0000000740096fe8,0x0000000745580000) Metaspace[元空间] used 3217K, capacity 4496K, committed 4864K, reserved 1056768K class space used 352K, capacity 388K, committed 512K, reserved 1048576K 8.2 离线工具查看

比如sun的gchistogcviewer离线分析工具,做个笔记先了解下还没用过,可视化好像很好用的样子。

8.3 自带的jconsole工具、jstat命令

终端输入jconsole就会出现jdk自带的gui监控工具

jconsole

可以根据内存使用情况间接了解内存使用和gc情况

jconsole

jstat命令

比如jstat -gcutil pid查看对应java进程gc情况

jstat

s0: 新生代survivor space0简称 就是准备复制的那块 单位为% s1:指新生代s1已使用百分比,为0的话说明没有存活对象到这边 e:新生代eden(伊甸园)区域(%)o:老年代(%)ygc:新生代 次数 ygct:minor gc耗时 fgct:full gc耗时(秒)GCT: ygct+fgct 耗时 几个疑问 1.GC是怎么判断对象是被标记的

通过枚举根节点的方式,通过jvm提供的一种oopMap的数据结构,简单来说就是不要再通过去遍历内存里的东西,而是通过OOPMap的数据结构去记录该记录的信息,比如说它可以不用去遍历整个栈,而是扫描栈上面引用的信息并记录下来。

总结:通过OOPMap把栈上代表引用的位置全部记录下来,避免全栈扫描,加快枚举根节点的速度,除此之外还有一个极为重要的作用,可以帮HotSpot实现准确式GC【这边的准确关键就是类型,可以根据给定位置的某块数据知道它的准确类型,HotSpot是通过oopMap外部记录下这些信息,存成映射表一样的东西】。

2.什么时候触发GC

简单来说,触发的条件就是GC算法区域满了或将满了。

minor GC(young GC):当年轻代中eden区分配满的时候触发[值得一提的是因为young GC后部分存活的对象会已到老年代(比如对象熬过15轮),所以过后old gen的占用量通常会变高] full GC: ①手动调用System.gc()方法 [增加了full GC频率,不建议使用而是让jvm自己管理内存,可以设置-XX:+ DisableExplicitGC来禁止RMI调用System.gc] ②发现perm gen(如果存在永久代的话)需分配空间但已经没有足够空间 ③老年代空间不足,比如说新生代的大对象大数组晋升到老年代就可能导致老年代空间不足。 ④CMS GC时出现Promotion Faield[pf] ⑤统计得到的Minor GC晋升到旧生代的平均大小大于老年代的剩余空间。 这个比较难理解,这是HotSpot为了避免由于新生代晋升到老年代导致老年代空间不足而触发的FUll GC。 比如程序第一次触发Minor GC后,有5m的对象晋升到老年代,姑且现在平均算5m,那么下次Minor GC发生时,先判断现在老年代剩余空间大小是否超过5m,如果小于5m,则HotSpot则会触发full GC(这点挺智能的) Promotion Faield:minor GC时 survivor space放不下[满了或对象太大],对象只能放到老年代,而老年代也放不下会导致这个错误。 Concurrent Model Failure:cms时特有的错误,因为cms时垃圾清理和用户线程可以是并发执行的,如果在清理的过程中 可能原因: 1 cms触发太晚,可以把XX:CMSInitiatingOccupancyFraction调小[比如-XX:CMSInitiatingOccupancyFraction=70 是指设定CMS在对内存占用率达到70%的时候开始GC(因为CMS会有浮动垃圾,所以一般都较早启动GC)] 2 垃圾产生速度大于清理速度,可能是晋升阈值设置过小,Survivor空间小导致跑到老年代,eden区太小,存在大对象、数组对象等情况 3.空间碎片过多,可以开启空间碎片整理并合理设置周期时间

full gc导致了concurrent mode failure,而不是因为concurrent mode failure错误导致触发full gc,真正触发full gc的原因可能是ygc时发生的promotion failure。

3.cms收集器是否会扫描年轻代

会,在初始标记的时候会扫描新生代。

虽然cms是老年代收集器,但是我们知道年轻代的对象是可以晋升为老年代的,为了空间分配担保,还是有必要去扫描年轻代。

4.什么是空间分配担保

在minor gc前,jvm会先检查老年代最大可用空间是否大于新生代所有对象总空间,如果是的话,则minor gc可以确保是安全的,

如果担保失败,会检查一个配置(HandlePromotionFailire),即是否允许担保失败。

如果允许:继续检查老年代最大可用可用的连续空间是否大于之前晋升的平均大小,比如说剩10m,之前每次都有9m左右的新生代到老年代,那么将尝试一次minor gc(大于的情况),这会比较冒险。

如果不允许,而且还小于的情况,则会触发full gc。【为了避免经常full GC 该参数建议打开】

这边为什么说是冒险是因为minor gc过后如果出现大对象,由于新生代采用复制算法,survivor无法容纳将跑到老年代,所以才会去计算之前的平均值作为一种担保的条件与老年代剩余空间比较,这就是分配担保。

这种担保是动态概率的手段,但是也有可能出现之前平均都比较低,突然有一次minor gc对象变得很多远高于以往的平均值,这个时候就会导致担保失败【Handle Promotion Failure】,这就只好再失败后再触发一次FULL GC,

5.为什么复制算法要分两个Survivor,而不直接移到老年代

这样做的话效率可能会更高,但是old区一般都是熬过多次可达性分析算法过后的存活的对象,要求比较苛刻且空间有限,而不能直接移过去,这将导致一系列问题(比如老年代容易被撑爆)

分两个Survivor(from/to),自然是为了保证复制算法运行以提高效率。

6.各个版本的JVM使用的垃圾收集器是怎么样的

准确来说,垃圾收集器的使用跟当前jvm也有很大的关系,比如说g1是jdk7以后的版本才开始出现。

并不是所有的垃圾收集器都是默认开启的,有些得通过设置相应的开关参数才会使用。比如说cms,需设置(XX:+UseConcMarkSweepGC)

这边有几个实用的命令,比如说server模式下

#UnlockExperimentalVMOptions UnlockDiagnosticVMOptions解锁获取jvm参数,PrintFlagsFinal用于输出xx相关参数,以Benchmark类测试,这边会有很多结果 大都看不懂- - 在这边查(usexxxxxxgc会看到jvm不同收集器的开关情况) java -server -XX:+UnlockExperimentalVMOptions -XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsFinal Benchmark #后面跟| grep ":"获取已赋值的参数[加:代表被赋值过] java -server -XX:+UnlockExperimentalVMOptions -XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsFinal Benchmark| grep ":" #获得用户自定义的设置或者jvm设置的详细的xx参数和值 java -server -XX:+PrintCommandLineFlags Benchmark

本人用的jdk8,这边UseParallelGC为true,参考深入理解jvm那本书说这个是Parallel Scavenge+Serial old搭配组合的开关,但是网上又说8默认是Parallel Scavenge+Parallel Old,我还是信书的吧 - -。

更多相关参数来源

常用参数

据说更高版本的jvm默认使用g1

7 stop the world具体是什么,有没有办法避免

stop the world简单来说就是gc的时候,停掉除gc外的java线程。

无论什么gc都难以避免停顿,即使是g1也会在初始标记阶段发生,stw并不可怕,可以尽可能的减少停顿时间。

8 新生代什么样的情况会晋升为老年代

对象优先分配在eden区,eden区满时会触发一次minor GC

对象晋升规则

1 长期存活的对象进入老年代,对象每熬过一次GC年龄+1(默认年龄阈值15,可配置)。

2 对象太大新生代无法容纳则会分配到老年代

3 eden区满了,进行minor gc后,eden和一个survivor区仍然存活的对象无法放到(to survivor区)则会通过分配担保机制放到老年代,这种情况一般是minor gc后新生代存活的对象太多。

4 动态年龄判定,为了使内存分配更灵活,jvm不一定要求对象年龄达到MaxTenuringThreshold(15)才晋升为老年代,若survior区相同年龄对象总大小大于survior区空间的一半,则大于等于这个年龄的对象将会在minor gc时移到老年代

8.怎么理解g1,适用于什么场景

G1 GC 是区域化、并行-并发、增量式垃圾回收器,相比其他 HotSpot 垃圾回收器,可提供更多可预测的暂停。增量的特性使 G1 GC 适用于更大的堆,在最坏的情况下仍能提供不错的响应。G1 GC 的自适应特性使 JVM 命令行只需要软实时暂停时间目标的最大值以及 Java 堆大小的最大值和最小值,即可开始工作。

g1不再区分老年代、年轻代这样的内存空间,这是较以往收集器很大的差异,所有的内存空间就是一块划分为不同子区域,每个区域大小为1m-32m,最多支持的内存为64g左右,且由于它为了的特性适用于大内存机器。

g1回收时堆内存情况

适用场景:

1.像cms能与应用程序并发执行,GC停顿短【短而且可控】,用户体验好的场景。

2.面向服务端,大内存,高cpu的应用机器。【网上说差不多是6g或更大】

3.应用在运行过程中经常会产生大量内存碎片,需要压缩空间【比cms好的地方之一,g1具备压缩功能】。

参考

深入理解Java虚拟机

JVM内存模型、指令重排、内存屏障概念解析

Java对象头

GC收集器

Major GC和Full GC的区别

JVM 垃圾回收 Minor gc vs Major gc vs Full gc

关于准确式GC、保守式GC

关于CMS垃圾收集算法的一些疑惑

图解cms

G1垃圾收集器介绍

详解cms回收机制

总结

JMM 是一种规范,是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题,而且写java代码的时候难免会经常和内存打交道,遇到各种内存溢出问题,有时候又难以定位问题,因此是一定要学习jmm以及GC的。

由于博主本人水平有限【目前还是小菜鸡】,所以花了点时间,写下这篇博客当做为笔记总结归纳,但是写博客这种事如果全都是照抄别人的成果就很没意思了,吸收别人的成果的同时,也希望自己有能力多写点自己独特的理解和干货后续继续更新,所以如果有哪里写的不好或写错请指出,以便我继续学习和改进。

read more
JUC 概览

在Java中,线程部分是一个重点,本篇文章说的JUC也是关于线程的。JUC就是java.util .concurrent工具包的简称。这是一个处理线程的工具包,JDK 1.5开始出现的。下面一起来看看它怎么使用。

一、volatile关键字与内存可见性

1、内存可见性:

先来看看下面的一段代码:

public class TestVolatile { public static void main(String[] args){ //这个线程是用来读取flag的值的 ThreadDemo threadDemo = new ThreadDemo(); Thread thread = new Thread(threadDemo); thread.start(); while (true){ if (threadDemo.isFlag()){ System.out.println("主线程读取到的flag = " + threadDemo.isFlag()); break; } } } } @Data class ThreadDemo implements Runnable{ //这个线程是用来修改flag的值的 public boolean flag = false; @Override public void run() { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } flag = true; System.out.println("ThreadDemo线程修改后的flag = " + isFlag()); } }

这段代码很简单,就是一个ThreadDemo类继承Runnable创建一个线程。它有一个成员变量flag为false,然后重写run方法,在run方法里面将flag改为true,同时还有一条输出语句。然后就是main方法主线程去读取flag。如果flag为true,就会break掉while循环,否则就是死循环。按道理,下面那个线程将flag改为true了,主线程读取到的应该也是true,循环应该会结束。看看运行结果:

从图中可以看到,该程序并没有结束,也就是死循环。说明主线程读取到的flag还是false,可是另一个线程明明将flag改为true了,而且打印出来了,这是什么原因呢?这就是内存可见性问题。

内存可见性问题:当多个线程操作共享数据时,彼此不可见。

看下图理解上述代码:

要解决这个问题,可以加锁。如下:

while (true){ synchronized (threadDemo){ if (threadDemo.isFlag()){ System.out.println("主线程读取到的flag = " + threadDemo.isFlag()); break; } } }

加了锁,就可以让while循环每次都从主存中去读取数据,这样就能读取到true了。但是一加锁,每次只能有一个线程访问,当一个线程持有锁时,其他的就会阻塞,效率就非常低了。不想加锁,又要解决内存可见性问题,那么就可以使用volatile关键字。

2、volatile关键字:

用法:

volatile关键字:当多个线程操作共享数据时,可以保证内存中的数据可见。用这个关键字修饰共享数据,就会及时的把线程缓存中的数据刷新到主存中去,也可以理解为,就是直接操作主存中的数据。所以在不使用锁的情况下,可以使用volatile。如下:

public volatile boolean flag = false;

这样就可以解决内存可见性问题了。

volatile和synchronized的区别:

volatile不具备互斥性(当一个线程持有锁时,其他线程进不来,这就是互斥性)。

volatile不具备原子性。

二、原子性

1、理解原子性:

上面说到volatile不具备原子性,那么原子性到底是什么呢?先看如下代码:

public class TestIcon { public static void main(String[] args){ AtomicDemo atomicDemo = new AtomicDemo(); for (int x = 0;x < 10; x++){ new Thread(atomicDemo).start(); } } } class AtomicDemo implements Runnable{ private int i = 0; public int getI(){ return i++; } @Override public void run() { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(getI()); } }

这段代码就是在run方法里面让i++,然后启动十个线程去访问。看看结果:

可以发现,出现了重复数据。明显产生了多线程安全问题,或者说原子性问题。所谓原子性就是操作不可再细分,而i++操作分为读改写三步,如下:

int temp = i; i = i+1; i = temp;

所以i++明显不是原子操作。上面10个线程进行i++时,内存图解如下:

看到这里,好像和上面的内存可见性问题一样。是不是加个volatile关键字就可以了呢?其实不是的,因为加了volatile,只是相当于所有线程都是在主存中操作数据而已,但是不具备互斥性。比如两个线程同时读取主存中的0,然后又同时自增,同时写入主存,结果还是会出现重复数据。

2、原子变量:

JDK 1.5之后,Java提供了原子变量,在java.util.concurrent.atomic包下。原子变量具备如下特点:

有volatile保证内存可见性。 用CAS算法保证原子性。

3、CAS算法:

CAS算法是计算机硬件对并发操作共享数据的支持,CAS包含3个操作数:

内存值V 预估值A 更新值B

当且仅当V==B时,才会把B的值赋给V,即V = B,否则不做任何操作。就上面的i++问题,CAS算法是这样处理的:首先V是主存中的值0,然后预估值A也是0,因为此时还没有任何操作,这时V=B,所以进行自增,同时把主存中的值变为1。如果第二个线程读取到主存中的还是0也没关系,因为此时预估值已经变成1,V不等于B,所以不进行任何操作。

4、使用原子变量改进i++问题:

原子变量用法和包装类差不多,如下:

//private int i = 0; AtomicInteger i = new AtomicInteger(); public int getI(){ return i.getAndIncrement(); }

只改这两处即可。

三、锁分段机制

JDK 1.5之后,在java.util.concurrent包中提供了多种并发容器类来改进同步容器类的性能。其中最主要的就是ConcurrentHashMap。

1、ConcurrentHashMap:

ConcurrentHashMap就是一个线程安全的hash表。我们知道HashMap是线程不安全的,Hash Table加了锁,是线程安全的,因此它效率低。HashTable加锁就是将整个hash表锁起来,当有多个线程访问时,同一时间只能有一个线程访问,并行变成串行,因此效率低。所以JDK1.5后提供了ConcurrentHashMap,它采用了锁分段机制。

如上图所示,ConcurrentHashMap默认分成了16个segment,每个Segment都对应一个Hash表,且都有独立的锁。所以这样就可以每个线程访问一个Segment,就可以并行访问了,从而提高了效率。这就是锁分段。**但是,**java 8 又更新了,不再采用锁分段机制,也采用CAS算法了。

2、用法:

java.util.concurrent包还提供了设计用于多线程上下文中的 Collection 实现: ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、CopyOnWriteArrayList 和 CopyOnWriteArraySet。当期望许多线程访问一个给 定 collection 时,ConcurrentHashMap 通常优于同步的 HashMap, ConcurrentSkipListMap 通常优于同步的 TreeMap。当期望的读数和遍历远远 大于列表的更新数时,CopyOnWriteArrayList 优于同步的 ArrayList。下面看看部分用法:

public class TestConcurrent { public static void main(String[] args){ ThreadDemo2 threadDemo2 = new ThreadDemo2(); for (int i=0;i<10;i++){ new Thread(threadDemo2).start(); } } } //10个线程同时访问 class ThreadDemo2 implements Runnable{ private static List<String> list = Collections.synchronizedList(new ArrayList<>());//普通做法 static { list.add("aaa"); list.add("bbb"); list.add("ccc"); } @Override public void run() { Iterator<String> iterator = list.iterator(); while (iterator.hasNext()){ System.out.println(iterator.next());//读 list.add("ddd");//写 } } }

10个线程并发访问这个集合,读取集合数据的同时再往集合中添加数据。运行这段代码会报错,并发修改异常。

将创建集合方式改成:

private static CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();

这样就不会有并发修改异常了。因为这个是写入并复制,每次生成新的,所以如果添加操作比较多的话,开销非常大,适合迭代操作比较多的时候使用。

四、闭锁

java.util.concurrent包中提供了多种并发容器类来改进同步容器的性能。ContDownLatch是一个同步辅助类,在完成某些运算时,只有其他所有线程的运算全部完成,当前运算才继续执行,这就叫闭锁。看下面代码:

public class TestCountDownLatch { public static void main(String[] args){ LatchDemo ld = new LatchDemo(); long start = System.currentTimeMillis(); for (int i = 0;i<10;i++){ new Thread(ld).start(); } long end = System.currentTimeMillis(); System.out.println("耗费时间为:"+(end - start)+"秒"); } } class LatchDemo implements Runnable{ private CountDownLatch latch; public LatchDemo(){ } @Override public void run() { for (int i = 0;i<5000;i++){ if (i % 2 == 0){//50000以内的偶数 System.out.println(i); } } } }

这段代码就是10个线程同时去输出5000以内的偶数,然后在主线程那里计算执行时间。其实这是计算不了那10个线程的执行时间的,因为主线程与这10个线程也是同时执行的,可能那10个线程才执行到一半,主线程就已经输出“耗费时间为x秒”这句话了。所有要想计算这10个线程执行的时间,就得让主线程先等待,等10个分线程都执行完了才能执行主线程。这就要用到闭锁。看如何使用:

public class TestCountDownLatch { public static void main(String[] args) { final CountDownLatch latch = new CountDownLatch(10);//有多少个线程这个参数就是几 LatchDemo ld = new LatchDemo(latch); long start = System.currentTimeMillis(); for (int i = 0; i < 10; i++) { new Thread(ld).start(); } try { latch.await();//这10个线程执行完之前先等待 } catch (InterruptedException e) { } long end = System.currentTimeMillis(); System.out.println("耗费时间为:" + (end - start)); } } class LatchDemo implements Runnable { private CountDownLatch latch; public LatchDemo(CountDownLatch latch) { this.latch = latch; } @Override public void run() { synchronized (this) { try { for (int i = 0; i < 50000; i++) { if (i % 2 == 0) {//50000以内的偶数 System.out.println(i); } } } finally { latch.countDown();//每执行完一个就递减一个 } } } }

如上代码,主要就是用latch.countDown()和latch.await()实现闭锁,详细请看上面注释即可。

五、创建线程的方式 --- 实现Callable接口

直接看代码:

public class TestCallable { public static void main(String[] args){ CallableDemo callableDemo = new CallableDemo(); //执行callable方式,需要FutureTask实现类的支持,用来接收运算结果 FutureTask<Integer> result = new FutureTask<>(callableDemo); new Thread(result).start(); //接收线程运算结果 try { Integer sum = result.get();//当上面的线程执行完后,才会打印结果。跟闭锁一样。所有futureTask也可以用于闭锁 System.out.println(sum); } catch (Exception e) { e.printStackTrace(); } } } class CallableDemo implements Callable<Integer>{ @Override public Integer call() throws Exception { int sum = 0; for (int i = 0;i<=100;i++){ sum += i; } return sum; } }

现在Callable接口和实现Runable接口的区别就是,Callable带泛型,其call方法有返回值。使用的时候,需要用FutureTask来接收返回值。而且它也要等到线程执行完调用get方法才会执行,也可以用于闭锁操作。

六、Lock同步锁

在JDK1.5之前,解决多线程安全问题有两种方式(sychronized隐式锁):

同步代码块 同步方法

在JDK1.5之后,出现了更加灵活的方式(Lock显式锁):

同步锁

Lock需要通过lock()方法上锁,通过unlock()方法释放锁。为了保证锁能释放,所有unlock方法一般放在finally中去执行。

再来看一下卖票案例:

public class TestLock { public static void main(String[] args) { Ticket td = new Ticket(); new Thread(td, "窗口1").start(); new Thread(td, "窗口2").start(); new Thread(td, "窗口3").start(); } } class Ticket implements Runnable { private int ticket = 100; @Override public void run() { while (true) { if (ticket > 0) { try { Thread.sleep(200); } catch (Exception e) { } System.out.println(Thread.currentThread().getName() + "完成售票,余票为:" + (--ticket)); } } } }

多个线程同时操作共享数据ticket,所以会出现线程安全问题。会出现同一张票卖了好几次或者票数为负数的情况。以前用同步代码块和同步方法解决,现在看看用同步锁怎么解决。

class Ticket implements Runnable { private Lock lock = new ReentrantLock();//创建lock锁 private int ticket = 100; @Override public void run() { while (true) { lock.lock();//上锁 try { if (ticket > 0) { try { Thread.sleep(200); } catch (Exception e) { } System.out.println(Thread.currentThread().getName() + "完成售票,余票为:" + (--ticket)); } }finally { lock.unlock();//释放锁 } } } }

直接创建lock对象,然后用lock()方法上锁,最后用unlock()方法释放锁即可。

七、等待唤醒机制

1、虚假唤醒问题:

生产消费模式是等待唤醒机制的一个经典案例,看下面的代码:

public class TestProductorAndconsumer { public static void main(String[] args){ Clerk clerk = new Clerk(); Productor productor = new Productor(clerk); Consumer consumer = new Consumer(clerk); new Thread(productor,"生产者A").start(); new Thread(consumer,"消费者B").start(); } } //店员 class Clerk{ private int product = 0;//共享数据 public synchronized void get(){ //进货 if(product >= 10){ System.out.println("产品已满"); }else { System.out.println(Thread.currentThread().getName()+":"+ (++product)); } } public synchronized void sell(){//卖货 if (product <= 0){ System.out.println("缺货"); }else { System.out.println(Thread.currentThread().getName()+":"+ (--product)); } } } //生产者 class Productor implements Runnable{ private Clerk clerk; public Productor(Clerk clerk){ this.clerk = clerk; } @Override public void run() { for (int i = 0;i<20;i++){ clerk.get(); } } } //消费者 class Consumer implements Runnable{ private Clerk clerk; public Consumer(Clerk clerk){ this.clerk = clerk; } @Override public void run() { for (int i = 0;i<20;i++){ clerk.sell(); } } }

这就是生产消费模式的案例,这里没有使用等待唤醒机制,运行结果就是即使是缺货状态,它也会不断的去消费,也会一直打印“缺货”,即使是产品已满状态,也会不断地进货。用等待唤醒机制改进:

//店员 class Clerk{ private int product = 0;//共享数据 public synchronized void get(){ //进货 if(product >= 10){ System.out.println("产品已满"); try { this.wait();//满了就等待 } catch (InterruptedException e) { e.printStackTrace(); } }else { System.out.println(Thread.currentThread().getName()+":"+ (++product)); this.notifyAll();//没满就可以进货 } } public synchronized void sell(){//卖货 if (product <= 0){ System.out.println("缺货"); try { this.wait();//缺货就等待 } catch (InterruptedException e) { e.printStackTrace(); } }else { System.out.println(Thread.currentThread().getName()+":"+ (--product)); this.notifyAll();//不缺货就可以卖 } } }

这样就不会出现上述问题了。没有的时候就生产,生产满了就通知消费,消费完了再通知生产。但是这样还是有点问题,将上述代码做如下改动:

if(product >= 1){ //把原来的10改成1 System.out.println("产品已满"); ...... public void run() { try { Thread.sleep(200);//睡0.2秒 } catch (InterruptedException e) { e.printStackTrace(); } for (int i = 0;i<20;i++){ clerk.sell(); } }

就做这两处修改,再次运行,发现虽然结果没问题,但是程序却一直没停下来。出现这种情况是因为有一个线程在等待,而另一个线程没有执行机会了,唤醒不了这个等待的线程了,所以程序就无法结束。解决办法就是把get和sell方法里面的else去掉,不要用else包起来。但是,即使这样,如果再多加两个线程,就会出现负数了。

new Thread(productor, "生产者C").start(); new Thread(consumer, "消费者D").start();

运行结果:

一个消费者线程抢到执行权,发现product是0,就等待,这个时候,另一个消费者又抢到了执行权,product是0,还是等待,此时两个消费者线程在同一处等待。然后当生产者生产了一个product后,就会唤醒两个消费者,发现product是1,同时消费,结果就出现了0和-1。这就是虚假唤醒。解决办法就是把if判断改成while。如下:

public synchronized void get() { //进货 while (product >= 1) { System.out.println("产品已满"); try { this.wait();//满了就等待 } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName() + ":" + (++product)); this.notifyAll();//没满就可以进货 } public synchronized void sell() {//卖货 while (product <= 0) {//为了避免虚假唤醒问题,wait方法应该总是在循环中使用 System.out.println("缺货"); try { this.wait();//缺货就等待 } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName() + ":" + (--product)); this.notifyAll();//不缺货就可以卖 }

只需要把if改成while,每次都再去判断一下,就可以了。

2、用Lock锁实现等待唤醒:

class Clerk { private int product = 0;//共享数据 private Lock lock = new ReentrantLock();//创建锁对象 private Condition condition = lock.newCondition();//获取condition实例 public void get() { //进货 lock.lock();//上锁 try { while (product >= 1) { System.out.println("产品已满"); try { condition.await();//满了就等待 } catch (InterruptedException e) { } } System.out.println(Thread.currentThread().getName() + ":" + (++product)); condition.signalAll();//没满就可以进货 }finally { lock.unlock();//释放锁 } } public void sell() {//卖货 lock.lock();//上锁 try { while (product <= 0) { System.out.println("缺货"); try { condition.await();//缺货就等待 } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName() + ":" + (--product)); condition.signalAll();//不缺货就可以卖 }finally { lock.unlock();//释放锁 } } }

使用lock同步锁,就不需要sychronized关键字了,需要创建lock对象和condition实例。condition的await()方法、signal()方法和signalAll()方法分别与wait()方法、notify()方法和notifyAll()方法对应。

3、线程按序交替:

首先来看一道题:

编写一个程序,开启 3 个线程,这三个线程的 ID 分别为 A、B、C, 每个线程将自己的 ID 在屏幕上打印 10 遍,要求输出的结果必须按顺序显示。 如:ABCABCABC…… 依次递归

分析:

线程本来是抢占式进行的,要按序交替,所以必须实现线程通信, 那就要用到等待唤醒。可以使用同步方法,也可以用同步锁。

编码实现:

public class TestLoopPrint { public static void main(String[] args) { AlternationDemo ad = new AlternationDemo(); new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 10; i++) { ad.loopA(); } } }, "A").start(); new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 10; i++) { ad.loopB(); } } }, "B").start(); new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 10; i++) { ad.loopC(); } } }, "C").start(); } } class AlternationDemo { private int number = 1;//当前正在执行的线程的标记 private Lock lock = new ReentrantLock(); Condition condition1 = lock.newCondition(); Condition condition2 = lock.newCondition(); Condition condition3 = lock.newCondition(); public void loopA() { lock.lock(); try { if (number != 1) { //判断 condition1.await(); } System.out.println(Thread.currentThread().getName());//打印 number = 2; condition2.signal(); } catch (Exception e) { } finally { lock.unlock(); } } public void loopB() { lock.lock(); try { if (number != 2) { //判断 condition2.await(); } System.out.println(Thread.currentThread().getName());//打印 number = 3; condition3.signal(); } catch (Exception e) { } finally { lock.unlock(); } } public void loopC() { lock.lock(); try { if (number != 3) { //判断 condition3.await(); } System.out.println(Thread.currentThread().getName());//打印 number = 1; condition1.signal(); } catch (Exception e) { } finally { lock.unlock(); } } }

以上编码就满足需求。创建三个线程,分别调用loopA、loopB和loopC方法,这三个线程使用condition进行通信。

八、ReadWriterLock读写锁

我们在读数据的时候,可以多个线程同时读,不会出现问题,但是写数据的时候,如果多个线程同时写数据,那么到底是写入哪个线程的数据呢?所以,如果有两个线程,写写/读写需要互斥,读读不需要互斥。这个时候可以用读写锁。看例子:

public class TestReadWriterLock { public static void main(String[] args){ ReadWriterLockDemo rw = new ReadWriterLockDemo(); new Thread(new Runnable() {//一个线程写 @Override public void run() { rw.set((int)Math.random()*101); } },"write:").start(); for (int i = 0;i<100;i++){//100个线程读 Runnable runnable = () -> rw.get(); Thread thread = new Thread(runnable); thread.start(); } } } class ReadWriterLockDemo{ private int number = 0; private ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); //读(可以多个线程同时操作) public void get(){ readWriteLock.readLock().lock();//上锁 try { System.out.println(Thread.currentThread().getName()+":"+number); }finally { readWriteLock.readLock().unlock();//释放锁 } } //写(一次只能有一个线程操作) public void set(int number){ readWriteLock.writeLock().lock(); try { System.out.println(Thread.currentThread().getName()); this.number = number; }finally { readWriteLock.writeLock().unlock(); } } }

这个就是读写锁的用法。上面的代码实现了一个线程写,一百个线程同时读的操作。

九、线程池

我们使用线程时,需要new一个,用完了又要销毁,这样频繁的创建销毁也很耗资源,所以就提供了线程池。道理和连接池差不多,连接池是为了避免频繁的创建和释放连接,所以在连接池中就有一定数量的连接,要用时从连接池拿出,用完归还给连接池。线程池也一样。线程池中有一个线程队列,里面保存着所有等待状态的线程。下面来看一下用法:

public class TestThreadPool { public static void main(String[] args) { ThreadPoolDemo tp = new ThreadPoolDemo(); //1.创建线程池 ExecutorService pool = Executors.newFixedThreadPool(5); //2.为线程池中的线程分配任务 pool.submit(tp); //3.关闭线程池 pool.shutdown(); } } class ThreadPoolDemo implements Runnable { private int i = 0; @Override public void run() { while (i < 100) { System.out.println(Thread.currentThread().getName() + ":" + (i++)); } } }

线程池用法很简单,分为三步。首先用工具类Executors创建线程池,然后给线程池分配任务,最后关闭线程池就行了。

总结:

以上为本文全部内容,涉及到了JUC的大部分内容。 本人也是初次接触,如有错误,希望大佬指点一二!

原文:https://www.jianshu.com/p/1f19835e05c0

read more
Java直接内存与非直接内存性能测试
什么是直接内存与非直接内存

根据官方文档的描述:

A byte bufferis either direct or non-direct. Given a direct byte buffer, the Java virtual machine will make a best effort to perform native I/O operations directly upon it. That is, it will attempt to avoid copying the buffer's content to (or from) an intermediate buffer before (orafter) each invocation of one of the underlying operating system's native I/O operations.

byte byffer可以是两种类型,一种是基于直接内存(也就是非堆内存);另一种是非直接内存(也就是堆内存)。

对于直接内存来说,JVM将会在IO操作上具有更高的性能,因为它直接作用于本地系统的IO操作。而非直接内存,也就是堆内存中的数据,如果要作IO操作,会先复制到直接内存,再利用本地IO处理。

从数据流的角度,非直接内存是下面这样的作用链:

本地IO-->直接内存-->非直接内存-->直接内存-->本地IO

而直接内存是:

本地IO-->直接内存-->本地IO

很明显,再做IO处理时,比如网络发送大量数据时,直接内存会具有更高的效率。

A direct byte buffer may be created by invoking the allocateDirect factory method of this class. The buffers returned by this method typically have somewhat higher allocation and deallocation costs than non-direct buffers. The contents of direct buffers may reside outside of the normal garbage-collected heap, and so their impact upon the memory footprint of an application might not be obvious. It is therefore recommended that direct buffers be allocated primarily for large, long-lived buffers that are subject to the underlying system's native I/O operations. In general it is best to allocate direct buffers only when they yield a measureable gain in program performance.

但是,不要高兴的太早。文档中也说了,直接内存使用allocateDirect创建,但是它比申请普通的堆内存需要耗费更高的性能。不过,这部分的数据是在JVM之外的,因此它不会占用应用的内存。

所以呢,当你有很大的数据要缓存,并且它的生命周期又很长,那么就比较适合使用直接内存。只是一般来说,如果不是能带来很明显的性能提升,还是推荐直接使用堆内存。

关于直接内存需要注意的,就是上面两点了,其他的关于视图啊、作用链啊,都是使用上的问题了。如果有兴趣,可以参考官方API ( 进去后搜索ByteBuffer,就能看到!),里面有少量的描述!重要的一些用法,还得自己摸索。

使用场景

通过上面的官方文档,与一些资料的搜索。可以总结下,直接内存的使用场景:

1 有很大的数据需要存储,它的生命周期又很长 2 适合频繁的IO操作,比如网络并发场景 申请分配地址速度比较

下面用一段简单的代码,测试下申请内存空间的速度:

inttime = 10000000; Date begin = newDate(); for(int i=0;i<time;i++){ ByteBuffer buffer = ByteBuffer.allocate(2); } Dateend = newDate(); System.out.println(end.getTime()-begin.getTime()); begin = newDate(); for(int i=0;i<time;i++){ ByteBuffer buffer = ByteBuffer.allocateDirect(2); } end = newDate(); System.out.println(end.getTime()-begin.getTime());

得到的测试结果如下:

在数据量提升时,直接内存相比于非直接内存的申请 有十分十分十分明显的性能问题!

读写速度比较

然后在写段代码,测试下读写的速度:

inttime = 1000; Date begin = newDate(); ByteBuffer buffer = ByteBuffer.allocate(2*time); for(int i=0;i<time;i++){ buffer.putChar('a'); } buffer.flip(); for(int i=0;i<time;i++){ buffer.getChar(); } Dateend = newDate(); System.out.println(end.getTime()-begin.getTime()); begin = newDate(); ByteBuffer buffer2 = ByteBuffer.allocateDirect(2*time); for(int i=0;i<time;i++){ buffer2.putChar('a'); } buffer2.flip(); for(int i=0;i<time;i++){ buffer2.getChar(); } end = newDate(); System.out.println(end.getTime()-begin.getTime());

测试的结果如下:

可以看到直接内存在直接的IO操作上,还是有明显的差异的!

作者:xingoo

出处:http://www.cnblogs.com/xing901022

read more
How to learn Spring Cloud – the practical way

I have recently spoken at a meetup about Practical Choreography with Spring Cloud Stream. It was a great event where I was asked many questions after the talk. One question got me thinking: *“What book about Spring Cloud do you recommend?” *which as it turns out boils down to “How do you learn Spring Cloud?”. I heard that question posed a few times before in different ways. Here, I will give you my answer on what I think is the best way of learning Spring Cloud.

With Spring Cloud being probably the hottest framework on JVM for integrating microservices, the interest in it is growing. Most people interested in the microservices are already familiar with Spring Boot. If you haven’t heard of it before, check out my Spring Boot introduction blog post, and definitely see the official site– it has some very good Getting Started Guides.

With that out of the way, let’s look at learning Spring Cloud!

Understand the Scope

The first thing to do when trying to learn something so big and diverse is understanding the scope. Learning Spring Cloud can mean many things. First of all, the Spring Cloud currently contains:

Spring Cloud Config Spring Cloud Netflix Spring Cloud Bus Spring Cloud for Cloud Foundry Spring Cloud Cloud Foundry Service Broker Spring Cloud Cluster Spring Cloud Consul Spring Cloud Security Spring Cloud Sleuth Spring Cloud Data Flow Spring Cloud Stream Spring Cloud Stream App Starters Spring Cloud Task Spring Cloud Task App Starters Spring Cloud Zookeeper Spring Cloud for Amazon Web Services Spring Cloud Connectors Spring Cloud Starters Spring Cloud CLI Spring Cloud Contract Spring Cloud Gateway

Wow! This is a lot to take in! Clearly, the number of different projects here means that you can’t learn it by simply going through them one by one with a hope of understanding or mastering Spring Cloud by the end of it.

So, what is the best strategy for learning such an extensive framework (or a microservice blueprint, as I describe it in another article)? I think the most sensible ways of learning is understanding what you would like to use Spring Cloud for. Setting yourself a learning goal.

Goal Oriented Learning

What kind of learning goals are we talking about here? Let me give you a few ideas:

Set up communication between microservices based on Spring Cloud Stream Build microservices that use configuration provided by Spring Cloud Config Build a small microservices system based on Orchestration- what is needed and how to use it Test microservices with Spring Cloud Contract Use Spring Cloud Data Flow to take data from one place, modify it and store it in Elastic Search

If you are interested in learning some parts of Spring Cloud, think of an absolutely tiny project and build it! Once you have done it, you know that you understood at least the basics and you validated it by having something working. I will quote Stephen R. Covey here (author of “The 7 Habits of Highly Effective People”

“to learn and not to do is really not to learn. To know and not to do is really not to know.”

With topics as complex and broad as Spring Cloud, this quote rings very true!

Study

You picked your goal and you want to get started. What resources can help you? I will give you a few ideas here, but remember- the goal is to learn only as much as necessary in order to achieve your goal. Don’t learn much more just yet, as you may end up overwhelmed and move further away from completing your goal. There will be time to learn more in depth. Let’s assume that your goal is Using Spring Cloud Config correctly in your personal project. Here are the resources I recommend:

Official Spring Cloud Config Quickstart to get a basic idea If you enjoy books and want to learn more Spring Cloud in the future – Spring Microservices in Action is a great reference. Don’t read it all yet! Check out the chapters on Spring Cloud Configuration and read as much as necessary to know what to do. If you use Pluralsight, then check out Java Microservices with Spring Cloud: Developing Services – a very good introduction! Again, start with the chapters on Spring Cloud Config. You can google the topic and find articles like Quick Intro to Spring Cloud Configuration You can even find YouTube videos about Spring Cloud Config

I really want to make a point here. There is a huge amount of resources out there, free or paid of very high quality. You can spend weeks just reviewing them, but this is a mistake. Chose what works for you and get moving towards your goal!

Do something – achieve your goal

Once you identified the resources you need, get on with your goal! If your goal was to learn about Spring Cloud Config- set up the server, get the clients connecting and experiment with it.

You should have enough information to complete your simple task. If you find that something is not working- great! That shows that you need to revisit the resources and correct your understanding.

If you completed your goal, but you want to experiment more with the tech- go for it! You have something working and playing with it is much more fun than reading dry tech documentation.

By playing with the technology you start to notice nuances and develop a deeper understanding. Understanding that will not be easily acquired by reading countless articles, as most things would just fly over your head.

Study Again

Once you completed your goal and played a little with the tech you should have a much better idea what you are dealing with. Now is the time to go deep! Read all you can around the area that you explored. See what you could have done differently, how it is used and what are the best practices.

Now, all the reading you will do will make much more sense and will be more memorable. Suddenly dry documentation turns into fascinating discoveries of what you could have done better. And the best of all- if something sounds really great- you have your test-bed to try it.

Teach

Teaching others really helps with memorizing and understanding the subject. This is one of the reasons why I am writing this blog. You not only get a chance of sharing your knowledge but also learn yourself by teaching.

If blogging is not your thing, you can talk to your colleagues or friends about what you have been tinkering with. You may be confronted with questions or perspectives that you did not consider before- great! Another chance to make the learning more complete.

One thing to remember is- don’t be afraid to teach. Even if what you have just learned seems basic to you- it was not so basic before you started learning it! If you were in this position, then so must be countless others!

There is a value to the unique way you can explain the subject in your own way. Especially given your practical experience gained from the goal that you achieved.

Staying up to Date

Spring Cloud is constantly changing and growing. If your ultimate goal is becoming an expert in this ecosystem, then you need to think about ways of staying up to date.

One thing that is pretty much a must is working with it. If you are not lucky enough to use it on your day job- make sure that you use it in your spare time. You could be building a personal project making use of the tech or simply tinker with it and try different things. What matters is that you actually get that hands-on experience.

The second part of staying fresh is knowing whats coming and reading other people experiences. Some of the sources I really enjoy following are:

The Spring.io blog with a very good newsletter Baeldung – an amazing source of Spring related articles and a weekly newsletter InfoQ Microservices – huge and very active website maintained by multiple authors Using Twitter to stay up to date and see what people are reading. I share plenty of articles on that topic with my @bartoszjd account.

These are just some of the sources that I follow. There are countless others. The point is to choose some that you enjoy reading and keep an eye for exciting stuff.

Conclusion

Spring Cloud is a huge and fascinating set of tools for building microservices. It can’t be learned as a “single thing”. Using different goals is the best way of approaching this learning.

The idea presented here can be used for learning any technical concept. I found it extremely beneficial for myself and used it with success. I really recommend checking out SimpleProgrammer’s Learning to learn article which describes very similar idea for learning new technologies or frameworks.

Happy learning!

原文:https://www.e4developer.com/2018/03/06/how-to-learn-spring-cloud-the-practical-way/

read more
八皇后算法解析

今天研究力扣的一道题死活写不出来对应的算法,没办法自己算法基础太差。于是看了下答案,发现使用什么回溯算法,菜鸟表示平时开发期间写的最复杂的程序就是写了两层for循环,已经很牛逼了有木有?这个回溯算法什么鬼?于是乎百度了下,算是了解了回溯算法是什么玩意儿。这里分析一波八皇后算法来加深一下理解。

八皇后算法描述如下:在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法!

下面来分析一波,假设此时我们想要在黑色方块位置放置一个皇后:

如果一列一列的放置皇后的话,图中黑色位置能放置一个皇后的合法性条件为:

1、绿色线条经过的方格没有皇后 (不处于同一斜线)

2、红色线条经过的方格没有皇后 (不处于同一行)

3、紫色线条经过的方格没有皇后 (不处于同一斜线)
也就是说如果以黑色方块位置为参照原点:(0,0)坐标点,紫色和绿色两个线条分别是斜率为1和-1的两个函数,如下图:

紫色线所代表的函数是:y = -x;

绿色先所代表的函数是:y=x;

(横坐标是列,纵坐标为行,注意行从上到下递增)

凡是位于这两条函数线上的位置(点)以及横坐标(说明位于同一行)都不能有皇后。
所以假设某一列皇后的位置用行来记录,比如queen[column] = row,意思是第column列的皇后的位置在第row行。

同行的逻辑很好判断,那么我们想要在黑色方块位置放置一个皇后,怎么判断前面几列是否在绿色线条和紫色线条上已经有了皇后呢?思路也很简单:

假设黑色方块的位置为n列,nRow行,假设位于m列的所在的行是否有皇后位于紫色线或者绿色上,那么就符合下面条件:

//假设此时即将在n列放置一个皇后,n>m ]//获取m列上皇后所在的行 int mRow = queen[m] int nRow = queen[n]; //行的差值 int rowDiff = nRow - mRow; //列的差值 int columnDiff = n-m;

上面代码中 rowDiff的绝对值等于columnDiff的绝对值的话,说明点位于y=x或者y=-x的函数线上:

就说明此时黑色方块的位置是不能放置皇后的,因为在紫色或者绿色线上已经有了皇后。
那么用代码来(currentColumn,curreentRow)是否可以放置皇后的方法如下

/** * 判断当(currentRow,currentColumn)是否可以放置皇后 * @param currentColumn * @return */ public boolean isLegal(int currentRow,int currentColumn) { //遍历前面几列 for(int preColumn=0;preColumn<currentColumn;preColumn++) { int row = queen[preColumn]; //说明在子preColumn的低currentRow已经有了皇后 if(row==currentRow) { return false; } //行与行的差值 int rowDiff= Math.abs(row -currentRow); //列于列的差值 int columnDiff = Math.abs(currentColumn-preColumn); //说明斜线上有皇后 if(rowDiff==columnDiff ){ return false; } }//end for //说明(currentRow,currentColumn)可以摆放。 return true; } }

因为博主是按照一列一列的方式来进行放置的,所以整体思路就是:在当前列逐步尝试每一行是否可以放置皇后,如果有一个可以放置皇后,就继续查看下一列的每一行是否可以放置皇后。所以代码如下:

int queen[] = new int[8]; int count = 0; private void eightQueen(int currentColumn) { //这个for循环的目的是尝试讲皇后放在当前列的每一行 for(int row=0;row<8;row++) { //判断当前列的row行是否能放置皇后 if(isLegal(row,currentColumn)) { //放置皇后 queen[currentColumn] = row; if(currentColumn!=7) { //摆放下一列的皇后 eightQueen(currentColumn+1); }else { //递归结束,此时row要++了 count++; } } }//end for }

需要注意的是当currentColumn==7的时候,说明此时已经完成了一种摆放方法,然后for循环继续执行,去尝试其他摆放方法。

测试一波,一共有92种摆放方法:

public static void main(String args[]) { Queen queen = new Queen(); queen.eightQueen(0); System.out.println("总共有 " +queen.count+ " 摆放方法"); }

所以结合八皇后的实现来看看到底什么是回溯算法,看百度百科解释 (rel=undefined):回溯算法实际上一个类似枚举的搜索尝试过程,主要是<font color#ff00ff>在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法

比如八皇后算法来说,我们根据约束条件判断某一列的某一行是否可以放置皇后,如果不可以就继续判断<font color #ff00ff>当前列的下一行是否可以放置皇后.如果可以放置皇后,就继续探寻下一列中可以放置皇后的那个位置。完成一次摆放后。再重新挨个尝试下一个可能的摆放方法。

下面用一个力扣的题 (rel=undefined)再次巩固下回溯算法的应用。该题描述如下:

给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的数字可以无限制重复被选取。 说明:所有数字(包括 target)都是正整数。解集不能包含重复的组合。 示例 1:输入: candidates = [2,3,6,7], target = 7, 所求解集为: [ [7], [2,2,3] ] 示例 2:输入: candidates = [2,3,5], target = 8, 所求解集为: [ [2,2,2,2], [2,3,3], [3,5] ]

做该题的重要条件是无重复的数组,那么问题就很好解了。

首先对数组从大到小排序。这是解题的关键。

然后为了减少不必要的遍历,我们要对原来的数组进行截取:

List<List<Integer>> res = new ArrayList<>(); //重要的要大小排列 Arrays.sort(candidates); //说明原数组中就没有满足target的数 if (candidates[0] > target) { return res; } List<Integer> newCandidates= new ArrayList<Integer>(); int len = candidates.length; // 取小于target的数 组成一个临时数组 for (int i = 0; i < len; i++) { int num = candidates[i]; if (num > target) { break; } newCandidates.add(num); } // end for

通过上面的步骤我们拿到了一个从小到大排列的无重复数组newCandidates,数组中的元素都<=target.

因为数组从小到大排列,所以我们有如下几种情况,以candidates = [2,3,5], target = 8为例:

符合条件的子数组满足条件如下

1、target循环减去一个数,如果能一直减到到差值等于0,那么这个数组成的数组就是一个解,比如[2,2,2,2];

2、target减去一个数,然后形成了一个新的newTarget=target-num[i],让这个newTarget减去下一个数num[i+1],然后执行步骤1,则又是一个解,比如[2,3,3];(其实步骤1是步骤2的一个特例)

3、target减去一个数,然后形成了一个新的newTarget=target-num[i],让这个newTarget减去下一个数num[i+1],如果能一直减到到差值等于0说明又是一个解.,比如[3,5];

如此得到了一个规律,只要是相减之后得到差值=0,就说明就得到一个解。

得到一个新的解之后继续循环数组中的下一个数字,继续执行1,2,3步骤即可。

所以完成的解法如下:

class Solution { public List<List<Integer>> combinationSum(int[] candidates, int target) { List<List<Integer>> res = new ArrayList<>(); //重要的要大小排列 Arrays.sort(candidates); List<Integer> temp = new ArrayList<Integer>(); if (candidates[0] > target) { return res; } int len = candidates.length; // 取小于target的数 足证一个临时数组 for (int i = 0; i < len; i++) { int num = candidates[i]; if (num > target) { break; } temp.add(num); } // end for // find(res, new ArrayList<>(), temp, target, 0); return res; } public void find(List<List<Integer>> res, List<Integer> tmp, List<Integer> candidates, int target, int start){ //target==0.找到一个新的解 if (target == 0) { res.add(new ArrayList<>(tmp)); }else if(target>0){ for (int i = start; i < candidates.size(); i++) { int num = candidates.get(i); if(num<=target){ tmp.add(num); //查找新的target int newTarget = target-num; find(res, tmp, candidates, newTarget, i); tmp.remove(tmp.size() - 1); } }//end for } } }

原文:https://blog.csdn.net/chunqiuwei/article/details/90113087#commentBox

read more
java 中单例模式DCL的缺陷及单例的正确写法

首先在说明单例设计模式中的 DCL 问题之前我们首先看看实现单例设计模式的两种方式:饿汉式和懒汉式。

什么是饿汉式?

饿汉式就是不管你是否用的上,一开始就先初始化对象(也叫做提前初始化)

代码示例:

public class EagerInitialization{ private EagerInitialization() {} private static Resource resource = new Resource(); public static Resource getResource(){ return resource; } } 什么是懒汉式?

懒汉式就是当你真正需要使用时才创建对象。

于是,关于懒汉式的问题也就随之产生了~~~

我们先看一下有问题的代码:

代码示例:

public class LazyInitialization{ private static Resource resource; public static Resource getResource(){ if (resource == null) resource = new Resource();//不安全! return resource; } }

我们都知道上面的这个代码在单线程中运行是没有问题的,但是在平时的开发中常常会使用多线程,此时这个方法就会出现问题,假设有两个线程 A、B,当 A 线程满足判断还未来得及执行到 resource = new Resource() 时,线程执行资格被 B 拿走,此时线程 B 进入 getResource(), 而此时它也满足 resource 的值为 null, 于是导致最后产生两个实例。

针对上面的问题,于是有了相应的解决方案,即线程安全的延迟初始化,可以解决懒汉式出现的上述问题:

代码示例:

public class LazyInitialization{ private static Resource resource; public synchronized static Resource getResource(){ if (resource == null) resource = new Resource(); return resource; } }

上面代码通过使用 synchronized 关键字将 getResource 变成同步函数来保证方法的原子性,从而保证了线程安全而防止最后多个线程产生多个实例的现象。

我们都知道,在上述例子当中,每次在调用 getResource() 时都需要进行同步,而且在大多数时这种同步是没有必要的,并且大量无用的同步会对性能造成极大的影响。为什么呢?因为在第一次调用 getResource() 方法时就已经创建了 resource 实例了,之后 resource 就不再为空,然而之后再调用 getResource 时都需要进行同步,从而对性能造成了很大的影响。基于这些问题,一个新的方法也就产生了,这也是我们需要着重讨论的一个方法——双重检查加锁 (Double Check Locking) DCL。

双重检查加锁 DCL (Double Check Locking)

首先我们看看 DCL 的代码:

示例代码:

public class DoubleCheckedLocking{ private static Resource resource; public static Resource getResource(){ if (resource == null) { synchronized (DoubleCheckedLocking.class) { if (resource == null) resource = new Resource(); } } return resource; } }

你可能会疑惑,这样做不是挺好么,这样就可以解决刚刚说的那些问题了么,当 resource 被实例化后再调用 getResource() 方法不就不会再进行同步,这样不就节约了资源,提升了性能么?

说的对,DCL 确实存在着这些优点,但是与此同时,这个方法也会带来相应的问题,因为这个方法是含有缺陷的。再次之前,先了解一下JVM内存模型。

JVM内存模型

JVM模型如下图:

Thread Stack 是线程私有的区域。他是java方法执行时的字典:它里面记录了局部变量表、 操作数栈、 动态链接、 方法出口等信息。

在《java虚拟机规范》一书中对这部分的描述如下:

栈帧( Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接 (Dynamic Linking)、 方法返回值和异常分派( Dispatch Exception)。

栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。

栈帧的存储空间分配在 Java 虚拟机栈( §2.5.5)之中,每一个栈帧都有自己的局部变量表( Local Variables, §2.6.1)、操作数栈( OperandStack, §2.6.2)和指向当前方法所属的类的运行时常量池( §2.5.5)的引用。

Java 中某个线程在访问堆中的线程共享变量时,为了加快访问速度,提升效率,会把该变量临时拷贝一份到自己的 Thread Stack 中,并保持和堆中数据的同步。

缺陷

首先我们看到,DCL 方法包含了层判断语句,第一层判断语句用于判断 resource 对象是否为空,也就是是否被实例化,如果为空时就进入同步代码块进一步判断,问题就出在了 resource 的实例化语句 resource = new Resource() 上,因为这个语句实际上不是原子性的。这句话可以大致分解为如下步骤:

1. 给 Resource 的实例分配内存 2. 初始化 Resource 构造器 3. 将 resource 实例指向分配的内存空间,此时 resource 实例就不再为空

我们都希望这条语句的执行顺序是上述的 1——>2——>3,但是,由于 Java 编译器允许处理器乱序执行,以及 JDK1.5 之前 JMM(Java Memory Medel,即 Java 内存模型)中 Cache、寄存器到主内存回写顺序的规定,上面的第二点和第三点的顺序是无法保证的,也就是说,执行顺序可能是 1——>2——>3 也可能是 1——>3——>2。

如果有两个线程 A 和 B,如果 A 线程执行完 1 后先执行 3 然后执行 2,并且在 3 执行完毕、2 未执行之前,被切换到线程 B 上,这时候 resource 因为已经在线程 A 内执行过了第三点(jvm将未完成 Resource 构造器的值拷贝回堆中),resource 已经是非空了,所以线程 B 直接拿走 resource,然后使用,然后顺理成章地报错,而且这种难以跟踪难以重现的错误很可能会隐藏很久。

好了,关于 DCL 的问题阐述完了,那么这个方法既然有问题,那么该如何修改呢?

Happen-Before 原则

通过遵守 Happen-Before 原则,解决并发顺序问题。

1. 同一个线程中,书写在前面的操作happen-before书写在后面的操作。这条规则是说,在单线程中操作间happen-before关系完全是由源代码的顺序决定的,这里的前提“在同一个线程中”是很重要的,这条规则也称为单线程规则 。这个规则多少说得有些简单了,考虑到控制结构和循环结构,书写在后面的操作可能happen-before书写在前面的操作,不过我想读者应该明白我的意思。 2. 对锁的unlock操作happen-before后续的对同一个锁的lock操作。这里的“后续”指的是时间上的先后关系,unlock操作发生在退出同步块之后,lock操作发生在进入同步块之前。这是条最关键性的规则,线程安全性主要依赖于这条规则。但是仅仅是这条规则仍然不起任何作用,它必须和下面这条规则联合起来使用才显得意义重大。这里关键条件是必须对“同一个锁”的lock和unlock。 如果操作A happen-before操作B,操作B happen-before操作C,那么操作A happen-before操作C。这条规则也称为传递规。 通过 volatile 防止指令重排序

在 JMM 的后续版本(Java 5.0 及以上)中,如果把 resource 声明为 volatile 类型,因为 volatile 可以防止指令的重排序(对 volatile 字段的写操作 happen-before 后续的对同一个字段的读操作),那么这样就可以启用 DCL,并且这种方式对性能的影响很小,因为 volatile 变量读取操作的性能通常只是略高于非 volatile 变量读取操作的性能。改进后的 DCL 方法如下代码所示

代码示例:

public class DoubleCheckedLocking{ private static volatile Resource resource; public static Resource getResource{ if (resource == null) { synchronized (DoubleCheckedLocking.class) { if (resource == null) resource = new Resource(); } } return resource; } }

但是,DCL 的这种方法已经被广泛地遗弃了,因为促使该模式出现的驱动力(无竞争同步的执行速度很慢,以及 JVM 启动时很慢)已经不复存在,因为它不是一种高效的优化措施。延迟初始化占位类模式能带来同样的优势,并且更容易理解,延迟初始化占位类模式代码如下:

代码示例:

public class ResourceFactory{ private static class ResourceHolder { public static Resource resource = new Resource(); } public static Resource getResource(){ return ResourceHolder.resource; } }

关于单例和 DCL 问题就分析到这里了,在实际开发当中由于经常要考虑到代码的效率和安全性,一般使用饿汉式和延长初始化占位类模式,而延迟占位类模式更是优势明显并且容易使用和理解,是良好的单例设计模式的实现方法。

参考资料:

《java 并发编程实战》

关于 volatile 的问题可以参考:
http://blog.csdn.net/wxwzy738/article/details/43238089

关于 DCL 的其他问题可以参考:
http://blog.csdn.net/ns_code/article/details/17359719
https://blog.csdn.net/qiyei2009/article/details/71813069
https://blog.csdn.net/u013393958/article/details/70941579

read more
Learn Gradle - 3 Java 快速入门

上一节主要对Gradle的脚本进行了简要的介绍,本节将继续学习Gradle的另外一个特性——插件(plugins)。

1、插件介绍

插件是对Gradle功能的扩展,Gradle有着丰富的插件,你可以在这里搜索相关插件(传送门)。本章将简要介绍Gradle的Java插件(Java plugin),这个插件会给你的构建项目添加一些任务,比如编译java类、执行单元测试和将编译的class文件打包成jar文件等。

Java插件是基于约定的(约定优于配置),它在项目的很多方面定义了默认值,例如,Java源文件应该位于什么位置。我们只要遵循插件的约定,就不需要在Gradle配置脚本进行额外的相关配置。当然,在某些情况下,你的项目不想或不能遵循这个约定也是可以的,这样你就需要额外的配置你的构建脚本。

Gradle Java插件对于项目文件存放的默认位置与maven类似。

<!--more-->

Java源码存放在目录:src/main/java

Java测试代码存放目录:src/test/java

资源文件存放目录:src/main/resources

测试相关资源文件存放目录:src/test/resources

所有输出文件位于目录:build

输出的jar文件位于目录:build/libs

2、一个简单的Java项目

新建一个文件build.gradle,添加代码:

apply plugin: 'java'

以上代码即配置java插件到构建脚本中。当执行构建脚本时,它将给项目添加一系列任务。我们执行:gradle build,来看看输出的结果:

:compileJava UP-TO-DATE :processResources UP-TO-DATE :classes UP-TO-DATE :jar UP-TO-DATE :assemble UP-TO-DATE :compileTestJava UP-TO-DATE :processTestResources UP-TO-DATE :testClasses UP-TO-DATE :test UP-TO-DATE :check UP-TO-DATE :build UP-TO-DATE BUILD SUCCESSFUL

根据输出结果可以看出,我们执行的build这个任务依赖其他任务,比如compileJava等,这就是java插件预先定义好的一系列任务。

你还可以执行一些其他的任务,比如执行:gradle clean,gradle assemble,gradle check等。

gradle clean:删除构建目录以及已经构建完成的文件;

gradle assemble(装配):编译和打包java代码,但是不会执行单元测试(从上面的任务依赖结果也可以看出来)。如果你应用了其他插件,那么还会完成一下其他动作。例如,如果你应用了War这个插件,那么这个任务将会为你的项目生成war文件。

gradle check:编译且执行测试。与assemble类似,如果你应用了其他包含check任务的插件,例如,Checkstyle插件,那么这个任务将会检查你的项目代码的质量,并且生成检测报告。

如果想知道Gradle当前配置下哪些任务可执行,可以执行:gradle tasks,例如应用了java插件的配置,执行该命令,输出:

:tasks ------------------------------------------------------------ All tasks runnable from root project ------------------------------------------------------------ Build tasks ----------- assemble - Assembles the outputs of this project. build - Assembles and tests this project. buildDependents - Assembles and tests this project and all projects that depend on it. buildNeeded - Assembles and tests this project and all projects it depends on. classes - Assembles classes 'main'. clean - Deletes the build directory. jar - Assembles a jar archive containing the main classes. testClasses - Assembles classes 'test'. Build Setup tasks ----------------- init - Initializes a new Gradle build. [incubating] wrapper - Generates Gradle wrapper files. [incubating] Documentation tasks ------------------- javadoc - Generates Javadoc API documentation for the main source code. Help tasks ---------- components - Displays the components produced by root project 'learn-gradle'. [incubating] dependencies - Displays all dependencies declared in root project 'learn-gradle'. dependencyInsight - Displays the insight into a specific dependency in root project 'learn-gradle'. help - Displays a help message. model - Displays the configuration model of root project 'learn-gradle'. [incubating] projects - Displays the sub-projects of root project 'learn-gradle'. properties - Displays the properties of root project 'learn-gradle'. tasks - Displays the tasks runnable from root project 'learn-gradle'. Verification tasks ------------------ check - Runs all checks. test - Runs the unit tests. Rules ----- Pattern: clean<TaskName>: Cleans the output files of a task. Pattern: build<ConfigurationName>: Assembles the artifacts of a configuration. Pattern: upload<ConfigurationName>: Assembles and uploads the artifacts belonging to a configuration. To see all tasks and more detail, run gradle tasks --all To see more detail about a task, run gradle help --task <task> BUILD SUCCESSFUL

小伙伴们看到这里会不会有疑问,如果在构建脚本中定义了名为tasks的任务,执行会是如何?好奇的小伙伴可以自己试一试噢。事实上,是会覆盖原有的任务的。

3、外部依赖

通常一个Java项目会依赖多个其他项目或jar文件,我们可以通过配置gradle仓库(repository)告诉gradle从哪里获取需要的依赖,并且gradle还可以配置使用maven仓库。例如,我们配置gradle使用maven中央仓库,在build.gradle中添加代码:

repositories { mavenCentral() }

接下来,我们来添加一些依赖。代码示例:

dependencies { compile group: 'commons-collections', name: 'commons-collections', version: '3.2' testCompile group: 'junit', name: 'junit', version: '4.+' }

关于依赖,暂时就点这么多。详细可以参考gradle依赖管理基础,也可以关注后续文章。

4、定义项目属性

Java插件会为项目添加一系列的属性,通常情况下,初始的Java项目使用这些默认配置就足够了,我们不需要进行额外的配置。但是如果默认属性不满足于你的项目,你也可以进行自定义项目的一些信息。例如我们为项目指定版本号和一些jar manifest信息。

sourceCompatibility = 1.5 version = '1.0' jar { manifest { attributes 'Implementation-Title': 'Gradle Quickstart', 'Implementation-Version': version } }

事实上,Java插件添加的一系列任务与我们之前在脚本中自定义的任务没什么区别,都是很常规的任务。我们可以随意定制和修改这些任务。例如,设置任务的属性、为任务添加行为、改变任务的依赖,甚至替换已有的任务。例如我们可以配置Test类型的test任务,当test任务执行的时候,添加一个系统属性。配置脚本如下:

test { systemProperties 'property': 'value' }

另外,与之前提到的“gradle tasks”命令类型,我们可以通过“gradle properties”来查看当前配置所支持的可配置属性有哪些。

5、将Jar文件发布到仓库 uploadArchives { repositories { flatDir { dirs 'repos' } } }

执行gradle uploadArchives,将会把相关jar文件发布到reops仓库中。更多参考:Publishing artifacts

6、构建多个Java项目

假设我们的项目结构如下所示:

multiproject/ --api/ --services/webservice/ --shared/ --services/shared/

项目api生成jar文件,Java客户端通过jar提供的接口访问web服务;项目services/webservice是一个webapp,提供web服务;项目shared 包含api和webservice公共代码;项目services/shared依赖shared项目,包含webservice公共代码。

接下来,我们开始定义多项目构建。

1)首先,我们需要添加一个配置文件:settings.gradle文件。settings.gradle位于项目的根目录,也就是multiproject目录。编辑settings.gradle,输入配置信息:

include "shared", "api", "services:webservice", "services:shared"

include是Gradle DSL定义的核心类型Settings的方法,用于构建指定项目。配置中指定的参数“shared”、“api”等值默认是当前配置目录的目录名称,而“services:webservice”将根据默认约定映射系统物理路径"services/webservice"(相对于根目录)。关于include更详细的信息可以参考:构建树

2)定义所有子项目公用配置。在根目录创建文件:build.gradle,输入配置信息:

subprojects { apply plugin: 'java' apply plugin: 'eclipse-wtp' repositories { mavenCentral() } dependencies { testCompile 'junit:junit:4.12' } version = '1.0' jar { manifest.attributes provider: 'gradle' } }

subprojects 是Gradle DSL定义的构建脚本模块之一,用于定义所有子项目的配置信息。在以上配置中,我们给所有子项目定义了使用“java”和“eclipse-wtp”插件,还定义了仓库、依赖、版本号以及jar(jar是Gradle的任务类型之一,任务是装配jar包,jar任务包含属性manifest,用于描述jar的信息,具体参考:Jar)。

我们在根目录执行gradle build命令时,这些配置会应用到所有子项目中。

3)给项目添加依赖

新建文件:api/build.gradle,添加配置:

dependencies { compile project(':shared') }

以上,我们定义了api项目依赖shared项目,当我们在根目录执行gradle build命令时,gradle会确保在编译api之前,先完成shared项目编译,然后才会编译api项目。

同样,添加services/webservice/build.gradle,添加配置:

dependencies { compile project(':services:shared') }

在根目录执行:gradle compileJava,输出:

:shared:compileJava UP-TO-DATE :shared:processResources UP-TO-DATE :shared:classes UP-TO-DATE :shared:jar UP-TO-DATE :api:compileJava UP-TO-DATE :services:compileJava UP-TO-DATE :services:shared:compileJava UP-TO-DATE :services:shared:processResources UP-TO-DATE :services:shared:classes UP-TO-DATE :services:shared:jar UP-TO-DATE :services:webservice:compileJava UP-TO-DATE BUILD SUCCESSFUL

通过输出信息我们就可以清楚看出依赖配置是否正确啦。

read more
Zookeeper是如何解决脑裂问题
前言

这是分布式系统中一个很实际的问题,书上说的不是很详细,整理总结一下。

1、脑裂和假死 1.1 脑裂

官方定义:当一个集群的不同部分在同一时间都认为自己是活动的时候,我们就可以将这个现象称为脑裂症状。通俗的说,就是比如当你的 cluster 里面有两个结点,它们都知道在这个 cluster 里需要选举出一个 master。那么当它们两之间的通信完全没有问题的时候,就会达成共识,选出其中一个作为 master。但是如果它们之间的通信出了问题,那么两个结点都会觉得现在没有 master,所以每个都把自己选举成 master。于是 cluster 里面就会有两个 master。举例:

UserA和UserB分别将自己的信息注册在RouterA和RouterB中。RouterA和RouterB使用数据同步(2PC),来同步信息。那么当UserA想要向UserB发送一个消息的时候,需要现在RouterA中查询出UserA到UserB的消息路由路径,然后再交付给相应的路径进行路由。

当脑裂发生的时候,相当RouterA和RouterB直接的联系丢失了,RouterA认为整个系统中只有它一个Router,RouterB也是这样认为的。那么相当于RouterA中没有UserB的信息,RouterB中没有UserA的信息了,此时UserA再发送消息给UserB的时候,RouterA会认为UserB已经离线了,然后将该信息进行离线持久化,这样整个网络的路由是不是就乱掉了。

对于Zookeeper来说有一个很重要的问题,就是到底是根据一个什么样的情况来判断一个节点死亡down掉了。 在分布式系统中这些都是有监控者来判断的,但是监控者也很难判定其他的节点的状态,唯一一个可靠的途径就是心跳,Zookeeper也是使用心跳来判断客户端是否仍然活着,但是使用心跳机制来判断节点的存活状态也带来了假死问题。

1.2 假死

ZooKeeper每个节点都尝试注册一个象征master的临时节点,其他没有注册成功的则成为slaver,并且通过watch机制监控着master所创建的临时节点,Zookeeper通过内部心跳机制来确定master的状态,一旦master出现意外Zookeeper能很快获悉并且通知其他的slaver,其他slaver在之后作出相关反应。这样就完成了一个切换。

这种模式也是比较通用的模式,基本大部分都是这样实现的,但是这里面有个很严重的问题,如果注意不到会导致短暂的时间内系统出现脑裂,因为心跳出现超时可能是master挂了,但是也可能是master,zookeeper之间网络出现了问题,也同样可能导致。这种情况就是假死,master并未死掉,但是与ZooKeeper之间的网络出现问题导致Zookeeper认为其挂掉了然后通知其他节点进行切换,这样slaver中就有一个成为了master,但是原本的master并未死掉,这时候client也获得master切换的消息,但是仍然会有一些延时,zookeeper需要通讯需要一个一个通知,这时候整个系统就很混乱可能有一部分client已经通知到了连接到新的master上去了,有的client仍然连接在老的master上如果同时有两个client需要对master的同一个数据更新并且刚好这两个client此刻分别连接在新老的master上,就会出现很严重问题。

1.3 总结

假死:由于心跳超时(网络原因导致的)认为master死了,但其实master还存活着。

脑裂:由于假死会发起新的master选举,选举出一个新的master,但旧的master网络又通了,导致出现了两个master ,有的客户端连接到老的master 有的客户端链接到新的master。

2、Zookeeper的解决方案

要解决Split-Brain的问题,一般有3种方式:

Quorums(ˈkwôrəm 法定人数) :比如3个节点的集群,Quorums = 2, 也就是说集群可以容忍1个节点失效,这时候还能选举出1个lead,集群还可用。比如4个节点的集群,它的Quorums = 3,Quorums要超过3,相当于集群的容忍度还是1,如果2个节点失效,那么整个集群还是无效的 Redundant communications:冗余通信的方式,集群中采用多种通信方式,防止一种通信方式失效导致集群中的节点无法通信。 Fencing, 共享资源的方式:比如能看到共享资源就表示在集群中,能够获得共享资源的锁的就是Leader,看不到共享资源的,就不在集群中。

ZooKeeper默认采用了Quorums这种方式,即只有集群中超过半数节点投票才能选举出Leader。这样的方式可以确保leader的唯一性,要么选出唯一的一个leader,要么选举失败。在ZooKeeper中Quorums有2个作用:

集群中最少的节点数用来选举Leader保证集群可用:通知客户端数据已经安全保存前集群中最少数量的节点数已经保存了该数据。一旦这些节点保存了该数据,客户端将被通知已经安全保存了,可以继续其他任务。而集群中剩余的节点将会最终也保存了该数据。 假设某个leader假死,其余的followers选举出了一个新的leader。这时,旧的leader复活并且仍然认为自己是leader,这个时候它向其他followers发出写请求也是会被拒绝的。因为每当新leader产生时,会生成一个epoch,这个epoch是递增的,followers如果确认了新的leader存在,知道其epoch,就会拒绝epoch小于现任leader epoch的所有请求。那有没有follower不知道新的leader存在呢,有可能,但肯定不是大多数,否则新leader无法产生。Zookeeper的写也遵循quorum机制,因此,得不到大多数支持的写是无效的,旧leader即使各种认为自己是leader,依然没有什么作用。 3、总结

总结一下就是,通过Quorums机制来防止脑裂和假死,当leader挂掉之后,可以重新选举出新的leader节点使整个集群达成一致;当出现假死现象时,通过epoch大小来拒绝旧的leader发起的请求,在前面也已经讲到过,这个时候,重新恢复通信的老的leader节点会进入恢复模式,与新的leader节点做数据同步,perfect。

原文: https://blog.csdn.net/u013374645/article/details/93140148

read more
There are no topics in this category.
Why don't you try posting one?