美文网首页@IT·互联网
共享带来的问题及解决方案

共享带来的问题及解决方案

作者: 我可能是个假开发 | 来源:发表于2023-12-09 09:36 被阅读0次

    一、共享带来的问题

    @Slf4j
    public class Test2 {
        static int count = 0;
        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(() -> {
                for (int i = 0; i < 5000; i++) {
                    count++;
                }
            }, "t1");
    
            Thread t2 = new Thread(() -> {
                for (int i = 0; i < 5000; i++) {
                    count--;
                }
            }, "t2");
    
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            log.debug("{}", count);
        }
    }
    
    16:33:49.787 [main] DEBUG juc.thread.Test2 - 101
    

    以上的结果可能是正数、负数、零。因为 Java 中对静态变量的自增,自减并不是原子操作。
    i++(i 为静态变量),产生的 JVM 字节码指令:

    {
      static int count;
        descriptor: I
        flags: ACC_STATIC
    
      public juc.thread.Test2();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 6: 0
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       5     0  this   Ljuc/thread/Test2;
    
      public static void main(java.lang.String[]) throws java.lang.InterruptedException;
        descriptor: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=4, locals=3, args_size=1
             0: new           #2                  // class java/lang/Thread
             3: dup
             4: invokedynamic #3,  0              // InvokeDynamic #0:run:()Ljava/lang/Runnable;
             9: ldc           #4                  // String t1
            11: invokespecial #5                  // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;Ljava/lang/String;)V
            14: astore_1
            15: new           #2                  // class java/lang/Thread
            18: dup
            19: invokedynamic #6,  0              // InvokeDynamic #1:run:()Ljava/lang/Runnable;
            24: ldc           #7                  // String t2
            26: invokespecial #5                  // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;Ljava/lang/String;)V
            29: astore_2
            30: aload_1
            31: invokevirtual #8                  // Method java/lang/Thread.start:()V
            34: aload_2
            35: invokevirtual #8                  // Method java/lang/Thread.start:()V
            38: aload_1
            39: invokevirtual #9                  // Method java/lang/Thread.join:()V
            42: aload_2
            43: invokevirtual #9                  // Method java/lang/Thread.join:()V
            46: getstatic     #10                 // Field log:Lorg/slf4j/Logger;
            49: ldc           #11                 // String {}
            51: getstatic     #12                 // Field count:I
            54: invokestatic  #13                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
            57: invokeinterface #14,  3           // InterfaceMethod org/slf4j/Logger.debug:(Ljava/lang/String;Ljava/lang/Object;)V
            62: return
          LineNumberTable:
            line 12: 0
            line 18: 15
            line 24: 30
            line 25: 34
            line 26: 38
            line 27: 42
            line 28: 46
            line 29: 62
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0      63     0  args   [Ljava/lang/String;
               15      48     1    t1   Ljava/lang/Thread;
               30      33     2    t2   Ljava/lang/Thread;
        Exceptions:
          throws java.lang.InterruptedException
        MethodParameters:
          Name                           Flags
          args
    
      static {};
        descriptor: ()V
        flags: ACC_STATIC
        Code:
          stack=1, locals=0, args_size=0
             0: ldc           #15                 // class juc/thread/Test2
             2: invokestatic  #16                 // Method org/slf4j/LoggerFactory.getLogger:(Ljava/lang/Class;)Lorg/slf4j/Logger;
             5: putstatic     #10                 // Field log:Lorg/slf4j/Logger;
             8: iconst_0
             9: putstatic     #12                 // Field count:I
            12: return
          LineNumberTable:
            line 5: 0
            line 8: 8
    }
    

    Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换


    image.png

    临界区 Critical Section

    一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

    • 一个程序运行多个线程本身是没有问题的
    • 问题出在多个线程访问共享资源
      • 多个线程读共享资源其实也没有问题
      • 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
    static int counter = 0;
    static void increment()
    // 临界区
    {
        counter++;
    }
    static void decrement()
    // 临界区
    {
        counter--;
    }
    

    竞态条件 Race Condition

    多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

    二、解决方案

    为了避免临界区的竞态条件发生,可以有以下方案

    • 阻塞式:synchronized,Lock
    • 非阻塞式:原子变量

    1.synchronized

    @Slf4j
    public class Test2 {
    
        static int count = 0;
    
        static Object lock = new Object();
    
        public static void main(String[] args) throws InterruptedException {
    
            Thread t1 = new Thread(() -> {
                for (int i = 0; i < 5000; i++) {
                    synchronized (lock){
                        count++;
                    }
                }
            }, "t1");
    
            Thread t2 = new Thread(() -> {
                for (int i = 0; i < 5000; i++) {
                    synchronized (lock){
                        count--;
                    }
                }
            }, "t2");
    
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            log.debug("{}", count);
        }
    }
    
    19:51:50.506 [main] DEBUG juc.thread.Test2 - 0
    

    优化:

    @Slf4j
    public class Test17 {
        public static void main(String[] args) throws InterruptedException {
            Room room = new Room();
            Thread t1 = new Thread(() -> {
                for (int i = 0; i < 5000; i++) {
                    room.increment();
                }
            }, "t1");
    
            Thread t2 = new Thread(() -> {
                for (int i = 0; i < 5000; i++) {
                    room.decrement();
                }
            }, "t2");
    
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            log.debug("{}", room.getCounter());
        }
    }
    
    class Room {
        private int counter = 0;
    
        //方法上的相当于锁住了当前对象this
        public synchronized void increment() {
            counter++;
        }
    
        public synchronized void decrement() {
            counter--;
        }
    
        //为了保证读取到的是正确的结果,而不是中间状态的结果,所以也要加锁
        public synchronized int getCounter() {
            return counter;
        }
    }
    

    方法上的 synchronized,锁住当前对象this

    class Test{
      public synchronized void test() {
      }
    }
    //等价于
    class Test{
      public void test() {
        synchronized(this) {
        }
      }
    }
    

    静态方法上的synchronized,锁住当前类对象

    class Test{
      public synchronized static void test() {
      }
    }
    等价于
    class Test{
      public static void test() {
        synchronized(Test.class) {
        }
      }
    }
    
    class Number{
      public static synchronized void a() {
        sleep(1);
        log.debug("1");
      }
      public static synchronized void b() {
        log.debug("2");
      }
    }
    public static void main(String[] args) {
       Number n1 = new Number();
       Number n2 = new Number();
       new Thread(()->{ n1.a(); }).start();
       new Thread(()->{ n2.b(); }).start();
    }
    

    因为静态方法锁的是类对象,所以n1和n2是一个类对象,能互斥。
    结果:1s 后12, 或 2 1s后 1

    public class ThreadUnsafeTest {
    
        public static void main(String[] args) {
            ThreadUnsafe test = new ThreadUnsafe();
            for (int i = 0; i < 2; i++) {
                new Thread(() -> test.method1(100), "Thread" + i).start();
            }
        }
    }
    
    class ThreadUnsafe{
        ArrayList<String> list = new ArrayList<>();
        public void method1(int loopNumber) {
            for (int i = 0; i < loopNumber; i++) {
                method2();
                method3();
            }
        }
        private void method2() {
            list.add("1");
        }
        private void method3() {
            list.remove(0);
        }
    }
    

    可能存在线程安全问题:

    Exception in thread "Thread1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
        at java.util.ArrayList.rangeCheck(ArrayList.java:659)
        at java.util.ArrayList.remove(ArrayList.java:498)
        at juc.thread.ThreadUnsafe.method3(ThreadUnsafeTest.java:37)
        at juc.thread.ThreadUnsafe.method1(ThreadUnsafeTest.java:28)
        at juc.thread.ThreadUnsafeTest.lambda$main$0(ThreadUnsafeTest.java:18)
        at java.lang.Thread.run(Thread.java:748)
    

    由于add操作不是原子的,所以存在两个线程同时去add时,最后的size被后来的add覆盖,导致两次add操作,size仍然是1,这时,两个线程再同时remove,size只有1,就会出现下标越界。

    变成局部变量,则不会出现线程安全问题:

    public class ThreadSafeTest {
    
        public static void main(String[] args) {
            ThreadSafe test = new ThreadSafe();
            for (int i = 0; i < 2; i++) {
                new Thread(() -> test.method1(400), "Thread" + i).start();
            }
        }
    }
    
    class ThreadSafe{
    
        public void method1(int loopNumber) {
            ArrayList<String> list = new ArrayList<>();
            for (int i = 0; i < loopNumber; i++) {
                method2(list);
                method3(list);
            }
        }
    
        private void method2(ArrayList<String> list) {
            list.add("1");
        }
    
        private void method3(ArrayList<String> list) {
            list.remove(0);
        }
    }
    

    每个线程调用时会创建其不同实例,没有共享。

    如果通过继承的方式,把变量共享出去了,则可能存在线程安全问题

    public class ThreadExtendUnsafeTest {
    
        public static void main(String[] args) {
            ThreadUnsafeSub test = new ThreadUnsafeSub();
            for (int i = 0; i < 2; i++) {
                new Thread(() -> test.method1(500), "Thread" + i).start();
            }
        }
    }
    
    class ThreadExtendUnsafe{
    
        public void method1(int loopNumber) {
            ArrayList<String> list = new ArrayList<>();
            for (int i = 0; i < loopNumber; i++) {
                method2(list);
                method3(list);
            }
        }
    
        public void method2(ArrayList<String> list) {
            list.add("1");
        }
    
        public void method3(ArrayList<String> list) {
            list.remove(0);
        }
    }
    
    class ThreadUnsafeSub extends ThreadExtendUnsafe{
    
        @Override
        public void method3(ArrayList<String> list) {
            new Thread(()->list.remove(0)).start();
        }
    }
    
    Exception in thread "Thread-999" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
        at java.util.ArrayList.rangeCheck(ArrayList.java:659)
        at java.util.ArrayList.remove(ArrayList.java:498)
        at juc.thread.ThreadUnsafeSub.lambda$method3$0(ThreadExtendUnsafeTest.java:46)
        at java.lang.Thread.run(Thread.java:748)
    

    三、常见线程安全类

    • String
    • Integer
    • StringBuffer
    • Random
    • Vector
    • Hashtable
    • java.util.concurrent 包下的类
      这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。
    Hashtable table = new Hashtable();
    new Thread(()->{
        table.put("key", "value1");
    }).start();
    new Thread(()->{
        table.put("key", "value2");
    }).start();
    
    • 每个方法是原子的,多个线程操作单个方法,可以保证原子性。
    • 但它们多个方法的组合不是原子的,需要在这多个方法外面再加锁才能保证原子性

    相关文章

      网友评论

        本文标题:共享带来的问题及解决方案

        本文链接:https://www.haomeiwen.com/subject/rxdgwdtx.html