面试基本概念原理最佳实践

1) Java中什么是竞态条件? 举个例子说明。

竞态条件会致使程序在并发状况下出现一些bugs。多线程对一些资源的竞争的时候就会产生竞态条件,若是首先要执行的程序竞争失败排到后面执行了, 那么整个程序就会出现一些不肯定的bugs。这种bugs很难发现并且会重复出现,由于线程间的随机竞争。一个例子就是无序处理html

竞态条件(Race Condition):计算的正确性取决于多个线程的交替执行时序时,就会发生竞态条件。java

最多见的竞态条件为:mysql

一,先检测后执行。执行依赖于检测的结果,而检测结果依赖于多个线程的执行时序,而多个线程的执行时序一般状况下是不固定不可判断的,从而致使执行结果出现各类问题。linux

对于main线程,若是文件a不存在,则建立文件a,可是在判断文件a不存在以后,Task线程建立了文件a,这时候先前的判断结果已经失效,(main线程的执行依赖了一个错误的判断结果)此时文件a已经存在了,可是main线程仍是会继续建立文件a,致使Task线程建立的文件a被覆盖、文件中的内容丢失等等问题。c++

 

多线程环境中对同一个文件的操做要加锁。面试

和大多数并发错误同样,竞态条件不老是会产生问题,还须要不恰当的执行时序 算法

 

2)Java中有哪些实现并发编程的方法

1. synchronized关键字spring

2. 使用继承自Object类的wait、notify、notifyAll方法
3. 使用线程安全的API和集合类:sql

  • 使用Vector、HashTable等线程安全的集合类
  • 使用Concurrent包中提供的ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentLinkedQueue等弱一致性的集合类
  • 在Collections类中有多个静态方法,它们能够获取经过同步方法封装非同步集合而获得的集合,如List list = Collection.synchronizedList(new ArrayList())。

4. 使用原子变量、volatile变量等
5. 使用Concurrent包中提供的信号量Semaphore、闭锁Latch、栅栏Barrier、交换器Exchanger、Callable&Future、阻塞队列BlockingQueue等.
6. 手动使用Lock实现基于锁的并发控制
7. 手动使用Condition或AQS实现基于条件队列的并发控制
8. 使用CAS和SPIN等实现非阻塞的并发控制数据库

  

3)进程间通讯的几种方式

1. 管道( pipe ):管道是一种半双工的通讯方式,数据只能单向流动,并且只能在具备亲缘关系的进程间使用。进程的亲缘关系一般是指父子进程关系。

2. 有名管道 (named pipe) : 有名管道也是半双工的通讯方式,可是它容许无亲缘关系进程间的通讯。

3. 信号量( semophore ) : 信号量是一个计数器,能够用来控制多个进程对共享资源的访问。它常做为一种锁机制,防止某进程正在访问共享资源时,其余进程也访问该资源。所以,主要做为进程间以及同一进程内不一样线程之间的同步手段。

4. 消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

5. 信号 ( sinal ) : 信号是一种比较复杂的通讯方式,用于通知接收进程某个事件已经发生。

6. 共享内存( shared memory ) :共享内存就是映射一段能被其余进程所访问的内存,这段共享内存由一个进程建立,但多个进程均可以访问。共享内存是最快的 IPC 方式,它是针对其余进程间通讯方式运行效率低而专门设计的。它每每与其余通讯机制,如信号两,配合使用,来实现进程间的同步和通讯。

7. 套接字( socket ) : 套解口也是一种进程间通讯机制,与其余通讯机制不一样的是,它可用于不一样机器间的进程通讯。

 

 

4)ConcurrentHashMap

ConcurrentHashMap是线程安全的HashMap,内部采用分段锁来实现,默认初始容量为16,装载因子为0.75f,分段16,每一个段的HashEntry<K,V>[]大小为2。键值都不能为null。每次扩容为原来容量的2倍,ConcurrentHashMap不会对整个容器进行扩容,而只对某个segment进行扩容。在获取size操做的时候,不是直接把全部segment的count相加就能够可到整个ConcurrentHashMap大小,也不是在统计size的时候把全部的segment的put, remove, clean方法所有锁住,这种方法过低效。在累加count操做过程当中,以前累加过的count发生变化的概率很是小,全部ConcurrentHashMap的作法是先尝试2(RETRIES_BEFORE_LOCK)次经过不锁住Segment的方式统计各个Segment大小,若是统计的过程当中,容器的count发生了变化,再采用加锁的方式来统计全部的Segment的大小。

 

5)ABA问题

ABA问题发生在相似这样的场景:线程1转变使用CAS将变量A的值替换为C,在此时,线程2将变量的值由A替换为C,又由C替换为A,而后线程1执行CAS时发现变量的值仍为A,因此CAS成功。但实际上这时的现场已经和最初的不一样了。大多数状况下ABA问题不会产生什么影响。若是有特殊状况下因为ABA问题致使,可用采用AtomicStampedReference来解决,原理:乐观锁+version。能够参考下面的案例来了解其中的不一样

 1 public class ABAQuestion
 2 {
 3     private static AtomicInteger atomicInt = new AtomicInteger(100);
 4     private static AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<Integer>(100,0);
 5  
 6     public static void main(String[] args) throws InterruptedException
 7     {
 8         Thread thread1 = new Thread(new Runnable(){
 9             @Override
10             public void run()
11             {
12                 atomicInt.compareAndSet(100, 101);
13                 atomicInt.compareAndSet(101, 100);
14             }
15         });
16  
17         Thread thread2 = new Thread(new Runnable(){
18             @Override
19             public void run()
20             {
21                 try
22                 {
23                     TimeUnit.SECONDS.sleep(1);
24                 }
25                 catch (InterruptedException e)
26                 {
27                     e.printStackTrace();
28                 }
29                 boolean c3 = atomicInt.compareAndSet(100, 101);
30                 System.out.println(c3);
31             }
32         });
33  
34         thread1.start();
35         thread2.start();
36         thread1.join();
37         thread2.join();
38  
39         Thread thread3 = new Thread(new Runnable(){
40             @Override
41             public void run()
42             {
43                 try
44                 {
45                     TimeUnit.SECONDS.sleep(1);
46                 }
47                 catch (InterruptedException e)
48                 {
49                     e.printStackTrace();
50                 }
51                 atomicStampedRef.compareAndSet(100, 101, atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1);
52                 atomicStampedRef.compareAndSet(101, 100, atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1);
53             }
54         });
55  
56         Thread thread4 = new Thread(new Runnable(){
57             @Override
58             public void run()
59             {
60                 int stamp = atomicStampedRef.getStamp();
61                 try
62                 {
63                     TimeUnit.SECONDS.sleep(2);
64                 }
65                 catch (InterruptedException e)
66                 {
67                     e.printStackTrace();
68                 }
69                 boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp+1);
70                 System.out.println(c3);
71             }
72         });
73         thread3.start();
74         thread4.start();
75     }
76 }
AtomicStampedRefrence

输出结果:

true
false
View Code

 

 

6)servlet是线程安全的吗

Servlet不是线程安全的。要解释为何Servlet为何不是线程安全的,须要了解Servlet容器(即Tomcat)是如何响应HTTP请求的。

当Tomcat接收到Client的HTTP请求时,Tomcat从线程池中取出一个线程,以后找到该请求对应的Servlet对象并进行初始化,以后调用service()方法。要注意的是每个Servlet对象在Tomcat容器中只有一个实例对象,便是单例模式。若是多个HTTP请求请求的是同一个Servlet,那么着两个HTTP请求对应的线程将并发调用Servlet的service()方法。

 
上图中的Thread1和Thread2调用了同一个Servlet1,因此 此时若是Servlet1中定义了实例变量或静态变量,那么可能会发生线程安全问题(由于全部的线程均可能使用这些变量)

