Java多线程基础问题

来源:互联网 发布:赢时胜软件上海分公司 编辑:程序博客网 时间:2024/06/06 11:45

主要是关于Java多线程中的40个问题的总结:

1、多线程的作用

2、创建线程的方式

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

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

5、CyclicBarrier和CountDownLatch的区别

6、volatile关键字的作用

7、什么是线程安全

8、一个线程如果出现了运行时异常会怎样

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

10、sleep()和wait()有什么区别

11、生产者消费者模型的作用是什么

12、ThreadLocal有什么作用

13、为什么wait()和notify()、notifyAll()方法要在同步块中被调用

14、wait()和notify()、notifyAll()方法在放弃对象锁时有什么区别

15、为什么要使用线程池

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

17、Synchronized和ReentrantLock的区别

18、ConcurrentHashMap的并发度是多少

19、ReadWriteLock是什么

20、FutureTask是什么

21、Java编写一个会导致死锁的程序

22、怎么唤醒一个阻塞的线程

23、不可变对象对多线程有什么帮助

24、什么是多线程的上下文切换

25、如果你提交任务时,线程池队列已满,这时会发生什么

26、Java中用到的线程调度算法是什么

27、Thread.sleep(0)的作用是什么

28、什么是自旋

29、什么是Java内存模型

30、什么是CAS

31、什么是乐观锁和悲观锁

32、单例模式的线程安全性

33、Semaphore有什么作用、

34、Hashtable的size()方法中明明只有一条语句”return count”,为什么还要做同步?

35、 线程类的构造方法、静态块是被哪个线程调用的?

36、 同步方法和同步块,哪个是更好的选择?

37、什么是AQS?(*)

38、高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?

解析如下:

1、多线程的作用
就我的理解来说,Java里的多线程主要是解决以下问题:
1)发挥多核CPU的优势,充分“压榨”CPU的性能
我们知道现在的机器都是多核心的,4核、8核甚至16核的也都不少见,如果是单线程的程序,那么在双核CPU上就浪费了50%,在4核CPU上就浪费了75%。当时对于单核CPU来说,多线程其实是“假”的多线程,只是线程切换的比较快,看着像多线程而已,对于单核CPU使用多线程可能并不会提高性能反而影响性能,因为线程切换也是有性能损失的,但是多线程能够让多核CPU同时运行,大大提升运行效率。。
2)防止阻塞。
从程序运行效率的角度来看,单核CPU不但不会发挥出多线程的优势,反而会因为在单核CPU上运行多线程导致线程上下文的切换,而降低程序整体的效果。但是单核CPU我们哈市要应用多线程,就是为了防止阻塞。试想,如果单核CPU使用单线程,那个这个线程只要阻塞了,比方说远程读取某个数据时,对方迟迟未返回有没有设置超时时间,那么你的整个程序在数据放回来之前就停止运行了,多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取时阻塞,也不会影响其它任务的执行。
3)便于建模
这是另外没有这么明显的优点了,假设有一个大的任务A,单线程编程,那么就要考虑很多,建立整个程序模型比较麻烦,但是如果把这个大的任务A分解成几个小任务,任务B、任务C等等,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多。
2、创建线程的方式
一般有三种
1)继承Thread类
2)实现Runnable类
3)实现Callable接口
一般推荐使用Runnable接口创建编程,Thread是类,而Runnable是接口;Thread本身实现了Runnable接口的类。我们知道,一个类只能有一个父类,但是却能实现多个接口,因此,Runnable具有更好的扩展性。
除此之外,Runnable还可以用于资源的共享,即多个线程都是基于一个Runnable对象建立的,它们会共享Runnabled对象上的资源,一般建议使用Runnable实现多线程。(参考:http://blog.csdn.net/l_kanglin/article/details/56014574)
3、start()方法和run()方法的区别
start()是Thread里面的方法,在调用start()方法会立刻启动线程,然后会自动的调用run()方法,但是调用start()方法会立即返回,不会阻塞线程,从而真正实现多线程。
1)start()方法来启动线程,真正实现了多线程运行。这是无须等待run()方法体代码执行完毕,可以直接继续执行下面的代码;通过调用Thread类的start()方法来启动一个线程,这是此线程是处于就绪状态,并没有运行。然后通过此Thread类调用方法run()来完成其运行操作的额,这里方法run()称为线程体,它包含了要执行的这个线程的内容,run方法运行结束,此线程终止。然后CPU再调度其它线程。
2)run()方法当做普通方法的方式调用,程序还是要顺序执行,要等待run方法体执行完毕后,才可继续执行下面的代码;程序中只有主线程–这一个线程,其程序执行路径还是只有一条,这样就没有达到多线程的目的。
4、Runnable接口和Callable接口的区别
1)Runnable接口里面的run()方法是没有返回值得,所以实现Runnable接口一般执行不带返回值的任务;
2)Callable接口里面的call()方法是有返回值得,返回值是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
有没有返回值是这两个接口的主要区别,Callable接口的功能更加强大,使用Callable+Future/FutureTask可以获取多线程运行的结果,可以在等待时间太长没获取到需要的数据下取消该线程的任务,非常有用。
5、CyclicBarrier和CountDownLatch的区别
这两个类都是JUC并发包下的非常有用的类
1)CountDownLatch(闭锁)相当于一扇门:在闭锁到达结束状态之前(闭锁的结束状态就是闭锁的计数器减到0),这扇门一直是关闭的,并没有任何线程可以通过;当闭锁到达结束状态时,这扇门会打开并允许所有的等待线程(在闭锁上调用了await()方法的线程)通过。并且当闭锁到达结束状态之后不可逆转,这扇门会一直保持保持打开的状态。应用场景:确保某些活动直到其它活动都结束了才继续执行。
2)CyclicBarrier栏栅,它允许一组线程互相等待,直到都到达某个公共屏蔽点(common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时的互相等待,此时CyclicBarrier很有用。因为该barrier在释放等待线程后可以重用,所以称它为循环的barrier。CyclicBarrier就像它名字的意思一样,可看成是个障碍,所有的线程必须到齐后才能一起通过这个障碍。

6、volatile关键字的作用
简单点说就是提供了可视性机制,其实在理解volatile关键字的作用的前提是要理解Java内存模型,关于Java内存模型,可以参考第31点,volatile关键字的作用主要是两个:
1)可视性:多个线程主要围绕可见性和原子性两个特性而展开,使用volatile关键字修饰的变量,保证了其在多线程之间的可见性,即每次读取到volatile变量,一定是最新的数据。
2)防止重排序:代码底层执行不像我们看到的高级语言–Java程序这么简单,它的执行是Java代码–>字节码–>根据字节码执行对应的C/C++代码–>C/C++代码被编译成汇编语言–>和硬件电路交互,现实中,为了获取更好的性能JVM可能会指令进行重排序,多线程下可能会出现一些意想不到的问题。使用volatile则会对禁止语义重排序,当然这也一点程度上降低了代码执行效率。

