1. 동기화의 기본 사용법
동기화는 Java에서 동시성 문제를 해결하기 위해 가장 일반적으로 사용되는 방법 중 하나이며 가장 간단한 방법이기도 합니다. 동기화에는 세 가지 주요 기능이 있습니다. (1) 스레드가 동기화 코드에 액세스할 때 상호 배타적인지 확인합니다. (2) 공유 변수에 대한 수정 사항을 적시에 확인할 수 있도록 합니다. (3) 재정렬 문제를 효과적으로 해결합니다. 문법적으로 말하면, 동기화에는 총 세 가지 용도가 있습니다.
(1) 일반 메소드 수정
(2) 정적 메소드 수정
(3) 코드 블록 수정
다음으로 이 세 가지 사용 방법을 여러 예제 프로그램을 통해 설명하겠습니다(비교의 편의를 위해 세 가지 코드는 동기화의 사용 방법이 다른 점을 제외하면 기본적으로 동일합니다).
1. 동기화하지 않음:
코드 세그먼트 1:
package com.paddx.test.concurrent; public class SynchronizedTest { public void method1(){ System.out.println("Method 1 start"); try { System.out.println("Method 1 execute"); Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Method 1 end"); } public void method2(){ System.out.println("Method 2 start"); try { System.out.println("Method 2 execute"); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Method 2 end"); } public static void main(String[] args) { final SynchronizedTest test = new SynchronizedTest(); new Thread(new Runnable() { @Override public void run() { test.method1(); } }).start(); new Thread(new Runnable() { @Override public void run() { test.method2(); } }).start(); } }
실행 결과는 다음과 같습니다. 스레드 1과 스레드 2가 동시에 실행 상태에 들어갑니다. , 스레드 2 실행 속도 스레드 1보다 빠르므로 스레드 2가 먼저 실행됩니다. 이 프로세스에서는 스레드 1과 스레드 2가 동시에 실행됩니다.
방법 1 시작
방법 1 실행
방법 2 시작
방법 2 실행
방법 2 끝
방법 1 끝
2. 공통 메소드 동기화:
코드 세그먼트 2:
package com.paddx.test.concurrent; public class SynchronizedTest { public synchronized void method1(){ System.out.println("Method 1 start"); try { System.out.println("Method 1 execute"); Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Method 1 end"); } public synchronized void method2(){ System.out.println("Method 2 start"); try { System.out.println("Method 2 execute"); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Method 2 end"); } public static void main(String[] args) { final SynchronizedTest test = new SynchronizedTest(); new Thread(new Runnable() { @Override public void run() { test.method1(); } }).start(); new Thread(new Runnable() { @Override public void run() { test.method2(); } }).start(); } }
실행 결과는 코드 세그먼트 1과 비교하면 스레드 2가 대기해야 함을 분명히 알 수 있습니다. 스레드 1의 메서드 1 실행. 완료된 후에만 method2 메서드가 실행될 수 있습니다.
방법 1 시작
방법 1 실행
방법 1 종료
방법 2 시작
방법 2 실행
방법 2 종료
3. 정적 메서드(클래스) 동기화
코드 세그먼트 3:
package com.paddx.test.concurrent; public class SynchronizedTest { public static synchronized void method1(){ System.out.println("Method 1 start"); try { System.out.println("Method 1 execute"); Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Method 1 end"); } public static synchronized void method2(){ System.out.println("Method 2 start"); try { System.out.println("Method 2 execute"); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Method 2 end"); } public static void main(String[] args) { final SynchronizedTest test = new SynchronizedTest(); final SynchronizedTest test2 = new SynchronizedTest(); new Thread(new Runnable() { @Override public void run() { test.method1(); } }).start(); new Thread(new Runnable() { @Override public void run() { test2.method2(); } }).start(); } }
실행 결과는 다음과 같습니다. 정적 메서드의 동기화는 본질적으로 클래스의 동기화입니다(정적 메서드는 본질적으로 클래스 메서드에 속합니다. 객체의 메서드가 아님) 따라서 test와 test2가 서로 다른 객체에 속하더라도 둘 다 동기식 테스트 클래스의 인스턴스에 속하므로 method1과 method2는 동시에 실행될 수 없고 순차적으로만 실행될 수 있습니다.
방법 1 시작
방법 1 실행
방법 1 종료
방법 2 시작
방법 2 실행
방법 2 종료
4. 코드 블록 동기화
코드 세그먼트 4:
package com.paddx.test.concurrent; public class SynchronizedTest { public void method1(){ System.out.println("Method 1 start"); try { synchronized (this) { System.out.println("Method 1 execute"); Thread.sleep(3000); } } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Method 1 end"); } public void method2(){ System.out.println("Method 2 start"); try { synchronized (this) { System.out.println("Method 2 execute"); Thread.sleep(1000); } } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Method 2 end"); } public static void main(String[] args) { final SynchronizedTest test = new SynchronizedTest(); new Thread(new Runnable() { @Override public void run() { test.method1(); } }).start(); new Thread(new Runnable() { @Override public void run() { test.method2(); } }).start(); } }
실행 결과는 다음과 같습니다. 스레드 1과 스레드 2가 해당 메소드에 진입하여 실행을 시작했지만 스레드 2는 동기화에 들어갔습니다. 블록, 스레드 1의 동기화 블록 실행이 완료될 때까지 기다려야 합니다.
방법 1 시작
방법 1 실행
방법 2 시작
방법 1 종료
방법 2 실행
방법 2 종료
2. 동기화 원리
위의 실행 결과에 대해 여전히 의문이 있는 경우 걱정하지 마십시오. 먼저 동기화의 원리를 이해하면 위의 문제가 한눈에 명확해집니다. 먼저 다음 코드를 디컴파일하여 동기화가 코드 블록을 동기화하는 방법을 살펴보겠습니다.
package com.paddx.test.concurrent; public class SynchronizedDemo { public void method() { synchronized (this) { System.out.println("Method 1 start"); } } }
디컴파일 결과:
이 두 명령어의 기능에 대해 우리는 JVM 사양의 설명을 직접 참조하세요:
monitorenter:
Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows: • If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor. • If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count. • If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.
이 단락의 요약 의미:
각 개체에는 모니터 잠금 장치(모니터)가 있습니다. 모니터가 점유되면 스레드가 monitorenter 명령을 실행할 때 프로세스는 다음과 같습니다.
1. 모니터가 0이면 스레드가 모니터에 들어갔다가 다시 들어갑니다. 숫자가 1로 설정되면 스레드가 모니터의 소유자가 됩니다.
2. 이미 모니터를 점유하고 있는 스레드만 재진입하는 경우 모니터에 진입하는 횟수가 1 증가합니다.
3. 다른 스레드가 이미 모니터를 점유하고 있는 경우 스레드는 모니터의 항목 번호가 0이 될 때까지 차단 상태에 들어간 다음 다시 모니터의 소유권을 얻으려고 시도합니다.
monitorexit:
The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref. The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.
이 구절의 일반적인 의미는 다음과 같습니다.
monitorexit를 실행하는 스레드는 다음과 같아야 합니다. objectref 해당 모니터의 소유자입니다.
명령어가 실행되면 모니터의 항목 번호가 1씩 감소합니다. 1이 감소한 후 항목 번호가 0이면 스레드는 모니터를 종료하고 더 이상 모니터의 소유자가 아닙니다. 이 모니터에 의해 차단된 다른 스레드는 이 모니터의 소유권을 얻으려고 시도할 수 있습니다.
이 두 단락의 설명을 통해 동기화의 구현 원리를 명확하게 볼 수 있습니다. 실제로 대기/알림 및 기타 메소드도 모니터 개체를 통해 완성됩니다. 이것이 wait/notify와 같은 메소드가 동기화된 블록이나 메소드에서만 호출될 수 있는 이유입니다. 그렇지 않으면 java.lang.IllegalMonitorStateException 예외가 발생합니다.
동기화 방식의 디컴파일 결과를 살펴보겠습니다.
소스코드:
package com.paddx.test.concurrent; public class SynchronizedMethod { public synchronized void method() { System.out.println("Hello World!"); } }
从反编译的结果来看,方法的同步并没有通过指令monitorenter和monitorexit来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。
三、运行结果解释
有了对Synchronized原理的认识,再来看上面的程序就可以迎刃而解了。
1、代码段2结果:
虽然method1和method2是不同的方法,但是这两个方法都进行了同步,并且是通过同一个对象去调用的,所以调用之前都需要先去竞争同一个对象上的锁(monitor),也就只能互斥的获取到锁,因此,method1和method2只能顺序的执行。
2、代码段3结果:
虽然test和test2属于不同对象,但是test和test2属于同一个类的不同实例,由于method1和method2都属于静态同步方法,所以调用的时候需要获取同一个类上monitor(每个类只对应一个class对象),所以也只能顺序的执行。
3、代码段4结果:
对于代码块的同步实质上需要获取Synchronized关键字后面括号中对象的monitor,由于这段代码中括号的内容都是this,而method1和method2又是通过同一的对象去调用的,所以进入同步块之前需要去竞争同一个对象上的锁,因此只能顺序执行同步块。
四 总结
Synchronized是Java并发编程中最常用的用于保证线程安全的方式,其使用相对也比较简单。但是如果能够深入了解其原理,对监视器锁等底层知识有所了解,一方面可以帮助我们正确的使用Synchronized关键字,另一方面也能够帮助我们更好的理解并发编程机制,有助我们在不同的情况下选择更优的并发策略来完成任务。对平时遇到的各种并发问题,也能够从容的应对。
更多Java 并发编程学习笔记之Synchronized简介相关文章请关注PHP中文网!