Home  >  Article  >  Java  >  How to implement multi-threaded programming in Java

How to implement multi-threaded programming in Java

PHPz
PHPzforward
2023-05-01 18:22:071397browse

1. Start a thread in the constructor

I have seen this problem in many codes. Start a thread in the constructor, similar to this:

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

What problems will this cause? If there is a class B that inherits class A, according to the order of Java class initialization, A's constructor will definitely be called before B's constructor is called, then the thread thread will also be started before B is fully initialized. When the thread is running If you use some variables in class A, you may not use the values ​​you expected, because you may assign new values ​​to these variables in the constructor of B. In other words, there will be two threads using these variables at this time, but these variables are not synchronized.

There are two ways to solve this problem: set A as final and not inheritable; or provide a separate start method to start the thread instead of placing it in the constructor.

2. Incomplete synchronization

We all know that the effective way to synchronize a variable is to protect it with synchronized. Synchronized may be an object lock or a class lock. , it depends on whether you are a class method or an instance method. However, when you synchronize a variable in method A, you also need to synchronize it elsewhere where the variable appears, unless you allow weak visibility or even produce error values. Code similar to this:

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

The setter method of x is synchronized, but the getter method is not, so there is no guarantee that x obtained by other threads through getX is the *** value. In fact, the synchronization of setX here is not necessary, because the writing of int is atomic, which the JVM specification has guaranteed, and multiple synchronizations have no meaning; of course, if this is not an int, but a double or long, Then both getX and setX will need to be synchronized, because double and long are both 64-bit, and writing and reading are divided into two 32-bits (this depends on the jvm implementation. Some jvm implementations may guarantee long and long Double's read and write are atomic), and atomicity is not guaranteed. Code like the above can actually be solved by declaring the variable as volatile.

3. When using an object as a lock, the reference of the object is changed, causing synchronization failure.

This is also a very common mistake, similar to the following code:

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

The synchronized block uses array[0] as the lock, but array[0] is changed in the synchronized block. The reference pointed to. Analyzing this scenario, the first thread acquires the lock of array[0], the second thread waits because it cannot acquire array[0], and after changing the reference of array[0], the third thread acquires The locks of the new array[0], the locks held by the first and third threads are different, and the purpose of synchronization and mutual exclusion has not been achieved at all. Such code modifications usually involve declaring the lock as a final variable or introducing a business-independent lock object to ensure that the reference will not be modified within the synchronized block.

4. Wait() is not called in the loop.

wait and notify are used to implement condition variables. You may know that wait and notify need to be called in a synchronized block to ensure that changes in conditions are atomic and visible. I often see a lot of code that is synchronized, but does not call wait in the loop. Instead, it uses if or even no conditional judgment:

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

The conditional judgment is to use if. What problems will this cause? You may call notify or notifyAll before judging the condition. Then the condition has been met and there will be no waiting. This is no problem. When the conditions are not met, the wait() method is called to release the lock and enter the waiting sleep state. If the thread is awakened under normal circumstances, that is, after the conditions are changed, then there is no problem and the following logical operations continue to be executed if the conditions are met. The problem is that the thread may be accidentally or even maliciously awakened. Since the condition is not judged again, the thread performs subsequent operations when the condition is not met. Unexpected wake-up may be caused by calling notifyAll, someone may wake up maliciously, or it may be an automatic wake-up in rare cases (called "pseudo wake-up"). Therefore, in order to prevent subsequent operations from being performed if the conditions are not met, it is necessary to judge the conditions again after being awakened. If the conditions are not met, continue to enter the waiting state, and then proceed with subsequent operations if the conditions are met.

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

The situation of calling wait without conditional judgment is more serious, because notify may have been called before waiting, so after calling wait and entering the waiting sleep state, there is no guarantee that the thread will wake up.

5. The synchronization range is too small or too large.

If the scope of synchronization is too small, the purpose of synchronization may not be achieved at all; if the scope of synchronization is too large, performance may be affected. A common example of a synchronization scope that is too small is the mistaken belief that two synchronized methods will be synchronized when called together. What needs to be remembered is 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;      }  }

The above is the detailed content of How to implement multi-threaded programming in Java. For more information, please follow other related articles on the PHP Chinese website!

Statement:
This article is reproduced at:yisu.com. If there is any infringement, please contact admin@php.cn delete