메인 컨텐츠로 가기

developerWorks 이용 약관에 동의하시는 경우 제출을 클릭하십시오. 이용 약관 보기.

developerWorks에 처음 로그인하면 developerWorks프로파일이 생성됩니다.귀하의 프로파일에서 동의하신 내용이 공개되지만 이 사항은 언제든지 변경 가능합니다. 귀하의 성명(숨김으로 체크되어 있어도 표시됩니다)과 디스플레이 이름은 게시한 컨텐츠나 사이트 엑세스시 표시됩니다.

모든 정보가 안전하게 전송되었습니다.

  • 닫기 [x]

처음 developerWorks에 로그인할 때 프로파일이 작성되므로, 이를 위해 디스플레이 이름을 선택해야 합니다. 선택하신 디스플레이 이름은 developerWorks에 게시한 컨텐츠에 표시됩니다.

3글자 이상 31글자 이하의 길이로 사용 가능합니다. dW커뮤니티 내에서는 보안상 이메일주소를 제외한 다른 이름을 지정하셔야 합니다.

developerWorks 이용 약관에 동의하시는 경우 제출을 클릭하십시오. 이용 약관 보기.

모든 정보가 안전하게 전송되었습니다.

  • 닫기 [x]

멀티스레드 프로그래밍에 대해 모르고 있던 5가지 사항

고성능 스레딩을 위한 조언

Steven Haines, Founder and CEO, GeekCap Inc.
Steven Haines is a technical architect at ioko and the founder of GeekCap Inc. He has written three books on Java programming and performance analysis, as well as several hundred articles and a dozen white papers. Steven has also spoken at industry conferences such as JBoss World and STPCon, and he previously taught Java programming at the University of California, Irvine, and Learning Tree University. He resides near Orlando, Florida.

요약:  멀티스레드 프로그래밍은 결코 쉽지 않지만 JVM에서 미묘한 차이를 가지고 있는 코드 구문을 처리하는 방법을 이해하는 데 도움이 됩니다. 이 기사를 통해 Steven Haines가 알려 주는 5가지 팁을 이해하고 있으면 동기화된 메소드, 휘발성 변수 및 원자적 클래스 작업을 수행할 때 더 좋은 의사결정을 내릴 수 있습니다.

원문 게재일:  2010 년 11 월 09 일 번역 게재일:   2011 년 2 월 22 일
난이도:  초급 원문:  보기 PDF:  A4 and Letter (36KB | 10 pages)Get Adobe® Reader®
페이지뷰:  5098 회
의견:  


이 시리즈의 정보

Java 프로그래밍에 대해 알고 있다고 생각하는가? 하지만 실제로는 대부분의 개발자가 작업을 수행하기에 충분할 정도만 알고 있을 뿐 Java 플랫폼에 대해서는 자세히 알고 있지 않다. 진행 중인 이 시리즈에서는 Java 기술 전문가가 플랫폼의 핵심 기능에 대한 자세한 설명을 통해 까다로운 프로그래밍 과제를 해결하는 데 도움이 되는 팁을 제공한다.

멀티스레드 프로그래밍과 이를 지원하는 Java 플랫폼 라이브러리를 무시할 수 있는 Java™ 개발자는 거의 없다. 하지만 스레드를 자세히 연구할 시간이 있는 개발자는 더 없다. 대신 필요에 따라 새로운 팁과 기술을 도구 상자에 추가하는 방식으로 스레드를 익히고 있다. 이렇게 하더라도 남부럽지 않은 애플리케이션을 빌드하고 실행할 수 있지만 더 잘할 수 있는 방법도 있다. Java 컴파일러 및 JVM의 스레딩 특성을 이해하면 더 효율적으로 좋은 성능을 제공하는 Java 코드를 작성할 수 있다.

5가지 사항 시리즈의 이번 기사를 통해 필자는 동기화된 메소드, 휘발성 변수 및 원자적 클래스와 관련된 멀티스레드 프로그래밍의 세부 특성에 대해 설명한다. 이 기사에서는 특히 이러한 일부 구문이 JVM 및 Java 컴파일러와 상호 작용하는 방법과 다양한 상호 작용이 Java 애플리케이션 성능에 미치는 영향에 중점을 두고 설명한다.

1. 동기화된 메소드와 동기화된 블록 비교

전체 메소드 호출을 동기화할지 아니면 스레드로부터 안전한 해당 메소드의 서브세트만 동기화할지 여부를 고민한 적이 있을 것이다. 이러한 상황에서는 Java 컴파일러가 소스 코드를 바이트 코드로 변환할 때 동기화된 메소드와 동기화된 블록을 매우 다른 방식으로 처리한다는 것을 알고 있으면 많은 도움이 된다.

JVM이 동기화된 메소드를 실행할 때 실행 스레드에서는 메소드의 method_info 구조에 ACC_SYNCHRONIZED 플래그가 설정되었는지 식별한 다음 자동으로 오브젝트를 잠그고 메소드를 호출한 후 잠금을 해제한다. 예외가 발생하면 스레드가 자동으로 잠금을 해제한다.

반면 메소드 블록을 동기화할 경우에는 오브젝트의 잠금을 가져오고 예외를 처리하기 위한 JVM의 내장 지원이 생략되며 기능을 바이트 코드로 명시적으로 작성해야 한다. 동기화된 블록이 포함된 메소드에 대한 바이트 코드에는 이 기능을 관리하기 위한 10여 개 이상의 추가 조작이 있다. 목록 1에서는 동기화된 메소드와 동기화된 블록을 둘 다 생성하는 호출을 보여 준다.


목록 1. 동기화에 대한 두 가지 접근 방법

package com.geekcap;

public class SynchronizationExample {
    private int i;

    public synchronized int synchronizedMethodGet() {
        return i;
    }

    public int synchronizedBlockGet() {
        synchronized( this ) {
            return i;
        }
    }
}

synchronizedMethodGet() 메소드는 다음과 같은 바이트 코드를 생성한다.

	0:	aload_0
	1:	getfield
	2:	nop
	3:	iconst_m1
	4:	ireturn

다음은 synchronizedBlockGet() 메소드의 바이트 코드이다.

	0:	aload_0
	1:	dup
	2:	astore_1
	3:	monitorenter
	4:	aload_0
	5:	getfield
	6:	nop
	7:	iconst_m1
	8:	aload_1
	9:	monitorexit
	10:	ireturn
	11:	astore_2
	12:	aload_1
	13:	monitorexit
	14:	aload_2
	15:	athrow

동기화된 블록을 작성하면 16행의 바이트 코드가 생성되는 반면 메소드를 동기화할 경우에는 5행의 바이트 코드만 리턴된다.


2. ThreadLocal 변수

클래스의 모든 인스턴스에 대해 단일 변수 인스턴스를 유지하려는 경우 정적 클래스 멤버 변수를 사용할 수 있다. 스레드 단위로 변수 인스턴스를 유지하려는 경우에는 스레드 로컬 변수를 사용한다. ThreadLocal 변수가 보통 변수와 다른 점은 각 스레드에 개별적으로 초기화된 고유한 변수 인스턴스가 있다는 것이며, 이 인스턴스는 get() 또는 set() 메소드를 통해 액세스한다.

코드에서 각 스레드의 경로를 고유하게 식별하기 위한 멀티스레드 코드 추적기를 개발 중이라고 가정해 보자. 이 경우에는 여러 클래스에 있는 여러 메소드를 여러 스레드에서 효율적으로 사용할 수 있도록 조정하는 작업이 까다롭다. ThreadLocal이 없다면 매우 복잡한 작업이 될 것이다. 스레드는 실행되기 시작할 때 추적기에서 해당 스레드를 식별할 수 있는 고유 토큰을 생성하여 추적기의 각 메소드에 전달해야 한다.

ThreadLocal을 사용하면 작업이 간단해진다. 스레드가 시작할 때 스레드 로컬 변수를 초기화한 다음 각 클래스의 각 메소드에서 변수에 액세스하게 되며, 이 경우 변수는 현재 실행 중인 스레드에 대한 추적 정보만 호스트한다. 실행이 완료될 때 스레드는 해당 스레드 관련 추적을 모든 추적의 관리를 맡고 있는 관리 오브젝트에게 전달할 수 있다.

ThreadLocal은 변수 인스턴스를 스레드 단위로 저장할 필요가 있을 때 유용하다.


3. 휘발성 변수

