在多线程中最为复杂和最为重要的就是线程安全。多个线程访问同一个对象的时候会导致线程安全问题。通过加锁可以避免这种问题。但是在串行执行的过程中又不用考虑线程安全问题,而使用串行程序效率低没有办法将CPU的利用率提升到最大。所以还要使用多线程并行执行,既然提到了多线程就必须面对线程安全问题。

共享资源

  在之前的博客中曾经提到过一个问题,就是JVM的内存模型,在内存模型中我们知道除了堆内存和方法区内存是被所有线程共同使用的,其他的程序计数器、JVM虚拟栈、本地方法栈等等资源都是每个线程独立的资源。那么什么叫做共享资源呢?从这个角度上看看堆内存和方法区内存可以被称为是所有线程共享的资源。也就是说共享资源是能同时被多个线程能共同操作资源。但是一个新的问题就来了,怎么保证多线程访问共享资源的数据一致性问题。解决这个问题就被称为是数据同步或者说是数据共享。

数据同步

数据一致性

  在系列博客一中,模拟了一个火车售票系统。设置一共有50张车票。会发现最后在控制台输出的内容其实并不是我们希望看到的结果。

public class TicketRunable implements Runnable {private int index = 1;private final static int MAX = 50;@Overridepublic void run() {while (index<=MAX){System.out.println(Thread.currentThread()+" 的号码是 :"+index++);}}public static void main(String[] args) {final TicketRunable ticketRunable = new TicketRunable();Thread thread1 = new Thread(ticketRunable,"一");Thread thread2 = new Thread(ticketRunable,"二");Thread thread3 = new Thread(ticketRunable,"三");Thread thread4 = new Thread(ticketRunable,"四");thread1.start();thread2.start();thread3.start();thread4.start();}
}

运行上面的这段代码我们会发现,每次运行的结果都是不一样的,但是总结一下主要是由三个问题

  • 第一,在程序中某一个编号从来没有出现过
  • 第二,在运行过程中有些号码重复出现
  • 第三,到程序结束的时候会出现一些号码超过50这个最大值。
数据不一致的原因分析
有些号码没有被显示

  在多线程运行过程中如果A线程和B线程同时执行,此时A线程开始获得index=34,如果这个时候线程B已经获取到了index=35,这个时候CPU将线程调度权交给了线程A这个时候线程A获取到的index的值就是35,直接进行加一操作之后直接输出,这样的话就会导致34这个号码被忽略了。

号码重复出现

  线程A执行index+1的操作,然后CPU将执行权交给了线程B,而这个时候线程A对于index的赋值操作并没有结束,而线程B拿到的index还是原来的值,然后执行index+1的操作,而在此时线程A获取到了CPU的调度权,这个时候继续执行index+1的操作,就会导致某个值重复出现

超出范围

  在线程A快要结束的时候获取到了index+1的操作此时,线程B也到了最后结束的时候,但是两个线程都拿到了满足条件的49,这个时候线程A先获取CPU资源进行index+1操作,而操作完成之后线程B也获得了资源,这个时候index的值已经是50了,但是由于判断的时候进入index+1操作的之前index是49此时A线程将其改为50并不会影响到对于值的判断,所以线程B就会在50的基础上继续执行index+1的操作。就会出现51。

那么怎么解决这个问题呢?在JDK1.5之前Java都是通过Synchronized关键字来解决这些问题的。Synchronized关键字提供了一种排他的机制,也就是说在同一时间内只能有一个线程进行一个基本的操作。

Synchronized关键字

  Synchronized关键字实现了防止内存一致性的错误。如果一个对象被多个线程访问,那么这个时候就需要实现对于对象的读写操作都是以同步的方式来实现的。

  • synchronized提供一个一种锁定机制,保证共享变量在多个线程访问的时候多个线程是互斥的。防止了数据一致性的问题。
  • synchronized 关键字包括monitor enter和monitor exit两个Java虚拟机指令,保证在任何时候线程执行了monitor enter指令成功之前都是从主内存中获取数据,而不是从缓存中获取数据,对于操作系统的内存模型到后面在进行细致说明。当执行了monitor exit命令之后共享变量的值必须被刷到主内存中。
  • synchronized关键字严格遵守happens-before原则,也就说上面提到的两个命令必须是同时存在才可以。

