纯面试 纯文字 看起来乱但适合面试总结
进程:在操作系统中能够独立运行,并且作为资源分配的基本单位。它表示运行中的程序。系统运行一个程序就是一个进程从创建、运行到消亡的过程。
线程:是一个比进程更小的执行单位,能够完成进程中的一个功能,也被称为轻量级进程。一个进程在其执行的过程中可以产生多个线程。
(推荐使用实现Runnable,因为继承只能继承一个,而实现了接口还能可以继承)
1)原子性
原子性指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部都不执行。
2)可见性
可见性指多个线程操作一个共享变量时,其中一个线程对变量进行修改后,其他线程可以立即看到修改的结果。
3)有序性
有序性,即程序的执行顺序按照代码的先后顺序来执行。
Synchronized关键字,Lock锁实现,分布式锁等。
这个问题经常被问到,但还是能从此区分出面试者对Java线程模型的理解程度。start()方法被用来启动新创建的线程,而且start()内部 调用了run()方法,这和直接调用run()方法 的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启 动,start()方法才会启动新线程。start()方法会将新创建的线程交给CPU去调度,CPU可以通过 轮转时间片去执行这个线程,至于是否分给该线程时间片那就看CPU了。
针对操作系统来说,线程有五种状态
① 这两个方法来自不同的类分别是,sleep来自Thread类,和wait来自Object类。 sleep是Thread的静态类方法,谁调用的谁去睡觉,即使在a线程里调用b的sleep方法,实际上还是a去睡觉,要让b线程睡觉要在b的代码中调用sleep。
② 锁: 最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。 sleep不让出系统资源;wait是进入线程等待池等待,出让系统资源,其他线程可以占用CPU。一般wait不会加时间限制,因为如果wait线程的运行资源不够,再出来也没用, 要等待其他线程调用notify/notifyAll唤醒等待池中的所有线程,才会进入就绪队列等待OS分配系统资源。sleep(milliseconds)可以用时间指定使它自动唤醒过来, 如果时间不到只能调用interrupt()强行打断。 Thread.sleep(0)的作用是“触发操作系统立刻重新进行一次CPU竞争”。
③ 使用范围:wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用。 synchronized(x){ x.notify() //或者wait() } 两者的线程状态都从运行—> 阻塞,不同的是sleep方法在阻塞的同时还带着锁,wait方法在阻塞
线程安全问题指的是在某一线程从开始访问到结束访问某一数据期间,该数据被其他的线程所修改,那么对于当前线程而言,该线程就发生了线程安全问题,表现形式为数据的缺失,数据不一致等。
线程安全问题发生的条件:
1)多线程环境下,即存在包括自己在内存在有多个线程。
2)多线程环境下存在共享资源,且多线程操作该共享资源。
3)多个线程必须对该共享资源有非原子性操作。
线程安全问题的解决思路:
1)尽量不使用共享变量,将不必要的共享变量变成局部变量来使用。
2)使用synchronized关键字同步代码块,或者使用jdk包中提供的Lock为操作进行加锁。
3)使用ThreadLocal为每一个线程建立一个变量的副本,各个线程间独立操作,互不影响。
线程的生命周期开销是非常大的,一个线程的创建到销毁都会占用大量的内存。同时如果不合理的创建了多个线程,cup的处理器数量小于了线程数量,那么将会有很多的线程被闲置,闲置的线程将会占用大量的内存,为垃圾回收带来很大压力,同时cup在分配线程时还会消耗其性能。
解决思路:
利用线程池,模拟一个池,预先创建有限合理个数的线程放入池中,当需要执行任务时从池中取出空闲的先去执行任务,执行完成后将线程归还到池中,这样就减少了线程的频繁创建和销毁,节省内存开销和减小了垃圾回收的压力。同时因为任务到来时本身线程已经存在,减少了创建线程时间,提高了执行效率,而且合理的创建线程池数量还会使各个线程都处于忙碌状态,提高任务执行效率,线程池还提供了拒绝策略,当任务数量到达某一临界区时,线程池将拒绝任务的进入,保持现有任务的顺利执行,减少池的压力。
1)死锁,假如线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。多个线程环形占用资源也是一样的会产生死锁问题。
解决方法:
想要避免死锁,可以使用无锁函数(cas)或者使用重入锁(ReentrantLock),通过重入锁使线程中断或限时等待可以有效的规避死锁问题。
2)饥饿,饥饿指的是某一线程或多个线程因为某些原因一直获取不到资源,导致程序一直无法执行。如某一线程优先级太低导致一直分配不到资源,或者是某一线程一直占着某种资源不放,导致该线程无法执行等。
解决方法:
与死锁相比,饥饿现象还是有可能在一段时间之后恢复执行的。可以设置合适的线程优先级来尽量避免饥饿的产生。
3)活锁,活锁体现了一种谦让的美德,每个线程都想把资源让给对方,但是由于机器“智商”不够,可能会产生一直将资源让来让去,导致资源在两个线程间跳动而无法使某一线程真正的到资源并执行,这就是活锁的问题。
阻塞是用来形容多线程的问题,几个线程之间共享临界区资源,那么当一个线程占用了临界区资源后,所有需要使用该资源的线程都需要进入该临界区等待,等待会导致线程挂起,一直不能工作,这种情况就是阻塞,如果某一线程一直都不释放资源,将会导致其他所有等待在这个临界区的线程都不能工作。当我们使用synchronized或重入锁时,我们得到的就是阻塞线程,如论是synchronized或者重入锁,都会在试图执行代码前,得到临界区的锁,如果得不到锁,线程将会被挂起等待,知道其他线程执行完成并释放锁且拿到锁为止。
解决方法:
可以通过减少锁持有时间,读写锁分离,减小锁的粒度,锁分离,锁粗化等方式来优化锁的性能。
在jdk1.6之后引入了锁升级的机制,在1.6之前,synchronized只是一把重量级的锁,要么就是没有锁要么就是全部给,现在不是了,现在分成了四种情况,无锁,偏向锁,然后是轻量级锁和重量级锁,然后无锁就是没有锁的过程,然后这个偏向锁呢它主要是针对一种情况就是,你有一个资源,然后有一些线程去申请,但是这个一些他不是一些,它是只有一个线程去申请了,并且这个线程已经申请到了这个锁,没有其他的线程跟它去争抢这个锁,那么当它进行同步代码块的执行的时候,它就没有必要释放这个锁了,因为它释放锁的过程是需要从用户态转换到内核态的,然后这个过程是需要消耗很多资源的,就是说没有必要释放,然后为了提高性能呢就引入了这个偏向锁,于是当有一个线程去执行,像有资源去争夺这个资源的时候,它就不释放锁了,继续持有这个锁,然后另外的话它就轻量级锁可以从偏向锁去升级到轻量级锁,如果有两个线程去争夺这个资源的话,那么那个偏向锁就会升级的,升级到轻量级锁,轻量级锁它用的是CAS,CAS它先去判断一下这个资源还是否能抢占得到,如果抢占不到的话,它就会进行一段忙等待时间,也是自旋操作,自旋操作一段时候还没有办法获取这个锁的话,那么它就会升级到就是 一个重量级锁了,然后重量级锁就是用一种就像管程实现的,然后或者是那个底层操作什么的,这就是重量级的了,就完全阻塞了
那么CAS会有什么问题
如果CAS失败,会一直循环,如果CAS一直不成功,可能会给CPU带来很大的开销
CAS会有ABA的问题,CAS它也是一个缩写,是Compare and Swap,然后它是有三个值的,一个是预期值,然后还有一个是新的值,然后它先去判断一下当前的这个数据是否和自己的预期是一样的,如果是一样的话说明没有被改变过,然后它就把新的值写入进去,但是如果是一种情况就是,先把这个数据变成A然后变成B再变成A,其实它中间是有一个变成B的过程的,但是如果是用这个CAS判断的话,它判断的是两个A,它就会不能发现这个变幻的过程,这是它的一个问题。但是一般来讲的话CAS都会用版本号或者是时间戳来实现,就可能避免这个问题了,另外就是原子类它底层也是用这个CAS来实现的,所以说原子类它只有增长和减少,它都是自己封装好的并不会让你自己去实现的。(JUC后面对原子类有一个优化一个叫做LongAdder的,也是CAS但是有优化的地方)
start()方法被用来启动新创建的线程,而start()内部调用了run()方法,这和直接调用run()方法的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程
1)是多个线程涉及到多个锁,这些锁存在着交叉,所以可能会导致了一个锁依赖的闭环。
例如:线程在获得了锁A并且没有释放的情况下去申请锁B,这时,另一个线程已经获得了锁B,在释放锁B之前又要先获得锁A,因此闭环发生,陷入死锁循环。
2)默认的锁申请操作是阻塞的。
所以要避免死锁,就要在一遇到多个对象锁交叉的情况,就要仔细审查这几个对象的类中的所有方法,是否存在着导致锁依赖的环路的可能性。总之是尽量避免在一个同步方法中调用其它对象的延时方法和同步方法。
如果线程是因为调用了wait()、sleep()或者join()方法而导致的阻塞,可以中断线程,并且通过抛出InterruptedException来唤醒它;如果线程遇到了IO阻塞,无能为力,因为IO是操作系统实现的,Java代码并没有办法直接接触到操作系统。
两者最大的区别,实现Callable接口的任务线程能返回执行结果,而实现Runnable接口的任务线程不能返回执行结果
Callable接口实现类中run()方法允许将异常向上抛出,也可以直接在内部处理(try…catch); 而Runnable接口实现类中run()方法的异常必须在内部处理掉,不能向上抛出
用过ThreadLocal吗?知道它什么原理吗?
Thread类里面都包含了一个Map,然后每个Map就对应了一个ThreadLocal,我觉得它写的特别厉害,然后这样的话它每次调用一个就是,ThreadLocal的一个类的话它就会调用那个ThreadMap里面的具体的那个ThreadLocal,这样的话它每个线程都对应自己的一个Local的那个对象,
它与synchronied是为了解决多线程下,同时访问共享变量而造成的数据不一致
ThreadLocalMap它的key跟value是什么
key是当前ThreadLocal对象 value是线程存入的对象
ThreadLocal在父子线程之间是可以继承的吗?
不可以,InheritableThreadLocal才可以
在进行导入导出Excel时,可能会有1W多个表导出,这时用多线程同时导出到不同的表单
用多线程接收大量的json数据
小题提交、视频审核、错题库
MQ那个地方就是用到多线程
设置-Xss,就是栈内存的大小,设置的栈的大小决定了函数调用的最大深度
-Xss设置的大小决定了函数调用的深度,如果函数调用的深度大于设置的Xss大小,那么将会套
“java.lang.StackOver”
线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。
这里借用《Java并发编程的艺术》提到的来说一下使用线程池的好处:
如果想让线程池执行任务的话需要实现的Runnable接口或Callable接口。 Runnable接口或Callable接口实现类都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor执行。两者的区别在于 Runnable 接口不会返回结果但是 Callable 接口可以返回结果。