多线程面试合集

线程与进程区别

每个正在系统上运行的程序都是一个进程。每个进程包含一到多个线程。线程是一组指令的集合,或者是程序的特殊段,它可以在程序里独立执行。也可以把它理解为代码运行的上下文。所以线程基本上是轻量级的进程,它负责在单个程序里执行多任务。通常由操作系统负责多个线程的调度和执行。
使用线程可以把占据时间长的程序中的任务放到后台去处理,程序的运行速度可能加快,在一些等待的任务实现上如用户输入、文件读写和网络收发数据等,线程就比较有用了。在这种情况下可以释放一些珍贵的资源如内存占用等等。
如果有大量的线程,会影响性能,因为操作系统需要在它们之间切换,更多的线程需要更多的内存空间,线程的中止需要考虑其对程序运行的影响。通常块模型数据是在多个线程间共享的,需要防止线程死锁情况的发生。
总结:进程是所有线程的集合,每一个线程是进程中的一条执行路径。

创建线程5种方法:

1.Thread
2.Runnable
3.使用匿名内部类
4.Callable
5.使用线程池创建线程

使用继承Thread类还是使用实现Runnable接口好?

使用实现实现Runnable接口好,原因实现了接口还可以继续继承,继承了类不能再继承。

常用线程api方法

start() 启动线程
currentThread() 获取当前线程对象
getID() 获取当前线程ID Thread-编号 该编号从0开始
getName() 获取当前线程名称
sleep(long mill) 休眠线程
Stop() 停止线程,

常用线程构造函数

Thread() 分配一个新的 Thread 对象
Thread(String name) 分配一个新的 Thread对象,具有指定的 name正如其名。
Thread(Runable r) 分配一个新的 Thread对象
Thread(Runable r, String name) 分配一个新的 Thread对象

守护线程

Java中有两种线程,一种是用户线程,另一种是守护线程。(主线程,gc线程,用户线程)
用户线程是指用户自定义创建的线程,主线程停止,用户线程不会停止。
守护线程当进程不存在或主线程停止,守护线程也会被停止。
使用setDaemon(true)方法设置为守护线程

多线程运行状态

新建状态、就绪状态、运行状态、阻塞状态及死亡状态。
1)新建状态
当用new操作符创建一个线程时, 例如new Thread(r),线程还没有开始运行,此时线程处在新建状态。 当一个线程处于新生状态时,程序还没有开始运行线程中的代码
2)就绪状态
一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的start()方法。当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法。当start()方法返回后,线程就处于就绪状态。
处于就绪状态的线程并不一定立即运行run()方法,线程还必须同其他线程竞争CPU时间,只有获得CPU时间才可以运行线程。因为在单CPU的计算机系统中,不可能同时运行多个线程,一个时刻仅有一个线程处于运行状态。因此此时可能有多个线程处于就绪状态。对多个处于就绪状态的线程是由Java运行时系统的线程调度程序(thread scheduler)来调度的。
3)运行状态
当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法.
4)阻塞状态
线程运行过程中,可能由于各种原因进入阻塞状态:
1>线程通过调用sleep方法进入睡眠状态;
2>线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者;
3>线程试图得到一个锁,而该锁正被其他线程持有;
4>线程在等待某个触发条件;

5)死亡状态
有两个原因会导致线程死亡:
1>run方法正常退出而自然死亡,
2>一个未捕获的异常终止了run方法而使线程猝死。 为了确定线程在当前是否存活着(就是要么是可运行的,要么是被阻塞了),需要使用isAlive方法。如果是可运行或被阻塞,这个方法返回true; 如果线程仍旧是new状态且不是可运行的, 或者线程死亡了,则返回false.

join()方法作用

join作用是让其他线程变为等待,t1.join();//让其他线程变为等待,直到当前t1线程执行完毕,才释放。
可以将两个交替执行的线程合并为顺序执行的线程。
比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。

需求:
创建一个线程,子线程执行完毕后,主线程才能执行。    
class JoinThread implements Runnable {

  public void run() {
    for (int i = 0; i < 100; i++) {
        System.out.println(Thread.currentThread().getName() + "---i:" + i);
    }
  }
}
public class JoinThreadDemo {

  public static void main(String[] args) {
    JoinThread joinThread = new JoinThread();
    Thread t1 = new Thread(joinThread);
    Thread t2 = new Thread(joinThread);
    t1.start();
    t2.start();
    try {
       //其他线程变为等待状态,等t1线程执行完成之后才能执行join方法。
        t1.join();
    } catch (Exception e) {

    }
    for (int i = 0; i < 100; i++) {
        System.out.println("main ---i:" + i);
    }
  }
}

yield()方法

Thread.yield()方法的作用:暂停当前正在执行的线程,并执行其他线程。(可能没有效果)
yield()让当前正在运行的线程回到可运行状态,以允许具有相同优先级的其他线程获得运行的机会。因此,使用yield()的目的是让具有相同优先级的线程之间能够适当的轮换执行。但是,实际中无法保证yield()达到让步的目的,因为,让步的线程可能被线程调度程序再次选中。
结论:大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。

为什么有线程安全问题?

当多个线程同时共享,同一个全局变量或静态变量,做写的操作时,可能会发生数据冲突问题,也就是线程安全问题。但是做读操作是不会发生数据冲突问题。
案例:需求现在有100张火车票,有两个窗口同时抢火车票,请使用多线程模拟抢票效果。

  • 代码:
class ThreadTrain1 implements Runnable {
  private int count = 100;
  private static Object oj = new Object();

  @Override
  public void run() {
    while (count > 0) {
        try {
            Thread.sleep(50);
        } catch (Exception e) {
            // TODO: handle exception
        }
        sale();
    }
  }

