<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>加多</title>
    <description>加多，著作《Java并发编程之美》，《Java异步编程实战》等，本站内容涵盖技术探讨与人生经验分享。</description>
    <link>http://www.huipeixue.cn/</link>
    <atom:link href="http://www.huipeixue.cn/feed.xml" rel="self" type="application/rss+xml"/>
    <pubDate>Sat, 08 Aug 2026 13:32:52 +0000</pubDate>
    <lastBuildDate>Sat, 08 Aug 2026 13:32:52 +0000</lastBuildDate>
    <generator>Jekyll v3.10.0</generator>
    
      <item>
        <title>Go并发编程之美-并发编程难在哪里</title>
        <description>&lt;h1 id=&quot;一前言&quot;&gt;一、前言&lt;/h1&gt;
&lt;p&gt;编写正确的程序本身就不容易，编写正确的并发程序更是难中之难，那么并发编程究竟难道哪里那？本节我们就来一探究竟。&lt;/p&gt;
&lt;h1 id=&quot;二数据竞争&quot;&gt;二、数据竞争&lt;/h1&gt;
&lt;p&gt;当两个或者多个线程（goroutine）在没有任何同步措施的情况下同时读写同一个共享资源时候，这多个线程（goroutine）就处于数据竞争状态，数据竞争会导致程序的运行结果超出写代码的人的期望。下面我们来看个例子：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Java&quot;&gt;package main
import (
	&quot;fmt&quot;
)

var a int

//goroutine1
func main() {
	
	//1,gouroutine2
	go func(){
		a = 1//1.1
	}()
	
	//2
	if 0 == a{//2.1
		fmt.Println(a)//2.2
	}
}
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
  &lt;li&gt;如上代码首先创建了一个int类型的变量，默认被初始化为0值，运行main函数会启动一个进程和这个进程中的一个运行main函数的goroutine(轻量级线程)&lt;/li&gt;
  &lt;li&gt;在main函数内使用go语句创建了一个新的goroutine（该goroutine运行匿名函数里面的内容）并启动运行，匿名函数内给变量赋值为1&lt;/li&gt;
  &lt;li&gt;main函数里面代码2判断如果变量a的值为0，则打印a的值。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;运行main函数后，启动的进程里面存在两个并发运行的线程，分别是开启的新goroutine(起名为goroutine2)和main函数所在的goroutine(起名为goroutine1),前者试图修改共享变量a,后者试图读取共享变量a,也就是存在两个线程在没有任何同步的情况下对同一个共享变量进行读写访问，这就出现了数据竞争，由于数据竞争存在，导致上面程序可能会有下面三种输出:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;输出0，由于运行时调度系统的随机性，会存在goroutine1的2.2代码比goroutine2的代码1.1先执行&lt;/li&gt;
  &lt;li&gt;输出1，当存在goroutine1先执行代码2.1,然后goroutine2在执行代码1.1，最后goroutine1在执行代码2.2的时候&lt;/li&gt;
  &lt;li&gt;什么都不输出，当goroutine2执行先于goroutine1的2.1代码时候。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;由于数据竞争的存在上面一段很短的代码会有三种可能的输出，究其原因是goroutine1和groutine2的运行时序是不确定的，也就是没有对他们的操作做同步，以便让这些内存操作变为可以预知的顺序执行。&lt;/p&gt;

&lt;p&gt;这里编写程序者或许受单线程模型的影响认为代码1.1会先于代码2.1执行，当发现输出不符合预期时候，或许会在代码2.1前面让goroutine1 休眠一会确保goroutine2执行完毕1.1后在让goroutine1执行2.1，这看起来或许有效，但是这是非常低效，并且并不是所有情况下都可以解决的。&lt;/p&gt;

&lt;p&gt;正确的做法可以使用信号量等同步措施，保证goroutine2执行完毕再让goroutine1执行代码2.1，如下面代码，我们使用sync包的WaitGroup来保证goroutine2执行完毕代码2.1后，goroutine1才可以执行步骤4.1,关于WaitGroup后面章节我们具体会讲解：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Java&quot;&gt;package main

import (
	&quot;fmt&quot;
	&quot;sync&quot;
)

var a int
var wg sync.WaitGroup//信号量
//goroutine1
func main() {
	//1.
	wg.Add(1);//一个信号
	
	//2. goroutine1
	go func(){
		a = 1//2.1
		wg.Done()
	}()
	
	wg.Wait()//3. 等待goroutine1运行结束
	
	//4
	if 0 == a{//4.1
		fmt.Println(a)//4.2
	}
}
&lt;/code&gt;&lt;/pre&gt;

&lt;h1 id=&quot;三操作的原子性&quot;&gt;三、操作的原子性&lt;/h1&gt;
&lt;p&gt;所谓原子性操作是指当执行一系列操作时候，这些操作那么全部被执行，那么全部不被执行，不存在只执行其中一部分的情况。在设计计数器时候一般都是先读取当前值，然后+1，然后更新，这个过程是读-改-写的过程，如果不能保证这个过程是原子性，那么就会出现线程安全问题。如下代码是线程不安全的，因为不能保证a++是原子性操作:&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;package&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;main&lt;/span&gt;

&lt;span class=&quot;nf&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;
	&lt;span class=&quot;s&quot;&gt;&quot;fmt&quot;&lt;/span&gt;
	&lt;span class=&quot;s&quot;&gt;&quot;sync&quot;&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;int32&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;wg&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sync&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;WaitGroup&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;//信号量&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;THREAD_NUM&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1000&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;//goroutine1&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;c1&quot;&gt;//1.信号&lt;/span&gt;
	&lt;span class=&quot;n&quot;&gt;wg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;Add&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;THREAD_NUM&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; 

	&lt;span class=&quot;c1&quot;&gt;//2. goroutine&lt;/span&gt;
	&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;THREAD_NUM&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;n&quot;&gt;go&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;func&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;//2.1&lt;/span&gt;
			&lt;span class=&quot;n&quot;&gt;wg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;Done&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;//2.2&lt;/span&gt;
		&lt;span class=&quot;o&quot;&gt;}()&lt;/span&gt;
	&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

	&lt;span class=&quot;n&quot;&gt;wg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;Wait&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;//3. 等待goroutine运行结束&lt;/span&gt;

	&lt;span class=&quot;n&quot;&gt;fmt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;Println&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;//4输出计数&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;ul&gt;
  &lt;li&gt;如上代码在main函数所在为goroutine内创建了THREAD_NUM个goroutine，每个新的goroutine执行代码2.1对变量count计数增加1。&lt;/li&gt;
  &lt;li&gt;这里创建了THREAD_NUM个信号量，用来在代码3处等待THREAD_NUM个goroutine执行完毕，然后输出最终计数，执行上面代码我们 期望输出1000，但是实际却不是。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这是因为a++操作本身不是原子性的，其等价于b := count;b=b+1;count=b;是三步操作，所以可能导致导致计数不准确，如下表：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;http://ata2-img.cn-hangzhou.img-pub.aliyun-inc.com/f32676e4f00b7621dcb8d4e23e3e2a5e.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;假如当前count=0那么t1时刻线程A读取了count值到变量countA,然后t2时刻递增countA值为1，同时线程B读取count的值0放到内存countB值为0（因为countA还没有写入主内存），t3时刻线程A才把countA为1的值写入主内存，至此线程A一次计数完毕，同时线程B递增CountB值为1，t4时候线程B把countB值1写入内存，至此线程B一次计数完毕。明明是两次计数，最后结果是1而不是2。&lt;/p&gt;

&lt;p&gt;上面的程序需要保证count++的原子性才是正确的，后面章节会知道使用sync/atomic包的一些原子性函数或者锁可以解决这个问题。&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;package&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;main&lt;/span&gt;

&lt;span class=&quot;nf&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;
	&lt;span class=&quot;s&quot;&gt;&quot;fmt&quot;&lt;/span&gt;
	&lt;span class=&quot;s&quot;&gt;&quot;sync&quot;&lt;/span&gt;
	&lt;span class=&quot;s&quot;&gt;&quot;sync/atomic&quot;&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;int32&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;wg&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sync&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;WaitGroup&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;//信号量&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;THREAD_NUM&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1000&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;//goroutine1&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;c1&quot;&gt;//1.信号&lt;/span&gt;
	&lt;span class=&quot;n&quot;&gt;wg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;Add&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;THREAD_NUM&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; 

	&lt;span class=&quot;c1&quot;&gt;//2. goroutine&lt;/span&gt;
	&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;THREAD_NUM&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;n&quot;&gt;go&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;func&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;c1&quot;&gt;//count++//&lt;/span&gt;
			&lt;span class=&quot;n&quot;&gt;atomic&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;AddInt32&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;//2.1&lt;/span&gt;
			&lt;span class=&quot;n&quot;&gt;wg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;Done&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;//2.2&lt;/span&gt;
		&lt;span class=&quot;o&quot;&gt;}()&lt;/span&gt;
	&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

	&lt;span class=&quot;n&quot;&gt;wg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;Wait&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;//3. 等待goroutine运行结束&lt;/span&gt;

	&lt;span class=&quot;n&quot;&gt;fmt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;Println&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;//4输出计数&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;如上代码使用原子性操作可以保证每次输出都是1000&lt;/p&gt;

&lt;h1 id=&quot;三内存访问同步&quot;&gt;三、内存访问同步&lt;/h1&gt;
&lt;p&gt;上节原子性操作第一个例子有问题是因为count++操作是被分解为类似b := count;b=b+1;count =b; 的三部操作，而多个goroutine同时执行count++时候并不是顺序执行者三个步骤的，而是可能交叉访问的。所以如果能对内存变量的访问添加同步访问措施，就可以避免这个问题：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Java&quot;&gt;package main

import (
	&quot;fmt&quot;
	&quot;sync&quot;
)

var count int32
var wg sync.WaitGroup //信号量
var lock sync.Mutex   //互斥锁
const THREAD_NUM = 1000

