java_多线程

java_多线程

  1. 线程创建方式;join用法;
  2. sleep和wait区别;
  3. 线程安全和不安全的java集合;
  4. StringBuffer和stringBuilder哪个是线程安全;
  5. hashmap源码;currentHashmap怎么线程安全;hashtable
  6. 同步锁关键字 区别;java1.6对synchronized的优化(偏向锁,轻量级锁(cas),重量级锁)

1. 线程创建方式;join用法;

线程的创建方式如下:

  • 继承Thread类
class MyThread extends Thread{

    //重写run方法
    @Override
    public void run() {
        //任务内容....
        System.out.println("当前线程是:"+Thread.currentThread().getName());
    }
}

// main方法调用
Thread thread = new MyThread();
    //线程启动
thread.start();
  • 实现Runable接口
//实现Runnable接口
class MyTask implements Runnable{

    //重写run方法
    public void run() {
        //任务内容....
        System.out.println("当前线程是:"+Thread.currentThread().getName());
    }
}
// main方法调用
Thread thread = new Thread(new MyTask());
    //线程启动
thread.start();
  • 使用FutureTask callable
class MyCallable implements Callable<Double>{

    @Override
    public Double call() {
         double d = 0;
         try {
             System.out.println("异步计算开始.......");
              d = Math.random()*10;
             d += 1000;
            Thread.sleep(2000);
             System.out.println("异步计算结束.......");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return d;//有返回值
    }
}

// 调用
public class Test {
  public static void main(String[] args) throws InterruptedException, ExecutionException {
    FutureTask<Double> task = new FutureTask(new MyCallable());
    //创建一个线程,异步计算结果
    Thread thread = new Thread(task);
    thread.start();
    //主线程继续工作
    Thread.sleep(1000);
    System.out.println("主线程等待计算结果...");
    //当需要用到异步计算的结果时,阻塞获取这个结果
    Double d = task.get();
    System.out.println("计算结果是:"+d);
}
}

  • 使用线程池
public class MyTest {
    public static void main(String[] args) {
         //创建一个只有一个线程的线程池
         ExecutorService executorService = Executors.newSingleThreadExecutor();
         //创建任务,并提交任务到线程池中
         executorService.execute(new MyRunable("任务1"));
         executorService.execute(new MyRunable("任务2"));
         executorService.execute(new MyRunable("任务3"));
    }
}
class MyRunable implements Runnable{

    private String taskName;

    public MyRunable(String taskName) {
        this.taskName = taskName;
    }

    @Override
    public void run() {
        System.out.println("线程池完成任务:"+taskName);
    }
}

join()方法:等待该线程终止,意思就是如果在主线程中调用该方法时,就会让主线程休眠,调用该方法的线程的run方法执
行完毕后再开始执行主线程,使用该方法时会抛出一个异常;
yield(): 暂停当前正在执行的线程对象,将其置为准备状态,然后和其他线程一起竞争。

关于run()方法的思考

看看下面这种情况:线程类Thread 接收了外部任务,同时又用匿名内部类的方式重写了内部的run()方法,这样岂不是有两个任务,
那么究竟会执行那个任务呢?还是两个任务一起执行呢?

Thread thread = new Thread(new MyTask()){
        @Override
        public void run() {//重写Thread类的run方法
            System.out.println("Thread 类的run方法");
        }
    };
    //线程启动
    thread.start();
//实现Runnable接口
class MyTask implements Runnable{

    //重写run方法
    @Override
    public void run() {
        //任务内容....
        System.out.println("这是Runnable的run方法");
    }
}

运行结果:
Thread 类的run方法

通过上面的结果,可以看出:线程最后执行的是Thread类内部的run()方法,这是为什么呢?我们先来分析一下JDK的Thread源码:

private Runnable target;
public void run() {
    if (target != null) {
        target.run();
    }
}

一切都清晰明了了,Thread类的run方法在没有重写的情况下,是判断一下是否有Runnable 对象传进来,如果有,那么就调用
Runnable 对象里的run方法;否则,就什么都不干,线程结束。所以,针对上面的例子,一旦你继承重写了Thread类的run()
方法,而你又想可以接收Runable类的对象,那么就要加上super.run(),执行没有重写时的run方法,改造的例子如下:

Thread thread = new Thread(new MyTask()){
        @Override
        public void run() {//重写Thread类的run方法
           //调用父类Thread的run方法,即没有重写时的run方法
            super.run();
            System.out.println("Thread 类的run方法");
        }
    };

运行结果:
这是Runnable的run方法
Thread 类的run方法

2. sleep和wait区别;

