Heim  >  Artikel  >  Java  >  Beispielanalyse der synchronisierten Schlüsselwort- und Thread-Sicherheitsprobleme in Java

Beispielanalyse der synchronisierten Schlüsselwort- und Thread-Sicherheitsprobleme in Java

高洛峰
高洛峰Original
2017-01-05 14:47:481091Durchsuche

Lassen Sie uns zunächst die grundlegende Verwendung von synchronisiertem Code überprüfen:

synchronisierter Codeblock, der geänderte Code wird zu einem synchronisierten Anweisungsblock und sein Umfang besteht darin, das Objekt dieses Codeblocks aufzurufen. Wir verwenden den synchronisierten Schlüsselwort Wenn möglich, schränken Sie den Umfang des Codesegments so weit wie möglich ein. Wenn Sie dem Codesegment eine Synchronisierung hinzufügen können, fügen Sie nicht der gesamten Methode eine Synchronisierung hinzu. Dies wird als Reduzierung der Granularität der Sperre und gleichzeitigere Gestaltung des Codes bezeichnet.

Synchronisierte Methode, die geänderte Methode wird zu einer synchronisierten Methode, ihr Umfang ist die gesamte Methode und das Objekt ist das Objekt, das diese Methode aufruft.

Synchronisierte statische Methode ändert eine statische Methode, ihr Geltungsbereich ist die gesamte statische Methode und ihr Ziel sind alle Objekte dieser Klasse.

Synchronisierte Klasse, ihr Geltungsbereich ist der Teil von „synchronized(className.class)“, der in Klammern nach „Synchronized“ eingeschlossen ist, und die Objekte, auf die sie einwirkt, sind alle Objekte dieser Klasse.

synchronized() () ist das gesperrte Objekt, das nur das Objekt selbst sperrt. Die synchronisierte Methode, die von verschiedenen Objekten derselben Klasse aufgerufen wird, wird nicht gesperrt Alle Objekte dieser Klasse, die diese Methode aufrufen, sind von der Sperre betroffen. Darüber hinaus kann in () ein bestimmtes Objekt hinzugefügt werden.

synchronized (object) {
 //在同步代码块中对对象进行操作 
}

synchronisiertes Schlüsselwort und Thread-Sicherheit
Ich dachte, dass die Verwendung des synchronisierten Schlüsselworts zum Umschließen des Codes die Thread-Synchronisierung sicher machen würde. Habe es getestet. Hat sich als völlig falsch herausgestellt. synchronisiert muss korrekt verwendet werden, um wirklich Thread-sicher zu sein. . . Obwohl ich diese Schreibweise kenne, dachte ich immer, dass ich aus Faulheit die falsche Methode verwendet habe.
Es scheint, dass der Grundstein noch nicht gelegt ist. Muss noch überprüft und verstärkt werden! Es ist unverzeihlich, einen solchen Fehler bei der Arbeit zu machen. Sie müssen wissen, dass die Daten überall dort, wo das synchronisierte Schlüsselwort verwendet wird, vertraulich sind! Viel schwitzen. . .
Posten Sie zuerst den Code:

package com; 
  
public class ThreadTest { 
 public static void main(String[] args) { 
  MyThread m1 = new MyThread(1); 
  MyThread m2 = new MyThread(2); 
  m1.start(); 
  m2.start(); 
 } 
} 
  
final class MyThread extends Thread { 
 private int val; 
  
 public MyThread(int v) { 
  val = v; 
 } 
 //这种做法其实是非线程安全的 
 public synchronized void print1(int v) { 
  for (int i = 0; i < 100; i++) { 
   System.out.print(v); 
  } 
 } 
  
 public void print2(int v) { 
  //线程安全 
  synchronized (MyThread.class) { 
   for (int i = 0; i < 100; i++) { 
    System.out.print(v); 
   } 
  } 
 } 
  
 public void run() { 
  print1(val); 
  // print2(val); 
 } 
}

Nur ​​um faul zu sein, schwitzen Sie. . . Programmierer sind immer faul. Schreiben Sie so wenig wie möglich. Ich habe MyThread als anonyme letzte innere Klasse zum einfachen Aufrufen geschrieben. Es verwendet den Thread mit der direktesten Vererbung, um eine Thread-Klasse zu implementieren, und definiert die auszuführende run()-Methode.
Annotieren Sie zunächst die Methode print2(), um zu sehen, was das Ergebnis von print1() ist. print1() ist eine Methode, die mit dem synchronisierten Schlüsselwort definiert wird. Ich dachte immer, dass dies auch Thread-Sicherheit erreichen könnte. Wie jeder weiß, habe ich mich geirrt.
Lassen Sie uns die main()-Methode direkt ausführen. Das Ergebnis des Konsolendrucks lautet wie folgt:

1212111121212121212121212121212121212121222222212121212。。。

ist eine Reihe kreuzweise gedruckter Ergebnisse von 1 und 2. In meiner Hauptmethode führe ich zuerst m1 und dann m2 aus, was zeigt, dass keine Thread-Synchronisierung erreicht wird!

MyThread m1 = new MyThread(1); 
MyThread m2 = new MyThread(2); 
m1.start(); 
m2.start();

Als nächstes kommentieren wir print1() in der run-Methode aus und führen print2() aus;
Die Konsole druckt wie folgt:

11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111112222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222

Der Thread ist tatsächlich sicher, Ich dachte immer, ich kenne diese Schreibweise, aber da diese Schreibweise etwas mehr Code enthält, habe ich nicht viel darüber nachgedacht, diesen Fehler erst heute bemerkt. Es scheint, dass es sich manchmal lohnt, nicht faul zu sein. Es ist wichtig, eine gute Grundlage zu legen. Einen langjährigen Fehler korrigieren.

Werfen wir einen Blick auf die konkreten Gründe.

Das synchronisierte Schlüsselwort kann als Modifikator einer Funktion oder als Anweisung innerhalb einer Funktion verwendet werden, was üblicherweise als Synchronisationsmethode und Synchronisationsanweisungsblock bezeichnet wird. Bei weiterer Klassifizierung kann synchronisiert auf Instanzvariablen, Objektreferenzen (Objektreferenzen), statische Funktionen und Klassenliterale (Klassennamenliterale) einwirken.
Bevor wir weiter darauf eingehen, müssen wir einige Punkte klären:
A. Unabhängig davon, ob das synchronisierte Schlüsselwort einer Methode oder einem Objekt hinzugefügt wird, handelt es sich bei der Sperre, die es erhält, um ein Objekt, anstatt einen Codeabschnitt oder eine Funktion als Sperre zu behandeln – und es ist wahrscheinlich, dass Objekte von anderen auf die synchronisierte Methode zugreifen Threads.
B. Jedem Objekt ist nur eine Sperre zugeordnet.
C. Das Erreichen einer Synchronisierung erfordert einen hohen Systemaufwand und kann sogar zu einem Deadlock führen. Vermeiden Sie daher unnötige Synchronisierungskontrollen.
Als Nächstes diskutieren wir die Auswirkungen der Synchronisierung an verschiedenen Stellen auf den Code:
Angenommen, P1 und P2 sind unterschiedliche Objekte derselben Klasse. Diese Klasse definiert Synchronisierungsblöcke oder Synchronisierungsmethoden in den folgenden Situationen: P1, P2 kann sie alle anrufen.
1. Wenn synchronisiert als Funktionsmodifikator verwendet wird, lautet der Beispielcode wie folgt:

Public synchronized void methodAAA() 
{ 
//…. 
}

Dies ist die Synchronisationsmethode. Welches Objekt ist zu diesem Zeitpunkt synchronisiert? Was gesperrt wird, ist das Objekt, das diese synchronisierte Methode aufruft. Das heißt, wenn ein Objekt P1 diese Synchronisationsmethode in verschiedenen Threads ausführt, wird ein gegenseitiger Ausschluss zwischen ihnen gebildet, um einen Synchronisationseffekt zu erzielen. Ein anderes Objekt P2, das von der Klasse generiert wird, zu der dieses Objekt gehört, kann diese Methode jedoch beliebig mit dem synchronisierten Schlüsselwort aufrufen.
Der obige Beispielcode entspricht dem folgenden Code:

public void methodAAA() 
{ 
synchronized (this) // (1) 
{ 
//….. 
} 
}

Worauf bezieht sich das unter (1)? Es bezieht sich auf das Objekt, das diese Methode aufruft, z. B. P1. Es ist ersichtlich, dass der Kern der Synchronisationsmethode darin besteht, die Synchronisation auf die Objektreferenz anzuwenden. ——Nur der Thread, der die P1-Objektsperre erhalten hat, kann die Synchronisationsmethode von P2 aufrufen. Die P1-Sperre hat damit nichts zu tun. Das Programm kann auch die Kontrolle über den Synchronisationsmechanismus verlieren In dieser Situation führt dies zu Datenverwirrung!

2. Synchronisierter Block, der Beispielcode lautet wie folgt:

public void method3(SomeObject so) 
{ 
synchronized(so) 
{ 
//….. 
} 
}

这时,锁就是so这个对象,谁拿到这个锁谁就可以运行它所控制的那段代码。当有一个明确的对象作为锁时,就可以这样写程序,但当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的instance变量(它得是一个对象)来充当锁:

class Foo implements Runnable 
{ 
private byte[] lock = new byte[0]; // 特殊的instance变量 
Public void methodA() 
{ 
synchronized(lock) { //… } 
} 
//….. 
}

注:零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。
3.将synchronized作用于static 函数,示例代码如下:

Class Foo 
{ 
public synchronized static void methodAAA() // 同步的static 函数 
{ 
//…. 
} 
public void methodBBB() 
{ 
synchronized(Foo.class) // class literal(类名称字面常量) 
} 
}

代码中的methodBBB()方法是把class literal作为锁的情况,它和同步的static函数产生的效果是一样的,取得的锁很特别,是当前调用这个方法的对象所属的类(Class,而不再是由这个Class产生的某个具体对象了)。
记得在《Effective Java》一书中看到过将 Foo.class和 P1.getClass()用于作同步锁还不一样,不能用P1.getClass()来达到锁这个Class的目的。P1指的是由Foo类产生的对象。
可以推断:如果一个类中定义了一个synchronized的static函数A,也定义了一个synchronized 的instance函数B,那么这个类的同一对象Obj在多线程中分别访问A和B两个方法时,不会构成同步,因为它们的锁都不一样。A方法的锁是Obj这个对象,而B的锁是Obj所属的那个Class。
小结如下:
搞清楚synchronized锁定的是哪个对象,就能帮助我们设计更安全的多线程程序。
还有一些技巧可以让我们对共享资源的同步访问更加安全:
1.定义private 的instance变量+它的 get方法,而不要定义public/protected的instance变量。如果将变量定义为public,对象在外界可以绕过同步方法的控制而直接取得它,并改动它。这也是JavaBean的标准实现方式之一。
2.如果instance变量是一个对象,如数组或ArrayList什么的,那上述方法仍然不安全,因为当外界对象通过get方法拿到这个instance对象的引用后,又将其指向另一个对象,那么这个private变量也就变了,岂不是很危险。这个时候就需要将get方法也加上synchronized同步,并且,只返回这个private对象的clone()――这样,调用端得到的就是对象副本的引用了。

总结一些synchronized注意事项:

当两个并发线程访问同一个对象中的synchronized代码块时,在同一时刻只能有一个线程得到执行,另一个线程受阻塞,必须等待当前线程执行完这个代码块以后才能执行该代码块。两个线程间是互斥的,因为在执行synchronized代码块时会锁定当前的对象,只有执行完该代码块才能释放该对象锁,下一个线程才能执行并锁定该对象。

当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。(两个线程使用的是同一个对象)

当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞(同上,两个线程使用的是同一个对象)。

更多实例解析Java中的synchronized关键字与线程安全问题相关文章请关注PHP中文网!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn