从源码的角度再学「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
HttpClient连接池原理及一次连接时序图
HttpClient介绍

HttpClient是一个实现了http协议的开源Java客户端工具库,可以通过程序发送http请求。

1.1.1 代码示例

以Get请求为例,以下代码获得google主页内容并将返回结果打印出来。

public final static void main(String[] args) throws Exception { HttpClient httpclient = new DefaultHttpClient(); try { HttpGet httpget = new HttpGet("http://www.google.com/"); System.out.println("executing request " + httpget.getURI()); // 创建response处理器 ResponseHandler<String> responseHandler = new BasicResponseHandler(); String responseBody = httpclient.execute(httpget, responseHandler); System.out.println("----------------------------------------"); System.out.println(responseBody); System.out.println("----------------------------------------"); } finally { //HttpClient不再使用时,关闭连接管理器以保证所有资源的释放 httpclient.getConnectionManager().shutdown(); } } 1.1.2 时序图

httpClient执行一次请求,即运行一次httpClient.execute()方法,时序图如下:

1.1.3.1 时序图编号说明

1.1、1.2、1.3等均为操作1的子操作,即:操作1 execute()中又分别调用了操作1.1 createClientConnectionManager()、操作1.2 createClientRequestDirector()以及操作1.3 requestDirector 对象的execute()方法等,以此类推。

按时间先后顺序分别编号为1,2,3等,以此类推。

1.1.3.2 主要类说明

对于图中各对象,httpClient jar包中均提供对应的接口及相应的实现类。

图中直接与服务器进行socket通信的是最右端接口OperatedClientConnection某一实现类的对象,图中从右到左进行了层层的封装,最终开发人员直接使用的是接口HttpClient某一实现类的对象进行请求的发送和响应的接收(如2.1.1代码示例)。

时序图中各对象所在类关系如下图类图所示(仅列出图中所出现的各个类及方法,参数多的方法省略部分参数,其他类属性和操作请参照源码):

1.1.3.2.1 接口OperatedClientConnection

该接口对应一个http连接,与服务器端建立socket连接进行通信。

1.1.3.2.2 接口ManagedClientConnection

该接口对一个http连接OperatedClientConnection进行封装,ManagedClientConnection维持一个PoolEntry<HttpRoute, OperatedClientConnection>路由和连接的对应。提供方法获得对应连接管理器,对http连接的各类方法,如建立连接,获得相应,关闭连接等进行封装。

1.1.3.2.3 接口RequestDirector

RequestDirector为消息的发送执行者,该接口负责消息路由的选择和可能的重定向,消息的鉴权,连接的分配回收(调用ClientConnectionManager相关方法),建立,关闭等并控制连接的保持。

连接是否保持以及保持时间默认原则如下:

连接是否保持:客户端如果希望保持长连接,应该在发起请求时告诉服务器希望服务器保持长连接(http 1.0设置connection字段为keep-alive,http 1.1字段默认保持)。根据服务器的响应来确定是否保持长连接,判断原则如下:

检查返回response报文头的Transfer-Encoding字段,若该字段值存在且不为chunked,则连接不保持,直接关闭。其他情况进入下一步。

检查返回的response报文头的Content-Length字段,若该字段值为空或者格式不正确(多个长度,值不是整数),则连接不保持,直接关闭。其他情况进入下一步

检查返回的response报文头的connection字段(若该字段不存在,则为Proxy-Connection字段)值

如果这俩字段都不存在,则http 1.1版本默认为保持,将连接标记为保持, 1.0版本默认为连接不保持,直接关闭。

如果字段存在,若字段值为close 则连接不保持,直接关闭;若字段值为keep-alive则连接标记为保持。

连接保持时间:连接交换至连接管理时,若连接标记为保持,则将由连接管理器保持一段时间;若连接没有标记为保持,则直接从连接池中删除并关闭entry。连接保持时,保持时间规则如下:

保持时间计时开始时间为连接交换至连接池的时间。

保持时长计算规则为:获取keep-alive字段中timeout属性的值,

若该字段存在,则保持时间为 timeout属性值*1000,单位毫秒。

若该字段不存在,则连接保持时间设置为-1,表示为无穷。

响应头日志示例:

17:59:42.051 [main] DEBUG org.apache.http.headers - << Keep-Alive: timeout=5, max=100 17:59:42.051 [main] DEBUG org.apache.http.headers - << Connection: Keep-Alive 17:59:42.051 [main] DEBUG org.apache.http.headers - << Content-Type: text/html; charset=utf-8 17:59:42.062 [main] DEBUG c.ebupt.omp.sop.srmms.SopHttpClient - Connection can be kept alive for 5000 MILLISECONDS

若需要修改连接的保持及重用默认原则,则需编写子类继承自AbstractHttpClient,分别覆盖其 createConnectionReuseStrategy() 和createConnectionKeepAliveStrategy() 方法。

1.1.3.2.4 接口ClientConnectionManager

ClientConnectionManager为连接池管理器,是线程安全的。Jar包中提供的具体实现类有BasicClientConnectionManager和PoolingClientConnectionManager。其中BasicClientConnectionManager只管理一个连接。PoolingClientConnectionManager管理连接池。

若有特殊需要,开发人员可自行编写连接管理器实现该接口。

连接管理器自动管理连接的分配以及回收工作,并支持连接保持以及重用。连接保持以及重用由RequestDirector进行控制。

1.1.3.2.5 接口HttpClient

接口HttpClient为开发人员直接使用的发送请求和接收响应的接口,是线程安全的。jar包中提供的实现类有:AbstractHttpClient, DefaultHttpClient, AutoRetryHttpClient, ContentEncodingHttpClient, DecompressingHttpClient, SystemDefaultHttpClient。其中其他所有类都继承自抽象类AbStractHttpClient,该类使用了门面模式,对http协议的处理进行了默认的封装,包括默认连接管理器,默认消息头,默认消息发送等,开发人员可以覆盖其中的方法更改其默认设置。

AbstractHttpClient默认设置连接管理器为BasicClientConnectionManager。若要修改连接管理器,则应该采用以下方式之一:

初始化时,传入连接池,例如:

ClientConnectionManager connManager = new PoolingClientConnectionManager();

HttpClient httpclient = new DefaultHttpClient(connManager);

编写httpClient接口的实现类,继承自AbstractHttpClient并覆盖其createClientConnectionManager()方法,在方法中创建自己的连接管理器。

1.1.3.3 方法说明

createClientConnectionManager(),创建连接池,该方法为protected。子类可覆盖修改默认连接池。

createClientRequestDirector(),创建请求执行者,该方法为protected。子类可覆盖但一般不需要。

httpClient中调用1.2方法所创建的请求执行者requestDirector的execute()方法。该方法中依次调用如下方法:

1.3.1调用连接管理器的requestConnection(route, userToken)方法,该方法调用连接池httpConnPool的lease方法,创建一个Future。Futrue用法参见Java标准API。返回clientConnectionRequest。

1.3.2调用clientConnectionRequest的getConnection(timeout, TimeUnit.MILLISECONDS)方法,该方法负责将连接池中可用连接分配给当前请求,具体如下:

创建clientConnectionOperator。

执行1.3.1中创建的Future的任务,该任务获得当前可用的poolEntry<router,OperatedClientConnection>并封装成managedClientConnectionImpl返回。

1.3.3调用 tryConnect(roureq, context)方法,该方法最终调用OperatedClientConnection的openning方法,与服务器建立socket连接。

1.3.4调用 tryExecute(roureq, context)方法,该方法最终调用OperatedClientConnection的receiveResponseHeader()和receiveResponseEntity()获得服务器响应。

1.3.5 判断连接是否保持用来重用,若保持,则设置保持时间,并将连接标记为可重用不保持则调用managedClientConnectionImpl的close方法关闭连接,该方法最终调用OperatedClientConnection的close()方法关闭连接。

最终respose返回至httpClient。

发送请求的线程需处理当前连接,若已被标记为重用,则交还至连接池管理器;否则,关闭当前连接。(使用响应处理器ResponseHanler)。本次请求结束。

1.2 HttpClient连接池

若连接管理器配置为PoolingClientConnectionManager,则httpClient将使用连接池来管理连接的分配,回收等操作。

1.2.1 连接池结构

连接池结构图如下,其中:

PoolEntry<HttpRoute, OperatedClientConnection>为路由和连接的对应。

routeToPool可以多个(图中仅示例两个);图中各队列大小动态变化,并不相等;

maxTotal限制的是外层httpConnPool中leased集合和available队列的总和的大小,leased和available的大小没有单独限制;

同理:maxPerRoute限制的是routeToPool中leased集合和available队列的总和的大小;

1.2.2.1 分配连接

分配连接给当前请求包括两部分:1从连接池获取可用连接PoolEntry;2.将连接与当前请求绑定。其中第一部分从连接池获取可用连接的过程为:

1 获取route对应连接池routeToPool中可用的连接,有则返回该连接。若没有则转入下一步。

2 若routeToPool和外层HttpConnPool连接池均还有可用的空间,则新建连接,并将该连接作为可用连接返回;否则进行下一步

3 将当前请求放入pending队列,等待执行。

4 上述过程中包含各个队列和集合的删除,添加等操作以及各种判断条件,具体流程如下:

1.2.2.2 回收连接

连接用完之后连接池需要进行回收,具体流程如下:

1 若当前连接标记为重用,则将该连接从routeToPool中的leased集合删除,并添加至available队列,同样的将该请求从外层httpConnPool的leased集合删除,并添加至其available队列。同时唤醒该routeToPool的pending队列的第一个PoolEntryFuture。将其从pending队列删除,并将其从外层httpConnPool的pending队列中删除。

2 若连接没有标记为重用,则分别从routeToPool和外层httpConnPool中删除该连接,并关闭该连接。

1.2.2.3 过期和空闲连接的关闭

连接如果标记为保持时,将由连接管理器保持一段时间,此时连接可能出现的情况是:

连接处于空闲状态,时间已超过连接保持时间

连接处于空闲状态,时间没有超过连接保持时间

以上两种情况中,随时都会出现连接的服务端已关闭的情况,而此时连接的客户端并没有阻塞着去接受服务端的数据,所以客户端不知道连接已关闭,无法关闭自身的socket。

连接池提供的方法:

首先连接池在每个请求获取连接时,都会在RouteToPool的available队列获取Entry并检测此时Entry是否已关闭或者已过期,若是则关闭并移除该Entry。

closeExpiredConnections()该方法关闭超过连接保持时间的空闲连接。

closeIdleConnections(timeout,tunit)该方法关闭空闲时间超过timeout的连接,空闲时间从交还给连接管理器时开始,不管是否已过期超过空闲时间则关闭。所以Idle时间应该设置的尽量长一点。

以上两个方法连接关闭的过程均是:

关闭entry;

RouteToPool中删除当前entry。先删available队列中的,如果没有,再删除leased集合中的。

httpConnPool中删除当前entry。删除过程同RouteToPool

唤醒阻塞在RouteToPool中的第一个future。

1.3.1 Tcp连接的关闭

Http连接实际上在传输层建立的是tcp连接,最终利用的是socket进行通信。http连接的保持和关闭实际上都和TCP连接的关闭有关。TCP关闭过程如下图:

说明:

TCP连接程序中使用socket编程进行实现。一条TCP是一条抽象的连接通道,由通信双方的IP+端口号唯一确定,两端分别通过socket实例进行操作,一个socket实例包括一个输入通道和输出通道,一端的输出通道为另一端的输入通道。

Tcp连接的关闭是连接的两端分别都需要进行关闭(调用close(socket),该函数执行发送FIN,等待ACK等图示操作)。实际上没有客户端和服务端的区别,只有主动关闭和被动关闭的区别。对于上层的其http连接,实际上也就是http服务端主动关闭或者http客户端主动关闭,而不管谁主动,最终服务端和客户端都需要调用close(socket)关闭连接。

主动关闭的一端A调用了close函数之后,若另一端B并没有阻塞着等待着数据,就无法检测到连接的A端已关闭,就没法关闭自身的socket,造成资源的浪费。http连接都是一次请求和响应,之后便交回给连接管理池,因此在http连接池中应当能够移除已过期或者空闲太久的连接,因为他们可能已经被服务器端关闭或者客户端短期内不再使用。

TIME_WAIT状态:

可靠地实现TCP全双工连接的终止

在进行关闭连接四路握手协议时,最后的ACK是由主动关闭端发出的,如果这个最终的ACK丢失,被动关闭端将重发最终的FIN,因此主动关闭端必须维护状态信息允许它重发最终的ACK。如果不维持这个状态信息,那么主动关闭端将发送RST分节(复位),被动关闭端将此分节解释成一个错误(在java中会抛出connection reset的SocketException)。因而,要实现TCP全双工连接的正常终止,主动关闭的客户端必须维持状态信息进入TIME_WAIT状态。

允许老的重复分节在网络中消逝

TCP分节可能由于路由器异常而“迷途”,在迷途期间,TCP发送端可能因确认超时而重发这个分节,迷途的分节在路由器修复后也会被送到最终目的地,这个原来的迷途分节就称为lost duplicate。在关闭一个TCP连接后,马上又重新建立起一个相同的IP地址和端口之间的TCP连接,后一个连接被称为前一个连接的化身(incarnation),那么有可能出现这种情况,前一个连接的迷途重复分组在前一个连接终止后出现,从而被误解成从属于新的化身。为了避免这个情况,TCP不允许处于TIME_WAIT状态的连接启动一个新的化身,因为TIME_WAIT状态持续2MSL,就可以保证当成功建立一个TCP连接的时候,来自连接先前化身的重复分组已经在网络中消逝。

2.1.1 版本

原Commons HttpClient:3.x不再升级维护,使用Apache HttpComponents的HttpClient代替。Pom文件修改如下:

1 原maven依赖:

<dependency> <groupId>commons-httpclient</groupId> <artifactId>commons-httpclient</artifactId> <version>3.1</version> </dependency>

2 替换为:

<dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.2.1</version> </dependency> 2.1.2 使用http连接池管理器

编写类继承自DefaultHttpClient(以下假设为SopHttpClient),覆盖其createClientConnectionManager()方法,方法中创建连接池管理器。

开启一个线程(假设为IdleConnectionMonitorThread)用来清除连接池中空闲和过期的连接。

2.1.3 保持HttpClient单例

Spring配置中使用默认scope,即单例模式,其他类使用时由Spring配置进行依赖注入,不要使用new方法。SopHttpClient应该提供方法destroy()并配置在Spring销毁该bean前调用,destory()方法中关闭对应连接池管理器和监控线程IdleConnectionMonitorThread。

2.1.4 异常处理机制(请求和响应):

编写类实现接口HttpRequestRetryHandler(可参照默认实现DefaultHttpRequestRetryHandler),并覆盖AbstractHttpClient中的createHttpRequestRetryHandler()方法创建新的重试处理机制。

2.1.5 参数可配置

各参数(连接池默认ip、端口和大小等,超时时间等)尽量都集中在SopHttpClient类中,设置为由Spring进行统一配置,且提供接口在程序中修改。

2.1.6.1 方式

HttpResponse response = httpclient.execute(httpMethod);

HttpEntity entity = response.getEntity();

这两段代码返回的entity是HttpEntity的实现类BasicManagedEntity。此时与本次请求关联的连接尚未归还至连接管理器。需要调用以下两条语句:

InputStream instream = entity.getContent();//获得响应具体内容

//处理响应:代码省略

instream.close();//关闭输入流同时会将连接交回至连接处理器

2.1.6.2 使用默认的响应处理器BasicResponseHandler

HttpClient Jar包中提供BasicResponseHandler。如果返回的类型能确定需要解码为String类型的话,推荐使用该响应处理器。

该处理器解码http连接响应字节流为String类型,对返回码>=300的响应进行了异常封装,并能够保证连接交还给连接池管理器。

该处理器将字节解码为字符的过程依次如下:

1 如果响应http报文Head部分由指定的charset,则使用该charset进行解码,否则进行下一步。例如使用UTF-8解码以下响应:

17:59:42.051 [main] DEBUG org.apache.http.headers - << Content-Type: text/html; charset=utf-8

2 如果响应报文未执行charset,则使用传入EntityUntils.toString()时指定的charset进行解码。否则进行下一步

3 使用ISO-8859-1进行解码。

2.1.6.3 BasicManagedEntity关闭连接池管理器原理

2.1.6.3.1

BasicManagedEntity实现了三个接口:HttpEntity,ConnectionReleaseTrigger, EofSensorWatcher。

调用BasicManagedEntity的getContent方法时,实际上初始化了EofSensorInputStream的实例,并将BasicManagedEntity当前对象自身作为EofSensorWatcher传入。

//BasicManagedEntity类的继承体系,HttpEntityWrapper实现了接口HttpEntity public class BasicManagedEntity extends HttpEntityWrapper implements ConnectionReleaseTrigger, EofSensorWatcher

BasicManagedEntity的getContent方法:

@Override public InputStream getContent() throws IOException { return new EofSensorInputStream(wrappedEntity.getContent(), this); } // EofSensorInputStream构造函数声明 public EofSensorInputStream(final InputStream in,final EofSensorWatcher watcher);

2.1.6.3.2

调用EofSensorInputStream的close方法,该方法调用自身的checkClose()方法,checkClose()方法中调入了传入的EofSensorWatcher watcher的streamClosed()方法并关闭输入流,由于上一步骤中实际传入的watcher是BasicManagedEntity的实例,因此实际上调用的是BasicManagedEntity的streamClose()方法。

//close方法 @Override public void close() throws IOException { // tolerate multiple calls to close() selfClosed = true; checkClose(); } //checkClose方法 protected void checkClose() throws IOException { if (wrappedStream != null) { try { boolean scws = true; // should close wrapped stream? if (eofWatcher != null) scws = eofWatcher.streamClosed(wrappedStream); if (scws) wrappedStream.close(); } finally { wrappedStream = null; } } }

2.1.6.3.3

BasicManagedEntity的streamClose()方法中将连接交回至连接池管理器。

public boolean streamClosed(InputStream wrapped) throws IOException { try { if (attemptReuse && (managedConn != null)) { boolean valid = managedConn.isOpen(); // this assumes that closing the stream will // consume the remainder of the response body: try { wrapped.close(); managedConn.markReusable(); } catch (SocketException ex) { if (valid) { throw ex; } } } } finally { releaseManagedConnection(); } return false;

}

2.1.7 其他

HttpClient 提供了非常灵活的架构,同时提供了很多接口,需要修改时,找到对应接口和默认实现类,参照默认实现类进行修改即可(或继承默认实现类,覆盖其对应方法)。通常需要更改的类有AbstractHttpClient和各种handler以及Strategy

文章转载自:https://developer.aliyun.com/article/11893

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?