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

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

我们总是开玩笑的说,“面试造航母,工作拧螺丝”,这句话在很多公司来说是没有错了,但是大环境就是这样,寒冬形势一点没有好转的迹象,所以多学点,也没什么坏处,何况这都是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的第二优势。
  
3、Runnable依赖Thread进行初始化

综上,这两种方式,我们一般尽量使用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官方文档都出来了,你就别跟我扯什么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接口,其他的方式底层实现还是这两种方式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值