일단 내가 찾아본 자료들로는 명시적으로 non static inner class의 memory를 해제할 순 없어보인다

무조건 outer class의 instance를 들고있게된다. (hidden으로 들고있게된다고 한다.)

 

 

 의문이 들었던 것은 non static inner class는 무조건 memory leak을 유발시키는 것인가에 대한 고민이었는데

그럴것 같진 않다.

 

GC의 ROOT에서부터 Reach가되는 Object instance가지 탐색을해서 reach하지 않게되면 instacne를 해제하게되는데 

 

아래 그림과 같은 경우는 outer class 및 inner class (non static inner class)의 instance는 사이좋게 GC에 의해 수거가 되게된다.

 

 

하지만 inner class에서 thread가 돌아가면서 긴 시간의 일을 하게되면 어떻게 될까?

 

GC ROOT쪽에서 Outer class instance의 참조를 만약 명시적으로 null 로 준다고 하여도

 

inner class의 Thread가 돌아가게 되고 이는 main process가 inner class instance를 참조하게되고 또한

 

outer class instance역시 참조를 당하기에 memory leak을 유발하게 될 것이다.

 

 

또한 위의 그림과 같은구조인데 static변수가 inner class의 instance를 들고있게되면 역시 위와 같은 

Reference chain이 생기게되어 memory leak이 발생하게 된다.

 

 

 

 

 

참고

https://blog.androidcafe.in/android-memory-leak-part-1-context-85cebdc97ab3

 

Android Memory Leak Part 1: Inner class

Vietnamese Link: https://medium.com/@huynhquangthao/2a487116496f

blog.androidcafe.in

 

 

 

static inner class는 static영역에 처음부터 상주하게 된다. 즉 프로세스가 실행되는 시점부터

이미 메모리에 올라가게된다. 그러므로 static inner class는 outer class의 static변수에는 접근이 가능하다.

 

사실 static inner class는 inner class 라 보기에 어려운 점이 한두가지가 아닌것이 일단 outer class에 대한

reference정보가 없다. 당연한것이 outer class는 static class가 아니고 인스턴스가 생성되기 전이므로

정보가 없게된다. 그러므로 Memory Leak을 관리하기 위한 이점이 있는편이다.

 

다만 static inner class라고 하더라도 outer class의 참조값 혹은 context를 받아와야하는 경우가 생긴다.

그럴때는, WeakRefernce를 사용해서 받아줘야한다.

 

 

 

 

-궁금해서 실험해본내역

static inner class 라고해서 내부의 변수들이 모두 static 변수는 아니다.

명시적으로 static inner class에 static으로 선언한애들만 static 변수가 되는것이다.

static inner class 의 일반변수들도 new로 인하여 인스턴스가 생성될때 할당되며 그때의 인스턴스를통해

맴버변수들에 접근할 수 있게된다. 

 

그러므로 static inner class라고 하더라도 멤버변수들이 outer class의 context같은 참조값을 들고있게된다면

위험한건 마찬가지다. 그러므로 WeakReference 변수로 받아줘야할 필요가 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
 
public class Main {
    static int c = 20;
    
    
    public static void main(String[] args) {
        
        TestClass tc1 = new TestClass();
        tc1.a = 3;
        
        TestClass tc2 = new TestClass();
        tc2.a = 4;
        
        System.out.println("tc1.a="+tc1.a+"  tc2.a="+tc2.a);
        System.out.println("tc1="+tc1+"  tc2="+tc2);
        
        System.out.println("tc1.b="+tc1.b+"  tc2.b="+tc2.b);
        
        // Main.TestClass.a = 3; //컴파일에러
        System.out.println("static TestClass.c=" + TestClass.c);
 
    }
    
    static class TestClass {
        int a;
        int b = c;
        static int c = 200;
        
    }
    
}
 
 
 

 

 

 

참고

 

https://movefast.tistory.com/41

 

14. Inner Class (Nested Class) and Interface - 중첩 클래스와 중첩 인터페이스

▶ 중첩 클래스 및 중첩 인터페이스를 사용하는 이유 - 자바는 객체 지향 프로그램으로 각 클래스가 관계를 맺으며 상호작용을 한다. 어떤 클래스는 많은 클래스와 상호작용을 하기도 하지만, 다른 어떤 클래스는..

