>  기사  >  Java  >  Java에서 다중 스레드 프로그래밍을 구현하는 방법

Java에서 다중 스레드 프로그래밍을 구현하는 방법

PHPz
PHPz앞으로
2023-05-01 18:22:071397검색

1. 생성자에서 스레드 시작

이 문제는 다음과 유사하게 생성자에서 시작됩니다.

public class A{     public A(){        this.x=1;        this.y=2;        this.thread=new MyThread();        this.thread.start();     }       }

이로 인해 발생하는 문제는 무엇인가요? 클래스 A를 상속하는 클래스 B가 있는 경우 Java 클래스 초기화 순서에 따라 B의 생성자가 호출되기 전에 반드시 A의 생성자가 호출되고, 스레드 스레드도 B가 완전히 초기화되기 전에 시작됩니다. running 클래스 A에서 일부 변수를 사용하면 B의 생성자에서 이러한 변수에 새로운 값을 할당할 수 있기 때문에 예상한 값을 사용하지 못할 수 있습니다. 즉, 이때 이 변수를 사용하는 쓰레드가 2개가 되는데, 이 변수는 동기화되지 않습니다.

이 문제를 해결하는 방법에는 두 가지가 있습니다. A를 상속 불가능으로 설정하거나 생성자에 배치하는 대신 스레드를 시작하는 별도의 시작 메서드를 제공하는 것입니다.

2. 불완전한 동기화

우리 모두는 변수를 동기화하는 효과적인 방법이 클래스 메소드인지 인스턴스인지에 따라 동기화가 객체 잠금일 수도 있고 클래스 잠금일 수도 있다는 것을 알고 있습니다. 방법. 그러나 방법 A에서 변수를 동기화하는 경우 약한 가시성을 허용하거나 오류 값을 생성하지 않는 한 변수가 나타나는 다른 곳에서도 변수를 동기화해야 합니다. 다음과 유사한 코드:

class A{    int x;    public int getX(){       return x;    }    public synchronized void setX(int x)    {       this.x=x;    }  }

x의 setter 메소드는 동기화되지만 getter 메소드는 동기화되지 않으므로 getX를 통해 다른 스레드에서 얻은 x가 절대값이라는 보장이 없습니다. 실제로 여기서 setX의 동기화는 필요하지 않습니다. 왜냐하면 int 쓰기는 JVM 사양에서 보장하는 원자성이며, int가 아니라 double 또는 long인 경우 다중 동기화는 의미가 없기 때문입니다. 그런 다음 getX와 setX를 모두 동기화해야 합니다. 왜냐하면 double과 long은 모두 64비트이고 쓰기와 읽기는 두 개의 32비트로 나누어지기 때문입니다(이는 jvm 구현에 따라 다릅니다. 일부 jvm 구현은 long과 long Double의 읽기를 보장할 수 있습니다. 및 쓰기는 원자성임) 원자성은 보장되지 않습니다. 위와 같은 코드는 실제로 변수를 휘발성으로 선언하면 해결될 수 있습니다.

3. 객체를 잠금으로 사용하는 경우 객체의 참조가 변경되어 동기화가 실패합니다.

이 역시 다음 코드와 유사한 매우 일반적인 오류입니다.

synchronized(array[0])  {     ......     array[0]=new A();     ......  }

동기화 블록은 배열[0]을 잠금으로 사용하지만, 배열[0]이 가리키는 참조가 동기화 블록에서 변경되었습니다. 이 시나리오를 분석하면 첫 번째 스레드는 배열[0]의 잠금을 획득하고, 두 번째 스레드는 배열[0]을 획득할 수 없기 때문에 기다리고, 배열[0]의 참조를 변경한 후 세 번째 스레드는 새 배열의 잠금을 획득합니다. [0], 첫 번째와 세 번째 스레드가 보유한 잠금이 다르며 동기화 및 상호 배제의 목적이 전혀 달성되지 않았습니다. 이러한 코드 수정에는 일반적으로 잠금을 최종 변수로 선언하거나 참조가 동기화된 블록 내에서 수정되지 않도록 보장하기 위해 비즈니스 독립적인 잠금 개체를 도입하는 작업이 포함됩니다.

4. Wait()는 루프에서 호출되지 않습니다.

wait 및 알림은 조건 변수를 구현하는 데 사용됩니다. 조건 변경이 원자적이고 가시적으로 이루어지도록 동기화된 블록에서 호출해야 한다는 것을 알 수 있습니다. 동기화는 되었지만 루프에서 wait를 호출하지 않고 if 또는 조건 판단을 사용하지 않는 코드를 자주 봅니다.

