java基础总结(面试高频问题)一:多线程创建的三种方式,线程安全(线程同步),start和run的区别
前言:最近在做java基础的总结工作,决定把这些java基础中的面试高频考点写出来,方便自己复习,也方便和我一样的新手查阅;笔者之前一直在做android端的opencv图像处理项目,但是由于项目的保密性,截止到上一篇博客,只更新到Android中配置opencv这一步,具体的功能实现会放到后续逐步更新出来;
话不多说,开始总结:
1:java创建多线程有三种方式:
(1):
第一种,通过继承Thread类创建线程类
通过继承Thread类来创建并启动多线程的步骤如下:
1、定义一个类继承Thread类,并重写Thread类的run()方法,run()方法的方法体就是线程要完成的任务,因此把run()称为线程的执行体;
2、创建该类的实例对象,即创建了线程对象;
3、调用线程对象的start()方法来启动线程;
(2):
第二种,通过实现Runnable接口创建线程类
这种方式创建并启动多线程的步骤如下:
1、定义一个类实现Runnable接口;
2、创建该类的实例对象obj;
3、将obj作为构造器参数传入Thread类实例对象,这个对象才是真正的线程对象;
4、调用线程对象的start()方法启动该线程;
(3):
第三种,通过Callable和Future接口创建线程
这种方式创建并启动多线程的步骤如下:
1、创建Callable接口实现类,并实现call()方法,该方法将作为线程执行体,且该方法有返回值,再创建Callable实现类的实例;
2、使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值;
3、使用FutureTask对象作为Thread对象的target创建并启动新线程;(因为FutureTask类继承了Runnable接口,因此可以使用Thread类的对象来启动线程)
4、调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
图解:
可以看到Callable接口只有一个call()方法,那么我们使用时只需要继承接口并实现call()方法;
但是Callable接口并没有继承Runnable接口,无法直接用Thread对象去启动线程,这该怎么办呢?这就需要使用FutureTask类去调用;
可以看到FutureTask类继承自RunnableFuture,而RunnableFuture又继承了Runnable和Future接口,因此FutureTask类的对象是可以被Thread类使用的;
那么FutureTask和Callable接口有什么关系呢?通过源码可以看到,FutureTask类的一个构造方法,其参数就是Callable对象,通过这个构造方法,就将这三者联系了起来;
综上,使用Callable接口创建并启动线程的方法就是下图所示,
第二种第三种的区别:
通过这两个接口创建线程,你要知道这两个接口的作用,下面我们就来了解这两个接口:通过实现Runnable接口创建多线程时,Thread类的作用就是把run()方法包装成线程的执行体,那么,是否可以直接把任意方法都包装成线程的执行体呢?从JAVA5开始,JAVA提供提供了Callable接口,该接口是Runnable接口的增强版,Callable接口提供了一个call()方法可以作为线程执行体,且call()方法比run()方法功能更强大,call()方法的功能的强大体现在:
1、call()方法可以有返回值;
2、call()方法可以声明抛出异常;
从这里可以看出,完全可以提供一个Callable对象作为Thread的target,而该线程的线程执行体就是call()方法。但问题是:Callable接口是JAVA新增的接口,而且它不是Runnable接口的子接口,所以Callable对象不能直接作为Thread的target。还有一个原因就是:call()方法有返回值,call()方法不是直接调用,而是作为线程执行体被调用的,所以这里涉及获取call()方法返回值的问题。
于是,JAVA5提供了Future接口来代表Callable接口里call()方法的返回值,并为Future接口提供了一个FutureTask实现类,该类实现了Future接口,并实现了Runnable接口,所以FutureTask可以作为Thread类的target,同时也解决了Callable对象不能作为Thread类的target这一问题。
2:相关文章链接:
(1)https://blog.****.net/yangyechi/article/details/88079983
(2)https://blog.****.net/baolingye/article/details/90755640?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-3.channel_param&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-3.channel_param
3:确保线程同步的方式(确保线程安全的方式):
(1)为什么要线程同步:
java的虚拟机JVM有主内存(Main Memory)和工作内存(Working Memory)两部分。主内存就是我们所说的堆内存,存放程序中的类实例,静态数据等变量,是线程共享的。而工作内存中存放的是从主内存中拷贝过来的变量以及访问方法所取得的局部变量。是每个线程独立的,其他线程不能访问。
每个线程都有自己的执行空间(工作内存),线程执行时用到某一变量,就会把该变量从主内存中拷贝过来,然后再对其进行操作:赋值,读取,修改等。。。操作完成后再将变量写回主内存。
当有多条线程同时访问主内存的共享数据时,如果不进行同步,就会发生错误。比如线程1获取了主内存的变量A,A的值本来是1,线程1获取后将A的值改成了2。但是线程1还没来得及将改变后的A的值写回主内存,此时线程2也读取了变量A,他读到的值还是1,这就导致了数据错误。因此需要线程同步来保证数据的正确性。java提供了多种机制保证线程同步,这里主要说下synchronized和Lock和volatile;
(2)synchronized(sin科瑞乃z的)关键字:
最简单的方式是加入synchronized关键字,只要将操作共享数据的语句加入synchronized关键字,在某一时段只会让一个线程执行完,在执行过程中,其他线程不能进来执行;
方法声明中使用同步(synchronized )关键字。当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。遵循以下五条原则:
<1>当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
<2>当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。
<3>尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。
<4>当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。那么,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。
<5>以上规则对其它对象锁同样适用。
(3)使用锁,Lock:
区别:
<1>Lock使用起来比较灵活,但需要手动释放和开启;采用synchronized不需要用户去手动释放锁,
当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;
<2>Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;
<3>在并发量比较小的情况下,使用synchronized是个不错的选择,但是在并发量比较高的情况下,其性能下降很严重,此时Lock是个不错的方案。
<4>使用Lock的时候,等待/通知是使用的Condition对象的await()/signal()/signalAll(),而使用synchronized的时候,则是对象的wait()/notify()/notifyAll();由此可以看出,使用Lock的时候,粒度更细了,一个Lock可以对应多个Condition。
<5>虽然Lock缺少了synchronized隐式获取释放锁的便捷性,但是却拥有了锁获取与是释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized所不具备的同步特性;
4:start()和run()的区别:
(1)start方法的作用是启动一个新线程,调用该方法的线程处于就绪(可运行)状态,并没有运行,一旦得到CPU的时间片,就开始执行其中的run方法,这里run方法称为线程体,run方法中包含线程要做的工作内容,当run方法运行结束后,线程也随即终止;start方法不能被重复调用,用start方法来启动线程,真实的实现了多线程运行;主线程接着完成run方法之后的内容,run方法的内容会在新线程中执行;
(2)run方法就和普通的成员方法一样,可以被重复调用;如果直接使用run方法,并不会启动新线程,程序中依旧只有主线程在工作,其程序执行依旧要按照代码顺序去执行,等待run方法中的内容执行结束后,才执行后面的代码,这样就没有达到多线程的目的;
(3)总结:
<1>start可以启动一个新线程,run不能;
<2>start不能被重复调用,run可以重复调用;
<3>start中的run方法可以不执行完就继续执行下面的代码,即进行线程的切换,run方法必须按顺序执行;
<4>start实现了多线程,run没有执行多线程;