movefast.tistory.com

일반 inner class는 outer class의 인스턴스를 먼저 생성하고나서야 inner class의 인스턴스를 생성할 수 있다.

 

static inner class는, outer class의 이름으로바로 접근하여 인스턴스를 생성할수있다.

 

이점만 봐도 사실 outer와 static inner class간의 메모리 영역은 다른곳에 상주하고있다고 생각을해도 될것같다.

 

 

 

 

SingleTon Class처럼 오래사는 인스턴스에게 가끔 context를 넘겨줘야할 상황이 있을수도있다.

그러나 엄청난 주의가 필요하다.

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
 
public class SingletonLeakActivity extends AppCompatActivity {
    
    private SingleTonClass stc;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_static_ref_leak);
 
        stc = SingleTonClass.getInstance(this);
    }
}
 
class SingleTonClass {
    private Context mContext;
    private static SingleTonClass instance;
    private SingleTonClass (Context context) {
        mContext = context;
    }
    static synchronized public SingleTonClass getInstance(Context context) {
        if (instance == null)
            instance = new SingleTonClass(context);
        return instance;
    }
 
}
 
cs

 

위의같은경우라면 SignleTonClass 인스턴스가 SigleTonLeakActivtiy의 context를 계속 쥐고있는 상황이생겨

액티비티가 종료되어도 GC가 수거를 못할수도 있게된다.

 

이럴땐 2가지 솔루션이있다.

1.this대신 getApplicatoinContext()를 넘겨주는것이다. Applicatoin이 살아있는동안의 context이고 액티비티들의 라이프사이클과 관련이없기때문이다.

use the Application context. This context will live as long as your application is alive and does not depend on the activities life cycle. 

결론=> 오래살아야하는 인스턴스중 context가 필요하다면 application-context를 넘겨주는게 좋다.  (activity-context를 넘겨주지마라)

 

 

2.하지만 액티비티의 라이프사이클이 필요해서 this / activity-context 넘겼다면 어떻게해야할까?

명시적으로 null처리를 잘해야한다. 위의예제같은경우는 액티비티(컴포넌트)가 onDestroy될때 싱글톤객체에게 context를 null로 만들도록 명시적으로 처리를 해줘야 할 것이다.

 

 

 

 

참고

https://m.blog.naver.com/PostView.nhn?blogId=rjs5730&logNo=221304797233&categoryNo=0&proxyReferer=https%3A%2F%2Fwww.google.com%2F

 

(Android) ActivityContext? ApplicationContext?

안녕하세요. 안드로이드 스튜디오 툴을 해보신 분이면 Context를 한번 쯤 보셨을 거예요. 이렇게 친숙한 ...

blog.naver.com

http://sunphiz.me/wp/archives/tag/applicationcontext

 

applicationcontext – Dog발자

컨텍스트(Context)란? 컨텍스트란 작게는 어플리케이션 자신이 가지고 있는 이미지, 문자열, 레이아웃 같은 리소스 참조를, 크게는 안드로이드 시스템 서비스에 접속하기 위한 관문 역할을 하는 객체다. 이에 대해서는 이미 좋은 글이 있다. 컨텍스트(Context)를 얻는 방법에는 무엇이 있나? 액티비티나 서비스에서 getApplicationContext() 호출 : Application 객체가 Context 형으로 반환된다. 액티비티나 서비스에서 getA

sunphiz.me

https://android-developers.googleblog.com/2009/01/avoiding-memory-leaks.html

 

context에 대한설명과 메모리 leak

 

Avoiding memory leaks

The latest Android and Google Play news and tips for app and game developers.

android-developers.googleblog.com

 

 

안드로이드 Activity 혹은, View들을 Static변수에 담는건 피하는게 좋다.

 

Java Life Cycle과 Android의 Component Life Cycle이 조금 차이가있기에 설명해보고자한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
 
 
public class StaticRefLeakActivity extends AppCompatActivity {
 
