zoukankan      html  css  js  c++  java
  • 并发和多线程(一)--创建线程的方式

      多线程这个概念,离我们很近,是因为面试的时候无论是笔试还是面试肯定会问到,只是深浅的区别。而工作中只有一些特殊的场景我们才会用到多线程的内容(当然互联网等公司除外)甚至有些开发人员从来没有用过多线程,所以可能又离得很远。但是我们总能在一些大厂包括很普通的公司的面试要求中看到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接口,其他的方式底层实现还是这两种方式。

  • 相关阅读:
    Day05_java方法 方法
    Day05_java流程控制 break、continue
    Day05_java流程控制 循环结构
    Day05_java流程控制 switch选择结构
    Day05_java流程控制结构
    Day04_java流程控制 顺序结构
    Day04_java流程控制 用户交换Scanner
    Day03_java基础 JavaDoc生成文档
    Day03_java基础 包机制
    ltib学习抄录
  • 原文地址:https://www.cnblogs.com/huigelaile/p/11703081.html
Copyright © 2011-2022 走看看