分类目录归档:java

java入门与提高::::::演道网java专栏提供一线java研发人员在学习工作中的经验,减少大家走的弯路,大量源码可以直接使用。

Java中字符串内存位置浅析

前言

之前写过一篇关于JVM内存区域划分的文章,但是昨天接到蚂蚁金服的面试,问到JVM相关的内容,解释一下JVM的内存区域划分,这部分答得还不错,但是后来又问了Java里面String存放的位置,之前只记得String是一个不变的量,应该是要存放在常量池里面的,但是后来问到new一个String出来应该是放到哪里的,这个应该是放到堆里面的,后来又问到String的引用是放在什么地方的,当时傻逼的说也是放在堆里面的,现在总结一下:基本类型的变量数据和对象的引用都是放在栈里面的,对象本身放在堆里面,显式的String常量放在常量池,String对象放在堆中。

常量池的说明

常量池之前是放在方法区里面的,也就是在永久代里面的,从JDK7开始移到了堆里面。这一改变我们可以从Oraclerelease version的notes里的** Important RFEs Addressed in JDK 7 **看到。

Area: HotSpot
Synopsis: In JDK 7, interned strings are no longer allocated in the permanent generation of the Java heap, but are instead allocated in the main part of the Java heap (known as the young and old generations), along with the other objects created by the application. This change will result in more data residing in the main Java heap, and less data in the permanent generation, and thus may require heap sizes to be adjusted. Most applications will see only relatively small differences in heap usage due to this change, but larger applications that load many classes or make heavy use of the String.intern() method will see more significant differences.
RFE: 6962931

String内存位置说明

  1. 显式的String常量

    String a = "holten";
    String b = "holten";
  • 第一句代码执行后就在常量池中创建了一个值为holten的String对象;
  • 第二句执行时,因为常量池中存在holten所以就不再创建新的String对象了。
  • 此时该字符串的引用在虚拟机栈里面。
  1. String对象

    String a = new String("holtenObj");
    String b = new String("holtenObj");
  • Class被加载时就在常量池中创建了一个值为holtenObj的String对象,第一句执行时会在堆里创建new String(“holtenObj”)对象;
  • 第二句执行时,因为常量池中存在holtenObj所以就不再创建新的String对象了,直接在堆里创建new String(“holtenObj”)对象。

验证一下

/** * Created by holten.gao on 2016/8/16. */
public class Main {
    publicstaticvoidmain(String[] args){
        String str1 = "高小天";
        String str2 = "高小天";
        System.out.println(str1==str2);//true
        
        String str3 = new String("高大天");
        String str4 = new String("高大天");
        System.out.println(str3==str4);//false
    }
}

返回结果:

true
false

Java实现直接选择排序

选择排序是常用内部排序的一种,常见的实现算法有直接选择排序算法和堆排序算法,选择排序的基本思想是每次从待排数据中选择第n小的数据放到排序列表的第n个位置,假如共有N个数据待排,那么经过N-1次排序后,待排数据就已经按照从小到大的顺序排列了。

  直接选择排序算法的思想比较简单:(假设数据放在一个数组a中,且数组的长度是N)

  1:从a[0]-a[N-1]中选出最小的数据,然后与a[0]交换位置

  2:从a[1]-a[N-1]中选出最小的数据,然后与a[1]交换位置(第1步结束后a[0]就是N个数的最小值)

  3:从a[2]-a[N-1]中选出最小的数据,然后与a[2]交换位置(第2步结束后a[1]就是N-1个数的最小值)

  以此类推,N-1次排序后,待排数据就已经按照从小到大的顺序排列了。

  直接选择排序的java实现如下:

package datasort;
//选择排序O(n*n)
public class SelectSort {
    public static void SelectSort(int[] array){
        for(int i=0;i            int k=i;
            for(int j=i;j                if(array[k]>array[j]){
                    k=j;
                }
            }
            if(k!=i){
                int temp=array[k];
                array[k]=array[i];
                array[i]=temp;
            }
        }
    }

    public static void main(String[] args) {
        int[] a={28,4,36,2,65,14,55,17};
        SelectSort(a);
        System.out.println();
        for(int i=0;i            System.out.print(a[i]+” “);
        }
    }
}

Java实现直接选择排序

Java集合-ArrayList

<

div id=”content” contentScore=”17126″>一、ArrayList 概述

ArrayList 是实现 List 接口的动态数组,所谓动态就是它的大小是可变的。实现了所有可选列表操作,并允许包括 null 在内的所有元素。除了实现 List 接口外,此类还提供一些方法来操作内部用来存储列表的数组的大小。

每个 ArrayList 实例都有一个容量,该容量是指用来存储列表元素的数组的大小。默认初始容量为 10。随着 ArrayList 中元素的增加,它的容量也会不断的自动增长。在每次添加新的元素时,ArrayList 都会检查是否需要进行扩容操作,扩容操作带来数据向新数组的重新拷贝,所以如果我们知道具体业务数据量,在构造 ArrayList 时可以给 ArrayList 指定一个初始容量,这样就会减少扩容时数据的拷贝问题。当然在添加大量元素前,应用程序也可以使用 ensureCapacity 操作来增加 ArrayList 实例的容量,这可以减少递增式再分配的数量。

注意,ArrayList 实现不是同步的。如果多个线程同时访问一个 ArrayList 实例,而其中至少一个线程从结构上修改了列表,那么它必须保持外部同步。所以为了保证同步,最好的办法是在创建时完成,以防止意外对列表进行不同步的访问:

        List list = Collections.synchronizedList(new ArrayList<>());

二、ArrayList 源码分析

ArrayList 我们使用的实在是太多了,非常熟悉,所以在这里将不介绍它的使用方法。ArrayList 是实现 List 接口的,底层采用数组实现,所以它的操作基本上都是基于对数组的操作。

2.1、底层使用数组

    /**
    * Default initial capacity.
    */
    private static final int DEFAULT_CAPACITY = 10;

    /**
    * Shared empty array instance used for empty instances.
    */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
    * Shared empty array instance used for default sized empty instances. We
    * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
    * first element is added.
    */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
    * The array buffer into which the elements of the ArrayList are stored.
    * The capacity of the ArrayList is the length of this array buffer. Any
    * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
    * will be expanded to DEFAULT_CAPACITY when the first element is added.
    */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
    * The size of the ArrayList (the number of elements it contains).
    *
    * @serial
    */
    private int size;

transient??为 Java 关键字,为变量修饰符,如果用 transient 声明一个实例变量,当对象存储时,它的值不需要维持。Java 的 serialization 提供了一种持久化对象实例的机制。当持久化对象时,可能有一个特殊的对象数据成员,我们不想用 serialization 机制来保存它。为了在一个特定对象的一个域上关闭 serialization,可以在这个域前加上关键字 transient。当一个对象被序列化的时候,transient 型变量的值不包括在序列化的表示中,然而非 transient 型的变量是被包括进去的。

这里 Object[] elementData,就是我们的 ArrayList 容器,下面介绍的基本操作都是基于该 elementData 变量来进行操作的。

2.2、构造函数

ArrayList 提供了三个构造函数:

ArrayList():默认构造函数,提供初始容量为 10 的空列表。

ArrayList(int initialCapacity):构造一个具有指定初始容量的空列表。

ArrayList(Collection c):构造一个包含指定 collection 的元素的列表,这些元素是按照该 collection 的迭代器返回它们的顺序排列的。

    /**
    * Constructs an empty list with the specified initial capacity.
    *
    * @param  initialCapacity  the initial capacity of the list
    * @throws IllegalArgumentException if the specified initial capacity
    *        is negative
    */
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException(“Illegal Capacity: “+
                                              initialCapacity);
        }
    }

    /**
    * Constructs an empty list with an initial capacity of ten.
    */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    /**
    * Constructs a list containing the elements of the specified
    * collection, in the order they are returned by the collection’s
    * iterator.
    *
    * @param c the collection whose elements are to be placed into this list
    * @throws NullPointerException if the specified collection is null
    */
    public ArrayList(Collection c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

2.3、新增

ArrayList 提供了 add(E e)、add(int index, E element)、addAll(Collection c)、addAll(int index, Collection c)、set(int index, E element) 这个五个方法来实现 ArrayList 增加。

add(E e):将指定的元素添加到此列表的尾部。

    /**
    * Appends the specified element to the end of this list.
    *
    * @param e element to be appended to this list
    * @return true (as specified by {@link Collection#add})
    */
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

这里 ensureCapacity() 方法是对 ArrayList 集合进行扩容操作,elementData(size++) = e,将列表末尾元素指向e。

add(int index, E element):将指定的元素插入此列表中的指定位置。

  public void add(int index, E element) {
            //判断索引位置是否正确
            if (index > size || index < 0)
                throw new IndexOutOfBoundsException(
                “Index: “+index+”, Size: “+size);
            //扩容检测
            ensureCapacity(size+1); 
            /*
            * 对源数组进行复制处理(位移),从index + 1到size-index。
            * 主要目的就是空出index位置供数据插入,
            * 即向右移动当前位于该位置的元素以及所有后续元素。
            */
            System.arraycopy(elementData, index,  elementData, index + 1,
                    size – index);
            //在指定位置赋值
            elementData[index] = element;
            size++;
            }

在这个方法中最根本的方法就是 System.arraycopy() 方法,该方法的根本目的就是将 index 位置空出来以供新数据插入,这里需要进行数组数据的右移,这是非常麻烦和耗时的,所以如果指定的数据集合需要进行大量插入(中间插入)操作,推荐使用 LinkedList。

addAll(Collection c):按照指定 collection 的迭代器所返回的元素顺序,将该 collection 中的所有元素添加到此列表的尾部。

    public boolean addAll(Collection c) {
            // 将集合C转换成数组
            Object[] a = c.toArray();
            int numNew = a.length;
            // 扩容处理,大小为size + numNew
            ensureCapacity(size + numNew); // Increments modCount
            System.arraycopy(a, 0, elementData, size, numNew);
            size += numNew;
          return numNew != 0;
        }

这个方法无非就是使用 System.arraycopy() 方法将 C 集合(先准换为数组)里面的数据复制到 elementData 数组中。这里就稍微介绍下 System.arraycopy(),因为下面还将大量用到该方法。该方法的原型为:public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)。它的根本目的就是进行数组元素的复制。即从指定源数组中复制一个数组,复制从指定的位置开始,到目标数组的指定位置结束。将源数组 src从srcPos 位置开始复制到 dest 数组中,复制长度为 length,数据从 dest 的 destPos 位置开始粘贴��

addAll(int index, Collection c):从指定的位置开始,将指定 collection 中的所有元素插入到此列表中。

    public boolean addAll(int index, Collection c) {
            //判断位置是否正确
            if (index > size || index < 0)
                throw new IndexOutOfBoundsException(“Index: ” + index + “, Size: “
                        + size);
            //转换成数组
            Object[] a = c.toArray();
            int numNew = a.length;
            //ArrayList容器扩容处理
            ensureCapacity(size + numNew); // Increments modCount
            //ArrayList容器数组向右移动的位置
            int numMoved = size – index;
            //如果移动位置大于0,则将ArrayList容器的数据向右移动numMoved个位置,确保增加的数据能够增加
            if (numMoved > 0)
                System.arraycopy(elementData, index, elementData, index + numNew,
                        numMoved);
            //添加数组
            System.arraycopy(a, 0, elementData, index,  numNew);
            //容器容量变大
            size += numNew; 
            return numNew != 0;
        }

set(int index, E element):用指定的元素替代此列表中指定位置上的元素。

    public E set(int index, E element) {
            //检测插入的位置是否越界
            RangeCheck(index);

            E oldValue = (E) elementData[index];
            //替代
            elementData[index] = element;
            return oldValue;
        }

2.4、删除

ArrayList 提供了 remove(int index)、remove(Object o)、removeRange(int fromIndex, int toIndex)、removeAll() 四个方法进行元素的删除。

remove(int index):移除此列表中指定位置上的元素。

 public E remove(int index) {
            //位置验证
            RangeCheck(index);

          modCount++;
          //需要删除的元素
            E oldValue = (E) elementData[index]; 
            //向左移的位数
            int numMoved = size – index – 1;
            //若需要移动,则想左移动numMoved位
            if (numMoved > 0)
                System.arraycopy(elementData, index + 1, elementData, index,
                        numMoved);
            //置空最后一个元素
            elementData[–size] = null; // Let gc do its work

            return oldValue;
        }

remove(Object o):移除此列表中首次出现的指定元素(如果存在)。

public boolean remove(Object o) {
            //因为ArrayList中允许存在null,所以需要进行null判断
            if (o == null) {
                for (int index = 0; index < size; index++)
                    if (elementData[index] == null) {
                        //移除这个位置的元素
                        fastRemove(index);
                        return true;
                    }
            } else {
                for (int index = 0; index < size; index++)
                    if (o.equals(elementData[index])) {
                        fastRemove(index);
                        return true;
                    }
            }
            return false;
        }

其中 fastRemove() 方法用于移除指定位置的元素。如下

 private void fastRemove(int index) {
            modCount++;
            int numMoved = size – index – 1;
            if (numMoved > 0)
                System.arraycopy(elementData, index+1, elementData, index,
                                numMoved);
            elementData[–size] = null; // Let gc do its work
        }

removeRange(int fromIndex, int toIndex):移除列表中索引在 fromIndex(包括)和 toIndex(不包括)之间的所有元素。

  protected void removeRange(int fromIndex, int toIndex)    {
            modCount++;
            int numMoved = size – toIndex;
            System
                    .arraycopy(elementData, toIndex, elementData, fromIndex,
                            numMoved);

            // Let gc do its work
            int newSize = size – (toIndex – fromIndex);
            while (size != newSize)
                elementData[–size] = null;
        }

removeAll():是继承自 AbstractCollection 的方法,ArrayList 本身并没有提供实现。

 public boolean removeAll(Collection c) {
            boolean modified = false;
            Iterator<?> e = iterator();
            while (e.hasNext()) {
                if (c.contains(e.next())) {
                    e.remove();
                    modified = true;
                }
            }
            return modified;
        }

2.5、查找

ArrayList 提供了 get(int index) 用读取 ArrayList 中的元素。由于 ArrayList 是动态数组,所以我们完全可以根据下标来获取 ArrayList 中的元素,而且速度还比较快,故 ArrayList 长于随机访问。

 public E get(int index) {
            RangeCheck(index);

            return (E) elementData[index];
        }

2.6、扩容

在上面的新增方法的源码中我们发现每个方法中都存在这个方法: ensureCapacity(),该方法就是 ArrayList 的扩容方法。在前面就提过 ArrayList 每次新增元素时都会需要进行容量检测判断,若新增元素后元素的个数会超过 ArrayList 的容量,就会进行扩容操作来满足新增元素的需求。所以当我们清楚知道业务数据量或者需要插入大量元素前,我可以使用 ensureCapacity 来手动增加 ArrayList 实例的容量,以减少递增式再分配的数量。

public void ensureCapacity(int minCapacity) {
            //修改计时器
            modCount++;
            //ArrayList容量大小
            int oldCapacity = elementData.length;
            /*
            * 若当前需要的长度大于当前数组的长度时,进行扩容操 作
            */
            if (minCapacity > oldCapacity) {
                Object oldData[] = elementData;
                //计算新的容量大小,为当前容量的1.5倍
                int newCapacity = (oldCapacity * 3) / 2 + 1;
                if (newCapacity < minCapacity)
                newCapacity = minCapacity;
                //数组拷贝,生成新的数组
                elementData = Arrays.copyOf(elementData, newCapacity);
            }
        }

在这里有一个疑问,为什么每次扩容处理会是 1.5 倍,而不是 2.5、3、4 倍呢?通过 google 查找,发现 1.5 倍的扩容是最好的倍数。因为一次性扩容太大(例如 2.5 倍)可能会浪费更多的内存(1.5 倍最多浪费 33%,而 2.5 被最多会浪费 60%,3.5 倍则会浪费 71%……)。但是一次性扩容太小,需要多次对数组重新分配内存,对性能消耗比较严重。所以 1.5 倍刚刚好,既能满足性能需求,也不会造成很大的内存消耗。

