java并发--全部线程机制实例详解
JAVA多线程并不是一个简单的知识点,而是由很多个琐碎的内容拼合在一起。有很多我们都说不上来的机制但是很重要,我们这里就将所有的常用的并发机制全部捞一遍。
休眠与让步
客观的影响线程任务的一种简单方法是调用sleep方法,sleep方法中止执行给定的时间,在这段时间过后继续进行程序中的操作。而与之不同的,我们使用yield方法是在run方法完成一个循环后,yield方法向CPU表示本线程的工作做的差不多了,可以让其他(具有相同优先级的)线程来使用CPU。
sleep
sleep方法会让你的线程固定休眠一段时间,之后再被唤醒继续执行代码。这使得线程调度器可以切换到另一个线程,进而驱动另一个任务。但是,具体驱动的是哪一个任务,这和底层的线程机制、操作系统有关。我们不能把程序的安全性寄希望于这种执行顺序。或者使用同步控制,或者根本不适用线程,自己只用协作例程,这些例程才会按照指定的顺序在互相之间传递控制权。
对sleep的调用可能抛出InterruptedException异常,这个异常将在run方法中被捕获。因为异常不能跨线程传播回main函数。
yield
我们使用yield方法是在run方法完成一个循环后,yield方法向CPU表示本线程的工作做的差不多了,可以让其他(具有相同优先级的)线程来使用CPU。但是需要注意的是,yield向CPU的表示并不一定会百分百采纳。事实上,yield经常被误用。
线程优先级
每个线程都有一个优先级,我们需要知道的是,在没经过特殊处理的时候,所有的线程优先级都是一样的。
默认的,我们把优先级分成1到10之间,高优先级的线程会先被操作。说到这里不由得让人想起操作系统中进程优先级、老化这类名词。事实上,java虚拟机也确实是用进程的优先级来类比出线程的优先级。但这样做最大的问题在于,每个操作系统对于进程优先级的处理并不相同,java的线程优先级也因此而具有平台变化性。
我们可以通过getPriority()方法来获取线程的优先级,而且我们也可以随时使用setPriority()来修改它。
Thread.currentThread().getPriority(); //获取线程优先级 Thread.currentThread().setPriority(); //修改线程优先级
我们不应该把程序的正确性依赖于线程优先级,我们应该尽量少的使用线程优先级。
守护/后台(daemon)线程
daemon thread,我们可以把他翻译成守护线程或者后台线程。
守护线程的作用是为其他的线程提供服务,如果其他所有的线程都被退出,只剩下守护线程,那么程序也就结束了。没有去单独运行守护线程的必要。比如说其他线程的计时器,我们就可以将它设置为一个守护线程。而且守护线程派生出的子线程也是守护线程。
t.setDaemon(ture); //将线程转换为守护线程 t.isDaemon(); //判断线程是否为守护线程
不要在守护线程中打开或使用任何资源!所有非守护线程退出时程序终止,那么你就要想到,后台会在不执行守护线程的finally子句的情况下终止其run方法。说出来你可能不信,但是确实如此,System.exit(0);是唯一让finally子句不执行的情况。
加入线程
一个线程可以在其他线程之上调用join方法。效果是等待一段时间直到被调用的那个线程结束之后,再回到这个线程继续向下进行。
首先在这个线程中要获得另外一个线程的引用,并且使用这个引用,调用join()方法。调用之后这个线程将被挂起,直到目标线程结束才恢复。也可以在调用join时带上一个超时参数,这样如果目标线程在这段时间到期时还没有结束的话,join方法也能返回。
在JAVA SE5中java.util.concurrent类库新增加了CyclicBarrier这样的工具,它们可能比最初的线程类库中的join更加有效。
线程组
关于线程组,在JDK1.2版本中还很流行,但是随着之后的版本的推出,线程组并不是那么好用,对于线程组我们可以忽略他。
最好把线程组看成一次不成功的尝试,你只要忽略就好了。——Joshua Bloch
捕获异常
因为多线程的机制,我们不能在main函数中捕获其他线程的异常。这就说明,如果我们不能在run方法中自己捕获异常,那么异常会被抛出到控制台上。除非我们采用特殊的机制来捕获。
package AllThread;/** * * @author QuinnNorris * * 捕获异常 */public class ExceptionThread { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub Thread th = new Thread(new Runnable() { @Override public void run() { // TODO Auto-generated method stub throw new RuntimeException(); } }); th.start(); } }
这是一段简单的代码,它会抛出一个运行时异常:
Exception in thread “Thread-0” java.lang.RuntimeException at AllThread.ExceptionThread$1.run(ExceptionThread.java:15) at java.lang.Thread.run(Thread.java:745)
我们可以看出, 由于没有去设计捕获异常,它被直接输出到控制台上。对于这种情况,为main函数加上try-catch语句是没有用的。
为了解决这种不能捕获未检查异常的情况,在JAVA SE5中引入了使用Executor的一种解决方法。
package AllThread;import java.lang.Thread.UncaughtExceptionHandler;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.ThreadFactory;/** * * @author QuinnNorris * * 使用UncaughtExceptionHandler捕获异常 */public class UEHThread { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub ExecutorService es = Executors.newCachedThreadPool(new ThreadFactory() { @Override public Thread newThread(Runnable r) { // TODO Auto-generated method stub Thread th = new Thread(r); th.setUncaughtExceptionHandler(new UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable e) { // TODO Auto-generated method stub System.out.println("catch it " + e); } }); return th; } }); es.execute(new Runnable() { @Override public void run() { // TODO Auto-generated method stub throw new RuntimeException(); } }); } }
因为我比较懒全部用内部类来表示,所以这段程序可能略有些难懂。首先我们创建了一个线程池,然后为这个创建线程池的静态方法赋予一个参数。这个参数是一个ThreadFactory类,这个类是用来描述在线程池中的线程具有的共性的。ThreadFactory有一个方法需要我们覆盖就是newThread方法,这个方法的参数是我们要处理的Runnable任务,也就是我们要加入到线程池中的Runnable任务。我们在这个方法中用一个th对象包含r对象,然后设置th对象的UncaughtExceptionHandler属性。这个setUncaughtExceptionHandler方法的参数是一个UncaughtExceptionHandler对象(这里我们第二次用内部类),UncaughtExceptionHandler类的唯一一个方法是uncaughtException。这个方法用来表示对线程未检查异常的处理方式,我们让他在控制台输出一句话。到这里我们对线程池的部署就完成了。
然后我们在这个线程池中添加一个Runnable任务,这个任务会抛出一个未检查异常。现在我们运行程序,控制台输出:
catch it java.lang.RuntimeException
到此,对于线程run方法中的未检查异常的处理就结束了。需要注意的是,我们向线程池中添加线程的方法要调用execute方法而不要使用submit方法,submit方法会把异常吞掉。从而控制台将会什么都不输出。
竞争条件
在操作系统中有一张让人印象深刻的图片。上面画的是一块块并排的进程,在这些进程里面分了几个线程,所有这些线程齐刷刷统一的指向进程的资源。资源会在线程间共享而不是每个线程都有一份独立的资源。在这种共享的情况下,很有可能有多个线程同时在访问一个资源,这种现象我们叫做竞争条件。
在一个银行系统中,每个线程分别管理一个账户,这些线程可能会进行转账的操作。
在一个线程进行操作的时候,他首先,会把账户余额存放到寄存器中,第二步,它将寄存器中的数字减少要转出的钱数,第三步,它将结果写回余额中。
问题在于,这个线程在执行完1、2步时,另外一个线程被唤醒并且修改了第一个线程的账户余额值,但是这个时候第一个线程并不知情。第一个线程等待第二个线程执行完毕后,继续他的第三步:将结果写回余额中。这个时候,它把第二个线程的操作刷掉了,所以钱数发生错误。
同步规则
如果你正在写一个变量,他可能接下来将被另一个线程读取,或者正在读取一个上一次已经被另一个线程写过的变量,那么你必须使用同步,并且,读写线程都必须用相同的监视器锁同步。——Brian
ReentrantLock
上面的例子告诉我们:如果我们的操作不是原子操作,被打断是肯定会发生的。我们没办法把代码变成原子操作,但是能将其上锁来保证安全性。在并发程序中,在访问资源或数据之前,要先给代码套一个锁。在锁被使用的期间,代码中涉及的资源不能被其他的线程访问,直到程序结束时再将锁打开。
ReentrantLock构造器
ReentrantLock类提供了两个构造器:一个是默认构造器,一个是带有公平策略的构造器。
首先,带有公平策略的锁会比正常的锁要慢很多。其次,在某些情况下公平策略并不能保证真正公平的。
如果我们没有特殊的理由真的需要公平策略的时候,尽量不要去使用这种锁。
锁的获取与释放
ReentrantLock myLock = new ReentrantLock(); //创建对象 myLock.lock(); //获取锁try{...} finally{ myLock.unlock(); //释放锁 }
一定要在finally中释放锁。如果不在finally中释放锁,锁确实将一直得不到释放。正如同我们在调用资源后会使用close()方法。值得一提的,当我们使用锁的时候,我们不能使用try-with-resource,因为这个锁并不是用close来关闭的。
ReentrantLock具有可重入性
如果你要在递归或者循环程序中使用锁,那么就放心的用吧。ReentrantLock锁具有可重入性,他会在每次调用lock()的时候维护一个计数记录着被调用的次数,在每一次的lock调用都必须要用unlock来释放。
条件对象
if(a>b) a.set(b-1);
上面是一个很简单的条件判断,但是我们在并发程序中不能直接这样书写。如果在这个线程刚刚做完判断之后,另外一个线程被唤醒,并且另外一个线程在操作之后使得a小于b(if语句中的条件已经不再正确)。但我们还会执行if中的语句,这是不正确的。
或许会想把整个if语句直接放在锁里面,确保自己的代码不会被打断。但是这样又存在一个问题,如果if判断是false,那么if中的语句不会被执行。但如果我们需要去执行if中的语句,甚至我们要一直等待if判断变的正确之后去执行if中的语句的情况下,这时if语句再也不会变得正确了,因为我们的锁把这个线程锁死,其他的线程没办法访问临界区并修改a和b的值让if判断变得正确。这时候我们只能放弃锁,等待其他线程使用,再获得锁,进行判断,如果判断仍未false就重复之前的操作。这种繁琐的过程是我们不希望的。
通常,线程在上锁进入临界区之后存在一个问题:线程所需的资源,在别的线程中使用或并不满足他们能执行的条件,这个时候我们需要用一个条件对象来管理这些得到了一个锁,但是不能做有用工作的线程。
Condition
Condition类在临界区起到了条件对象的作用。
我们用ReentrantLock类中的newCondition方法来获取一个条件对象。
Condition cd = myLock.newCondition();
我们在if语句下面直接跟上await方法,这个方法表示这个线程被阻塞,并放弃了锁,进入等待状态等其他的线程来操作。其他的线程在顺利执行if语句内容之后,调用signalAll方法,这个方法将会重新去激活所有的因为这个条件被阻塞的线程,让这些线程重新获得机会,这些线程被允许从被阻塞的地方继续进行。此时,线程应该再次测试该条件,如果还是不能满足条件,需要再次重复上述操作。
ReentrantLock myLock = new ReentrantLock();//创建锁对象myLock.lock();//给下面的临界区上锁Condition cd = myLock.newCondition();//创建一个Condition对象,这个cd对象表示条件对象while(!(a>b)) cd.await();//上面的while循环和await方法调用是标准写法//如果不能满足if的条件,那么他将进入阻塞状态,放弃锁,等待别人去激活它a.set(b-1);//一直等到从while循环出来,满足了判断的条件,我们执行自己的功能cd.signalAll();//调用signalAll方法去激活其他的被阻塞的线程。如果所有的线程都在等待其他线程signalAll,则进入死锁
总结来说,Condition对象和锁有这样几个特点。
锁可以用来保护代码片段,任何时刻只能有一个线程进入被保护的区域
锁可以管理试图进入临界区的线程
锁可以拥有一个或多个条件对象
每个条件对象管理那些因为前面所描述的原因而不能被执行但已经进入被保护代码段的线程
synchronized
ReentrantLock和Condition对象是一种用来保护代码片段的方法。还可以通过使用关键字synchronized来修饰方法,从而给方法添加一个内部锁。java的每一个对象都有一个内部锁,每个内部锁会保护那些被synchronized修饰的方法。也就是说,如果想调用这个方法,首先要获得内部的对象锁。
所有对象都自动含有单一的锁(也叫做监视器)。当在对象上调用其任意synchronized方法的时候,此对象都被加锁,这时该对象上的其他synchronized方法只有等到前一个方法调用完毕并释放了锁之后才能被调用。
与ReentrantLock比较
我们先拿出上面的代码:
public void function(){ ReentrantLock myLock = new ReentrantLock(); myLock.lock(); Condition cd = myLock.newCondition(); while(!(a>b)) cd.await(); a.set(b-1); cd.signalAll(); }
如果我们用synchronized来实现这段代码,将会变成下面的样子:
public synchronized void function(){ while(!(a>b)) wait(); a.set(b-1); notifyAll(); }
需要我们注意的是,在使用synchronized关键词时,无需再去用ReentrantLock和Condition对象,我们用wait方法替换了await方法,notifyAll方法替换了signalAll方法。这样写确实比之前的简单了很多。
静态synchronized
将静态方法声明为synchronized也是合法的。如果调用这种方法,将会获取相关的类对象的内部锁。比如我们调用Test类中的静态方法,这时,Test.class对象的锁将被锁住。
内部锁和条件的局限性
内部锁虽然简便,但是他存在着很多限制:
不能中断一个正在试图获得锁的线程
试图获得锁时不能设定超时
因为不能通过Condition来实例化条件。每个锁仅有单一的条件,可能是不够的
以上就是java并发--全部线程机制实例详解的详细内容,更多请关注其它相关文章!
上一篇: Python中记录循环次数的方法
下一篇: 单例模式的一点小疑问