Javaマルチスレッド(3):スレッドセーフの問題と解決策

一緒に書く習慣を作りましょう!「ナゲッツデイリーニュープラン・4月アップデートチャレンジ」に参加して8日目です。クリックしてイベントの詳細をご覧ください

1.スレッドセーフとは

マルチスレッド環境で実行されたコードの結果が私たちの期待に沿っている場合、つまり、結果がシングルスレッド環境である必要がある場合、プログラムはスレッドセーフ        であると言われますスレッドセーフではないということは、複数のスレッドでのプログラムの実行結果が期待を満たさないことを意味します。

2.スレッドの不安定さを引き起こす要因

2.1プリエンプティブ実行

 

2.2複数のスレッドが同じ変数を同時に変更する

public class ThreadDemo16 {
    static class Counter {
        // 变量
        private int number = 0;
        // 循环次数
        private int count;

        public Counter(int count) {
            this.count = count;
        }

        // ++方法
        public int increment() {
            int tmp = 0;
            for (int i = 0; i < count; i++) {
                tmp++;
            }
            return tmp;
        }

        // --方法
        public int decrement() {
            int tmp = 0;
            for (int i = 0; i < count; i++) {
                tmp--;
            }
            return tmp;
        }

        public int getNumber() {
            return number;
        }
    }

    static int num1 = 0;
    static int num2 = 0;

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter(100000);

        Thread thread1 = new Thread(() -> num1 = counter.increment());

        Thread thread2 = new Thread(() -> num2 = counter.decrement());

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println("最终结果:" + (num1 + num2));
    }
}
复制代码

演算結果:

。 

 

2.3非不可分操作

 アトミック性とは何ですか?

        コードの一部を部屋と見なし、各スレッドは部屋に入りたい人です。Aが部屋に入った後、それが出てこないことを保証するメカニズムがない場合、Bも部屋に入り、部屋のAのプライバシーを妨害することができます。これはアトミックではありません。

 Javaステートメントは必ずしもアトミックである必要はなく、必ずしも単なる命令でもありません。

****たとえば、今見たn++は実際には次の3つのステップで構成されています。

  1. メモリからCPUにデータを読み取ります
  2. データを更新する
  3. CPUにデータを書き戻す

アトミック性がマルチスレッドにもたらすことを保証しない問題は何ですか? 

        スレッドが変数を操作していて、途中で他のスレッドが介入している場合、操作が中断されると、結果が間違っている可能性があります。これは、スレッドのプリエンプティブスケジューリングとも密接に関連しています。スレッドが「プリエンプティブ」でない場合、アトミック性がなくても、大きな問題にはなりません。 

2.4メモリの可視性の問題

        可視性とは、スレッドによる共有変数値への変更を他のスレッドがタイムリーに確認できることを意味します。

public class ThreadDemo17 {
    private static boolean flag = true;

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            System.out.println("线程1:开始执行" + LocalDateTime.now());
            while (flag) {
            }
            System.out.println("线程1:结束执行" + LocalDateTime.now());
        });
        thread1.start();

        Thread thread2 = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程2:修改flag = false" + LocalDateTime.now());
            flag = false;
        });
        thread2.start();

    }
}
复制代码

 演算結果:

スレッド2がフラグをfalseに変更し、スレッド1が実行を終了しないことがわかります。これはメモリの可視性の問題です。

 

2.5命令の並べ替え

命令の並べ替えとは何ですか?

たとえば、次のようなコードの一部です。

  1. フロントデスクに行き、Uディスクを取り外します
  2. 教室に行き、10分間の宿題を書きます
  3. フロントデスクに行き、宅配便を受け取ります

        如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题,可以少跑一次前台。这种叫做指令重排序 。 编译器优化的本质是调整代码的执行顺序,在单线程下没问题,但在多线程下容易出现混乱,从而造成线程安全问题。 

那么有这么多线程不安全问题,该如何解决呢?

 

 

3. 解决线程不安全问题

3.1 volatile 解决内存可见性和指令重排序问题

        volatile 可以解决内存可见性指令重排序的问题,代码在写入 volatile 修饰的变量的时候: 

  • 改变线程⼯作内存中volatile变量副本的值;
  • 将改变后的副本的值从⼯作内存刷新到主内存。

代码在读取 volatile 修饰的变量的时候:

  • 从主内存中读取volatile变量的最新值到线程的⼯作内存中;
  • 从⼯作内存中读取volatile变量的副本。

