并发编程面试题3
可以看到,生产者生产完了,消费者立马消费,消费者消费完了,生产者又立马生产;这是一个轮流的过程,一直保持着生产者跟消费者之间的互相等待。
方法3:信号灯法
并发协作模型“生产者/消费者模式”—>信号灯法
使用一个标志位,如果标志位为true,就等待,如果为false,就通知另一个线程。
代码模拟场景:
演员(生产者)表演节目,观众(消费者)观看节目。
用一个标志位控制线程什么时候等待,什么时候通知。
publicclassThreadDemo2{publicstaticvoidmain(String[]args){TVtv=newTV;newPlayer(tv).start;newWatcher(tv).start;}}//生产者->演员classPlayerextendsThread{TVtv;publicPlayer(TVtv){this.tv=tv;}//生产@Overridepublicvoidrun{for(inti=0;i观众classWatcherextendsThread{TVtv;publicWatcher(TVtv){this.tv=tv;}//消费@Overridepublicvoidrun{for(inti=0;i节目classTV{//演员表演,观众等待//观众观看,演员等待//表演的节目Stringvoice;//标志位booleanflag=true;//表演publicsynchronizedvoidplay(Stringvoice){if(!flag){try{this.wait;}catch(InterruptedExceptione){e.printStackTrace;}}this.voice=voice;System.out.println("演员表演了:"+voice);this.flag=!this.flag;//通知观众观看this.notifyAll;}//表演publicsynchronizedvoidwatch{if(flag){try{this.wait;}catch(InterruptedExceptione){e.printStackTrace;}}System.out.println("观众观看了:"+voice);this.flag=!this.flag;//通知演员表演this.notifyAll;}}
演员演一段,观众看一段
方法四:while轮询的方式
importjava.util.ArrayList;importjava.util.List;publicclassMyList{privateListlist=newArrayList;publicvoidadd{list.add("elements");}publicintsize{returnlist.size;}}importmylist.MyList;publicclassThreadAextendsThread{privateMyListlist;publicThreadA(MyListlist){super;this.list=list;}@Overridepublicvoidrun{try{for(inti=0;i
在这种方式下,线程A不断地改变条件,线程ThreadB不停地通过while语句检测这个条件(list.size==5)是否成立,从而实现了线程间的通信。但是这种方式会浪费CPU资源。之所以说它浪费资源,是因为JVM调度器将CPU交给线程B执行时,它没做啥“有用”的工作,只是在不断地测试某个条件是否成立。就类似于现实生活中,某个人一直看着手机屏幕是否有电话来了,而不是:在干别的事情,当有电话来时,响铃通知TA电话来了。
notify/notifyAll/sleep/wait的区别
我们通过对这些方法分析,sleep方法属于Thread类,而wait/notify/notifyAll属于Object基础类,也就是说每个对象都有wait/notify/notifyAll的功能。
sleep不会释放锁,而wait会释放锁。
sleep必须捕获异常,而wait/notify/notifyAll不需要捕获异常。
sleep可以在任何地方使用,而wait/notify/notifyAll只能在同步控制方法或者同步控制块里面使用。
介绍
notify:随机唤醒一个等待该对象同步锁的线程,进入就绪队列等待CPU的调度;这里的唤醒是由JVM确定唤醒哪个线程,而且不是按优先级决定。
notifyAll:唤醒所有的等待该对象同步锁的线程,进入就绪队列等待CPU调度;注意唤醒的是notify之前wait的线程,对于notify之后的wait线程是没有效果的。
wait:调用时需要先获得该Object的锁,调用后,会把当前的锁释放掉同时阻塞住;但可以通过调用该Object的notify或者notifyAll来重新获得锁。
sleep:在指定的时间内让正在执行的线程暂停执行,但不会释放锁。
同步方法和同步块,哪个是更好的选择?
同步块是更好的选择,同步块更要符合开放调用的原则,只在需要锁住的代码块锁住相应的对象,同步方法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率。
请知道一条原则:同步的范围越小越好。
借着这一条,我额外提一点,虽说同步的范围越少越好,但是在Java虚拟机中还是存在着一种叫做锁粗化的优化方法,这种方法就是把同步范围变大。这是有用的,比方说StringBuffer,它是一个线程安全的类,自然最常用的append方法是一个同步方法,我们写代码的时候会反复append字符串,这意味着要进行反复的加锁->解锁,这对性能不利,因为这意味着Java虚拟机在这条线程上要反复地在内核态和用户态之间进行切换,因此Java虚拟机会将多次调用append方法的代码进行一个锁粗化的操作,将多
次的append的操作扩展到append方法的头尾,变成一个大的同步块,这样就减少了加锁-->解锁的次数,有效地提升了代码执行的效率。
什么是线程同步和线程互斥?线程同步和互斥的区别?同步有哪几种实现方式?
线程同步
同步,又称直接制约关系,是指多个线程彼此合作,通过一定的逻辑关系来共同完成一个任务,必须严格按照规定的某种先后顺序来运行。一般来说,同步关系中往往包含互斥,同时对临界区的资源会按照某种逻辑顺序进行访问,如先生产后使用
另一种说法
互斥解决了「多进程/线程」对临界区使用的问题,但是它没有解决「多进程/线程」协同工作的问题
我们都知道在多线程里,每个线程一定是顺序执行的,它们各自独立,以不可预知的速度向前推进,但有时候我们希望多个线程能密切合作,以实现一个共同的任务。
所谓同步,就是「多进程/线程间」在一些关键点上可能需要互相等待与互通消息,这种相互制约的等待与互通信息称为「进程/线程」同步。
简单说:同步是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。同步其实已经实现了互斥,所以同步是一种更为复杂的互斥
线程互斥
当有若干个线程都要使用某一共享资源(共享的数据和硬件资源)时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步。
简单说:互斥是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的,互斥是一种特殊的同步。
线程同步和互斥的区别
互斥是通过竞争对资源的独占使用,彼此之间不需要知道对方的存在,执行顺序是一个乱序。
同步是协调多个相互关联线程合作完成任务,彼此之间知道对方存在,执行顺序往往是有序的。
实现线程同步的方法
同步代码方法:sychronized关键字修饰的方法同步代码块:sychronized关键字修饰的代码块
使用特殊变量域volatile实现线程同步:volatile关键字为域变量的访问提供了一种免锁机制
使用重入锁实现线程同步:reentrantlock类是可冲入、互斥、实现了lock接口的锁他与sychronized方法具有相同的基本行为和语义
使用信号量首先我们要清楚临界资源和临界区的概念,临界资源就是同一时刻只允许一个线程(或进程)访问的资源,临界区就是访问临界资源的代码段。信号量是一种特殊的变量,用来控制对临界资源的使用,在多个进程或线程都要访问临界资源的时候,就需要控制多个进行或线程对临界资源的使用。信号量机制通过p、v操作实现。p操作:原子减1,申请资源,当信号量为0时,p操作阻塞;v操作:原子加1,释放资源。
在监视器(Monitor)内部,是如何做线程同步的?
在java虚拟机中,监视器和锁在Java虚拟机中是一块使用的,监视器监视一块同步代码块,每一个监视器都和一个对象引用相关联,为了确保一次只有一个线程执行同步代码块(线程同步),每个对象都关联着一把锁。一旦方法或者代码块被synchronized修饰,那么这个部分就放入了监视器的监视区域,线程在获取锁之前不允许执行该部分的代码,确保一次只能有一个线程执行该部分的代码,另外java还提供了显式监视器(Lock)和隐式监视器(synchronized)两种锁方案
servlet和Struts2的action是线程安全吗?SpringMVC的Controller是线程安全的吗?
servlet是线程安全吗?
Servlet不是线程安全的,servlet是单实例多线程的,当多个线程同时访问同一个方法,是不能保证共享变量的线程安全性的。
Struts2的action是线程安全吗?
Struts2的action是多实例多线程的,是线程安全的,每个请求过来都会new一个新的action分配给这个请求,请求完成后销毁。
SpringMVC的Controller是线程安全的吗?
不是的
总结
Struts2好处是不用考虑线程安全问题;Servlet和SpringMVC需要考虑线程安全问题,但是性能可以提升不用处理太多的gc,可以使用ThreadLocal来处理多线程的问题。
线程的安全问题体现和导致原因和解决方案?
线程的安全性问题体现
原子性:一个或者多个操作在CPU执行的过程中不被中断的特性。
可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到。
有序性:程序执行的顺序按照代码的先后顺序执行。
导致原因
线程切换带来的原子性问题
缓存导致的可见性问题
编译优化带来的有序性问题
解决办法
JDK的Atomic开头的原子类,synchronized、Lock,可以解决原子性问题
synchronized、volatile、Lock,可以解决可见性问题
Happens-Before规则可以解决有序性问题
在Java程序中怎么保证多线程的运行安全?
方法一:使用安全类,比如java.util.concurrent下的类,使用原子类AtomicInteger
方法二:使用自动锁synchronized。
方法三:使用手动锁Lock。
手动锁Java示例代码如下:
你对线程优先级的理解是什么?
每一个线程都是有优先级的,一般来说,高优先级的线程在运行时会具有优先权,这依赖于线程调度的实现,这个实现是和操作系统相关的(OSdependent)。我们可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行。理论上,优先级高的线程比优先级低的线程获得更多的CPU时间。实际上,线程获得的CPU时间通常由包括优先级在内的多个因素决定。线程优先级是一个int变量(从1-10),1代表最低优先级,10代表最高优先级。
Java的线程优先级调度会委托给操作系统去处理,所以与具体的操作系统优先级有关,如非特别需要,一般无需设置线程优先级。
线程类的构造方法和静态块是被哪个线程调用的?run方法呢?
这是一个非常刁钻和狡猾的问题。
请记住:线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的。
如果说上面的说法让你感到困惑,那么我举个例子,假设Thread2中new了Thread1,main函数中new了Thread2,那么:
Thread2的构造方法、静态块是main线程调用的,Thread2的run方法是Thread2自己调用的
Thread1的构造方法、静态块是Thread2调用的,Thread1的run方法是Thread1自己调用的
什么是dump文件?你如何在Java中获取线程堆栈?
什么是线程dump?
线程dump是非常有用的诊断java应用问题的工具,每一个java虚拟机都有及时生成显示所有线程在某一点状态的线程dump能力。虽然每个java虚拟机线程dump打印输出格式上略微有一些不同,但是线程dump的信息包含线程基本信息、线程的运行状态、标识、调用栈;调用的堆栈包含完整的类名,所执行的方法,如果可能的话还有源代码的行数。
其中:
线程的一些基本信息:名称、优先级及id
线程状态:waitingoncondition
线程的调用栈
线程锁住的资源:locked
JVM中的许多问题都可以使用线程dump文件来进行诊断,其中比较典型的包括线程阻塞,CPU使用率过高,JVMCrash,堆内存不足和类装载等问题。
你如何在Java中获取线程堆栈(dump文件)?
在windows环境中
在启动程序的控制台里敲:Ctrl+Break,线程的dump会产生在标准输出中(缺省标准输出就是控制台,如果对输出进行了重定向,则要查看输出文件)
在linux环境中
通过使用jps命令获取到线进程的pid
通过使用jstackpid命令打印线程堆栈
jstack[option]pid--参数1.-F强制打印堆栈2.-m打印java和native(C++)堆栈信息3.-l打印额外的信息,包括锁信息
另外提一点,Thread类提供了一个getStackTrace方法也可以用于获取线程堆栈。这是一个实例方法,因此此方法是和具体线程实例绑定的,每次获取获取到的是具体某个线程当前运行的堆栈
注意:jps和ps的作用差不多,都是显示当前系统的java进程情况及进程id,top主要看cpu,内存使用情况及占用资源最多的进程由高到低排序,关注点在于资源占用情况
使用top查看目前正在运行的进程使用系统资源情况
当前占用cpu最高26.5%的进程为27796的java程序
使用jstack将所有线程信息导出到指定文件中
使用jstack[-l]pid>xxx.log命令将所有线程信息输入到指定文件中
当'jstack[-l]pid'没有响应,使用jstack-F[-m][-l]pid>xxx.log命令强制导出堆栈dump
一个线程运行时发生异常会怎样?
如果异常没有被捕获该线程将会停止执行,如果该异常被捕获或抛出,则程序继续运行。
Thread.UncaughtExceptionHandler是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的时候,JVM会使用Thread.getUncaughtExceptionHandler来查询线程的UncaughtExceptionHandler并将线程和异常作为参数传递给handler的uncaughtException方法进行处理。
Java线程数过多会造成什么异常?
(1)线程的生命周期开销非常高
(2)消耗过多的CPU资源
如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争CPU资源时还将产生其他性能的开销。
(3)降低稳定性
JVM在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同,并且承受着多个因素制约,包括JVM的启动参数、Thread构造函数中请求栈的大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么可能抛出OutOfMemoryError异常。
为什么代码会重排序?
在执行程序时,为了提高性能,处理器和编译器常常会对指令进行重排序,但是不能随意重排序,不是你想怎么排序就怎么排序,它需要满足以下两个条件:
编译器和处理器都必须遵守as-if-serial语义,as-if-serial语义的意思指:不管编译器和处理器为了提高并行度怎么重排序,(单线程)程序的执行结果不能被改变。
存在数据依赖关系的不允许重排序
as-if-serial规则和happens-before规则是什么?两者的区别?
as-if-serial规则(语义)
as-if-serial语义的意思指:不管编译器和处理器为了提高并行度怎么重排序,(单线程)程序的执行结果不能被改变。
Happens-Before规则
1、程序次序规则:在一个线程内,按照程序控制流顺序,书写在前面的操作先行发生于书写在后面的操作。
2、管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。
3、volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作。
4、线程启动规则:Thread对象的start方法先行发生于此线程的每一个动作。
5、线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测。
6、线程中断规则:对线程interrupt方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
7、对象终结原则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize垃圾回收方法的开始。
as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。
as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。
as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。
LockSupport详解
LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,当然阻塞之后肯定得有唤醒的方法。每个使用LockSupport的线程都会与一个许可关联,如果该许可可用,那这个线程调用park将会立即返回,否则阻塞。如果许可不可用,则可以调用unpark使其可用(许可只有一个,不可累加)
LockSupport有常用的方法如下,主要有两类方法:park和unpark
publicstaticvoidpark(Objectblocker);//暂停当前线程publicstaticvoidparkNanos(Objectblocker,longnanos);//暂停当前线程,不过有超时时间的限制publicstaticvoidparkUntil(Objectblocker,longdeadline);//暂停当前线程,直到某个时间publicstaticvoidpark;//无期限暂停当前线程publicstaticvoidparkNanos(longnanos);//暂停当前线程,不过有超时时间的限制publicstaticvoidparkUntil(longdeadline);//暂停当前线程,直到某个时间publicstaticvoidunpark(Threadthread);//恢复当前线程publicstaticObjectgetBlocker(Threadt);
为什么叫park呢,park英文意思为停车。我们如果把Thread看成一辆车的话,park就是让车停下,unpark就是让车启动然后跑起来。
publicclassLockSupportDemo{publicstaticObjectu=newObject;staticChangeObjectThreadt1=newChangeObjectThread("t1");staticChangeObjectThreadt2=newChangeObjectThread("t2");publicstaticclassChangeObjectThreadextendsThread{publicChangeObjectThread(Stringname){super(name);}@Overridepublicvoidrun{synchronized(u){System.out.println("in"+getName);LockSupport.park;if(Thread.currentThread.isInterrupted){System.out.println("被中断了");}System.out.println("继续执行");}}}publicstaticvoidmain(String[]args)throwsInterruptedException{t1.start;Thread.sleep(1000L);t2.start;Thread.sleep(3000L);t1.interrupt;LockSupport.unpark(t2);t1.join;t2.join;}}
int1被中断了继续执行int2继续执行
这儿park和unpark其实实现了wait和notify的功能,不过还是有一些差别的。
park不需要获取某个对象的锁
因为中断的时候park不会抛出InterruptedException异常,所以需要在park之后自行判断中断状态,然后做额外的处理
我们再来看看Objectblocker,这是个什么东西呢?这其实就是方便在线程dump的时候看到具体的阻塞对象的信息。
"t1"#10prio=5os_prio=31tid=0x00007f95030cc800nid=0x4e03waitingoncondition[0x00007000011c9000]java.lang.Thread.State:WAITING(parking)atsun.misc.Unsafe.park(NativeMethod)atjava.util.concurrent.locks.LockSupport.park(LockSupport.java:304)//`下面的这个信息`atcom.wtuoblist.beyond.concurrent.demo.chapter3.LockSupportDemo$ChangeObjectThread.run(LockSupportDemo.java:23)//-locked(ajava.lang.Object)
park和unpark的使用不会出现死锁的情况,这是因为park和unpark会对每个线程维持一
个许可(boolean值)
unpark调用时,如果当前线程还未进入park,则许可为true
park调用时,判断许可是否为true,如果是true,则继续往下执行;如果是false,则等待,直到许可为true
总结一下
park和unpark可以实现类似wait和notify的功能,但是并不和wait和notify交叉,也就是说unpark不会对wait起作用,notify也不会对park起作用。
park和unpark的使用不会出现死锁的情况
blocker的作用是在dump线程的时候看到阻塞对象的信息
hreadLocal是什么?原理是什么?有哪些使用场景?
什么是ThreadLocal?
ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
packagecom.javaBase.LineDistance;/***〈一句话功能简述〉;*〈功能详细描述〉**@authorjxx*@see[相关类/方法](可选)*@since[产品/模块版本](可选)*/publicclassTestThreadLocal{publicstaticvoidmain(String[]args){ThreadLocalthreadLocal=newMyThreadLocal;Threadt1=newThread(newRunnable{@Overridepublicvoidrun{for(inti=0;i{@OverrideprotectedIntegerinitialValue{return0;}}}
线程2:1线程1:1线程2:2线程3:1线程1:2线程3:2线程2:3线程3:3线程1:3
可知个线程之间对ThreadLocal的操作互不影响。