volatile的一个重要作用就是和CAS结合,保证了原子性和可视性,详细的可以参考并发包下的原子性概述。
7、什么是线程安全
个人理解:一段代码在单线程和多线程并发环境下,永远都能获得一样的结果,那么就是线程安全的。
需要注意的是,线程安全是分几个级别的:
(1)不可变(对象)
像String、Integer、Long这些,都是final类型的表,还有final类型的变量,任何一个线程都改变不了它们的值,要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用。
(2)绝对线程安全
不管运行时环境如何,调用者都不需要额外的同步措施。要做到这一点通常需要付出额外的代价,Java中标注自己是线程安全的类,实际上绝大多数都不是线性安全的,不过绝对线程安全的类,Java中也有,比方说CopyOnWriteArrayList、CopyOnWriteArraySet等
(3)相对线程安全
相对线程安全也就是我们通常意义上说的线程安全,像Vector这种,add、remove方法都是原子操作,不会被打断,但也仅限于此,如果有个线程在遍历某个Vector、有个线程同时在add这个Vector,99%的情况下都会出ConcurrentModificationException,也就是fail-fast机制。
(4)线程非安全
像ArrayList、LinkedList、HashMap等都是线程非安全的类,没有加任何保证线程安全的措施
8、一个线程如果出现了运行时异常会怎样
如果这个异常没有被捕获的话,这个线程就停止执行了。另外重要的一点是:如果这个线程持有某个对象的监视器,那么这个对象锁就会被立即释放掉。
9、如何在两个线程之间共享数据
通过线程之间共享对象就可以了,然后通过wait/notify/notifyAll、await/signal/signalAll进行唤起和等待,这里需要注意的是保证多线程环境下的数据安全性。比方说阻塞队列BlockingQueue就是为线程之间共享数据而设计的。
10、sleep()和wait()有什么区别
相同点:sleep()和wait()方法都可以使当前线程等待一段时间
不同点:在于如果线程持有某个对象的锁,sleep()方法不会放弃这个对象的锁;wait()会放弃这个对象的锁。
11、生产者消费者模型的作用是什么
(1)解耦,这是生产者消费者模型附带的作用,解耦意味着生产者和消费者之间的联系少,联系越少越可以独自发展而不需要受到相互的制约。
(2)通过平衡生产者的生产能力和消费者的消费能力来提升整个系统的运行效率,这是生产者消费者模型最重要的作用。
12、ThreadLocal有什么作用
简单说ThreadLocal就是一种以空间换时间的做法,在每个Thread里面维护了一个以开地址法实现的ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,自然就没有线程安全的问题。
13、为什么wait()和notify()、notifyAll()方法要在同步块中被调用
这是JDK强制的,wait()方法和notify()/notifyAll()方法在调用前都必须先获得对象的锁。
14、wait()和notify()、notifyAll()方法在放弃对象锁时有什么区别
wait()方法立即释放掉对象锁,notify()/notifyAll()方法则会等待线程剩余代码执行完毕后才会放弃对象的锁。
15、为什么要使用线程池
(1)避免频繁的创建和销毁线程,达到线程对象的重用。(创建和销毁线程的开销都是挺大的)
(2)使用线程池海尔可以根据项目灵活的控制并发的数目。
16、怎么检测一个线程是否持有对象监视器
有方法可以判断某个线程是否持有对象的锁:Thread类提供了一个holdLock(Object obj)方法,当且仅当对象obj的监视器被某条线程持有的时候才会返回true,注意这是一个static方法,这意味着某条线程指的是当前线程。
17、Synchronized和ReentrantLock的区别
Synchronized是一个关键字,而ReentrantLock是类,这是二者的本质区别。既然ReentrantLock是类,那么就提供了比Synchronized更多更灵活的特性,可以被继承、可以有方法,可以有各种各样的变量,ReentrantLock比Synchronized的扩张性体现在几点上:
(1)ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁
(2)ReentrantLock可以获取各种锁的信息
(3)ReentrantLock可以灵活的实现多路通知
另外,二者的锁机制是不一样的,ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的是对象头mark word。
18、ConcurrentHashMap的并发度是多少
看看源码就知道ConcurrentHashMap使用了分段锁,默认的并发度是16.ConcurrentHashMap的并发度就是segment的大小,默认为16,这意味着最多同时可以有16条线程操作ConcurrentHashMap,这也是它的最大优势,任何情况下,Hashtable就不能同时有两条线程获取Hashtable中的数据。
19、ReadWriteLock是什么
ReadWriteLock是一个读写锁接口,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。
20、FutureTask是什么
FutureTask表示一个异步运算的任务。可以传入Callable的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。同时,FutureTask也是Runnable接口的实现类,所以FutureTask也可以放入线程池中。
21、Java编写一个会导致死锁的程序
真正理解什么是死锁,这个问题其实不难,几个步骤:
(1)两个线程里面分别持有两个Object对象:lock1和lock2。这两个lock作为同步代码块的锁;
(2)线程1的run()方法中同步代码块先获取lock1的对象锁,Thread.sleep(xxx),时间不需要太多,50毫秒差不多了,然后接着获取lock2的对象锁。这么做主要是为了防止线程1启动一下子就连续获得了lock1和lock2两个对象的对象锁
(3)线程2的run)(方法中同步代码块先获取lock2的对象锁,接着获取lock1的对象锁,当然这时lock1的对象锁已经被线程1锁持有,线程2肯定是要等待线程1释放lock1的对象锁的
这样,线程1”睡觉”睡完,线程2已经获取了lock2的对象锁了,线程1此时尝试获取lock2的对象锁,便被阻塞,此时一个死锁就形成了。
22、怎么唤醒一个阻塞的线程
如果线程是因为调用了wait()、sleep()、或者join()方法而导致的阻塞,可以中断线程,并且通过抛出InterruptedException来唤醒它;如果线程遇到了IO阻塞,由于是操作系统实现的,Java代码并没有方法去接触到操作系统而解决它。
23、不可变对象对多线程有什么帮助
不可变对象保证了对象的可见性,而且不可变对象天生就是线程安全的
24、什么是多线程的上下文切换
多线程的上下文切换是指CPU控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取CPU执行权的过程,这涉及运行环境的保存与切换,存在时间开销
25、如果你提交任务时,线程池队列已满,这时会发生什么
(1)如果用的是无界队列,比如LinkedBlockingQueue,没关系,继续添加任务到阻塞队列中等待执行,因为LinkedBlockingQueue可以近乎任务是一个无穷大的队列,可以无限存放任务。
(2)如果使用的是有界队列,比方说ArrayBlockingQueue的话,任务首先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue满了,则会使用拒绝策略RejectedExecutionHander处理满了的任务,默认是AbortPolice。
26、Java中用到的线程调度算法是什么
抢占式算法。一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。
27、Thread.sleep(0)的作用是什么
简答点说就是主动让出一次CPU:
由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。
28、什么是自旋
很多synchronized里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然synchronized里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在synchronized的边界做忙循环,这就是自旋。如果做了多次忙循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。
29. 什么是Java内存模型

Java内存模型定义了一种多线程访问Java内存的规范。Java内存模型要完整讲不是这里几句话能说清楚的,我简单总结一下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操作失败,永远都不可能成功。

比如原子类:AtomicInteger 就是volatile+CAS实现的。
31、什么是乐观锁和悲观锁
其实这两种锁就是对线程安全的最悲观和最乐观的一种假设。
(1)乐观锁:
就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是发生,因此它不需要持有锁,将比较-替换这两个动作作为以一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。
(2)悲观锁:对于并发间操作产生的线程安全问题持悲观态度,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的额锁,就像synchronized,直接上锁操作资源。
32、单例模式的线程安全性
单例模式的线程安全意味着:某个类的实例在多线程环境下只会被创建一次来。
(1)恶汉式单例模式:线程安全
(2)懒汉式单例模式:非线程安全
(3)双检锁(DCL)单例模式:线程安全
33、Semaphore有什么作用、
常用作并发数控制器。Semaphore就是一个信号量,它的作用是限制某段代码块的并发数。Semaphore有一个构造函数,可以传入一个int整数n,表示某段代码最多只有n个线程可以访问,如果超出了n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果Semaphore构造函数中传入的int型整数n=1,相当于变成了一个synchronized了。
34. Hashtable的size()方法中明明只有一条语句”return count”,为什么还要做同步?

这是我之前的一个困惑,不知道大家有没有想过这个问题。某个方法中如果有多条语句,并且都在操作同一个类变量,那么在多线程环境下不加锁,势必会引发线程安全问题,这很好理解,但是size()方法明明只有一条语句,为什么还要加锁?

关于这个问题,在慢慢地工作、学习中,有了理解,主要原因有两点:
(1)同一时间只能有一条线程执行固定类的同步方法,但是对于类的非同步方法,可以多条线程同时访问。所以,这样就有问题了,可能线程A在执行Hashtable的put方法添加数据,线程B则可以正常调用size()方法读取Hashtable中当前元素的个数,那读取到的值可能不是最新的,可能线程A添加了完了数据,但是没有对size++,线程B就已经读取size了,那么对于线程B来说读取到的size一定是不准确的。而给size()方法加了同步之后,意味着线程B调用size()方法只有在线程A调用put方法完毕之后才可以调用,这样就保证了线程安全性

(2)CPU执行代码,执行的不是Java代码,这点很关键,一定得记住。Java代码最终是被翻译成汇编代码执行的,汇编代码才是真正可以和硬件电路交互的代码。即使你看到Java代码只有一行,甚至你看到Java代码编译之后生成的字节码也只有一行,也不意味着对于底层来说这句语句的操作只有一个。一句”return count”假设被翻译成了三句汇编语句执行,完全可能执行完第一句,线程就切换了。
35. 线程类的构造方法、静态块是被哪个线程调用的?

这是一个非常刁钻和狡猾的问题。请记住:线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的。

如果说上面的说法让你感到困惑,那么我举个例子,假设Thread2中new了Thread1,main函数中new了Thread2,那么:
(1)Thread2的构造方法、静态块是main线程调用的,Thread2的run()方法是Thread2自己调用的

(2)Thread1的构造方法、静态块是Thread2调用的,Thread1的run()方法是Thread1自己调用的
36. 同步方法和同步块,哪个是更好的选择?

同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率。请知道一条原则:同步的范围越小越好。

借着这一条,我额外提一点,虽说同步的范围越少越好,但是在Java虚拟机中还是存在着一种叫做锁粗化的优化方法,这种方法就是把同步范围变大。这是有用的,比方说StringBuffer,它是一个线程安全的类,自然最常用的append()方法是一个同步方法,我们写代码的时候会反复append字符串,这意味着要进行反复的加锁->解锁,这对性能不利,因为这意味着Java虚拟机在这条线程上要反复地在内核态和用户态之间进行切换,因此Java虚拟机会将多次append方法调用的代码进行一个锁粗化的操作,将多次的append的操作扩展到append方法的头尾,变成一个大的同步块,这样就减少了加锁–>解锁的次数,有效地提升了代码执行的效率。
37. 什么是AQS?(*)

简单说一下AQS,AQS全称为AbstractQueuedSychronizer,翻译过来应该是抽象队列同步器。

如果说java.util.concurrent的基础是CAS的话,那么AQS就是整个Java并发包的核心了,ReentrantLock、CountDownLatch、Semaphore等等都用到了它。AQS实际上以双向队列的形式连接所有的Entry,比方说ReentrantLock,所有等待的线程都被放在一个Entry中并连成双向队列,前面一个线程使用ReentrantLock好了,则双向队列实际上的第一个Entry开始运行。

AQS定义了对双向队列所有的操作,而只开放了tryLock和tryRelease方法给开发者使用,开发者可以根据自己的实现重写tryLock和tryRelease方法,以实现自己的并发功能。
38. 高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?

这是我在并发编程网上看到的一个问题,把这个问题放在最后一个,希望每个人都能看到并且思考一下,因为这个问题非常好、非常实际、非常专业。关于这个问题,个人看法是:
(1)高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换
(2)并发不高、任务执行时间长的业务要区分开看:
  a)假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以加大线程池中的线程数目,让CPU处理更多的业务
  b)假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换
(3)并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。

0 0
原创粉丝点击