怎样使用synchronized关键字

方法加锁操作

  对于方法的同步来说,就是在方法上加上synchronized关键字

代码块加锁操作

  Java中有时候需要使用代码块进行操作,在代码块之前加上synchronized,就表示代码块加锁

public class TicketWindows implements Runnable {private int index = 1;private final static int MAX = 500;private final static Object MUTEX = new Object();@Overridepublic void run() {synchronized (MUTEX) {while (index <= MAX) {System.out.println(Thread.currentThread() + " 的号码是 :" + index++);try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}}}}public static void main(String[] args) {final TicketWindows ticketRunable = new TicketWindows();Thread thread1 = new Thread(ticketRunable, "一");Thread thread2 = new Thread(ticketRunable, "二");Thread thread3 = new Thread(ticketRunable, "三");Thread thread4 = new Thread(ticketRunable, "四");thread1.start();thread2.start();thread3.start();thread4.start();}
}

运行上面代码之后会发现每隔一秒钟输出一条数据。但是代码的结果引起了我的注意。并没有出现多个线程争抢的打印的效果。因为synchronized根本没有互斥锁定对应的作用域,线程之间进行lock的争抢只能发生在于monitor关联的同一个引用上,上面代码中每个线程争抢的monitor关联的引用都是彼此独立。所以不可能出现互斥的操作。

问题分析

F:\developersrc\GIT\JavaHighConcurrency\JavaHC\target\classes\com\example\charp04>javap -c TicketWindows.class
Compiled from "TicketWindows.java"
public class com.example.charp04.TicketWindows implements java.lang.Runnable {public com.example.charp04.TicketWindows();Code:0: aload_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: aload_05: iconst_16: putfield      #2                  // Field index:I9: returnpublic void run();Code:0: getstatic     #3                  // Field MUTEX:Ljava/lang/Object;3: dup4: astore_15: monitorenter6: aload_07: getfield      #2                  // Field index:I10: sipush        50013: if_icmpgt     7716: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;19: new           #6                  // class java/lang/StringBuilder22: dup23: invokespecial #7                  // Method java/lang/StringBuilder."<init>":()V26: invokestatic  #8                  // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;29: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;32: ldc           #10                 // String  的号码是 :34: invokevirtual #11                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;37: aload_038: dup39: getfield      #2                  // Field index:I42: dup_x143: iconst_144: iadd45: putfield      #2                  // Field index:I48: invokevirtual #12                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;51: invokevirtual #13                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;54: invokevirtual #14                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V57: getstatic     #15                 // Field java/util/concurrent/TimeUnit.SECONDS:Ljava/util/concurrent/TimeUnit;60: ldc2_w        #16                 // long 10l63: invokevirtual #18                 // Method java/util/concurrent/TimeUnit.sleep:(J)V66: goto          669: astore_270: aload_271: invokevirtual #20                 // Method java/lang/InterruptedException.printStackTrace:()V74: goto          677: aload_178: monitorexit79: goto          8782: astore_383: aload_184: monitorexit85: aload_386: athrow87: returnException table:from    to  target type57    66    69   Class java/lang/InterruptedException6    79    82   any82    85    82   anypublic static void main(java.lang.String[]);Code:0: new           #4                  // class com/example/charp04/TicketWindows3: dup4: invokespecial #21                 // Method "<init>":()V7: astore_18: new           #22                 // class java/lang/Thread11: dup12: aload_113: ldc           #23                 // String 一15: invokespecial #24                 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;Ljava/lang/String;)V18: astore_219: new           #22                 // class java/lang/Thread22: dup23: aload_124: ldc           #25                 // String 二26: invokespecial #24                 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;Ljava/lang/String;)V29: astore_330: new           #22                 // class java/lang/Thread33: dup34: aload_135: ldc           #26                 // String 三37: invokespecial #24                 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;Ljava/lang/String;)V40: astore        442: new           #22                 // class java/lang/Thread45: dup46: aload_147: ldc           #27                 // String 四49: invokespecial #24                 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;Ljava/lang/String;)V52: astore        554: aload_255: invokevirtual #28                 // Method java/lang/Thread.start:()V58: aload_359: invokevirtual #28                 // Method java/lang/Thread.start:()V62: aload         464: invokevirtual #28                 // Method java/lang/Thread.start:()V67: aload         569: invokevirtual #28                 // Method java/lang/Thread.start:()V72: returnstatic {};Code:0: new           #29                 // class java/lang/Object3: dup4: invokespecial #1                  // Method java/lang/Object."<init>":()V7: putstatic     #3                  // Field MUTEX:Ljava/lang/Object;10: return
}
"四" #15 prio=5 os_prio=0 tid=0x000000001a701000 nid=0x278c waiting for monitor entry [0x000000001b69f000]java.lang.Thread.State: BLOCKED (on object monitor)at com.example.charp04.TicketWindows.run(TicketWindows.java:20)- waiting to lock <0x00000000d7e302f0> (a java.lang.Object)at java.lang.Thread.run(Thread.java:748)Locked ownable synchronizers:- None"三" #14 prio=5 os_prio=0 tid=0x000000001a700800 nid=0x4410 waiting for monitor entry [0x000000001b59f000]java.lang.Thread.State: BLOCKED (on object monitor)at com.example.charp04.TicketWindows.run(TicketWindows.java:20)- waiting to lock <0x00000000d7e302f0> (a java.lang.Object)at java.lang.Thread.run(Thread.java:748)Locked ownable synchronizers:- None"二" #13 prio=5 os_prio=0 tid=0x000000001a700000 nid=0xb24 waiting for monitor entry [0x000000001b49f000]java.lang.Thread.State: BLOCKED (on object monitor)at com.example.charp04.TicketWindows.run(TicketWindows.java:20)- waiting to lock <0x00000000d7e302f0> (a java.lang.Object)at java.lang.Thread.run(Thread.java:748)Locked ownable synchronizers:- None"一" #12 prio=5 os_prio=0 tid=0x000000001a6fd000 nid=0x1cac waiting on condition [0x000000001b39f000]java.lang.Thread.State: TIMED_WAITING (sleeping)at java.lang.Thread.sleep(Native Method)at java.lang.Thread.sleep(Thread.java:340)at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)at com.example.charp04.TicketWindows.run(TicketWindows.java:23)- locked <0x00000000d7e302f0> (a java.lang.Object)at java.lang.Thread.run(Thread.java:748)Locked ownable synchronizers:- None