  public void sale() {
    // 前提 多线程进行使用、多个线程只能拿到一把锁。
    // 保证只能让一个线程 在执行 缺点效率降低
    // synchronized (oj) {
        if (count > 0) {
          System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - count + 1) + "票");
          count--;
        }
    // }
  }
}

public class ThreadDemo {
  public static void main(String[] args) {
    ThreadTrain1 threadTrain1 = new ThreadTrain1();
    Thread t1 = new Thread(threadTrain1, "①号窗口");
    Thread t2 = new Thread(threadTrain1, "②号窗口");
    t1.start();
    t2.start();
  }
}

结论发现,多个线程共享同一个全局成员变量时,做写的操作可能会发生数据冲突问题。

线程安全解决办法

问:如何解决多线程之间线程安全问题?
答:使用多线程之间同步synchronized或使用锁(lock)。

问:为什么使用线程同步或使用锁能解决线程安全问题呢?
答:将可能会发生数据冲突问题(线程不安全问题),只能让当前一个线程进行执行。代码执行完成后释放锁,然后才能让其他线程进行执行。这样的话就可以解决线程不安全问题。

问:什么是多线程之间同步?
答:当多个线程共享同一个资源,不会受到其他线程的干扰。

同步代码块

  • 什么是同步代码块?
    就是将可能会发生线程安全问题的代码,给包括起来。
    synchronized(同一个数据){
    可能会发生线程冲突问题
    }
    这就是同步代码块
定义:

synchronized(对象)      //这个对象可以为任意对象 
{ 
   需要被同步的代码 
} 

对象如同锁,持有锁的线程可以在同步中执行 ,没持有锁的线程即使获取CPU的执行权,也进不去 。

  • 同步的前提:
    1,必须要有两个或者两个以上的线程
    2,必须是多个线程使用同一个锁
    3,必须保证同步中只能有一个线程在运行
    好处:解决了多线程的安全问题
    弊端:多个线程需要判断锁,较为消耗资源、抢锁的资源。

    代码样例:
    private static Object oj = new Object();      
    public void sale() {
      // 前提 多线程进行使用、多个线程只能拿到一把锁。
      // 保证只能让一个线程 在执行 缺点效率降低
       synchronized (oj) {
        if (count > 0) {
          System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - count + 1) + "票");
          count--;
        }
       }
    }
    

同步函数

  • 同步函数:在方法上修饰synchronized 称为同步函数。
  • 同步函数用的是什么锁?为什么?
    答:同步函数使用this锁。
    证明方式: 一个线程使用同步代码块(this明锁),另一个线程使用同步函数。如果两个线程抢票不能实现同步,那么会出现数据错误。
  • 代码:
class ThreadTrain2 implements Runnable {
private int count = 100;
public boolean flag = true;
private static Object oj = new Object();

@Override
public void run() {
    if (flag) {

        while (count > 0) {

            synchronized (this) {
                if (count > 0) {
                    try {
                        Thread.sleep(50);
                    } catch (Exception e) {
                        // TODO: handle exception
                    }
                    System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - count + 1) + "票");
                    count--;
                }
            }

        }

    } else {
        while (count > 0) {
            sale();
        }
    }

}

public synchronized void sale() {
    // 前提 多线程进行使用、多个线程只能拿到一把锁。
    // 保证只能让一个线程 在执行 缺点效率降低
    // synchronized (oj) {
    if (count > 0) {
        try {
            Thread.sleep(50);
        } catch (Exception e) {
            // TODO: handle exception
        }
        System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - count + 1) + "票");
        count--;
    }
    // }
  }
}

public class ThreadDemo2 {
  public static void main(String[] args) throws InterruptedException {
    ThreadTrain2 threadTrain1 = new ThreadTrain2();
    Thread t1 = new Thread(threadTrain1, "①号窗口");
    Thread t2 = new Thread(threadTrain1, "②号窗口");
    t1.start();
    Thread.sleep(40);
    threadTrain1.flag = false;
    t2.start();
  }
 }

静态同步函数

静态同步函数:方法上加上static关键字,使用synchronized 关键字修饰,使用类.class文件作为锁对象。
静态的同步函数使用的锁为该函数所属字节码文件对象,可以用 getClass方法获取,也可以用当前 类名.class 表示。

代码样例:

synchronized (ThreadTrain.class) {
        System.out.println(Thread.currentThread().getName() + ",出售 第" + (100 - trainCount + 1) + "张票.");
        trainCount--;
        try {
            Thread.sleep(100);
        } catch (Exception e) {
        }
}

面试常问:
一个线程使用同步函数,另一个线程使用同步代码块(this),能够实现同步。
一个线程使用同步函数,另一个线程使用同步代码块(非this),不能实现同步。
一个线程使用同步函数,另一个线程使用静态同步函数,不能实现同步。
总结:
同步函数使用this锁;
同步代码块可使用任意对象锁或者this锁;
静态同步函数使用类的字节码.class文件锁。

多线程死锁

  • 多线程死锁:同步中嵌套同步,导致锁无法释放

  • 代码:

    class ThreadTrain6 implements Runnable {
    // 这是货票总票数,多个线程会同时共享资源
    private int trainCount = 100;
    public boolean flag = true;
    private Object obj= new Object();
    
    @Override
    public void run() {
      if (flag) {
          while (true) {
              synchronized (obj) {
                  // 锁(同步代码块)在什么时候释放? 代码执行完, 自动释放锁.
                  // 如果flag为true 先拿到 obj锁,在拿到this 锁、 才能执行。
                  // 如果flag为false先拿到this,在拿到obj锁,才能执行。
                  // 死锁解决办法:不要在同步中嵌套同步。
                  sale();
              }
          }
      } else {
          while (true) {
              sale();
          }
      }
    }
    
    public synchronized void sale() {
      synchronized (obj) {
          if (trainCount > 0) {
              try {
                  Thread.sleep(40);
              } catch (Exception e) {
    
              }
              System.out.println(Thread.currentThread().getName() + ",出售 第" + (100 - trainCount + 1) + "张票.");
              trainCount--;
          }
        }
      }
    }
    
    public class DeadlockThread {
      public static void main(String[] args) throws InterruptedException {
        ThreadTrain6 threadTrain = new ThreadTrain6(); // 定义 一个实例
        Thread thread1 = new Thread(threadTrain, "一号窗口");
        Thread thread2 = new Thread(threadTrain, "二号窗口");
        thread1.start();
        Thread.sleep(40);
        threadTrain.flag = false;
        thread2.start();
      }
    }
    

原因分析:
线程一先拿到同步代码块的obj锁,再拿到同步函数的this锁;
线程一先拿到同步函数的this锁,再拿到同步代码块的obj锁,
每个线程都需要对方的锁,但又互不让锁,就会导致死锁。

多线程有三大特性

原子性、可见性、有序性

什么是原子性

即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
一个很经典的例子就是银行账户转账问题: 比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。这2个操作必须要具备原子性才能保证不出现一些意外的问题。
我们操作数据也是如此,比如i = i+1;其中就包括,读取i的值,计算i,写入i。这行代码在Java中是不具备原子性的,则多线程运行肯定会出问题,所以也需要我们使用同步和lock这些东西来确保这个特性了。
原子性其实就是保证数据一致、线程安全一部分,

什么是可见性

当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
若两个线程在不同的cpu,那么线程1改变了i的值还没刷新到主存,线程2又使用了i,那么这个i值肯定还是之前的,线程1对变量的修改线程没看到这就是可见性问题。

什么是有序性

程序执行的顺序按照代码的先后顺序执行。
一般来说处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。如下:
int a = 10; //语句1
int r = 2; //语句2
a = a + 3; //语句3
r = a*a; //语句4
则因为重排序,他还可能执行顺序为 2-1-3-4,1-3-2-4 但绝不可能 2-1-4-3,因为这打破了依赖关系。 显然重排序对单线程运行是不会有任何问题,而多线程就不一定了,所以我们在多线程编程时就得考虑这个问题了。

Java内存模型

共享内存模型指的就是Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入时,能对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。

从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:
1. 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
2. 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。

下面通过示意图来说明这两个步骤:


如上图所示,本地内存A和B有主内存中共享变量x的副本。假设初始时,这三个内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。随后,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1。
从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。

总结
Java内存模型:java内存模型简称jmm,定义了一个线程对另一个线程可见。共享变量存放在主内存中,每个线程都有自己的本地内存,当多个线程同时访问一个数据的时候,可能本地内存没有及时刷新到主内存,所以就会发生线程安全问题。
Java内存结构:是属于jvm内存分配,不要和Java内存模型搞混。

volatile

volatile 关键字的作用是变量在多个线程之间可见。

  • 代码:
classThreadVolatileDemo extends Thread {

  public boolean flag= true;

  @Override

  public void run() {

    System.out.println("开始执行子线程....");

    while (flag) {

    }

    System.out.println("线程停止");

}

  public void setRuning(boolean flag) {

    this.flag= flag;

  }

}

public class ThreadVolatile {

  public static voidmain(String[] args) throws InterruptedException {

    ThreadVolatileDemo threadVolatileDemo = new ThreadVolatileDemo();

    threadVolatileDemo.start();

    Thread.sleep(3000);

    threadVolatileDemo.setRuning(false);

    System.out.println("flag 已经设置成false");

    Thread.sleep(1000);

    System.out.println(threadVolatileDemo.flag);

  }

}

已经将结果设置为fasle为什么?还一直在运行呢。
原因:线程之间是不可见的,读取的是副本,没有及时读取到主内存结果。
解决办法:使用volatile关键字将解决线程之间可见性, 强制线程每次读取该值的时候都去“主内存”中取值。

Volatile非原子性

注意: Volatile非原子性

public class VolatileNoAtomic extends Thread {

  private static volatile int count;

  // private static AtomicInteger count = new AtomicInteger(0);

  private static void addCount() {

    for (int i = 0;i< 1000;i++) {
      count++;
      // count.incrementAndGet();
    }
    System.out.println(count);
  }
  public void run() {
    addCount();
  }
  public static void main(String[] args) {
    VolatileNoAtomic[] arr = new VolatileNoAtomic[100];
    for (int i = 0; i < 10; i++) {
        arr[i] = new VolatileNoAtomic();
    }
    for (int i = 0; i < 10; i++) {
        arr[i].start();
    }
  }
}

运行结果:



结果发现 数据不同步,因为Volatile不用具备原子性。

使用AtomicInteger原子类
AtomicInteger是一个提供原子操作的Integer类,通过线程安全的方式操作加减。(JDK1.5并发包中的类)

public class VolatileNoAtomic extends Thread {
  static int count = 0;
  private static AtomicInteger atomicInteger = new AtomicInteger(0);

  @Override
  public void run() {
    for (int i = 0; i < 1000; i++) {
        //等同于i++
        atomicInteger.incrementAndGet();
    }
    System.out.println(count);
  }

  public static void main(String[] args) {
    // 初始化10个线程
    VolatileNoAtomic[] volatileNoAtomic = new VolatileNoAtomic[10];
    for (int i = 0; i < 10; i++) {
        // 创建
        volatileNoAtomic[i] = new VolatileNoAtomic();
    }
    for (int i = 0; i < volatileNoAtomic.length; i++) {
        volatileNoAtomic[i].start();
    }
  }
}

volatile与synchronized区别

仅靠volatile不能保证线程的安全性。(原子性)
①volatile轻量级,只能修饰变量。
synchronized重量级,还可修饰方法
②volatile只能保证数据的可见性,不能用来同步,因为多个线程并发访问volatile修饰的变量不会阻塞。
synchronized不仅保证可见性,而且还保证原子性(数据一致),因为,只有获得了锁的线程才能进入临界区,从而保证临界区中的所有语句都全部执行。多个线程争抢synchronized锁对象时,会出现阻塞。
③线程安全性
线程安全性包括两个方面:1)可见性,2)原子性。
从上面自增的例子中可以看出:仅仅使用volatile并不能保证线程安全性。而synchronized则可实现线程的安全性。

什么是多线程之间通讯

多线程之间通讯,其实就是多个线程在操作同一个资源,但是操作的动作不同。

多线程之间通讯需求:
第一个线程写入(input)用户,另一个线程取读取(out)用户。实现读一个,写一个操作。

画图演示:



代码实现:

  • 共享资源源实体类
class Res {
  public String userSex;
  public String userName;
}
  • 输入线程资源
class IntThrad extends Thread {
  private Res res;

  public IntThrad(Res res) {
    this.res = res;
  }

  @Override
  public void run() {
    int count = 0;
    while (true) {
            if (count == 0) {
                res.userName = "余胜军";
                res.userSex = "男";
            } else {
                res.userName = "小紅";
                res.userSex = "女";
            }
            count = (count + 1) % 2;
        }
  }
}
  • 输出线程
class OutThread extends Thread {
  private Res res;

  public OutThread(Res res) {
    this.res = res;
  }

  @Override
  public void run() {
    while (true) {
            System.out.println(res.userName + "--" + res.userSex);
    }
  }
}
  • 运行代码
public class ThreadDemo01 {
  public static void main(String[] args) {
    Res res = new Res();
    InputThread inputThread = new InputThread(res);
    OutThrad outThrad = new OutThrad(res);
    inputThread.start();
    outThrad.start();
  }
}
  • 运行结果



    注意:数据发生错乱,造成线程安全问题(消费者可能还没读,生产者就已经修改了)

  • 解决线程安全问题
    1)如果使用volatile,只能保证数据的可见性,不能保证原子性。
    2)如果使用synchronized,虽然可以保证原子性,但是消息会被重复生产与消费。
    如图所示:
    输入线程加上synchronized
class IntThrad extends Thread {
  private Res res;

  public IntThrad(Res res) {
    this.res = res;
  }

  @Override
  public void run() {
    int count = 0;
    while (true) {
        synchronized (res) {
            if (count == 0) {
                res.userName = "余胜军";
                res.userSex = "男";
            } else {
                res.userName = "小紅";
                res.userSex = "女";
            }
            count = (count + 1) % 2;
        }

    }
   }
}

输出线程加上synchronized

class Res {
  public String userName;
  public String sex;
}

class InputThread extends Thread {
  private Res res;

  public InputThread(Res res) {
    this.res = res;
  }

  @Override
  public void run() {
    int count = 0;
    while (true) {
        synchronized (res) {
          if (count == 0) {
            res.userName = "余胜军";
            res.sex = "男";
          } else {
            res.userName = "小红";
            res.sex = "女";
          }
            count = (count + 1) % 2;
        }
    }
  }
}

class OutThrad extends Thread {
  private Res res;

  public OutThrad(Res res) {
    this.res = res;
  }

  @Override
  public void run() {
    while (true) {
        synchronized (res) {
            System.out.println(res.userName + "," + res.sex);
        }
    }
  }
}

public class ThreadDemo01 {
  public static void main(String[] args) {
    Res res = new Res();
    InputThread inputThread = new InputThread(res);
    OutThrad outThrad = new OutThrad(res);
    inputThread.start();
    outThrad.start();
  }
}

解决方法
要同时满足以下三点
1)生产线程生产一个,消费线程立马消费
2)当生产者没用任何生产,消费者不能进行读
3)当消费者没有消费完,生产者不能再继续生产

wait()、notify、notifyAll()方法

wait()、notify()、notifyAll()是三个定义在Object类里的方法,可以用来控制线程的状态。
这三个方法最终调用的都是jvm级的native方法,随着jvm运行平台的不同可能有些许差异。
如果对象调用了wait方法就会使持有该对象的线程把该对象的控制权交出去,然后处于等待状态。
如果对象调用了notify方法就会通知某个正在等待这个对象的控制权的线程可以继续运行。
如果对象调用了notifyAll方法就会通知所有等待这个对象控制权的线程继续运行。
注意:一定要在线程同步中使用,并且是同一个锁的资源。

class Res {
  public String userSex;
  public String userName;
  //线程通讯标识
  public boolean flag = false;
}

class IntThrad extends Thread {
  private Res res;

  public IntThrad(Res res) {
    this.res = res;
  }

  @Override
  public void run() {
    int count = 0;
    while (true) {
        synchronized (res) {
            if (res.flag) {
                try {
                   // 当前线程变为等待,但是可以释放锁
                    res.wait();
                } catch (Exception e) {

                }
            }
            if (count == 0) {
                res.userName = "余胜军";
                res.userSex = "男";
            } else {
                res.userName = "小紅";
                res.userSex = "女";
            }
            count = (count + 1) % 2;
            res.flag = true;
            // 唤醒当前线程
            res.notify();
        }
      }
    }
}

class OutThread extends Thread {
  private Res res;

  public OutThread(Res res) {
    this.res = res;
  }

