因为是即看即整理,为了后期自己查看方便,内容可能会有些杂乱,后期会抽时间进行归类汇总。
一、多线程
1、多线程的作用:
(1)发挥多核cpu的优势。
单核CPU上所谓的"多线程"那是假的多线程,同一时间处理器只会处理一段逻辑,只不过线程之间切换得比较快,看着像多个线程"同时"运行罢了。多核CPU上的多线程才是真正的多线程,它能让你的多段逻辑同时工作,多线程,可以真正发挥出多核CPU的优势来,达到充分利用CPU的目的。
(2)防止阻塞。
(3)便于建模
比如一个大的任务A,如果分解成几个小任务,任务B,任务C,任务D,分别建立程序模型,并通过多线程分别运行这几个任务,就会方便很多。
2、创建线程的方式:
(1)继承Thread类
继承Thread类后重写run方法,通过start方法启动线程
(2) 实现Runnable接口
实现Runnable接口,重写run方法,并把Runnable实例传给Thread对象,Thread的start方法调用run方法,再调用Runnable实例的run方法启动线程。
注意:因为可以实现多个接口,但继承只能继承一个,所以采用方法(2)会是自己的方法更灵活一些,也能减少程序之间的耦合度。
(3)实现Callable接口
实现Callable接口并重写call方法,将callale实例传给FutureTask对象,再把FutureTask对象传给Thread对象。
注意:与(1)(2)的区别就是,(3)有返回值,会返回一个异步处理的结果Future对象并能抛出异常。
3、start()方法与run()方法的区别
start()方法启动了线程,开启了多线程,使不同线程的run()方法里面的代码交替执行。因为run()方法只是Thread的一个普通方法,若没有调用start()方法,只调用了run(),那代码则是同步执行,一个执行完了,才可以执行另外一个。其次,run()方法必须是public权限,返回类型为void。
4、 Runnable接口和Callable接口的区别
Runnable接口没有返回值,callable接口有返回值,可以在多线程运行时获取多线程的运行结果,从而方便程序员决定是否需要在等待时间太长没有获取到需要的数据的情况下取消该线程的任务。
5、CyclicBarrier和CountDownLatch的区别
都在java.util.concurrent下,表示代码运行到某个点上。
区别:
(1)CyclicBarrier在线程运行到某个点之后,会停止运行,知道所有线程都运行到这个点后,所有线程才开始重新运行。CountDownLatch是当某个线程运行到某个点上后,知识给某个数值-1之后,就继续运行。
(2)CyclicBarrier唤起一个任务,CountDownLatch换起多个任务
(3)CyclicBarrier可以重用,CountDownLatch不可重用,计数值为0该CountDownLatch就不可再用了。
6、volatile关键字的作用
(1)volatile修饰的变量,保证了其在多线程之间的可见性,即每次读取到的volatile变量,一定是最新数据。
(2)为了获取更好地性能,jvm可能会对指令进行重排序,则会影响多线程,出现意想不到的问题。所以使用volatile禁止予以重排序,不够也一定程度降低了代码执行效率。
7、什么是线程安全
若你的代码在单线程下执行和在多线程下执行永远获得一样的结果,那么代码就是线程安全的。
8、Java中如何获取到线程dump文件
死循环、死锁、阻塞、页面打开慢等问题,打线程dump是最好的解决问题的途径。所谓线程dump也就是线程堆栈,获取到线程堆栈有两步:
1)获取到线程的pid,可以通过使用jps命令,在Linux环境下还可以使用ps -ef | grep java
2)打印线程堆栈,可以通过使用jstack pid命令,在Linux环境下还可以使用kill -3 pid
另外提一点,Thread类提供了一个getStackTrace()方法也可以用于获取线程堆栈。这是一个实例方法,因此此方法是和具体线程实例绑定的,每次获取获取到的是具体某个线程当前运行的堆栈
9、一个线程如果出现了运行时异常会怎么样
若异常没有被捕获,则该线程会停止执行。注意:若这个线程持有某个对象的监视器,那么这个对象监视器会被立即释放。
10、如何在两个线程之间共享数据
通过线程之间共享对象就可以实现,然后通过wait/notify/notifyAll、await/signal/signalAll进行唤起和等待,比方说阻塞队列BlockingQueue就是为线程之间共享数据而设计的。
11、sleep方法和wait方法有什么区别
sleep()方法不会释放锁,会定时自动唤醒
wait()方法会释放锁,需要notify()或notifyAll()唤醒
12、生产者消费者模型的作用是什么
(1)通过平衡生产者的生产能力和消费者的消费能力来提升整个系统的运行效率,这是生产者消费者模型最重要的作用
(2)解耦,这是生产者消费者模型附带的作用,解耦意味着生产者和消费者之间的联系少,联系越少越可以独自发展而不需要收到相互的制约
13、ThreadLocal有什么用
ThreadLocal是一个线程的本地化对象,是一种用空间换时间的方法,为每一个线程都创建一个本地副本,将数据进行隔离,数据的改变都发生在副本当中,实现了数据不共享,保证了线程安全。
14、为什么wait()方法和notify()/notifyAll()方法要在同步块中被调用
JDK强制的,wait(),notify()和notifyAll()在调用前都必须先获得对象锁。
每个对象都只有一个独占锁,对于对象的非同步方法而言,任意时刻可以有任意个线程调用该方法。但对于对象的同步方法而言,同一时刻只有拥有独占锁的同步方法才可以被调用。从而同步方法中的其他线程就处于阻塞状态。
若一个拥有独占锁的线程调用该对象同步方法的wait()方法,则该线程会释放独占锁,并进入等待队列;当某个线程调用notify(),notifyAll()则将等待队列的线程转移到入口队列,然后让他们竞争用有锁。
所以调用线程本身必须拥有锁,并且在同步方法中调用这三个方法才有意义。
15、wait()方法和notify()/notifyAll()方法在放弃对象监视器时有什么区别
wait()方法会立即释放对象监视器;
notify()/notifyAll()则会等待线程剩余代码执行完毕才会释放对象监视器。
16、为什么要使用线程池
避免频繁地创建和销毁线程,尽量减少创建和销毁的次数,提高服务程序效率。
线程池——实现创建若干个翅中可执行的线程放入一个池中,需要的时候从池中获取线程,不用自行创建,使用完毕不需要销毁,而是放入池中。
16.1创建线程池的四种方式:
(1)newSingleThreadExecutor:创建一个单线程的线程池,用唯一的工作线程来执行任务,保证所有的任务按照指定顺序来执行。
(2)newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
(3)newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
(4)newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
17、怎么检测一个线程是否持有对象监视器
Thread类提供了一个holdsLock(Object obj)方法,当且仅当对象obj的监视器被某条线程持有的时候才会返回true,注意这是一个static方法,这意味着“某条线程”指的是当前线程。
18、synchronized和ReentrantLock的区别
synchronized是关键字,会自动释放锁
ReentrantLock是类,要求程序员手工释放,并且最好在finally 块中释放。作为类,就会更加灵活,可以被继承,可以有方法,可以有各种各样的类变量,体现在以下几点:
(1)可以对获取锁的等待时间进行设置,避免了死锁,如定时锁等候,中断锁等候
(2)可以获得各种锁的信息
(3)可以灵活地实现多路通知
19、ConcurrentHashMap的并发度是什么
ConcurrentHashMap的并发度就是segment的大小,默认为16,这意味着最多同时可以有16条线程操作ConcurrentHashMap。
ConcurrentHashMap利用锁分段技术来保证线程安全。锁分段技术就是将数据分成一段段地存储,然后给每段数据配一把锁。当一个线程占用锁访问其中一个段的数据的时候,其他段的数据也能被其他线程访问。
20、ReadWriteLock是什么
ReadWriteLock是一个读写锁接口,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。
21、FutureTask是什么
FutureTask表示一个异步运算的任务。FutureTask里面可以传入一个Callable的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。当然,由于FutureTask也是Runnable接口的实现类,所以FutureTask也可以放入线程池中。
22、怎么唤醒一个阻塞的线程
如果线程是因为调用了wait()、sleep()或者join()方法而导致的阻塞,可以中断线程,并且通过抛出InterruptedException来唤醒它;如果线程遇到了IO阻塞,无能为力,因为IO是操作系统实现的,Java代码并没有办法直接接触到操作系统。
23、不可变对象对多线程有什么帮助
不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。
24、什么是多线程的上下文切换
多线程的上下文切换是指CPU控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取CPU执行权的线程的过程。
25、如果你提交任务时,线程池队列已满,这时会发生什么
这里区分一下:
1)如果使用的是无界队列LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为LinkedBlockingQueue可以近乎认为是一个无穷大的队列,可以无限存放任务
2)如果使用的是有界队列比如ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue满了,会根据maximumPoolSize的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue继续满,那么则会使用拒绝策略RejectedExecutionHandler处理满了的任务,默认是AbortPolicy
26、Java中用到的线程调度算法是什么
抢占式。一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。
27、Thread.sleep(0)的作用是什么
由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。
28、什么是自旋
很多synchronized里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然synchronized里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在synchronized的边界做忙循环,这就是自旋。如果做了多次忙循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。
29、什么是Java内存模型
1)Java内存模型将内存分为了主内存和工作内存。类的状态,也就是类之间共享的变量,是存储在主内存中的,每次Java线程用到这些主内存中的变量的时候,会读一次主内存中的变量,并让这些内存在自己的工作内存中有一份拷贝,运行自己线程代码的时候,用到这些变量,操作的都是自己工作内存中的那一份。在线程代码执行完毕之后,会将最新的值更新到主内存中去
2)定义了几个原子操作,用于操作主内存和工作内存中的变量
3)定义了volatile变量的使用规则
4)happens-before,即先行发生原则,定义了操作A必然先行发生于操作B的一些规则,比如在同一个线程内控制流前面的代码一定先行发生于控制流后面的代码、一个释放锁unlock的动作一定先行发生于后面对于同一个锁进行锁定lock的动作等等,只要符合这些规则,则不需要额外做同步措施,如果某段代码不符合所有的happens-before规则,则这段代码一定是线程非安全的。
30、什么是CAS
CAS,全称为Compare and Swap,即比较-替换。假设有三个操作数:内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,才会将内存值修改为B并返回true,否则什么都不做并返回false。当然CAS一定要volatile变量配合,这样才能保证每次拿到的变量是主内存中最新的那个值,否则旧的预期值A对某条线程来说,永远是一个不会变的值A,只要某次CAS操作失败,永远都不可能成功。
31、什么是乐观锁和悲观锁
1)乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。
2)悲观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。
32、单例模式的线程安全性
单例模式的线程安全意味着:某个类的实例在多线程环境下只会被创建一次出来。单例模式有很多种的写法,我总结一下:
1)饿汉式单例模式的写法:线程安全(适用于:如果单件模式实例在系统中经常会被用到)
在程序启动或单件模式类被加载的时候,单件模式实例就已经被创建。
2)懒汉式单例模式的写法:非线程安全(适用于:单件模式在系统中会很少用到或者几乎不会用到)
当程序第一次访问单件模式实例时才进行创建。
3)双检锁单例模式的写法:线程安全
是一种使用同步块加锁的方法。程序员称其为双重检查锁,因为会有两次检查 instance == null
,一次是在同步块外,一次是在同步块内。
33、线程类的构造方法、静态块是被哪个线程调用的
线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的。
如果说上面的说法让你感到困惑,那么我举个例子,假设Thread2中new了Thread1,main函数中new了Thread2,那么:
1)Thread2的构造方法、静态块是main线程调用的,Thread2的run()方法是Thread2自己调用的
2)Thread1的构造方法、静态块是Thread2调用的,Thread1的run()方法是Thread1自己调用的
34、同步方法和同步块,哪个是更好的选择
同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率。请知道一条原则:同步的范围越小越好。
借着这一条,我额外提一点,虽说同步的范围越少越好,但是在Java虚拟机中还是存在着一种叫做锁粗化的优化方法,这种方法就是把同步范围变大。这是有用的,比方说StringBuffer,它是一个线程安全的类,自然最常用的append()方法是一个同步方法,我们写代码的时候会反复append字符串,这意味着要进行反复的加锁->解锁,这对性能不利,因为这意味着Java虚拟机在这条线程上要反复地在内核态和用户态之间进行切换,因此Java虚拟机会将多次append方法调用的代码进行一个锁粗化的操作,将多次的append的操作扩展到append方法的头尾,变成一个大的同步块,这样就减少了加锁-->解锁的次数,有效地提升了代码执行的效率。
35、高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?
这是我在并发编程网上看到的一个问题,把这个问题放在最后一个,希望每个人都能看到并且思考一下,因为这个问题非常好、非常实际、非常专业。关于这个问题,个人看法是:
1)高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换
2)并发不高、任务执行时间长的业务要区分开看:
a)假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以加大线程池中的线程数目,让CPU处理更多的业务
b)假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换
c)并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考其他有关线程池的文章。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。