Synchronized

为什么要学习Synchronized?

在我们学习多线程的时候,会遇到共享内存两个重要的问题。一个是竞态条件,另一个是内存可见性。解决这两个问题的一种方案是使用Synchronized。
在介绍什么是竞态条件,什么是内存可见性之前,我们先讲解一下synchronized的用法和基本原理。

用法 (synchronized可以用于修饰类的实例方法、静态方法和代码块)

  • synchronized修饰普通同步方法:锁对象为当前实例对象
public synchronized void sayHello(){System.out.println("Hello World");
}
  • synchronized修饰静态同步方法:锁对象为当前的类Class对象
public static synchronized void sayHello(){System.out.println("Hello World");
}
  • synchronized修饰同步代码块:锁对象是synchronized后面括号里配置的对象这个对象可以使某个对象,也可以是某个类。
synchronized(this){}
synchronized(""){}
synchronized(xxx.class){}

注意事项:

1.使用synchronized修饰非静态方法或者使用synchronized修饰代码块时制定的为实例对象时,同一个类的不同对象拥有自己的锁,因此不会相互阻塞。

下面代码中synchronized中参数为this,而我创建了2个实例对象,此时锁对象为this。代码结果表明了同一个类的不同对象拥有自己的锁,因此不会相互阻塞。

public class ThreadTest03 extends Thread{private int number=10;@Overridepublic void run() {synchronized (this){say();for(int i=10;i>0;i--){if(number>0){System.out.println(Thread.currentThread().getName()+"  "+--number);}}}}public synchronized  void say(){System.out.println(Thread.currentThread().getName()+"我会说话");}public static void main(String[] args) {ThreadTest03 t1=new ThreadTest03();ThreadTest03 t2=new ThreadTest03();t1.start();System.out.println("t1启动");t2.start();System.out.println("t2启动");}
}

结果展示:

2. 使用synchronized修饰类和对象时,由于类对象和实例对象分别拥有自己的监视器锁,因此不会相互阻塞。

类对象只有一个,而实例对象可以有多个。当synchronized参数为类对象时,因为类对象只有一个,当其中一个A线程拿到这把锁时,另一个B线程会被阻塞,因为这个线程拿不到这把锁。只能等A线程释放这把锁。而synchronized参数为实例对象时,下边的代码的实例对象有2个,所以当synchronized的参数为this时,谁调用,这个this就是哪一个实例对象。对象不同,所以他们拥有自己的监视器锁,因为不会产生相互阻塞的情况。

public class ThreadTest03 extends Thread{private int number=10;@SneakyThrows@Overridepublic void run() {getThreadClass();synchronized (ThreadTest03.class){for(int i=10;i>0;i--){if(number>0){System.out.println(Thread.currentThread().getName()+"  "+--number);}}}}public  void getThreadClass() throws InterruptedException {synchronized (this){System.out.println(Thread.currentThread().getName()+" "+this.getState());Thread.sleep(1000);for(int i=0;i<5;i++){System.out.println(this.getName());}}}public static void main(String[] args) {ThreadTest03 t1=new ThreadTest03();ThreadTest03 t2=new ThreadTest03();t1.start();System.out.println("t1启动");t2.start();System.out.println("t2启动");}
}

结果展示:

这里就展示了一部分结果,从线程状态来看,当线程调用synchronized参数为this的代码块时,t1,t2为2个不同实例对象,因为各自有自己的锁,互不阻塞。

3.使用使用synchronized修饰实例对象时,如果一个线程正在访问实例对象的一个synchronized方法时,其它线程不仅不能访问该synchronized方法,该对象的其它synchronized方法也不能访问,因为一个对象只有一个监视器锁对象,但是其它线程可以访问该对象的非synchronized方法。

public class SynLock02 {public static void main(String[] args) {Phone1 p1=new Phone1();Phone1 p2=new Phone1();new Thread(()->p.SendSms(),"A").start();new Thread(()->p.call(),"B").start();new Thread(()->p2.sayHello(),"C").start();}
}class Phone1{// synchronized 锁的是方法的调用者,谁先调用,谁先执行public synchronized  void call(){try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("我会打电话");}public synchronized void sendSms(){System.out.println("我会发短信");}// 普通方法不受锁的控制public void sayHello(){System.out.println("hello");}
}

结果展示:

可以很清楚的看到没有被synchronized修饰的方法,不受约束,当CPU分给调用此方法的时间片后,即可执行此方法。

4.线程A访问实例对象的非static synchronized方法时,线程B也可以同时访问实例对象的static synchronized方法,因为前者获取的是实例对象的监视器锁,而后者获取的是类对象的监视器锁,两者不存在互斥关系。

public class SynLock03 {public static void main(String[] args) {Phone3 p1=new Phone3();Phone3 p2=new Phone3();new Thread(()->p1.sendSms(),"A").start();new Thread(()->p2.call(),"B").start();}
}
class Phone3{// 静态  类加载 锁的是class 类模板public static synchronized  void call(){System.out.println("我会打电话");}public synchronized void sendSms(){try {// 休眠,来此判断B线程状态是否为RUNNABLETimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("我会发短信");}
}

结果展示:

从结果看来,已证实此说法“.线程A访问实例对象的非static synchronized方法时,线程B也可以同时访问实例对象的static synchronized方法”。

synchronized实现原理

synchronized的实现原理要从Java对象头(32为例)来讲起,我们先来看一下Java的对象头
Java的对象头有两种方式,一种是普通对象,另一种为数组对象。
Java的普通对象组成:

|--------------------------------------------------------------|
|                     Object Header (64 bits)                  |
|------------------------------------|-------------------------|
|        Mark Word (32 bits)         |    Klass Word (32 bits) |
|------------------------------------|-------------------------|

Java的数组对象组成:

|---------------------------------------------------------------------------------|
|                                 Object Header (96 bits)                         |
|--------------------------------|-----------------------|------------------------|
|        Mark Word(32bits)       |    Klass Word(32bits) |  array length(32bits)  |
|--------------------------------|-----------------------|------------------------|

Mark Word

|-------------------------------------------------------|--------------------|
|                  Mark Word (32 bits)                  |       State        |
|-------------------------------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 |       Normal       |
|-------------------------------------------------------|--------------------|
|  thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 |       Biased       |
|-------------------------------------------------------|--------------------|
|               ptr_to_lock_record:30          | lock:2 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
|               ptr_to_heavyweight_monitor:30  | lock:2 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
|                                              | lock:2 |    Marked for GC   |
|-------------------------------------------------------|--------------------|

Mark Work:

  • identity_hashcode:每一个对象都会有一个自身的hashcode
  • age:分代年龄(关于垃圾回收GC),4位,对象在幸存区复制1次,年龄就会+1,然后对象从新生代到老年代会存在一个关于年龄的阈值,如果达到了这个阈值,这个对象就会放到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。
  • thread:持有偏向锁的线程ID
  • lock::2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个mark word表示的含义不同。
  • biased_lock:对象是否启用了偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。
biased_lock lock 状态
0 01 无锁
1 01 偏向锁
0 00 轻量级锁
0 10 重量级锁
0 11 GC标记

关于这些锁升级的描述,在这里不在叙述,后续也会出一篇关于锁升级的文章。

  • 关于state状态描述

    • Normal:正常(无状态)
    • Biased:偏向锁
    • Lightweight Locked:轻量级锁
    • Heavyweight Locked:重量级锁
    • Marked for GC:GC

这是对象头(64位) ,与对象头(32位)相似。
Mark Word的位长度为JVM的一个Word大小,32位JVM的Mark Word为32位,64位JVM的Mark Word为64位。

|------------------------------------------------------------------------------|--------------------|
|                                  Mark Word (64 bits)                         |       State        |
|------------------------------------------------------------------------------|--------------------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 |       Normal       |
|------------------------------------------------------------------------------|--------------------|
| thread:54 |       epoch:2        | unused:1 | age:4 | biased_lock:1 | lock:2 |       Biased       |
|------------------------------------------------------------------------------|--------------------|
|                       ptr_to_lock_record:62                         | lock:2 | Lightweight Locked |
|------------------------------------------------------------------------------|--------------------|
|                     ptr_to_heavyweight_monitor:62                   | lock:2 | Heavyweight Locked |
|------------------------------------------------------------------------------|--------------------|
|                                                                     | lock:2 |    Marked for GC   |
|------------------------------------------------------------------------------|--------------------|

关于Java对象头相关文章可以看一下这篇:https://www.jianshu.com/p/3d38cba67f8b

Monitor 监视器

Monitor被翻译为监视器或管程,如果涉及到操作系统,Monitor通常翻译为管程。每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Workd就被设置指向Monitor对象的指针

Monitor大致结构如下:

  • WaitSet:当有线程调用wait()方法时,线程将会进入到waiting中进行等待唤醒。
  • EntryList:可以看作是一个阻塞等待队列,非公平。
  • Owner:所有者,如果其中一个线程指向了Owner,那么需要等待这个线程执行完他所要做的任务。这时如果有其他线程(同一个对象)进来,那么需要阻塞等待,进入到EntryLis队列当中。

下图解释: 如果有一个Thread-01的线程进来,那么对象头里面的Mark Word会指向这个监视器,当这个线程执行完所需要执行的任务后,就会唤醒阻塞队列中的某一个线程(Thread-04、Thread-05、Thread-06)。

Waiting与Blocked有什么不同之处?

  • Waiting里面的线程是已经拿到过锁的,只不过因为调用了wait()方法,释放了锁。等待其他线程来调用notify()或notifyAll()方法来进行唤醒,但是唤醒后并不意味着直接可以拿到锁,还是需要进入到EntryList阻塞等待队列中进行竞争
  • Blocked里面的线程从来都没有拿到过锁

注意:

  • 不同对象有不同的监视器
  • 下图中的sychronized必须进入到同一个对象的monitor才有上述的效果
  • 不加synchronized的对象不会关联监视器

Monitor字节码分析
看如下代码:

public class ThreadTest04 {static final Object o=new Object();static int number=0;public static void main(String[] args) {synchronized (o){number++;}}
}

这里分析的main方法里面的字节码

0 getstatic #2    // object引用(从synchronized开始)3 dup   // 复制最高操作位数堆栈值 (这里就是复制一份然后存储到astore_1临时变量当中)4 astore_1  // lock引用给到-> slot 15 monitorenter  // 这里将lock对象 MarkWord置为Monitor指针6 getstatic #3   // 这里对number变量进行操作 9 iconst_1 // 准备常数 number
10 iadd  // 进行++操作
11 putstatic #3  // 赋值给number
14 aload_1   // lock引用
15 monitorexit  // 将lock对象Mark Word重置,唤醒EntryList
16 goto 24 (+8)  // 如果没有异常,直接return结束
19 astore_2   // slot 2 异常 exception对象
20 aload_1    // lock的引用
21 monitorexi t  // 将lock对象Mark Word重置,唤醒EntryList
22 aload_2    // slot 2 (e) exception对象
23 athrow   // 抛出异常 throw e
24 return

异常表:
这里的异常会有一个范围从6到16、从19到22 ,如果出现异常就跳转到19行。

好啦,Java对象以及Monitor工作原理,想必大家应该有所收获。

下面我们来讲讲什么是竞态条件和内存可见性?

什么是竞态条件?

竞态条件指的是当多个线程访问和操作同一个对象时,最终结果与执行顺序有关,可能正确也可能不正确。看下面代码。

