JUCスレッド研究ノート

目次

バックグラウンド

場合

前書き

応用

CountDownLatch

前書き

応用

呼び出し可能なインターフェイス

前書き

使用する

ロック同期ロック

前書き

使用する

揮発性キーワード

同時コンテナクラス

前書き

その他のコンテナクラス

誤ったウェイクアップ

状態の目覚めと待機

読み取り/書き込みロック

前書き

使用法

スレッドプール

前書き

使用法

ForkJoinPoolブランチ/マージフレームワーク

前書き

使用する

代わりに

結論

バックグラウンド

リラックスし続けてください...先月のJUCスレッドの勉強のメモを整理してください。JUCはjava.util.concurrentパッケージの略語であり、サンプルコードはすべてjdk8環境で実行されています。

場合

前書き

CASはCompareAnd Swapの略語であり、データのアトミック性を確保するために使用されます。これは、同時操作とデータ共有のハードウェアサポートです。

メモリ値V、推定値A、更新値Bの3つの値が含まれます

V == A、V = Bの場合に限り、それ以外の場合は何もしません

応用

JavaのCASの実装クラスは、値をアトミックに変更できるアトミックデータクラスです。

i = i ++などの元の変更は、次の3つのステップに分かれています。

int temp = i;

i = i + 1;

i =温度;

これは、マルチスレッド環境では明らかに安全ではありません

public class Main {
    public static void main(String[] args) {
        CASDemo casDemo = new CASDemo();
        for (int i = 0; i < 10; i++) {
            new Thread(casDemo).start();
        }
    }
}


class CASDemo implements Runnable {
    private int mNum = 0;

    @Override
    public void run() {
        try {
            Thread.sleep(500);
            System.out.println(Thread.currentThread().getName() + ":" + getNum());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }


    public int getNum() {
        return mNum++;
    }
}

出力結果の値が重複している可能性がありますが、アトミッククラスに変更した場合、これは発生しません。

public class Main {
    public static void main(String[] args) {
        CASDemo casDemo = new CASDemo();
        for (int i = 0; i < 10; i++) {
            new Thread(casDemo).start();
        }
    }
}


class CASDemo implements Runnable {
    private AtomicInteger mAtomicNum = new AtomicInteger(0);

    @Override
    public void run() {
        try {
            Thread.sleep(500);
            System.out.println(Thread.currentThread().getName() + ":" + getAtomicNum());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }


    public int getAtomicNum() {
        return mAtomicNum.getAndIncrement();
    }
}

CASアルゴリズムを使用して操作のアトミック性を確保することに加えて、アトミック変数はvolatileキーワードを使用してメモリの可視性を確保します

CountDownLatch

前書き

CountDownLatch(ラッチ):一部の操作が完了すると、他のすべてのスレッドの操作のみが完了し、現在の操作は引き続き実行されます

応用

使用法次のコードとそのコメントに参加する

public class Main {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(5); // 初始化闭锁对象,给出初值
        LatchDemo demo = new LatchDemo(latch);


        long start = System.currentTimeMillis();
        for (int i = 0; i < 5; i++) {
            new Thread(demo).start();
        }


        latch.await(); // 等待闭锁值为0

        System.out.println("耗费时间:" + (System.currentTimeMillis() - start));
    }
}


class LatchDemo implements Runnable {


    private final CountDownLatch mLatch;


    public LatchDemo(CountDownLatch latch) {
        mLatch = latch;
    }


    @Override
    public void run() {
        for (int i = 0; i < 50000; i++) {
            if (i % 2 == 0) {
                System.out.println(i);
            }
        }


        synchronized (mLatch) { // 由于闭锁对象被多个线程引用,所以此处加个同步锁
            mLatch.countDown(); // 子线程执行完,闭锁值-1
        }
    }
}

呼び出し可能なインターフェイス

前書き

Runnableインターフェースの実装とThreadクラスの継承に加えて、Callableインターフェースの実装はスレッドを実装する3番目の方法です.Runnableインターフェースの実装と比較して、Callableインターフェースはタイプを指定する必要があり、メソッドには戻り値と例外があります、FutureTaskのサポートが必要です

使用する

最初にCallableインターフェイスを実装し、ジェネリックをStringとして指定し、長さ10のランダムな文字シーケンスを生成します

class CallableDemo implements Callable<String> {
    private static final String sCHARACTERS = "abcdefghijklmnopqrstuvmxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    private final Random mRandom = new Random();