注意 :直接访问工作内存(实际是 CPU 的寄存器或者 CPU 的缓存), 速度非常快, 但是可能出现数据不⼀致的情况,加上 volatile ,强制读写内存,速度虽然慢了,但是数据变得更准确了。

volatile 演示:

public class ThreadDemo17 {
    private volatile static boolean flag = true;

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            System.out.println("线程1:开始执行" + LocalDateTime.now());
            while (flag) {
            }
            System.out.println("线程1:结束执行" + LocalDateTime.now());
        });
        thread1.start();

        Thread thread2 = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程2:修改flag = false" + LocalDateTime.now());
            flag = false;
        });
        thread2.start();

    }
}
复制代码

运行结果:

 给之前的代码加上 volatile 之后,线程1接收到了flag的改变,从而结束了执行,解决了内存可见性问题。

volatile 缺点

        volatile 虽然可以解决内存可见性和指令重排序的问题,但是解决不了原子性问题,因此对于 ++ 和 --操作的线程非安全问题依然解决不了,比如以下代码:

public class ThreadDemoVolatile {
    static class Counter {
        // 变量
        private volatile int number = 0;

        // 循环次数
        private final int MAX_COUNT;

        public Counter(int MAX_COUNT) {
            this.MAX_COUNT = MAX_COUNT;
        }

        // ++ 方法
        public void increase() {
            for (int i = 0; i < MAX_COUNT; i++) {
                number++;
            }
        }

        // -- 方法
        public void desc() {
            for (int i = 0; i < MAX_COUNT; i++) {
                number--;
            }
        }

        public int getNumber() {
            return number;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter(100000);
        Thread thread1 = new Thread(counter::increase);
        thread1.start();
        Thread thread2 = new Thread(counter::desc);
        thread2.start();

        // 等待线程执行完成
        thread1.join();
        thread2.join();
        System.out.println("最终结果:" + counter.getNumber());
    }
}
复制代码

 

3.2 锁(synchronized 和 lock)

3.2.1 synchronized

synchronized 基本用法:

  1. 修饰静态方法

    public class ThreadSynchronized {
    
        private static int number = 0;
    
        static class Counter {
            // 循环次数
            private static final int count = 100000;
    
            // ++方法
            public synchronized static void increase() {
                for (int i = 0; i < count; i++) {
                    number++;
                }
            }
    
            // --方法
            public synchronized static void desc() {
                for (int i = 0; i < count; i++) {
                    number--;
                }
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            Thread thread1 = new Thread(Counter::increase);
            thread1.start();
            Thread thread2 = new Thread(Counter::desc);
            thread2.start();
    
            // 等待线程执行完毕
            thread1.join();
            thread2.join();
            System.out.println("执行结果:" + number);
        }
    }
    复制代码

    ​​

     

  2. 修饰普通⽅法

    public class ThreadSynchronized2 {
        private static int number = 0;
    
        static class Counter {
            private static final int count = 100000;
    
            // ++方法
            public synchronized void increase() {
                for (int i = 0; i < count; i++) {
                    number++;
                }
            }
    
            // --方法
            public synchronized void desc() {
                for (int i = 0; i < count; i++) {
                    number--;
                }
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            Counter counter = new Counter();
            Thread thread1 = new Thread(counter::increase);
            thread1.start();
            Thread thread2 = new Thread(counter::desc);
            thread2.start();
    
            // 等待线程执行完毕
            thread1.join();
            thread2.join();
            System.out.println("最终结果:" + number);
        }
    }
    复制代码

    ​​

     

  3. 修饰代码块

    public class ThreadSynchronized3 {
        private static int number = 0;
    
        static class Counter {
            private static final int count = 100000;
    
            // 自定义锁对象
            final Object myLock = new Object();
    
            // ++方法
            public void increase() {
                for (int i = 0; i < count; i++) {
                    synchronized (myLock) {
                        number++;
                    }
                }
            }
    
            // --方法
            public void desc() {
                for (int i = 0; i < count; i++) {
                    synchronized (myLock) {
                        number--;
                    }
                }
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            Counter counter = new Counter();
            Thread thread1 = new Thread(counter::increase);
            thread1.start();
            Thread thread2 = new Thread(counter::desc);
            thread2.start();
    
            thread1.join();
            thread2.join();
            System.out.println("最终结果:" + number);
        }
    }
    复制代码