public class ThreadTest02 extends Thread {private static int number=0;@Overridepublic void run() {for(int i=0;i<1000;i++){number++;}}public static void main(String[] args) throws InterruptedException {int num=1000;Thread[] t=new Thread[num];for (int i = 0; i <num ; i++) {t[i]=new ThreadTest02();t[i].start();}for (int i=0;i<num;i++){t[i].join(); // 为了让main线程等待他们执行完,然后输出此结果}System.out.println(number);}
}

运行结果:

这段代码很容易理解,有一个共享静态变量number,初始值为0,在main方法中创建了1000个线程,每个线程对counter循环加1000次,main线程等待所有线程结束后输出counter的值。 期望的结果是100万,但实际执行,每次输出的结果都不一样,大多数情况下是99万吧。为什么会这样?这是因为number++这个操作不是原子操作。

  • 首先去number的当前值
  • 在当前值的基础上加1
  • 将新值重新复制给number

因为竞态条件的产生可能会出现某两个线程同时执行第一步,取到了相同的number值,比如都取到了50,第一个线程执行完后number变为51,而第二个线程执行完后还是51,最终的结果就与期望不符。此时如果要解决这个问题,有多种方案,这里就是用synchronized解决。
解决方案:

public class ThreadTest02 extends Thread {private static int number=0;@Overridepublic void run() {// 这里使用的synchronized的代码块synchronized (""){for(int i=0;i<1000;i++){number++;}}}public static void main(String[] args) throws InterruptedException {int num=1000;Thread[] t=new Thread[num];for (int i = 0; i <num ; i++) {t[i]=new ThreadTest02();t[i].start();}for (int i=0;i<num;i++){t[i].join(); // 加入线程,谁调用让谁加入}System.out.println(number);}
}

上述代码中,synchronized参数中我使用的锁是同一个对象,我没有去使用this,因为在循环当中,我是new了1000个对象,所以去调用start的方法的是不同的对象,所以在这里使用this起不到任何用处。
如果还不是很懂,那么我在举一个生活当中的案例。

卖票案例

public class TicketTest {public static void main(String[] args){Ticket t = new Ticket();for(int i=0;i<4;i++){  // 模拟4家卖票机构new Thread(()-> {try {t.sale();} catch (InterruptedException e) {e.printStackTrace();}},i+"").start();}}
}
class Ticket{// 假如一共100张票private static int ticketNumber=100;public void sale() throws InterruptedException {while(true){if(ticketNumber>0) {Thread.sleep(100);  // 这里停顿,是为了模拟出票的时间System.out.println("线程"+Thread.currentThread().getName()+"卖出了1张票还剩"+--ticketNumber+"张");}else{break;}}}
}

运行结果:

代码里面它们有一个共享的变量ticketNumber,初始化的值为100,main方法中创建了4个线程,每个线程启动后,都会对ticketNumber不停的-1,直到为0停止。

但是当运行出来后,结果与我们期望的结果不一致。为什么呢?

因为竞态条件的产生,可能会有多个线程同时执行第一步,取到了相同的ticketNumber值,比如第一个线程取到了100减去了1,还剩99张票。第二线程还是从100的基础上减1,没有在第一个线程执行后的结果后减1。导致出现了同一张票重复销售的情况。解决这种问题,可以尝试加锁,一种方案是使用synchronized

解决方案 (synchronized代码块)

public class TicketTest {public static void main(String[] args){Ticket t = new Ticket();for(int i=0;i<4;i++){  // 模拟4家卖票机构new Thread(()-> {try {t.sale();} catch (InterruptedException e) {e.printStackTrace();}},i+"").start();}}
}
class Ticket{// 假如一共100张票private static int ticketNumber=100;public void sale() throws InterruptedException {while(true){synchronized (this){if(ticketNumber==0){return;  // 当其中一个线程获取锁后,先检查票数是否为0,如果为0直接return}if(ticketNumber>0) {Thread.sleep(100);  // 这里停顿,是为了模拟出票的时间System.out.println("线程"+Thread.currentThread().getName()+"卖出了1张票还剩"+--ticketNumber+"张");}else{break;}}if(ticketNumber==0){System.out.println("车票已售空!!!");}}}
}

结果展示:

这里没有使用synchronized修饰sale方法,因为不适合模拟抢票案例。目的是为了让多个线程同时去卖票。如果在方法中使用synchronized,那么其中一个线程会一直占有锁,其他线程只能被阻塞。大家可自行去尝试。

什么是内存可见性?

内存可见性就是多个线程共享访问和操作相同的变量,但一个线程对一个共享变量的修改,另一个线程并不能马上被看到,甚至永远也看不到。


public class ThreadTest01 {private  static  boolean flag=false;public static void main(String[] args) throws InterruptedException {Thread01 t=new Thread01();t.start();Thread.sleep(1000); // 主线程休息1秒flag=true;System.out.println("主线程修改flag值,主线程结束");}static class Thread01 extends Thread{@Overridepublic void run() {while(!flag){/* try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}*///System.out.println("1");}System.out.println("子线程结束");}}
}

当我们去运行此代码时,你会发现主线程运行完毕,并修改了flag的值,子线程并没有结束。为什么会这样呢?
当主线程开始运行的时候,flag为false,并创建了一个子线程,这个子线程会将flag复制到运行的内存中,子线程在运行时,flag一直为false。进入while后,条件一直为true。主线程休息一会后,将flag变为了true,但是影响不了子线程的运行内存中的flag值,因此flag在子线程中一直为false。所以会陷入死循环。

在计算机的系统中,除了内存。数据还会被缓存在CPU的寄存器以及各种缓存中,当访问一个变量时,可能直接从寄存器或CPU缓存中获取,而不一定到内存中去取,当修改一个变量时,也可能是先写到缓存中,稍后才会同步更新到内存中。在单线程的程序中,这一般不是问题。但是在多线程的程序中,尤其是在有很多CPU的情况下,这就是严重的问题。一个线程对内存的修改,另一个线程看不到,一是修改没有及时同步到内存,二是另一个线程根本就没从内存读。

如何解决上述问题:

  1. synchronized或显示锁同步
  2. volatile关键字

synchronized解决上述问题

public class ThreadTest01 {private  static  boolean flag=false;public static void main(String[] args) throws InterruptedException {Thread01 t=new Thread01();t.start();Thread.sleep(1000); // 主线程休息1秒flag=true;System.out.println("主线程修改flag值,主线程结束");}static class Thread01 extends Thread{@Overridepublic void run() {while(!flag){synchronized (this){}/* try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}*///System.out.println("1");}System.out.println("子线程结束");}}
}

其实还有两种方法,在while循环中除了使用了synchronized的代码块,还有一个定时休眠以及一个打印语句(这两种方法我注释了)。后两种方法也能够结束循环。
具体原因:

先看一下多线程下的内存模型


对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。

println()方法为什么会结束循环?

我们来看一下println()方法的源码

public void println(String x) {synchronized (this) {print(x);newLine();}}

println方法中被synchronized加锁了。他会做出以下操作:

  • 获取同步锁
  • 清空内存
  • 从主内存中拷贝新的对象副本到工作线程中
  • 继续执行代码,刷新主内存的数据
  • 释放同步锁

在清空内存刷新内存的过程中,子线程有这么一个操作:获取锁到释放锁。子线程的Flag就变成了true(从主内存拷贝对象副本到线程工作内存中),所以就跳出了循环。指令重排序的情况也就不会出现了,这也是volatile关键字的两种特性之一,所以使用volatile关键字修饰flag变量也能解决此问题。

sleep()方法为什么也能结束循环?

子线程调用sleep()时,线程虽然休眠了,但是对象的机锁没有被释放。当锁释放后,又会从从主内存拷贝对象副本到线程工作内存中。

不过,如果只是为了保证内存可见性,使用synchronize的成本有点高,有一个轻量级的方式,那就是使用volatile关键字去修饰这个flag变量。具体volatile是做什么的?这里就不解释了。因为此文章是针对于synchronized,后期我会出一篇关于volatile关键字的文章。

死锁问题

使用synchronized,要注意死锁。所谓的死锁就是类似这种线程,比如有a和b两个线程。a持有锁对象lockA,b持有锁对象lockB,b在等待锁lockA时,a线程和b线程都陷入了相互等待,最后谁都执行不下去。

这种情况,应该尽量避免在持有在持有一个锁的同时去申请另一个锁,如果确实需要多个锁,所有代码都应该按照相同的顺序去申请锁。可以约定都先申请lockA,在申请lockB。

public class ThreadTest05 {private static Object lockA=new Object();private static Object lockB=new Object();private static void threadA(){new Thread(()->{synchronized (lockA){try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}synchronized (lockB){}}}).start();}private static void threadB(){new Thread(()->{synchronized (lockB){try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}synchronized (lockA){}}}).start();}public static void main(String[] args) {threadA();threadB();}
}

解决:

  • 应该尽量避免在持有一个锁的同时去申请另一个锁,如果确实需要多个锁,所有代码都应该按照相同的顺序去申请锁。
  • 使用显式锁接口Lock,它支持尝试获取锁(tryLock)和带时间限制的获取锁方法,使用这些方法可以在获取不到锁的时候释放已经持有的锁,然后再次尝试获取锁或干脆放弃,以避免死锁
// 可以都先约定好,都先申请了lockA,在去申请lockB
public class ThreadTest05 {private static Object lockA=new Object();private static Object lockB=new Object();private static void threadA(){new Thread(()->{synchronized (lockA){try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}synchronized (lockB){}}}).start();}private static void threadB(){new Thread(()->{synchronized (lockA){try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}synchronized (lockB){}}}).start();}public static void main(String[] args) {threadA();threadB();}
}

总结:

  • synchronized可以保证原子性操作
  • synchronized可以保证内存可见性,在释放锁时,所有写入都会写回内存,而获得锁后,都会从内存中读取最新的数据。如果只是简单操作变量的话,可以用volatile修饰该变量,替代synchronized来减少成本。
  • 使用synchronized要注意死锁问题
  • 可重入性:每个锁关联一个线程持有者和一个计数器。当计数器为0时表示该锁没有被任何线程持有,那么任何线程都都可能获得该锁而调用相应方法。当一个线程请求成功后,JVM会记下持有锁的线程,并将计数器计为1。此时其他线程请求该锁,则必须等待。而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增。当线程退出一个synchronized方法/块时,计数器会递减,如果计数器为0则释放该锁

一位还未大学毕业的老程序员,知识有限,有不对的地方,希望大家告知于我。我们一起进步,加油!!!

synchronized背后不为人知的秘密相关推荐

  1. 潜伏研发群一个月,我发现了程序员不为人知的秘密!这也太可爱了吧

    文章来源于网易号丨InfoQ:Q妹,文章未删改 在公司研发群潜伏了一个月后,Q妹发现了一些不为人知的秘密,这群程序员着实让人上头- (一) 他们没有<吐槽大会>中码农庞博 那般能说会道,高 ...

  2. 揭秘360背后不为人知的产品文化

    揭秘360背后不为人知的产品文化 文/易北辰 最近创投圈流行"论资排辈",王石.柳传志84级创业者,朱新礼.俞敏洪92级创业者,98-00级是超黄金一代,涌现马云.王志东.张朝阳. ...

  3. 潜伏研发群一个月,我发现了程序员不为人知的秘密

    一 在公司研发群潜伏了一个月后 Q妹发现了一些不为人知的秘密 这群程序员着实让人上头- 他们没有<吐槽大会>中码农庞博 那般能说会道,高大帅气 相反,有着鲜明个性且具有辨识度的他们 是一群 ...