好比下面的Servlet中的 name 和 i变量就会引起线程安全问题。

 

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class ThreadSafeServlet extends HttpServlet {

    public static String name = "Hello";   //静态变量,可能发生线程安全问题
    int i;  //实例变量,可能发生线程安全问题
    SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

    @Override
    public void init() throws ServletException {
        super.init();
        System.out.println("Servlet初始化");
    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.printf("%s:%s[%s]\n", Thread.currentThread().getName(), i, format.format(new Date()));
        i++;
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.printf("%s:%s[%s]\n", Thread.currentThread().getName(), i, format.format(new Date()));
        resp.getWriter().println("<html><body><h1>" + i + "</h1></body></html>");
    }
}

 



9)linux/unix下统计一行字符串出现指定字符的次数

一种方案: 先把 | 替换成换行符,wc -l 统计时候的的数量就必定是result + 1   最后把1减掉

unix系统用 sed 's/|/\n/g' 没法替换(linux 能够),因此考虑用tr替换

echo $(echo "10.1.4.90|15/Jan/2018:08:44:38|GET|pi.easyond/|" | tr '|' '\n' | wc -l)-1 | bc  

 

 

10)快速失败(fail - fast)和安全失败(fail - safe)

一 快速失败 fail-fast

 在用迭代器遍历一个集合对象时,若是遍历过程当中对集合对象的内容进行了修改(增长,删除,修改), 则会抛出 ConcurrentModificationException

原理: 迭代器在遍历时直接访问集合中的内容,而且在遍历过程当中使用一个modCount变量。集合在被遍历期间若是内容发生变化,就会改变modCount的值,每当迭代器使用hasNext()/next()遍历下一个元素以前,都会检测modCount变量是否为expectedModCount值,是的话就返回遍历,不然抛出异常,终止遍历。

下面以AbstractList的实现举例,其余原理同,再也不累述

注意:这里异常的抛出条件是检测到this.modCount != l.modCount, 若是集合发生变化时又刚好修改了modCount的值使上述条件语句知足,那么异常不会抛出。所以,不能依赖这个异常是否抛出的逻辑进行并发操做的编程,这个异常只建议用于检测并发修改的bug

场景 java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程当中被修改)

 

二 安全失败 fail-safe

采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历

原理: 因为迭代时时对原集合的拷贝进行遍历,因此在遍历过程当中对原集合所作的修改并不能被迭代器检测到,因此不会触发ConcurrentModificationException

缺点: 基于拷贝内容的优势是避免了ConcurrentModificationException,但相对地,迭代器并不能访问到修改后的内容。即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器无从得知。

场景: java.util.concurrent包下的容器都是安全失败,能够在多线程下并发使用,并发修改。

 

 

11)mysql的undo和redo

http://www.cnblogs.com/Bozh/archive/2013/03/18/2966494.html
 
 
 

12)cookie和session

http://blog.csdn.net/gavin_john/article/details/51376364
 
 

13)网站短连接生成算法

https://www.zhihu.com/question/29270034
 
 
 

14)linux下kill -9 pid强制不能杀掉进程缘由 

kill -9发送SIGKILL信号将其终止,可是如下两种状况不起做用:
a、该进程处于"Zombie"状态(使用ps命令返回defunct的进程)。此时进程已经释放全部资源,但还未获得其父进程的确认。"zombie"进程要等到下次重启时才会消失,但它的存在不会影响系统性能。
b、 该进程处于"kernel mode"(核心态)且在等待不可得到的资源。处于核心态的进程忽略全部信号处理,所以对于这些一直处于核心态的进程只能经过重启系统实现。进程在AIX 中会处于两种状态,即用户态和核心态。只有处于用户态的进程才能够用“kill”命令将其终止。 

用top命令查看发现zombie进程数是0,看来这三个进程不属于僵尸进程,应该是b这中状况,就是这些进程进入核心态等待磁盘资源时出现磁盘空间不足的故障,这时我强制关闭了数据库,因此这几个进程就一直处于核心态没法被杀除,看来只能重启了。
 
 

15)给定a,b两个文件,各存放50亿个url,每一个url各占用64字节,内存限制是4G,若是找出a,b文件共同的url

 思路:能够估计每一个文件的大小为5G*64=300G,远大于4G。因此不可能将其彻底加载到内存中处理。考虑采起分而治之的方法。 遍历文件a,对每一个url求取hash(url)%1000,而后根据所得值将url分别存储到1000个小文件(设为a0,a1,...a999)当中。这样每一个小文件的大小约为300M。遍历文件b,采起和a相同的方法将url分别存储到1000个小文件(b0,b1....b999)中。这样处理后,全部可能相同的url都在对应的小文件(a0 vs b0, a1 vs b1....a999 vs b999)当中,不对应的小文件(好比a0 vs b99)不可能有相同的url。而后咱们只要求出1000对小文件中相同的url便可。 好比对于a0 vs b0,咱们能够遍历a0,将其中的url存储到hash_map当中。而后遍历b0,若是url在hash_map中,则说明此url在a和b中同时存在,保存到文件中便可。 若是分红的小文件不均匀,致使有些小文件太大(好比大于2G),能够考虑将这些太大的小文件再按相似的方法分红小小文件便可

 

 

16)100亿个url中的重复url已经搜索词汇的topK问题

有一个包含100亿个URL的文件,假设每一个URL占用64B,请找出其中全部重复的URL。

将文件经过哈希函数成多个小的文件,因为哈希函数全部重复的URL只可能在同一个文件中,在每一个文件中利用一个哈希表作次数统计。就能找到重复的URL。这时候要注意的就是给了多少内存,咱们要根据文件大小结合内存大小决定要分割多少文件

topK问题和重复URL实际上是同样的,重复的多了才会变成topK。其实就是在上述方法后得到全部的重复URL排个序,可是有点不必,由于咱们要找topK时,最极端的状况也就是topK在用一个文件中,因此咱们只须要每一个文件的topK个URL,以后再进行排序,这样就比找出所有的URL再排序好。还有一个topK个URL到最后仍是须要排序,因此咱们在找每一个文件的topK时,是否只须要找到topK个,其它顺序不用管,那么咱们就能够用大小为K的小根堆遍历哈希表。这样又能够下降查找的时间。

 

17)如何判断服务器须要扩容

有不少指标,最基本的好比CPU利用率,内存占用率,网络带宽等。软件层面好比QPS等。

容量设计的时候须要考虑突发流量,通常要保留20%到30%的备用容量。

集群还要考虑灾备,好比部分机器发生故障的时候接负载均衡如何正确引流限流,避免雪崩式故障(cascading failure)。

至于工具的话,要看现有的技术框架。主要的目的就是作好实时监控和预警。

18)linux下如何查看进行占用CPU和内存的状况

1 ps 

按内存大小排序(%MEM为第三列,%CPU为第二列,不在雷述)

ps -aux默认是按PID从小到大显示的,若想按占用内存大小排序则须要借助另外sort命令针对第4列(内存所在列)排序:

ps -aux | sort -k4rn
 
  

咱们还能够借助awk来指定显示哪几列信息:

ps -aux |awk '{print $2, $4, $11}' | sort -k2rn | head -n 10
 
  

2 top 

在命令行提示符执行top命令.输入大写P,则结果按CPU占用降序排序。输入大写M,结果按内存占用降序排序

  

20)Arrays.sort() 和Collections.sort() 底层是用什么排序

1 Arrays.sort()

public static void sort(int[] a) {
    DualPivotQuicksort.sort(a, 0, a.length - 1, null, 0, 0);
}

DualPivotQuicksort翻译过来就是双轴快速排序,再点进去发现有这样的判断

if (right - left < QUICKSORT_THRESHOLD) {
    sort(a, left, right, true);
    return;
}

能够发现若是数组的长度小于QUICKSORT_THRESHOLD的话就会使用这个双轴快速排序,而这个值是286。

 那若是大于286,它会检测数组的连续上升/降低的特性好很差,好的话用归并排序,坏的话用快排

再回到上面的双轴快速排序,点进去

sort(int[] a, int left, int right, boolean leftmost)这个方法发现
若是数据长度小于(值为47)的话,就会用插入排序INSERTION_SORT_THRESHOLD

总结:

长度大于等于286且连续性好->归并

长度大于等于286且连续性很差->双轴快速排序

长度小于286且大于等于47->双轴快速排序

长度小于47->插入排序

 

2 Collections.sort()

一路点进去发现到了Arrays类中

public static <T> void sort(T[] a, Comparator<? super T> c) {
    if (c == null) {
        sort(a);
    } else {
        if (LegacyMergeSort.userRequested)
            legacyMergeSort(a, c);
        else
            TimSort.sort(a, 0, a.length, c, null, 0, 0);
    }
}

会发现若是LegacyMergeSort.userRequested为true的话就会使用归并排序,能够经过下面代码设置为true

System.setProperty("java.util.Arrays.useLegacyMergeSort", "true"); 

不过方法legacyMergeSort的注释上有这么一句话,说明之后传统归并可能会被移除了。

/** To be removed in a future release. */

若是不为true的话就会用一个叫TimSort的排序算法。

 

 

21)Object中有哪些方法

public方法: getClass()  hashCode()  equals()  toString()  notify()系列  wait()系列

protected方法: clone() finalize()

private方法: registerNatives, 该方法做用是将不一样平台c/c++实现的方法映射到java的native方法

    private static native void registerNatives();
    static {
        registerNatives();
    }

 

 

 

22)jvm GC根节点的选择

java经过可达性分析来判断对象是否存活,基本思想是经过一系列称为"GC roots"的对象做为起始点,能够做为根节点的是:

(1) 虚拟机栈(栈帧中的本地变量表)中引用的对象

(2) 本地方法栈中JNI(即native方法)引用的对象

(3) 方法区中静态属性引用的对象

(4) 方法区中常量引用的对象

做为GC roots的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表)中。

我我的理解: 想下java内存模型的5块,gc主要是收集堆和持久代,刚好对应栈分配的堆对象和静态&常量所申请的持久代

 

 

23)类加载主要步骤

  • 加载 把 class 文件的二进制字节流加载到 jvm 里面
  • 验证 确保 class 文件的字节流包含的信息符合当前 jvm 的要求 有文件格式验证, 元数据验证, 字节码验证, 符号引用验证等
  • 准备 正式为类变量分配内存并设置类变量初始值的阶段, 初始化为各数据类型的零值
  • 解析 把常量值内的符号引用替换为直接引用的过程
  • 初始化 执行类构造器()方法
  • 使用 根据相应的业务逻辑代码使用该类
  • 卸载 类从方法区移除

 

24)Class.forName和classloader的区别

java中class.forName()和classLoader均可用来对类进行加载。
class.forName()前者除了将类的.class文件加载到jvm中以外,还会对类进行解释,执行类中的static块。
而classLoader只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。 

 

25)获取class类的四种方式

1.调用运行时类自己的.class属性

Class clazz = String.class;

2,经过运行时类的对象获取

Person p = new Person();

Class clazz = p.getClass();

3.经过Class的静态方法获取:体现反射的动态性

String className = “java.util.commons”;

Class clazz = Class.forName(className);

4.经过类的加载器

String className = “java.util.commons”;

ClassLoader classLoader = this.getClass().getClassLoader();

Class clazz = classLoader.loadClass(className);

 

 

26)String为何是final的

final的出现是为了避免想改变,而不想改变的理由有两点: 设计(安全)或者效率

1 设计安全

确保不会在子类中改变语义。若是有一个String的引用,它引用的必定是一个string对象,而不多是其余类的对象

2 效率

设计成final,jvm不用对相关方法在虚函数表中查询,而直接定位到String类的相关方法,提升了执行效率

 一旦建立是不能被修改的。字符串对象是不可改变的,那么它们能够共享

 

27)kafka等mq如何保证有序性