    static Activity leakActivity;
    static TextView leakText;
 
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_static_ref_leak);
 
        leakActivity = this;
        leakText = new TextView(this);
    }
 
}
 
 

 

위와 같은 코드가 있을때 메모리 참고는 아래그림처럼 된다.

static변수들은 process가 시작할때 초기화되고 static영역에 자리잡게된다. 클래스의 인스턴스와는 전혀 다른

메모리공간에 상주한다고 봐도된다.

 

그런데 위의 코드에서 Activity가 종료된다고 생각해보자.(다른화면으로의 전환) Activitiy가 종료된다는것은 안드로이드 컴포넌트의 수명주기가 종료되었다는것이다. 하지만 객체의 수명은 이제 GC에의해 참조되는것이 없으면 끝나게된다. 

 

하지만...static영역은 프로세스(앱의실행)가 끝나지 않았기에 아직 StaticRefLeakActivtiy를 참조하고있고 또한, 익명의 TextView 인스턴스 역시 참조당하고 있다. TextView인스턴스 역시 this(StaticRefLeakActivtiy의 인스턴스)를 참조하고있기에 문제가되는상황이다. (GC가 수거를못해감)

 

그렇기에 static에 뷰 혹은 액티비티 인스턴스를 담는것은 매우 위험하다. 

만약 정말 그래야하는 상황? 이 생긴다면 해제를 정말 기가막히게 잘해줘야할 것이다.

 

 

 

참고

 

https://medium.com/@zhangqichuan/memory-leak-in-android-4a6a7e8d7780

 

Memory leak in Android

If Java is your first programming language, most likely you will take memory management for granted and let the built-in JVM Garbage…

medium.com

 

Thread (스레드) 설정

ThreadPoolExecutor는 작업자 스레드 개수와 풀의 생성과 종료뿐만이 아니라, 모든 스레드의 속성도

정의한다.  흔히 설정하는 응용프로그램 동작은 UI스레드와 경쟁하지않도록 스레드 우선순위를 낮추는 것이다.



작업자 스레드는 ThreadFactory 인터페이스의 구현을 통해 설정된다. 스레드 풀은 우선순위,이름,

예외 핸들러와 같은 작업자 스레드의 속성을 정의할 수 있다.



Thread 속성을 customize 한 고정 스레드풀



class LowPriorityThreadFactory implements ThreadFactory{
private static int count = 1;

public Thread newThread(Runnable r){
Thread t = new Thraed(r);
t.setPriority(4);
t.setUncaughtExceptionHandler(new Thraed.UncaughtExceptionHandler(){}
@Override
public void uncaugghtException(Thread t, Throwable e){
Log.d(TAG, "Thread = "+t.getName() + ", error="+e.getMessage());
});
return t;
}
}

Executors.newFixedThreadPool(10, new LowPriorityThreadFactory());




스레드 풀은 많은 스레드를 가지고 있고, 그 스레들은 실행시간을 두고 UI 스레드와 경쟁하기 때문에,

일반적으로 작업자 스레드에는 UI 스레드보다 낮은 우선순위를 할당하는 것이 좋다.


우선순위가 커스텀 ThreadFactory보다 낮지 않은 경우, 작업자 스레드는 기본적으로 UI 스레드와  같은 우선순위를 얻는다.



ThreadPoolExecutor 확장

ThreadPoolExecutor는 주로 독립적으로 사용되지만, 프로그램이 실행자 또는 실행자의 태스크를 추적할 수 있도록 확장될 수 있다. 응용프로그램은 스레드가 실핼될 때 마다 취하는 동작을 추가하기 위해 다음 메서드를 정의할 수 있다.



void beforeExecute(Thread t, Runnable r)

스레드를 실행하기전에 런타임 라이브러리에 의해 실행된다.



void afterExecute(Runnable r, Throwable t)

스레드가 정상적으로 또는 예외에 의해 종료된 후 런타임 라이브러리에의해 실행된다.


void terminated()

스레드 풀 종료 후 실행중이거나 실행을 위해 대기중인 태스크가 없을떄 런타임 라이브러리에 의해 실행된다.




처음 두메서드에는 Thread와 Runnable 객체가 전달된다. 전달되는 순서가 두 메서드에서 반대라는 점에 유의해라.

스레드 풀에서 얼마나 많은 태스크가 현재 실행되는지 추적하는 기초적인 커스텀 스레드 풀을 보여준다.



스레드 풀에서 진행중인 태스크의 수를 추적하는 예제 code


public class TaskTrackingThreadPool extends ThreadPoolExecutor{

private AtomicInteger mTaskCount = new AtomicInteger(0);

public TaskTrackingThreadPool(){
super(3, 3, 0L, TimeUnit.SECONDS, new LinkedBlockingQueue());
}
@Override
protected void beforeExecute(Thread t, Runnalbe r){
super.beforeExecutor(t,r);
mTaskCount.getAndIncrement();
}

@Override
protected afterExecute(Runnable r, Throwable t){
super.afterExecute(r,t);
mTaskCount.getAndDecrement();
}

public int getNbrOfTasks(){
return mTaskCount.get();
}


}



beforeExecute는 태스크 실행전에 mTakseCoutner를 증가시키고 afterExecute는 실행후에

카운트를 감소시킨다. 


외부 관찰자는 언제든 getNbrOfTasks를 통해 현재 실행 태스크의 수를

요청할 수 있다. 


작업자 스레드와 외부 관찰자 스레드는 동시에 공유멤버변수를 접근할 수 있다.

따라서 스레드 안전을 보장하기 위해 AtomicInteger로 정의했다.



커스텀 스레드 풀


ThreadPoolExecutor 설정


ThreadPoolExecutor에 의해 스레드의 생성과 종료뿐만 아니라 Task의 큐잉에도 사용되며 다음과 같은 생성자에서 수행된다.



 ThreadPoolExecutor executor = new ThreadPoolExecutor(

   int corePoolSize, //핵심풀크기

   int maximumPoolSie,  //최대풀크기

   long keepAliveTime,  //생존 유지시간

   TimeUnit unit,  //생존 유지 시간의 단위

   BlockingQueue workQueue; //태스크 큐 유형

);




핵심풀 크기

스레드 풀에 포함되는 스레드의 하한, 실제로 스레드풀은0개 스레드로부터

시작하지만, 핵심 풀 크기에 도달하면 스레드 개수는 하한 이하로 떨어지지않는다.

풀에서 작업자 스레드의 개수가 핵심 풀크기보다 적을때, 태스크가 큐에 추가되면, 작업을 기다리는 유휴스레드가 있는경우에도 새로운 스레드가 생성될 것이다. 일단 작업자 스레드의 개수가 핵심풀크기보다 같거나 많아지면,

큐가 가득찬경우 새로운 작업자 스레드가 생성된다. 즉 큐는 스레드 생성에 대한 우선권을 얻는다.



최대 풀 크기

동시에 실행할 수 있는 스레드의 최대개수.

최대 풀 크기에 도달한 후, 큐에 더해진 태스크는 태스크를 처리할  유휴 스레드가 사용가능해질 떄까지 큐에서 기다린다.



최대 유휴시간(생존 유지시간)

유휴 스레드는 처리하기 위해 들어오는 태스크를 준비하기 위해 활성 상태를 유지하지만, 생존시간이 설정된 경우 시스템은 비핵심 풀 스레드를 회수할 수 있다.

생존시간은 TimeUnit에 의해 설정된 시간 단위로 측정된다.



태스크 큐 유형

Task가 작업자 스레드에 의해 처리될 수 있을때까지 소비자에 의해 더지핸 Task를 보유하는 BlockingQueue의 구현.



- 스레드 풀 설계

스레드 풀은 동시에 백그라운드 Task를 실행해야 하는 스레드를 관리하는데 

도움을 주지만, 사용자는 여전히 제한된 자원 소비와 높은 처리량을 얻기 위해 현명하게

설정해야 한다. 기본적인 목표는 필요한 것보다 더 많은 메모리를 사용하지 않고

하드웨어에 의해 허용되는 가장높은 속도로 작업을 처리하는 스레드 풀을 생성하는 것이다.




크기나스레드풀의 최대크기를 정한다. 스레드의 최대 개수가 너무작으면

