ホームページ  >  記事  >  Java  >  Java 同時実行 -- すべてのスレッド メカニズムの例の詳細な説明

Java 同時実行 -- すべてのスレッド メカニズムの例の詳細な説明

Y2J
Y2Jオリジナル
2017-04-25 09:20:461499ブラウズ

JAVA マルチスレッドは単純な知識ポイントではなく、多くの些細な内容が組み合わされて構成されています。名前を付けることはできない多くのメカニズムがありますが、それらは非常に重要です。ここでは、一般的に使用されるすべての同時実行メカニズムについて説明します。

スリープと譲歩


スレッドタスクに客観的に影響を与える簡単な方法は、sleep メソッドを呼び出すことです。sleep メソッドは、一定時間実行を一時停止し、この時間の経過後にプログラム内の操作を継続します。これとは異なり、run メソッドがループを完了した後に、yield メソッドを使用します。yield メソッドは、このスレッドの作業がほぼ完了し、他のスレッド (同じ優先度を持つ) が CPU を使用できることを CPU に示します。 sleep

sleep メソッドは、スレッドを一定期間スリープさせ、その後復帰してコードの実行を継続します。これにより、スレッド スケジューラが別のスレッドに切り替えて、別のタスクを駆動できるようになります。ただし、どのタスクが駆動されるかは、基礎となるスレッド メカニズムとオペレーティング システムによって異なります。プログラムのセキュリティをこの実行順序に依存することはできません。同期制御を使用するか、スレッドをまったく使用せず、協調ルーチンのみを使用します。これらのルーチンは、指定された順序で相互に制御を転送します。

スリープへの呼び出しにより InterruptedException がスローされる場合があり、これは run メソッドでキャッチされます。例外をスレッド間でメイン関数に伝播させることができないためです。

yield

run メソッドがループを完了した後、yield メソッドは、このスレッドの作業がほぼ完了し、他のスレッド (同じ優先度を持つ) が使用できることを CPU に示すため、yield メソッドを使用します。 CPU。ただし、CPU に対する歩留まりの表現が 100% 採用されるわけではないことに注意してください。実際、収量は誤用されることがよくあります。

スレッドの優先順位

すべてのスレッドには優先順位があることを知っておく必要があります。特別な処理を行わなくても、すべてのスレッドの優先順位は同じであるということです。


デフォルトでは優先度を1~10に分けており、優先度の高いスレッドから動作します。そういえば、オペレーティング システムの

プロセス優先度

エイジングといった用語を思わずにはいられません。実際、Java 仮想マシンはプロセスの優先順位を使用して、スレッドの優先順位を類推します。しかし、これに関する最大の問題は、オペレーティング システムごとにプロセス優先度の処理方法が異なり、Java のスレッド優先度にもプラットフォームのばらつきがあることです。 getPriority() メソッドを通じてスレッドの優先度を取得できます。また、setPriority() を使用していつでも優先度を変更することもできます。

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

プログラムの正確性をスレッド優先度に依存すべきではなく、スレッド優先度の使用を可能な限り少なくする必要があります。

デーモン スレッド

デーモン スレッドは、デーモン スレッドまたはバックグラウンド スレッドに変換できます。 デーモン スレッドの役割は、他のスレッドにサービスを提供することです。

他のスレッドがすべて終了し、デーモン スレッドだけが残った場合、プログラムは終了します
。別のデーモン スレッドを実行する必要はありません。たとえば、他のスレッドのタイマーについては、デーモン スレッドとして設定できます。また、デーモンスレッドから派生した子スレッドもデーモンスレッドです。

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

デーモン スレッド内のリソースを開いたり使用したりしないでください。

デーモン以外のスレッドがすべて終了するとプログラムは終了します。その場合、バックグラウンドはデーモン スレッドのfinally句を実行せずにrunメソッドを終了すると考える必要があります。信じられないかもしれませんが、これは事実であり、finally 句が実行されない唯一の状況です。

Join Threads

スレッドは他のスレッドの join メソッドを呼び出すことができます。その結果、呼び出されたスレッドが終了するまで一定期間待機し、その後このスレッドに戻って下方向に続行します。 まず、このスレッド内の別のスレッドから参照を取得し、この参照を使用して join() メソッドを呼び出します。呼び出し後、このスレッドはターゲット スレッドが終了するまで一時停止されます。 join を呼び出すときにタイムアウト パラメータを指定することもできます。これにより、この時間が経過してもターゲット スレッドが終了していない場合に、join メソッドが戻ることもできます。


JAVA SE5 では、java.util.concurrent クラス ライブラリに CyclicBarrier などのツールが新たに追加されており、元のスレッド クラス ライブラリに参加するよりも効果的である可能性があります。

スレッドグループ

スレッドグループについては、JDK1.2バージョンでは依然として非常に人気がありますが、後続のバージョンのリリースにより、スレッドグループはそれほど使いにくくなりました。スレッドグループについては無視しても問題ありません。

スレッド グループは、無視してもよい失敗した試みとして考えるのが最善です。 ——ジョシュア・ブロック

捕获异常


因为多线程的机制,我们不能在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对象的锁将被锁住。

内部ロックと条件の制限

内部ロックはシンプルですが、多くの制限があります:

  1. ロックを取得しようとしているスレッドを中断することはできません

  2. ロックを取得しようとするときにタイムアウトを設定することはできませんロックを取得します

  3. Condition を通じて条件をインスタンス化できないためです。ロックごとに条件を 1 つだけ持つだけでは十分ではない可能性があります

以上がJava 同時実行 -- すべてのスレッド メカニズムの例の詳細な説明の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。