  4. 天猫店群还能做多久?天猫店群不为人知的秘密,揭秘月入十万的传言!

    天猫店群还能做多久?天猫店群不为人知的秘密,揭秘月入十万的传言! 大家好,我是电商火火. 很多人听天猫店群圈内的朋友说,天猫店群项目简单易上手,且能轻轻松松月入十万. 看到别人在做而且赚到了不少钱,想 ...

  5. CC讲坛-大脑疾病背后的秘密-许执恒

    <CC讲坛>第二十期于2017年7月27日在北京东方梅地亚中心M剧场举行,中国科学院遗传与发育生物学研究所研究员许执恒出席并进行题为<大脑疾病背后的秘密>的演讲. 胚胎时期大脑 ...

  6. 云计算背后的秘密(6)-NoSQL数据库的综述

    我本来一直觉得NoSQL其实很容易理解的,我本身也已经对NoSQL有了非常深入的研究,但是在最近准备YunTable的Chart的时候,发现NoSQL不仅非常博大精深,而且我个人对NoSQL的理解也只 ...

  7. 云计算背后的秘密(1)-MapReduce

    之前在IT168上已经写了一些关于云计算误区的文章,虽然这些文章并不是非常技术,但是也非常希望它们能帮助大家理解云计算这一新浪潮,而在最近几天,IT168的唐蓉同学联系了我,希望我能将云计算背后的一些 ...

  8. C#不为人知的秘密-缓冲区溢出

    开场白 各位朋友们,当你们看到网上传播关于微软windows.IE对黑客利用"缓冲区溢出".0day漏洞攻击的新闻,是否有过自己也想试试身手,可惜无从下手的感慨?本文将完全使用C# ...

  9. if快还是switch快?解密switch背后的秘密

    这是我的第 57 篇原创文章 条件判断语句是程序的重要组成部分,也是系统业务逻辑的控制手段.重要程度和使用频率更是首屈一指,那我们要如何选择 if 还是 switch 呢?他们的性能差别有多大?swi ...

最新文章

  1. 【EventBus】EventBus 源码解析 ( 注册订阅者 | 注册订阅方法详细过程 )
  2. 8 -- 深入使用Spring -- 5...1 启用Spring缓存
  3. Nginx的基本介绍反向代理
  4. 06--JDBC各种连接方式的对比
  5. Session 的钝化与活化
  6. 视频内容理解在手淘逛逛中的应用与落地
  7. #ifdef #else #endif 的用法
  8. 理解 RIPv1使用广播更新路由与RIPv2使用组播更新路由的区别
  9. SecureCRT使用小技巧
  10. webgis之geowebcache跨域
  11. 基于词典的社交媒体内容的情感分析(Python实现)
  12. mysql压缩版8.0安装_mysql8.0压缩版安装和配置教程
  13. 刷新页面后怎样让hover样式停留不消失
  14. Idea终端中无法使用maven命令问题解决
  15. mac 长时间锁屏后进入无声音
  16. 最新Java面试题整理!java字符大写转小写
  17. 如何使用xposed强制开启android webview debug模式
  18. 前端开发之SEO(搜索引擎优化)
  19. Java 网络编程之swing图形化QQ聊天室
  20. 将字符串“abc123“转化为 字符串“a21cb3“JAVA实现

热门文章

  1. adprw指令通讯案例_【智】S7200PLC与台达变频器MODBUS简单通讯案例详解
  2. 【iOS】使用NSURLSession网络请求
  3. Spark的核心RDD(Resilient Distributed Datasets弹性分布式数据集)
  4. 怦然心栋-冲刺日志(第1天)
  5. C语言如何在printf中输出百分号%
  6. LaTex 中插入visio图片
  7. 1024分辨率《新少林寺》HD国语中字无水印
  8. 风车签名管理 for Mac版 - 让签名后的APP可以完全管控和实时监测
  9. 半实物仿真测试平台技术背景及总体介绍
  10. python穷举法列举_穷举法应用举例.doc