synchronized(lock)  {     if(isEmpty()       lock.wait();       }

조건 판단은 if를 사용하는 것입니다. 이것이 어떤 문제를 일으킬까요? 조건을 판단하기 전에 inform 또는 informAll을 호출하면 조건이 충족되고 대기 시간이 발생하지 않습니다. 조건이 충족되지 않으면 wait() 메서드가 호출되어 잠금을 해제하고 대기 절전 상태로 들어갑니다. 정상적인 상황, 즉 조건이 변경된 후 스레드가 깨어나면 문제가 없으며 조건이 만족되면 다음 논리 연산이 계속 실행됩니다. 문제는 스레드가 실수로 깨어날 수도 있고 심지어 악의적으로 깨어날 수도 있다는 점입니다. 조건이 다시 판단되지 않기 때문에 조건이 충족되지 않으면 스레드가 후속 작업을 수행합니다. informAll을 호출하여 예기치 않은 깨우기가 발생할 수도 있고, 누군가 악의적으로 깨울 수도 있고, 드물게 자동 깨우기("의사 깨우기"라고 함)일 수도 있습니다. 따라서, 조건이 만족되지 않을 경우 후속 작업이 수행되는 것을 방지하기 위해, 조건이 만족되지 않으면 계속해서 대기 상태로 진입한 후, 각성된 후 다시 조건을 판단하는 것이 필요하다. 조건이 충족되면.

synchronized(lock)  {     while(isEmpty()       lock.wait();       }

조건 판단 없이 wait를 호출하는 상황은 더욱 심각합니다. 왜냐하면 wait 전에 알림이 호출되었을 수 있으므로 wait를 호출하고 대기 절전 상태에 들어간 후 스레드가 깨어날 것이라는 보장이 없기 때문입니다.

5. 동기화 범위가 너무 작거나 너무 큽니다.

동기화 범위가 너무 작으면 동기화 목적이 전혀 달성되지 않을 수 있고, 동기화 범위가 너무 크면 성능에 영향을 줄 수 있습니다. 너무 작은 동기화 범위의 일반적인 예는 두 개의 동기화된 메서드가 함께 호출될 때 동기화된다는 잘못된 믿음입니다. 기억해야 할 것은 Atomic+Atomic!=Atomic입니다.

Map map=Collections.synchronizedMap(new HashMap());  if(!map.containsKey("a")){           map.put("a", value);  }

这是一个很典型的错误,map是线程安全的,containskey和put方法也是线程安全的,然而两个线程安全的方法被组合调用就不一定是线程安全的了。因为在containsKey和put之间,可能有其他线程抢先put进了a,那么就可能覆盖了其他线程设置的值,导致值的丢失。解决这一问题的方法就是扩大同步范围,因为对象锁是可重入的,因此在线程安全方法之上再同步相同的锁对象不会有问题。

Map map = Collections.synchronizedMap(new HashMap());  synchronized (map) {       if (!map.containsKey("a")) {           map.put("a", value);       }   }

注意,加大锁的范围,也要保证使用的是同一个锁,不然很可能造成死锁。 Collections.synchronizedMap(new HashMap())使用的锁是map本身,因此没有问题。当然,上面的情况现在更推荐使用ConcurrentHashMap,它有putIfAbsent方法来达到同样的目的并且满足线程安全性。

同步范围过大的例子也很多,比如在同步块中new大对象,或者调用费时的IO操作(操作数据库,webservice等)。不得不调用费时操作的时候,一定要指定超时时间,例如通过URLConnection去invoke某个URL时就要设置connect timeout和read timeout,防止锁被独占不释放。同步范围过大的情况下,要在保证线程安全的前提下,将不必要同步的操作从同步块中移出。

6、正确使用volatile

在jdk5修正了volatile的语义后,volatile作为一种轻量级的同步策略就得到了大量的使用。volatile的严格定义参考jvm spec,这里只从volatile能做什么,和不能用来做什么出发做个探讨。

volatile可以用来做什么?

1)状态标志,模拟控制机制。常见用途如控制线程是否停止:

private volatile boolean stopped;  public void close(){     stopped=true;  }   public void run(){      while(!stopped){        //do something     }       }

前提是do something中不会有阻塞调用之类。volatile保证stopped变量的可见性,run方法中读取stopped变量总是main memory中的***值。

2)安全发布,如修复DLC问题。

private volatile IoBufferAllocator instance;  public IoBufferAllocator getInsntace(){      if(instance==null){          synchronized (IoBufferAllocator.class) {              if(instance==null)                  instance=new IoBufferAllocator();          }      }      return instance;  }

3)开销较低的读写锁

public class CheesyCounter {      private volatile int value;       public int getValue() { return value; }       public synchronized int increment() {          return value++;      }  }

synchronized保证更新的原子性,volatile保证线程间的可见性。

volatile不能用于做什么?

1)不能用于做计数器

public class CheesyCounter {      private volatile int value;       public int getValue() { return value; }       public int increment() {          return value++;      }  }

因为value++其实是有三个操作组成的:读取、修改、写入,volatile不能保证这个序列是原子的。对value的修改操作依赖于value的***值。解决这个问题的方法可以将increment方法同步,或者使用AtomicInteger原子类。

2)与其他变量构成不变式

一个典型的例子是定义一个数据范围,需要保证约束lower< upper。

public class NumberRange {      private volatile int lower, upper;       public int getLower() { return lower; }      public int getUpper() { return upper; }       public void setLower(int value) {           if (value > upper)               throw new IllegalArgumentException();          lower = value;      }       public void setUpper(int value) {           if (value < lower)               throw new IllegalArgumentException();          upper = value;      }  }

尽管讲lower和upper声明为volatile,但是setLower和setUpper并不是线程安全方法。假设初始状态为(0,5),同时调用setLower(4)和setUpper(3),两个线程交叉进行,***结果可能是(4,3),违反了约束条件。修改这个问题的办法就是将setLower和setUpper同步:

public class NumberRange {      private volatile int lower, upper;       public int getLower() { return lower; }      public int getUpper() { return upper; }       public synchronized void setLower(int value) {           if (value > upper)               throw new IllegalArgumentException();          lower = value;      }       public synchronized void setUpper(int value) {           if (value < lower)               throw new IllegalArgumentException();          upper = value;      }  }

위 내용은 Java에서 다중 스레드 프로그래밍을 구현하는 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 yisu.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제