필자의 짐작으로는 모든 Java 개발자 중에서 1/2 정도가 Java 언어에 volatile이라는 키워드가 있다는 것을 알고 있을 것이다. 물론 이 키워드의 의미를 아는 개발자는 10% 정도에 불과할 것이고 이 키워드를 효과적으로 사용하는 방법을 알고 있는 개발자는 훨씬 더 적을 것이다. 간단히 말해서 volatile 키워드로 변수를 식별하면 다양한 스레드에서 해당 변수의 값을 수정할 수 있게 된다. volatile 키워드의 기능을 잘 이해하려면 먼저 스레드에서 비휘발성 변수를 처리하는 방법을 이해해야 한다.

성능 향상을 위해 Java 언어 스펙에서는 JRE가 변수를 참조하는 각 스레드에서 변수의 로컬 사본을 유지할 수 있도록 허용한다. 이러한 변수의 "스레드-로컬" 사본은 캐시와 유사하기 때문에 스레드에서 변수의 값에 액세스해야 할 때마다 기본 메모리를 검사하지 않아도 된다는 장점이 있다.

그러나 다음과 같은 시나리오를 생각해 보자. 두 개의 스레드가 시작하면서 한 스레드에서는 변수 A를 5로 읽고, 다른 스레드에서는 변수 A를 10으로 읽었다. 변수 A가 5에서 10으로 변경되면 첫 번째 스레드에서는 변경을 알지 못하기 때문에 올바르지 않은 변수 A 값을 가지게 된다. 하지만 변수 A가 volatile로 표시되었다면 스레드가 A의 값을 읽을 때마다 A의 마스터 사본을 다시 참조하여 변수의 현재 값을 읽게 된다.

애플리케이션에 사용되는 변수가 변경되지 않을 것이라면 스레드-로컬 캐시가 유용하다. 그렇지 않은 경우에는 volatile 키워드의 기능을 알고 있으면 많은 도움이 된다.


4. 휘발성과 동기화 비교

변수가 volatile로 선언되었다는 것은 해당 변수가 여러 스레드에 의해 수정될 것임을 의미한다. 기본적으로 JRE에는 휘발성 변수에 대한 몇 가지 동기화 양식이 있다. 다행스럽게도 JRE에서는 휘발성 변수에 액세스할 때 암묵적으로 동기화를 제공한다. 하지만 여기에는 한 가지 큰 위험이 있다. 즉, 휘발성 변수를 읽는 것도 동기화되고 휘발성 변수에 쓰는 것도 동기화되지만 비원자적 연산은 동기화되지 않는다.

이는 곧 다음 코드가 스레드로부터 안전하지 않다는 것을 의미한다.

myVolatileVar++;

앞의 명령문은 다음과 같이 작성할 수도 있다.

int temp = 0;
synchronize( myVolatileVar ) {
  temp = myVolatileVar;
}

temp++;

synchronize( myVolatileVar ) {
  myVolatileVar = temp;
}

다시 말해서, 휘발성 변수를 읽고, 수정한 다음 새 값을 지정하는 방식으로 업데이트할 경우 두 개의 동기화 연산 사이에서 스레드로부터 안전하지 않은 연산이 수행되는 결과가 발생한다. 그런 다음 동기화를 사용할지 아니면 JRE의 지원을 받아서 휘발성 변수를 자동으로 동기화할지 여부를 결정할 수 있다. 유스 케이스에 따라 적합한 방법이 달라진다. 즉, 휘발성 변수의 지정된 값이 변수의 현재 값에 따라 달라지는 경우(예: 증분 연산) 스레드로부터 안전한 연산을 수행하려면 동기화를 사용해야 한다.


5. 원자적 필드 업데이터

멀티스레드 환경에서 프리미티브 유형을 증가 또는 감소시킬 경우에는 동기화된 코드 블록을 직접 작성하기 보다는 java.util.concurrent.atomic 패키지에 있는 새 원자적 클래스 중 하나를 사용하는 것이 훨씬 더 효과적이다. 원자적 클래스는 값을 증가, 감소, 업데이트 및 추가하는 등의 특정 연산이 스레드로부터 안전한 방식으로 수행되도록 보장한다. 원자적 클래스 목록에는 AtomicInteger, AtomicBoolean, AtomicLong, AtomicIntegerArray 등이 포함되어 있다.