  • Sleep Thread类静态方法,任何地方调用,必须捕获异常; 不释放同步锁; 用interrupt打断;
  • wait obj方法,在同步块内调用; 释放同步锁; 用notify唤醒;

3. 线程安全和不安全的java集合;

线程安全集合:
Vector HashTable StringBuffer
以Concurrent 开头的集合类,如ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、ConcurrentLinkedQueue 和 ConcurrentLinkedDeque。
以CopyOnWrite 开头的集合类,如CopyOnWriteArrayList、CopyOnWriteArraySet。

线程不安全集合:
ArrayList、LinkedList HashSet、TreeSet HashMap、TreeMap StringBuilder

4. StringBuffer和stringBuilder哪个是线程安全;

StringBuffer是线程安全的

5. hashmap源码;currentHashmap怎么线程安全;hashtable

5.1 hashmap

hashmap1.7: 数组+链表
HashMap1.8: 数组+链表+红黑树

在讲解put方法之前,先看看hash方法,看怎么计算哈希值的。

    static final int hash(Object key) {
        int h;
        /**先获取到key的hashCode,然后进行移位再进行异或运算,为什么这么复杂,不用想肯定是为了减少hash冲突**/
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

下面来看看put方法。

public V put(K key, V value) {
        /**四个参数,第一个hash值,第四个参数表示如果该key存在值,如果为null的话,则插入新的value,最后一个参数,在hashMap中没有用,可以不用管,使用默认的即可**/
        return putVal(hash(key), key, value, false, true);
    }

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        //tab 哈希数组,p 该哈希桶的首节点,n hashMap的长度,i 计算出的数组下标
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //获取长度并进行扩容,使用的是懒加载,table一开始是没有加载的,等put后才开始加载
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        /**如果计算出的该哈希桶的位置没有值,则把新插入的key-value放到此处,此处就算没有插入成功,也就是发生哈希冲突时也会把哈希桶的首节点赋予p**/
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        //发生哈希冲突的几种情况
        else {
            // e 临时节点的作用, k 存放该当前节点的key
            Node<K,V> e; K k;
            //第一种,插入的key-value的hash值,key都与当前节点的相等,e = p,则表示为首节点
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //第二种,hash值不等于首节点,判断该p是否属于红黑树的节点
            else if (p instanceof TreeNode)
                /**为红黑树的节点,则在红黑树中进行添加,如果该节点已经存在,则返回该节点(不为null),该值很重要,用来判断put操作是否成功,如果添加成功返回null**/
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //第三种,hash值不等于首节点,不为红黑树的节点,则为链表的节点
            else {
                //遍历该链表
                for (int binCount = 0; ; ++binCount) {
                    //如果找到尾部,则表明添加的key-value没有重复,在尾部进行添加
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //判断是否要转换为红黑树结构
                        if (binCount >= TREEIFY_THRESHOLD - 1)
                            treeifyBin(tab, hash);
                        break;
                    }
                    //如果链表中有重复的key,e则为当前重复的节点,结束循环
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //有重复的key,则用待插入值进行覆盖,返回旧值。
            if (e != null) {
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        //到了此步骤,则表明待插入的key-value是没有key的重复,因为插入成功e节点的值为null
        //修改次数+1
        ++modCount;
        //实际长度+1,判断是否大于临界值,大于则扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        //添加成功
        return null;
}

添加的步骤如下:

  1. 是否初始化,如没有,则先初始化
  2. 通过hash与数组长度与运算计算的数组下表, 如果计算出的该哈希桶的位置没有值,说明没hash冲突,直接把new一个新node放在该数组下标下。
  3. 如果计算的数组下标已经有值了(p),说明有hash冲突,需要把新key的hash和新key的值与p的hash和p的key值进行对比,分以下几种情况:
    第一种,hash和key都相等,e = p,则表示在首节点冲突,会用新的value替换掉首节点的value;
    第二种,否则,判断p是否是红黑树,如果是则在红黑树里进行添加;
    第三种,否则,说明p是一个数组的首节点的链表结构,遍历这个链表,
    如果中间找到一个hash和key都一样的节点,则用新value替换该链表中的节点value;
    如果找到末尾都没有重复的,则将新节点添加到链表的末尾,然后判断是否需要转为红黑树(默认节点数大于8会转为红黑树)

下面来看看扩容方法resize。

final Node<K,V>[] resize() {
        //把没插入之前的哈希数组做我诶oldTal
        Node<K,V>[] oldTab = table;
        //old的长度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //old的临界值
        int oldThr = threshold;
        //初始化new的长度和临界值
        int newCap, newThr = 0;
        //oldCap > 0也就是说不是首次初始化,因为hashMap用的是懒加载
        if (oldCap > 0) {
            //大于最大值
            if (oldCap >= MAXIMUM_CAPACITY) {
                //临界值为整数的最大值
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //标记##,其它情况,扩容两倍,并且扩容后的长度要小于最大值,old长度也要大于16
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                //临界值也扩容为old的临界值2倍
                newThr = oldThr << 1;
        }
        /**如果oldCap<0,但是已经初始化了,像把元素删除完之后的情况,那么它的临界值肯定还存在,
           如果是首次初始化,它的临界值则为0
        **/
        else if (oldThr > 0)
            newCap = oldThr;
        //首次初始化,给与默认的值
        else {
            newCap = DEFAULT_INITIAL_CAPACITY;
            //临界值等于容量*加载因子
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        //此处的if为上面标记##的补充,也就是初始化时容量小于默认值16的,此时newThr没有赋值
        if (newThr == 0) {
            //new的临界值
            float ft = (float)newCap * loadFactor;
            //判断是否new容量是否大于最大值,临界值是否大于最大值
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        //把上面各种情况分析出的临界值,在此处真正进行改变,也就是容量和临界值都改变了。
        threshold = newThr;
        //表示忽略该警告
        @SuppressWarnings({"rawtypes","unchecked"})
            //初始化
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        //赋予当前的table
        table = newTab;
        //此处自然是把old中的元素,遍历到new中
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                //临时变量
                Node<K,V> e;
                //当前哈希桶的位置值不为null,也就是数组下标处有值,因为有值表示可能会发生冲突
                if ((e = oldTab[j]) != null) {
                    //把已经赋值之后的变量置位null,当然是为了好回收,释放内存
                    oldTab[j] = null;
                    //如果下标处的节点没有下一个元素
                    if (e.next == null)
                        //把该变量的值存入newCap中,e.hash & (newCap - 1)并不等于j
                        newTab[e.hash & (newCap - 1)] = e;
                    //该节点为红黑树结构,也就是存在哈希冲突,该哈希桶中有多个元素
                    else if (e instanceof TreeNode)
                        //把此树进行转移到newCap中
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { /**此处表示为链表结构,同样把链表转移到newCap中,就是把链表遍历后,把值转过去,在置位null**/
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        //返回扩容后的hashMap
        return newTab;
}

扩容方法分为以下几步:

  1. 遍历旧数组,找出数组对应下标的node节点。
  2. 如果这个节点没有下一个元素,说明没有冲突,重新在新数组里计算下标并放入(newTab[e.hash & (newCap - 1)] = e;);
  3. 如果这个节点有下一个元素,如果是红黑树的话,把这个红黑树转移到新数组里;
  4. 否则,是一个链表,通过(e.hash & oldCap)将该链表均分为两个链表,放在newTab[j]和newTab[j + oldCap]的位置。

其他更详细的信息,可以参考如下链接:
https://blog.csdn.net/m0_37914588/article/details/82287191
https://blog.csdn.net/login_sonata/article/details/76598675

5.1 concurrentHashmap

1.7:ReentrantLock + segment + HashEntry
1.8:synchronize + cas + hashEntry

concurrentHashmap之所以线程安全,是因为1.7里用了lock方法,1.8里用了synchronized关键字;

5.3 hashtable

hashtable线程安全是因为每个增删的操作都加了synchronized关键字;
hashtable 的初始容量为11; hashmap为16;

     // Hashtable初始化
     public Hashtable() {
             this(11, 0.75f);
         }
     // HashTable扩容
     rehash(){
     int newCapacity = (oldCapacity << 1) + 1;
     }



     // HashMap 的初始容量为16
     static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
     // HashMap 扩容
     resize() {
     newThr = oldThr << 1;
     }

5.4 如何线程安全的使用HashMap三种方式:

//Hashtable
Map<String, String> hashtable = new Hashtable<>();

//synchronizedMap
Map<String, String> synchronizedHashMap = Collections.synchronizedMap(new HashMap<String, String>());

//ConcurrentHashMap
Map<String, String> concurrentHashMap = new ConcurrentHashMap<>();

6. 同步锁关键字 区别;java1.6对synchronized的优化(偏向锁,轻量级锁(cas),重量级锁)

Java中的每个对象都可以作为锁. 具体变现为以下3中形式.

  • 对于普通同步方法, 锁是当前实例对象.
  • 对于静态同步方法, 锁是当前类的Class对象.
  • 对于同步方法块, 锁是synchronized括号里配置的对象.

JVM基于进入和退出Monitor对象来实现方法同步和代码块同步, 但是两者的实现细节不一样.

  • 代码块同步: 通过使用monitorenter和monitorexit指令实现的.
  • 同步方法: ACC_SYNCHRONIZED修饰

monitorenter指令是在编译后插入到同步代码块的开始位置, 而monitorexit指令是在编译后插入到同步代码块的结束处或异常处.

6.1 synchronized和volatile的区别

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是
立即可见的。
2)禁止进行指令重排序。
volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;
synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。

1.volatile仅能使用在变量级别;
synchronized则可以使用在变量、方法、和类级别的

2.volatile仅能实现变量的修改可见性,并不能保证原子性;
synchronized则可以保证变量的修改可见性和原子性

3.volatile不会造成线程的阻塞;
synchronized可能会造成线程的阻塞。

4.volatile标记的变量不会被编译器优化;
synchronized标记的变量可以被编译器优化

6.2 Java对象头(存储锁类型)

在HotSpot虚拟机中, 对象在内存中的布局分为三块区域: 对象头, 示例数据和对其填充.
对象头中包含两部分: MarkWord 和 类型指针.
如果是数组对象的话, 对象头还有一部分是存储数组的长度.
多线程下synchronized的加锁就是对同一个对象的对象头中的MarkWord中的变量进行CAS操作.

MarkWord

Mark Word用于存储对象自身的运行时数据, 如HashCode, GC分代年龄, 锁状态标志, 线程持有的锁, 偏向线程ID等等.
占用内存大小与虚拟机位长一致(32位JVM -> MarkWord是32位, 64位JVM->MarkWord是64位).

类型指针

类型指针指向对象的类元数据, 虚拟机通过这个指针确定该对象是哪个类的实例.

对象头的长度
长度 内容 说明
32/64bit MarkWord 存储对象的hashCode或锁信息等
32/64bit Class Metadada Address 存储对象类型数据的指针
32/64bit Array Length 数组的长度(如果当前对象是数组)

如果是数组对象的话, 虚拟机用3个字节(32/64bit + 32/64bit + 32/64bit)存储对象头; 如果是普通对象的话, 虚拟机用2字节存储对象头(32/64bit + 32/64bit).

6.3 优化后synchronized锁的分类

级别从低到高依次是:
无锁状态
偏向锁状态
轻量级锁状态
重量级锁状态

锁可以升级, 但不能降级. 即: 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁是单向的.

下面看一下每个锁状态时, 对象头中的MarkWord这一个字节中的内容是什么. 以32位为例.

无锁状态
25bit 4bit 1bit(是否是偏向锁) 2bit(锁标志位)
对象的hashCode 对象分代年龄 0 01
偏向锁状态
23bit 2bit 4bit 1bit 2bit
线程ID epoch 对象分代年龄 1 01
轻量级锁状态
30bit 2bit
指向栈中锁记录的指针 00
重量级锁状态
30bit 2bit
指向互斥量(重量级锁)的指针 10

6.4 锁的升级(进化)

6.4-1.偏向锁

偏向锁是针对于一个线程而言的, 线程获得锁之后就不会再有解锁等操作了, 这样可以省略很多开销. 假如有两个线程来竞争该锁话, 那么偏向锁就失效了, 进而升级成轻量级锁了.
为什么要这样做呢? 因为经验表明, 其实大部分情况下, 都会是同一个线程进入同一块同步代码块的. 这也是为什么会有偏向锁出现的原因.
在Jdk1.6中, 偏向锁的开关是默认开启的, 适用于只有一个线程访问同步块的场景.

偏向锁的加锁

当一个线程访问同步块并获取锁时, 会在锁对象的对象头和栈帧中的锁记录里存储锁偏向的线程ID, 以后该线程进入和退出同步块时不需要进行CAS操作来加锁和解锁, 只需要简单的测试一下锁对象的对象头的MarkWord里是否存储着指向当前线程的偏向锁(线程ID是当前线程), 如果测试成功, 表示线程已经获得了锁; 如果测试失败, 则需要再测试一下MarkWord中偏向锁的标识是否设置成1(表示当前是偏向锁), 如果没有设置, 则使用CAS竞争锁, 如果设置了, 则尝试使用CAS将锁对象的对象头的偏向锁指向当前线程.

偏向锁的撤销

偏向锁使用了一种等到竞争出现才释放锁的机制, 所以当其他线程尝试竞争偏向锁时, 持有偏向锁的线程才会释放锁. 偏向锁的撤销需要等到全局安全点(在这个时间点上没有正在执行的字节码). 首先会暂停持有偏向锁的线程, 然后检查持有偏向锁的线程是否存活, 如果线程不处于活动状态, 则将锁对象的对象头设置为无锁状态; 如果线程仍然活着, 则锁对象的对象头中的MarkWord和栈中的锁记录要么重新偏向于其它线程要么恢复到无锁状态, 最后唤醒暂停的线程(释放偏向锁的线程).

总结

偏向锁在Java6及更高版本中是默认启用的, 但是它在程序启动几秒钟后才激活. 可以使用-XX:BiasedLockingStartupDelay=0来关闭偏向锁的启动延迟, 也可以使用-XX:-UseBiasedLocking=false来关闭偏向锁, 那么程序会直接进入轻量级锁状态.

6.4-2.轻量级锁

当出现有两个线程来竞争锁的话, 那么偏向锁就失效了, 此时锁就会膨胀, 升级为轻量级锁.

轻量级锁加锁

线程在执行同步块之前, JVM会先在当前线程的栈帧中创建用户存储锁记录的空间, 并将对象头中的MarkWord复制到锁记录中. 然后线程尝试使用CAS将对象头中的MarkWord替换为指向锁记录的指针. 如果成功, 当前线程获得锁; 如果失败, 表示其它线程竞争锁, 当前线程便尝试使用自旋来获取锁, 之后再来的线程, 发现是轻量级锁, 就开始进行自旋.

轻量级锁解锁

轻量级锁解锁时, 会使用原子的CAS操作将当前线程的锁记录替换回到对象头, 如果成功, 表示没有竞争发生; 如果失败, 表示当前锁存在竞争, 锁就会膨胀成重量级锁.

总结

总结一下加锁解锁过程, 有线程A和线程B来竞争对象c的锁(如: synchronized(c){} ), 这时线程A和线程B同时将对象c的MarkWord复制到自己的锁记录中, 两者竞争去获取锁, 假设线程A成功获取锁, 并将对象c的对象头中的线程ID(MarkWord中)修改为指向自己的锁记录的指针, 这时线程B仍旧通过CAS去获取对象c的锁, 因为对象c的MarkWord中的内容已经被线程A改了, 所以获取失败. 此时为了提高获取锁的效率, 线程B会循环去获取锁, 这个循环是有次数限制的, 如果在循环结束之前CAS操作成功, 那么线程B就获取到锁, 如果循环结束依然获取不到锁, 则获取锁失败, 对象c的MarkWord中的记录会被修改为重量级锁, 然后线程B就会被挂起, 之后有线程C来获取锁时, 看到对象c的MarkWord中的是重量级锁的指针, 说明竞争激烈, 直接挂起.

解锁时, 线程A尝试使用CAS将对象c的MarkWord改回自己栈中复制的那个MarkWord, 因为对象c中的MarkWord已经被指向为重量级锁了, 所以CAS失败. 线程A会释放锁并唤起等待的线程, 进行新一轮的竞争.

6.4-3.锁的比较
优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗, 和执行非同步代码方法的性能相差无几. 如果线程间存在锁竞争, 会带来额外的锁撤销的消耗. 适用于只有一个线程访问的同步场景
轻量级锁 竞争的线程不会阻塞, 提高了程序的响应速度 如果始终得不到锁竞争的线程, 使用自旋会消耗CPU 追求响应时间, 同步快执行速度非常快
重量级锁 线程竞争不适用自旋, 不会消耗CPU 线程堵塞, 响应时间缓慢 追求吞吐量, 同步快执行时间速度较长
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,755评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,369评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,799评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,910评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,096评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,159评论 3 411
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,917评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,360评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,673评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,814评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,509评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,156评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,123评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,641评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,728评论 2 351

推荐阅读更多精彩内容

  • 一、sychronized介绍 并发时,多个线程需要操作同一个资源,容易导致错误数据的产生,为了解决这个问题,当存...
    落地生涯阅读 3,872评论 0 3
  • 1. 计算机系统 使用高速缓存来作为内存与处理器之间的缓冲,将运算需要用到的数据复制到缓存中,让计算能快速进行;当...
    AI乔治阅读 536评论 0 12
  • 一、背景 对于Java来说我们知道,Java代码首先会编译成Java字节码,字节码被类加载器加载到JVM里,JVM...
    高级java架构师阅读 638评论 0 7
  • 眼看着就又要过年了,但是日子越近,不想回家的心情就越发严重,当然我的年纪不会面临回家被父母被催婚的情况,我家里也不...
    夜雨谣阅读 240评论 0 1
  • 暴雨后的阳光充足至极!陆地上的任何物体都尽情的展露到阳光下。它们渴望被太阳覆盖,但有的也未必喜欢太阳,因为太阳一视...
    方轱辘阅读 141评论 0 1