Java多线程问题(上)

一、多线程有什么用?

一个可能在不少人看来很扯淡的一个问题:我会用多线程就行了,还管它有什么用?在我看来,这个回答更扯淡。所谓”知其然知其因此然”,”会用”只是”知其然”,”为何用”才是”知其因此然”,只有达到”知其然知其因此然”的程度才能够说是把一个知识点运用自如。OK,下面说说我对这个问题的见解:java

(1)发挥多核CPU的优点程序员

随着工业的进步,如今的笔记本、台式机乃至商用的应用服务器至少也都是双核的,4核、8核甚至16核的也都很多见,若是是单线程的程序,那么在双核CPU上就浪费了50%,在4核CPU上就浪费了75%。单核CPU上所谓的”多线程”那是假的多线程,同一时间处理器只会处理一段逻辑,只不过线程之间切换得比较快,看着像多个线程”同时”运行罢了。多核CPU上的多线程才是真正的多线程,它能让你的多段逻辑同时工做,多线程,能够真正发挥出多核CPU的优点来,达到充分利用CPU的目的。面试

(2)防止阻塞编程

从程序运行效率的角度来看,单核CPU不但不会发挥出多线程的优点,反而会由于在单核CPU上运行多线程致使线程上下文的切换,而下降程序总体的效率。可是单核CPU咱们仍是要应用多线程,就是为了防止阻塞。试想,若是单核CPU使用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据吧,对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来以前就中止运行了。多线程能够防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。设计模式

(3)便于建模安全

这是另一个没有这么明显的优势了。假设有一个大的任务A,单线程编程,那么就要考虑不少,创建整个程序模型比较麻烦。可是若是把这个大的任务A分解成几个小任务,任务B、任务C、任务D,分别创建程序模型,并经过多线程分别运行这几个任务,那就简单不少了。服务器

二、建立线程的方式

比较常见的一个问题了,通常就是两种:多线程

(1)继承Thread类并发

(2)实现Runnable接口异步

可是二者究竟孰优孰劣呢-》咱们都知道在Java中是不支持多继承的,这就意味着一个类只能右一个“爸爸”,可是Java的类却能够实现N个接口,只要实现这些接口里面的方法便可。面向对象的编程的一个设计模式就是面向接口编程,因此咱们通常更提倡第二种方法。可是若是要说第三种方法,Java也是有的。Java中有线程池的概念,也能够借助线程池来建立相关的线程,相关内容不作太多论述。

三、start()方法和run()方法的区别

只有调用了start()方法,才会表现出多线程的特性,不一样线程的run()方法里面的代码交替执行。若是只是调用run()方法,那么代码仍是同步执行的,必须等待一个线程的run()方法里面的代码所有执行完毕以后,另一个线程才能够执行其run()方法里面的代码,也就是说,若是单独调用run()方法的话,和类的其余方法并无太大区别,并不会建立一个线程用来专门执行。

四、Runnable接口和Callable接口的区别

有点深的问题了,也看出一个Java程序员学习知识的广度。

Runnable接口中的run()方法的返回值是void,它作的事情只是纯粹地去执行run()方法中的代码而已;Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合能够用来获取异步执行的结果。

这实际上是颇有用的一个特性,由于多线程相比单线程更难、更复杂的一个重要缘由就是由于多线程充满着未知性,某条线程是否执行了?某条线程执行了多久?某条线程执行的时候咱们指望的数据是否已经赋值完毕?没法得知,咱们能作的只是等待这条多线程的任务执行完毕而已。而Callable+Future/FutureTask却能够获取多线程运行的结果,能够在等待时间太长没获取到须要的数据的状况下取消该线程的任务,真的是很是有用。

五、CyclicBarrier和CountDownLatch的区别

两个看上去有点像的类,都在java.util.concurrent下,均可以用来表示代码运行到某个点上,两者的区别在于:

(1)CyclicBarrier的某个线程运行到某个点上以后,该线程即中止运行,直到全部的线程都到达了这个点,全部线程才从新运行;CountDownLatch则不是,某线程运行到某个点上以后,只是给某个数值-1而已,该线程继续运行

(2)CyclicBarrier只能唤起一个任务,CountDownLatch能够唤起多个任务

(3)CyclicBarrier可重用,CountDownLatch不可重用,计数值为0该CountDownLatch就不可再用了