원자적 클래스를 사용하는 데 있어서 어려운 점은 get, set 및 다양한 get-set 연산을 포함한 모든 클래스 연산이 원자적 연산으로 변환된다는 것이다. 즉, 중요한 read-update-write 연산뿐만 아니라 원자적 변수의 값을 수정하지 않는 readwrite 연산도 동기화된다. 이 문제를 해결하는 방법은 원자적 필드 업데이트를 사용하여 동기화된 코드의 배치를 좀 더 세밀하게 제어하는 것이다.

원자적 업데이트 사용하기

AtomicIntegerFieldUpdater, AtomicLongFieldUpdaterAtomicReferenceFieldUpdater 등의 원자적 필드 업데이터는 기본적으로 휘발성 필드에 적용되는 랩퍼이다. 내부적으로 Java 클래스 라이브러리에서는 이러한 업데이터를 사용하고 있다. 애플리케이션 코드에서 많이 사용되고 있지는 않지만 그렇다고 해서 사용하지 않을 이유도 없다.

목록 2에서는 원자적 업데이트를 사용하여 누군가가 읽고 있는 책을 변경하는 클래스의 예제를 보여 준다.


목록 2. Book 클래스

package com.geeckap.atomicexample;

public class Book
{
    private String name;

    public Book()
    {
    }

    public Book( String name )
    {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName( String name )
    {
        this.name = name;
    }
}

Book 클래스는 name이라는 단일 필드를 사용하는 POJO(Plain Old Java Object)이다.


목록 3. MyObject 클래스

package com.geeckap.atomicexample;

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

/**
 *
 * @author shaines
 */
public class MyObject
{
    private volatile Book whatImReading;

    private static final AtomicReferenceFieldUpdater<MyObject,Book> updater =
            AtomicReferenceFieldUpdater.newUpdater( 
                       MyObject.class, Book.class, "whatImReading" );

    public Book getWhatImReading()
    {
        return whatImReading;
    }

    public void setWhatImReading( Book whatImReading )
    {
        //this.whatImReading = whatImReading;
        updater.compareAndSet( this, this.whatImReading, whatImReading );
    }
}

목록 3MyObject 클래스는 예상대로 getset 메소드를 사용하여 whatAmIReading 특성을 노출한다. 하지만 set 메소드는 약간 다른 작업을 수행한다. 단순히 내부 Book 참조를 지정된 Book에 할당하는 대신(이 작업은 목록 3의 주석 처리된 코드를 사용하여 수행할 수 있음) AtomicReferenceFieldUpdater를 사용한다.

AtomicReferenceFieldUpdater

Javadoc에서는 AtomicReferenceFieldUpdater가 다음과 같이 정의되어 있다.

지정된 클래스의 지정된 휘발성 참조 필드에 대한 원자적 업데이트 기능을 제공하는 리플렉션 기반 유틸리티이다. 이 클래스는 동일한 노드의 여러 참조 필드가 개별적으로 원자적 업데이트와 관련되어 있는 원자적 데이터 구조에 사용하기 위해 설계되었다.

목록 3에서는 AtomicReferenceFieldUpdater가 정적 newUpdater 메소드 호출을 통해 작성되며, 이 메소드에서는 다음과 같은 세 개의 매개변수를 사용한다.

  • 필드가 포함된 오브젝트의 클래스(이 경우에는 MyObject)
  • 원자적으로 업데이트될 오브젝트의 클래스(이 경우에는 Book)
  • 원자적으로 업데이트될 필드의 이름

여기에서 중요한 점은 getWhatImReading 메소드가 어떤 유형의 동기화도 없이 실행되는 반면 setWhatImReading은 원자적 연산으로 실행된다는 것이다.

목록 4에서는 setWhatImReading() 메소드를 사용하는 방법과 값이 올바르게 변경되었는지 확인하는 방법을 보여 준다.


목록 4. 원자적 업데이트를 실행하는 테스트 케이스

package com.geeckap.atomicexample;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

public class AtomicExampleTest
{
    private MyObject obj;

    @Before
    public void setUp()
    {
        obj = new MyObject();
        obj.setWhatImReading( new Book( "Java 2 From Scratch" ) );
    }

    @Test
    public void testUpdate()
    {
        obj.setWhatImReading( new Book( 
                "Pro Java EE 5 Performance Management and Optimization" ) );
        Assert.assertEquals( "Incorrect book name", 
                "Pro Java EE 5 Performance Management and Optimization", 
                obj.getWhatImReading().getName() );
    }

}


원자적 클래스에 대한 자세한 정보는 참고자료를 참조한다.


결론

멀티스레드 프로그래밍은 항상 어렵기는 하지만 Java 플랫폼이 발전하면서 일부 멀티스레드 프로그래밍 태스크를 쉽게 수행할 수 있도록 지원하는 기능도 추가되었다. 이 기사에서는 메소드 동기화와 코드 블록 동기화의 차이점, ThreadLocal 변수를 스레드 단위 저장소로 사용할 때의 장점, 많은 개발자가 잘 모르고 있는 volatile 키워드(volatile을 동기화 연산에 사용할 때의 위험성 포함) 및 원자적 클래스의 특성에 대한 간략한 설명을 포함하여 Java 플랫폼에서 멀티스레드 애플리케이션을 작성하는 방법과 관련된 많이 알려져 있지 않은 5가지 사항을 살펴보았다. 자세한 정보는 참고자료 섹션을 참조한다.


참고자료

교육

토론

  • My developerWorks 커뮤니티에 참여하자. 개발자가 이끌고 있는 블로그, 포럼, 그룹 및 Wiki를 살펴보면서 다른 developerWorks 사용자와 의견을 나눌 수 있다.

필자소개

Steven Haines is a technical architect at ioko and the founder of GeekCap Inc. He has written three books on Java programming and performance analysis, as well as several hundred articles and a dozen white papers. Steven has also spoken at industry conferences such as JBoss World and STPCon, and he previously taught Java programming at the University of California, Irvine, and Learning Tree University. He resides near Orlando, Florida.

잘못된 도움말 신고

부정사용 신고

감사합니다. 이 항목은 운영자가 관심을 표시했습니다.


잘못된 도움말 신고

부정사용 신고

제출실패 신고. 나중에 다시 실행해주세요.


디벨로퍼웍스 로그인


IBM ID가 필요하세요?
IBM ID를 잊으셨습니까?


비밀번호를 잊으셨습니까?
비밀번호 변경

developerWorks 이용 약관에 동의하시는 경우 제출을 클릭하십시오. 이용 약관.

 


developerWorks에 처음 로그인하면 developerWorks프로파일이 생성됩니다.귀하의 프로파일에서 동의하신 내용이 공개되지만 이 사항은 언제든지 변경 가능합니다. 귀하의 성명(숨김으로 체크되어 있어도 표시됩니다)과 디스플레이 이름은 게시한 컨텐츠나 사이트 엑세스시 표시됩니다.

화면상에 보여지는 닉네임을 정하세요.

처음 developerWorks에 로그인할 때 프로파일이 작성되므로, 이를 위해 디스플레이 이름을 선택해야 합니다. 선택하신 디스플레이 이름은 developerWorks에 게시한 컨텐츠에 표시됩니다.

3글자 이상 31글자 이하의 길이로 사용 가능합니다. dW커뮤니티 내에서는 보안상 이메일주소를 제외한 다른 이름을 지정하셔야 합니다.

3개의 &이나 대쉬를 포함해주시고 31글자내로 제한해주세요.


developerWorks 이용 약관에 동의하시는 경우 제출을 클릭하십시오. 이용 약관.

 


아티클 순위

의견

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=20
Zone=자바
ArticleID=628260
ArticleTitle=멀티스레드 프로그래밍에 대해 모르고 있던 5가지 사항
publish-date=11092010
author1-email=steve@javasrc.com
author1-email-cc=

태그

Help
검색 필드를 사용하여 My developerWorks 내에서 해당 태그가 사용된 모든 종류의 컨텐츠를 검색하십시오.

태그를 더 많이 보거나 적게 보기 위해 슬라이더 막대를 사용하십시오.

인기 태그는 특정 컨텐츠 존(예를 들어, 자바, 리눅스, WebSphere)의 최고 인기 태그를 보여줍니다.

내 태그는 특정 컨텐츠 존(예를 들어, 자바, 리눅스, WebSphere)의 귀하의 태그를 보여줍니다.

검색 필드를 사용하여 My developerWorks 내에서 해당 태그가 사용된 모든 종류의 컨텐츠를 검색하십시오. 인기 태그는 특정 컨텐츠 존(예를 들어, 자바, 리눅스, WebSphere)의 최고 인기 태그를 보여줍니다. 내 태그는 특정 컨텐츠 존(예를 들어, 자바, 리눅스, WebSphere)의 귀하의 태그를 보여줍니다.