引言

  为了更加形象的描述并发的基础知识,因此本文LZ采用了园子里一度大火的标题形式——“没听说过XXXX,就不要说你XXXX了”。希望能够给猿友们一个醒目的警醒,借此来普及并发的基础知识,也讨论一下这些内容。

  对于大多数人而言,并发亦近矣,亦远矣。

  如果你问一个程序猿,“你知道并发吗?”。

  估计不少人会说,“恩,知道个大概吧!”。

  如果此时你再继续追问下去,可能得到的仍然会是一些千篇一律的答案。比如,“并发应该就是多个线程一起运行”、“并发的时候应该加锁,加synchronized关键字”,“并发的时候采用时间片轮询的方式”等等诸如此类的答案。

  其实大多数人都是知道并发的,但却大部分是一知半解,这也是为什么LZ说,并发亦近亦远,近是因为几乎所有程序猿都听说过,远是因为大部分人还都只停留在初级阶段,包括现在刚入门的LZ本人。如果写一个简单的并发程序,大部分猿友们估计都能胜任,不过若是稍微复杂一点的,可能就会出现很多问题,或者自以为没有问题。

  本文的主要目的,一个是普及一点并发的基础知识,一个是巩固一下LZ自己对并发的理解。如果哪位猿友对此也有兴趣的话,不妨试着看下去,看能否有所收获。

线程安全

  线程安全这个词汇实在是折磨人,它给人一种错觉,让你仿佛很轻松的理解了它,但实则是一个典型的笑面虎,背后冷不丁就给你一刀,让你血溅职场。

  我们先来看下这个词语组成的词汇都有哪些,首先后面可以加一“性”字,此为线程安全性。另外,如果后面加“类”或者“程序”,就组成了线程安全类或者是线程安全程序。很显然,线程安全性是类和程序的属性,就像一个类或者程序的其它属性一样,例如扩展性、维护性等等。

  到现在重点就出来了,到底什么是线程安全性?从字面上看,线程安全性就是一个类或者程序在多线程的环境中运行是安全的。可是这显然是废话,重点还是落在了安全性上面。怎么才能称作是安全的?

  LZ这里先贴出一个比较官方的解释,接下来再和各位猿友侃侃大山。安全性是指,某个类的行为与其规范完全一致。那么我们现在就可以将整句话连起来了,也就是说,线程安全性就是指,一个类或者程序在多线程的环境下,其行为与规范完全一致的特性。

  有的猿友可能会说,“我们开发从来都没有规范的,OK?既然如此,何来与规范一致一说?”。是的,只是如果哪位猿友心里冒出这么一句话的话,说明你对这里的“规范”两字理解错误了,这里的规范可不是指的编码规范。LZ举个简单的例子来说明,这个规范的意思是什么。

public class Region {

    private int left;

    private int right;

    public Region() {
super();
} public Region(int left, int right) {
super();
if (left <= right) {
this.left = left;
this.right = right;
}else {
this.left = left;
this.right = right;
}
} public void setLeft(int left) {
if (left > right) {
this.left = right;
}else {
this.left = left;
}
} public void setRight(int right) {
if (right < left) {
this.right = left;
}else {
this.right = right;
}
} public boolean in(int value){
return value >= left && value <= right;
} public String toString(){
return "[" + left + "," + right + "]";
} }

  看一下上面这个类,它表示一个整数区间,对于一个区间来讲,我们自然而然的有一些规则,比如区间左边的值必须小于或者等于右边的值。在上面的类当中,我们也在很多地方限制着客户端的输入,试图保持这种规则(但是在多线程环境下,我们这种约束将显得非常薄弱)。

  我们说这种规则就是上面提到的规范,也就是说对于Region类来说,始终保持它是一个有效的区间,就是它的规范。因此对于Region类来说,它的线程安全性就是指它可以在多线程的环境下保持它是一个有效的区间(left小于等于right)。对于Region是一个有效的区间这件事来说,其实就相当于在说in方法不能永久返回false。如果我们更加抽象点来说,就是说方法的行为应该与预期的一致。

  由此我们可以看出,一个类或程序的规范,就是指它能够始终保持一定的约束条件。比如一个应用类的stop方法,在客户端调用后,必须能够保证应用被正确关闭等等,这些方法的使用说明其实就是一种规范。