线程内存分析

  synchronized关键字提供了一种互斥锁定机制,也就是说在同一时间内只能有一个线程访问共享资资源

public class Mutex {private final static Object  MUTEX = new Object();public void doSource(){synchronized (MUTEX){try {TimeUnit.SECONDS.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}}public static void main(String[] args) {final Mutex mutex = new Mutex();for (int i = 0; i < 5; i++) {new Thread(mutex::doSource).start();}}
}


运行上面的程序使用Jconsole连接产看线程信息可以看到只有一个线程是处于TIMED_WAITING(Sleep)状态,其他的线程都是在BLOCKED状态。使用jstack -l pid名查看对应的栈信息会发现

D:\jdk1.8\bin>jstack -l 2312
2019-05-02 21:41:17
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.152-b16 mixed mode):"DestroyJavaVM" #17 prio=5 os_prio=0 tid=0x0000000002b62800 nid=0x23e4 waiting on condition [0x0000000000000000]java.lang.Thread.State: RUNNABLELocked ownable synchronizers:- None"Thread-4" #16 prio=5 os_prio=0 tid=0x000000001a9d1000 nid=0x40a4 waiting for monitor entry [0x000000001b67f000]java.lang.Thread.State: BLOCKED (on object monitor)at com.example.charp04.Mutex.doSource(Mutex.java:18)- waiting to lock <0x00000000d5f1d5c8> (a java.lang.Object)at com.example.charp04.Mutex$$Lambda$1/990368553.run(Unknown Source)at java.lang.Thread.run(Thread.java:748)Locked ownable synchronizers:- None"Thread-3" #15 prio=5 os_prio=0 tid=0x000000001a9d0800 nid=0xb3c waiting for monitor entry [0x000000001b57f000]java.lang.Thread.State: BLOCKED (on object monitor)at com.example.charp04.Mutex.doSource(Mutex.java:18)- waiting to lock <0x00000000d5f1d5c8> (a java.lang.Object)at com.example.charp04.Mutex$$Lambda$1/990368553.run(Unknown Source)at java.lang.Thread.run(Thread.java:748)Locked ownable synchronizers:- None"Thread-2" #14 prio=5 os_prio=0 tid=0x000000001a9cf800 nid=0x4750 waiting for monitor entry [0x000000001b47e000]java.lang.Thread.State: BLOCKED (on object monitor)at com.example.charp04.Mutex.doSource(Mutex.java:18)- waiting to lock <0x00000000d5f1d5c8> (a java.lang.Object)at com.example.charp04.Mutex$$Lambda$1/990368553.run(Unknown Source)at java.lang.Thread.run(Thread.java:748)Locked ownable synchronizers:- None"Thread-1" #13 prio=5 os_prio=0 tid=0x000000001a9cf000 nid=0x8bc waiting for monitor entry [0x000000001b37f000]java.lang.Thread.State: BLOCKED (on object monitor)at com.example.charp04.Mutex.doSource(Mutex.java:18)- waiting to lock <0x00000000d5f1d5c8> (a java.lang.Object)at com.example.charp04.Mutex$$Lambda$1/990368553.run(Unknown Source)at java.lang.Thread.run(Thread.java:748)Locked ownable synchronizers:- None"Thread-0" #12 prio=5 os_prio=0 tid=0x000000001a9ce000 nid=0x4634 waiting on condition [0x000000001b27e000]java.lang.Thread.State: TIMED_WAITING (sleeping)at java.lang.Thread.sleep(Native Method)at java.lang.Thread.sleep(Thread.java:340)at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)at com.example.charp04.Mutex.doSource(Mutex.java:18)- locked <0x00000000d5f1d5c8> (a java.lang.Object)at com.example.charp04.Mutex$$Lambda$1/990368553.run(Unknown Source)at java.lang.Thread.run(Thread.java:748)Locked ownable synchronizers:- None

