JAVA多執行緒並不是一個簡單的知識點,而是由很多個瑣碎的內容拼合在一起。有很多我們都說不上來的機制但是很重要,我們這裡就將所有的常用的並發機制全部撈一遍。
客觀的影響執行緒任務的簡單方法是呼叫sleep方法,sleep方法中止執行給定的時間,在這段時間過後繼續進行程序中的操作。而與之不同的,我們使用yield方法是在run方法完成一個循環後,yield方法向CPU表示本線程的工作做的差不多了,可以讓其他(具有相同優先權的)線程來使用CPU 。
sleep方法會讓你的執行緒固定休眠一段時間,之後再被喚醒繼續執行程式碼。這使得線程調度器可以切換到另一個線程,進而驅動另一個任務。但是,具體驅動的是哪一個任務,這和底層的執行緒機制、作業系統有關。我們不能把程式的安全性寄望於這種執行順序。或者使用同步控制,或根本不適用線程,自己只用協作例程,這些例程才會按照指定的順序在互相之間傳遞控制權。
對sleep的呼叫可能會拋出InterruptedException異常,而這個異常將會在run方法中被捕獲。因為異常不能跨線程傳播回main函數。
我們使用yield方法是在run方法完成一個循環後,yield方法向CPU表示本線程的工作做的差不多了,可以讓其他(具有相同優先權的)執行緒來使用CPU。但要注意的是,yield向CPU的表示並不一定會百分之百採納。事實上,yield經常被誤用。
每個執行緒都有一個優先權,我們需要知道的是,在沒經過特殊處理的時候,所有的執行緒優先順序都是一樣的。
預設的,我們把優先權分成1到10之間,高優先權的執行緒會先被操作。說到這裡不由得讓人想起作業系統中進程優先權、老化這類名詞。事實上,java虛擬機器也確實是用行程的優先權來類比出執行緒的優先權。但這樣做最大的問題在於,每個作業系統對於進程優先權的處理並不相同,java的執行緒優先權也因此而具有平台變化性。
我們可以透過getPriority()方法來取得執行緒的優先權,而且我們也可以隨時使用setPriority()來修改它。
Thread.currentThread().getPriority(); //获取线程优先级 Thread.currentThread().setPriority(); //修改线程优先级
我們不應該把程式的正確性依賴於執行緒優先權,我們應該盡量少的使用執行緒優先權。
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 myLock = new ReentrantLock(); //创建对象 myLock.lock(); //获取锁try{...} finally{ myLock.unlock(); //释放锁 }
一定要在finally中释放锁。如果不在finally中释放锁,锁确实将一直得不到释放。正如同我们在调用资源后会使用close()方法。值得一提的,当我们使用锁的时候,我们不能使用try-with-resource,因为这个锁并不是用close来关闭的。
如果你要在递归或者循环程序中使用锁,那么就放心的用吧。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类在临界区起到了条件对象的作用。
我们用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对象和锁有这样几个特点。
锁可以用来保护代码片段,任何时刻只能有一个线程进入被保护的区域
锁可以管理试图进入临界区的线程
锁可以拥有一个或多个条件对象
每个条件对象管理那些因为前面所描述的原因而不能被执行但已经进入被保护代码段的线程
ReentrantLock和Condition对象是一种用来保护代码片段的方法。还可以通过使用关键字synchronized来修饰方法,从而给方法添加一个内部锁。java的每一个对象都有一个内部锁,每个内部锁会保护那些被synchronized修饰的方法。也就是说,如果想调用这个方法,首先要获得内部的对象锁。
所有对象都自动含有单一的锁(也叫做监视器)。当在对象上调用其任意synchronized方法的时候,此对象都被加锁,这时该对象上的其他synchronized方法只有等到前一个方法调用完毕并释放了锁之后才能被调用。
我们先拿出上面的代码:
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也是合法的。如果调用这种方法,将会获取相关的类对象的内部锁。比如我们调用Test类中的静态方法,这时,Test.class对象的锁将被锁住。
內部鎖定雖然簡便,但是他存在著許多限制:
不能中斷一個正在試圖取得鎖定的執行緒
試圖取得鎖定時不能設定逾時
因為不能透過Condition來實例化條件。每個鎖僅有單一的條件,可能是不夠的
以上是java並發--全部執行緒機制實例詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!