Maison >Java >javaDidacticiel >Concurrence Java - explication détaillée de tous les exemples de mécanismes de thread

Concurrence Java - explication détaillée de tous les exemples de mécanismes de thread

Y2J
Y2Joriginal
2017-04-25 09:20:461595parcourir

Le multi-threading JAVA n'est pas un simple point de connaissance, mais consiste en de nombreux contenus triviaux rassemblés. Il existe de nombreux mécanismes que nous ne pouvons pas nommer, mais ils sont très importants. Nous passerons ici en revue tous les mécanismes de concurrence couramment utilisés.

Veille et concession


Un moyen simple d'affecter objectivement les tâches du thread consiste à appeler la méthode sleep La méthode sleep suspend l'exécution. Après ce délai, continuez l'opération dans le programme . Différent de cela, nous utilisons la méthode rendement une fois que la méthode run a terminé un cycle. La méthode yield indique au CPU que le travail de ce thread est presque terminé et que d'autres threads (avec la même priorité) peuvent utiliser le CPU. . .

sleep

La méthode sleep fera dormir votre thread pendant une période de temps déterminée, puis se réveillera pour continuer à exécuter le code. Cela permet au planificateur de threads de basculer vers un autre thread, qui à son tour pilote une autre tâche. Cependant, la tâche pilotée dépend du mécanisme de thread sous-jacent et du système d’exploitation. Nous ne pouvons pas compter sur cet ordre d'exécution pour la sécurité du programme. Soit vous utilisez le contrôle de synchronisation, soit vous n'utilisez pas de threads du tout et utilisez uniquement des routines coopératives. Ces routines se transféreront le contrôle dans l'ordre spécifié.

L'appel à sleep peut lancer InterruptedException, qui sera interceptée dans la méthode run. Parce que les exceptions ne peuvent pas être propagées à la fonction principale à travers les threads.

yield

Nous utilisons la méthode rendement car une fois que la méthode run a terminé un cycle, la méthode rendement indique au CPU que le travail de ce thread est presque terminé et peut être utilisé par d'autres threads ( ) avec la même priorité pour utiliser le CPU. Il convient toutefois de noter que la représentation du rendement du CPU peut ne pas être adoptée à 100 %. En fait, le rendement est souvent mal utilisé.

Priorité des fils


Chaque fil a une priorité. Ce que nous devons savoir, c'est que, sans traitement spécial, tous les fils Les priorités sont les mêmes.

Par défaut, nous divisons les priorités entre 1 et 10, et les threads hautement prioritaires seront exploités en premier. En parlant de cela, les gens ne peuvent s'empêcher de penser à des termes tels que priorité des processus et vieillissement dans le système d'exploitation. En fait, la machine virtuelle Java utilise la priorité du processus pour analogiser la priorité du thread. Mais le plus gros problème est que chaque système d'exploitation gère la priorité des processus différemment, et la priorité des threads de Java a également une variabilité de plate-forme.

Nous pouvons obtenir la priorité du fil via la méthode getPriority(), et nous pouvons également utiliser setPriority() pour la modifier à tout moment.

Thread.currentThread().getPriority(); 
  //获取线程优先级 
  Thread.currentThread().setPriority(); 
  //修改线程优先级

Nous ne devons pas dépendre de la priorité des threads pour l'exactitude du programme, nous devons utiliser la priorité des threads le moins possible.

Fil gardien/arrière-plan (démon)


fil démon, nous pouvons le traduire en fil démon ou en fil d'arrière-plan.

Le rôle du thread démon est de fournir des services aux autres threads Si tous les autres threads sont quittés et qu'il ne reste que le thread démon, alors le programme se terminera. Il n'est pas nécessaire d'exécuter le thread démon séparément. Par exemple, pour les timers d’autres threads, nous pouvons le définir comme thread démon. Et les threads enfants dérivés du thread démon sont également des threads démon.

t.setDaemon(ture);  
  //将线程转换为守护线程 
  t.isDaemon();  
  //判断线程是否为守护线程

N'ouvrez ni n'utilisez aucune ressource dans le fil du démon ! Le programme se termine lorsque tous les threads non-démons se terminent, vous devez alors penser que l'arrière-plan mettra fin à sa méthode run sans exécuter la clausefinal du thread démon. Vous ne le croyez peut-être pas, mais c'est vrai. System.exit(0); est la seule situation dans laquelle la clause final n'est pas exécutée.

Rejoindre les fils de discussion


Un fil de discussion peut appeler la méthode de jointure sur d'autres fils de discussion. L'effet est d'attendre un certain temps jusqu'à la fin du thread appelé, puis de revenir à ce thread pour continuer vers le bas.

Tout d'abord, dans ce fil, vous devez obtenir une référence d'un autre fil et utiliser cette référence pour appeler la méthode join(). Après l'appel, ce thread sera suspendu jusqu'à la fin du thread cible. Vous pouvez également apporter un paramètre de délai d'attente lors de l'appel de join, de sorte que si le thread cible ne s'est pas terminé à l'expiration de ce délai, la méthode join peut également revenir.

Dans JAVA SE5, la bibliothèque de classes java.util.concurrent a de nouveaux outils ajoutés tels que CyclicBarrier, qui peuvent être plus efficaces que la jointure dans la bibliothèque de classes de thread d'origine.

Groupe de threads


Concernant le groupe de threads, il était encore très populaire dans la version JDK1.2, mais avec l'introduction des versions ultérieures, le groupe de threads n'est plus Tellement simple à utiliser qu’on peut l’ignorer pour les groupes de threads.

Il est préférable de considérer les groupes de discussions comme une tentative infructueuse que vous pouvez simplement ignorer. ——Josué 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对象和锁有这样几个特点。

  1. 锁可以用来保护代码片段,任何时刻只能有一个线程进入被保护的区域

  2. 锁可以管理试图进入临界区的线程

  3. 锁可以拥有一个或多个条件对象

  4. 每个条件对象管理那些因为前面所描述的原因而不能被执行但已经进入被保护代码段的线程

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对象的锁将被锁住。

Limitations des verrous et conditions internes

Bien que les verrous internes soient simples, ils présentent de nombreuses limitations :

  1. Impossible d'interrompre un processus en cours. le thread essayant d'acquérir le verrou

  2. ne peut pas définir de délai d'attente

  3. lors de la tentative d'acquisition du verrou car la condition ne peut pas être instanciée via Condition. N'avoir qu'une seule condition par serrure peut ne pas suffire

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn