实现线程之间的通信
当线程间是可以共享资源时,线程间的通信是协调它们的重要的手段。
1)Object类中wait() \ notify() \ notifyAll()方法。
2)用Condition接口。
Condition是被绑定到Lock上的,要创建一个Lock的Condition对象必须用newCondition()方法。在一个Lock对象里面可以创建多个Condition对象,线程可以注册在指定的Condition对象中,从而可以有选择性地进行线程通知,在线程调度上更加灵活。
在Condition中,用await()替换wait(),用signal()替换notify(),用signal()替换notifyAll(),传统线程的通信方式,Condition都可以实现。调用Condition对象中的方法时,需要被包含在lock()和unlock()之间。
3)管道实现线程间的通信。
实现方式:一个线程发送数据到输出管道流,另一个线程从输入管道流中读取数据。
基本流程:
一、创建管道输出流Pipe的OutputStream pos和管道输入流PipedInputStream pis。
二、将pos和pis匹配,pos.connect(pis)。
三、将pos赋给信息输入信息的线程,pis赋给获取信息的线程,就可以实现线程间的通讯了。
Consumer1:0
Consumer1:1
Consumer1:2
Consumer1:3
Consumer2:4
Consumer2:5
Consumer1:6
缺点:
1)管道流只能在两个线程之间传递数据。
线程consumer1和consumer2同时从pis中read数据,当线程producer往管道流中写入一段数据(1,2,3,4,5,6)后,每一个时刻只有一个线程能获取到该数据,并不是两个线程都能获取到producer发送来的数据,因此一个管道流只能用于两个线程间的通讯。
2)管道流只能实现单向发送,如果要两个线程之间互通讯,则需要两个管道流。
线程producer通过管道流向线程consumer发送数据,如果线程consumer想给线程producer发送数据,则需要新建另一个管道流pos1和pis1,将pos1赋给consumer1,将pis1赋给producer1.
四、使用volatile关键字
如何确保线程安全?
如果多个线程同时运行某段代码,如果每次运行结果和单线程运行的结果是一样的,而且其他变量的值也和预期的是一样的,就是线程安全的。
Synchronized,Lock,原子类(如AtomicInteger等),同步容器、并发容器、阻塞队列、同步辅助类(比如CountDownLatch,Semaphore,CyclicBarrier)。
多线程的优点和缺点
优点:
1)充分利用cpu,避免cpu空转
2)程序响应更快
缺点:
1)上下文切换的开销
当CPU从执行一个线程切换到另一个线程的时候,他需要先存储当前线程的本地数据,程序指针等,然后载入另一个线程的本地数据,程序指针等,最后才开始执行。这种切换称为"上下文切换"。CPU会在一个上下文中执行一个线程,然后切换到另外一个上下文中执行另外一个线程。上下文切换并不廉价,如果没有必要,应该减少上下文切换的发生。
2)增加资源消耗
线程在运行的时候需要从计算机里面得到一些资源。除了CPU,线程还需要一些内存来维持它本地的堆栈。它也需要占用操作系统的一些资源来管理线程。
3)编程更复杂
在多线程访问共享数据的时候,要考虑线程安全问题。
写出3条遵循的多线程的最佳实践
1)给线程起个有意义的名字
2)避免锁定和缩小同步的范围
相对于同步方法,我更喜欢同步块,因为它可以拥有对锁的绝对控制权。
3)多用同步辅助类,少用wait和notify
首先,COuntDownLatch,Semaphore、CyclicBarrier这些 同步辅助类简化了编码操作,而且wait和notify很难实现对复杂控制流的控制。其次,这些类是由最好的企业编写和维护在后续的JDK中它们还会不断优化和改善,使用这些更高等级的同步工具可以让程序很轻轻松松的获得优化
4)多用并发容器,少用同步容器
ConcurrentHashMap
多线程的性能一定优于单线程吗?
不一定,要看具体的任务以及计算机的配置。比如:
对于单核CPU,如果是CPU密集型任务,如解压文件,多线程的性能反而不如单线程性能,因为解压文件需要一直还在那用CPU资源,如果采用多线程,线程切换导致的开销反而会让性能下降。如果是交互类型的任务,肯定是需要使用多线程的。
对于多核CPU,对于解压文件来说,多线程肯定优于单线程,因为多个线程能够更加充分利用每个核的资源。
wait()和sleep()的区别
1)这两个方法来自不同的类,sleep来自Thread类,是静态方法,wait()是Object类里面的方法,和notify()或者notifyAll()方法配套使用,来实现线程间的通信。
2)最主要是sleep是将当前线程挂起指定的时间,没有释放锁;而wait 方法释放了锁,使得其他线程可以使用同步控制块或者方法。
3)使用范围:wait、notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以再任何地方使用。
synchronized(x) { x.notify() //或者wait() }
sleep和wait必须捕获异常(Thread.sleep()和Object.wait()都会抛出InterruptedException),notify和notifyAll不需要捕获异常。
Java中interrupted()和isInterrupted()方法的区别
两个方法都是判断线程是否停止的方法。
1)前者是静态方法,后者是非静态方法。interrupted是作用于当前正在运行的线程,isInterrupted是作用于调用该方法的线程对象所对应的线程。(线程对象对应的线程不一定是当前运行的线程。例如我们可以在A线程中调用B线程对象的isInterrupted方法,此时当前正在运行的线程就是A线程。)
2)前者会将中断状态清除而后者不会。
Java创建线程之后,直接调用start()方法和run()方法的区别?
1)start()方法来启动线程,并在新线程中运行run()方法,真正实现了多线程运行。这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码;通过调用Thread类的start()方法来启动一个线程,这时线程是处于就绪状态,并没有运行,然后通过此Thread类调用run()方法来完成其运行操作,这里方法run()称为线程体,它包含了要执行的这个线程的内容,run()方法运行结束,此线程终止。然后CPU再调度其他线程。
2)直接调用run()方法的话,会把run()方法当做普通方法来调用,会在当前线程中执行run()方法,而不会启动新线程来运行run()方法。程序还是要顺序执行,要等待run()方法体执行完毕后,才可以继续执行下面的代码;程序中只有主线程这一个线程,某程序执行路径还是只有一条,这样就没有达到多线程的目的。
什么是线程的上下文切换?
对于单核CPU,CPU在一个时刻只能运行一个线程,当在运行一个线程的过程中转去运行另外一个线程,这个叫做线程上下文切换(对于进程也是类似)。
线程上下文切换过程中会记录程序计数器、CPU寄存器的状态等数据。
虽然多线程可以使得任务执行的效率得到提升,但是由于在线程切换时同样会带来一定的开销代价,并且多个线程会导致系统资源占用的增加,所以在进行多线程编程时要注意这些因素。
怎么检测一个线程是否拥有锁?
在java.lang.Thread中有一个方法叫holdsLock(Object obj),它返回true,如果当且仅当当前线程拥有某个具体对象的锁
用户线程和守护线程有什么区别?
在Java中创建一个线程,他就被称为用户线程。将一个用户线程设置为守护线程的方法就是在调用start()方法之前,调用对象的setDamon(true)方法。一个守护线程是在后台执行并且不会阻止JVM终止的线程,守护线程的作用是为其他线程的运行提供便利服务。当没有用户线程在运行的时候,JVM关闭程序并且退出。一个守护线程创建的子线程依然是守护线程。
守护线程的一个典型例子就是垃圾回收器。
线程的状态
有三个线程T1、T2、T3,怎么保证他们按顺序执行使用join()方法
在一个主线程中,要求有大量子线程执行完之后,主线程菜执行完成。多种方式,考虑效率。
1)在主函数中使用join()方法。
2)CountDownLatch,一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。
3)使用线程池
Java程序如何停止一个线程
建议使用异常法来终止线程的继续运行。在想要被中断执行的过程中,调用interrupted()方法,该方法用来校验当前线程是否已经被中断,即该线程是否被打上了中断的标记,并不会使得线程立即停止运行,如果返回true,则抛出异常,停止线程的运行。在线程外,调用interrupted()方法,使得该线程打上中断的标记。
如何在两个线程间共享数据
一、每个线程执行的代码相同
若每个线程执行的代码相同,共享数据就比较简单,可以使用同一个Runnable对象,这个Runnable对象就有那个共享数据。
二、每个线程执行的代码不同
如果每个线程执行的代码不同,这时候需要用不同的Runnable对象,将需要共享的数据封装成一个对象,将该对象传给执行不同代码的Runnable对象。
Java中堆和栈有什么不同
栈是一块和线程紧密相关的内存区域。每个线程都有自己的栈内存,用于存储本地变量,方法参数和栈调用,一个线程中存储的变量对其他线程是不可见的。而堆是所有线程共享的一片公用内存区域。对象都在堆里创建,为了提升效率线程会从堆中农一个缓存到自己的栈,如果多个线程使用该变量就可能引发问题,这时volatile变量就可以发挥作用了,它要求线程从主存中读取变量值。
Java中的同步容器类和缺陷
在Java中,同步容器类主要包括2类:
1)Vector、HashTable。
2)Collections类中提供的静态工厂方法创建的类。Collections.synchronizedXXX()。
缺陷:
1)性能问题。
在有多个线程进行访问时,如果多个线程都只是进行读取操作,那么每个时刻就只能有一个线程进行读取,其他线程便只能等待,这些线程必须竞争同一把锁。
2)ConcurrentModificationException异常。
在对Vector等容器进行迭代修改时,会报ConcurrentModificationException异常。但是在并发容器中(如ConcurrentHashMap,CopyOnWriteArrayList等)不会出现这个问题。
为什么说ConcurrentHashMap是弱一致性的?以及为何多个线程并发修改ConcurrentHashMap时不会报ConcurrentModificationException?
1)ConcurrentHashMap#get()
正是因为get操作几乎所有时候都是一个无锁操作,使得同一个Segment实例上的put和get可以同时进行,这就是get操作是弱一致性的根本原因。
2)ConcurrentHashMap#clear()
clear方法很简单,代码如下:
public void clear() { for (int i = 0;i < segments.length; i++) segments[i].clear(); }
因为没有全局的锁,在清除完一个segment之后,正在清理下一个segment的时候,已经清理的segment可能又被加入了新的数据,因此clear返回的时候,ConcurrentHashMap中是可能存在数据的。因此clear方法是弱一致性的。
3)ConcurrentHashMap中的迭代器
在遍历过程中,如果已经遍历的数组上的内容变化了,迭代器不会抛出 ConcurrentModificationException异常。如果未遍历的数组上的内容发生了变化,则有可能反映到迭代过程中。这就是ConcurrentHashMap迭代器弱一致性的表现。
在这种迭代方式中,当iterator被创建后,集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时new新的数据从而不影响原有的数据,iterator完成后再将头指针替换为新的数据,这样iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变,更重要的,这保证了多个线程并发执行的连续性和扩展性,是性能提升的关键。
总结:ConcurrentHashMap的弱一致性主要是为了提升效率,是一致性与效率之间的一种权衡。要成为强一致性,就得到处使用锁,甚至是全局锁,折旧与HashTable和同步HashMap一样了。