线程安全类

  通过上面的描述,我们知道了线程安全性的定义,或者说,我们已经知道要满足线程安全性需要达到什么要求。那么对于一个类来说,它的线程安全性如果被满足,它就是一个线程安全的类。

  对于线程安全的类,我们有一些可描述的规律,接下来LZ就和各位分享一下这些规律,很多时候,它对我们非常有用。

  1、无状态的对象一定是线程安全的。

  这一条规律实在是太有用了,很多时候,我们的代码处于多线程的环境下,而我们往往苦恼于这些代码的安全性。此时,如果你的类是无状态的,那么你就可以高枕无忧的在多线程环境下使用它。

  为什么说无状态的对象一定是线程安全的?

  一个对象如果没有状态,则意味着对象不存在运行时状态的改变,因此无论是单线程还是多线程的情况下,都不会使对象处于不正确的状态。大多数时候,无状态的对象就是一堆代码的持有者而已,它每一个方法的变量都封闭在独立的线程当中,线程相互之间无法共享变量,因此它们也无法互相影响各自的行为。因此,在多线程的环境下,我们首先推荐的就是无状态对象。

  下例就是一个无状态对象,它没有任何域,自然也就没有状态。

public class NonStatusObject{

  public void handle(String param){
System.out.println(param);
} }

  2、不可变对象一定是线程安全的。

  提到不可变对象,总让人不知不觉的想到基本类型的包装类,比如Java当中的String就是典型的不可变对象。不可变对象的不可变性与无状态的对象非常相似,只是无状态对象通过不添加任何状态保持对象在运行时状态的不可变性,而不可变对象则通常通过final域来强制达到这一特性,不过要注意的是,如果final域指向的是可变对象,则该对象依然可能是可变的。

  比如一个List的包装类,如果提供了对List的操作,那么既然内部的List是final类型的,该对象依然是可变的,我们看下面的例子。

import java.util.ArrayList;
import java.util.List; public class ListWrapper<E> { private final List<E> list; public ListWrapper(){
list = new ArrayList<E>();
} public boolean contains(E e){
return list.contains(e);
} public void add(E e){
list.add(e);
} public void remove(E e){
list.remove(e);
} }

  这个类其实有时候是有用的,尽管它很简单,但是它可以弥补JDK1.5加入泛型的弊病,比如remove方法的参数是Object。但是很可惜,它唯一的域是final类型的,但却不是不可变的。因为我们提供了add和remove方法,这些方法依然可以改变这个类的状态,因为list的状态就是它的状态。倘若我们在构造函数中加入一些初始化的元素,并且去掉add和remove方法,那么尽管该类引用了可变的非线程安全的类,但它依然是不可变的,也就是说依然是线程安全的。

  3、除了以上两种对象,我们通常都需要使用加锁机制来保证对象的线程安全性。

  这一条基本上道出了大部分的情况,很多时候,我们无法将一个可能处于多线程环境的对象设计成以上两种,这时就需要我们进行合适的加锁机制来保证它的线程安全性。通常情况下,我们希望一个对象是无状态的或者不可变的,这可以大大降低程序的复杂性,请尽量这么做。

加锁机制(何时加锁)

  既然有时候我们必须使用锁机制来保证类的线程安全性,那么我们最关心的就是两件事,第一件是何时加锁,第二件是如何加锁

  关于何时加锁这个问题,我们主要关注以下几点来决定,这些内容都是并发的精髓。

  1、原子性

  原子性,我们通俗的理解就是,一个操作要么就做完,要么就没开始,不存在做了一部分的情况,那么这个操作就具有原子性。这个简单的理解其实有一个重大漏洞,那就是这个操作是针对什么层次来说的,这将直接影响我们的判断。比如下面这个被用的烂透了的例子,万年的自增。