只有Thread-0持有 locked <0x00000000d5f1d5c8> (a java.lang.Object)对象锁处于sleep状态,其他的线程则是处于- waiting to lock <0x00000000d5f1d5c8> (a java.lang.Object)等待获取锁的状态。

JVM基本操作指令

  在JDK的bin路径下面有很多的Java提供的命令例如javap 就是对于程序的class编码进行反编译。这样的话就会出现大量的JVM的命令。

F:\developersrc\GIT\JavaHighConcurrency\JavaHC\target\classes\com\example\charp04>javap -c Mutex.class
Compiled from "Mutex.java"
public class com.example.charp04.Mutex {public com.example.charp04.Mutex();Code:0: aload_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: returnpublic void doSource();Code:0: getstatic     #2 获取MUTEX                 // Field MUTEX:Ljava/lang/Object;3: dup4: astore_15: monitorenter6: getstatic     #3                  // Field java/util/concurrent/TimeUnit.SECONDS:Ljava/util/concurrent/TimeUnit;9: ldc2_w        #4                  // long 100l12: invokevirtual #6                  // Method java/util/concurrent/TimeUnit.sleep:(J)V15: goto          2318: astore_219: aload_220: invokevirtual #8                  // Method java/lang/InterruptedException.printStackTrace:()V23: aload_124: monitorexit25: goto          3328: astore_329: aload_130: monitorexit31: aload_332: athrow33: returnException table:from    to  target type6    15    18   Class java/lang/InterruptedException6    25    28   any28    31    28   anypublic static void main(java.lang.String[]);Code:0: new           #9                  // class com/example/charp04/Mutex3: dup4: invokespecial #10                 // Method "<init>":()V7: astore_18: iconst_09: istore_210: iload_211: iconst_512: if_icmpge     4215: new           #11                 // class java/lang/Thread18: dup19: aload_120: dup21: invokevirtual #12                 // Method java/lang/Object.getClass:()Ljava/lang/Class;24: pop25: invokedynamic #13,  0             // InvokeDynamic #0:run:(Lcom/example/charp04/Mutex;)Ljava/lang/Runnable;30: invokespecial #14                 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V33: invokevirtual #15                 // Method java/lang/Thread.start:()V36: iinc          2, 139: goto          1042: returnstatic {};Code:0: new           #16                 // class java/lang/Object3: dup4: invokespecial #1                  // Method java/lang/Object."<init>":()V7: putstatic     #2                  // Field MUTEX:Ljava/lang/Object;10: return
}