六、volatile关键字的做用

一个很是重要的问题,是每一个学习、应用多线程的Java程序员都必须掌握的。理解volatile关键字的做用的前提是要理解Java内存模型,这里就不讲Java内存模型了,能够参见第31点,volatile关键字的做用主要有两个:

(1)多线程主要围绕可见性和原子性两个特性而展开,使用volatile关键字修饰的变量,保证了其在多线程之间的可见性,即每次读取到volatile变量,必定是最新的数据

(2)代码底层执行不像咱们看到的高级语言—-Java程序这么简单,它的执行是Java代码–>字节码–>根据字节码执行对应的C/C++代码–>C/C++代码被编译成汇编语言–>和硬件电路交互,现实中,为了获取更好的性能JVM可能会对指令进行重排序,多线程下可能会出现一些意想不到的问题。使用volatile则会对禁止语义重排序,固然这也必定程度上下降了代码执行效率

从实践角度而言,volatile的一个重要做用就是和CAS结合,保证了原子性,详细的能够参见java.util.concurrent.atomic包下的类,好比AtomicInteger。

七、什么是线程安全

又是一个理论的问题,各式各样的答案有不少,我给出一个我的认为解释地最好的:若是你的代码在多线程下执行和在单线程下执行永远都能得到同样的结果,那么你的代码就是线程安全的

这个问题有值得一提的地方,就是线程安全也是有几个级别的:

(1)不可变

像String、Integer、Long这些,都是final类型的类,任何一个线程都改变不了它们的值,要改变除非新建立一个,所以这些不可变对象不须要任何同步手段就能够直接在多线程环境下使用

(2)绝对线程安全

无论运行时环境如何,调用者都不须要额外的同步措施。要作到这一点一般须要付出许多额外的代价,Java中标注本身是线程安全的类,实际上绝大多数都不是线程安全的,不过绝对线程安全的类,Java中也有,比方说CopyOnWriteArrayList、CopyOnWriteArraySet

(3)相对线程安全

相对线程安全也就是咱们一般意义上所说的线程安全,像Vector这种,add、remove方法都是原子操做,不会被打断,但也仅限于此,若是有个线程在遍历某个Vector、有个线程同时在add这个Vector,99%的状况下都会出现ConcurrentModificationException,也就是fail-fast机制,这种状况也会出如今当你使用Iterator遍历时候出现。

(4)线程非安全

这个就没什么好说的了,ArrayList、LinkedList、HashMap等都是线程非安全的类

八、Java中如何获取到线程dump文件

死循环、死锁、阻塞、页面打开慢等问题,打线程dump是最好的解决问题的途径。所谓线程dump也就是线程堆栈,获取到线程堆栈有两步:

(1)获取到线程的pid,能够经过使用jps命令,在Linux环境下还可使用ps -ef | grep java

(2)打印线程堆栈,能够经过使用jstack pid命令,在Linux环境下还可使用kill -3 pid

另外提一点,Thread类提供了一个getStackTrace()方法也能够用于获取线程堆栈。这是一个实例方法,所以此方法是和具体线程实例绑定的,每次获取获取到的是具体某个线程当前运行的堆栈,

九、一个线程若是出现了运行时异常会怎么样

若是这个异常没有被捕获的话,这个线程就中止执行了。另外重要的一点是:若是这个线程持有某个某个对象的监视器,那么这个对象监视器会被当即释放

十、如何在两个线程之间共享数据

经过在线程之间共享对象就能够了,而后经过wait/notify/notifyAll、await/signal/signalAll进行唤起和等待,比方说阻塞队列BlockingQueue就是为线程之间共享数据而设计的。这里也就引出了生产者消费者模型的两种编写方法,一个是wait/nodify,另一个是BlockingQueue。

十一、sleep方法和wait方法有什么区别

1.sleep方法和wait方法均可以用来放弃CPU必定的时间,不一样点在于若是线程持有某个对象的监视器,sleep方法不会放弃这个对象的监视器,wait方法会放弃这个对象的监视器

2.sleep方法是在Thread上的,而wait方法是在Object上的,再调用wait方法时,线程必定要持有相关的锁,也就是说必定要在synchronized块内执行。

十二、生产者消费者模型的做用是什么

这个问题很理论,可是很重要:

(1)经过平衡生产者的生产能力和消费者的消费能力来提高整个系统的运行效率,这是生产者消费者模型最重要的做用

(2)解耦,这是生产者消费者模型附带的做用,解耦意味着生产者和消费者之间的联系少,联系越少越能够独自发展而不须要收到相互的制约,让我想到了消息队列,可是不清楚具体应该不该该这么讲。

1三、ThreadLocal有什么用

简单说ThreadLocal就是一种以空间换时间的作法,在每一个Thread里面维护了一个以开地址法实现的ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,天然就没有线程安全方面的问题了。其实内部的Map是一个以Thread为键的一个特殊map,由于每一个线程都有独特的实例,因此天然后不会有数据冲突问题,也就间接实现了数据隔离。

1四、为何wait()方法和notify()/notifyAll()方法要在同步块中被调用

这是JDK强制的,wait()方法和notify()/notifyAll()方法在调用前都必须先得到对象的锁。咱们能够逆思惟来考虑这个问题,假设不要求这些方法都方法同步块的话,也就是说再调用的时候线程并无持有锁对象,那么问题来了,既然没有持有锁对象,调用wait()方法的话线程要如何在一个锁对象上等待并释放锁,由于明明就没有得到锁对象,何谈释放呢。

wait()的含义:在一个锁(任意Object)上等待,而且释放该锁的全部权。

nodify()含义:唤醒一个锁对象上调用wait方法致使等待的线程,与wait方法呼应。

1五、wait()方法和notify()/notifyAll()方法在放弃对象监视器时有什么区别

wait()方法和notify()/notifyAll()方法在放弃对象监视器的时候的区别在于:wait()方法当即释放对象监视器,notify()/notifyAll()方法则会等待线程剩余代码执行完毕才会放弃对象监视器

1六、为何要使用线程池

这里就涉及到一个线程池的设计思想。线程在使用的时候,有很大一部分开销就是用来建立和销毁线程,并且有的线程执行的是短任务,这就意味着线程建立和销毁的代价太大。咱们能够类比数据链接池的思想,像c3p0,dbcp之类的数据链接池都在作着相似的工做,那就是帮咱们维护连接/线程。归根到底就是两个字:复用

1七、怎么检测一个线程是否持有对象监视器

我也是在网上看到一道多线程面试题才知道有方法能够判断某个线程是否持有对象监视器:Thread类提供了一个holdsLock(Object obj)方法,当且仅当对象obj的监视器被某条线程持有的时候才会返回true,注意这是一个static方法,这意味着“某条线程”指的是当前线程

1八、synchronized和ReentrantLock的区别

synchronized是和if、else、for、while同样的关键字,ReentrantLock是类,这是两者的本质区别。既然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,能够被继承、能够有方法、能够有各类各样的类变量,ReentrantLock比synchronized的扩展性体如今几点上:

(1)ReentrantLock能够对获取锁的等待时间进行设置,这样就避免了死锁

(2)ReentrantLock能够获取各类锁的信息

(3)ReentrantLock能够灵活地实现多路通知

 ( 4 ) Synchronized的使用要比ReetrantLock更简单一些,并且不用咱们管理锁,可是ReetrantLock要在使用完以后释放锁对象,通常在finally块中调用release方法。

另外,两者的锁机制其实也是不同的。至于这个不一样点,我会在不一样的博客里面详细介绍。

1九、ConcurrentHashMap的并发度是什么

ConcurrentHashMap的并发度就是segment的大小,默认为16,这意味着最多同时能够有16条线程操做ConcurrentHashMap,这也是ConcurrentHashMap对Hashtable的最大优点,任何状况下,Hashtable能同时有两条线程获取Hashtable中的数据吗?答案是NO!

20、ReadWriteLock是什么

首先明确一下,不是说ReentrantLock很差,只是ReentrantLock某些时候有局限。若是使用ReentrantLock,可能自己是为了防止线程A在写数据、线程B在读数据形成的数据不一致,但这样,若是线程C在读数据、线程D也在读数据,读数据是不会改变数据的,没有必要加锁,可是仍是加锁了,下降了程序的性能。

由于这个,才诞生了读写锁ReadWriteLock。ReadWriteLock是一个读写锁接口,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提高了读写的性能

相关文章
相关标签/搜索