线程安全就是防止某个对象或者值在多个线程中被修改而导致的数据不一致问题,因此我们就需要通过同步机制保证在同一时刻只有一个线程能够访问到该对象或数据,修改数据完毕之后,再将最新数据同步到主存中,使得其他线程都能够得到这个最新数据。下面我们就来了解Java一些基本的同步机制。

volatile关键字

Java提供了一种稍弱的同步机制即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的。然而,在访问volatile变量时不会执行加锁操作,因此也就不会使线程阻塞,因此volatile变量是一种比synchronized关键字更轻量级的同步机制。

volatile变量对所有的线程都是可见的,对volatile变量所有的写操作都能立即反应到其他线程之中,即volatile变量在各个线程中是一致的。

有一种情况需要注意:volatile的语义不能确保递增(count++)的原子性,除非你能确保只有一个线程对变量执行写操作。

public class VolatileTest{

public static volatile int i;

public static void increase(){

i++;

}

}

查看字节码: javap -c -l VolatileTest.class

public class VolatileTest {

public static volatile int i;

public VolatileTest();

Code:

0: aload_0

1: invokespecial #1 // Method java/lang/Object."":()V

4: return

LineNumberTable:

line 1: 0

public static void increase();

Code:

0: getstatic #2 // Field i:I, 把i的值取到了操作栈顶,volatile保证了i值此时是正确的.

3: iconst_1

4: iadd // increase,但其他线程此时可能已经把i值加大了好多

5: putstatic #2 // Field i:I ,把这个已经out of date的i值同步回主内存中,i值被破坏了.

8: return

LineNumberTable:

line 6: 0

line 7: 8

}

加锁机制即可以确保原子性又可以确保可见性,而volatile变量只能确保可见性。

内置锁-synchronized

Java中最常用的同步机制就是synchronized关键字,它是一种基于语言的粗略锁,能够作用于对象、函数、Class。每个对象都只有一个锁,谁能够拿到这个锁谁就得到了访问权限。当synchronized作用于函数时,实际上锁的也是对象,锁定的对象是该函数所在类的对象。而synchronized作用于Class时则锁的是这个Class类,并非某个具体对象。

synchronized同步方法和同步块

public class SynchronizedDemo {

/**

* @param args

*/

public static void main(String[] args) {

// TODO Auto-generated method stub

final Test test = new Test();

new Thread(new Runnable() {

@Override

public void run() {

// TODO Auto-generated method stub

test.syncMethod(Thread.currentThread());

}

}).start();

new Thread(new Runnable() {

@Override

public void run() {

// TODO Auto-generated method stub

test.syncMethod(Thread.currentThread());

}

}).start();

new Thread(new Runnable() {

@Override

public void run() {

// TODO Auto-generated method stub

test.asyncMethod(Thread.currentThread());

}

}).start();

}

}