//    i++;

  博客园的大神们不让LZ直接输入i++,因此这里加了个注释符号(这算不算一个bug,0.0)。i++这个操作,从编程语言的层次来讲,它是一个原子操作,因为它只有一句代码,如果你去调试这行代码,它一定无法执行一半或一部分。但是如果从汇编语言的层次来讲,它就不是一个原子操作,因为它有好几条指令(看过计算机原理系列的猿友应该非常清楚),既然有好几条指令,那么就意味着i++这个操作在汇编层次,可以存在做了一部分的情况。

  对于原子性的层次定义,一般应该以CPU提供的指令集为准,至少我们认为,一个指令是无法拆分的操作。从这个角度来看,我们Java当中大部分看似原子性的操作,其实都不是原子操作,比如刚才提到的自增、赋值操作等等。如果在并发环境中,一个操作无法保证其原子性,可能就需要进行加锁操作。

  1.1、竞态条件

  上面已经简单的提了一下原子性的概念,接下来,我们再来看一个和原子性密切相关的概念——竞态条件。竞态条件的含义是,操作的正确性要取决于多线程之间指令执行的顺序。

  看了上面的定义,大部分猿友估计会唏嘘不已,因为多线程之间指令执行的顺序完全是不定的。如果我们考虑一个多线程程序可能的指令执行顺序,或许会得到10种、100种甚至更多种可能,而我们的程序可能在其中几种情况下执行是正确的,也就是说,我们的程序正确的概率可能为1/10、1/100甚至1/1000000。

  惊呆了,这是中彩票的概率吧?

  我们可以这么去想,当你中了500万的彩票时,你的程序或许就能正确执行了。程序的正确性完全取决于“运气”,这就是典型的竞态条件。比如下面这个更典型的单例模式当中经常出现的方式。

public static SingletonObject getInstance(){
if (instance == null) {
instance = new SingletonObject();
}
return instance;
}

  这里就出现了竞态条件,因为instance是否为单例,取决于指令执行的顺序。举一个极端的例子,假设10个线程同时运行这个方法,如果这10个线程每一个都判断完instance是否为null之后挂起,那这10个线程在再次被唤醒时都将会去执行new的操作,我们假设每个线程的new和return操作都会一起执行完,然后才把CPU让给其它线程。最终的结果会是,这10个线程得到了10个不一样的实例。各位猿友可以执行一下下面这个简单的测试程序,它将开启100个线程同时执行getInstance方法。

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; public class SingletonObject { private static SingletonObject instance; private SingletonObject(){} public static SingletonObject getInstance(){
if (instance == null) {
instance = new SingletonObject();
}
return instance;
} public static void main(String[] args) throws InterruptedException {
int threadCounts = 100;
int testCounts = 10000;
for (int i = 0; i < testCounts; i++) {
test(threadCounts);
}
} public static void test(int threadCounts) throws InterruptedException{
ExecutorService executorService = Executors.newCachedThreadPool();
final CountDownLatch startFlag = new CountDownLatch(1);
final CountDownLatch counter = new CountDownLatch(threadCounts);
final Set<String> instanceSet = Collections.synchronizedSet(new HashSet<String>());
for (int i = 0; i < threadCounts; i++) {
executorService.execute(new Runnable() {
public void run() {
try {
startFlag.await();
} catch (InterruptedException e) {}
instanceSet.add(SingletonObject.getInstance().toString());
counter.countDown();
}
});
}
startFlag.countDown();
counter.await();
     SingletonObject.instance = null;
if (instanceSet.size() > 1) {
System.out.print("{");
for (String instance : instanceSet) {
System.out.print("[" + instance + "]");
}
System.out.println("}");
}
executorService.shutdown();
} }

  以上的测试共执行1万次,这是为了加大出错几率。基本上,你总能看到以下这样的输出。

{[SingletonObject@16930e2][SingletonObject@7259da]}

  这说明在一次测试中,生成了两个SingletonObject对象(可能会有更多,LZ运行了一小会就见到一次14个的)。可以看出,并不是这10000次测试都会出错,相对来说,出错的概率还是非常小的。这正是竞态条件的发生形式,在一定的指令执行序列下,程序就会出错,比如单例模式实际上变成了非单例的情况。

  1.2、复合操作

  顾名思义,复合操作就是非原子性的操作,两者具有互斥性,也就是说,一个操作要么属于原子操作,要么属于复合操作。上面的if块就是一个典型的复合操作,根据某一个变量的值,决定下一步的行为。通常情况下,使用同步关键字(synchronized)可以使得复合操作变成原子操作,但我们往往更推荐使用现有的类库去实现原子性。

  比如一个并发的计数器,就可以写成如下形式。

import java.util.concurrent.atomic.AtomicInteger;

public class ConcurrentCounter {

    private final AtomicInteger count = new AtomicInteger(0);

    public int getCount(){
return count.get();
} public int increment(){
return count.incrementAndGet();
} public int decrement(){
return count.decrementAndGet();
} }

  这里我们使用现有的线程安全类来实现一个并发计数器,这省去了我们很多工作,比如自增并返回、递减并返回这些复合操作(实际上AtomicInteger提供了很多常用的复合操作,并保证原子性)。这样做的好处是,不容易出错,性能可能更高(比如ConcurrentHashMap),分析起来更简单。实际上,我们包装了一个线程安全的类,使之成为了另外一个线程安全的类。

  2、可见性

  可见性这玩意实在是太奇葩了,以至于亮瞎了LZ的一双氪金人眼。为了把可见性写的更神秘一点,LZ先给出一个简单的例子。

public class Integer {

    private int value;

    public int getValue() {
return value;
} public void setValue(int value) {
this.value = value;
} }

  这个类是Java类库中Integer类的伪劣产品,各位猿友想象一下,它是一个线程安全的类吗?(JDK中的Integer是不可变对象,因此是线程安全的)

  乍一看好像是的,因为这个类太简单了,而且没有竞态条件(当前的行为不受之前状态的影响)。但是很抱歉,这个类依然不是线程安全的。原因就是因为它的可见性不能保证,因此在多线程环境下,如果一个线程设置了value的值为100,那么另外一个线程或许会看不到100这个值。

  为何会这样呢?

  我们依然回想一下计算机原理当中的内容,在计算机原理当中我们曾经无数次的接触过寄存器与存储器,在汇编级别的代码当中,我们会发现,很多变量的赋值是不会反应到存储器当中的,它们有时候一直存在于寄存器当中。这样一来,可见性就好解释了,有时候一个线程A去读取一个变量,这时候它会瞄准存储器的某一个位置进行读取操作,它或许会期待另外一个线程B去改变存储器的值,但事实往往是,另外一个线程B只是把值隐藏在了寄存器,而导致线程A永远看不到这个更新后的值。

  还有另外一种情况是,编译器会将现有的程序进行乱序重组,或许表面看起来,我们是先给一个变量赋值,然后又在另外一个线程去读取它,但事实可能是我们先去读取了这个变量,然后才进行的赋值。

  不管是哪种情况,一旦牵扯到可见性,就说明程序的行为是不可预见的。换句话说,我们的程序如果想要正确的运行,和中彩票是一个概念,需要一定的概率才能发生,这当然是我们不能容忍的。

  因此,我们必须保证一个对象的可见性,否则在共享一个对象时,就会非常的危险。对于上面这个简单的整数类,我们只要给get/set方法加上synchronized关键字,就可以保证它的可见性。这是由于synchronized关键字不仅保证了同步机制,更重要的是禁用了乱序重组以及保证了值对存储器的写入,这样就可以保证可见性。

  

加锁机制(如何加锁)

  上面主要回答了各位我们应该在何时加锁,看似很复杂,但其实更难的还是在如何加锁的问题上。因为如果不考虑简单性或者性能等一些问题,给一个类的全部方法加上synchronized关键字就可以确保这个类的线程安全性。但是很显然,这种做法很多时候是不可取的,除非你想收到上级的“夸奖”。

  如果一个多线程环境下的类无法做成无状态或者是不可变对象,那么我们就只能尝试去做一些同步机制,来保证它的线程安全性,或者说保证它可以正常工作。这个问题很难一概而论,不过在绝大多数情况下,我们秉持这样一个原则去进行同步,那就是总是用同一个锁去保护需要协变的状态

  这一句话显然无法概括所有加锁的情况,但是却是LZ个人感觉能解决大部分问题的方法。接下来LZ就举一个简单的例子,比如上面的区间类,它当中就有一些明显的协变状态(协变状态是LZ个人起的名字,意思是想指那些需要相互协助变化的状态)。我们接下来就尝试将上面的区间类变成线程安全的类。

public class Region {

    private int left;

    private int right;

    public Region() {
super();
} public Region(int left, int right) {
super();
if (left <= right) {
this.left = left;
this.right = right;
}else {
this.left = left;
this.right = right;
}
} public synchronized void setLeft(int left) {
if (left > right) {
this.left = right;
}else {
this.left = left;
}
} public synchronized void setRight(int right) {
if (right < left) {
this.right = left;
}else {
this.right = right;
}
} public synchronized boolean in(int value){
return value >= left && value <= right;
} public String toString(){
return "[" + left + "," + right + "]";
} }

  方法非常简单,我们只是简单的给三个方法加上了synchronized关键字,但不可否认的是,它现在已经是一个线程安全的类(我们对toString的显示要求不高,因此不进行同步)。这个类当中很显然left和right变量是一组协变状态,它们两个之间需要相互协助的变化,而不可以单独进行改变。

  其实在现实当中,这样的协变状态有很多。比如我们常用的ArrayList,它当中就有一个Object数组和一个size标识,这两个状态很明显是需要协变的,一旦object数组有所变化,size就要跟随着变化,这样的话在多线程当中使用时,就需要将二者使用同一个锁进行同步(一般情况下,我们会使用当前对象充当这个锁,即this关键字)。

  如果一个方法当中,并不全是协变状态,我们就可以进行局部同步(使用synchronized同步块),这样就可以减少性能的损失,但也要保证一定的简单性,否则的话,这段程序维护起来会非常头疼。

  接下来,我们看一个简单的例子,我们给区间类加一些输出语句,来显示同步块的使用。

public class Region {

    private int left;

    private int right;

    public Region() {
super();
} public Region(int left, int right) {
super();
if (left <= right) {
this.left = left;
this.right = right;
}else {
this.left = left;
this.right = right;
}
} public void setLeft(int left) {
System.out.println("before setLeft:" + toString());
synchronized (this) {
if (left > right) {
this.left = right;
}else {
this.left = left;
}
}
System.out.println("after setLeft:" + toString());
} public void setRight(int right) {
System.out.println("before setRight:" + toString());
synchronized (this) {
if (right < left) {
this.right = left;
}else {
this.right = right;
}
}
System.out.println("after setRight:" + toString());
} public synchronized boolean in(int value){
return value >= left && value <= right;
} public String toString(){
return "[" + left + "," + right + "]";
} }

  这里我们为了尽可能的保证程序的性能,所以使用了同步块,在进行输出语句的调用时,并不会将当前对象锁定。众所周知,JAVA在I/O方面的处理是比较慢的,因此在同步的语句当中,我们应当尽量的将I/O语句移出同步块(当然还包括其它的一些处理较慢的语句)。

  这里LZ再举一个非常常见的例子,就是对于循环一个列表的处理,以下这段代码节选自JDK1.6当中Observable类(观察者模式当中的被观察者父类)。

public void notifyObservers(Object arg) {
/*
* a temporary array buffer, used as a snapshot of the state of
* current Observers.
*/
Object[] arrLocal; synchronized (this) {
/* We don't want the Observer doing callbacks into
* arbitrary code while holding its own Monitor.
* The code where we extract each Observable from
* the Vector and store the state of the Observer
* needs synchronization, but notifying observers
* does not (should not). The worst result of any
* potential race-condition here is that:
* 1) a newly-added Observer will miss a
* notification in progress
* 2) a recently unregistered Observer will be
* wrongly notified when it doesn't care
*/
if (!changed)
return;
arrLocal = obs.toArray();
clearChanged();
} for (int i = arrLocal.length-1; i>=0; i--)
((Observer)arrLocal[i]).update(this, arg);
}

  可以看到,这个方法的任务是通知所有的观察者,也就是说,需要循环obs这个list列表,并挨个调用update方法。但是这里并没有直接循环obs这个列表,而是使用了一个临时变量arrLocal,并获取到obs的一个快照(snapshot)进行循环。这就是为了保证同步的情况下,尽量的提高性能,因为update方法当中可能会有一些很占用时间的操作,这样的话,如果我们直接对obs循环期间进行同步,那么就可能会导致被观察者被锁定相当长的一段时间。

总结

  并发算是编程当中的一个高级课题,所以难度可能会较高。但话说回来,只要你在做Java Web,就一定离不开并发。所以看似高级课题的并发,其实一直都与你日夜相伴。从某种意义上来讲,真正要入门web的前提,就是搞清楚并发的相关内容,因为在运维的过程中,往往代码中出现的bug都是非常简单的,而难的地方,就是一些并发所带来的偶然性问题,这就需要你对并发有一定深入的了解才能发现问题的所在。

  好了,本章内容就到此为止了,尽管LZ也是刚刚入门,但还是希望本文能给各位带来一些帮助。

没听说过这些,就不要说你懂并发了,two。的更多相关文章

  1. 没听说过这些,就不要说你懂并发了,three。

    引言 很久没有跟大家再聊聊并发了,今天LZ闲来无事,跟大家再聊聊并发.由于时间过去的有点久,因此LZ就不按照常理出牌了,只是把自己的理解记录在此,如果各位猿友觉得有所收获,就点个推荐或者留言激励下LZ ...

  2. PAT 甲级 1145 Hashing - Average Search Time (25 分)(读不懂题,也没听说过平方探测法解决哈希冲突。。。感觉题目也有点问题)

    1145 Hashing - Average Search Time (25 分)   The task of this problem is simple: insert a sequence of ...

  3. 为什么Java有GC调优而没听说过有CLR的GC调优?

    前言 在很多的场合我都遇到过一些群友提这样的一些问题: 为什么Java有GC调优而CLR没有听说过有GC调优呢? 到底是Java的JVM GC比较强还是C#使用的.NET CLR的GC比较强呢? 其实 ...

  4. 【python】装饰器听了N次也没印象,读完这篇你就懂了

    装饰器其实一直是我的一个"老大难".这个知识点就放在那,但是拖延症... 其实在平常写写脚本的过程中,这个知识点你可能用到不多 但在面试的时候,这可是一个高频问题. 一.什么是装饰 ...

  5. 【python】递归听了N次也没印象,读完这篇你就懂了

    听到递归总觉得挺高大上的,为什么呢?因为对其陌生,那么今天就来一文记住递归到底是个啥. 不过先别急,一起来看一个问题:求10的阶乘(10!). 求x的阶乘,其实就是从1开始依次乘到x.那么10的阶乘就 ...

  6. Mono为何能跨平台?聊聊CIL(MSIL)

    前言: 其实小匹夫在U3D的开发中一直对U3D的跨平台能力很好奇.到底是什么原理使得U3D可以跨平台呢?后来发现了Mono的作用,并进一步了解到了CIL的存在.所以,作为一个对Unity3D跨平台能力 ...

  7. 不知道张(zhāng)雱(pāng)是谁?你out了!

    张(zhāng)雱(pāng)是谁?也许你已经听说过了,也许你还没听说过呢,不过你一定听说过老刘——刘强东,没错,这二人是有关系的,什么关系,京东是老刘的,而张雱呢?张雱是京东旗下52家关联公司法人代 ...

  8. Asp.net 面向接口可扩展框架之类型转化基础服务

    新框架正在逐步完善,可喜可贺的是基础服务部分初具模样了,给大家分享一下 由于基础服务涉及面太广,也没开发完,这篇只介绍其中的类型转化部分,命名为类型转化基础服务,其实就是基础服务模块的类型转化子模块 ...

  9. 做个体面有尊严的IT人【转自界面】

    向老罗致敬,好人终有好报: 转自网站:界面-http://www.jiemian.com/article/231843.html [华盛顿] 史蒂夫·马奎斯隐居在华盛顿郊外的一栋小木屋里,没有电视.没 ...

随机推荐

  1. Java基础 之软引用、弱引用、虚引用 ·[转载]

    Java基础 之软引用.弱引用.虚引用 ·[转载] 2011-11-24 14:43:41 Java基础 之软引用.弱引用.虚引用 浏览(509)|评论(1)   交流分类:Java|笔记分类: Ja ...

  2. springboot+mybatis+mysql创建简单web后台项目

    第一步:搭建框架 新建进入这个页面 新建名字,第一次可以默认,然后下一步 第三步:选择依赖 第四步:新建项目名和存放项目路径(你可以新建一个文件夹存放) 点击finish,首次创建Springboot ...

  3. 【websocket-sharp】使用

    一 介绍 WebSocket# 提供了实现WebSocket协议客户端和服务器. WebSocket协议是基于TCP的一种新的网络协议.它实现了浏览器与服务器全双工(full-duplex)通信——允 ...

  4. Leetcode Weekly Contest 86

    Weekly Contest 86 A:840. 矩阵中的幻方 3 x 3 的幻方是一个填充有从 1 到 9 的不同数字的 3 x 3 矩阵,其中每行,每列以及两条对角线上的各数之和都相等. 给定一个 ...

  5. centos安装swoole

        编译安装swoole: cd && wget https://github.com/swoole/swoole-src/archive/1.8.6-stable.tar.gz  ...

  6. hive使用derby的服务模式(可以远程模式)

    hive默认使用的derby的嵌入模式.这个就面临着,无法多个并发hive shell共享的问题. 使用MySQL服务器也可以解决问题,但安装.配置太麻烦了. 可以使用轻量级的derby的c/s服务模 ...

  7. Windows下基于Python3安装Ipython Notebook(即Jupyter)。python –m pip install XXX

    1.安装Python3.x,注意修改环境变量path(追加上python安装目录,如:D:\Program Files\Python\Python36-32) 2.查看当前安装的第三方包:python ...

  8. Word中摘要和正文同时分栏后,正文跑到下一页,怎么办?或Word分栏后第一页明明有空位后面的文字却自动跳到第二页了,怎么办?

    问题1:Word中摘要和正文同时分栏后,正文跑到下一页,怎么办?或Word分栏后第一页明明有空位后面的文字却自动跳到第二页了,怎么办? 答:在word2010中,菜单栏中最左侧选“文件”->“选 ...

  9. Caused by: org.apache.velocity.exception.MethodInvocationException: Invocation of method 'getUser' in class org.uncommons.reportng.ReportMetadata threw exception class java.net.UnknownHostException :

    Running TestSuite [TestNG] [WARN] Ignoring duplicate listener : org.uncommons.reportng.HTMLReporter ...

  10. Jenkins+github的一次定时构建示例

    首先说明,我的电脑环境是windows,所以以下的示例是基于windows10 X64. 一.新建任务,填写名称,选择类型,点击左下角的[确定] 二.配置 1.General 2.源码管理 之前在gi ...