一、为什么引入线程阻塞机制?
为了解决对共享存储区的访问冲突,Java 引入了同步机制,现在让我们来考察多个线程对共享资源的访问,显然同步机制已经不够了,因为在任意时刻所要求的资源不一定已经准备好了被访问,反过来,同一时刻准备好了的资源也可能不止一个。为了解决这种情况下的访问控制问题,Java 引入了对阻塞机制的支持。
阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪),学过操作系统的同学对它一定已经很熟悉了。Java 提供了大量方法来支持阻塞,下面让我们逐一分析。
二、Java中实现线程阻塞的方法:
(1)线程睡眠:Thread.sleep (long millis)方法,使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。
(2)线程等待:Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 唤醒方法。这个两个唤醒方法也是Object类中的方法,行为等价于调用 wait() 一样。wait() 和 notify() 方法:两个方法配套使用,wait() 使得线程进入阻塞状态,它有两种形式,一种允许 指定以毫秒为单位的一段时间作为参数,另一种没有参数,前者当对应的 notify() 被调用或者超出指定时间时线程重新进入可执行状态,后者则必须对应的 notify() 被调用.
(3)线程礼让,Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。yield() 使得线程放弃当前分得的 CPU 时间,但是不使线程阻塞,即线程仍处于可执行状态,随时可能再次分得 CPU 时间。调用 yield() 的效果等价于调度程序认为该线程已执行了足够的时间从而转到另一个线程.
(4)线程自闭,join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
(5)suspend() 和 resume() 方法:两个方法配套使用,suspend()使得线程进入阻塞状态,并且不会自动恢复,必须其对应的resume() 被调用,才能使得线程重新进入可执行状态。典型地,suspend() 和 resume() 被用在等待另一个线程产生的结果的情形:测试发现结果还没有产生后,让线程阻塞,另一个线程产生了结果后,调用 resume() 使其恢复。Thread中suspend()和resume()两个方法在JDK1.5中已经废除,不再介绍。因为有死锁倾向。
这里,笔者放入一张线程生命周期的经典图片,来帮助读者理解,里面展示了一个线程从创建->运行->阻塞->运行->死亡的全过程:
三、常用线程名词解释
主线程:JVM调用程序main()所产生的线程。
当前线程:这个是容易混淆的概念。一般指通过Thread.currentThread()来获取的进程。
后台线程:指为其他线程提供服务的线程,也称为守护线程。JVM的垃圾回收线程就是一个后台线程。用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束
前台线程:是指接受后台线程服务的线程,其实前台后台线程是联系在一起,就像傀儡和幕后操纵者一样的关系。傀儡是前台线程、幕后操纵者是后台线程。由前台线程创建的线程默认也是前台线程。可以通过isDaemon()和setDaemon()方法来判断和设置一个线程是否为后台线程。
可见进程:可见进程是指一些不在前台,但用户依然可见的进程,举例来说:各种widget、输入法等,都属于visibe。这部分进程虽然不在前台,但与我们的使用也是密切相关,我们并不希望它被系统终止。
“前台可见进程服务于后台空进程”——这是记录线程重要性的口诀,
重要性一次递减即,前台进程>可见进程>服务进程>后台进程>空进程。
线程类的一些常用方法:
sleep(): 强迫一个线程睡眠N毫秒。
isAlive(): 判断一个线程是否存活。
join(): 等待线程终止。
activeCount(): 程序中活跃的线程数。
enumerate(): 枚举程序中的线程。
currentThread(): 得到当前线程。
isDaemon(): 一个线程是否为守护线程。
setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束)
setName(): 为线程设置一个名称。
wait(): 强迫一个线程等待。
notify(): 通知一个线程继续运行。
setPriority(): 设置一个线程的优先级。
补充:java处理线程阻塞的小技巧
在java中我们使用多线程去处理一些业务,如果业务比较复杂且当并发量有挺大的时候,很有可能出现线程阻塞的问题。
案例:
有一个触发接口,根据触发的信息内部开启多个线程去执行业务,每个线程都会去执行两种业务:私有业务(比如调用不同的接口)、公共业务(比如执行存储、mq发送等等),当私有业务处理时间很快而公共业务处理时间比较长,这样的情景下就可以把私有业务和公共业务分到不同线程执行。
例如:
当触发了这个接口,根据接口触发的信息,需要开启10个线程,那么就可以创建10个线程去执行它的私有业务,然后再额外创建一个线程去拿到前面那10个线程的执行返回结果并进行公共业务的处理。
这样有个好处,就是能让线程池很快的回收线程,能有效防止线程的阻塞
量化:
单个私有业务1秒钟能执行完成,单个公共业务需要5秒钟才能执行完成,如果接口被触发,发现需要创建100个线程执行,那么线程池回收这些线程池至少需要等待6秒,如果按照前面说的分成两个线程,那么就需要创建101个线程,而1秒后就能回收掉执行完成的100个线程
但是这里需要做权衡,如果接口被触发的时候发现需要开启的线程比较多且公共业务很耗时,这种情况下执行公共业务只有单个线程同步执行,那么这个线程就会执行比较长的时间,所以执行公共业务的时候也可根据实际情况开启多个线程。
下面写了个小demo:
1.私有业务的类:
@Component public class Calculation { public Result cal(String req, int a, int b) { System.out.println("请求id:" + req + " 结果:" + (a + b)); return new Result(req, a + b); } }
2.公共业务的类:
@Component public class SomethingElse { public void doElse(Result result) { try { System.out.println(Thread.currentThread().getName() + " : 开始做其他事情,请求号:" + result.getReq() + " ,请求结果:" + result.getSum()); Thread.sleep(2000); System.out.println(Thread.currentThread().getName() + " : 完成做其他事情,请求号:" + result.getReq() + " ,请求结果:" + result.getSum()); } catch (InterruptedException e) { } } }
3.私有业务的线程类:
public class CallTask implements Callable{ private String req; private int a; private int b; @Override public Result call() throws Exception { Calculation calculation = Main.applicationContext.getBean(Calculation.class); return calculation.cal(req, a, b); } public CallTask(String req, int a, int b) { this.req = req; this.a = a; this.b = b; } // getter and setter 等等 }
4.公共业务的线程类:
public class ElseTask implements Runnable { private CompletionServicecs; private int threadCount; public ElseTask(CompletionService cs, int threadCount) { this.cs = cs; this.threadCount = threadCount; } @Override public void run() { SomethingElse somethingElse = Main.applicationContext.getBean(SomethingElse.class); doElse(somethingElse); } private void doElse(SomethingElse somethingElse) { try { for (int i = 0; i < threadCount; i++) { Future take = cs.take(); Result result = take.get(); somethingElse.doElse(result); } } catch (Exception e) { } } // getter and setter 等等 }
6.测试主方法:
@Service public class Main implements ApplicationContextAware { public static ApplicationContext applicationContext = null; public static void main(String[] args) throws InterruptedException { AbstractApplicationContext appContext = new ClassPathXmlApplicationContext("application01.xml"); ExecutorService executorService = Executors.newFixedThreadPool(100); CompletionServicecs = new ExecutorCompletionService(executorService); //这里启动执行计算的线程 cs.submit(new CallTask("req001", 0, 1)); cs.submit(new CallTask("req002", 0, 2)); cs.submit(new CallTask("req003", 0, 3)); cs.submit(new CallTask("req004", 0, 4)); cs.submit(new CallTask("req005", 0, 5)); //专门的监控线程,并执行其他耗时的线程 executorService.execute(new ElseTask(cs, 5)); executorService.shutdown(); appContext.registerShutdownHook(); } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } }
执行结果如下:
核心思想: 将多线程的公有的业务抽出来(前提是公有业务比较耗时,不然就没必要了)在其他线程里面执行。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。如有错误或未考虑完全的地方,望不吝赐教。