  @Override
  public void run() {
    while (true) {
        synchronized (res) {
            if (!res.flag) {
                try {
                    res.wait();
                } catch (Exception e) {
                    // TODO: handle exception
                }
            }
            System.out.println(res.userName + "--" + res.userSex);
            res.flag = false;
            res.notify();
        }
    }
  }
}

public class ThreaCommun {
  public static void main(String[] args) {
    Res res = new Res();
    IntThrad intThrad = new IntThrad(res);
    OutThread outThread = new OutThread(res);
    intThrad.start();
    outThread.start();
  }
}

wait与sleep区别

1)对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。
2)sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。在调用sleep()方法的过程中,线程不会释放对象锁。
而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。

JDK1.5-Lock

在 jdk1.5 之后,并发包中新增了 Lock 接口(以及相关实现类)用来实现锁功能,Lock 接口提供了与 synchronized 关键字类似的同步功能,但需要在使用时手动获取锁和释放锁。

  • Lock写法
Lock lock  = new ReentrantLock();
lock.lock();
try{
  //可能会出现线程安全的操作
}finally{
  //一定在finally中释放锁
  //也不能把获取锁在try中进行,因为有可能在获取锁的时候抛出异常
   lock.unlock();
}

Lock 接口与 synchronized 关键字的区别

Lock 接口可以尝试非阻塞地获取锁 当前线程尝试获取锁。如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁。
Lock 接口能被中断地获取锁 与 synchronized 不同,获取到锁的线程能够响应中断,当获取到的锁的线程被中断时,中断异常将会被抛出,同时锁会被释放。
Lock 接口在指定的截止时间之前获取锁,如果截止时间到了依旧无法获取锁,则返回。

Condition用法

Condition的功能类似于在传统的线程技术中的,Object.wait()和Object.notify()的功能。

  • 用法
Condition condition = lock.newCondition();
condition.await();    //类似wait
condition. signal(); //类似notify
  • 代码
class Res {
public String userName;
public String sex;
public boolean flag = false;
Lock lock = new ReentrantLock();
}

class InputThread extends Thread {
  private Res res;
  Condition newCondition;
  public InputThread(Res res,   Condition newCondition) {
    this.res = res;
    this.newCondition=newCondition;
  }

  @Override
  public void run() {
    int count = 0;
    while (true) {
        // synchronized (res) {

        try {
            res.lock.lock();
            if (res.flag) {
                try {
//                      res.wait();
                    newCondition.await();
                } catch (Exception e) {
                    // TODO: handle exception
                }
            }
            if (count == 0) {
                res.userName = "余胜军";
                res.sex = "男";
            } else {
                res.userName = "小红";
                res.sex = "女";
            }
            count = (count + 1) % 2;
            res.flag = true;
//              res.notify();
            newCondition.signal();
         } catch (Exception e) {
            // TODO: handle exception
          }finally {
            res.lock.unlock();
       }
   }

    // }
    }
}

class OutThrad extends Thread {
private Res res;
private Condition newCondition;
public OutThrad(Res res,Condition newCondition) {
    this.res = res;
    this.newCondition=newCondition;
}

@Override
public void run() {
    while (true) {
//          synchronized (res) {
        try {
            res.lock.lock();
            if (!res.flag) {
                try {
//                      res.wait();
                    newCondition.await();
                } catch (Exception e) {
                    // TODO: handle exception
                }
            }
            System.out.println(res.userName + "," + res.sex);
            res.flag = false;
  //                res.notify();
            newCondition.signal();
        } catch (Exception e) {
            // TODO: handle exception
        }finally {
            res.lock.unlock();
        }
//          }
    }

  }
}

public class ThreadDemo01 {

   public static void main(String[] args) {
    Res res = new Res();
    Condition newCondition = res.lock.newCondition();
    InputThread inputThread = new InputThread(res,newCondition);
    OutThrad outThrad = new OutThrad(res,newCondition);
    inputThread.start();
    outThrad.start();
  }
}

如何停止线程?

停止线程思路

  1. 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
  2. 使用stop方法强行终止线程(这个方法不推荐使用,因为stop和suspend、resume一样,也可能发生不可预料的结果)。
  3. 使用interrupt方法中断线程。

什么是ThreadLocal

ThreadLocal提供一个线程的局部变量,访问某个线程拥有自己局部变量。
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

  • ThreadLocal的接口方法
    void set(Object value)设置当前线程的线程局部变量的值。
    public Object get()该方法返回当前线程所对应的线程局部变量。
    public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 1.5新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
    protected Object initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。

  • 案例:创建三个线程,每个线程生成自己独立序列号。
    代码:

class Res {
  // 生成序列号共享变量
  public static Integer count = 0;
  public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
    protected Integer initialValue() {

        return 0;
    };
  };

  public Integer getNum() {
    int count = threadLocal.get() + 1;
    threadLocal.set(count);
    return count;
  }
}


public class ThreadLocaDemo2 extends Thread {
  private Res res;

  public ThreadLocaDemo2(Res res) {
    this.res = res;
  }

  @Override
  public void run() {
    for (int i = 0; i < 3; i++) {
        System.out.println(Thread.currentThread().getName() + "---" + "i---" + i + "--num:" + res.getNum());
    }

  }

  public static void main(String[] args) {
    Res res = new Res();
    ThreadLocaDemo2 threadLocaDemo1 = new ThreadLocaDemo2(res);
    ThreadLocaDemo2 threadLocaDemo2 = new ThreadLocaDemo2(res);
    ThreadLocaDemo2 threadLocaDemo3 = new ThreadLocaDemo2(res);
    threadLocaDemo1.start();
    threadLocaDemo2.start();
    threadLocaDemo3.start();
  }
}

ThreadLocal实现原理

ThreadLocal通过map集合,
map.put(“当前线程”,值);
map.get(“当前线程”);



练习题

设计4个线程,其中两个线程每次对j增加1,另外两个线程对j每次减少1。

public class MultiThread {
  private int j;

  public static void main(String[] args) {
    MultiThread mt = new MultiThread();
    Inc inc = mt.new Inc();
    Dec dec = mt.new Dec();
    // 4个线程(0、1、2、3)
    for (int i = 0; i < 2; i++) {
        Thread t = new Thread(inc);
        t.start();
        t = new Thread(dec);
        t.start();
    }
    // System.exit(0);// 如果报错加上此句
  }

  // 对j增加1的方法
  private synchronized void inc() {
    j++;
    System.out.println(Thread.currentThread().getName() + "-inc:" + j);
  }

  // 对j减少1的方法
  private synchronized void dec() {
    j--;
    System.out.println(Thread.currentThread().getName() + "-dec:" + j);
  }

  // 内部类实现线程
  class Inc implements Runnable {
    public void run() {
        for (int i = 0; i < 100; i++) {
            inc();
        }
    }
  }

  class Dec implements Runnable {
    public void run() {
        for (int i = 0; i < 100; i++) {
            dec();
        }
    }
  }
}

同步容器类:

Vector与ArrayList区别

1.ArrayList是最常用的List实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。数组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要将已经有数组的数据复制到新的存储空间中。当从ArrayList的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。
2.Vector与ArrayList一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一个线程能够写Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,访问它比访问ArrayList慢。
注意: Vector线程安全,底层对方法加上synchronized关键字、ArrayList线程不安全,底层方法未加同步关键字。
Vector源码类——Add方法源码类


ArrayList源码——Add方法源码

HashTable与HashMap

1.HashMap不是线程安全的
HashMap是一个接口,是Map接口的子接口,是将键映射到值的对象,其中键和值都是对象,并且不能包含重复键,但可以包含重复值。HashMap允许null key和null value,而hashtable不允许。
2.HashTable是线程安全的一个Collection。
3.HashMap是Hashtable的轻量级实现(非线程安全的实现),他们都完成了Map接口,主要区别在于HashMap允许空(null)键值(key),由于非线程安全,效率上可能高于HashTable。
HashMap允许将null作为一个entry的key或者value,而HashTable不允许。
HashMap把HashTable的contains方法去掉了,改成containsValue和containsKey。
注意: HashTable线程安全,HashMap线程不安全。
源码分析:原理同Vector与ArrayList

synchronizedMap

Collections.synchronizedMap(hashMap); 将线程不安全的集合变为线程安全集合。
源码分析


ConcurrentHashMap

ConcurrentMap接口下有两个重要的实现 :
1)ConcurrentHashMap
2)ConcurrentskipListMap (支持并发排序功能。弥补ConcurrentHashMap)
ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的HashTable,它们有自己的锁。
只要多个修改操作发生在不同的段上,它们就可以并发进行。
把一个整体分成了16个段Segment,也就是最高支持16个线程的并发修改操作。
这也是在多线程场景时减小锁的粒度从而降低锁竞争的一种方案。
并且代码中大多共享变量使用volatile关键字声明,目的是第一时间获取修改的内容,性能非常好。


CountDownLatch

CountDownLatch类位于java.util.concurrent包下,利用它可以实现类似计数器的功能。
CountDownLatch结果为0, 阻塞变为运行状态。
比如有一个任务A,它要等待其他4个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。

public class Test002 {
  public static void main(String[] args) throws InterruptedException {
    System.out.println("等待子线程执行完毕...");
    CountDownLatch countDownLatch = new CountDownLatch(2);
    new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("子线程," + Thread.currentThread().getName() + "开始执行...");
            countDownLatch.countDown();// 每次减去1
            System.out.println("子线程," + Thread.currentThread().getName() + "结束执行...");
        }
    }).start();
    new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("子线程," + Thread.currentThread().getName() + "开始执行...");
            countDownLatch.countDown();
            System.out.println("子线程," + Thread.currentThread().getName() + "结束执行...");
        }
    }).start();

    countDownLatch.await();// 调用当前方法主线程阻塞  countDown结果为0, 阻塞变为运行状态
    System.out.println("两个子线程执行完毕....");
    System.out.println("继续主线程执行..");
  }
}

CyclicBarrier

CyclicBarrier初始化时规定一个数目,然后计算调用了CyclicBarrier.await()进入等待的线程数。当线程数达到了这个数目时,所有进入等待状态的线程被唤醒并继续。
CyclicBarrier就象它名字的意思一样,可看成是个障碍, 所有的线程必须到齐后才能一起通过这个障碍。
CyclicBarrier初始时还可带一个Runnable的参数, 此Runnable任务在CyclicBarrier的数目达到后,所有其它线程被唤醒前被执行。

class Writer extends Thread {
  private CyclicBarrier cyclicBarrier;
  public Writer(CyclicBarrier cyclicBarrier){
     this.cyclicBarrier=cyclicBarrier;
  }

  @Override
  public void run() {
    System.out.println("线程" + Thread.currentThread().getName() + ",正在写入数据");
    try {
        Thread.sleep(3000);
    } catch (Exception e) {
        // TODO: handle exception
    }
    System.out.println("线程" + Thread.currentThread().getName() + ",写入数据成功.....");
    
    try {
        cyclicBarrier.await();
    } catch (Exception e) {
    }
    System.out.println("所有线程执行完毕..........");
  }
}

public class Test001 {
  public static void main(String[] args) {
    CyclicBarrier cyclicBarrier=new CyclicBarrier(5);
    for (int i = 0; i < 5; i++) {
        Writer writer = new Writer(cyclicBarrier);
        writer.start();
    }
  }
}

Semaphore

Semaphore是一种基于计数的信号量。它可以设定一个阈值,基于此,多个线程竞争获取许可信号,做自己的申请后归还,超过阈值后,线程申请许可信号将会被阻塞。Semaphore可以用来构建一些对象池,资源池之类的,比如数据库连接池,我们也可以创建计数为1的Semaphore,将其作为一种类似互斥锁的机制,这也叫二元信号量,表示两种互斥状态。它的用法如下:
wc.availablePermits(); //用来获取当前可用的资源数量
wc.acquire(); //申请资源
wc.release();// 释放资源

  • 代码结构

      // 创建一个计数阈值为5的信号量对象  
      // 只能5个线程同时访问  
      Semaphore semp = new Semaphore(5);  
        
      try {  
          // 申请许可  
          semp.acquire();  
          try {  
              // 业务逻辑  
          } catch (Exception e) {  
        
          } finally {  
              // 释放许可  
              semp.release();  
          }  
      } catch (InterruptedException e) {  
        
      }  
    

案例:
需求: 一个厕所只有3个坑位,但是有10个人来上厕所,那怎么办?假设10的人的编号分别为1-10,并且1号先到厕所,10号最后到厕所。那么1-3号来的时候必然有可用坑位,顺利如厕,4号来的时候需要看看前面3人是否有人出来了,如果有人出来,进去,否则等待。同样的道理,4-10号也需要等待正在上厕所的人出来后才能进去,并且谁先进去这得看等待的人是否有素质,是否能遵守先来先上的规则。

  • 代码:
class Parent implements Runnable {
  private String name;
  private Semaphore wc;
  public Parent(String name,Semaphore wc){
    this.name=name;
    this.wc=wc;
  }
  @Override
  public void run() {
    try {
        // 剩下的资源(剩下的茅坑)
        int availablePermits = wc.availablePermits();
        if (availablePermits > 0) {
            System.out.println(name+"天主我也,我有茅坑了...");
        } else {
            System.out.println(name+"怎么没有茅坑了...");
        }
        //申请茅坑 如果资源达到3次,就等待
        wc.acquire();
        System.out.println(name+"终于轮我上厕所了..爽啊");
        Thread.sleep(new Random().nextInt(1000)); // 模拟上厕所时间。
        System.out.println(name+"厕所上完了...");            
    } catch (Exception e) {

    }finally{
        wc.release();
    }
  }
}

public class TestSemaphore02 {
  public static void main(String[] args) {
    // 一个厕所只有3个坑位,但是有10个人来上厕所,那怎么办?假设10的人的编号分别为1-10,并且1号先到厕所,10号最后到厕所。那么1-3号来的时候必然有可用坑位,顺利如厕,4号来的时候需要看看前面3人是否有人出来了,如果有人出来,进去,否则等待。同样的道理,4-10号也需要等待正在上厕所的人出来后才能进去,并且谁先进去这得看等待的人是否有素质,是否能遵守先来先上的规则。
     Semaphore semaphore = new Semaphore(3);
    for (int i = 1; i <=10; i++) {
         Parent parent = new Parent("第"+i+"个人,",semaphore);
         new Thread(parent).start();
    }
  }
}

并发队列:

在并发队列上JDK提供了两套实现:
一个是以ConcurrentLinkedQueue为代表的高性能队列,
一个是以BlockingQueue接口为代表的阻塞队列,
无论哪种都继承自Queue。

ConcurrentLinkedDeque

ConcurrentLinkedQueue是一个适用于高并发场景下的队列,通过无锁的方式,实现了高并发状态下的高性能。通常ConcurrentLinkedQueue性能好于BlockingQueue。它是一个基于链接节点的无界线程安全队列,该队列的元素遵循先进先出的原则。头是最先加入的,尾是最近加入的,该队列不允许null元素。
ConcurrentLinkedQueue重要方法:
add() 和offer() 都是加入元素的方法(在ConcurrentLinkedQueue中这两个方法没有任何区别)
poll() 和peek() 都是取头元素节点,区别在于前者会删除元素(出队列),后者不会。

  • 代码示例:
ConcurrentLinkedDeque q = new ConcurrentLinkedDeque();
//入队列
q.offer("张三");
q.offer("李四");
//获取总长度
System.out.println(q.size());
//从头获取元素,删除该元素(出队列)
System.out.println(q.poll());
//从头获取元素,不刪除该元素
System.out.println(q.peek());
//获取总长度
System.out.println(q.size());
  • 输出:
2
张三
李四
1

BlockingQueue

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。
这两个附加的操作是:
在队列为空时,获取元素的线程会等待队列变为非空。
当队列满时,存储元素的线程会等待队列可用。

阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

BlockingQueue即阻塞队列,从阻塞这个词可以看出,在某些情况下对阻塞队列的访问可能会造成阻塞。
被阻塞的情况主要有如下两种:
1)当队列满了的时候进行入队列操作
2)当队列空了的时候进行出队列操作
因此,当一个线程试图对一个已经满了的队列进行入队列操作时,它将会被阻塞,除非有另一个线程做了出队列操作;同样,当一个线程试图对一个空队列进行出队列操作时,它将会被阻塞,除非有另一个线程进行了入队列操作。

在Java中,BlockingQueue的接口位于java.util.concurrent 包中(在Java5版本开始提供),由上面介绍的阻塞队列的特性可知,阻塞队列是线程安全的。
在新增的Concurrent包中,BlockingQueue很好的解决了多线程中,如何高效安全“传输”数据的问题。通过这些高效并且线程安全的队列类,为我们快速搭建高质量的多线程程序带来极大的便利。

