并发和多线程(一)--创建线程的方式

  多线程这个概念,离我们很近,是因为面试的时候无论是笔试还是面试肯定会问到,只是深浅的区别。而工作中只有一些特殊的场景我们才会用到多线程的内容(当然互联网等公司除外)甚至有些开发人员从来没有用过多线程,所以可能又离得很远。但是我们总能在一些大厂包括很普通的公司的面试要求中看到IO、多线程、并发等概念,所以除非你没想过在技术的路上去发展,否则这些是必须掌握的,也是Java的基础。

  我们总是开玩笑的说,"面试造航母,工作拧螺丝",这句话没有错了,但是大环境就是这样,也没什么坏处,能够督促你去学习进步。你不能改变大环境,就只能去努力,这些也是你作为一个程序员的本分。我之前网上有看到说,"面试老是问一些,工作中从来用不到的东西有意思吗?",感觉这句话贼搞笑,真的是为自己菜找借口,为什么不想想自己为啥不会,别人就能回答上来,如果哪天真要你写一些底层的东西,你怎么办,难道这时候你还要去百度吗?

  最近刚找完工作,本人技术一般,但是看到有人写的这个面试评语,还是很反感的,忍不住多BB了几句话。

什么是线程? 

  单个进程中执行中每个任务就是一个线程。线程是进程中执行运算的最小单位。这是百度的定义,线程在Java中就是Thread,启动Main函数就是启动了主线程。

  多线程能够更好的利用CPU等资源,提供更好、更快的响应。多线程带了好处,肯定也有不好的地方。例如线程安全问题,线程上下文切换的开销等。所以我们在说Redis为什么能达到这么高的QPS,也会把单线程说出来,没有线程上下文的切换,简化了算法之类的。

线程上下文切换

  是指存储和恢复CPU状态的过程,通过程序计数器使线程可以从中断点恢复执行,多线程效率很高,但是线程上下文切换有不小的开销。

线程安全

  线程安全是多线程不可避免的东西,主要是对于临界资源的操作,临界资源包括:属性、对象、数据库等(一般调用方法不考虑,因为方法是在栈中执行的,而栈是线程私有)。所以要保证临界资源在同一时刻只有一个线程进行操作,实现线程操作互斥的方式,例如:synchronized和lock等。

创建线程的方式

  这个大家肯定都知道,但是网上写的文章,有说两种,三种,四种,六种。。。面试的时候到底回答几种呢,让人懵逼。

   Oracle官方文档:https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html明确写到两种,继承Thread和实现Runnable,但是很多人可能会说,Callable,线程池,定时器不都可以创建线程吗,但是如果看到源码实现,他们创建线程的方式不会超脱这两种方式。

Thread

public class ThreadClass extends Thread{

    @Override
    public void run() {
        System.out.println("Thread测试");
    }

    public static void main(String[] args) {
        new ThreadClass().start();
    }
}

Runnable

public class RunnableClass implements Runnable{

    @Override
    public void run() {
        System.out.println("Runnable测试");
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new RunnableClass());
        thread.start();
    }
}

Thread和Runnable的区别:

  1、就是继承类和实现接口的区别,因为只能单继承,所以Runnable接口天生就有优势。

  2、我们项目中一般都是使用线程池,线程池的优势就不说了,而使用线程池启动的任务必须Runnable或者Callable,所以这是Runnable的第二优势。

  综上,这两种方式,我们一般尽量使用Runnable创建线程。

Thread和Runnable的本质区别:

  我们查看Thread的源码发现,run()是这样的

//Runnable通过Thread构造函数传入
public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
}

private Runnable target;

@Override
public void run() {
    if (target != null) {
        target.run();
    }
}

  1、继承Thread类,通过debug就知道if(target != null) {target.run()};这部分代码不会走,因为run()被重写了

  2、实现Runnable接口,通过Thread构造函数传入Runnable对象,会走上面的代码。

  所以Runnable最终调用了target.run(),而Thread将run()重写了,这才是他俩本质上的区别。

思考题:同时使用Thread和Runnable两种方式

public static void main(String[] args) {
    new Thread(() ->
        System.out.println("Thread测试")
    ) {
        @Override
        public void run() {
            System.out.println("Thread测试");
        }
    }.start();

}

结果:Thread测试

  上面通过匿名内部类的方式将Runnable传入Thread,以及重写了run()。我们看到结果并没有走Runnable的内容。

原因:

  当我们重写了Run()之后,此时的Run()已经是System.out.println("Thread测试");,而不再是if(target != null) {target.run()};所以,即使把Runnable对象传递到Thread,也不会走target.run()的内容。

  我们此时知道,对于创建线程的方式,改如何回答了。

如何回答创建线程的方式:

  通过Oracle官方文档,知道有两种方式,继承Thread和实现Runnable接口,然后讲一下区别,哪种比较好。然后说本质上就是一种,就是构造Thread类。但是run()的执行单元有两种,把他俩本质上的区别说一下。

  上面的这个回答,个人认为B格还是有的,Oracle官方文档都出来了,你还跟我BB什么Callable和线程池?如果问你线程池,你再进行回答

线程池创建线程

ExecutorService threadPool = Executors.newFixedThreadPool(10);
Thread thread = new Thread(() ->
        System.out.println(Thread.currentThread().getName())
);
for (int i = 0; i < 10; i++) {
    threadPool.submit(thread);
}
threadPool.shutdown();

  上面是随便写的线程池代码,启动了10个线程,当我们查看源码的时候发现

public Thread newThread(Runnable r) {
    Thread t = new Thread(group, r,
                          namePrefix + threadNumber.getAndIncrement(),
                          0);
    if (t.isDaemon())
        t.setDaemon(false);
    if (t.getPriority() != Thread.NORM_PRIORITY)
        t.setPriority(Thread.NORM_PRIORITY);
    return t;
}

  内部还是通过Runnable传入new Thread(),所以说线程池不是一种新的创建线程方式。

  同理,包括Callable,定时器等创建线程的方式,都不可能超脱Thread和Runnable的范围,需要看到本质上。

总结:

  本质上,创建线程的方式只有继承Thread和实现Runnable接口,其他的方式底层实现还是这两种方式。

posted @ 2019-10-19 10:30  Diamond-Shine  阅读(561)  评论(0编辑  收藏  举报