충분한 속도로 큐에서 태스크를 꺼내지않아 성능저하될수있따.


예를들어 모든 스레드가 긴 I/O연산을 실행하는 경우, I/O 동작이 완료될떄까지 실행시간을 얻지 못하고 

기다리는 짧은 수명의 태스크가 있을 수 있다.


반면 너무 많은 스레드도 CPU가 실행 대신 스레드의 전환에 많은 시간을 사용해야하므로 성능에 부정적인 영향을 미칠 수 있다.


스레드 풀의 크기는 하부의 하드웨어, 좀더 정확히말해 사용가능한 CPU 개수를 기준으로 하는것이 좋다. 안드로이드는 Runtime 클래스에서 CPU개수를 알아낼 수 있다.


int N  = Runtime.getRuntume().availableProcessors();


N은 실제로 동시에 실행될 수 있는 태스크의 최대 개수다.


스레드 풀의크기 N은 독립적이고 비차단적인 태스크들의 운용하기에 충분할수있다.

(고도의 연산을 요하는등)



역동성



제한 또는 무제한 태스크 큐

스레드풀은 일반적으로 제한 또는 무제한 태스크 큐와 함꼐 사용한다.

무제한 큐는 무한증가할 수 있어서 메모리가 고갈할 수 있는 반면, 제한 큐의 자원소비는

더 잘관리될 수 있다 한편 제한 큐는 그 크기와 포화정책을 모두 준비해야하낟.

포화정책이란 거부된 태스크를 생산자가 어떻게 처리할지를 뜻한다.



제한 또는 무제한의 큐를 구현한 것이 LinkedBlockingQueue, PriorityBlockingQueue, ArrayBlockingQueue다.

뒤의 두가지 큐는 제한 큐고, 첫번째 큐는 기본적으로 무제한 큐지만 제한 큐로 구성할 수 있다.


스레드 풀


Thread pool은 task 큐와, 작업자 스레드 집합의 조합.


작업자 스레드는 생산자-소비자 구조를 형성한다.


생산자는 큐에 Task를 추가하고, 작업자 스레드는 새로운 백그라운드 실행을

수행할 준비가 된 유휴 스레드가 있을 때마다 Task를 소비한다.




작업자 스레드 풀은, 태스크를 실행하는 활성 스레드와 실행을 기다리는 유휴 스레드를 모두포함할 수 있다.



스레드 풀의 장점

- 작업자 스레드는 실행할 다음 태스크를 기다리기 위해서, 살아 있을 수 있음. 이는

스레드가 매 태스크를 위해 생성과 파괴(오버헤드 증가)될 필요 없다는 것을 의미한다.


- 스레드 풀은 스레드의 최대개수로 정의된다. 이는 응용프로그램 메모리를 소비하는

백그라운드 스레드 수가 많아져서 스레드 풀에 과부하가 걸리는것을 막기 위해서다.


-모든 작업자 스레드의 생명주기는 스레드 풀 생명주기에 의해 제어된다.



미리 정의된 스레드 풀

Executor 프레임워크는 Executors 팩토리 클래스에서 만들어진 미리 정의된 스레드 풀 유형을 포함한다.


- 고정 크기

고정크기 스레드 풀은, 사용자가 정의한 개수의 작업자 스레드를 유지한다.

종료된 스레드는 작업자 스레드의 일정 스레드 수를 유지하기 위해 새로운 스레드로 대체딘다.

고정된 풀 유형은 Executors.nexFixedThreadPool(n) 으로 생성되며 n은 스레드 수다.


이 스레드 풀 유형은 무한한 태스크 큐를 사용한다. 즉 새로운 태스크가 추가로 더해지면 큐가 자유롭게 증가할수있다.

그러므로 생산자는 태스크를 삽입하는데 실패하지 않는다.



- 동적 크기

동적 크기의 스레드 풀, 즉 캐시된 스레드 풀은 처리할 태스크가 있을 때 새로운

스레드를 만든다. 유휴 스레드는 실행할 새로운 태스크를 60초간 기다리고, 태스크 큐가 비어있는

경우 종료된다. 따라서 스레드 풀은 실행할 태스크 수와 함꼐 늘어나고 줄어든다.