处理这个 ensureCapacity() 这个扩容数组外,ArrayList 还给我们提供了将底层数组的容量调整为当前列表保存的实际元素的大小的功能。它可以通过 trimToSize() 方法来实现。该方法可以最小化 ArrayList 实例的存储量。

 public void trimToSize() {
            modCount++;
            int oldCapacity = elementData.length;
            if (size < oldCapacity) {
                elementData = Arrays.copyOf(elementData, size);
            }
        }

Java内存模型-锁

<

div id=”content” contentScore=”6969″>

锁的释放-获取建立的 happens before 关系

锁是 java 并发编程中最重要的同步机制。锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息。下面是锁释放-获取的示例代码:

class MonitorExample {
    int a = 0;

    public synchronized void writer() {  //1
        a++;                             //2
    }                                    //3

    public synchronized void reader() {  //4
        int i = a;                       //5
        ……
    }                                    //6
}   

假设线程 A 执行 writer() 方法,随后线程 B 执行 reader() 方法。根据 happens before 规则,这个过程包含的 happens before 关系可以分为两类:

  1. 根据程序次序规则,1 happens before 2, 2 happens before 3; 4 happens before 5, 5 happens before 6。
  2. 根据监视器锁规则,3 happens before 4。
  3. 根据 happens before 的传递性,2 happens before 5。

上述 happens before 关系的图形化表现形式如下:

在上图中,每一个箭头链接的两个节点,代表了一个 happens before 关系。黑色箭头表示程序顺序规则;橙色箭头表示监视器锁规则;蓝色箭头表示组合这些规则后提供的 happens before保证。

上图表示在线程A释放了锁之后,随后线程B获取同一个锁。在上图中,2 happens before 5。因此,线程A在释放锁之前所有可见的共享变量,在线程B获取同一个锁之后,将立刻变得对B线程可见。

锁释放和获取的内存语义

当线程释放锁时,JMM 会把该线程对应的本地内存中的共享变量刷新到主内存中。以上面的MonitorExample 程序为例,A线程释放锁后,共享数据的状态示意图如下:

当线程获取锁时,JMM 会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须要从主内存中去读取共享变量。下面是锁获取的状态示意图:

对比锁释放-获取的内存语义与 volatile 写-读的内存语义,可以看出:锁释放与 volatile 写有相同的内存语义;锁获取与 volatile 读有相同的内存语义。

下面对锁释放和锁获取的内存语义做个总结:

  • 线程 A 释放一个锁,实质上是线程 A 向接下来将要获取这个锁的某个线程发出了(线程 A 对共享变量所做修改的)消息。
  • 线程 B 获取一个锁,实质上是线程 B 接收了之前某个线程发出的(在释放这个锁之前对共享变量所做修改的)消息。
  • 线程 A 释放锁,随后线程 B 获取这个锁,这个过程实质上是线程 A 通过主内存向线程 B 发送消息。

锁内存语义的实现

本文将借助 ReentrantLock 的源代码,来分析锁内存语义的具体实现机制。

请看下面的示例代码:

class ReentrantLockExample {
int a = 0;
ReentrantLock lock = new ReentrantLock();

public void writer() {
    lock.lock();         //获取锁
    try {
        a++;
    } finally {
        lock.unlock();  //释放锁
    }
}

public void reader () {
    lock.lock();        //获取锁
    try {
        int i = a;
        ……
    } finally {
        lock.unlock();  //释放锁
    }
}
} 

在 ReentrantLock 中,调用 lock() 方法获取锁;调用 unlock() 方法释放锁。

ReentrantLock 的实现依赖于 java 同步器框架 AbstractQueuedSynchronizer(本文简称之为AQS)。AQS 使用一个整型的 volatile 变量(命名为 state)来维护同步状态,马上我们会看到,这个 volatile 变量是 ReentrantLock 内存语义实现的关键。 下面是ReentrantLock 的类图(仅画出与本文相关的部分):

ReentrantLock 分为公平锁和非公平锁,我们首先分析公平锁。

使用公平锁时,加锁方法 lock() 的方法调用轨迹如下:

  1. ReentrantLock : lock()
  2. FairSync : lock()
  3. AbstractQueuedSynchronizer : acquire(int arg)
  4. ReentrantLock : tryAcquire(int acquires)

在第4步真正开始加锁,下面是该方法的源代码:

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();   //获取锁的开始,首先读volatile变量state
    if (c == 0) {
        if (isFirst(current) &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)  
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}   

从上面源代码中我们可以看出,加锁方法首先读 volatile 变量 state。

在使用公平锁时,解锁方法 unlock() 的方法调用轨迹如下:

  1. ReentrantLock : unlock()
  2. AbstractQueuedSynchronizer : release(int arg)
  3. Sync : tryRelease(int releases)

在第3步真正开始释放锁,下面是该方法的源代码:

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);           //释放锁的最后,写volatile变量state
    return free;
}   

从上面的源代码我们可以看出,在释放锁的最后写 volatile 变量 state。

公平锁在释放锁的最后写 volatile 变量 state;在获取锁时首先读这个 volatile 变量。根据 volatile 的 happens-before 规则,释放锁的线程在写 volatile 变量之前可见的共享变量,在获取锁的线程读取同一个 volatile 变量后将立即变的对获取锁的线程可见。

现在我们分析非公平锁的内存语义的实现。

非公平锁的释放和公平锁完全一样,所以这里仅仅分析非公平锁的获取。

使用非公平锁时,加锁方法 lock() 的方法调用轨迹如下:

  1. ReentrantLock : lock()
  2. NonfairSync : lock()
  3. AbstractQueuedSynchronizer : compareAndSetState(int expect, int update)

在第3步真正开始加锁,下面是该方法的源代码:

protected final boolean compareAndSetState(int expect, int update) {
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
} 

 

该方法以原子操作的方式更新 state 变量,本文把 java 的 compareAndSet() 方法调用简称为 CAS。JDK 文档对该方法的说明如下:如果当前状态值等于预期值,则以原子方式将同步状态设置为给定的更新值。此操作具有 volatile 读和写的内存语义。

这里我们分别从编译器和处理器的角度来分析,CAS 如何同时具有 volatile 读和 volatile 写的内存语义。

前文我们提到过,编译器不会对 volatile 读与 volatile 读后面的任意内存操作重排序;编译器不会对 volatile 写与 volatile 写前面的任意内存操作重排序。组合这两个条件,意味着为了同时实现 volatile 读和 volatile 写的内存语义,编译器不能对 CAS 与 CAS 前面和后面的任意内存操作重排序。

下面我们来分析在常见的 intel x86 处理器中,CAS 是如何同时具有 volatile 读和 volatile 写的内存语义的。

下面是 sun.misc.Unsafe 类的 compareAndSwapInt() 方法的源代码:

public final native boolean compareAndSwapInt(Object o, long offset,
                                              int expected,
                                              int x);  

可以看到这是个本地方法调用。这个本地方法在 openjdk 中依次调用的 C++ 代码为:unsafe.cpp,atomic.cpp 和 atomicwindowsx86.inline.hpp。这个本地方法的最终实现在 openjdk 的如下位置:openjdk-7-fcs-src-b147-27jun2011openjdkhotspotsrcoscpuwindowsx86vm atomicwindowsx86.inline.hpp(对应于 windows 操作系统,X86 处理器)。下面是对应于 intel x86 处理器的源代码的片段:

// Adding a lock prefix to an instruction on MP machine
// VC++ doesn't like the lock prefix to be on a single line
// so we can't insert a label after the lock prefix.
// By emitting a lock prefix, we can define a label after it.
#define LOCK_IF_MP(mp) __asm cmp mp, 0  
                       __asm je L0      
                       __asm _emit 0xF0 
                       __asm L0:

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  // alternative for InterlockedCompareExchange
  int mp = os::is_MP();
  __asm {
    mov edx, dest
    mov ecx, exchange_value
    mov eax, compare_value
    LOCK_IF_MP(mp)
    cmpxchg dword ptr [edx], ecx
  }
} 

