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