Executors.newCachedThreadPool()로 생성한다.




- 싱글스레드 실행자

이 풀은 큐에서 task를 처리하기위해 하나의 작업자 스레드를 가진다.

태스크는 차례대로 실행되고 스레드 안전이 침해될수없다.

Executors.newSingleThreadExecutor()로 생성한다.

Executor 프레임워크를 사용하면 다음과 같은 일들을 처리할 수있습니다.



 - 스레드에서 실행되기를 기다리는 task 수를 제어하는 작업자 스레드 pool과 큐를 설정

- 비정상적으로 종료되는 스레드를 초래하는 에러 확인

- 완료되는 스레드를 기다리고 스레드로부터 결과를 가져온다.

- 스레드의 일괄처리를 실행하고, 고정된 순서로 결과를 가져온다.

- 사용자가 더 빨리 결과를 확인할 수 있도록 알맞은 시간에 백그라운드 스레드를 시작한다.




Excutor 프레임워크의 기본 구셩요소는 Executor 인터페이스다.


이것의 주요목표는 태스크의 생성을 분리하여(Runnnalbe) 앞에서 나열한 응용프로그램 동작들을 가능하게 하는 것이다.



이 인터페이스는 단하나의 메서드를 포함한다.

 public interface Executor{

void execute(Runnalbe command);
}




Executor 로 제어할 수 있는 동작으로는 다음과 같다.


- 태스크 큐잉

- 태스크 실행순서

- 태스크 실행 유형(직렬 또는 동시)




Executor 예제

AsyncTask에서 사용된 직렬 Task 실행자를 구현. SerialExecutor는 생산자-소비자 패턴으로 구현


Thread는 Runnable Task를 생성하고, 큐에 Task를 배치하며, 소비자 스레드는 큐에서 Task 를 제거하고 처리한다.



private static class SerialExecutor implemtents Executor{
final ArrayDeque mTasks = new ArrayDeque();
Runnable mActive;

public synchronized void execute(final Runnable r){

mTasks.offer(new Runnable(){
public void run{
try{
r.run();
}
finally{
scheduleNext();
}
}
});
if( mActive == null){
scheduleNext();
}
protected sysnchronized void scheduleNext(){
if((mActive=mTasks.poll())!= null){
THREAD_POOL_EXECUTOR.excute(mActive);
}
}

}
}





실행자 실행동작특징은 다음과 같다.


- 태스크 큐잉

ArrayDeque - 데큐, 양방향으로 꺼낼 수 있는 큐.

             스레드에 의해 처리될떄까지 삽입된 Task들을 보유한다.


- 태스크 실행순서

모든 태스크는 mTask.offer()를 통해 데큐의 끝에 넣어진다. 따라서

결과는 삽입된 태스크들 fifo 순서다.



- 태스크 실행 유형

Task는 직렬로 실행되지만, 같은 스레드에서 실행될 필요없다.

Task가 실행완료할떄 마다 r.run()이 완료할떄마다, scheduleNext()가 호출된다.

이것은 큐에서 다음 Task를 가져와서 스레드가 Task그를 실행할 수 있는 스레드 풀안의 다른 Executor로 보낸다.



요컨대 SerialExecutor는 각각 다른 스레드에서 Task를 처리할 수 있으며 Task들의 순차적인 실행을 보장하는 실행환경을 

구성한다.


<순차실행이지, 동시실행은 아님>




이전 Thread 포스팅 입니다. 참고하시려면 클릭해서 보시면 되겠습니다.

Thread 안전, Thread safety <= 클릭

JAVA Thread 암시적 잠금 종류 및 생산자와 소비자<=클릭


Intro

Multi Thread가 반응성있도록 동작하려면, 앱을 설계시, Thread 생성과 Task 실행을 염두해두어야 합니다. 

별로 추천하고싶지 않은 설계방법 2가지를 먼저 살펴보겠습니다.



Thread or Task(스레드 및 태스크) 설계 및 실행 전략에 대해 살펴보도록 하겠습니다.



여기서설명하는 Task과 Thread의 정확한 의미구분을 하고 먼저 넘어가겠습니다.