    ​​

synchronized 特性:

1. 互斥。synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同⼀个对象 synchronized 就会 阻塞等待。

  • 进入 synchronized 修饰的代码块, 相当于 加锁,
  • 退出 synchronized 修饰的代码块, 相当于 解锁。

2. 刷新内存。 synchronized 的⼯作过程: 

  1. 获得互斥锁
  2. 从主内存拷贝变量的最新副本到⼯作的内存
  3. 执行代码
  4. 将更改后的共享变量的值刷新到主内存
  5. 释放互斥锁

所以 synchronized 也能保证内存可见性.

​3. 可重入。 synchronized 同步块对同⼀条线程来说是可重入的,不会出现自己把自己锁死的问题。

public class ThreadSynchronized4 {
    public static void main(String[] args) {
        synchronized (ThreadSynchronized4.class) {
            System.out.println("主线程得到锁");
            synchronized (ThreadSynchronized4.class) {
                System.out.println("主线程再次得到锁");
            }
        }
    }
}
复制代码

 

注意 :

  1.  加同一把锁。

    public class ThreadSynchronized6 {
        private static final int count = 100000;
        static int num = 0;
    
        public static void main(String[] args) throws InterruptedException {
            Object obj = new Object();
            Object obj2 = new Object();
            Thread t1 = new Thread(() -> {
                synchronized (obj) {
                    for (int i = 0; i < count; i++) {
                        num++;
                    }
                }
            }, "线程1");
            t1.start();
            Thread t2 = new Thread(() -> {
                synchronized (obj2) {
                    for (int i = 0; i < count; i++) {
                        num--;
                    }
                }
            }, "线程2");
            t2.start();
            t1.join();
            t2.join();
            System.out.println("最终执⾏结果:" + num);
        }
    }
    复制代码

  2. 实例类可以使用 this,静态类使用 xxx.class。

  3. synchronized用的锁是存在Java对象头的:


  4. 上⼀个线程解锁之后, 下⼀个线程并不是立即就能获取到锁. 而是要靠操作系统来 "唤醒". 这也就是操作系统线程调度的⼀部分工作.

  5. 假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不⼀定就能获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则.

3.3.2 ****Lock

ロックの基本的な使用法:

public class ThreadLock {
    public static void main(String[] args) {
        // 1.创建锁对象
        Lock lock = new ReentrantLock();
        // 2.加锁
        lock.lock();
        try {
            // 业务代码
            System.out.println("hello");
        } finally {
            lock.unlock();
        }
    }
}
复制代码

ロックノート:

  • lock()は、tryの前、またはtryの最初の行に配置する必要があります。そうしないと、2つの問題が発生し
    ます。1。tryに配置すると、tryコードの例外が原因でロックが失敗し、最終的にロックを解除する操作も行います。
    2.ロック解除例外は、試行中のビジネス例外をカバーするため、エラーのトラブルシューティングがより困難になります。
  • 最後にunlock()を内部に配置する必要があります。

フェアロックとアンフェアロックをロックします。

  • 公正なロック:複数のスレッドは、ロックを適用する順序でロックを取得します。スレッドはキューに直接入り、キューの最初の場所で常にロックを取得できます。
  • 不公平なロック:複数のスレッドがロックを取得すると、直接取得しようとします。取得できない場合は待機キューに入り、取得できる場合は直接取得します。

ソースコードから、Lockはデフォルトで不公平なロックを作成することがわかります。trueを渡すと、公平なロックが作成されます。

同期VSロック:

  • ロックはより柔軟で、より多くのメソッドがあります。
  • ロックの種類は異なります。ロックはデフォルトでは不公平なロックですが、公平なロックとして指定することもできます。同期は不公平なロックのみになります。
  • lock()メソッドの呼び出しは、ロック状態を待機している同期スレッドとは異なり、lock()メソッドはWAITTINGになり、synchronizedはBLOCKEDになります。
  • Synchronizedは、JVMによって提供されるロックであり、自動的にロックしてロックを解放しますが、Lockでは、開発者が自分でロックをロックして解放する必要があります。
  • Synchronizedはメソッド(静的メソッド/通常のメソッド)とコードブロックを変更できますが、Lockはコードのみを変更できます。

おすすめ

転載: juejin.im/post/7088642481381703688
おすすめ