    @Override
    public String call() throws Exception {
        StringBuilder builder = new StringBuilder();


        for (int i = 0; i < 10; i++) {
            int index = Math.abs(mRandom.nextInt()) % sCHARACTERS.length();
            builder.append(sCHARACTERS.charAt(index));
        }
        return builder.toString();
    }
}

次に、FutureTaskを使用してタスクを実行し、結果を取得します

public class Main {
    public static void main(String[] args) throws InterruptedException {
        try {
            CallableDemo demo = new CallableDemo();
            FutureTask<String> task = new FutureTask<>(demo); // 实例化FutureTask,泛型和Callable的泛型一致
            new Thread(task).start();
            System.out.println(task.get()); // get()方法会阻塞,直到结果返回。因此FutureTask也可以用于闭锁
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

また、FutureTaskを使用して、スレッドの実行時間をカウントするためのブロッキングを実現することも比較的簡単です。

try {
    CallableDemo demo = new CallableDemo();
    long start = System.currentTimeMillis();

    for (int i = 0; i < 10; i++) {
        FutureTask<String> task = new FutureTask<>(demo);
        new Thread(task).start();
        System.out.println(task.get());
    }

    System.out.println("耗费时间:" + (System.currentTimeMillis() - start));
} catch (ExecutionException e) {
    e.printStackTrace();
}

ロック同期ロック

前書き

同期コードブロックと同期メソッドに加えて、ロックはマルチスレッドセーフの問題を解決する3番目の方法です。同期メソッドと同期コードブロックよりも柔軟性があります。lock()を介してロックする必要があります。メソッドを実行し、unlock()メソッドを介して解放します。

使用する

コードに直接移動します。unlock()メソッドはtry-catchのfinallyブロックに配置する必要があります

public class Main {
    public static void main(String[] args) throws InterruptedException {
        try {
            Ticket ticket = new Ticket();
            for (int i = 0; i < 3; i++) {
                new Thread(ticket).start();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class Ticket implements Runnable {
    private int mTick = 100;
    private Lock mLock = new ReentrantLock();


    @Override
    public void run() {
        while (mTick > 0) {
            mLock.lock();
            mTick--;
            System.out.println(Thread.currentThread().getName() + ":" + mTick);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                mLock.unlock();
            }
        }
    }
}

揮発性キーワード

1はじめに

揮発性は、メモリの可視性の問題を解決するために使用されます。メモリの可視性の問題は、複数のスレッドが共有データを操作するときに、それらが独自のスレッドのTLABにアクセスするため、相互の操作が見えないという問題を指します。TLABについて、関連セクションのヒープJVMスタディノートの記事を参照してください。

メモリの可視性の問題を解決するには、同期、ロック、および揮発性の方法を使用できます

2.使用する

次のコードでは、メインスレッドのdemo.isFlag()の値はfalseです。TLABが存在するため、メインスレッドと子スレッドのフラグ値を同期的に変更することはできません。

public class Main {
    public static void main(String[] args) throws InterruptedException {
        try {
            ThreadDemo demo = new ThreadDemo();
            new Thread(demo).start();


            while (true) {
                if (demo.isFlag()) {
                    System.out.println("........");
                    break;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}


class ThreadDemo implements Runnable {
    private boolean mFlag = false;


    @Override
    public void run() {
        try {
            Thread.sleep(200);
        } catch (Exception e) {


        }


        mFlag = true;
    }


    public boolean isFlag() {
        return mFlag;
    }


    public void setFlag(boolean flag) {
        mFlag = flag;
    }
}

ただし、volatileキーワードの追加は異なります。Volitaleはフラグのメモリの可視性を変更します。子スレッドがフラグを変更すると、メモリに直接フラッシュされます。メインスレッドはメモリからフラグを直接読み取り、その値はフラグがTrueに変更されました

public class Main {
    public static void main(String[] args) throws InterruptedException {
        try {
            ThreadDemo demo = new ThreadDemo();
            new Thread(demo).start();

            while (true) {
                if (demo.isFlag()) {
                    System.out.println("........");
                    break;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}




class ThreadDemo implements Runnable {
    private volatile boolean mFlag = false;

    @Override
    public void run() {
        try {
            Thread.sleep(200);
        } catch (Exception e) {

        }


        mFlag = true;
    }

    public boolean isFlag() {
        return mFlag;
    }

    public void setFlag(boolean flag) {
        mFlag = flag;
    }
}

しかし、その後の問題は、揮発性がパフォーマンスの低下をもたらすことですが、それでも同期よりも高いですが、揮発性は相互に排他的ではなく、変数のアトミック性を保証できません。変数の操作をTLABからメインメモリに変更するだけです。

同時コンテナクラス

同時コンテナクラスについて学ぶための例としてConcurrentHashMapを取り上げます

前書き

HashMapスレッドは安全ではなく、HashTableスレッドは安全ですが、効率は低くなります。

ConcurrentHashMapは、ロックセグメンテーションメカニズムを使用し、concurrentLevelを使用してロックセグメンテーションを測定します。デフォルト値は16です。各セグメントは、独立したロックであるハッシュマップを維持するため、複数のスレッドが異なるロックセグメントにアクセスする場合、スレッドセーフであるだけでなく効率的でもあります。

ただし、jdk1.8以降、内部のConcurrentHashMapもCASアルゴリズムに置き換えられました。

その他のコンテナクラス

同期TreeMapはConcurrentSkipListMapに置き換えることができます。読み取りとトラバーサルの数が更新の数よりもはるかに多い場合は、CopyOnWriteArrayListを使用して同期されたArrayListを置き換えることができます。

同期コンテナクラスの使用法は通常のコンテナクラスと同じです。使用方法と使用方法はここではスキップしますが、マルチスレッドの記事を参照して、シーケンスの蓄積を実現します。並行チェーンキュー

誤ったウェイクアップ

False wakeupは、wait()メソッドがifステートメントでラップされている場合、次のコードのように、ウェイクアップ時にエラーが発生することを意味します。

class Clerk {
    private int mNum = 0;


    public synchronized void add() throws InterruptedException {
        if (mNum >= 1) {
            System.out.println(Thread.currentThread().getName() + ": " + "mNum > 1..");
            wait();
        }


        mNum += 1;
        System.out.println(Thread.currentThread().getName() + ": " + mNum);
        notifyAll();
    }


    public synchronized void sub() throws InterruptedException {
        if (mNum <= 0) {
            System.out.println(Thread.currentThread().getName() + ": " + "mNum < 0..");
            wait();
        }


        mNum -= 1;
        System.out.println(Thread.currentThread().getName() + ": " + mNum);
        notifyAll();
    }
}


class Producer implements Runnable {
    private Clerk mClerk;


    public Producer(Clerk mClerk) {
        this.mClerk = mClerk;
    }


    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            try {
                mClerk.add();
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}


class Consumer implements Runnable {
    private Clerk mClerk;


    public Consumer(Clerk mClerk) {
        this.mClerk = mClerk;
    }


    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            try {
                mClerk.sub();
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

2つのプロデューサーと2つのコンシューマーを実行すると、mCountが負であることがわかります。これは、プロデューサーが店員に通知した後、複数のコンシューマーがmCount-1操作を実行したためです。

誤ったウェイクアップの問題を解決するには、ループ内でwait()メソッドを呼び出す必要があります

class Clerk {
    private int mNum = 0;


    public synchronized void add() throws InterruptedException {
        while (mNum >= 1) {
            System.out.println(Thread.currentThread().getName() + ": " + "mNum > 1..");
            wait();
        }


        mNum += 1;
        System.out.println(Thread.currentThread().getName() + ": " + mNum);
        notifyAll();
    }


    public synchronized void sub() throws InterruptedException {
        while (mNum <= 0) {
            System.out.println(Thread.currentThread().getName() + ": " + "mNum < 0..");
            wait();
        }


        mNum -= 1;
        System.out.println(Thread.currentThread().getName() + ": " + mNum);
        notifyAll();
    }
}


class Producer implements Runnable {
    private Clerk mClerk;


    public Producer(Clerk mClerk) {
        this.mClerk = mClerk;
    }


    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            try {
                mClerk.add();
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}


class Consumer implements Runnable {
    private Clerk mClerk;


    public Consumer(Clerk mClerk) {
        this.mClerk = mClerk;
    }


    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            try {
                mClerk.sub();
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

状態の目覚めと待機

同期ロックはプロデューサーコンシューマーを実装することもでき、ロックオブジェクトのConditionオブジェクトにも同様の待機メソッドとウェイクアップメソッドがあります。

class Clerk {
    private int mNum = 0;
    private Lock mLock = new ReentrantLock();
    private Condition mCondition = mLock.newCondition();


    public void add() throws InterruptedException {
        mLock.lock();


        try {
            while (mNum >= 1) {
                System.out.println(Thread.currentThread().getName() + ": " + "mNum > 1..");
                mCondition.await(50, TimeUnit.MILLISECONDS);
            }


            mNum += 1;
            System.out.println(Thread.currentThread().getName() + ": " + mNum);
            mCondition.signalAll();
        } finally {
            mLock.unlock();
        }
    }


    public void sub() throws InterruptedException {
        mLock.lock();


        try {
            while (mNum <= 0) {
                System.out.println(Thread.currentThread().getName() + ": " + "mNum < 0..");
                mCondition.await(50, TimeUnit.MILLISECONDS);
            }


            mNum -= 1;
            System.out.println(Thread.currentThread().getName() + ": " + mNum);
            mCondition.signalAll();
        } finally {
            mLock.unlock();
        }


    }
}


class Producer implements Runnable {
    private Clerk mClerk;


    public Producer(Clerk mClerk) {
        this.mClerk = mClerk;
    }


    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            try {
                mClerk.add();
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}


class Consumer implements Runnable {
    private Clerk mClerk;


    public Consumer(Clerk mClerk) {
        this.mClerk = mClerk;
    }


    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            try {
                mClerk.sub();
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

同数のプロデューサーとコンシューマーを開始して検証します

条件を使用して、ABCの交互印刷など、整然とした交互のスレッドを実現します

public class Main {
    public static void main(String[] args) throws InterruptedException {
        try {
            AlterDemo demo = new AlterDemo();
            new Thread(() -> {
                for (int i = 0; i < 20; i++) {
                    demo.printA();
                }
            }).start();

            new Thread(() -> {
                for (int i = 0; i < 20; i++) {
                    demo.printB();
                }
            }).start();

            new Thread(() -> {
                for (int i = 0; i < 20; i++) {
                    demo.printC();
                }
            }).start();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class AlterDemo {
    private int mNum = 1;
    private Lock mLock = new ReentrantLock();
    private Condition mCondition1 = mLock.newCondition();
    private Condition mCondition2 = mLock.newCondition();
    private Condition mCondition3 = mLock.newCondition();

    public void printA() {
        mLock.lock();

        try {
            while (mNum != 1) {
                mCondition1.await();
            }

            System.out.println("A");

            mNum = 2;
            mCondition2.signal();
        } catch (Exception e) {

        } finally {
            mLock.unlock();
        }
    }

    public void printB() {
        mLock.lock();

        try {
            while (mNum != 2) {
                mCondition2.await();
            }

            System.out.println("B");

            mNum = 3;
            mCondition3.signal();
        } catch (Exception e) {

        } finally {
            mLock.unlock();
        }
    }

    public void printC() {
        mLock.lock();

        try {
            while (mNum != 3) {
                mCondition3.await();
            }

            System.out.println("C");

            mNum = 1;
            mCondition1.signal();
        } catch (Exception e) {

        } finally {
            mLock.unlock();
        }
    }
}

読み取り/書き込みロック

前書き

読み取り/書き込みロックは、書き込み/書き込みの相互排除を保証するためのものです

使用法

readLock()メソッドとwriteLock()メソッドを使用して、読み取り/書き込みロックオブジェクトの読み取りロックと書き込みロックを取得します。

class WriteReadDemo {
    private int mNum = 0;
    private ReadWriteLock mLock = new ReentrantReadWriteLock();
    
    public void set(int num) {
        mLock.writeLock().lock();
        
        try {
            mNum = num;
        } finally {
            mLock.writeLock().unlock();
        }
    }
    
    public void get() {
        mLock.readLock().lock();
        
        try {
            System.out.println(Thread.currentThread().getName() + ": " + mNum);
        } finally {
            mLock.readLock().unlock();
        }
    }
}

次に、スレッドの検証を開始します

WriteReadDemo demo = new WriteReadDemo();


for (int i = 0; i < 50; i++) {
    new Thread(() -> demo.set((int) (Math.random() * 100)), "Write").start();
}


for (int i = 0; i < 100; i++) {
    new Thread(demo::get, "Read" + i).start();
}

スレッドプール

前書き

スレッドの作成、維持、および破棄のオーバーヘッドを削減するために、直接新しいのではなく、スレッドプールを使用してスケジューリングスレッドを作成できます。

使用法

スレッドプールは、エグゼキュータの関連する新しいメソッドを介して作成できます

newFixedThreadPool()は、固定サイズのスレッドプールを作成します

newCachedThreadPool()はキャッシュスレッドプールを作成し、容量は必要に応じて調整できます

newSingleThreadExecutor()はシングルスレッドプールを作成します

newScheduledThreadPool()は、固定サイズのスレッドプールを作成し、タスクを定期的に実行します

ExecutorService pool = Executors.newFixedThreadPool(5);

作成後、スレッドプールのsubmit()メソッドを使用してタスクを実行できます

for (int i = 0; i < 10; i++) {
    pool.submit(() -> {
        int sum = 0;
        for (int j = 0; j < 51; j++) {
            sum += j;
        }
        System.out.println(Thread.currentThread().getName() + ": " + sum);
    });
}

最後に、スレッドプールを閉じます

pool.shutdown();

shutdown()は、実行中のすべての子スレッドが終了するのを待ってからシャットダウンし、shutdownNow()は、まだ終了していない子スレッドをすぐにシャットダウンして終了します。

ScheduledExecutorServiceスケジューリングタスクの場合、schedule()メソッドを呼び出し、実行可能または呼び出し可能なオブジェクトを渡し、遅延および遅延ユニットを渡す必要があります

ScheduledExecutorService pool = Executors.newScheduledThreadPool(5);

for (int i = 0; i < 10; i++) {
    pool.schedule(() -> {
        int sum = 0;
        for (int j = 0; j < 51; j++) {
            sum += j;
        }
        System.out.println(Thread.currentThread().getName() + ": " + sum);
    }, (long) (Math.random() * 1000), TimeUnit.MILLISECONDS);
}


pool.shutdown();

スレッドプールのソースコードを読むことに興味がある場合は、Androidの開発と学習のためのスレッドプールのソースコードの簡単な分析の記事を参照してください。

ForkJoinPoolブランチ/マージフレームワーク

前書き

フォーク/結合フレームワーク:必要に応じて、大きなタスク(Fork)を分割できなくなるまでいくつかの小さなタスクに分割します。次に、小さなタスク(Join)の結果を大きなタスクの結果にマージします。

これはMapReduceフレームワークに似ていますが、MapReduceの分割プロセスが避けられない点が異なります。

 

仕事を盗むモード:

サブタスクスレッドがそのタスクキュー内のタスクを完了し、大きなタスクが終了していない場合、実行する他のサブタスクスレッドのキューの最後からランダムにタスクを盗みます

使用する

まず、タスククラスを定義し、RecursiveTask親クラスを継承し、ジェネリック型を指定して、compute()メソッドを実装します。

class FJSumCalculate extends RecursiveTask<Long> {
    private long mStart, mEnd;
    private static final long sTHRESHOLD = 100L; // 拆分临界值


    public FJSumCalculate(long start, long end) {
        mStart = start;
        mEnd = end;
    }


    @Override
    protected Long compute() {
        long length = mEnd - mStart;
        if (length <= sTHRESHOLD) {
            long sum = 0L;


            for (long i = mStart; i <= mEnd; i++) {
                sum += i;
            }


            return sum;
        }

        // 拆分
        long middle = (mEnd + mStart) / 2;
        FJSumCalculate left = new FJSumCalculate(mStart, middle - 1);
        left.fork();


        FJSumCalculate right = new FJSumCalculate(middle, mEnd);
        right.fork();

        // 合并
        return left.join() + right.join();
    }

小さすぎない重要な値の選択に注意してください

次に、mainメソッドでテストし、ForkJoinPoolを使用してタスクを実行します


ForkJoinPool pool = new ForkJoinPool();

System.out.println(pool.invoke(new FJSumCalculate(1L, 99999L)));

pool.shutdown();

代わりに

LongStreamをjava8で使用して、同様の並列累積ロジックを実行することもできます。

LongStream.rangeClosed(1L, 99999L).parallel().reduce(Long::sum).getAsLong();

結論

これでJUCスレッドスタディノートは終わりです。皆さんを歓迎します。

おすすめ

転載: blog.csdn.net/qq_37475168/article/details/107060372