Rumah  >  Artikel  >  Java  >  Bagaimana untuk melaksanakan pengaturcaraan berbilang benang di Java

Bagaimana untuk melaksanakan pengaturcaraan berbilang benang di Java

PHPz
PHPzke hadapan
2023-05-01 18:22:071397semak imbas

1. Mulakan utas dalam pembina

Saya telah melihat masalah ini dalam banyak kod, sama seperti ini:

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

Apakah masalah ini akan menyebabkan? Jika terdapat kelas B yang mewarisi kelas A, mengikut susunan permulaan kelas Java, pembina A pasti akan dipanggil sebelum pembina B dipanggil, maka utas benang juga akan dimulakan sebelum B dimulakan sepenuhnya berjalan Jika anda menggunakan beberapa pembolehubah dalam kelas A, anda mungkin tidak menggunakan nilai yang anda jangkakan, kerana anda mungkin menetapkan nilai baharu kepada pembolehubah ini dalam pembina B. Dalam erti kata lain, akan ada dua utas yang menggunakan pembolehubah ini pada masa ini, tetapi pembolehubah ini tidak disegerakkan.

Terdapat dua cara untuk menyelesaikan masalah ini: tetapkan A sebagai muktamad dan tidak boleh diwarisi; atau sediakan kaedah permulaan yang berasingan untuk memulakan utas dan bukannya meletakkannya dalam pembina.

2. Penyegerakan tidak lengkap

Kita semua tahu bahawa cara yang berkesan untuk menyegerakkan pembolehubah adalah melindunginya dengan disegerakkan mungkin kunci objek atau kunci kelas . , ia bergantung pada sama ada anda adalah kaedah kelas atau kaedah contoh. Walau bagaimanapun, apabila anda menyegerakkan pembolehubah dalam kaedah A, anda juga perlu menyegerakkannya di tempat lain di mana pembolehubah itu muncul, melainkan anda membenarkan keterlihatan yang lemah atau bahkan menghasilkan nilai ralat. Kod yang serupa dengan ini:

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

Kaedah penetap x disegerakkan, tetapi kaedah pengambil tidak, jadi tiada jaminan bahawa x diperolehi oleh utas lain melalui getX ialah nilai ***. Sebenarnya, penyegerakan setX di sini tidak perlu, kerana penulisan int adalah atom, yang telah dijamin oleh spesifikasi JVM, dan penyegerakan berbilang tidak bermakna, jika ini bukan int, tetapi double atau panjang, Kemudian kedua-dua getX dan setX perlu disegerakkan, kerana double dan long adalah kedua-duanya 64-bit, dan menulis dan membaca dibahagikan kepada dua 32-bit (ini bergantung pada pelaksanaan jvm. Sesetengah pelaksanaan jvm mungkin menjamin bacaan Double yang panjang dan panjang dan menulis adalah atom), dan atomicity tidak dijamin. Kod seperti di atas sebenarnya boleh diselesaikan dengan mengisytiharkan pembolehubah sebagai tidak menentu.

3. Apabila menggunakan objek sebagai kunci, rujukan objek ditukar, menyebabkan kegagalan penyegerakan.

Ini juga ralat yang sangat biasa, serupa dengan kod berikut:

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

Blok disegerakkan menggunakan tatasusunan[0] sebagai kunci, tetapi tatasusunan[0] ditukar dalam blok yang disegerakkan Rujukan menunjuk kepada. Menganalisis senario ini, utas pertama memperoleh kunci tatasusunan[0], utas kedua menunggu kerana ia tidak dapat memperoleh tatasusunan[0], dan selepas menukar rujukan tatasusunan[0], utas ketiga memperoleh Kunci tatasusunan baharu [0], kunci yang dipegang oleh utas pertama dan ketiga adalah berbeza, dan tujuan penyegerakan dan pengecualian bersama tidak tercapai sama sekali. Pengubahsuaian kod sedemikian biasanya melibatkan pengisytiharan kunci sebagai pembolehubah akhir atau memperkenalkan objek kunci bebas perniagaan untuk memastikan rujukan tidak akan diubah suai dalam blok disegerakkan.

4. Tunggu() tidak dipanggil dalam gelung.

tunggu dan beritahu digunakan untuk melaksanakan pembolehubah keadaan Anda mungkin tahu bahawa tunggu dan maklumkan perlu dipanggil dalam blok yang disegerakkan untuk memastikan perubahan dalam keadaan adalah atom dan boleh dilihat. Saya sering melihat banyak kod yang disegerakkan, tetapi tidak memanggil tunggu dalam gelung, tetapi menggunakan jika atau bahkan tiada penghakiman bersyarat:

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

Pertimbangan bersyarat adalah untuk menggunakan jika masalah ini akan menyebabkan? Anda boleh menghubungi memberitahu atau memberitahuSemua sebelum menilai syarat tersebut. Apabila syarat tidak dipenuhi, kaedah tunggu() dipanggil untuk melepaskan kunci dan memasuki keadaan tidur menunggu. Jika benang dibangkitkan dalam keadaan biasa, iaitu selepas syarat diubah, maka tidak ada masalah dan operasi logik berikut terus dilaksanakan jika syarat dipenuhi. Masalahnya ialah benang itu mungkin terjaga secara tidak sengaja atau malah berniat jahat Memandangkan keadaan tidak dinilai lagi, benang melakukan operasi seterusnya apabila syarat itu tidak dipenuhi. Bangun yang tidak dijangka mungkin disebabkan oleh panggilan notifyAll, seseorang mungkin bangun dengan niat jahat, atau ia mungkin bangun automatik dalam kes yang jarang berlaku (dipanggil "pseudo wake-up"). Oleh itu, untuk mengelakkan operasi berikutnya daripada dilakukan jika syarat tidak dipenuhi, adalah perlu untuk menilai syarat sekali lagi selepas dikejutkan Jika syarat tidak dipenuhi, teruskan memasuki keadaan menunggu, dan kemudian meneruskan operasi berikutnya jika syarat dipenuhi.

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

Situasi memanggil tunggu tanpa penghakiman bersyarat adalah lebih serius, kerana pemberitahuan mungkin telah dipanggil sebelum menunggu, jadi selepas memanggil tunggu dan memasuki keadaan tidur menunggu, tidak ada jaminan bahawa benang akan bangun.

5. Julat penyegerakan terlalu kecil atau terlalu besar.

Jika skop penyegerakan terlalu kecil, tujuan penyegerakan mungkin tidak tercapai sama sekali jika skop penyegerakan terlalu besar, prestasi mungkin terjejas. Contoh biasa skop penyegerakan yang terlalu kecil ialah kepercayaan yang salah bahawa dua kaedah yang disegerakkan akan disegerakkan apabila dipanggil bersama-sama Apa yang perlu diingat ialah 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;      }  }

Atas ialah kandungan terperinci Bagaimana untuk melaksanakan pengaturcaraan berbilang benang di Java. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Kenyataan:
Artikel ini dikembalikan pada:yisu.com. Jika ada pelanggaran, sila hubungi admin@php.cn Padam