notify和notifyAll的区别
notify
notify可以唤醒一个处于等待状态的线程,上代码:
public class Main{ public static void main(String[] args) { Object lock = new Object(); Thread threadA = new Thread(new Runnable() { @Override public void run() { synchronized (lock) { try { lock.wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } print(); } } }); Thread threadB = new Thread(new Runnable() { @Override public void run() { synchronized (lock) { print(); lock.notify(); } } }); threadA.setName("threadA"); threadB.setName("threadB"); threadA.start(); threadB.start(); } public static void print() { System.out.println("当前线程:"+Thread.currentThread().getName()+"执行print"); try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("当前线程:"+Thread.currentThread().getName()+"执行完毕"); } }
执行结果:
代码解释:
线程A在开始执行时立即调用wait进入无限等待状态,如果没有别的线程来唤醒它,它将一直等待下去,所以此时B持有锁开始执行,并且在执行完毕时调用了notify方法,该方法可以唤醒wait状态的A线程,于是A线程苏醒,开始执行剩下的代码。
notifyAll
notifyAll可以用于唤醒所有等待的线程,使所有处于等待状态的线程都变为ready状态,去重新争夺锁。
public class Main{ public static void main(String[] args) { Object lock = new Object(); Thread threadA = new Thread(new Runnable() { @Override public void run() { synchronized (lock) { try { lock.wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } print(); } } }); Thread threadB = new Thread(new Runnable() { @Override public void run() { synchronized (lock) { print(); lock.notifyAll(); } } }); threadA.setName("threadA"); threadB.setName("threadB"); threadA.start(); threadB.start(); } public static void print() { System.out.println("当前线程:"+Thread.currentThread().getName()+"执行print"); try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("当前线程:"+Thread.currentThread().getName()+"执行完毕"); } }
执行结果:
要唤醒前一个例子中的线程A,不光notify方法可以做到,调用notifyAll方法同样也可以做到,那么两者有什么区别呢?
区别
要说清楚他们的区别,首先要简单的说一下Java synchronized的一些原理,在openjdk中查看java的源码可以看到,java对象中存在monitor锁,monitor对象中包含锁池和等待池。
锁池,
假设有多个对象进入synchronized块争夺锁,而此时已经有一个对象获取到了锁,那么剩余争夺锁的对象将直接进入锁池中。
等待池,
假设某个线程调用了对象的wait方法,那么这个线程将直接进入等待池,而等待池中的对象不会去争夺锁,而是等待被唤醒。
下面可以说notify和notifyAll的区别了:
notifyAll会让所有处于等待池中的线程全部进入锁池去争夺锁,而notify只会随机让其中一个线程去争夺锁。
yield方法
概念
/** * A hint to the scheduler that the current thread is willing to yield * its current use of a processor. The scheduler is free to ignore this * hint. * * <p> Yield is a heuristic attempt to improve relative progression * between threads that would otherwise over-utilise a CPU. Its use * should be combined with detailed profiling and benchmarking to * ensure that it actually has the desired effect. * * <p> It is rarely appropriate to use this method. It may be useful * for debugging or testing purposes, where it may help to reproduce * bugs due to race conditions. It may also be useful when designing * concurrency control constructs such as the ones in the * {@link java.util.concurrent.locks} package. */ public static native void yield();
yield源码上有一段长长的注释,其大意是说:当前线程调用yield方法时,会给当前线程调度器一个暗示,当前线程愿意让出CPU的使用,但是它的作用应结合详细的分析和测试来确保已经达到了预期的效果,因为调度器可能会无视这个暗示,使用这个方法是不那么合适的,或许在测试环境中使用它会比较好。
测试:
public class Main{ public static void main(String[] args) { Thread threadA = new Thread(new Runnable() { @Override public void run() { System.out.println("ThreadA正在执行yield"); Thread.yield(); System.out.println("ThreadA执行yield方法完成"); } }); Thread threadB = new Thread(new Runnable() { @Override public void run() { System.out.println("ThreadB正在执行yield"); Thread.yield(); System.out.println("ThreadB执行yield方法完成"); } }); threadA.setName("threadA"); threadB.setName("threadB"); threadA.start(); threadB.start(); }
测试结果:
可以看出,存在不同的测试结果,这里选出两张。
第一种结果:线程A执行完yield方法,让出cpu给线程B执行。然后两个线程继续执行剩下的代码。
第二种结果:线程A执行yield方法,让出cpu给线程B执行,但是线程B执行yield方法后并没有让出cpu,而是继续往下执行,此时就是系统无视了这个暗示。
interrupt方法
中止线程
interrupt函数可以中断一个线程,在interrupt之前,通常使用stop方法来终止一个线程,但是stop方法过于暴力,它的特点是,不论被中断的线程之前处于一个什么样的状态,都无条件中断,这会导致被中断的线程后续的一些清理工作无法顺利完成,引发一些不必要的异常和隐患,还有可能引发数据不同步的问题。
温柔的interrupt方法
interrupt方法的原理与stop方法相比就显得温柔的多,当调用interrupt方法去终止一个线程时,它并不会暴力地强制终止线程,而是通知这个线程应该要被中断了,和yield一样,这也是一种暗示,至于是否应该中断,由被中断的线程自己去决定。当对一个线程调用interrupt方法时:
- 如果该线程处于被阻塞状态,则立即退出阻塞状态,抛出InterruptedException异常。
- 如果该线程处于running状态,则将该线程的中断标志位设置为true,被设置的线程继续运行,不受影响,当运行结束时由线程决定是否被中断。
线程池
线程池的引入是用来解决在日常开发的多线程开发中,如果开发者需要使用到非常多的线程,那么这些线程在被频繁的创建和销毁时,会对系统造成一定的影响,有可能系统在创建和销毁这些线程所耗费的时间会比完成实际需求的时间还要长。
另外,在线程很多的状况下,对线程的管理就形成了一个很大的问题,开发者通常要将注意力从功能上转移到对杂乱无章的线程进行管理上,这项动作实际上是非常耗费精力的。
利用Executors创建不同的线程池满足不同场景的需求
newFixThreadPool(int nThreads)
指定工作线程数量的线程池。
newCachedThreadPool()
处理大量中断事件工作任务的线程池,
- 试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程。
- 如果线程闲置的时间超过阈值,则会被终止并移出缓存。
- 系统长时间闲置的时候,不会消耗什么资源。
newSingleThreadExecutor()
创建唯一的工作线程来执行任务,如果线程异常结束,会有另一个线程取代它。可保证顺序执行任务。
newSingleThreadScheduledExecutor()与newScheduledThreadPool(int corePoolSize)
定时或周期性工作调度,两者的区别在于前者是单一工作线程,后者是多线程
newWorkStealingPool()
内部构建ForkJoinPool,利用working-stealing算法,并行地处理任务,不保证处理顺序。
Fork/Join框架:把大任务分割称若干个小任务并行执行,最终汇总每个小任务后得到大任务结果的框架。
为什么要使用线程池
线程是稀缺资源,如果无限制地创建线程,会消耗系统资源,而线程池可以代替开发者管理线程,一个线程在结束运行后,不会销毁线程,而是将线程归还线程池,由线程池再进行管理,这样就可以对线程进行复用。
所以线程池不但可以降低资源的消耗,还可以提高线程的可管理性。
使用线程池启动线程
public class Main{ public static void main(String[] args) { ExecutorService newFixThreadPool = Executors.newFixedThreadPool(10); newFixThreadPool.execute(new Runnable() { @Override public void run() { // TODO Auto-generated method stub System.out.println("通过线程池启动线程成功"); } }); newFixThreadPool.shutdown(); } }
新任务execute执行后的判断
要知道这个点首先要先说说ThreadPoolExecutor的构造函数,其中有几个参数:
- corePoolSize:核心线程数量。
- maximumPoolSize:线程不够用时能创建的最大线程数。
- workQueue:等待队列。
那么新任务提交后会执行下列判断:
- 如果运行的线程少于corePoolSize,则创建新线程来处理任务,即时线程池中的其它线程是空闲的。
- 如果线程池中的数量大于等于corePoolSize且小于maximumPoolSize,则只有当workQueue满时,才创建新的线程去处理任务。
- 如果设置的corePoolSize和maximumPoolSize相同,则创建的线程池大小是固定的,如果此时有新任务提交,若workQueue未满,则放入workQueue,等待被处理。
- 如果运行的线程数大于等于maximumPoolSize,maximumPoolSize,这时如果workQueue已经满了,则通过handler所指定的策略来处理任务。
handler 线程池饱和策略
- AbortPolicy:直接抛出异常,默认。
- CallerRunsPolicy:用调用者所在的线程来执行任务。
- DiscardOldestPolicy:丢弃队列中靠最前的任务,并执行当前任务。
- DiscardPolicy:直接丢弃任务
- 自定义。
线程池的大小如何选定
这个问题并不是什么秘密,在网上各大技术网站均有文章说明,我就拿一个最受认可的写上吧
- CPU密集型:线程数 = 核心数或者核心数+1
- IO密集型:线程数 = CPU核数*(1+平均等待时间/平均工作时间)
当然这个也不能完全依赖这个公式,更多的是要依赖平时的经验来操作,这个公式也只是仅供参考而已。
结语
本文提供了一些Java多线程和并发方面最最基础的知识,适合初学者了解Java多线程的一些基本知识,如果想了解更多的关于并发方面的内容可以看: