Heim >Java >javaLernprogramm >Java-Parallelität – detaillierte Erläuterung aller Beispiele für Thread-Mechanismen
JAVA-Multithreading ist kein einfacher Wissenspunkt, sondern besteht aus vielen trivialen Inhalten, die zusammengefügt werden. Es gibt viele Mechanismen, die wir nicht benennen können, aber sie sind sehr wichtig. Hier werden wir alle häufig verwendeten Parallelitätsmechanismen durchgehen.
Eine einfache Möglichkeit, Thread-Aufgaben objektiv zu beeinflussen, besteht darin, die Schlafmethode aufzurufen. Die Schlafmethode unterbricht die Ausführung bis Nach Ablauf dieser Zeit den Vorgang im Programm fortsetzen . Im Gegensatz dazu verwenden wir die Yield-Methode, nachdem die Run-Methode einen Zyklus abgeschlossen hat. Die Yield-Methode zeigt der CPU an, dass die Arbeit dieses Threads fast erledigt ist und andere Threads (mit derselben Priorität) die CPU verwenden können . .
Die Sleep-Methode lässt Ihren Thread für einen festgelegten Zeitraum schlafen und wacht dann auf, um mit der Ausführung des Codes fortzufahren. Dadurch kann der Thread-Scheduler zu einem anderen Thread wechseln, der wiederum eine andere Aufgabe steuert. Welche Aufgabe gesteuert wird, hängt jedoch vom zugrunde liegenden Thread-Mechanismus und dem Betriebssystem ab. Wir können uns im Hinblick auf die Sicherheit des Programms nicht auf diese Ausführungsreihenfolge verlassen. Verwenden Sie entweder die Synchronisationssteuerung oder verwenden Sie überhaupt keine Threads und verwenden Sie nur kooperative Routinen. Diese Routinen übertragen die Kontrolle untereinander in der angegebenen Reihenfolge.
Der Aufruf von Sleep kann eine InterruptedException auslösen, die in der run-Methode abgefangen wird. Weil Ausnahmen nicht über Threads hinweg an die Hauptfunktion zurückpropagiert werden können.
Wir verwenden die Yield-Methode, denn nachdem die Run-Methode einen Zyklus abgeschlossen hat, zeigt die Yield-Methode der CPU an, dass die Arbeit dieses Threads fast erledigt ist und kann von anderen ( ) Threads mit der gleichen Priorität verwendet werden, um die CPU zu nutzen. Es ist jedoch zu beachten, dass die Darstellung der Ausbeute an die CPU möglicherweise nicht zu 100 % übernommen wird. Tatsächlich wird der Ertrag häufig missbraucht.
Jeder Thread hat eine Priorität. Wir müssen wissen, dass alle Threads ohne spezielle Verarbeitung die gleichen Prioritäten haben.
Standardmäßig unterteilen wir die Prioritäten in 1 bis 10, und Threads mit hoher Priorität werden zuerst bearbeitet. Wenn man hiervon spricht, denkt man zwangsläufig an Begriffe wie Prozesspriorität und Alterung im Betriebssystem. Tatsächlich nutzt die Java Virtual Machine die Priorität des Prozesses, um die Priorität des Threads zu analogisieren. Das größte Problem dabei ist jedoch, dass jedes Betriebssystem die Prozesspriorität anders handhabt und die Thread-Priorität von Java auch Plattformvariabilität aufweist.
Wir können die Priorität des Threads über die Methode getPriority() ermitteln und sie auch jederzeit mit setPriority() ändern.
Thread.currentThread().getPriority(); //获取线程优先级 Thread.currentThread().setPriority(); //修改线程优先级
Wir sollten uns für die Korrektheit des Programms nicht auf die Thread-Priorität verlassen, wir sollten die Thread-Priorität so wenig wie möglich nutzen.
Daemon-Thread, wir können ihn in einen Daemon-Thread oder Hintergrund-Thread übersetzen.
Die Rolle des Daemon-Threads besteht darin, Dienste für andere Threads bereitzustellen Wenn alle anderen Threads beendet werden und nur der Daemon-Thread übrig bleibt, wird das Programm beendet. Es ist nicht erforderlich, einen separaten Daemon-Thread auszuführen. Für die Timer anderer Threads können wir ihn beispielsweise als Daemon-Thread festlegen. Und die vom Daemon-Thread abgeleiteten untergeordneten Threads sind ebenfalls Daemon-Threads.
t.setDaemon(ture); //将线程转换为守护线程 t.isDaemon(); //判断线程是否为守护线程
Öffnen oder verwenden Sie keine Ressourcen im Daemon-Thread! Das Programm wird beendet, wenn alle Nicht-Daemon-Threads beendet werden. Dann müssen Sie davon ausgehen, dass der Hintergrund seine Ausführungsmethode beendet, ohne die Final-Klausel des Daemon-Threads auszuführen. Sie glauben es vielleicht nicht, aber es ist wahr. System.exit(0); ist die einzige Situation, in der die final-Klausel nicht ausgeführt wird.
Ein Thread kann die Join-Methode für andere Threads aufrufen. Der Effekt besteht darin, eine Zeit lang zu warten, bis der aufgerufene Thread endet, und dann zu diesem Thread zurückzukehren, um nach unten fortzufahren.
Zuerst müssen Sie in diesem Thread eine Referenz von einem anderen Thread erhalten und diese Referenz verwenden, um die Methode join() aufzurufen. Nach dem Aufruf wird dieser Thread angehalten, bis der Ziel-Thread endet. Sie können beim Aufrufen von Join auch einen Timeout-Parameter mitbringen, sodass die Join-Methode auch zurückkehren kann, wenn der Ziel-Thread nach Ablauf dieser Zeit nicht beendet wurde.
In JAVA SE5 verfügt die Klassenbibliothek java.util.concurrent über neu hinzugefügte Tools wie CyclicBarrier, die möglicherweise effektiver sind als Join in der ursprünglichen Thread-Klassenbibliothek.
Die Thread-Gruppe war in der JDK1.2-Version immer noch sehr beliebt, mit der Einführung nachfolgender Versionen ist sie jedoch nicht mehr beliebt Es ist so einfach zu verwenden, dass wir es für Thread-Gruppen ignorieren können.
Stellen Sie sich Thread-Gruppen am besten als einen erfolglosen Versuch vor, den Sie einfach ignorieren können. ——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对象的锁将被锁住。
Obwohl interne Sperren einfach sind, weisen sie viele Einschränkungen auf:
Kann einen laufenden Prozess nicht unterbrechen Thread, der versucht, die Sperre zu erhalten
kann beim Versuch, die Sperre zu erhalten, kein Timeout
festlegen, da die Bedingung nicht über Condition instanziiert werden kann. Es reicht möglicherweise nicht aus, nur eine einzige Bedingung pro Sperre zu haben
Das obige ist der detaillierte Inhalt vonJava-Parallelität – detaillierte Erläuterung aller Beispiele für Thread-Mechanismen. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!