常用的队列主要有以下两种:(当然通过不同的实现方式,还可以延伸出很多不同类型的队列,DelayQueue就是其中的一种)
1)先进先出(FIFO):先插入的队列的元素也最先出队列,类似于排队的功能。从某种程度上来说这种队列也体现了一种公平性。
2)后进先出(LIFO):后插入队列的元素最先出队列,这种队列优先处理最近发生的事件。

多线程环境中,通过队列可以很容易实现数据共享,比如经典的“生产者”和“消费者”模型中,通过队列可以很便利地实现两者之间的数据共享。假设我们有若干生产者线程,另外又有若干个消费者线程。如果生产者线程需要把准备好的数据共享给消费者线程,利用队列的方式来传递数据,就可以很方便地解决他们之间的数据共享问题。但如果生产者和消费者在某个时间段内,万一发生数据处理速度不匹配的情况呢?理想情况下,如果生产者产出数据的速度大于消费者消费的速度,并且当生产出来的数据累积到一定程度的时候,那么生产者必须暂停等待一下(阻塞生产者线程),以便等待消费者线程把累积的数据处理完毕,反之亦然。然而,在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。好在此时,强大的concurrent包横空出世了,而它也给我们带来了强大的BlockingQueue。(在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒)

ArrayBlockingQueue

ArrayBlockingQueue是一个有边界的阻塞队列,它的内部实现是一个数组。有边界的意思是它的容量是有限的,我们必须在其初始化的时候指定它的容量大小,容量大小一旦指定就不可改变。
ArrayBlockingQueue是以先进先出的方式存储数据,最新插入的对象是尾部,最新移出的对象是头部。
下面是一个初始化和使用ArrayBlockingQueue的例子:

ArrayBlockingQueue<String> arrays = new ArrayBlockingQueue<String>(3);
arrays.add("李四");
arrays.add("张军");
arrays.add("张军");
// 添加阻塞队列
arrays.offer("张三", 1, TimeUnit.SECONDS);

LinkedBlockingQueue

LinkedBlockingQueue阻塞队列大小的配置是可选的,如果我们初始化时指定一个大小,它就是有边界的,如果不指定,它就是无边界的。说是无边界,其实是采用了默认大小为Integer.MAX_VALUE的容量 。它的内部实现是一个链表。
和ArrayBlockingQueue一样,LinkedBlockingQueue 也是以先进先出的方式存储数据,最新插入的对象是尾部,最新移出的对象是头部。
下面是一个初始化和使LinkedBlockingQueue的例子:

LinkedBlockingQueue linkedBlockingQueue = new LinkedBlockingQueue(3);
linkedBlockingQueue.add("张三");
linkedBlockingQueue.add("李四");
linkedBlockingQueue.add("李四");
System.out.println(linkedBlockingQueue.size());

PriorityBlockingQueue

PriorityBlockingQueue是一个没有边界的队列,它的排序规则和java.util.PriorityQueue一样。需要注意,PriorityBlockingQueue中允许插入null对象。所有插入PriorityBlockingQueue的对象必须实现 java.lang.Comparable接口,队列优先级的排序规则就是按照我们对这个接口的实现来定义的。另外,我们可以从PriorityBlockingQueue获得一个迭代器Iterator,但这个迭代器并不保证按照优先级顺序进行迭代。

SynchronousQueue

SynchronousQueue队列内部仅允许容纳一个元素。当一个线程插入一个元素后会被阻塞,除非这个元素被另一个线程消费。

使用BlockingQueue模拟生产者与消费者

class Producer extends Thread{
  private BlockingQueue queue;
  private volatile boolean flag=true;
  private static AtomicInteger count=new AtomicInteger();

  public Producer(BlockingQueue queue){
    this.queue=queue;
  }
  @Override
  public void run() {
    System.out.println(getName()+"生产者线程启动...");
    try {
        while (flag){
            System.out.println(getName()+"生产者开始生产消息...");
            //如果flag为true,queue就入队列。(原子类进行计数)
            Integer i = count.incrementAndGet();
            boolean offer = queue.offer(i);
            if(offer){
                System.out.println(getName()+"生产者生产生产消息:"+i+"成功");
            }else {
                System.out.println(getName()+"生产者生产生产消息:"+i+"失败");
            }
            Thread.sleep(1000);
        }
    }catch (Exception e){

    }finally {
        System.out.println(getName()+"生产者线程停止...");
    }
  }

  public void stopThread(){
    this.flag=false;
  }
}


class Consumer extends Thread{
  private BlockingQueue queue;
  private volatile boolean flag=true;

  public Consumer(BlockingQueue queue){
    this.queue=queue;
  }
  @Override
  public void run() {
    System.out.println(getName()+"消费者线程启动...");
    try {
        while (flag){
            System.out.println(getName()+"消费者开始消费消息...");
            //如果flag为true,queue就出队列
            Integer poll = (Integer) queue.poll(2, TimeUnit.SECONDS);
            if(poll != null){
                System.out.println(getName()+"消费者获取消息:"+poll+"成功");
            }else {
                System.out.println(getName()+"消费者获取消息:"+poll+"失败");
                this.flag=false;
            }

        }
    }catch (Exception e){

    }finally {
        System.out.println(getName()+"消费者线程停止...");
    }
  }
}

public class ProduceConsumerThread {
  public static void main(String[] args) throws InterruptedException {
    BlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(10);
    Producer p1 =new Producer(queue);
    Producer p2 =new Producer(queue);
    Consumer c1 =new Consumer(queue);

    p1.start();
    p2.start();
    c1.start();

    Thread.sleep(3*1000);
    p1.stopThread();
    p2.stopThread();
  }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容