最适合初学者了解的Java多线程与并发基础

前言

本文会介绍Java中多线程与并发的基础,适合初学者食用,如果想看关于多线程与并发稍微进阶一些的内容可以看我的另一篇博客— 《锁》

线程与进程的区别

在计算机发展初期,每台计算机是串行地执行任务的,如果碰上需要IO的地方,还需要等待长时间的用户IO,后来经过一段时间有了批处理计算机,其可以批量串行地处理用户指令,但本质还是串行,还是不能并发执行。如何解决并发执行的问题呢?于是引入了进程的概念,每个进程独占一份内存空间,进程是内存分配的最小单位,相互间运行互不干扰且可以相互切换,现在我们所看到的多个进程“同时"在运行,实际上是进程高速切换的效果。

那么有了线程之后,我们的计算机系统看似已经很完美了,为什么还要进入线程呢?如果一个进程有多个子任务,往往一个进程需要逐个去执行这些子任务,但往往这些子任务是不相互依赖的,可以并发执行,所以需要CPU进行更细粒度的切换。所以就引入了线程的概念,线程隶属于某一个进程,它共享进程的内存资源,相互间切换更快速。

进程与线程的区别

1.进程是资源分配的最小单位,线程是CPU调度的最小单位。所有与进程相关的资源,均被记录在PCB中。

2.线程隶属于某一个进程,共享所属进程的资源。线程只由堆栈寄存器、程序计数器和TCB构成。

3.进程可以看作独立的应用,线程不能看作独立的应用。

4.进程有独立的地址空间,相互不影响,而线程只是进程的不同执行路径,如果线程挂了,进程也就挂了。所以多进程的程序比多线程程序健壮,但是切换消耗资源多。

Java中进程与线程的关系

1.运行一个程序会产生一个进程,进程至少包含一个线程。

2.每个进程对应一个JVM实例,多个线程共享JVM中的堆。

3.Java采用单线程编程模型,程序会自动创建主线程

4.主线程可以创建子线程,原则上要后于子线程完成执行。

线程的start方法和run方法的区别

  • 区别

    Java中创建线程的方式有两种,不管使用继承Thread的方式还是实现Runnable接口的方式,都需要重写run方法。调用start方法会创建一个新的线程并启动,run方法只是启动线程后的回调函数,如果调用run方法,那么执行run方法的线程不会是新创建的线程,而如果使用start方法,那么执行run方法的线程就是我们刚刚启动的那个线程。

  • 程序验证

    public class Main {
        public static void main(String[] args) {
            Thread thread = new Thread(new SubThread());
            thread.run();
            thread.start();
        }
        
    }
    class SubThread implements Runnable{
    
        @Override
        public void run() {
            // TODO Auto-generated method stub
            System.out.println("执行本方法的线程:"+Thread.currentThread().getName());
        }
        
    }
    
    run和start区别

Thread和Runnable的关系

  • Thread源码

    Thread
  • Runnable源码

    Runnable
  • 区别

    通过上述源码图,不难看出,Thread是一个类,而Runnable是一个接口,Runnable接口中只有一个没有实现的run方法,可以得知,Runnable并不能独立开启一个线程,而是依赖Thread类去创建线程,执行自己的run方法,去执行相应的业务逻辑,才能让这个类具备多线程的特性。

  • 使用继承Thread方式和实现Runable接口方式分别创建子线程

    • 使用继承Thread类方式创建子线程

      public class Main extends Thread{
          public static void main(String[] args) {
              Main main = new Main();
              main.start();
          }
          @Override
          public void run() {
              System.out.println("通过继承Thread接口方式创建子线程成功,当前线程名:"+Thread.currentThread().getName());
          }
          
      }
      

      运行结果:

      Thread方式创建
    • 使用实现Runnable接口方式创建子线程

      public class Main{
          public static void main(String[] args) {
              SubThread subThread = new SubThread();
              Thread thread = new Thread(subThread);
              thread.start();
          }
          
      }
      class SubThread implements Runnable{
      
          @Override
          public void run() {
              // TODO Auto-generated method stub
              System.out.println("通过实现Runnable接口创建子线程成功,当前线程名:"+Thread.currentThread().getName());
          }
          
      }
      

      运行结果:

      Runnable方式创建
    • 使用匿名内部类方式创建子线程

      public class Main{
          public static void main(String[] args) {
              Thread thread = new Thread(new Runnable() {
                  @Override
                  public void run() {
                      // TODO Auto-generated method stub
                      System.out.println("使用匿名内部类方式创建线程成功,当前线程名:"+Thread.currentThread().getName());
                  }
              });
              thread.start();
          }
      }
      

      运行结果:

      匿名内部类方式创建
  • 关系

    1.Thread是实现了Runnable接口的类,使得run支持多线程。2

    2.因类的单一继承原则,推荐使用Runnable接口,可以使程序更加灵活。

如何实现处理多线程的返回值

通过刚才的学习,我们知道多线程的逻辑需要放到run方法中去执行,而run方法是没有返回值的,那么遇到需要返回值的状况就不好解决,那么如何实现子线程返回值呢?

  • 主线程等待法

    通过让主线程等待,直到子线程运行完毕为止。

    实现方式:

    public class Main{
        static String str;
        public static void main(String[] args) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    str="子线程执行完毕";
                }
            });
            thread.start();
            //如果子线程还未对str进行赋值,则一直轮转
            while(str==null) {}
            System.out.println(str);
        }
    }
    
  • 使用Thread中的join()方法

    join()方法可以阻塞当前线程以等待子线程处理完毕。

    实现方式:

    public class Main{
        static String str;
        public static void main(String[] args) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    str="子线程执行完毕";
                }
            });
            thread.start();
            //如果子线程还未对str进行赋值,则一直轮转
            try {
                thread.join();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            System.out.println(str);
        }
    }
    

    join方法能做到比主线程等待法更精准的控制,但是join方法的控制粒度并不够细。比如,我需要控制子线程将字符串赋一个特定的值时,再执行主线程,这种操作join方法是没有办法做到的。

  • 通过Callable接口实现:通过FutureTask或者线程池获取

    在JDK1.5之前,线程是没有返回值的,通常程序猿需要获取子线程返回值颇费周折,现在Java有了自己的返回值线程,即实现了Callable接口的线程,执行了实现Callable接口的线程之后,可以获得一个Future对象,在该对象上调用一个get方法,就可以执行子线程的逻辑并获取返回的Object。

    实现方式1(直接获取 错误示例)

    public class Main implements Callable<String>{
    
        @Override
        public String call() throws Exception {
            // TODO Auto-generated method stub
            String str = "我是带返回值的子线程";
            return str;
        }
        public static void main(String[] args) {
            Main main = new Main();
            try {
                String str = main.call();
             /*这种方式为什么是错误方式?
               和上文说的一样,run()方法和start()方法的区别就在于
               run()方法是线程启动后的回调方法,如果直接调用,相当于没有创建这个线程
               还是由主线程去执行。
               所以这里的call也一样,如果直接调用call,并没有子线程被创建,
               而是相当于直接调用了类中的实例方法,获取了返回值,
               从头到尾并没有子线程的存在。*/
                System.out.println(str);
            } catch (Exception e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
    

    运行结果

    直接获取

    实现方式2(使用FutureTask)

    public class Main implements Callable<String>{
    
        @Override
        public String call() throws Exception {
            // TODO Auto-generated method stub
            String str = "我是带返回值的子线程";
            return str;
        }
        public static void main(String[] args) {
            FutureTask<String> task = new FutureTask<String>(new Main());
            new Thread(task).start();
            try {
                if(!task.isDone()) {
                    System.out.println("任务没有执行完成");
                }
                System.out.println("等待中...");
                Thread.sleep(3000);
                System.out.println(task.get());
                
            } catch (InterruptedException | ExecutionException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
    

    运行结果

    使用FutureTask

    实现方法3(使用线程池配合Future获取)

    public class Main implements Callable<String>{
    
        @Override
        public String call() throws Exception {
            // TODO Auto-generated method stub
            String str = "我是带返回值的子线程";
            return str;
        }
        public static void main(String[] args) throws InterruptedException, ExecutionException {
            ExecutorService newCacheThreadPool = Executors.newCachedThreadPool(); 
            Future<String> future = newCacheThreadPool.submit(new Main());
            if(!future.isDone()) {
                System.out.println("线程尚未执行结束");
            }
            System.out.println("等待中");
            Thread.sleep(300);
            System.out.println(future.get());
            newCacheThreadPool.shutdown();
        }
    }
    

    运行结果

    使用线程池配合Future

线程的状态

Java线程主要分为以下六个状态:新建态(new)运行态(Runnable)无限期等待(Waiting)限期等待(TimeWaiting)阻塞态(Blocked)结束(Terminated)

  • 新建(new)

    新建态是线程处于已被创建但没有被启动的状态,在该状态下的线程只是被创建出来了,但并没有开始执行其内部逻辑。

  • 运行(Runnable)

    运行态分为ReadyRunning,当线程调用start方法后,并不会立即执行,而是去争夺CPU,当线程没有开始执行时,其状态就是Ready,而当线程获取CPU时间片后,从Ready态转为Running态。

  • 等待(Waiting)

    处于等待状态的线程不会自动苏醒,而只有等待被其它线程唤醒,在等待状态中该线程不会被CPU分配时间,将一直被阻塞。以下操作会造成线程的等待:

    1.没有设置timeout参数的Object.wait()方法。

    2.没有设置timeout参数的Thread.join()方法。

    3.LockSupport.park()方法(实际上park方法并不是LockSupport提供的,而是在Unsafe中,LockSupport只是对其做了一层封装,可以看我的另一篇博客《锁》,里面对于ReentrantLock的源码解析有提到这个方法)。

  • 限期等待(TimeWaiting)

    处于限期等待的线程,CPU同样不会分配时间片,但存在于限期等待的线程无需被其它线程显式唤醒,而是在等待时间结束后,系统自动唤醒。以下操作会造成线程限时等待:

    1.Thread.sleep()方法。

    2.设置了timeout参数的Object.wait()方法。

    3.设置了timeout参数的Thread.join()方法。

    4.LockSupport.parkNanos()方法。

    5.LockSupport.parkUntil()方法。

  • 阻塞(Blocked)

    当多个线程进入同一块共享区域时,例如Synchronized块、ReentrantLock控制的区域等,会去整夺锁,成功获取锁的线程继续往下执行,而没有获取锁的线程将进入阻塞状态,等待获取锁。

  • 结束(Terminated)

    已终止线程的线程状态,线程已结束执行。

Sleep和Wait的区别

Sleep和Wait者两个方法都可以使线程进入限期等待的状态,那么这两个方法有什么区别呢?

1.sleep方法由Thread提供,而wait方法由Object提供。

2.sleep方法可以在任何地方使用,而wait方法只能在synchronized块或synchronized方法中使用(因为必须获wait方法会释放锁,只有获取锁了才能释放锁)。

3.sleep方法只会让出CPU,不会释放锁,而wait方法不仅会让出CPU,还会释放锁

测试代码:

public class Main{
    public static void main(String[] args) {
        Thread threadA = new Thread(new ThreadA());
        Thread threadB = new Thread(new ThreadB());
        
        threadA.setName("threadA");
        threadB.setName("threadB");
        
        threadA.start();
        threadB.start();
    }

    public static synchronized void print() {
        System.out.println("当前线程:"+Thread.currentThread().getName()+"执行Sleep");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println("当前线程:"+Thread.currentThread().getName()+"执行Wait");
        try {
            Main.class.wait(1000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println("当前线程:"+Thread.currentThread().getName()+"执行完毕");
    }
}
class ThreadA implements Runnable{
    @Override
    public void run() {
        // TODO Auto-generated method stub
        Main.print();
    }
    
}
class ThreadB implements Runnable{
    @Override
    public void run() {
        // TODO Auto-generated method stub
        Main.print();
    }
    
}

执行结果:

sleep和wait的区别

从上面的结果可以分析出:当线程A执行sleep后,等待一秒被唤醒后继续持有锁,执行之后的代码,而执行wait之后,立即释放了锁,不仅让出了CPU还让出了锁,而后线程B立即持有锁开始执行,和线程A执行了同样的步骤,当线程B执行wait方法之后,释放锁,然后线程A拿到锁打印了第一个执行完毕,然后线程B打印执行完毕。

notify和notifyAll的区别

  • notify

    notify可以唤醒一个处于等待状态的线程,上代码:

    public class Main{
        public static void main(String[] args) {
            Object lock = new Object();
            Thread threadA = new Thread(new Runnable() {
                
                @Override
                public void run() {
                    synchronized (lock) {
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                        print();
                        
                    }
                }
            });
            Thread threadB = new Thread(new Runnable() {
                
                @Override
                public void run() {
                    synchronized (lock) {
                        print();
                        lock.notify();
                    }
                    
                }
            });
            
            threadA.setName("threadA");
            threadB.setName("threadB");
            
            threadA.start();
            threadB.start();
        }
    
        public static void print() {
                System.out.println("当前线程:"+Thread.currentThread().getName()+"执行print");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                System.out.println("当前线程:"+Thread.currentThread().getName()+"执行完毕");
            
        }
    }
    

    执行结果:

    notify

    代码解释:线程A在开始执行时立即调用wait进入无限等待状态,如果没有别的线程来唤醒它,它将一直等待下去,所以此时B持有锁开始执行,并且在执行完毕时调用了notify方法,该方法可以唤醒wait状态的A线程,于是A线程苏醒,开始执行剩下的代码。

  • notifyAll

    notifyAll可以用于唤醒所有等待的线程,使所有处于等待状态的线程都变为ready状态,去重新争夺锁。

    public class Main{
        public static void main(String[] args) {
            Object lock = new Object();
            Thread threadA = new Thread(new Runnable() {
                
                @Override
                public void run() {
                    synchronized (lock) {
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                        print();
                        
                    }
                }
            });
            Thread threadB = new Thread(new Runnable() {
                
                @Override
                public void run() {
                    synchronized (lock) {
                        print();
                        lock.notifyAll();
                    }
                    
                }
            });
            
            threadA.setName("threadA");
            threadB.setName("threadB");
            
            threadA.start();
            threadB.start();
        }
    
        public static void print() {
                System.out.println("当前线程:"+Thread.currentThread().getName()+"执行print");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                System.out.println("当前线程:"+Thread.currentThread().getName()+"执行完毕");
            
        }
    }
    
    

    执行结果:

    notifyAll

    要唤醒前一个例子中的线程A,不光notify方法可以做到,调用notifyAll方法同样也可以做到,那么两者有什么区别呢?

  • 区别

    要说清楚他们的区别,首先要简单的说一下Java synchronized的一些原理,在openjdk中查看java的源码可以看到,java对象中存在monitor锁,monitor对象中包含锁池等待池(这部分的详细内容在另一篇文章《锁》中有详细介绍,这里就简单说一说)

    锁池,假设有多个对象进入synchronized块争夺锁,而此时已经有一个对象获取到了锁,那么剩余争夺锁的对象将直接进入锁池中。

    等待池,假设某个线程调用了对象的wait方法,那么这个线程将直接进入等待池,而等待池中的对象不会去争夺锁,而是等待被唤醒。

    下面可以说notify和notifyAll的区别了:

    notifyAll会让所有处于等待池中的线程全部进入锁池去争夺锁,而notify只会随机让其中一个线程去争夺锁

yield方法

  • 概念

        /**
         * A hint to the scheduler that the current thread is willing to yield
         * its current use of a processor. The scheduler is free to ignore this
         * hint.
         *
         * <p> Yield is a heuristic attempt to improve relative progression
         * between threads that would otherwise over-utilise a CPU. Its use
         * should be combined with detailed profiling and benchmarking to
         * ensure that it actually has the desired effect.
         *
         * <p> It is rarely appropriate to use this method. It may be useful
         * for debugging or testing purposes, where it may help to reproduce
         * bugs due to race conditions. It may also be useful when designing
         * concurrency control constructs such as the ones in the
         * {@link java.util.concurrent.locks} package.
         */
        public static native void yield();
    

    yield源码上有一段长长的注释,其大意是说:当前线程调用yield方法时,会给当前线程调度器一个暗示,当前线程愿意让出CPU的使用,但是它的作用应结合详细的分析和测试来确保已经达到了预期的效果,因为调度器可能会无视这个暗示,使用这个方法是不那么合适的,或许在测试环境中使用它会比较好

    测试:

    public class Main{
        public static void main(String[] args) {
            Thread threadA = new Thread(new Runnable() {
                
                @Override
                public void run() {
                    System.out.println("ThreadA正在执行yield");
                    Thread.yield();
                    System.out.println("ThreadA执行yield方法完成");
                }
            });
            Thread threadB = new Thread(new Runnable() {
                
                @Override
                public void run() {
                    System.out.println("ThreadB正在执行yield");
                    Thread.yield();
                    System.out.println("ThreadB执行yield方法完成");
                    
                }
            });
            
            threadA.setName("threadA");
            threadB.setName("threadB");
            
            threadA.start();
            threadB.start();
        }
    

    测试结果:

    yieldA
    yieldB

    可以看出,存在不同的测试结果,这里选出两张。

    第一种结果:线程A执行完yield方法,让出cpu给线程B执行。然后两个线程继续执行剩下的代码。

    第二种结果:线程A执行yield方法,让出cpu给线程B执行,但是线程B执行yield方法后并没有让出cpu,而是继续往下执行,此时就是系统无视了这个暗示

interrupt方法

  • 中止线程

    interrupt函数可以中断一个线程,在interrupt之前,通常使用stop方法来终止一个线程,但是stop方法过于暴力,它的特点是,不论被中断的线程之前处于一个什么样的状态,都无条件中断,这会导致被中断的线程后续的一些清理工作无法顺利完成,引发一些不必要的异常和隐患,还有可能引发数据不同步的问题。

  • 温柔的interrupt方法

    interrupt方法的原理与stop方法相比就显得温柔的多,当调用interrupt方法去终止一个线程时,它并不会暴力地强制终止线程,而是通知这个线程应该要被中断了,和yield一样,这也是一种暗示,至于是否应该中断,由被中断的线程自己去决定。当对一个线程调用interrupt方法时:

    1.如果该线程处于被阻塞状态,则立即退出阻塞状态,抛出InterruptedException异常。

    2.如果该线程处于running状态,则将该线程的中断标志位设置为true,被设置的线程继续运行,不受影响,当运行结束时由线程决定是否被中断。

线程池

线程池的引入是用来解决在日常开发的多线程开发中,如果开发者需要使用到非常多的线程,那么这些线程在被频繁的创建和销毁时,会对系统造成一定的影响,有可能系统在创建和销毁这些线程所耗费的时间会比完成实际需求的时间还要长。另外,在线程很多的状况下,对线程的管理就形成了一个很大的问题,开发者通常要将注意力从功能上转移到对杂乱无章的线程进行管理上,这项动作实际上是非常耗费精力的。

  • 利用Executors创建不同的线程池满足不同场景的需求

    • newFixThreadPool(int nThreads)

      指定工作线程数量的线程池。

    • newCachedThreadPool()

      处理大量中断事件工作任务的线程池,

      1.试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程。

      2.如果线程闲置的时间超过阈值,则会被终止并移出缓存。

      3.系统长时间闲置的时候,不会消耗什么资源。

    • newSingleThreadExecutor()

      创建唯一的工作线程来执行任务,如果线程异常结束,会有另一个线程取代它。可保证顺序执行任务。

    • newSingleThreadScheduledExecutor()与newScheduledThreadPool(int corePoolSize)

      定时或周期性工作调度,两者的区别在于前者是单一工作线程,后者是多线程

    • newWorkStealingPool()

      内部构建ForkJoinPool,利用working-stealing算法,并行地处理任务,不保证处理顺序。

      Fork/Join框架:把大任务分割称若干个小任务并行执行,最终汇总每个小任务后得到大任务结果的框架。

  • 为什么要使用线程池

    线程是稀缺资源,如果无限制地创建线程,会消耗系统资源,而线程池可以代替开发者管理线程,一个线程在结束运行后,不会销毁线程,而是将线程归还线程池,由线程池再进行管理,这样就可以对线程进行复用。

    所以线程池不但可以降低资源的消耗,还可以提高线程的可管理性。

  • 使用线程池启动线程

    public class Main{
        public static void main(String[] args) {
            ExecutorService newFixThreadPool = Executors.newFixedThreadPool(10);
            newFixThreadPool.execute(new Runnable() {
                
                @Override
                public void run() {
                    // TODO Auto-generated method stub
                    System.out.println("通过线程池启动线程成功");
                }
            });
            newFixThreadPool.shutdown();
        }
    }
    
  • 新任务execute执行后的判断

    要知道这个点首先要先说说ThreadPoolExecutor的构造函数,其中有几个参数:

    1.corePoolSize:核心线程数量。

    2.maximumPoolSize:线程不够用时能创建的最大线程数。

    3.workQueue:等待队列。

    那么新任务提交后会执行下列判断:

    1.如果运行的线程少于corePoolSize,则创建新线程来处理任务,即时线程池中的其它线程是空闲的。

    2.如果线程池中的数量大于等于corePoolSize且小于maximumPoolSize,则只有当workQueue满时,才创建新的线程去处理任务。

    3.如果设置的corePoolSize和maximumPoolSize相同,则创建的线程池大小是固定的,如果此时有新任务提交,若workQueue未满,则放入workQueue,等待被处理。

    4.如果运行的线程数大于等于maximumPoolSize,maximumPoolSize,这时如果workQueue已经满了,则通过handler所指定的策略来处理任务。

  • handler 线程池饱和策略

    AbortPolicy:直接抛出异常,默认。

    CallerRunsPolicy:用调用者所在的线程来执行任务。

    DiscardOldestPolicy:丢弃队列中靠最前的任务,并执行当前任务。

    DiscardPolicy:直接丢弃任务

    自定义。

  • 线程池的大小如何选定

    这个问题并不是什么秘密,在网上各大技术网站均有文章说明,我就拿一个最受认可的写上吧

    CPU密集型:线程数 = 核心数或者核心数+1

    IO密集型:线程数 = CPU核数*(1+平均等待时间/平均工作时间)

    当然这个也不能完全依赖这个公式,更多的是要依赖平时的经验来操作,这个公式也只是仅供参考而已。

结语

本文提供了一些Java多线程和并发方面最最基础的知识,适合初学者了解Java多线程的一些基本知识,如果想了解更多的关于并发方面的内容可以看我的另一篇博客 《锁》

欢迎大家访问我的个人博客:Object's Blog

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

推荐阅读更多精彩内容

  • 本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。 首先讲...
    李欣阳阅读 2,436评论 1 15
  • Java多线程学习 [-] 一扩展javalangThread类 二实现javalangRunnable接口 三T...
    影驰阅读 2,946评论 1 18
  • JUC 原创者:文思,感谢尚硅谷,资料来源于尚硅谷 目录: 1、volatile关键字与内存可见性 2、原子变量与...
    文思li阅读 2,265评论 0 1
  • 来源: https://www.cnblogs.com/albertrui/p/8383799.html 一、前言...
    青青子衿zq阅读 519评论 0 0
  • 关于选择继承Thread还是实现Runnable接口? 其实Thread也是实现Runnable接口的: 复制代码...
    简单应用阅读 487评论 1 1