其中有两个关键的地方
monitorenter
  每个对象都与一个monitor有关联,一个monitor的lock只能被一个线程在同一时间获取。在线程获取monitor所有权的时候会出现以下的几种情况

  1. 如果monitor的计数器为0,就意味着还没有被lock,如果有线程获取到对应的monitor的时候则这个计数器就会进行加1的操作。
  2. 如果一个有monitor的线程重新被加载会导致的后果就是monitor继续累加操作
  3. 一个monitor已经被线程所有,当其他线程进行获取操作的时候就会进入到Blocking状态直到计数器为0,才会获得monitor的所有权。

monitorexit
  这个操作的主要作用就是放弃monitor的所有权,要想放弃所有权首先你要先拥有所有权,也就是说要对计数器进行减一的操作。

注意
  1. 在设置monitor锁定的时候锁定的关联的对象不能为空,如果为空的话没有对应的关联对象,也就没有monitor
  2. 不推荐将synchronized的作用域搞的太大,如上面展示了一样。在锁定范围内的数据要保持在一个范围内,如果锁定的返回过大导致monitor之间的东西太多了就会导致效率低下等问题的出现。
  3. 不同的monitor锁定同样的方法
  4. 多个锁的交叉会导致死锁

程序出现死锁的原因

1. 程序死锁
  1. 交叉锁导致程序出现死锁
  2. 内存不足
  3. 应答式数据交换
  4. 文件锁
  5. 数据库锁
  6. 死循环导致死锁
2.死锁判断
  1. 交叉锁引起的死锁,通过jstack工具或者使用jconsole工具进行判断盘查。
public class DeadLock {private final Object MUTEX_READ = new Object();private final Object MUTEX_WRITE = new Object();public void read() {synchronized (MUTEX_READ) {System.out.println(Thread.currentThread().getName() + "获取到读锁");synchronized (MUTEX_WRITE){System.out.println(Thread.currentThread().getName()+"获取到写锁");}System.out.println(Thread.currentThread().getName()+"释放写锁");}System.out.println(Thread.currentThread().getName()+"释放读锁");}public void write() {synchronized (MUTEX_WRITE) {System.out.println(Thread.currentThread().getName() + "获取到写锁");synchronized (MUTEX_READ){System.out.println(Thread.currentThread().getName()+"获取到读锁");}System.out.println(Thread.currentThread().getName()+"释放读锁");}System.out.println(Thread.currentThread().getName()+"释放写锁");}public static void main(String[] args) {final DeadLock deadLock = new DeadLock();new Thread(()->{while (true){deadLock.read();}},"读线程").start();new Thread(()->{while (true){deadLock.write();}},"写线程").start();}
}

当然还可以有更多的诊断工具。

总结

  通过对Synchronized关键字的深入分析,了解了synchronized的锁定机制。同时也引出了编程中的问题。当我们在编程过程中经常会因为锁的不正确使用导致死锁或者其他问题的发生,这个时候就需要会使用分析工具来进行分析问题。

Java高并发编程详解系列-线程安全数据同步相关推荐

  1. Java高并发编程详解系列-线程上下文设计模式及ThreadLocal详解

    导语   在之前的分享中提到过一个概念就是线程之间的通信,都知道在线程之间的通信是一件很消耗资源的事情.但是又不得不去做的一件事情.为了保证多线程线程安全就必须进行线程之间的通信,保证每个线程获取到的 ...

  2. Java高并发编程详解系列-线程上下文类加载

    前面的分享中提到的最多的概念就是关于类加载器的概念,但是当我们查看Thread源码的时候会发现如下的两个方法,这两个方法就是获取或者设置线程的上下文类加载器的方法,那么为什么要设置这两个方法呢?这个就 ...

  3. Java高并发编程详解系列-线程通信

      进程间的通信,又被称为是进程内部的通信,我们都知道每个进程中有多个线程在执行,多个线程要互斥的访问共享资源的时候会发送对应的等待信号或者是唤醒线程执行等信号.那么这些信号背后还有什么样的技术支持呢 ...

  4. Java高并发编程详解系列-线程异常处理

    前面的博客中主要描述的关于线程的概念,通过源码分析了解线程的基本操作方式,但是如何在线程运行期间获取异常信息呢?这就要使用到一个Hook线程了 线程运行时的异常   在Thread类中,关于线程运行时 ...

  5. Java高并发编程详解系列-线程生命周期观察者

    引言   在之前的博客中我们知道,Thread提供了很多可获取的状态,以及判断是否alive的方法,但是这些方法都是线程本身提供的,在Runnable运行的过程中所处的状态是无法直接获取到的到,例如什 ...

  6. Java高并发编程详解系列-线程池原理自定义线程池

    之前博客的所有内容是对单个线程的操作,例如有Thread和Runnable的使用以及ThreadGroup等的使用,但是对于在有些场景下我们需要管理很多的线程,而对于这些线程的管理有一个统一的管理工具 ...

  7. Java高并发编程详解系列-Java线程入门

    根据自己学的知识加上从各个网站上收集的资料分享一下关于java高并发编程的知识点.对于代码示例会以Maven工程的形式分享到个人的GitHub上面.   首先介绍一下这个系列的东西是什么,这个系列自己 ...

  8. Java高并发编程详解系列-7种单例模式

    引言 在之前的文章中从技术以及源代码的层面上分析了关于Java高并发的解决方式.这篇博客主要介绍关于单例设计模式.关于单例设计模式大家应该不会陌生,作为GoF23中设计模式中最为基础的设计模式,实现起 ...

  9. Java高并发编程详解系列-Future设计模式

    导语   假设,在一个使用场景中有一个任务需要执行比较长的时间,通常需要等待任务执行结束之后或者是中途出错之后才能返回结果.在这个期间调用者只能等待,对于这个结果Future设计模式提供了一种凭据式的 ...

最新文章

  1. python的with用法(参考)
  2. JavaScript——String转DOM对象解决方案
  3. openjudge基础题3计算书费
  4. 3.12 SE11创建锁对象
  5. python语言的类型是_Python的语言类型
  6. 黄金为什么贵,黄金都有什么用处?
  7. 修改caffe源码--支持多标签--关键点检测
  8. SAP BW报表使用操作手册——基础操作
  9. 个人项目集 - Oliver Chu
  10. WAREZ无形帝国(盗版之源)
  11. PCIE 转 spi 总线
  12. android gridview 做日历,android日历控件
  13. 微信小助手简版 WeChatSeptet for Mac安装教程!
  14. ftp 发生意外错误 0x8ffe2740
  15. Sam Altman专访:GPT-4没太让我惊讶,ChatGPT则让我喜出望外
  16. 我的北京工作居住证申请之旅
  17. 计算机上安装的网络协议,怎么安装网络协议
  18. 无线电视服务器主机名,电视服务器主机名怎么填
  19. 【NOIp普及组 2009】分数线划定
  20. JZOJ 6305.最小值【思维】【dp】

热门文章

  1. mysql exp 注入_使用exp进行SQL报错注入
  2. VMWare虚拟机三种网络形式
  3. MICROSOFT REPORT VIEWER 2012之无法加载相关的dll
  4. Oracle在Linux上的预配置
  5. syslog-ng记录history日志
  6. Bing Maps进阶系列八:在Bing Maps中集成OpenStreetMap地图
  7. 让自己慢下来(2)-朋友们的回复
  8. SpringFramework之javax.servlet.http.HttpSession
  9. Docker CE for CentOS的安装(对官方文档的翻译)
  10. Vbs压缩备份文件夹以日期命名