Task는 일의 단위입니다. 하나의 메서드일수도있고, 메서드 집합일수도있으며, 응용프로그램에서 의미있는 한가지의 일을 Task라 생각하시면 되겠습니다.

Thread 는 Process를 점유할수있는 하나의 실행 흐름입니다.


보통 자바, 안드로이드에서는 Task와 Thread를 같은 의미인것처럼 사용하긴하지만, 정확히는 의미가 차이가 있습니다..


1) 모든 Task에 하나의 Thread 사용

모든 Task가 동일한 Thread에서 실행되는 경우입니다. 사용가능한 Process를 사용하지 않아, 반응성 없는 앱이 될 수 있습니다.

다른 Thread를 이용해서 Task를 적절히 분리하여 병행실행이 가능하기때문입니다.


2) 각각의 Task 당 하나의 Thread 사용

각각의 Task에 대해 시작, 종료되는 새로운 Thread에서 항상 Task를 실행하도록 작성을하면,

Task가 자주 생성되며 수명이 짧은 경우에는 Thread 생성 및 해체작업의 Overhead가 증가되게됩니다. 그러므로 성능 저하 이슈로 이어지게 됩니다.


.


순차실행

하나의 Task가 완료되면 다음 Task가 실행되는 구조입니다. 따라서 Task의 실행 간격에 중첩은 없습니다.

그러므로 2가지의 강점을 가지게 됩니다.


1) Thread Safety가 지켜집니다.

2) Multi Thread 보다 적은 memory를 소비하는 하나의 Thread로 실행할 수 있습니다.



하지만 처리량이 적고, 순차적이기 때문에, 하나의 태스크가 실행되지않으면, 그이후 태스크 작업은 절대 일어나지 않습니다. 

시작지연이나, 전혀 실행되지 않는 문제가 생기게 됩니다.



동시 실행

Task는 병렬 및 Interleave(처리할 Data의 dependency 없애 동시에 실행하는 방법)로 실행됩니다. 더 나은 CPU 사용률을 가지기에

처리량이 뛰어나고, 반응성이 좋습니다.

하지만 Thread Safety문제로 인해서, 동기화를 구현해줘야 합니다.


그렇기때문에 효과적으로 Multi Thread를 설계하려면, 순차실행과 동시실행을 통해 실행환경을 만들어야 합니다. 융통성있게 개발자가 구현해야할 몫입니다.



동시 실행 설계

동시 실행설계는 매우 다양한 방법으로 설계할 수 있습니다. 


1) 자원 생성과 해체 빈도를 감소시키기 위해, 항상 새로운 Thread를 만드는 것보다 재사용을 건장합니다.

2) 필요이상으로 Thread를 사용하지 말아야합니다. thread가 많을수록 더많은 memory와 process 시간이 소비되며, 개발자의 부담으로 이어지게 됩니다.


마무리

안드로이드 앱은, single 및 multi process platform에서 성능 향상 및 반응성있도록, multiThread로 만들어져야 합니다. 

Thread는 하나의 Process에서 실행을 공유하거나 multiprocess를 사요할 수 있을떄 진정한 동시성(concurrency)를 활용할 수 있게 됩니다.

향상된 성능은 증가한 복잡성뿐만 아니라, Thread간 공유자원을 보호하고 데이터 일관성을 유지해야하는 책임을 가지게 됩니다.


그만큼 멀티스레드의 장점도있지만 그만큼 개발자의 부담감으로 이어지겠습니다.



암시적 잠금 사용


자바에서 암시적 잠금은 synchronized를 사용하면된다. 하지만 사용하는 방식에따라 다른 방법으로 공유자원을 접근 읽기쓰기를 수행할 수 있습니다. synchronized 키워드는 다양한 암시적 잠금을 제공하게됩니다.


*1.) 객체 인스턴스를 둘러싸는 암시적 잠금으로 작동하는 메서드 레벨

 synchronized void writeVal(){

     sharedVal++;

}

객체의 인스턴스 메서드가 여러스레드에서 호출될때, 동기화문제를 해결합니다.


*2.)객체 인스턴스를 둘러싸는 암시적 잠금으로 작동하는 블록 레벨

  void writeVal(){

    synchronized(this){

       sharedVal++;

   }

}