如上面源代码所示,程序会根据当前处理器的类型来决定是否为 cmpxchg 指令添加 lock 前缀。如果程序是在多处理器上运行,就为 cmpxchg 指令加上 lock 前缀(lock cmpxchg)。反之,如果程序是在单处理器上运行,就省略 lock 前缀(单处理器自身会维护单处理器内的顺序一致性,不需要 lock 前缀提供的内存屏障效果)。

intel 的手册对 lock 前缀的说明如下:

  1. 确保对内存的读-改-写操作原子执行。在 Pentium 及 Pentium 之前的处理器中,带有lock 前缀的指令在执行期间会锁住总线,使得其他处理器暂时无法通过总线访问内存。很显然,这会带来昂贵的开销。从 Pentium 4,Intel Xeon 及 P6 处理器开始,intel 在原有总线锁的基础上做了一个很有意义的优化:如果要访问的内存区域(area of memory)在 lock 前缀指令执行期间已经在处理器内部的缓存中被锁定(即包含该内存区域的缓存行当前处于独占或以修改状态),并且该内存区域被完全包含在单个缓存行(cache line)中,那么处理器将直接执行该指令。由于在指令执行期间该缓存行会一直被锁定,其它处理器无法读/写该指令要访问的内存区域,因此能保证指令执行的原子性。这个操作过程叫做缓存锁定(cache locking),缓存锁定将大大降低 lock 前缀指令的执行开销,但是当多处理器之间的竞争程度很高或者指令访问的内存地址未对齐时,仍然会锁住总线。
  2. 禁止该指令与之前和之后的读和写指令重排序。
  3. 把写缓冲区中的所有数据刷新到内存中。

上面的第2点和第3点所具有的内存屏障效果,足以同时实现 volatile 读和 volatile 写的内存语义。

经过上面的这些分析,现在我们终于能明白为什么 JDK 文档说 CAS 同时具有 volatile 读和volatile 写的内存语义了。

现在对公平锁和非公平锁的内存语义做个总结:

  • 公平锁和非公平锁释放时,最后都要写一个 volatile 变量 state。
  • 公平锁获取时,首先会去读这个 volatile 变量。
  • 非公平锁获取时,首先会用 CAS 更新这个 volatile 变量,这个操作同时具有 volatile 读和 volatile 写的内存语义。

从本文对 ReentrantLock 的分析可以看出,锁释放-获取的内存语义的实现至少有下面两种方式:

  1. 利用 volatile 变量的写-读所具有的内存语义。
  2. 利用 CAS 所附带的 volatile 读和 volatile 写的内存语义。

concurrent 包的实现

由于 java 的 CAS 同时具有 volatile 读和 volatile 写的内存语义,因此 Java 线程之间的通信现在有了下面四种方式:

  1. A 线程写 volatile 变量,随后 B 线程读这个 volatile 变量。
  2. A 线程写 volatile 变量,随后 B 线程用 CAS 更新这个 volatile 变量。
  3. A 线程用 CAS 更新一个volatile变量,随后 B 线程用 CAS 更新这个 volatile 变量。
  4. A 线程用 CAS 更新一个 volatile 变量,随后 B 线程读这个 volatile 变量。

Java 的 CAS 会使用现代处理器上提供的高效机器级别原子指令,这些原子指令以原子方式对内存执行读-改-写操作,这是在多处理器中实现同步的关键(从本质上来说,能够支持原子性读-改-写指令的计算机器,是顺序计算图灵机的异步等价机器,因此任何现代的多处理器都会去支持某种能对内存执行原子性读-改-写操作的原子指令)。同时,volatile 变量的读/写和 CAS 可以实现线程之间的通信。把这些特性整合在一起,就形成了整个 concurrent 包得以实现的基石。如果我们仔细分析 concurrent 包的源代码实现,会发现一个通用化的实现模式:

  1. 首先,声明共享变量为 volatile;
  2. 然后,使用 CAS 的原子条件更新来实现线程之间的同步;
  3. 同时,配合以 volatile 的读/写和 CAS 所具有的 volatile 读和写的内存语义来实现线程之间的通信。

AQS,非阻塞数据结构和原子变量类(java.util.concurrent.atomic 包中的类),这些 concurrent 包中的基础类都是使用这种模式来实现的,而 concurrent 包中的高层类又是依赖于这些基础类来实现的。从整体来看,concurrent 包的实现示意图如下:

Java内存模型-final

<

