>  기사  >  Java  >  Java의 동기화된 키워드 및 스레드 안전성 문제에 대한 분석 예

Java의 동기화된 키워드 및 스레드 안전성 문제에 대한 분석 예

高洛峰
高洛峰원래의
2017-01-05 14:47:481091검색

먼저 동기화의 기본 사용법을 검토해 보겠습니다.

동기화 코드 블록, 수정된 코드는 동기화된 명령문 블록이 되며 해당 범위는 이 코드 블록의 개체를 호출하는 것입니다. 키워드 가능하다면 코드 세그먼트의 범위를 최대한 좁히세요. 코드 세그먼트에 동기화를 추가할 수 있다면 전체 메서드에 동기화를 추가하지 마세요. 이를 잠금의 세분성을 줄이고 코드의 동시성을 높이는 것입니다.

동기화된 메서드의 경우 수정된 메서드는 동기화된 메서드가 되며, 해당 범위는 전체 메서드이고 대상 개체는 이 메서드를 호출하는 개체입니다.

동기화된 정적 메서드는 정적 메서드를 수정하며 해당 범위는 전체 정적 메서드이고 대상은 이 클래스의 모든 개체입니다.

동기화 클래스의 범위는 동기화 뒤 괄호로 묶인 동기화(className.class) 부분이며, 작동하는 개체는 모두 이 클래스의 개체입니다.

synchronized()()는 잠긴 개체입니다. 전역 잠금 기능. 이 메서드를 호출하는 이 클래스의 모든 개체는 잠금의 영향을 받습니다. 또한 특정 개체를 잠그려면 ()에 추가할 수 있습니다.

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

동기화 키워드와 스레드 안전성
동기화 키워드를 사용하여 코드를 감싸면 스레드 동기화가 안전할 것이라고 생각했습니다. 테스트했습니다. 완전히 틀린 것으로 밝혀졌습니다. 실제로 스레드로부터 안전하려면 동기화를 올바르게 사용해야 합니다. . . 이런 글을 쓰는 방법을 알면서도 게으름 때문에 잘못된 방법을 썼다는 생각이 늘 있었습니다.
아직 기반이 마련되지 않은 것 같습니다. 아직 검토하고 강화해야합니다! 직장에서 이런 실수를 하는 것은 용서할 수 없는 일입니다. 동기화 키워드를 사용하는 곳마다 데이터가 중요하다는 점을 꼭 아셔야 합니다! 땀을 많이 흘린다. . .
코드를 먼저 게시하세요.

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); 
 } 
}

게으르면 땀을 흘리세요. . . 프로그래머는 항상 게으르다. 가능한 한 적게 쓰십시오. 나는 쉽게 호출할 수 있도록 MyThread를 익명의 최종 내부 클래스로 작성했습니다. Thread의 가장 직접적인 상속을 사용하여 스레드 클래스를 구현하고 실행해야 하는 run() 메서드를 정의합니다.
먼저 print2() 메서드에 주석을 달아 print1()의 결과가 무엇인지 확인하세요. print1()은 동기화된 키워드를 사용하여 정의된 메서드입니다. 나는 이것이 스레드 안전성을 달성할 수도 있다고 항상 생각했습니다. 다들 아시다시피 제가 틀렸습니다.
main() 메서드를 직접 실행해 보겠습니다. 콘솔 인쇄 결과는 다음과 같습니다.

1212111121212121212121212121212121212121222222212121212。。。

는 1과 2를 교차 인쇄한 일련의 결과입니다. 내 주요 방법에서는 m1을 먼저 실행한 다음 m2를 실행했는데 이는 스레드 동기화가 달성되지 않았음을 나타냅니다.

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

다음으로 run 메서드에서 print1()을 주석 처리하고 print2()를 실행합니다.
콘솔은 다음과 같이 인쇄합니다.

11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111112222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222

스레드는 실제로 안전합니다. 늘 이런 글쓰기 방식을 안다고 생각했는데, 이런 글쓰기 방식에는 코드가 좀 더 많아져서 별 생각이 없었는데, 오늘에서야 이런 실수를 깨달았습니다. 때로는 게으르지 않는 것이 도움이 되는 것 같습니다. 기초를 잘 다지는 것이 중요합니다. 오랜 실수를 바로잡습니다.

구체적인 이유를 살펴보겠습니다.

동기화 키워드는 함수의 수식어로 사용될 수도 있고, 함수 내의 명령문으로 사용될 수도 있는데, 이를 보통 동기화 방법, 동기화 명령문 블록이라고 합니다. 더 자세히 분류된 경우 동기화는 인스턴스 변수, 개체 참조(객체 참조), 정적 함수 및 클래스 리터럴(클래스 이름 리터럴)에 대해 작동할 수 있습니다.
자세히 설명하기 전에 몇 가지 사항을 명확히 해야 합니다.
A. 동기화된 키워드가 메소드나 객체에 추가되었는지 여부에 관계없이, 이것이 획득하는 잠금은 코드 조각이나 함수를 잠금으로 취급하는 것이 아니라 객체입니다. 그리고 동기화된 메소드는 다른 객체에 의해 액세스될 가능성이 높습니다. 스레드.
B. 각 객체에는 연관된 잠금이 하나만 있습니다.
ㄷ. 동기화를 달성하려면 시스템 오버헤드가 많이 필요하고 교착 상태가 발생할 수도 있으므로 불필요한 동기화 제어를 피하십시오.
다음으로 코드의 여러 위치에서 동기화가 미치는 영향에 대해 논의해 보겠습니다.
P1과 P2가 동일한 클래스의 서로 다른 객체라고 가정합니다. 이 클래스는 P1, P2 상황에서 동기화 블록 또는 동기화 방법을 정의합니다. 모두 부를 수 있어요.
1. 동기화를 함수 수정자로 사용하는 경우 샘플 코드는 다음과 같습니다.

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

이것이 동기화 방법입니다. 그러면 이때 어떤 개체가 동기화됩니까? 잠그는 것은 이 동기화된 메소드를 호출하는 객체입니다. 즉, 객체 P1이 서로 다른 스레드에서 이 동기화 방법을 실행할 때 상호 배제가 형성되어 동기화 효과를 얻습니다. 그러나 이 객체가 속한 Class에 의해 생성된 또 다른 객체 P2는 동기화된 키워드를 사용하여 임의로 이 메소드를 호출할 수 있습니다.
위 샘플 코드는 다음 코드와 동일합니다.

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

(1)의 내용은 무엇을 의미하나요? P1과 같이 이 메서드를 호출하는 개체를 나타냅니다. 동기화 방법의 본질은 개체 참조에 동기화를 적용하는 것임을 알 수 있습니다. ——P1 개체 잠금을 획득한 스레드만 P1의 동기화 방법을 호출할 수 있습니다. P2에 관한 한 P1 잠금은 이와 관련이 없습니다. 프로그램은 동기화 메커니즘의 제어를 제거할 수도 있습니다. 이 상황에서는 데이터 혼란이 발생합니다!

2. 동기화된 블록의 샘플 코드는 다음과 같습니다.

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中文网!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.