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. 解决Word出错--一打开就反复重启的问题
  2. spark streaming 入门例子
  3. win32下多线程同步方式之临界区,互斥量,事件对象,信号量
  4. BFE Ingress Controller正式发布!
  5. python time模块计算时长_python time模块详解
  6. Kafka 优化参数 unclean.leader.election.enable
  7. linux shell 读取文件脚本
  8. ORACLE expdp/impdp导出实例
  9. CSS样式:2、超出隐藏控制
  10. 小米虚高的估值泡沫要破了么?
  11. HTTP Status 500 - Could not write content: Object is null 解决方法
  12. 学习 shell —— 相对路径转换为绝对路径
  13. OrCAD的下载与安装的详细步骤
  14. 判断三点方向(顺时针或逆时针)
  15. Python中sep是函数吗?该怎么使用?
  16. 鸿蒙OS应用(HarmonyOS Application)开发常见示例源码
  17. BAT机器学习面试1000道
  18. 关于小米文件管理器的介绍及源码下载
  19. Android 必须知道2018年流行的框架库及开发语言,看这一篇就够了!
  20. 长城内外皆vivo,什么全面屏手机这么大排场?

热门文章

  1. Visio画图打印为pdf另存为eps插入latex后,图片显示不全
  2. cv2最强仿射变换(支持n点对齐,可进行人脸对齐)
  3. Go 语言 for 循环、break、continue 讲解
  4. CWDM光模块和普通光模块的区别
  5. AppleWWDRCA.cer证书安装
  6. Cocos 十年 | 业界大佬齐送祝福,同心至远方
  7. 六大常用分布的矩估计和最大似然估计推导过程
  8. 广义矩估计的一般步骤_广义矩估计.ppt
  9. php中session时间,php中session过期时间的设置方法
  10. springboot读取resources下文件方式