@IT·互联网

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

2023-12-09  本文已影响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

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

二、解决方案

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

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)

三、常见线程安全类

Hashtable table = new Hashtable();
new Thread(()->{
    table.put("key", "value1");
}).start();
new Thread(()->{
    table.put("key", "value2");
}).start();
上一篇下一篇

猜你喜欢

热点阅读