//goroutine1
func main() {
	//1.信号
	wg.Add(THREAD_NUM)

	//2. goroutine
	for i := 0; i &amp;lt; THREAD_NUM; i++ {
		go func() {
			lock.Lock()   //2.1
			count++       //2.2
			lock.Unlock() //2.3
			wg.Done()     //2.4
		}()
	}

	wg.Wait() //3. 等待goroutine运行结束

	fmt.Println(count) //4输出计数
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
  &lt;li&gt;如上代码创建了一个互斥锁lock,然后goroutine内在执行count++前先获取锁，执行完毕后在释放锁。&lt;/li&gt;
  &lt;li&gt;当1000个goroutine同时执行到代码2.1时候只有一个线程可以获取到锁，其他的线程被阻塞，直到获取到锁的goroutine释放了锁。也就是这1000个线程的并发行使用锁转换为了串行执行，也就是对共享内存变量的访问施加了同步措施。&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;四总结&quot;&gt;四、总结&lt;/h1&gt;
&lt;p&gt;本文我们从数据竞争、原子性操作、内存同步三个方面探索了并发编程到底难在哪里，后面章节我们会结合go的内存模型和happen-before原则在具体探索这些难点如何解决。&lt;/p&gt;
</description>
        <pubDate>Thu, 01 Oct 2020 00:00:00 +0000</pubDate>
        <link>http://www.huipeixue.cn/go/2020/10/01/go-concurrenty.html</link>
        <guid isPermaLink="true">http://www.huipeixue.cn/go/2020/10/01/go-concurrenty.html</guid>
        
        <category>go</category>
        
        
        <category>go</category>
        
      </item>
    
      <item>
        <title>浅谈分布式领域CAP理论</title>
        <description>&lt;h1 id=&quot;一前言&quot;&gt;一、前言&lt;/h1&gt;
&lt;p&gt;CAP是分布式系统的重要理论，在大型分布式系统中一致性（Consistency），高可用性（High-Availability），分区可容忍性（Partition-tolerance）是设计者都希望能同时达到的，但是根据CAP理论一个系统最多能实现3中其2。本文不去探讨CAP理论的结论的正确性，而是去试图概述这三个特性是什么。&lt;/p&gt;
&lt;h1 id=&quot;二-一致性&quot;&gt;二、 一致性&lt;/h1&gt;
&lt;p&gt;一个完全一致性的系统可以保证一旦你修改了系统中存储的一个状态，那么在下次显示修改改状态值之前所有访问这个状态值的操作获取的状态值都是一样的。&lt;/p&gt;

&lt;p&gt;[example1]例如一个单点MySQL数据库实例天然会保证状态的一致性，这是因为只有一个节点来保持这个状态。&lt;/p&gt;

&lt;p&gt;[exmaple2]例如有两个MySQL数据库实例，并且数据是使用分片的方式将所有的数据分散到两个数据库实例上面，也就是两个数据库里面存放的为整体数据的一部分，那么这个情况下系统仍然天然具有完整一致性。&lt;/p&gt;

&lt;p&gt;[exmaple3]假如现在数据库设置为主-主备份系统，其中一个数据库接受到插入数据请求时候，必须将该请求信息提交给另外一个数据库后，当前数据库的插入请求才被认为完成。那么为了保证100%的一致性，主-主备份数据库节点之间需要进行通信，并且随着备份节点的增多，系统整体性能会下降。&lt;/p&gt;

&lt;h1 id=&quot;三高可用性&quot;&gt;三、高可用性&lt;/h1&gt;
&lt;p&gt;在上面3个例子中[exmaple1]和[exmaple2]不是高可用性的，其中[exmaple1]如果数据库实例出现故障，那么100%的数据将会丢失，[exmaple2]中如果有一个节点发送故障，那么将会有50%的数据丢失。[exmaple3]是保证高可用的一个解决方法，一个简单的MySQL服务器备份（多主模式）可以提供100%的可用性，增加数据副本节点的数量将会直接增加系统的可用性，使用副本不仅保证了硬件故障时候的可用性，还有助于负载平衡和并发操作，特别是读取操作， “Slave”MySQL实例就是这种“复制”的完美例子。&lt;/p&gt;

&lt;h1 id=&quot;四分区可容忍性&quot;&gt;四、分区可容忍性&lt;/h1&gt;
&lt;p&gt;假设[exmaple3]中两个MYSQL服务器处于两个不同的数据中心，并且两个服务器之间的网络失去了连接(比如其中一个节点挂了)，那么当前系统处于了分区状态，由于网络原因，数据库之间就无法进行数据同步，那么两个数据库就处于不一致性的状态了。当系统处于分区状态时候就需要衡量A和C直接那个更重要了，如果银行觉的一致性是非常重要的，并且在停机期间禁用写操作，则两个分行的所有银行账户现在将被冻结，直到网络恢复，然后这则将失去集群的“可用性”，如果银行觉得可用性比较重要，则没有挂的一个系统继续进行提供单点的服务，而这是牺牲了一致性。&lt;/p&gt;

&lt;h1 id=&quot;五总结&quot;&gt;五、总结&lt;/h1&gt;
&lt;p&gt;本文用了三个例子来阐述CAP，其中example1和example2上都能保证强一致性，但不能保证可用性。example3这个例子，由于存在分区，就需要在一致性与可用性之间进行选择。对应复制的策略很多场景下不需要保证强一致性，但是有时候需要保证C。比如银行交易需要保证C则要保证本地库和异地库同时进行更新才算成功。但是有些情况下需要保证高可用性，当一个数据库发生故障后自动切换到备库写，以便继续对外提供服务.&lt;/p&gt;

&lt;h1 id=&quot;六参考&quot;&gt;六、参考&lt;/h1&gt;
&lt;p&gt;本文翻译自文章 http://www.royans.net/wp/2010/02/14/brewers-cap-theorem-on-distributed-systems/ 并加入了作者的理解。&lt;/p&gt;

</description>
        <pubDate>Fri, 11 Sep 2020 00:00:00 +0000</pubDate>
        <link>http://www.huipeixue.cn/java/2020/09/11/cap.html</link>
        <guid isPermaLink="true">http://www.huipeixue.cn/java/2020/09/11/cap.html</guid>
        
        <category>java</category>
        
        
        <category>java</category>
        
      </item>
    
      <item>
        <title>你真的了解Netty中@Sharable？</title>
        <description>&lt;h1 id=&quot;一前言&quot;&gt;一、前言&lt;/h1&gt;
&lt;p&gt;Netty 是一个可以快速开发网络应用程序的基于事件驱动的异步 网络通讯 框架，它大大简化了 TCP 或者 UDP 服务器的网络编程。Netty 的应用还是比较广泛的，比如阿里巴巴开源的 Dubbo 和 Sofa-Bolt等 框架底层网络通讯都是基于 Netty 来实现的。Netty的设计是精妙的，其中每个设计点都值得我们去深思，本节我们来看看Netty中@Sharable的设计哲学&lt;/p&gt;

&lt;h1 id=&quot;二netty基础快速回顾&quot;&gt;二、Netty基础快速回顾&lt;/h1&gt;
&lt;p&gt;如果你熟悉netty请直接看第三节&lt;/p&gt;

&lt;p&gt;io.netty.channel.Channel 是 Netty 框架自己定义的一个通道接口，Netty 实现的客户端 NIO 套接字通道是 NioSocketChannel，提供的服务器端 NIO 套接字通道是 NioServerSocketChannel。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;NioSocketChannel：客户端套接字通道，内部管理了一个 Java NIO 中的 java.nio.channels.SocketChannel 实例，用来创建 SocketChannel 实例和设置该实例的属性，并调用 Connect 方法向服务端发起 TCP 链接等。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;NioServerSocketChannel：服务器端监听套接字通道，内部管理了一个 Java NIO 中的 java.nio.channels.ServerSocketChannel 实例，用来创建 ServerSocketChannel 实例和设置该实例属性，并调用该实例的 bind 方法在指定端口监听客户端的链接。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Channel 与 socket 的关系：在Netty中 Channel 有两种，对应客户端套接字通道 NioSocketChannel，内部管理 java.nio.channels.SocketChannel 套接字，对应服务器端监听套接字通道 NioServerSocketChannel，其内部管理自己的 java.nio.channels.ServerSocketChannel 套接字。也就是 Channel 是对 socket 的装饰或者门面，其封装了对 socket 的原子操作。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;EventLoopGroup：Netty 之所以能提供高性能网络通讯，其中一个原因是因为它使用 Reactor 线程模型。在netty中每个 EventLoopGroup 本身是一个线程池，其中包含了自定义个数的 NioEventLoop，每个 NioEventLoop 是一个线程，并且每个 NioEventLoop 里面持有自己的 selector 选择器。&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;在 Netty 中客户端持有一个 EventLoopGroup 用来处理网络 IO 操作，在服务器端持有两个 EventLoopGroup，其中 boss 组是专门用来接收客户端发来的 TCP 链接请求的，worker 组是专门用来具体处理完成三次握手的链接套接字的网络 IO 请求的。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Channel 与 EventLoop 的关系：Netty 中 NioEventLoop 是 EventLoop 的一个实现，每个 NioEventLoop 中会管理自己的一个 selector 选择器和监控选择器就绪事件的线程；每个 Channel 只会关联一个 NioEventLoop；&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;当 Channel 是客户端通道 NioSocketChannel 时候，会注册 NioSocketChannel 管理的 SocketChannel 实例到自己关联的 NioEventLoop 的 selector 选择器上，然后 NioEventLoop 对应的线程会通过 select 命令监控感兴趣的网络读写事件；&lt;/p&gt;

&lt;p&gt;当 Channel 是服务端通道 NioServerSocketChannel 时候，NioServerSocketChannel 本身会被注册到 boss EventLoopGroup 里面的某一个 NioEventLoop 管理的 selector 选择器上，而完成三次握手的链接套接字是被注册到了 worker EventLoopGroup 里面的某一个 NioEventLoop 管理的 selector 选择器上；&lt;/p&gt;

&lt;p&gt;需要注意是多个 Channel 可以注册到同一个 NioEventLoop 管理的 selector 选择器上，这时候 NioEventLoop 对应的单个线程就可以处理多个 Channel 的就绪事件；但是每个 Channel 只能注册到一个固定的 NioEventLoop 管理的 selector 选择器上。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;ChannelPipeline：Netty 中的 ChannelPipeline 类似于 Tomcat 容器中的 Filter 链，属于设计模式中的责任链模式，其中链上的每个节点就是一个 ChannelHandler。在 netty 中每个 Channel 有属于自己的 ChannelPipeline，对从 Channel 中读取或者要写入 Channel 中的数据进行依次处理, 如下图是 netty 源码里面的一个图：&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/2020/nettyshare-1.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;需要注意一点是虽然每个 Channel（更底层说是每个 socket）有自己的 ChannelPipeline，但是每个 ChannelPipeline 里面可以复用通一个 ChannelHandler(也就是标注了@Sharable注解的handler可以被复用)。&lt;/p&gt;

&lt;h1 id=&quot;三channelhandler&quot;&gt;三、ChannelHandler&lt;/h1&gt;
&lt;p&gt;上节我们提到每个 Channel（更底层说是每个 socket）有自己的 ChannelPipeline，每个 ChannelPipeline里面管理者一系列的ChannelHandler。&lt;/p&gt;

&lt;p&gt;正常情况下每个 Channel自己的 ChannelPipeline管理的同一个ChannelHandler Class对象的实例都是直接new的一个新实例，也就是原型模式，而不是单例模式。
&lt;img src=&quot;/assets/images/2020/nettyshare-2.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;如上图当我们启动Netty服务端时候，会设置childHander，这个childHander会当服务器接受到完成TCP三次握手链接的时候给当前完成握手的Channel通道创建一个ChannelPipeline，并且创建一个EchoServerHandler的实例加入到Channel通道的ChannelPipeline。也就是说服务器接受的所有Channel对应的ChannelPipeline里面管理者自己的EchoServerHandler实例，而不是同一个。&lt;/p&gt;

&lt;p&gt;但是有时候我们却想让不同Channel对应的ChannelPipeline里面管理同一个EchoServerHandler实例，比如为了全局的一些统计信息，既然上面说当服务器接受到完成TCP三次握手链接的时候给当前完成握手的Channel通道创建一个ChannelPipeline，并且创建一个EchoServerHandler的实例加入到Channel通道的ChannelPipeline，那么我们创建一个单例的EchoServerHandler传递给childHandler是不是就可以了？我们修改上面代码如下：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/2020/nettyshare-3.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;这样当服务器接受到完成TCP三次握手链接的时候给当前完成握手的Channel通道创建一个ChannelPipeline，并且添加同一个EchoServerHandler的实例到对应管道。&lt;/p&gt;

&lt;p&gt;启动上面代码，然后客户端发起多个链接时候，会有下面结果：
&lt;img src=&quot;/assets/images/2020/nettyshare-4.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;这是因为我们的EchoServerHandler没有被添加@Sharable注解，现在添加如下：
&lt;img src=&quot;/assets/images/2020/nettyshare-5.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;再次运行就OK了。&lt;/p&gt;

&lt;p&gt;具体检查的代码是DefaultChannelPipeline类的checkMultiplicity的checkMultiplicity方法：
&lt;img src=&quot;/assets/images/2020/nettyshare-6.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/2020/nettyshare-7.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;可知当添加到不同管线的是不同的实例时候，不同连接在检查时候h.added总是返回的false，所以不会抛出异常。当添加到不同管线的是同一个实例时候，由于是单例，所以第一个连接会把单例的对象的added设置为了true，所以其他连接检查时候发现没有添加@Sharable注解并且当前added为true则会抛出异常。&lt;/p&gt;
&lt;h1 id=&quot;四总结&quot;&gt;四、总结&lt;/h1&gt;
&lt;p&gt;正常情况下同一个ChannelHandler,的不同的实例会被添加到不同的Channel管理的管线里面的，但是如果你需要全局统计一些信息，比如所有连接报错次数（exceptionCaught）等，这时候你可能需要使用单例的ChannelHandler，需要注意的是这时候ChannelHandler上需要添加@Sharable注解。&lt;/p&gt;
</description>
        <pubDate>Thu, 10 Sep 2020 00:00:00 +0000</pubDate>
        <link>http://www.huipeixue.cn/java/2020/09/10/netty-share.html</link>
        <guid isPermaLink="true">http://www.huipeixue.cn/java/2020/09/10/netty-share.html</guid>
        
        <category>java</category>
        
        
        <category>java</category>
        
      </item>
    
      <item>
        <title>Java异步编程实战：基于JDK中的Future实现异步编程</title>
        <description>&lt;h1 id=&quot;一前言&quot;&gt;一、前言&lt;/h1&gt;
&lt;p&gt;本节主要讲解如何使用JDK中的Future实现异步编程，这包含如何使用FutureTask实现异步编程以及其内部实现原理；&lt;/p&gt;

&lt;h1 id=&quot;二-jdk-中的future&quot;&gt;二、 JDK 中的Future&lt;/h1&gt;

&lt;p&gt;在Java并发包（JUC包）中Future代表着异步计算结果，Future中提供了一些列方法用来检查计算结果是否已经完成，还提供了同步等待任务执行完成的方法，以及获取计算结果的方法等。当计算结果完成时只能通过提供的get系列方法来获取结果，如果使用了不带超时时间的get方法则在计算结果完成前，调用线程会被一直阻塞。另外计算任务是可以使用cancle方法来取消的，但是一旦一个任务计算完成，则不能再被取消了。&lt;/p&gt;

&lt;p&gt;首先我们看下Future接口的类图结构如图3-1-1：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/2020/future1.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;图3-1-1Future类图&lt;/p&gt;

&lt;p&gt;如上图3-1-1Future类总共就有5个接口方法，下面我们来一一讲解：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;V get() throws InterruptedException, ExecutionException :等待异步计算任务完成，并返回结果；如果当前任务计算还没完成则会阻塞调用线程直到任务完成；如果在等待结果的过程中有其他线程取消了该任务，则调用线程抛出CancellationException异常；如果在等待结果的过程中有其他线程中断了该线程，则调用线程抛出InterruptedException异常；如果任务计算过程中抛出了异常，则调用线程会抛出ExecutionException异常。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;V get(long timeout, TimeUnit unit)
     throws InterruptedException, ExecutionException, TimeoutException:相比get()方法多了超时时间，不同在于线程调用了该方法后在任务结果没有计算出来前调用线程不会一直被阻塞，而是会在等待timeout个unit单位的时间后抛出TimeoutException异常后返回。添加超时时间这避免了调用线程死等的情况，让调用线程可以及时释放。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;boolean isDone():如果计算任务已经完成则返回true，否则返回false，需要注意的是任务完成是指任务正常完成了或者由于抛出异常而完成了或者任务被取消了。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;boolean cancel(boolean mayInterruptIfRunning) :尝试取消任务的执行;如果当前任务已经完成或者任务已经被取消了，则尝试取消任务会失败；如果任务还没被执行时候，调用了取消任务，则任务将永远不会被执行；如果任务已经开始运行了，这时候取消任务，则参数mayInterruptIfRunning将决定是否要将正在执行任务的线程中断，如果为true则标识要中断，否则标识不中断；当调用取消任务后，在调用isDone()方法，后者会返回true，随后调用isCancelled()方法也会一直返回true；该方法会返回false，如果任务不能被取消，比如任务已经完成了，任务已经被取消了。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;boolean isCancelled():如果任务在被执行完毕前被取消了，则该方法返回true，否则返回false。&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;三-jdk中的futuretask&quot;&gt;三 JDK中的FutureTask&lt;/h1&gt;
&lt;h2 id=&quot;31-futuretask-概述&quot;&gt;3.1 FutureTask 概述&lt;/h2&gt;
&lt;p&gt;FutureTask代表了一个可被取消的异步计算任务，该类实现了Future接口，比如提供了启动和取消任务、查询任务是否完成、获取计算结果的接口。&lt;/p&gt;

&lt;p&gt;FutureTask任务的结果只有当任务完成后才能获取，并且只能通过get系列方法获取，当结果还没出来时候，线程调用get系列方法会被阻塞；另外一旦任务被执行完成，任务不能被重启，除非运行时候使用了runAndReset方法；FutureTask中的任务可以是Callable类型，也可以是Runnable类型（因为FutureTask实现了Runnable接口），FutureTask类型的任务可以被提交到线程池执行。&lt;/p&gt;

&lt;p&gt;我们修改上节的例子如下：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;public class AsyncFutureExample {

	public static String doSomethingA() {

		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(&quot;--- doSomethingA---&quot;);

		return &quot;TaskAResult&quot;;
	}

	public static String doSomethingB() {
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(&quot;--- doSomethingB---&quot;);
		return &quot;TaskBResult&quot;;

	}

	public static void main(String[] args) throws InterruptedException, ExecutionException {

		long start = System.currentTimeMillis();

		// 1.创建future任务
		FutureTask&amp;lt;String&amp;gt; futureTask = new FutureTask&amp;lt;String&amp;gt;(() -&amp;gt; {
			String result = null;
			try {
				result = doSomethingA();

			} catch (Exception e) {
				e.printStackTrace();
			}
			return result;
		});

		// 2.开启异步单元执行任务A
		Thread thread = new Thread(futureTask, &quot;threadA&quot;);
		thread.start();

		// 3.执行任务B
		String taskBResult = doSomethingB();

		// 4.同步等待线程A运行结束
		String taskAResult = futureTask.get();
		
		//5.打印两个任务执行结果
		System.out.println(taskAResult + &quot; &quot; + taskBResult); 
		System.out.println(System.currentTimeMillis() - start);

	}
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;如上代码doSomethingA和doSomethingB方法都是有返回值的任务，main函数内代码1创建了一个异步任务futureTask，其内部执行任务doSomethingA。&lt;/li&gt;
  &lt;li&gt;代码2则创建了一个线程，并且以futureTask为执行任务，并且启动；代码3使用main线程执行任务doSomethingB，这时候任务doSomethingB和doSomethingA是并发运行的，等main函数运行doSomethingB完毕后，执行代码4同步等待doSomethingA任务完成，然后代码5打印两个任务的执行结果。&lt;/li&gt;
  &lt;li&gt;如上可知使用FutureTask可以获取到异步任务的结果。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;当然我们也可以把FutureTask提交到线程池来执行，使用线程池运行方式代码如下：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;	// 0自定义线程池
	private final static int AVALIABLE_PROCESSORS = Runtime.getRuntime().availableProcessors();
	private final static ThreadPoolExecutor POOL_EXECUTOR = new ThreadPoolExecutor(AVALIABLE_PROCESSORS,
			AVALIABLE_PROCESSORS * 2, 1, TimeUnit.MINUTES, new LinkedBlockingQueue&amp;lt;&amp;gt;(5),
			new ThreadPoolExecutor.CallerRunsPolicy());

	public static void main(String[] args) throws InterruptedException, ExecutionException {

		long start = System.currentTimeMillis();

		// 1.创建future任务
		FutureTask&amp;lt;String&amp;gt; futureTask = new FutureTask&amp;lt;String&amp;gt;(() -&amp;gt; {
			String result = null;
			try {
				result = doSomethingA();

			} catch (Exception e) {
				e.printStackTrace();
			}
			return result;
		});

		// 2.开启异步单元执行任务A
		POOL_EXECUTOR.execute(futureTask);

		// 3.执行任务B
		String taskBResult = doSomethingB();

		// 4.同步等待线程A运行结束
		String taskAResult = futureTask.get();
		// 5.打印两个任务执行结果
		System.out.println(taskAResult + &quot; &quot; + taskBResult);
		System.out.println(System.currentTimeMillis() - start);
	}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;如上可知代码0创建了一个线程池，代码2添加异步任务到线程池，这里我们是调用了线程池的execute方法把futureTask提交到线程池的，其实下面代码与上面是等价的：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;	public static void main(String[] args) throws InterruptedException, ExecutionException {

		long start = System.currentTimeMillis();

		// 1.开启异步单元执行任务A
		Future&amp;lt;String&amp;gt; futureTask = POOL_EXECUTOR.submit(() -&amp;gt; {
			String result = null;
			try {
				result = doSomethingA();

			} catch (Exception e) {
				e.printStackTrace();
			}
			return result;
		});

		// 2.执行任务B
		String taskBResult = doSomethingB();

		// 3.同步等待线程A运行结束
		String taskAResult = futureTask.get();
		// 4.打印两个任务执行结果
		System.out.println(taskAResult + &quot; &quot; + taskBResult);
		System.out.println(System.currentTimeMillis() - start);
	}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;如上代码1我们调用了线程池的submit方法提交了一个任务到线程池，然后返回了一个FutureTask对象。&lt;/p&gt;

&lt;h2 id=&quot;32-futuretask的类图结构&quot;&gt;3.2 FutureTask的类图结构：&lt;/h2&gt;
&lt;p&gt;由于FutureTask在异步编程领域还是比较重要的，所以我们有必要探究下其原理，以便加深对异步的理解，首先我们来看下其类图结构如图3-2-2-1：
&lt;img src=&quot;/assets/images/2020/future2.png&quot; alt=&quot;image.png&quot; /&gt;
图3-2-2-1 FutureTask的类图&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;如上时序图3-2-2-1FutureTask实现了Future接口的所有方法，并且实现了Runnable接口，所以其是可执行任务，可以投递到线程池或者线程来执行。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;FutureTask中变量state是一个使用volatile关键字修饰（用来解决多线程下内存不可见问题，具体可以参考《Java并发编程之美》一书）的int类型，用来记录任务状态，任务状态枚举值如下：&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;pre&gt;&lt;code class=&quot;language-Java&quot;&gt;    private static final int NEW          = 0;
    private static final int COMPLETING   = 1;
    private static final int NORMAL       = 2;
    private static final int EXCEPTIONAL  = 3;
    private static final int CANCELLED    = 4;
    private static final int INTERRUPTING = 5;
    private static final int INTERRUPTED  = 6;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;一开始任务状态会被初始化为NEW；当通过set、 setException、 cancel函数设置任务结果时候，任务会转换为终止状态；在任务完成过程中，设置任务状态会变为COMPLETING（当结果被使用set方法设置时候），也可能会经过INTERRUPTING状态（当使用cancel(true)方法取消任务并中断任务时候）；当任务被中断后，任务状态为INTERRUPTED；当任务被取消后，任务状态为CANCELLED；当任务正常终止时候，任务状态为NORMAL；当任务执行异常后，任务会变为EXCEPTIONAL状态。&lt;/p&gt;

&lt;p&gt;另外在任务运行过程中，任务可能的状态转换路径如下:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;NEW -&amp;gt; COMPLETING -&amp;gt; NORMAL ：正常终止流程转换&lt;/li&gt;
  &lt;li&gt;NEW -&amp;gt; COMPLETING -&amp;gt; EXCEPTIONAL：执行过程中发生异常流程转换&lt;/li&gt;
  &lt;li&gt;NEW -&amp;gt; CANCELLED：任务还没开始就被取消&lt;/li&gt;
  &lt;li&gt;NEW -&amp;gt; INTERRUPTING -&amp;gt; INTERRUPTED：任务被中断&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;从上述转换可知，任务最终只有四种终态：NORMAL、EXCEPTIONAL、CANCELLED、INTERRUPTED，另外可知任务的状态值是从上到下递增的。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;类图中callable是有返回值的可执行任务，创建FutureTask对象时候，可以通过构造函数传递该任务。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;类图中outcome是任务运行的结果，可以通过get系列方法来获取该结果,另外outcome这里没有被修饰为volatile，是因为变量state已经被使用volatile修饰了，这里是借用volatile的内存语义来保证写入outcome时候会把值刷新到主内存，读取时候会从主内存读取，从而避免多线程下内存不可见问题（可以参考《Java并发编程之美》一书）。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;类图中runner变量，记录了运行该任务的线程，这个是在FutureTask的run方法内使用CAS函数设置的。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;类图中waiters变量是一个WaitNode节点，使用Treiber stack实现的无锁栈，栈顶元素就是使用waiters代表，栈用来记录所有等待任务结果的线程节点，其定义为：&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;pre&gt;&lt;code class=&quot;language-Java&quot;&gt;    static final class WaitNode {
        volatile Thread thread;
        volatile WaitNode next;
        WaitNode() { thread = Thread.currentThread(); }
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可知其是一个简单的链表，用来记录所有等待结果而被阻塞的线程。&lt;/p&gt;

&lt;p&gt;最后我们看下其构造函数：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Java&quot;&gt;    public FutureTask(Callable&amp;lt;V&amp;gt; callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如上代码可知构造函数内保存了传递的callable任务到callable变量，并且设置任务状态为NEW，这里由于state为volatile修饰，所以写入state的值可以保证callable的写入也会被刷入主内存，这避免了多线程下内存不可见性。&lt;/p&gt;

&lt;p&gt;另外还有一个构造函数：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Java&quot;&gt;    public FutureTask(Runnable runnable, V result) {
        this.callable = Executors.callable(runnable, result);
        this.state = NEW;     
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;该函数传入一个Runnable类型的任务，由于该任务是不具有返回值的，所以这里使用Executors.callable方法进行适配，适配为Callable类型的任务.&lt;/p&gt;

&lt;p&gt;Executors.callable(runnable, result);把Runnable类型任务转换为了callable：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Java&quot;&gt;    public static &amp;lt;T&amp;gt; Callable&amp;lt;T&amp;gt; callable(Runnable task, T result) {
        if (task == null)
            throw new NullPointerException();
        return new RunnableAdapter&amp;lt;T&amp;gt;(task, result);
    }

    static final class RunnableAdapter&amp;lt;T&amp;gt; implements Callable&amp;lt;T&amp;gt; {
        final Runnable task;
        final T result;
        RunnableAdapter(Runnable task, T result) {
            this.task = task;
            this.result = result;
        }
        public T call() {
            task.run();
            return result;
        }
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如上可知使用了适配器模式来做转换。&lt;/p&gt;

&lt;p&gt;另外FutureTask中使用了UNSAFE机制来操作内存变量：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Java&quot;&gt; private static final sun.misc.Unsafe UNSAFE;

    private static final long stateOffset;//state变量的偏移地址
    private static final long runnerOffset;//runner变量的偏移地址
    private static final long waitersOffset;//waiters变量的偏移地址
    static {
        try {
            //获取UNSAFE的实例
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class&amp;lt;?&amp;gt; k = FutureTask.class;
            //获取变量state的偏移地址
            stateOffset = UNSAFE.objectFieldOffset
                (k.getDeclaredField(&quot;state&quot;));
            //获取变量runner的偏移地址
            runnerOffset = UNSAFE.objectFieldOffset
                (k.getDeclaredField(&quot;runner&quot;));
            //获取变量waiters变量的偏移地址
            waitersOffset = UNSAFE.objectFieldOffset
                (k.getDeclaredField(&quot;waiters&quot;));
        } catch (Exception e) {
            throw new Error(e);
        }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如上代码分别获取了FutureTask中几个变量在FutureTask对象内的内存地址偏移量，以便实现中使用UNSAFE的CAS操作来操作这些变量。&lt;/p&gt;

&lt;h2 id=&quot;33--futuretask的run-方法&quot;&gt;3.3  FutureTask的run() 方法&lt;/h2&gt;
&lt;p&gt;该方法是任务的执行体，线程是调用该方法来具体运行任务的，如果任务没有被取消，则该方法会运行任务，并且设置结果到outcome变量里面，其代码：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Java&quot;&gt;public void run() {
    //1.如果任务不是初始化的NEW状态，或者使用CAS设置runner为当前线程失败，则直接返回
    if (state != NEW ||
        !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                     null, Thread.currentThread()))
        return;
    //2.如果任务不为null，并且任务状态为NEW，则执行任务
    try {
        Callable&amp;lt;V&amp;gt; c = callable;
        if (c != null &amp;amp;&amp;amp; state == NEW) {
            V result;
            boolean ran;
            //2.1执行任务，如果OK则设置ran标记为true
            try {
                result = c.call();
                ran = true;
            } catch (Throwable ex) {
            //2.2执行任务出现异常，则标记false，并且设置异常
                result = null;
                ran = false;
                setException(ex);
            }
            //3.任务执行正常，则设置结果
            if (ran)
                set(result);
        }
    } finally {
        
        
        runner = null;
        int s = state;
       //4.为了保证调用cancel(true)的线程在该run方法返回前中断任务执行的线程
        if (s &amp;gt;= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
}

private void handlePossibleCancellationInterrupt(int s) {
    //为了保证调用cancel在该run方法返回前中断任务执行的线程
    //这里使用Thread.yield()让run方法执行线程让出cpu执行权，以便让
    //cancel(true)的线程执行cancel(true)中的代码中断任务线程
    if (s == INTERRUPTING)
        while (state == INTERRUPTING)
            Thread.yield(); // wait out pending interrupt
}
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;如上代码1，如果任务不是初始化的NEW状态，或者使用CAS设置runner为当前线程失败，则直接返回；这个可以防止同一个FutureTask对象被提交给多个线程来执行，导致run方法被多个线程同时执行造成混乱。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;代码2，如果任务不为null，并且任务状态为NEW，则执行任务，其中代码2.1调用c.call()具体执行任务，如果任务执行OK，则调用set方法把结果记录到result,并设置ran为true；否则执行任务过程中抛出异常则设置result为null，ran为false，并且调用setException设置异常信息后，任务就处于终止状态，其中setException代码如下：&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;pre&gt;&lt;code class=&quot;language-Java&quot;&gt;protected void setException(Throwable t) {
    //2.2.1
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        outcome = t;
        UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
        //2.2.1.1
        finishCompletion();
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如上代码，使用CAS尝试设置任务状态state为COMPLETING，如果CAS成功则把异常信息设置到outcome变量，并且设置任务状态为EXCEPTIONAL终止状态，然后调用finishCompletion，其代码：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Java&quot;&gt;private void finishCompletion() {
    //a遍历链表节点
    for (WaitNode q; (q = waiters) != null;) {
        //a.1 CAS设置当前waiters节点为null
        if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
            //a.1.1
            for (;;) {
                //唤醒当前q节点对应的线程
                Thread t = q.thread;
                if (t != null) {
                    q.thread = null;
                    LockSupport.unpark(t);
                }
                //获取q的下一个节点
                WaitNode next = q.next;
                if (next == null)
                    break;
                q.next = null; //help gc
                q = next;
            }
            break;
        }
    }
    //b。所有阻塞的线程都被唤醒后，调用done方法
    done();

    callable = null;        // callable设置为null
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如上代码比较简单，就是当任务已经处于终态后，激活waiters链表中所有由于等待获取结果而被阻塞的线程，并从waiters链表中移除他们，等所有由于等待该任务结果的线程被唤醒后，调用done（）方法，done默认实现为空实现。&lt;/p&gt;

&lt;p&gt;上面我们讲了当任务执行过程中出现异常后如何处理的，下面我们看代码3，当任务是正常执行完毕后set(result)的实现：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Java&quot;&gt;protected void set(V v) {
    //3.1
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        outcome = v;
        UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
        finishCompletion();
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如上代码3.1，使用CAS尝试设置任务状态state为COMPLETING，如果CAS成功则把任务结果设置到outcome变量，并且设置任务状态为NORMAL终止状态，然后调用finishCompletion唤醒所有因为等待结果而被阻塞的线程。&lt;/p&gt;

&lt;h2 id=&quot;34-futuretask的get方法&quot;&gt;3.4 FutureTask的get()方法&lt;/h2&gt;
&lt;p&gt;等待异步计算任务完成，并返回结果；如果当前任务计算还没完成则会阻塞调用线程直到任务完成；如果在等待结果的过程中有其他线程取消了该任务，则调用线程会抛出CancellationException异常；如果在等待结果的过程中有线程中断了该线程，则抛出InterruptedException异常；如果任务计算过程中抛出了异常，则会抛出ExecutionException异常。&lt;/p&gt;

&lt;p&gt;其代码如下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Java&quot;&gt;public V get() throws InterruptedException, ExecutionException {
    //1.获取状态，如有需要则等待
    int s = state;
    if (s &amp;lt;= COMPLETING)
        //等待任务终止
        s = awaitDone(false, 0L);
    //2.返回结果
    return report(s);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
  &lt;li&gt;如上代码1获取任务的状态，如果任务状态的值小于等于COMPLETING则说明任务还没有被完成，所以调用awaitDone挂起调用线程。&lt;/li&gt;
  &lt;li&gt;代码2如果任务已经被完成，则返回结果。下面我们看awaitDone方法实现：&lt;/li&gt;
&lt;/ul&gt;

&lt;pre&gt;&lt;code class=&quot;language-Java&quot;&gt;private int awaitDone(boolean timed, long nanos)
    throws InterruptedException {
    //1.1超时时间
    final long deadline = timed ? System.nanoTime() + nanos : 0L;
    WaitNode q = null;
    boolean queued = false;
    //1.2 循环，等待任务完成
    for (;;) {
        //1.2.1任务被中断，则移除等待线程节点，抛出异常
        if (Thread.interrupted()) {
            removeWaiter(q);
            throw new InterruptedException();
        }
        //1.2.2 任务状态&amp;gt;COMPLETING说明任务已经终止
        int s = state;
        if (s &amp;gt; COMPLETING) {
            if (q != null)
                q.thread = null;
            return s;   
        }
        //1.2.3任务状态为COMPLETING
        else if (s == COMPLETING) // cannot time out yet
            Thread.yield();
        //1.2.4为当前线程创建节点
        else if (q == null)
            q = new WaitNode();
        //1.2.5 添加当前线程节点到链表
        else if (!queued)
            queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
                                               q.next = waiters, q);
        //1.2.6 设置了超时时间
        else if (timed) {
            nanos = deadline - System.nanoTime();
            if (nanos &amp;lt;= 0L) {
                removeWaiter(q);
                return state;
            }
            LockSupport.parkNanos(this, nanos);
        }
        //1.2.7没有设置超时时间
        else
            LockSupport.park(this);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
  &lt;li&gt;如上代码1.1获取设置的超时时间，如果传递的timed为false说明没有设置超时时间，则deadline设置为0&lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;代码1.2无限循环等待任务完成，其中代码1.2.1如果发现当前线程被中断则从等待链表中移除当前线程对应的节点（如果队列里面有该节点的话），然后抛出
InterruptedException异常；代码1.2.2如果发现当前任务状态大于COMPLETING说明任务已经进入了终态（可能是NORMAL、EXCEPTIONAL、CANCELLED、INTERRUPTED中的一种），则把执行任务的线程的引用设置为null，并且返回结果；&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;代码1.2.3如果当前任务状态为COMPLETING说明任务已经接近完成了，就差设置结果到outCome了，则这时候让当前线程放弃CPU执行，意在让任务执行线程获取到CPU然后让任务状态从COMPLETING转换到终态NORMAL，这样可以避免当前调用get系列的方法的线程被挂起，然后在被唤醒的开销；&lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;代码1.2.4如果当前q为null，则创建一个与当前线程相关的节点，代码1.2.5如果当前线程对应节点还没放入到waiters管理的等待列表，则使用CAS操作放入；&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;代码1.2.6如果设置了超时时间则使用LockSupport.parkNanos(this, nanos)让当前线程挂起deadline时间，否则会调用 LockSupport.park(this);让线程一直挂起直到其他线程调用了unpark方法,并且以当前线程为参数（比如finishCompletion（）方法）。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;另外带超时参数的V get(long timeout, TimeUnit unit)方法与get()方法类似，只是添加了超时时间，这里不再累述。&lt;/p&gt;

&lt;h2 id=&quot;35-futuretask的cancelboolean-mayinterruptifrunning方法&quot;&gt;3.5 FutureTask的cancel(boolean mayInterruptIfRunning)方法&lt;/h2&gt;
&lt;p&gt;尝试取消任务的执行，如果当前任务已经完成或者任务已经被取消了，则尝试取消任务会失败；如果任务还没被执行时候，调用了取消任务，则任务将永远不会被执行；如果任务已经开始运行了，这时候取消任务，则参数mayInterruptIfRunning将决定是否要将正在执行任务的线程中断，如果为true则标识要中断，否则标识不中断；&lt;/p&gt;

&lt;p&gt;当调用取消任务后，在调用isDone()方法，后者会返回true，随后调用isCancelled()方法也会一直返回true；该方法会返回false，如果任务不能被取消，比如任务已经完成了，任务已经被取消了。&lt;/p&gt;

&lt;p&gt;cancel方法的代码如下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Java&quot;&gt;public boolean cancel(boolean mayInterruptIfRunning) {
    //1.如果任务状态为New则使用CAS设置任务状态为INTERRUPTING或者CANCELLED
    if (!(state == NEW &amp;amp;&amp;amp;
          UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
              mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
        return false;
    //2.如果设置了中断正常执行任务线程，则中断
    try {    
        if (mayInterruptIfRunning) {
            try {
                Thread t = runner;
                if (t != null)
                    t.interrupt();
            } finally { // final state
                UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
            }
        }
    } finally {
        //3.移除并激活所有因为等待结果而被阻塞的线程
        finishCompletion();
    }
    return true;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
  &lt;li&gt;如上代码1，如果任务状态为New则使用CAS设置任务状态为INTERRUPTING或者CANCELLED，如果mayInterruptIfRunning设置为了true则说明要中断正在执行任务的线程，则使用CAS设置任务状态为INTERRUPTING，否则设置为CANCELLED；如果CAS失败则直接返回false。&lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;如果CAS成功，则说明当前任务状态已经为INTERRUPTING或者CANCELLED，如果mayInterruptIfRunning为true则中断执行任务的线程，然后设置任务状态为INTERRUPTED。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;最后代码3移除并激活所有因为等待结果而被阻塞的线程。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;另外我们可以使用isCancelled()方法判断一个任务是否被取消了，使用isDone()方法判断一个任务是否处于终态了。&lt;/p&gt;

&lt;p&gt;总结：当我们创建一个FutureTask时候，其任务状态初始化为NEW，当我们把任务提交到线程或者线程池后，会有一个线程来执行该FutureTask任务，具体是调用其run方法来执行任务。在任务执行过程中，我们可以在其他线程调用FutureTask的get()方法来等待获取结果，如果当前任务还在执行，则调用get的线程会被阻塞然后放入FutureTask内的阻塞链表队列；多个线程可以同时调用get方法，这些线程可能都会被阻塞到了阻塞链表队列。当任务执行完毕后会把结果或者异常信息设置到outcome变量，然后会移除和唤醒FutureTask内的阻塞链表队列里面的线程节点，然后这些由于调用FutureTask的get方法而被阻塞的线程就会被激活。&lt;/p&gt;

&lt;h2 id=&quot;36-futuretask的局限性&quot;&gt;3.6 FutureTask的局限性&lt;/h2&gt;
&lt;p&gt;FutureTask虽然提供了用来检查任务是否执行完成、等待任务执行结果、获取任务执行结果的方法，但是这些特色并不足以让我们写出简洁的并发代码。比如它并不能清楚的表达出多个FutureTask之间的关系，另外为了从Future获取结果，我们必须调用get()方法，而该方法还是会在任务执行完毕前阻塞调用线程的，这明显不是我们想要的。&lt;/p&gt;

&lt;p&gt;我们真正要想要的是：&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;可以将两个或者多个异步计算结合在一起变成一个，这包含两个或者多个异步计算是相互独立的时候或者第二个异步计算依赖第一个异步计算结果的时候。&lt;/li&gt;
  &lt;li&gt;对反应式编程的支持，也就是当任务计算完成后能进行通知，并且可以以计算结果作为一个行为动作的参数进行下一步计算，而不是仅仅提供调用线程以阻塞的方式获取计算结果。&lt;/li&gt;
  &lt;li&gt;可以通过编程的方式手动设置（代码的方式）Future的结果；FutureTask则不可以让用户通过函数来设置其计算结果，而是其任务内部来进行设置。&lt;/li&gt;
  &lt;li&gt;可以等多个Future对应的计算结果都出来后做一些事情&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;为了克服FutureTask的局限性，以及满足我们对异步编程的需要，JDK8中提供了CompletableFuture，CompletableFuture是一个可以通过编程方式显式的设置计算结果和状态以便让任务结束的Future,本书后面章节我们会具体讲解。&lt;/p&gt;

&lt;h1 id=&quot;四总结&quot;&gt;四、总结&lt;/h1&gt;
&lt;p&gt;本节内容摘自《Java异步编程实战》中的一小节。《Java异步编程实战》一书是国内首本系统讲解Java异步编程的书籍，本书涵盖了Java中常见的异步编程场景：这包含单JVM内的异步编程、以及跨主机通过网络通讯的远程过程调用的异步调用与异步处理、Web请求的异步处理、以及常见的异步编程框架原理解析和golang语言内置的异步编程能力。&lt;/p&gt;
</description>
        <pubDate>Thu, 10 Sep 2020 00:00:00 +0000</pubDate>
        <link>http://www.huipeixue.cn/java/2020/09/10/jdkfuture.html</link>
        <guid isPermaLink="true">http://www.huipeixue.cn/java/2020/09/10/jdkfuture.html</guid>
        
        <category>java</category>
        
        
        <category>java</category>
        
      </item>
    
      <item>
        <title>一个简单流式迭代DB数据的后端支持组件</title>
        <description>&lt;h1 id=&quot;一前言&quot;&gt;一、前言&lt;/h1&gt;
&lt;p&gt;日常需求中，经常会有从DB检索符合条件的数据的需求，如果数据量比较大，不可能使用默认方式一次请求把全部数据加载到内存，因为这可能导致OOM。本文就介绍一个组件，可以让我们使用起来类型流式迭代方式，批次迭代部分数据到内存。&lt;/p&gt;
&lt;h1 id=&quot;二工具介绍&quot;&gt;二、工具介绍&lt;/h1&gt;
&lt;p&gt;本组件github地址为：https://github.com/PhantomThief/cursor-iterator&lt;/p&gt;

&lt;p&gt;要使用该工具首先需要引入maven配置：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Java&quot;&gt;        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;com.github.phantomthief&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;cursor-iterator&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;1.0.12&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后可以下面方式使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-Java&quot;&gt;   // 0.A fake DAO for test
    public static List&amp;lt;PersonDo&amp;gt; getUsersAscById(Long startId, int limit, NamedParameterJdbcTemplate jdbcTemplate) {
        if (startId == null) {
            startId = 0L;
        }

        String sql = &quot;select * from table where oid&amp;lt;= &quot; + startId + &quot; order by oid desc limit &quot; + limit;
        System.out.println(sql);
        List&amp;lt;PersonDo&amp;gt; list = jdbcTemplate.query(sql, new BeanPropertyRowMapper&amp;lt;PersonDo&amp;gt;(PersonDo.class));
        return list;
    }

    public static void main(String[] arg) throws SQLException {
        //1.创建数据源
        HikariDataSource dataSource = getDataSource();
        NamedParameterJdbcTemplate jdbcTemplate = new NamedParameterJdbcTemplate(dataSource);

        //2.游标访问
        CursorIterator&amp;lt;Long, PersonDo&amp;gt; users = CursorIterator.newBuilder()
                .start(Long.MAX_VALUE) //2.1游标起始值
                .cursorExtractor(PersonDo::getOid)//2.2从DO中获取表主键id的方法
                .bufferSize(1024)//2.3每次从DB拉取多少条记录
                .build((cursor, limit) -&amp;gt; getUsersAscById(cursor, limit, jdbcTemplate));//2.4拉取数据函数

        //3.转换流为反应式流
        Flux.fromStream(users.stream())
                .buffer(256)//3.1缓冲
                .subscribe(personDos -&amp;gt; {//3.2订阅
                    System.out.println(ObjectMapperUtils.toJSON(personDos));
                }, throwable -&amp;gt; {//3.3异常处理
                    System.out.println(ObjectMapperUtils.toJSON(throwable));
                });

        //4,关闭数据源
        dataSource.close();
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
  &lt;li&gt;如上代码1我们创建了一个数据源和jdbc模板，这个大家可以各自创建。&lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;如上代码2使用CursorIterator创建了一个游标迭代器，其中2.1说明游标起始值为long型最大值，代码2.2设置数据库Do对象的主键key获取方法，代码2.3说明每次迭代从DB里面获取多少数据，代码2.4则设置每次从db迭代数据的DAO函数。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;代码3则我们把游标迭代器转换为flux流对象，以便我们可以对db数据进行缓冲处理，这里我们聚合256条数据为一个元素，代码3.2我们订阅转换后的流，并打印。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;如上代码，我们可以保证每次最多有1024条记录到内存中，然后把这1024条记录聚合为每个含有256个记录的流，流中元素是个list。&lt;/p&gt;

&lt;p&gt;如上代码其实我们起到了反压的效果，当我们的subscribe订阅函数处理速度快时候，就会加快从db迭代数据出来的速度，当处理速度慢时候，就会反向控制从db迭代出数据的速度。&lt;/p&gt;

&lt;h1 id=&quot;三总结&quot;&gt;三、总结&lt;/h1&gt;
&lt;p&gt;本文我们介绍了一种简单流式迭代DB数据的后端支持组件，使用该组件我们可以很方便的替代我们自己手动写代码循环迭代数据的场景，另外我们使用&lt;a href=&quot;https://zhailuxu.github.io/java/2020/08/02/JDK8stream.html&quot;&gt;Flux将jdk8流转换为了反应式流&lt;/a&gt;，从而可以让我们享受反应式编程带来的丰富的操作符。关于CursorIterator的内部原理，下期会进行讲解。&lt;/p&gt;
</description>
        <pubDate>Wed, 09 Sep 2020 00:00:00 +0000</pubDate>
        <link>http://www.huipeixue.cn/java/2020/09/09/cursoriterator.html</link>
        <guid isPermaLink="true">http://www.huipeixue.cn/java/2020/09/09/cursoriterator.html</guid>
        
        <category>java</category>
        
        
        <category>java</category>
        
      </item>
    
      <item>
        <title>关于如何阅读源码的一点体会</title>
        <description>&lt;h1 id=&quot;一前言&quot;&gt;一、前言&lt;/h1&gt;
&lt;p&gt;日常交流过程中，经常会被问到为何要阅读源码和如何阅读源码的问题，本文则谈下我的理解。&lt;/p&gt;
&lt;h1 id=&quot;二为何要阅读源码&quot;&gt;二、为何要阅读源码&lt;/h1&gt;
&lt;p&gt;首先来聊下为何要阅读源码。我们在做项目时候一般会遇到下面的问题：
问题一：不知道如何去设计，比如刚入职场时，来一个需求需做概要设计，不知如何下手，不得不去看当前系统类似需求是如何设计的，然后仿照去设计。&lt;/p&gt;

&lt;p&gt;问题二：设计的时候，考虑问题不周全，相比职场新手，这类人对一个需求依靠自己的经验已经能够拿出一个概要设计，但是设计中经常会遗漏一些异常细节。&lt;/p&gt;

&lt;p&gt;对于这些问题，说到底主要是因为经验不够，而经验主要从项目实践中积累，所以招聘单位一般都会限定工作时间大于 3 年，因为这些人的项目经验相对较丰富，项目中遇到的场景相对较多。&lt;/p&gt;

&lt;p&gt;工作经验的积累来自于年限与实践，然而看源码可以扩展我们的思路，这是变相增加我们经验的不错方法。虽然不能短时间内通过时间积累经验，但是可以通过学习开源框架、开源项目来获取。&lt;/p&gt;

&lt;p&gt;另外进职场后一般都要先熟悉现有系统，如果有文档还好，没文档的话就得自己去翻代码研究。如果大家之前对阅读源码有经验，那么在研究新系统的代码逻辑时就不会那么费劲了。&lt;/p&gt;

&lt;p&gt;还有一点就是当你使用框架或者工具做开发时，如果你对它的实现有所了解，就能最大化的减少出故障的可能。&lt;/p&gt;

&lt;p&gt;比如并发队列 ArrayBlockingQueue 里面元素入队有个 offer 和 put 方法，虽然某个时间点你知道使用 offer 方法时，当队列满了就会丢弃要入队的元素，之后 offer 方法会返回 false，而不会阻塞当前线程；使用 put 方法时当队列满了，则会挂起当前线程，直到队列有空闲元素，入队成功后才返回。&lt;/p&gt;

&lt;p&gt;但是人是善忘的，当你一段时间不使用，就会忘记他们的区别，这时当你使用时，需进入 offer 和 put 的源码看他们的实现。进入 offer 方法一看，哦，原来队列满后直接返回了 false；进入 put 方法一看，哦，原来队列满后，直接使用条件变量的 await 方法挂起了当前线程；知道了他们的区别，你就可以根据自己的需求来选择了。&lt;/p&gt;

&lt;p&gt;看源码最大的好处是可以开阔思维，提升架构设计能力。有些东西仅靠书本和自己思考是很难学到的，必须通过看源码，看别人如何设计，然后思考为何这样设计才能获取。&lt;/p&gt;

&lt;p&gt;能力的提高不在于你写了多少代码，做了多少项目，而在于给你一个业务场景时，你是否能拿出几种靠谱的解决方案，并且说出各自的优缺点。而如何才能拿出来，一来靠经验，二来靠归纳总结，而看源码可以快速增加你的经验。&lt;/p&gt;
&lt;h1 id=&quot;三如何阅读源码&quot;&gt;三、如何阅读源码&lt;/h1&gt;
&lt;p&gt;那么如何阅读源码呢？在你看某一个框架的源码前，先去 Google 查找这个开源框架的官方介绍，通过资料看看该框架有几个模块，各个模块是做什么的，之间是什么联系，每个模块都有哪些核心类，在阅读源码时可以着重看这些类。&lt;/p&gt;

&lt;p&gt;然后对哪个模块感兴趣就去写个小 demo，先了解下这个模块的具体作用，然后在 debug 进入看具体实现，在 debug 过程中第一遍是走马观花，简略的看下调用逻辑，都用了哪些类；&lt;/p&gt;

&lt;p&gt;第二遍需有重点的 debug，看看这些类担任了架构图里的哪些功能，使用了哪些设计模式？如果第二遍有感觉了，便大致知道了整体代码的功能实现，但是对整体代码结构还不是很清晰，毕竟代码里面多个类来回调用，很容易迷失当前断点的来处，那么你可以进行第三遍 debug，这时候你最好把主要类的调用时序图、以及类图结构画出来，等画好后，在对着时序图分析调用流程，就可以清楚的知道类之间的调用关系，通过类图就可以知道类的功能、以及相互之间的依赖关系了。&lt;/p&gt;

&lt;p&gt;另外开源框架里面每个功能类或者方法一般都有注释，这些注释是一手的好资料，比如 JUC 包里的一些并发组件的注释，就已经提供了它们的设计原理和使用场景。&lt;/p&gt;

&lt;p&gt;另外阅读源码时，最好画出时序图和类图，因为人总是善忘的，如果隔一段时间后你在去看之前看过的源码，虽然有些印象，但当你想去看某个模块的逻辑时，又需根据 demo 再从头 debug 了，而如果有了这俩图，就可以从时序图或者类图里面直接找，并且看一眼时序图就知道整个模块的脉络了。&lt;/p&gt;

&lt;p&gt;还有查框架使用说明最好去官网查，虽然是英文，但是看久了就好了，毕竟还有 Google 翻译呐！
当然研究代码时不一定非要 debug 三遍，其实这里说的是三种掌握程度，如果你 debug 一遍就能掌握，那自然更好啦。&lt;/p&gt;
&lt;h1 id=&quot;四总结&quot;&gt;四、总结&lt;/h1&gt;
&lt;p&gt;本文谈了自己关于阅读源码的一些心得体会，欢迎大家留言来共同探讨阅读源码的更佳方案，让大家阅读源码不在是难事。&lt;/p&gt;

</description>
        <pubDate>Thu, 03 Sep 2020 00:00:00 +0000</pubDate>
        <link>http://www.huipeixue.cn/java/2020/09/03/readcode.html</link>
        <guid isPermaLink="true">http://www.huipeixue.cn/java/2020/09/03/readcode.html</guid>
        
        <category>java</category>
        
        
        <category>java</category>
        
      </item>
    
      <item>
        <title>关于面试的一点事情</title>
        <description>&lt;h1 id=&quot;前言&quot;&gt;前言&lt;/h1&gt;
&lt;p&gt;一场正规的技术面试一般包含：自我介绍、基础技术点考察、项目考察、算法考察、面试者QA环节。&lt;/p&gt;

&lt;h1 id=&quot;自我介绍&quot;&gt;自我介绍&lt;/h1&gt;
&lt;p&gt;面试一开始一般是自我介绍环节，面试者可以讲述自己的工作经历以及所做过的比较有亮点的项目，介绍项目时候最好能和面试官互动，让他对你的项目感兴趣。&lt;/p&gt;

&lt;p&gt;另外自我介绍也可以讲述自己最近在研读那些书或者学习那些新技术，一般招聘单位都喜欢比较好学、喜欢探究新技术的人，因为这些人，不墨守陈规，不固步自封，可以给公司带来新的血液。&lt;/p&gt;

&lt;h1 id=&quot;基础知识&quot;&gt;基础知识&lt;/h1&gt;

&lt;p&gt;基础知识考察时候面试官大多会问简历上你写的擅长的技术领域，所以简历上写的技术领域一定要真的懂，不懂的一定不要写。当然有些公司会有自己的题库或者面试官有自己想要考察的点，面试时候会问面试者简历外的知识点，这个就需要面试者在面试对应公司前，去google对应公司历年面试题目，这个基本都可以搜到一些，搜到的一定要弄懂。&lt;/p&gt;

&lt;p&gt;基础知识考察时，如果遇到不会的，也不要紧张，这个很正常。因为没有谁能无所不知，其实某方面的技术专家，在他擅长的领域也有他不知道的。如果真的不知道，可以思考几分钟，告诉面试官这块确实不了解。如果你对考察点相关的其他知识比较熟悉，则可以告诉面试官**这块我倒是比较了解，如果面试官感兴趣则更好，如果不，则也无妨。&lt;/p&gt;

&lt;h1 id=&quot;项目&quot;&gt;项目&lt;/h1&gt;
&lt;p&gt;讲解项目时候一定要先把项目背景讲解清楚，也就是这个项目是什么业务背景，用来解决什么业务问题的，然后在从整上讲解技术方案，最后可以着重讲解技术实现中遇到的问题或者比较有亮点的地方。&lt;/p&gt;

&lt;h1 id=&quot;算法&quot;&gt;算法&lt;/h1&gt;

&lt;p&gt;至于算法面，最常见的是考察链表，二叉树，和给你一个业务场景，让你选择合适算法来解。现在面试算法一般是在牛客网进行远程视频答题，需要注意该网站在答题的时候不要切换tab去网上搜答案，因为网站会监控到页面失焦。&lt;/p&gt;

&lt;p&gt;面试算法这个有一定概率运气成分，因为算法题很多，如果复习到了，那么很容易就可以过关，复习不到则很难临时想到解法。关于算法这个只能考面试者平时的积累，多刷leetcode。&lt;/p&gt;

&lt;h1 id=&quot;qa&quot;&gt;QA&lt;/h1&gt;
&lt;p&gt;面试者QA环节，一般是面试官让面试者问自己问题，这时候是面试者深入了解该公司的时候（面试者可能之前通过网络或者其他渠道了解过该公司，但是这些消息可能不准确），你可以问你面试的部门具体是做什么的，什么规模，需要什么技术栈等等。&lt;/p&gt;

&lt;h1 id=&quot;总结&quot;&gt;总结&lt;/h1&gt;

&lt;p&gt;面试是一件很严肃的事情，不进行充分准备就去面试无疑相当于上战场不拿武器，其实这是对自己不负责的表现，也是对面试官的不尊重（浪费人家的时间）。&lt;/p&gt;

&lt;p&gt;最后祝每个求职者，能都拿到自己心仪的offer！&lt;/p&gt;

</description>
        <pubDate>Thu, 03 Sep 2020 00:00:00 +0000</pubDate>
        <link>http://www.huipeixue.cn/java/2020/09/03/mianshi.html</link>
        <guid isPermaLink="true">http://www.huipeixue.cn/java/2020/09/03/mianshi.html</guid>
        
        <category>java</category>
        
        
        <category>java</category>
        
      </item>
    
      <item>
        <title>MySQL can&apos;t specify target table for update in FROM clause</title>
        <description>&lt;p&gt;在开发过程经常遇到更新mysql表的情景，例如根据id更新用户的状态。比如我们需要更新id小于100的用户的状态，假设你的sql如下:&lt;/p&gt;
&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;update&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Users&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;valid&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Users&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;100&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;那么当你信心满满的去执行时候，却会发现执行结果如下：
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#1093 - You can&apos;t specify target table &apos;Users&apos; for update in FROM clause&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;为何会出现这样情况那？其实是因为MySQL中不允许更新在内部select语句中使用的表，如下：&lt;/p&gt;
&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;update&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;myTable&lt;/span&gt;
   &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;  &lt;span class=&quot;n&quot;&gt;myTable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;A&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 
&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt;  &lt;span class=&quot;n&quot;&gt;B&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;myTable&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;许多其他数据库引擎都支持此功能，但MySQL没有，因此我们需要解决该限制。&lt;/p&gt;

&lt;p&gt;那么在mysql中我们如何解决这个问题那?其实可以使用子查询生成临时来解决，文初的sql可以修改为下面：&lt;/p&gt;
&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;update&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Users&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;valid&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;  &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Users&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;100&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;如上sql该查询中的别名t很重要，它告诉MySQL从该选择查询中创建一个临时表t，并把它用作更新语句的条件。&lt;/p&gt;

</description>
        <pubDate>Tue, 01 Sep 2020 00:00:00 +0000</pubDate>
        <link>http://www.huipeixue.cn/java/2020/09/01/mysqlupdate.html</link>
        <guid isPermaLink="true">http://www.huipeixue.cn/java/2020/09/01/mysqlupdate.html</guid>
        
        <category>java</category>
        
        
        <category>java</category>
        
      </item>
    
      <item>
        <title>我的Java三剑客配套源码</title>
        <description>&lt;h1 id=&quot;一前言&quot;&gt;一、前言&lt;/h1&gt;
&lt;p&gt;随书源码目前都托管到github上，无论你是否购买了书籍，都可以免费clone到本地，如果感觉好，别忘了star哦。&lt;/p&gt;

&lt;h1 id=&quot;二介绍&quot;&gt;二、介绍&lt;/h1&gt;
&lt;h2 id=&quot;21-java并发编程之美&quot;&gt;2.1 Java并发编程之美&lt;/h2&gt;
&lt;p&gt;我的一本书是2018年11月出版的&lt;a href=&quot;https://zhailuxu.github.io/java/2020/08/02/java-concurrency.html&quot;&gt;《Java并发编程之美》&lt;/a&gt;
，该书深入浅出的介绍了Java并发编程基础，然后图文结合的方式剖析了JUC包源码原理。&lt;/p&gt;

&lt;p&gt;该书出版时，并没有发布配套的源码，因为我认为里面都是比较简单的代码示例，希望大家能手动练习，但是最近2年陆续有童鞋询问我是否有源码。本着配套完整的原则，最近我有吧书中示例代码整理下，并托管到了github，地址为：
&lt;a href=&quot;https://github.com/zhailuxu/concurrent-program-demo/&quot;&gt;https://github.com/zhailuxu/concurrent-program-demo/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;另外也有朋友问我是否会出该书的第二版，答案是肯定的，Java并发博大精深，随着最近两年对其更深刻的理解，以及知识的完善，目前我认为不久将来，我应该把异步Future、ForkJoinPool、JDK8流、Java 各种IO线程模型等纳入到本书。&lt;/p&gt;

&lt;h2 id=&quot;22-java异步编程实战&quot;&gt;2.2 Java异步编程实战&lt;/h2&gt;
&lt;p&gt;我出版的第二本书是&lt;a href=&quot;https://zhailuxu.github.io/java/2020/08/02/java-async.html&quot;&gt;《Java异步编程实战》&lt;/a&gt; ，其是2019年12年出版。
异步编程是可以让程序并行运行的一种手段，其可以让程序中的一个工作单元与主应用程序线程分开独立运行，使用它有许多好处，例如可以提高应用程序的性能和响应能力。&lt;/p&gt;

&lt;p&gt;虽然Java中不同技术域提供了相应的异步编程技术，但是对异步编程技术的描述散落到了不同技术域的技术文档中，并没有一个统一的地方对这些技术进行梳理归纳。另外这些技术之间是什么关系，各自的出现都是为了解决什么问题，我们也很难找到资料来解释。&lt;/p&gt;

&lt;p&gt;本书的出现则是为了打破这种局面，本书旨在把Java中相关的异步编程技术进行归纳分类总结，然后呈现给大家，让大家可以有一个统一的地方来查看与探究。&lt;/p&gt;

&lt;p&gt;本书的代码也托管到了github,地址为：
&lt;a href=&quot;https://github.com/zhailuxu/async-program-demo&quot;&gt;https://github.com/zhailuxu/async-program-demo&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;33-深度剖析apache-dubbo技术内幕&quot;&gt;3.3 深度剖析Apache Dubbo技术内幕&lt;/h2&gt;
&lt;p&gt;我出版的第三本书是&lt;a href=&quot;https://zhailuxu.github.io/java/2020/08/02/dubbo.html&quot;&gt;《深度剖析Apache Dubbo技术内幕》&lt;/a&gt;，其也是2019年12年出版。&lt;/p&gt;

&lt;p&gt;在单体应用时，不同业务模块部署在同一个JVM进程内，这时候通过本地调用就可以解决不同业务模块之间的相互引用；但多体应用时，不同业务模块大多部署到不同机器上，这时候一 个高效、稳定的RPC框架就显得特别重要了。Apache Dubbo作为阿里巴巴开源的分布式RPC框架，其已进入了Apache 孵化器项目，相信在开源社区的不断贡献下，其会成为RPC框架中的佼佼者。&lt;/p&gt;

&lt;p&gt;本书通过原理与实践相结合、由浅入深、通俗易懂的方式讲解了Dubbo框架如何使用与内核原理实现，详细读者阅读完本书后，对一个RPC框架应该具有那些模块，以及各个模块之间如何组合起来的，有深入的理解；本书适合Java中高级研发工程师，以及对RPC框架技术感兴趣，希望探究RPC框架内部实现原理的人员阅读。&lt;/p&gt;

&lt;p&gt;本书的代码也托管到了github,地址为：
&lt;a href=&quot;https://github.com/zhailuxu/Dubbo-Demo&quot;&gt;https://github.com/zhailuxu/Dubbo-Demo&lt;/a&gt;&lt;/p&gt;
</description>
        <pubDate>Sun, 30 Aug 2020 00:00:00 +0000</pubDate>
        <link>http://www.huipeixue.cn/java/2020/08/30/bookcode.html</link>
        <guid isPermaLink="true">http://www.huipeixue.cn/java/2020/08/30/bookcode.html</guid>
        
        <category>java</category>
        
        
        <category>java</category>
        
      </item>
    
      <item>
        <title>程序员必备的办公神器</title>
        <description>&lt;h1 id=&quot;一前言&quot;&gt;一、前言&lt;/h1&gt;
&lt;p&gt;在办公室工作的人员，最大的特征是久坐不动，并且如果使用笔记本办公，则很容易长期勾着头看屏幕。时间长了，则很容易导致腰疼，颈椎，肩椎变形。本文我们来推荐一些办公必备神器，来帮助大家避免这些问题。&lt;/p&gt;
&lt;h1 id=&quot;二神器介绍&quot;&gt;二、神器介绍&lt;/h1&gt;
&lt;h2 id=&quot;21-笔记本支架&quot;&gt;2.1 笔记本支架&lt;/h2&gt;
&lt;p&gt;在没有电脑支架看电脑时候，我们一般会勾着头去看屏幕，时间长了，颈椎就会前倾。颈椎前倾第一会造成头部对颈椎的压力增加，二来即使身材再好，颈椎一旦前倾，个人魅力就会大大打折扣，特别是女生来说。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/2020/worktool/1.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;其实导致我们勾着头看屏幕，原因是因为屏幕相对视线太靠下，所以如果我们可以把屏幕调高就可以解决，本文我们推荐两款电脑支架神器，第一款是坐着时候使用，一个是站立时候使用：
&lt;img src=&quot;/assets/images/2020/worktool/2.png&quot; alt=&quot;image.png&quot; /&gt;
&lt;img src=&quot;/assets/images/2020/worktool/3.png&quot; alt=&quot;image.png&quot; /&gt;
&lt;img src=&quot;/assets/images/2020/worktool/4.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;扫码购买：
&lt;img src=&quot;/assets/images/2020/worktool/5.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;刚介绍的第一款是坐立时候使用，并且非常便于放到背包里面携带。但是久坐不利于腰椎健康，所以下面推荐第二款站立支架：
&lt;img src=&quot;/assets/images/2020/worktool/6.png&quot; alt=&quot;image.png&quot; /&gt;
&lt;img src=&quot;/assets/images/2020/worktool/7.png&quot; alt=&quot;image.png&quot; /&gt;
&lt;img src=&quot;/assets/images/2020/worktool/8.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;扫码购买：
&lt;img src=&quot;/assets/images/2020/worktool/9.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;22-座椅头枕&quot;&gt;2.2 座椅头枕&lt;/h2&gt;
&lt;p&gt;一般办公室座椅都提供舒适的椅子，但是很少提供头枕配件，有时候想靠着椅背休息时，头靠在椅子上边缘特别不舒服，其实椅子上边缘可以加个头枕的：
&lt;img src=&quot;/assets/images/2020/worktool/10.png&quot; alt=&quot;image.png&quot; /&gt;
&lt;img src=&quot;/assets/images/2020/worktool/11.png&quot; alt=&quot;image.png&quot; /&gt;
&lt;img src=&quot;/assets/images/2020/worktool/12.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://upload-images.jianshu.io/upload_images/5879294-f9f095b309553df8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://upload-images.jianshu.io/upload_images/5879294-6413320a4818eda5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://upload-images.jianshu.io/upload_images/5879294-f4d8f006bc624a15.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;扫码购买：
&lt;img src=&quot;/assets/images/2020/worktool/13.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;23-午睡神器&quot;&gt;2.3 午睡神器&lt;/h2&gt;
&lt;p&gt;对应有午睡习惯的人来说，午睡睡不好，下午时间就会比较昏昏沉沉的，工作效率极差，而一般坐着或者趴着午睡特别不舒服，趴着容易造成胳膊酸痛和腰椎酸痛。这里我们推荐午睡神器，折叠床：
&lt;img src=&quot;/assets/images/2020/worktool/14.png&quot; alt=&quot;image.png&quot; /&gt;
扫码购买：
&lt;img src=&quot;/assets/images/2020/worktool/15.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;然后如果环境有噪音，则可以配好降噪耳塞和睡眠眼罩：
&lt;img src=&quot;/assets/images/2020/worktool/16.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;扫码购买：
&lt;img src=&quot;/assets/images/2020/worktool/17.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h1 id=&quot;三总结&quot;&gt;三、总结&lt;/h1&gt;
&lt;p&gt;工作很重要，但是照顾好我们自己的身体更重要，合理利用办公神器，无论对工作效率的提高，还是对自己的身体的负责，都是一种价值投资。&lt;/p&gt;
</description>
        <pubDate>Mon, 03 Aug 2020 00:00:00 +0000</pubDate>
        <link>http://www.huipeixue.cn/java/2020/08/03/workneedtool.html</link>
        <guid isPermaLink="true">http://www.huipeixue.cn/java/2020/08/03/workneedtool.html</guid>
        
        <category>java</category>
        
        
        <category>java</category>
        
      </item>
    
  </channel>
</rss>