传统的队列在服务器上保存有序的消息,若是多个consumers同时从这个服务器消费消息,服务器就会以消息存储的顺序向consumer分发消息。虽然服务器按顺序发布消息,可是消息是被异步的分发到各consumer上,因此当消息到达时可能已经失去了原来的顺序,这意味着并发消费将致使顺序错乱。为了不故障,这样的消息系统一般使用“专用consumer”的概念,其实就是只容许一个消费者消费消息,固然这就意味着失去了并发性。

在这方面Kafka作的更好,经过分区的概念,Kafka能够在多个consumer组并发的状况下提供较好的有序性和负载均衡。将每一个分区分只分发给一个consumer组,这样一个分区就只被这个组的一个consumer消费,就能够顺序的消费这个分区的消息。由于有多个分区,依然能够在多个consumer组之间进行负载均衡。注意consumer组的数量不能多于分区的数量,也就是有多少分区就容许多少并发消费。

Kafka只能保证一个分区以内消息的有序性,在不一样的分区之间是不能够的,这已经能够知足大部分应用的需求。若是须要topic中全部消息的有序性,那就只能让这个topic只有一个分区,固然也就只有一个consumer组消费它。

 

28)java对象引用方式 - 强引用,软引用,弱引用,虚引用

从jdk1.2版本开始,把对应的引用分为4种级别,由高到低分别为: 强引用 > 软引用 > 弱引用 > 虚引用.

(1) 强引用

这是最广泛的引用。当内存空间不足时,java虚拟机宁愿抛出OutOfMemoryError使程序异常终止,也不会随意回收具备强引用的对象来解决内存不足问题

(2) 软引用

软引用在内存空间足够时,垃圾回收器不会进行回收。但内存不足时就会进行回收这些对象的内存。软引用可用来实现内存敏感的高速缓存

(3) 弱引用

具备比软引用更短的生命周期,在垃圾回收器线程扫描它所管辖的内存区域时,一旦有弱引用,无论当前内存空间足够与否,都会回收它的内存。不过,因为垃圾回收器时一个优先级很低的线程,所以不必定会发现那些只具备弱引用的对象

(4)虚引用

顾名思义,就是形同虚设,与其余几种引用都不一样,虚引用并不会决定对象的生命周期。若是一个对象仅持有虚引用,那么它就和没有任何引用同样,在任什么时候候均可能被垃圾回收。

 

特别注意,在程序设计中通常不多使用弱引用与虚引用,使用软引用的状况较多,这是由于软引用能够加速JVM对垃圾内存的回收速度,能够维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。

 

 

100)其余

(2) spring中的九种设计模式:  http://ju.outofmemory.cn/entry/78206
(3) 关于MapReduce的理解?  https://www.zhihu.com/question/23345991/answer/223113502
  (5) 美团点评技术团队-swap太高分析linux与jvm的内存关系(写的灰常好)    http://www.importnew.com/14486.html
(6) happens-before在java源码中的应用(CopyOnWriteArrayList & FutureTask)  http://ifeve.com/easy-happens-before/
  (7) 全局惟一自增id生成策略   http://www.javashuo.com/article/p-qchoxqve-cc.html
(8) Semaphore,能够整理一下,没什么难度,主要是流量控制   https://www.jianshu.com/p/0090341c6b80
(10) hashmap1.7&1.8     http://www.importnew.com/20386.html
(12) 红黑树                   http://blog.csdn.net/u011240877/article/details/53329023
  (14) 删除list的元素的各类办法对比  http://blog.csdn.net/claram/article/details/53410175  
(15)逃逸分析   http://www.importnew.com/27262.html

(16)分类清晰的简单面试题 https://juejin.im/entry/5838f9f1128fe1006bdaa456  

(17)tcp粘包  http://www.javashuo.com/article/p-otqxlujw-cd.html

(18)lru cache的算法实现 http://www.javashuo.com/article/p-tvkmduth-bv.html

dubbo。http://wyj.shiwuliang.com/2017/02/25/Dubbo%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90%20%E2%80%94%20%E6%9C%8D%E5%8A%A1%E6%B3%A8%E5%86%8C/

(19)单点登陆sso  http://www.cnblogs.com/ywlaker/p/6113927.html