1.)과 거의 비슷하나 강점이 있습니다.

위의 코드의 장점은, 임계영역에 포함된 코드의 정확한 블록을 제어할 수 있고, 실제로 보호될 상태와 관련된 코드만 줄여서 다룰 수 있게 됩니다. 일표이상으로 원자영역(atomic) 을 크게 만들지 않습니다. 


*3.)다른객체의 암시적 잠금을 가지는 블록 레벨

private final Object myLockKey= new Object(); 

 void writeVal(){

  synchronized(myLockKey){

     sharedVal++;

  }

}

클래스내에서 여러개의 잠금을 사용할 수 있게 됩니다. 


*4.)클래스 인스턴스를 둘러싸는 암시적 잠금으로 작동하는 메서드 레벨

 synchronized static void writeVal(){

     sharedVal++;

}


*5.)클래스 인스턴스를 둘러싸는 암시적 잠금으로 작동하는 블록레벨

 static void writeVal(){

   synchronized(MyClass.class){  //static 변수.

      sharedVal++;

   }

}





예제:소비자와 생산자


스레드끼리 공동으로 작업할때, 순서를 의도적으로 조율해야할 경우가 있습니다. 보통 이러한 패턴을 소비자 생성자 패턴이라 부릅니다.



다음클래스는, 생산부분과, 소비부분이 있습니다. 리스트에 변수를 추가하고 삭제를하는 시나리오를 생각해보겠습니다.

생산메서드는, 리스트의 변수를 늘리고, 소비리스트는 변수를 줄일겁니다. 생산메서드는 용량이 꽉차면 소비가 이뤄질때까지 소유권을 포기하고(열쇠를 포기) 대기해야되며, 소비메서드는 용량이 0이면 소비를멈추고 자바 모니터 소유권을 포기하고(열쇠를 포기하고) 대기해야할 것입니다.


public class ConAndPro{

  private LinkedList<String> mList = new LinkedList<Steing>();

  private Object lock = new Object();


 //생산

  public void produce(){

     while(true){

    synchronized(lock){

      while(mList.size() == 10){

          lock.wait();  //자바모니터에서 소유권을 포기하고 넘겨줍니다.(notify가올때까지 여기서 대기됨)

       }

      list.add("supply");

      lock .notify();

    }


     }

  


 //소비

  public void consume(){

    while(true){

    synchronized(lock){

      while(mList.size() == 0){

          lock.wait();  //자바모니터에서 소유권을 포기하고 넘겨줍니다.(notify가올때까지 여기서 대기됨)

       }

      list.removeFirst();

      lock .notify();

    }


     }

  


.


lock 에 대해 동기화가 되었습니다. lock.wait은, lock에 의해 동기화된부분에서 열쇠,자바모니터를 포기한다는 의미입니다. 소유권을 내어놓게됩니다.

lock.notify는 lock임계영역에서 열쇠를 포기해서 대기중인 스레드에게 다시 소유권을 주어 lock 임계영역에서 스레드 활동을 할수있도록 해줍니다.


물론 sychonized(lock) 이부분은 어떤 스레드가 동기화변수 "lock"을가지고있는지 확인하고 없으면 자신이 들어가고, 있으면 밖에서 대기를 하다가 synchronized안에 들어가게 됩니다. 


들어가게 되어도, 생산 소비할때 조건이 안된다면 wait을 만나서 잠시 lock을 내어놓고, 다른 lock의 임계영역에 들어간 스레드가 notify 해줄때까지 기다리게 됩니다. notify를 받으면 다시 lock임계영역에소 소유권을 가지게되어 다시 스레드가 synchronized(lock) 에서 멈췄던 부분에서부터 시작되게 됩니다.



아래 코드는 생산과 소비동작을 실행하는 두개의 Thread 예시입니다.


 final ConAndPro cp = new ConAndPro();


new Thread(new Runnable(){

 @Override

 public voice run(){

     cp.produce();

  

  }

}.start();


new Thread(new Runnable(){

 @Override

 public voice run(){

     cp.consume();

  

  }

}.start();







+ Recent posts