div id=”content” contentScore=”7442″>与前面介绍的锁和 volatile 相比较(见http://www.linuxidc.com/Linux/2016-08/134512.htm),对 final 域的读和写更像是普通的变量访问。对于final 域,编译器和处理器要遵守两个重排序规则:

  1. 在构造函数内对一个 final 域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
  2. 初次读一个包含 final 域的对象的引用,与随后初次读这个 final 域,这两个操作之间不能重排序。

下面,我们通过一些示例性的代码来分别说明这两个规则:

public class FinalExample {
    int i;                            //普通变量
    final int j;                      //final变量
    static FinalExample obj;

    public void FinalExample () {     //构造函数
        i = 1;                        //写普通域
        j = 2;                        //写final域
    }

    public static void writer () {    //写线程A执行
        obj = new FinalExample ();
    }

    public static void reader () {       //读线程B执行
        FinalExample object = obj;       //读对象引用
        int a = object.i;                //读普通域
        int b = object.j;                //读final域
    }
}  

这里假设一个线程 A 执行 writer() 方法,随后另一个线程 B 执行 reader() 方法。下面我们通过这两个线程的交互来说明这两个规则。

写 final 域的重排序规则

写 final 域的重排序规则禁止把 final 域的写重排序到构造函数之外。这个规则的实现包含下面2个方面:

  • JMM 禁止编译器把 final 域的写重排序到构造函数之外。
  • 编译器会在 final 域的写之后,构造函数 return 之前,插入一个 StoreStore 屏障。这个屏障禁止处理器把 final 域的写重排序到构造函数之外。

现在让我们分析 writer() 方法。writer() 方法只包含一行代码:finalExample = new FinalExample()。这行代码包含两个步骤:

  1. 构造一个 FinalExample 类型的对象;
  2. 把这个对象的引用赋值给引用变量 obj。

假设线程 B 读对象引用与读对象的成员域之间没有重排序(马上会说明为什么需要这个假设),下图是一种可能的执行时序:

在上图中,写普通域的操作被编译器重排序到了构造函数之外,读线程B错误的读取了普通变量i初始化之前的值。而写 final 域的操作,被写 final 域的重排序规则“限定”在了构造函数之内,读线程 B 正确的读取了 final 变量初始化之后的值。

写 final 域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的 final 域已经被正确初始化过了,而普通域不具有这个保障。以上图为例,在读线程 B “看到”对象引用 obj 时,很可能 obj 对象还没有构造完成(对普通域i的写操作被重排序到构造函数外,此时初始值2还没有写入普通域i)。

读 final 域的重排序规则

读 final 域的重排序规则如下:

  • 在一个线程中,初次读对象引用与初次读该对象包含的 final 域,JMM 禁止处理器重排序这两个操作(注意,这个规则仅仅针对处理器)。编译器会在读 final 域操作的前面插入一个 LoadLoad 屏障。

初次读对象引用与初次读该对象包含的 final 域,这两个操作之间存在间接依赖关系。由于编译器遵守间接依赖关系,因此编译器不会重排序这两个操作。大多数处理器也会遵守间接依赖,大多数处理器也不会重排序这两个操作。但有少数处理器允许对存在间接依赖关系的操作做重排序(比如 alpha 处理器),这个规则就是专门用来针对这种处理器。

reader() 方法包含三个操作:

  1. 初次读引用变量 obj;
  2. 初次读引用变量 obj 指向对象的普通域 j。
  3. 初次读引用变量 obj 指向对象的 final 域 i。

现在我们假设写线程 A 没有发生任何重排序,同时程序在不遵守间接依赖的处理器上执行,下面是一种可能的执行时序:

在上图中,读对象的普通域的操作被处理器重排序到读对象引用之前。读普通域时,该域还没有被写线程A写入,这是一个错误的读取操作。而读 final 域的重排序规则会把读对象 final 域的操作“限定”在读对象引用之后,此时该 final 域已经被 A 线程初始化过了,这是一个正确的读取操作。

读 final 域的重排序规则可以确保:在读一个对象的 final 域之前,一定会先读包含这个 final 域的对象的引用。在这个示例程序中,如果该引用不为 null,那么引用对象的 final 域一定已经被 A 线程初始化过了。

如果 final 域是引用类型

上面我们看到的 final 域是基础数据类型,下面让我们看看如果 final 域是引用类型,将会有什么效果?

请看下列示例代码:

public class FinalReferenceExample {
final int[] intArray;                     //final是引用类型
static FinalReferenceExample obj;

public FinalReferenceExample () {        //构造函数
    intArray = new int[1];              //1
    intArray[0] = 1;                   //2
}

public static void writerOne () {          //写线程A执行
    obj = new FinalReferenceExample ();  //3
}

public static void writerTwo () {          //写线程B执行
    obj.intArray[0] = 2;                 //4
}

public static void reader () {              //读线程C执行
    if (obj != null) {                    //5
        int temp1 = obj.intArray[0];       //6
    }
}
}   

这里 final 域为一个引用类型,它引用一个 int 型的数组对象。对于引用类型,写 final 域的重排序规则对编译器和处理器增加了如下约束:

  1. 在构造函数内对一个 final 引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。

对上面的示例程序,我们假设首先线程 A 执行 writerOne() 方法,执行完后线程 B 执行 writerTwo() 方法,执行完后线程 C 执行 reader() 方法。下面是一种可能的线程执行时序:

在上图中,1 是对 final 域的写入,2 是对这个 final 域引用的对象的成员域的写入,3是把被构造的对象的引用赋值给某个引用变量。这里除了前面提到的1不能和3重排序外,2和3也不能重排序。

JMM 可以确保读线程 C 至少能看到写线程 A 在构造函数中对 final 引用对象的成员域的写入。即 C 至少能看到数组下标 0 的值为 1。而写线程 B 对数组元素的写入,读线程 C 可能看的到,也可能看不到。JMM 不保证线程 B 的写入对读线程 C 可见,因为写线程 B 和读线程 C 之间存在数据竞争,此时的执行结果不可预知。

如果想要确保读线程 C 看到写线程 B 对数组元素的写入,写线程 B 和读线程 C 之间需要使用同步原语(lock 或 volatile)来确保内存可见性。

为什么 final 引用不能从构造函数内“逸出”

前面我们提到过,写 final 域的重排序规则可以确保:在引用变量为任意线程可见之前,该引用变量指向的对象的 final 域已经在构造函数中被正确初始化过了。其实要得到这个效果,还需要一个保证:在构造函数内部,不能让这个被构造对象的引用为其他线程可见,也就是对象引用不能在构造函数中“逸出”。为了说明问题,让我们来看下面示例代码:

public class FinalReferenceEscapeExample {
final int i;
static FinalReferenceEscapeExample obj;

public FinalReferenceEscapeExample () {
    i = 1;                              //1写final域
    obj = this;                          //2 this引用在此“逸出”
}

public static void writer() {
    new FinalReferenceEscapeExample ();
}

public static void reader {
    if (obj != null) {                     //3
        int temp = obj.i;                 //4
    }
}
}  

假设一个线程 A 执行 writer() 方法,另一个线程 B 执行 reader() 方法。这里的操作2使得对象还未完成构造前就为线程 B 可见。即使这里的操作 2 是构造函数的最后一步,且即使在程序中操作 2 排在操作 1 后面,执行 read() 方法的线程仍然可能无法看到 final 域被初始化后的值,因为这里的操作 1 和操作 2 之间可能被重排序。实际的执行时序可能如下图所示:

从上图我们可以看出:在构造函数返回前,被构造对象的引用不能为其他线程可见,因为此时的 final 域可能还没有被初始化。在构造函数返回后,任意线程都将保证能看到 final 域正确初始化之后的值。

final 语义在处理器中的实现

现在我们以 x86 处理器为例,说明 final 语义在处理器中的具体实现。

上面我们提到,写 final 域的重排序规则会要求译编器在 final 域的写之后,构造函数return 之前,插入一个 StoreStore 障屏。读 final 域的重排序规则要求编译器在读 final 域的操作前面插入一个 LoadLoad 屏障。

由于 x86 处理器不会对写-写操作做重排序,所以在 x86 处理器中,写 final 域需要的 StoreStore 障屏会被省略掉。同样,由于 x86 处理器不会对存在间接依赖关系的操作做重排序,所以在 x86 处理器中,读 final 域需要的 LoadLoad 屏障也会被省略掉。也就是说在 x86 处理器中,final 域的读/写不会插入任何内存屏障!

JSR-133 为什么要增强 final 的语义

在旧的 Java 内存模型中 ,最严重的一个缺陷就是线程可能看到 final 域的值会改变。比如,一个线程当前看到一个整形 final 域的值为 0(还未初始化之前的默认值),过一段时间之后这个线程再去读这个 final 域的值时,却发现值变为了 1(被某个线程初始化之后的值)。最常见的例子就是在旧的 Java 内存模型中,String 的值可能会改变(参考文献 2 中有一个具体的例子,感兴趣的读者可以自行参考,这里就不赘述了)。

为了修补这个漏洞,JSR-133 专家组增强了 final 的语义。通过为 final 域增加写和读重排序规则,可以为 java 程序员提供初始化安全保证:只要对象是正确构造的(被构造对象的引用在构造函数中没有“逸出”),那么不需要使用同步(指 lock 和 volatile 的使用),就可以保证任意线程都能看到这个 final 域在构造函数中被初始化之后的值。

远程接口设计经验分享

摘要:

远程接口设计经验分享
写在前边
分布式架构是互联网应用的基础架构,很多新人入职以来就开始负责编写和调用阿里的各种远程接口。但如同结婚一般,用对一个正确的接口就如同嫁一个正确的人一样,往往难以那么顺利的实现,或多或少大家都会在这个上边吃亏。
每年双十一系统调用复盘的时候,我都会听到以下声音
你们调我…

远程接口设计经验分享

写在前边

分布式架构是互联网应用的基础架构,很多新人入职以来就开始负责编写和调用阿里的各种远程接口。但如同结婚一般,用对一个正确的接口就如同嫁一个正确的人一样,往往难以那么顺利的实现,或多或少大家都会在这个上边吃亏。

每年双十一系统调用复盘的时候,我都会听到以下声音

  • 你们调我的接口报错了竟然不会自己重试?
  • 我的返回值应该从这里取
  • 我返回isSuccess() == true,不代表业务成功,你还需要判断ERROR_CODE
  • 这个ERROR_CODE没说全部都要重试啊!
  • 这个ERROR_CODE必须要重试!

还有很多了,本文的目标就是帮助大家思考,如何设计自己的远程接口,让接口做到健壮易用,节省大家在这块泥潭中所挣扎的时间。

一个日志服务LogService

PS:本例子的代码可以见 Excavatore-DEMO

苍老师
上课!大家好,我是你们的苍老师。今天就由我来给大家讲讲如何编写一个健壮的远程接口。老师将在这里给大家设计一个集中式的日志系统。

虽然这个系统的存在不合理,但这是能找到的最简单例子,所以不要在课堂上就系统的合理性展开讨论,否则老师会生气的哟~

老师

系统架构

一个集中性的日志服务器,要求应用通过日志系统提供的日志服务,将所有日志集中统一的输出到固定的文件中。

系统架构图

日志服务器架构

小明
xiaoming_head 这很简单嘛,根据系统的要求和架构特性,我很快就能写出接口定义,老师你看。“如果方法顺利无异常返回,则说明日志已经被成功写入了日志文件”

接口v0.1版

/**
 * 日志服务
 * @author : xiaoming.xm@cainiao.com
 * @version: 0.1
 */
public interface LogService {

    /**
     * 记录INFO级别日志
     *
     * @param format 日志模版(同String.format())
     * @param args   日志参数
     */
    void info(String format, Serializable... args);

}
苍老师
非常好,但这种接口只能用在单机版的程序中,如果遇到远程调用的场景就不适用了。要了解这个事实,首先大家就要知道远程调用的大概实现原理。 老师

RPC调用

什么是RPC调用

RPC(Remote Procedure Call)远程过程调用,一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的技术实现。

RPC采用C/S模式。请求程序就是一个客户机,而服务提供程序就是一个服务器。首先,客户机调用进程发送一个有进程参数的调用信息到服务进程,然后等待应答信息。在服务器端,进程保持睡眠状态直到调用信息的到达为止。当一个调用信息到达,服务器获得进程参数,计算结果,发送答复信息,然后等待下一个调用信息,最后,客户端调用进程接收答复信息,获得进程结果,然后调用执行继续进行。

以上信息摘录自百度百科

一次完整的RPC调用过程

RPC调用流程

  • 请求过程
  1. 客户端函数将参数传递到客户端句柄
  2. 客户端句柄将请求序号、远程方法、参数等信息封装到请求对象中,并完成请求对象序列化形成请求报文,通过网络客户端发送请求报文。
  3. 请求报文通过网络客户端网络服务端所约定的协议(HTTP、RMI或自定义)进行通讯。
  4. 网络服务端收到请求报文之后,通过反序列化,从请求对象中解析出远程方法、参数等信息,并根据这些信息找到服务器句柄
  5. 通过服务器句柄完成服务器函数的本地调用过程

    自此,整个请求流程完成。

  • 应答过程
  1. 服务器函数执行的过程将结果返回服务器句柄,返回的结果可能是正常返回,也可能是以抛异常的形式返回。
  2. 服务器句柄根据返回的值与请求序号封装到应答对象中,并完成应答对象的序列化,形成应答报文,通过网络服务端发送应答报文。
  3. 应答报文通过网络服户端网络客务端所约定的协议(HTTP、RMI或自定义)进行通讯。
  4. 网络客户端收到应答报文之后,通过反序列化,从应答对象中解析出请求序号所挂钩的客户端句柄
  5. 客户端句柄将返回数据返回到客户端函数,以返回值或抛异常的形式将信息返回

    自此,整个应答流程完成。

苍老师
一次完整的RPC调用一共分10步,每一步都有可能出错,所以在设计一个远程接口的时候必须充分考虑到所有的出错可能,与客户端约定出错的应对方案。无论哪个环节出问题,都要求你的业务逻辑依旧保证不能错乱! 老师

小明
xiaoming_head 不愧是苍老师,果然 博 大精深。我明白了,因为增加了远程访问的因素,所以原本单机中非常小的出错概率就被放大了,这也不得不让程序被迫感知和处理这些通讯错误。

那请问遇到这些错误都应该怎样进行归纳和处理呢?

一次远程调用出错的可能

通讯框架错误

通讯框架错误根据发生环节分可以细分为

  1. Marshell & UnMarshell

    C/S双方采用了不一致的序列化/反序列化算法,导致在通讯之前或之后无法正常取得通讯的对象。从而导致双方在编码、解码的过程中发生错误。

    如果你的通讯框架使用了Hessian那基本上你都有机会遇到过。至于序列化和反序列化的梗,都可以开个专题了。这里就不在啰嗦。

  2. 网络通讯错误

    系统错误会导致无法预测的异常产生,具体取决于RPC的实现方式。对于这种错误,唯一的处理方式只有:另外找时间/机会重试。

业务系统错误

业务系统错误分两种情况

  1. 业务错误

    Client传递了违背业务规则的参数,导致业务逻辑处理失败。这种错误无论重复多少次都会得到一样结局。

  2. 系统错误

    Server处理内部逻辑时出现了无法控制的错误,常见的有:

  • 数据库访问失败
  • 文件写入失败
  • 网络通讯失败

    一般遇到这种错误,可以通过重试解决。

各种出错场景&解决方案梳理

出错情况 解决方案 是否重试
通讯框架错误 抛出框架异常 重试
系统错误 抛出系统异常 重试
业务错误 返回明确的错误码 禁止重试

小明
xiaoming_head 嗯,我了解了,一个好的远程方法定义必须考虑到上边所罗列的异常场景,要求做到明确的错误处理约定。那请问苍老师这个接口应该如何写呢?

苍老师
先别着急,要写出健壮的接口,你还有几个概念要理解。首先我们先来看这个接口的声明。我的比你多了两个重要的信息ResultDO<Void>LogException,接下来我会讲解这定义这两个类的作用 老师

代码组织

如果你有机会重新搭建一个应用,推荐大家采用分包的策略来考虑自己的模块组织。

代码组织结构

  • common:定义core和client所共用的内容

    • 业务接口声明
    • LogService
    • Domain对象(这里为了简单,所有的DO、TO、DTO都统一命名为DO)
    • ResultDO<T>
    • 业务异常
    • LogException
  • client:富客户端,在这一层可以组织cache、业务无关的通用校验,这一次层并非必须。

    • 服务客户端实现
    • LogServiceClient
    • AsyncLogServiceClient
  • core:业务服务的实现,这一层的代码运行在服务端。

    • 服务业务逻辑实现,同时内部按照习惯可以再次分层为(ServiceManagerDao)
    • LogServiceImpl

代码组织图

正确处理返回值

这套RPC接口声明的理念在于:如何通过约定区分出系统异常与业务异常。区分的关键就在于ResultDO<?>LogException

  • ResultDO<T>

    info方法不需要返值,但服务端需要在业务出错的时候,将错误码返回给客户端,以便友好的错误提示。所以在Result对象中有两个方法:

    • public boolean isSuccess();

    isSuccesstrue时表明业务处理成功:当客户端获取到这个值时,表明服务端已正确经接请求到并且成功的处理了这个请求,业务完成。这是最好的情况。

    isSuccessfalse时表明业务处理失败:当客户端获取到时,表明服务端已经正确接到请求,但业务处理失败,失败原因在错误码errorCode中体现。

    • public String getErrorCode();

    当服务端正确接到请求,但业务处理失败时,失败的原因以错误码形式返回。

  • LogException

    这个异常主要用于收缩和屏蔽服务层的具体错误信息,当服务端遇到无法处理的错误情况时,需要继续向客户端外抛,让客户端来择机进行重试。客户端亦可通过LogException快速判断当前业务中断的原因来自于LogService的失败。

客户端对返回值的处理总结

  • 客户端处理逻辑表

    调用情况 isSuccess errorCode throw LogException throw Exception 客户端处理
    框架错误 / / / true 重试
    系统错误 / / true / 重试
    业务错误 false true / / 不重试
    成功返回 true true / / 不重试

    所有情况也不是一层不变。比如业务错误返回错误码,但有时处于性能考虑(抛异常非常消耗JVM性能),可以在接口声明中约定部分错误码也必须要进入重试。但这种场景越少越好,而且一旦做出约定,出于接口向下兼容的考虑,这种需要重试的错误码自声明以来,只能减少不能增加,否则会引起兼容问题。

苍老师
老师也见过有系统在ResultDO中声明了public boolean isReTry();方法,这样当系统发生业务错误的时候,是否重试的判断就交由isReTry()来进行判断,这也是不错的选择。 老师
  • 增加isReTry后的客户端处理逻辑表

    调用情况 isSuccess isReTry errorCode throw LogException throw Exception 客户端处理
    框架错误 / / / / true 重试
    系统错误 / / / true / 重试
    业务错误 false true true / / 重试
    业务错误 false false true / / 不重试
    成功返回 true / true / / 不重试

为什么要有Client

老实说,这一层不是必须的,很多情况下客户端直接使用服务端声明的Service接口足矣。但若遇到在客户端容灾、增强的场景,则ServiceClient的优势就体现出来。

ServiceClient

接口v0.2版

/**
 * 日志服务
 * @author : cangjingkong.cjk@cainiao.com
 * @version: 0.2
 */
public interface LogService {

    /**
     * 记录INFO级别日志
     *
     * @param format 日志模版(同String.format())
     * @param args   日志参数
     * @return 记录日志是否成功
     * @throws LogException 记录日志发生异常
     */
    ResultDO<Void> info(String format, Serializable... args)
            throws LogException;

}
小明
xiaoming_head 一个好的系统约定能减少很多不必要的错误,但毕竟不是所有系统都是新的系统,在面临各种先人的智慧时,如何让不符合约定的远程接口也纳入约定来?

苍老师
在面对先人的智慧时,改变现有被大量调用的接口声明是不可能的,在这种情况下存在即合理,哪怕明知接口声明或实现存在问题,你也不能去变更这个接口。接口维护原则请听下堂课《远程接口维护经验分享》。

当遇到这种不在约定的接口时,需要用装饰模式将不规范的接口包装成为规范的接口。

老师

接口的Wrapper

几乎可以肯定的,在公司中你肯定不是第一个声明接口的人。所以当你定出了远程接口设计规范之后,如何面对老接口则成了一个头疼的问题。

先人的智慧是无穷的,现在我们讨论的问题,我们的前辈都已经面临并解决了(运气不好你可能还会遇到新手练手写的接口),只是解决的方法各种各样,没有形成约定。何解?

此时可以考虑使用装饰模式将不规范的接口重新包装成符合设计规范的接口,这样做有两个好处:

  • 解决老接口不规范问题
  • 减小老接口暴露到业务代码中的概率

    这里需要解释下。外部接口的定义不受控制,如果此时一个Service需要升级,则改动、回归、代码REVIEW范围仅限于Wrapper类即可,若将所有业务代码直接引用外部的Service/ServiceClient类,则升级的回归面将被放大。

所以无论对方声明的接口是否符合约定,我都会建议客户端不要直接使用Service/ServiceClient,而是Wrapper一层。

小明
xiaoming_head 太好了,经过老师提点,我终于写出了一个健壮的远程接口,并知道如何与客户端约定重试的关系。

不过我还是想问问,这种远程的日志系统存在是否不是太合理,老师你举这个例子是不是不太恰当?

滚

PS:本例子的代码可以见 Excavatore-DEMO

该文章来自于阿里巴巴技术协会(ATA)精选文章。

Linux平台下安装SSH

什么是SSH?Secure Shell(缩写为SSH),由IETF的网络工作小组(Network Working Group)所制定;SSH为一项创建在应用层和传输层基础上的安全协议,为计算机上的Shell(壳层)提供安全的传输和使用环境。传统的网络服务程序,如rsh、FTP、POP和Telnet其本质上都是不安全的;因为它们在网络上用明文传送数据、用户帐号和用户口令,很容易受到中间人(man-in-the-middle)攻击方式的攻击。就是存在另一个人或者一台机器冒充真正的服务器接收用户传给服务器的数据,然后再冒充用户把数据传给真正的服务器。而SSH是目前较可靠,专为远程登录会话和其他网络服务提供安全性的协议。利用SSH协议可以有效防止远程管理过程中的信息泄露问题。通过SSH可以对所有传输的数据进行加密,也能够防止DNS欺骗和IP欺骗。SSH之另一项优点为其传输的数据可以是经过压缩的,所以可以加快传输的速度。SSH有很多功能,它既可以代替Telnet,又可以为FTP、POP、甚至为PPP提供一个安全的“通道”。

下面以CentOS平台为例,说明怎么安装ssh服务器。下面为了方便,都是以root用户权限去操作,实际情况下,请使用一般用户权限去使用,只需要加入sudo就行。

[root@localhost ~]# yum install openssh-server
这样就可以安装好ssh-server了。
  检验SSH是否安装好了,可以用下面的命令:

[linuxidc@localhost ~]$ ssh -version
OpenSSH_5.3p1, OpenSSL 1.0.0-fips 29 Mar 2010
Bad escape character ‘rsion’.
如果显示上面的信息,说明安装好了SSH。

  有些用户可能会问,我怎么记地是安装openssh-server而不是直接yum install ssh呢?这里告诉大家一个方法(CentOS平台),可以快速知道我们需要安装程序的名称。如下:

[root@localhost ~]# yum search ssh
Loaded plugins: fastestmirror, refresh-packagekit, security
Loading mirror speeds from cached hostfile
 * base: mirrors.btte.net
 * extras: mirrors.btte.net
 * updates: www.ftp.ne.jp
============================= N/S Matched: ssh =============================
ksshaskpass.x86_64 : A KDE version of ssh-askpass with KWallet support
libssh2.i686 : A library implementing the SSH2 protocol
libssh2.x86_64 : A library implementing the SSH2 protocol
libssh2-devel.i686 : Development files for libssh2
libssh2-devel.x86_64 : Development files for libssh2
libssh2-docs.x86_64 : Documentation for libssh2
openssh.x86_64:An open source implementation of SSH protocol versions 1and 2
openssh-askpass.x86_64 : A passphrase dialog for OpenSSH and X
openssh-clients.x86_64 : An open source SSH client applications
openssh-ldap.x86_64 : A LDAP support for open source SSH server daemon
openssh-server.x86_64 : An open source SSH server daemon
pam_ssh_agent_auth.i686 : PAM module for authentication with ssh-agent
pam_ssh_agent_auth.x86_64 : PAM module for authentication with ssh-agent
trilead-ssh2.noarch : SSH-2 protocol implementation in pure Java
trilead-ssh2-javadoc.noarch : Javadoc for trilead-ssh2
jsch.noarch : Pure Java implementation of SSH2
python-paramiko.noarch : A SSH2 protocol library for python
python-twisted-conch.x86_64 : SSH and SFTP protocol implementation
together with clients and servers

  Name and summary matches only, use “search all” for everything.
从上面我们可以看到openssh-server,这就是我们需要安装的程序名称。
如果你是Ubuntu平台呢?没关系,可以运行下面的命令,也可以达到上面一样的效果

[root@localhost ~]# apt-get update //这行可以不要
[root@localhost ~]# apt-cache search ssh
  下面我们列出ssh服务sshd在各个执行等级的启动情况:

[root@localhost ~]# chkconfig –list sshd
sshd      0:off    1:off    2:off    3:off    4:off    5:off    6:off
手动启动sshd服务(需要root权限),命令如下:

[root@localhost ~]# /etc/init.d/sshd start
Generating SSH1 RSA host key:                              [  OK  ]
Generating SSH2 RSA host key:                              [  OK  ]
Generating SSH2 DSA host key:                              [  OK  ]
Starting sshd:                                            [  OK  ]
这样可以在putty或别的客户端登录到这台Linux机器了。如果需要停止ssh服务可以运行下面的命令

[root@localhost ~]# /etc/init.d/sshd stop
Stopping sshd:                                            [  OK  ]

相关阅读:

Ubuntu 下配置 SSH服务全过程及问题解决 http://www.linuxidc.com/Linux/2011-09/42775.htm

Ubuntu 12.04下安装Git,SSH及出现的Permission denied解决办法 http://www.linuxidc.com/Linux/2013-06/85336.htm

Ubuntu 12.10下OpenSSH的离线安装方法 http://www.linuxidc.com/Linux/2013-04/82814.htm

Ubuntu下SSH安装或设置 http://www.linuxidc.com/Linux/2013-07/87368.htm

更多CentOS相关信息见CentOS

Java 简单的Java Socket程序

//服务器端:ServerDemo.java

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerDemo
{
public static void main(String[] args)
{
ServerSocket ss = null;
try {
ss = new ServerSocket(8888);
        //服务器接收到客户端的数据后,创建与此客户端对话的Socket
        Socket socket = ss.accept();
       
        //用于向客户端发送数据的输出流
        DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
       
        //用于接收客户端发来的数据的输入流
        DataInputStream dis = new DataInputStream(socket.getInputStream());
       
        System.out.println(“服务器接收到客户端的连接请求:” + dis.readUTF());
       
        //服务器向客户端发送连接成功确认信息
        dos.writeUTF(“接受连接请求,连接成功!”);
       
        //不需要继续使用此连接时,关闭连接
        socket.close();
        ss.close();
} catch (IOException e){
  e.printStackTrace();
  }
  }
}

//客户端:ClientDemo.java

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
public class ClientDemo {
  public static void main(String[] args)
  {
  Socket socket = null;
  try {
      socket = new Socket(“localhost”,8888);
           
            //获取输出流,用于客户端向服务器端发送数据
            DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
           
            //获取输入流,用于接收服务器端发送来的数据
            DataInputStream dis = new DataInputStream(socket.getInputStream());
           
            //客户端向服务器端发送数据
            dos.writeUTF(“我是客户端,请求连接!”);
           
            //打印出从服务器端接收到的数据
            System.out.println(dis.readUTF());
           
            //不需要继续使用此连接时,记得关闭哦
            socket.close();
  } catch (UnknownHostException e)
      {
      e.printStackTrace();
      } catch (IOException e)
        {
      e.printStackTrace();
        }
  }
}

运行,开两个CMD窗口,分别运行:java ServerDemo和java ClientDemo

运行结果如下:

java ServerDemo

服务器接收到客户端的连接请求:我是客户端,请求连接!

java ClientDemo

接受连接请求,连接成功!

Linux Socket 编程I/O Mutiplexing poll 和 epoll  http://www.linuxidc.com/Linux/2014-03/97443.htm

Linux Socket 编程I/O Multiplexing http://www.linuxidc.com/Linux/2014-03/97442.htm

Socket网络编程-Mina 实例 http://www.linuxidc.com/Linux/2014-02/96881.htm

Lua加入Socket库支持 http://www.linuxidc.com/Linux/2013-12/93517.htm

Python编写的Socket服务器和客户端 http://www.linuxidc.com/Linux/2013-08/89299.

Java JDBC高级特性

1、JDBC批处理

实际开发中需要向数据库发送多条SQL语句,这时,如果逐条执行SQL语句,效率会很低,因此可以使用JDBC提供的批处理机制。Statement和PreparedStatemen都实现了批处理。测试表结构如下:

Statement批处理程序示例

package server;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;

import com.mysql.jdbc.PreparedStatement;

public class DemoJDBC {
    public static void main(String[] args) throws Exception {
        // 加载驱动类
        Class.forName(“com.mysql.jdbc.Driver”);
   
        // 通过DriverManager获取数据库连接
        String url = “jdbc:mysql://192.168.1.150/test”;
        String user = “teamtalk”;
        String password = “123456”;
        Connection connection = (Connection) DriverManager.getConnection(
                url, user, password);
       
        String sql1 = “DROP TABLE IF EXISTS people”;
        String sql2 = “CREATE TABLE people(id int, name varchar(20))”;
        String sql3 = “INSERT people VALUES(2, ‘hdu’)”;
        String sql4 = “UPDATE people SET id = 1”;
        Statement statement = (Statement) connection.createStatement();
        statement.addBatch(sql1);
        statement.addBatch(sql2);
        statement.addBatch(sql3);
        statement.addBatch(sql4);
        statement.executeBatch();
       
        ResultSet resultSet = statement.executeQuery(“SELECT * from people”);
        while (resultSet.next()) {
            System.out.println(resultSet.getString(1) + ” ” + resultSet.getString(2));
        }
    }
}

PreparedStatement批处理

package server;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;

import com.mysql.jdbc.PreparedStatement;

public class DemoJDBC {
    public static void main(String[] args) throws Exception {
        // 加载驱动类
        Class.forName(“com.mysql.jdbc.Driver”);
   
        // 通过DriverManager获取数据库连接
        String url = “jdbc:mysql://192.168.1.150/test”;
        String user = “teamtalk”;
        String password = “123456”;
        Connection connection = (Connection) DriverManager.getConnection(
                url, user, password);
       
        PreparedStatement statement =  (PreparedStatement) connection.prepareStatement(“INSERT people VALUES(?,?)”);
        for (int i = 1; i < 4; i++) {
            statement.setInt(1, i);
            statement.setString(2, “hdu” + i);
            statement.addBatch();
        }
        statement.executeBatch();
       
        ResultSet resultSet = statement.executeQuery(“SELECT * from people”);
        while (resultSet.next()) {
            System.out.println(resultSet.getString(1) + ” ” + resultSet.getString(2));
        }
    }
}

2、JDBC处理事务

针对JDBC处理事务的操作,在Connection接口中,提供了3个相关的方法,具体如下:

1 setAutoCommit(boolean autoCommit); // 设置是否自动提交事务
2 commit(); // 提交事务
3 rollback(); // 撤销事务

将setAutoCommit()方法参数设置为false后,事务必须使用conn.commit()方法提交,而事务回滚不一定显式执行conn.rollback()。如果程序最后没有执行conn.commit(),事务也会回滚,一般是直接抛出异常,终止程序的正常执行。因此,通常情况下,会conn.rollback()语句放在catch语句块执行。

package demo.jdbc;

import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;

import com.mysql.jdbc.Connection;
import com.mysql.jdbc.PreparedStatement;
import com.mysql.jdbc.Statement;

public class FirstJDBC {
    public static void main(String[] args) throws ClassNotFoundException, SQLException, InterruptedException {
        Connection connection = null;
        try {
            // 加载驱动类
            Class.forName(“com.mysql.jdbc.Driver”);
       
            // 通过DriverManager获取数据库连接
            String url = “jdbc:mysql://192.168.1.150/test”;
            String user = “teamtalk”;
            String password = “123456”;
            connection = (Connection) DriverManager.getConnection(
                    url, user, password);
            // 关闭事务的自动提交
            connection.setAutoCommit(false);
           
            Statement statement = (Statement) connection.createStatement();
            PreparedStatement statement1 = (PreparedStatement) connection.prepareStatement(“INSERT people VALUES(?, ?)”);
            PreparedStatement statement2 = (PreparedStatement) connection.prepareStatement(“INSERT people VALUES(?, ?)”);
           
            statement1.setInt(1, 1);
            statement1.setString(2, “hdu1”);
            statement2.setInt(1, 2);
            statement2.setString(2, “hdu2”);
           
            statement1.executeUpdate();
            statement2.executeUpdate();
           
            ResultSet resultSet = statement.executeQuery(“SELECT * from people”);
            while (resultSet.next()) {
                System.out.println(resultSet.getString(1) + ” ” + resultSet.getString(2));
            }
        }
        catch (Exception e) {
            // 回滚事务
            connection.rollback();
            e.printStackTrace();
        }
    }
}

更多详情

Java安全管理器

Java安全管理器处理必要的权限检查。缺省的实现是Java.lang.SecurityManager类中,如有必要可以派生出子类或取代之。对于不安全的行为,Java就会强制安全管理器进行安全检查。

安全管理器是如何工作的:

你的代码调用需要权限的代码,如new FileInputStream(“filename”)
那代码就会调用安全管理器检查权限
安全管理器决定权限是否允许。这通常(而不是必须的)会与java.security.AccessController交互。如果没有权限,就会出现运行异常:java.lang.SecurityException,否则不会返回异常而正常运行。安全管理器会监督整个执行线程已确保每个类都有相应权限。
如果没有异常,说明有足够的权限,程序将继续正常运行。
设置安全管理器有2个常用的方法

执行命令    java -Djava.security.manager FileWriteTest
程序中指定
           SecurityManager security = new SecurityManager();
           System.setSecurityManager(security);

Java.policy文件

    某些权限必须赋予类,这是在java.policy文件中定义,java.policy文件保存在目录$JAVA_HOME/jre/lib/security中。

Java中的权限

    Java本身定义了许多权限,顶层类包括:

java.security.AllPermission—对所有文件都有的全部权限
java.security.BasicPermission—大多数简单权限的超类
java.io.FilePermission—文件读写权限
java.net.SocketPermission—打开连接远程计算机的包,并用DNS解析主机名
BasicPermission有许多派生类定义其他权限

javax.sound.sampled.AudioPermission—访问音频函数的权限,如播放、录音等。
javax.awt.AWTPermission—与图形有关的行为的权限,比如访问剪贴板或读AWT事件。
java.net.NetPermission—处理URL的权限,必然增加一个口令认证。
java.util.PropertyPermission—读写系统属性的权限。
java.lang.reflect.ReflectPermission—使用reflection的权限。
java.lang.RuntimePermission—运行时安全元素的权限,比如设置类装载器或安全管理器。
java.security.SecurityPermission—安全元素的权限,像安