class Test {

public synchronized void syncMethod(Thread thread) {

for(int i = 0;i < 3;i++) {

System.out.println(thread.getName());

try {

Thread.sleep(100);

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

}

public void asyncMethod(Thread thread) {

synchronized (this) {

for(int i = 0;i < 3;i++) {

System.out.println(thread.getName()+2);

}

}

}

}

syncMethod和asyncMethod代码块都加锁时结果:

Thread-0

Thread-0

Thread-0

Thread-1

Thread-1

Thread-1

Thread-2

Thread-2

Thread-2 #多个线程不能同时访问同一个对象中的synchronized锁的方法或代码块

syncMethod加锁和asyncMethod代码块不加锁时结果:

class Test {

public synchronized void syncMethod(Thread thread) {

for(int i = 0;i < 3;i++) {

System.out.println(thread.getName());

try {

Thread.sleep(100);

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

}

public void asyncMethod(Thread thread) {

synchronized (this) {

for(int i = 0;i < 3;i++) {

System.out.println(thread.getName());

}

}

}

}

Thread-0

Thread-22

Thread-22

Thread-22

Thread-0

Thread-0

Thread-1

Thread-1

Thread-1 #其他线程可以访问同一个对象的非同步方法或代码块

syncMethod不加锁和asyncMethod代码块不加锁时结果:

class Test {

public void syncMethod(Thread thread) {

for(int i = 0;i < 3;i++) {

System.out.println(thread.getName());

try {

Thread.sleep(100);

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

}

public void asyncMethod(Thread thread) {

for(int i = 0;i < 3;i++) {

System.out.println(thread.getName()+2);

}

}

}

Thread-0

Thread-1

Thread-22

Thread-22

Thread-22

Thread-0

Thread-1

Thread-1

Thread-0

synchronized同步方法和同步块锁定的是引用对象,synchronized作用于引用对象是防止其他线程访问同一个对象的synchronized代码块或方法,但可以访问其他非同步代码块或方法。

synchronized同步Class对象和静态方法

public class SynchronizedDemo {

/**

* @param args

*/

public static void main(String[] args) {

// TODO Auto-generated method stub

final Test test = new Test();

new Thread(new Runnable() {

@Override

public void run() {

// TODO Auto-generated method stub

Test.syncStaticMethod(Thread.currentThread());

}

}).start();

new Thread(new Runnable() {

@Override

public void run() {

// TODO Auto-generated method stub

Test.syncStaticMethod(Thread.currentThread());

}

}).start();

new Thread(new Runnable() {

@Override

public void run() {

// TODO Auto-generated method stub

Test.asyncStaticMethod(Thread.currentThread());

}

}).start();

}

}

class Test {

public synchronized static void syncStaticMethod(Thread thread) {

for (int i = 0; i < 3; i++) {

System.out.println(thread.getName());

try {

Thread.sleep(50);

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

}

public static void asyncStaticMethod(Thread thread) {

synchronized (Test.class) {

for (int i = 0; i < 3; i++) {

System.out.println(thread.getName() + 22);

try {

Thread.sleep(50);

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

}

}

}

syncStaticMethod和asyncStaticMethod代码块都加锁的结果:

Thread-0

Thread-0

Thread-0

Thread-222

Thread-222

Thread-222

Thread-1

Thread-1

Thread-1 ##多个线程不能同时访问添加了synchronized锁的代码块和方法。

syncStaticMethod加锁和asyncStaticMethod代码块不加锁的结果:

class Test {

public synchronized static void syncStaticMethod(Thread thread) {

for (int i = 0; i < 3; i++) {

System.out.println(thread.getName());

try {

Thread.sleep(50);

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

}

public static void asyncStaticMethod(Thread thread) {

for (int i = 0; i < 3; i++) {

System.out.println(thread.getName() + 22);

try {

Thread.sleep(50);

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

}

}

Thread-0

Thread-222

Thread-222

Thread-0

Thread-0

Thread-222

Thread-1

Thread-1

Thread-1 ##多个线程可以同时访问非同步的代码块和方法

syncStaticMethod加锁和asyncStaticMethod代码块都不加锁的结果:

class Test {

public static void syncStaticMethod(Thread thread) {

for (int i = 0; i < 3; i++) {

System.out.println(thread.getName());

try {

Thread.sleep(50);

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

}

public static void asyncStaticMethod(Thread thread) {

for (int i = 0; i < 3; i++) {

System.out.println(thread.getName() + 22);

try {

Thread.sleep(50);

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

}

}

Thread-0

Thread-1

Thread-222

Thread-1

Thread-0

Thread-222

Thread-1

Thread-0

Thread-222

synchronized同步Class对象和静态方法锁的是Class对象,它的作用是防止多个线程同时访问添加了synchronized锁的代码块和方法。

总结

当一个线程正在访问一个对象的synchronized方法,那么其他线程不能访问该对象的其他synchronized方法,因为一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所有无法访问该对象的其他synchronized方法。

当一个线程正在访问一个对象的synchronized方法,那么其他线程能访问该对象的非synchronized方法。因为非synchronized方法不需要获取该对象的锁。

如果一个线程A需要访问对象object1的synchronized方法fun1,另外一个线程B需要访问对象object2的synchronized方法fun1,即使object1和object2是同一类型,也不会产生线程安全问题,因为他们访问的是不同的对象,所以不存在互斥问题。

如果一个线程执行一个对象的非static synchronized方法,另一个线程执行这个对象所属类的static synchronized方法,此时不会发生互斥现象,因为访问static synchronized方法占用的是类锁,而访问非static synchronized方法占用的是对象锁,所以不存在互斥现象。

需要注意的是:对于synchronized方法或者synchronized代码块,当出现异常时,JVM会自动释放当前线程占用的锁,因此不会由于异常导致出现死锁现象。

显示锁-ReentrantLock与Condition

ReentrantLock

在JDk 5.0之前,协调共享对象的访问时,只有synchronized和volatile。Java 6.0增加了一种新的机制:ReentrantLock。显示锁ReentrantLock和内置锁synchronized相比,实现了相同的语义,但是具有更高的灵活性。

内置锁synchronized的获取和释放都在同一个代码块中,而显示锁ReentrantLock则可以将锁的获得和释放分开。同时显示锁可以提供轮训锁和定时锁,同时可以提供公平锁或者非公平锁。

ReentrantLock的基本操作如下:

函 数

作 用

lock()

获取锁

tryLock()

尝试获取锁

tryLock(timeout,Timeunit unit)

在指定时间内尝试获取锁

unLock()

释放锁

newCondition

获取锁的Condition

使用ReentrantLock的一般是lock、tryLock与unLock成对出现,需要注意的是,千万不要忘记调用unLock来释放锁,否则会引发死锁等问题。

ReentrantLock的常用形式如下所示:

Lock lock = new ReentrantLock();

public void run() {

lock.lock();

try {

//执行任务

} finally {

lock.unlock();

}

}

需要注意的是,lock必须在finally块中释放,否则,如果受保护的代码块抛出异常,锁就有可能永远得不到释放。而使用synchronized同步,JVM将确保锁会获得自动释放,这也是Lock没有完全替代掉synchronized的原因。

当JVM用synchronized管理锁定请求和释放行为时,JVM在生成线程转储时能够包括锁定信息,这些对调式有非常大的价值,因为它们能标识死锁和其他异常行为的来源。Lock类只是普通的类,JVM不知道具体哪个线程拥有Lock对象。

Condition

在ReentrantLock类中有一个重要的函数newCondition(),该函数用于获取lock上的一个条件,也就是说Condition是和Lock绑定的。Condition用于实现线程间的通信,它是为了解决Object.wait()、notify()、notifyAll()难以使用的问题。

Condition的基本操作如下所示:

方 法

作 用

await()

线程等待

await(int time,TimeUnit unit)

线程等待特定的时间,超过时间则为超时

signal()

随机唤醒某个等待线程

signalAll()

唤醒所有等待中的线程

综合应用

下面通过ReentrantLock和Condition类实现一个简单的阻塞队列。如果调用take方法时集合中没有数据,那么调用线程阻塞;如果调用put方法时,集合数据已满则调用线程阻塞。但是这两个阻塞条件是不同的,分别为notFull和notEmpty。MyArrayBlockingQueue的实现代码如下:

public class MyArrayBlockingQueue {

// 数据数组

private final T[] items;

// 锁

private final Lock mLock = new ReentrantLock();

// 数组满的条件

private Condition notFull = mLock.newCondition();

// 数组空的条件

private Condition notEmpty = mLock.newCondition();

// 头部

private int head;

// 尾部

private int tail;

// 数据数量

private int count;

public MyArrayBlockingQueue(int maxSize) {

// TODO Auto-generated constructor stub

items = (T[]) new Object[maxSize];

}

public MyArrayBlockingQueue() {

// TODO Auto-generated constructor stub

this(10);

}

public void put(T t) {

mLock.lock();

try {

// 如果数据已满,等待

while (count == getCapacity()) {

System.out.println("数据已满,请等待");

notFull.await();

}

System.out.println("存入数据");

items[tail] = t;

if (++tail == getCapacity()) {

tail = 0;

}

++count;

// 唤醒等待数据的线程

notEmpty.signalAll();

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

} finally {

mLock.unlock();

}

}

public T take() {

mLock.lock();

try {

// 如果数组数据为空,则阻塞

while (count == 0) {

System.out.println("还没有数据,等待");

notEmpty.await();

}

System.out.println("取出数据");

T t = items[head];

items[head] = null;

if (++head == getCapacity()) {

head = 0;

}

--count;

// 唤醒添加数据的线程

notFull.signalAll();

return t;

} catch (InterruptedException e) {

// TODO: handle exception

} finally {

mLock.unlock();

}

return null;

}

public int getCapacity() {

return items.length;

}

public int size() {

mLock.lock();

try {

return count;

} finally {

mLock.unlock();

}

}

/**

* @param args

*/

public static void main(String[] args) {

// TODO Auto-generated method stub

final MyArrayBlockingQueue mQueue = new MyArrayBlockingQueue<>(

5);

new Thread(new Runnable() {

@Override

public void run() {

// TODO Auto-generated method stub

while (true) {

for(int i = 0;i < 3;i++)

mQueue.put("just");

try {

Thread.sleep(50);

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

}

}).start();

new Thread(new Runnable() {

@Override

public void run() {

// TODO Auto-generated method stub

while (true) {

mQueue.take();

}

}

}).start();

}

}

结果打印

存入数据

存入数据

存入数据

取出数据

取出数据

取出数据

还没有数据,等待

存入数据

存入数据

存入数据

取出数据

取出数据

取出数据

还没有数据,等待

信号量-Semaphore

Semaphore是一个计数信号量,它的本质是一个“共享锁”。信号量维护一个信号许可集合,线程可以通过调用acquire()来获取信号量的许可。当信号量有可用的许可时,线程能获取该许可;否则线程必须等到,直到有可用的许可为止。线程可以通过release()来释放它所持有的信号量许可。

Semaphore实现的功能类似食堂窗口。例如,食堂只有3个销售窗口,要吃饭的有5个人,那么同时只有3个人买饭菜,每个人占用一个窗口,另外2人只能等待。当前3个人有人离开之后,后续的人才可以占用窗口进行购买。这里的窗口就是我们所说的许可集,这里为3.一个人占用窗口时相当于他调用acquire()获取了许可,当他离开时也就等于调用release()释放了许可,这样后续的人才可以得到许可。下面看看具体的示例:

public class SemaphoreTest {

/**

* @param args

*/

public static void main(String[] args) {

// TODO Auto-generated method stub

ExecutorService service = Executors.newFixedThreadPool(3);

final Semaphore semaphore = new Semaphore(3);

for(int i = 0;i < 5;i++) {

service.submit(new Runnable() {

@Override

public void run() {

// TODO Auto-generated method stub

try {

semaphore.acquire();

System.out.println("剩余许可: " + semaphore.availablePermits());

Thread.sleep(2000);

semaphore.release();

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

});

}

}

}

结果打印:

剩余许可: 0

剩余许可: 0

剩余许可: 0

剩余许可: 2

剩余许可: 1

上述结果中:前三行是立刻输出的,后两行是等待2秒之后才输出。原因是,信号量的许可集是3个,而消费线程是5个。前3个线程获取了许可之后,信号量的许可就为0。此时后面的线程再调用acquire()就会阻塞,直到前3个线程执行完之后,释放了许可(不需要同时释放许可)后两个线程才能获取许可并且继续执行。

循环栅栏-CyclicBarrier

CyclicBarrier是一个同步辅助类,允许一组线程互相等待,直到达到某个公共屏障点。因为该barrier在释放等待线程后可以重用,所有称为循环的barrier。

下面看看示例:

public class CyclicBarrierTest {

private static final int SIZE = 5;

private static CyclicBarrier mCyclicBarrier;

/**

* @param args

*/

public static void main(String[] args) {

// TODO Auto-generated method stub

mCyclicBarrier = new CyclicBarrier(SIZE, new Runnable() {

@Override

public void run() {

// TODO Auto-generated method stub

System.out.println("--满足条件执行特定操作,参与者: "+ mCyclicBarrier.getParties());

}

});

for(int i = 0;i < SIZE;i++) {

new WorkerThread().start();

}

}

static class WorkerThread extends Thread {

@Override

public void run() {

// TODO Auto-generated method stub

try {

System.out.println(Thread.currentThread().getName() + "等待CyclicBarrier");

//将mCyclicBarrier的参与者数量加1

mCyclicBarrier.await();

//mCyclicBarrier的参与者数量加5时,才继续往后执行

System.out.println(Thread.currentThread().getName()+"继续执行");

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

} catch (BrokenBarrierException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

}

}

结果打印:

Thread-1等待CyclicBarrier

Thread-0等待CyclicBarrier

Thread-2等待CyclicBarrier

Thread-3等待CyclicBarrier

Thread-4等待CyclicBarrier

--满足条件执行特定操作,参与者: 5

Thread-4继续执行

Thread-3继续执行

Thread-2继续执行

Thread-0继续执行

Thread-1继续执行

从结果可以看出,只有当有5个线程调用了mCyclicBarrier.await()方法后,后续的任务才会继续执行。上述例子中的5个WorkThread就位之后首先会执行一个Runnable,也就是CyclicBarrier构造函数的第二个参数,该参数也可以省略。执行该Runnable之后才会继续执行下面的任务。CyclicBarrier实际上相当于可以用于多个线程等待,直到某个条件被满足后开始继续执行后续的任务。对于该示例来说,这里的条件也就是有指定个数的线程调用了mCyclicBarrier.await()方法。

闭锁-CountDownLatch

CountDownLatch是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待,直到条件被满足。

示例如下:

public class CountDownLatchTest {

private static final int LATCH_SIZE = 5;

/**

* @param args

*/

public static void main(String[] args) {

// TODO Auto-generated method stub

try {

CountDownLatch countDownLatch = new CountDownLatch(LATCH_SIZE);

for(int i = 0;i < LATCH_SIZE;i++) {

new WorkerThread(countDownLatch).start();

}

System.out.println("主线程等待");

countDownLatch.await();

System.out.println("主线程继续执行");

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

static class WorkerThread extends Thread {

private CountDownLatch latch;

public WorkerThread(CountDownLatch latch) {

this.latch = latch;

}

@Override

public void run() {

// TODO Auto-generated method stub

try {

Thread.sleep(1000);

System.out.println(Thread.currentThread().getName() + "执行操作");

//将latch的数量减1

latch.countDown();

} catch (Exception e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

}

}

结果打印:

主线程等待

Thread-3执行操作

Thread-1执行操作

Thread-0执行操作

Thread-4执行操作

Thread-2执行操作

主线程继续执行

5个WorkThread对象在执行完操作之后会调用CountDownLatch的countDown()函数,当5个WorkThread全都调用了countDown()之后主线程就会被唤醒继续执行任务。

CountDownLatch与CyclicBarrier区别

CountDownLatch的作用是允许1或者多个线程等待其他线程完成执行,而CyclicBarrier则是允许N个线程相互等待。

CountDownLatch的计数器无法被重置,CyclicBarrier的计数器可以被重置后使用。

java线程同步的作用_Java并发编程之线程同步相关推荐

  1. java线程同步的实现_Java并发编程(三) - 实战:线程同步的实现

    synchronized关键字 首先,来看一个多线程竞争临界资源导致的同步不安全问题. package com.example.weishj.mytester.concurrency.sync; /* ...

  2. java 关闭守护线程_Java并发编程之线程生命周期、守护线程、优先级、关闭和join、sleep、yield、interrupt...

    Java并发编程中,其中一个难点是对线程生命周期的理解,和多种线程控制方法.线程沟通方法的灵活运用.这些方法和概念之间彼此联系紧密,共同构成了Java并发编程基石之一. Java线程的生命周期 Jav ...

  3. java queue 线程安全_java并发编程之线程安全方法

    线程安全的实现方法,包含如下方式 一, 互斥同步 使用互斥锁的方式. 举个栗子 synchronized,最常用的同步实现方案, ReentrantLock,java并发包中工具,后续介绍. 互斥同步 ...

  4. java线程状态_java并发编程之线程状态

    java线程中,线程状态是如何转换的呢?这一次我们一起来学习下. 线程状态: NEW: 线程创建之后,还没有启动.这时候它的状态就是NEW RUNNABLE: 正在Java虚拟机下跑任务的线程的状态. ...

  5. 判断线程是否执行完毕_Java并发编程 | 线程核心机制,基础概念扩展

    源码地址:GitHub || GitEE 一.线程基本机制 1.概念描述 并发编程的特点是:可以将程序划分为多个分离且独立运行的任务,通过线程来驱动这些独立的任务执行,从而提升整体的效率.下面提供一个 ...

  6. java的尝试性问题_Java并发编程实战 03互斥锁 解决原子性问题

    文章系列 摘要 在上一篇文章02Java如何解决可见性和有序性问题当中,我们解决了可见性和有序性的问题,那么还有一个原子性问题咱们还没解决.在第一篇文章01并发编程的Bug源头当中,讲到了把一个或者多 ...

  7. c++ 线程池_JAVA并发编程:线程池ThreadPoolExecutor源码分析

    前面的文章已经详细分析了线程池的工作原理及其基本应用,接下来本文将从底层源码分析一下线程池的执行过程.在看源码的时候,首先带着以下两个问题去仔细阅读.一是线程池如何保证核心线程数不会被销毁,空闲线程数 ...

  8. java 类里面对象共享_Java并发编程 - 对象的共享

    编写正确的并发程序,关键问题在于:在访问共享的可变状态时需要进行正确的管理.同步代码块和同步方法可以确保以原子的方式执行操作,同步还有另一个重要的方面:内存可见性. 可见性 为了确保多个线程之间对内存 ...

  9. java 线程安全的原因_Java并发编程——线程安全性深层原因

    线程安全性深层原因 这里我们将会从计算机硬件和编辑器等方面来详细了解线程安全产生的深层原因. 缓存一致性问题 CPU内存架构 随着CPU的发展,而因为CPU的速度和内存速度不匹配的问题(CPU寄存器的 ...

  10. java中同步组件_Java并发编程(自定义同步组件)

    并发包结构图: 编写一个自定义同步组件来加深对同步器的理解 业务要求: * 编写一个自定义同步组件来加深对同步器的理解. * 设计一个同步工具:该工具在同一时刻,只允许至多两个线程同时访问,超过两个线 ...

最新文章

  1. phpcms v9整合Ueditor
  2. 通过端到端的数据侦测提升QoS
  3. ADO和DAO的区别
  4. 刷题总结——road(ssoi)
  5. php-open网站还可以用,nginx+php使用open_basedir限制站点目录防止跨站
  6. 对鸢尾花数据集和月亮数据集,分别采用线性LDA、k-means和SVM算法进行二分类可视化分析
  7. LeetCode 407. Trapping Rain Water II
  8. Qt核心剖析: moc
  9. Prism初研究之使用Prism 5.0开发模块化应用
  10. 需求与商业模式分析-2-商业模式类型
  11. RecyclerView 实现多种布局(上半部Gridview样式,下半部Listview样式)以及多种数据类型实现不同布局
  12. IIS DirectoryEntry
  13. 如何擦除Altera FPGA的配置器件EPCS中的内容
  14. R语言中的apply(),lapply(),sapply(),tapply()函数以及示例
  15. _ZN10tensorflow8internal21CheckOpMessageBuilder9NewStringB5cxx11Ev
  16. 代理模式——远程代理(一)
  17. javascript_JSON.parse() 与 JSON.stringify()_ZHOU125disorder_
  18. 如何安装OCSNG及GLPI
  19. 二元logistics回归
  20. enumerate函数、self参数错误

热门文章

  1. Windows 7无线路由器解决
  2. 4.RabbitMQ实战 --- 解决Rabbit相关问题:编码与模式,RPC
  3. 10.Linux 高性能服务器编程 --- 信号
  4. 2.OAuth 简介(2)
  5. 广东石油化工学院大学计算机基础,大学计算机基础习题集-北京石油化工学院文档.doc...
  6. sql中用于子查询的几个关键词 any(some是 any的别名),all,in,exists
  7. 绝对定位元素、浮动元素会生成一个块级框
  8. 内置模块--又称为常用模块
  9. sql server 本地复制订阅 实现数据库服务器 读写分离
  10. JAVA 最新 环境搭建(JDK 1.8 + Tomcat 9 + eclipse oxygen + mysql 5.7)