Composition

기존 클래스가 새로운 클래스의 구성요소가 되는것

상속

장점

  • 코드의 재사용을 통해서 중복을 줄인다.
  • 확장성이 증가한다.
  • 클래스 간의 계층적 관계를 구성함으로써 다형성을 구현할 수 있다.
  • 개발 시간이 단축된다.

단점

  • 캡슐화를 깨뜨린다.
    • 상위 클래스의 구현이 하위 클래스에게 노출되기 때문에 캡슐화가 깨진다.
    • 따라서 자식 클래스가 부모 클래스에 강하게 결합 및 의존하게 된다.
    • 부모 클래스의 내부 구현이 달라지면 코드 한줄 건드리지 않은 하위 클래스가 오작동할 수 있다.
    • 부모클래스의 결함이 자식 클래스에게 넘어온다.

컴포지션

  • 상속처럼 기존의 클래스를 확장하는 것이 아닌, 새로운 클래스를 생성하여 private 필드로 기존 클래스의 인스턴스를 참조하는 방식이 바로 컴포지션이다.
    • forwarding이라고도 부른다.
  • 새로운 클래스이기 때문에, 여기서 어떤한 생성 작업이 일어나더라도 기존의 클래스는 전혀 영향을 받지 않는다는 점이 핵심이다.
  • 메서드를 호출하는 방식으로 동작하기 때문에 캡슐화를 깨뜨리지 않는다.
  • 상위클래스에 의존하지 않기 때문에 변화에 유연하다.

일반적으로 상속은 Is -a 관계에 있다고 표현한다.

조합도 이와 비슷하게 Has -a 관계에 있다.

그렇다면 상속은 사용하면 안될까?

답은 아니다. 쓰는 경우가 따로 명확히 존재한다는 뜻!

상속을 써야하는 경우

  • 명확한 Is -a 관계의 경우
  • 상위 클래스가 확장할 목적으로 설계되었고 문서화도 잘되어 있는 경우

'Computer science > JAVA' 카테고리의 다른 글

Interned String in Java  (0) 2023.03.22
Error & Exception  (0) 2023.03.16
GC  (0) 2023.03.16
JVM  (0) 2023.03.16
Instrinsic Lock  (0) 2023.01.08

Interned String in Java

자바의 문자열은 불변이다. String의 함수를 호출을 하면 해당 객체를 직접 수정하는 것이 아니라, 함수의 결과로 해당 객체가 아닌 다른 객체를 반환한다. 그러나 항상 그런 것은 아니다.

  • 대문자로 문자열 생성 후 String의 toUpperCase()를 호출하면 내부 구현에서 lower case의 문자가 발견되지 않으면 기존의 객체를 반환한다.
  • 문자열변수.interen()을 하게되면
    • 해당 문자열과 동일한 값을 가진 문자열을 상수풀에서 찾고 있다면 해당 문자열을 바라보게 하고 없다면 새로 생성하여 반환합니다.
  • 언제 사용될까?
    • 만약 문자열을 == 비교연산으로 비교해야 한다면. intern() 메서드가 사용될 수 있다. 해당 메서드에서 상수풀을 찾아 같은 값을 가지고 있다면 해당 참조를 반환해주기 때문에 속도가 더 빠를 수 있지만 해당 문자열이 상수풀에 없을때는 equals보다 느릴 수 도 있다. 상황에 따라 잘 사용해야할 것 같다!

'Computer science > JAVA' 카테고리의 다른 글

Composition  (0) 2023.03.22
Error & Exception  (0) 2023.03.16
GC  (0) 2023.03.16
JVM  (0) 2023.03.16
Instrinsic Lock  (0) 2023.01.08

Error & Exception

Error 와 Exception의 차이

  • Error
    • 시스템이 종료되어야 할 수준과 같이 수습할 수 없는 심각한 문제를 의미한다.
  • Exception
    • 개발자가 구현한 로직에서 발생한 실수나 사용자의 영향에 의해 발생한다.

Exception Handling

  • 잘못된 하나로 인해 전체 시스템이 무너지는 결과를 방지하기 위한 기술적인 처리이다.
  • 예외가 발생하는 주요 원인
    • 사용자의 잘못된 데이터 입력
    • 잘못된 연산
    • 개발자가 로직을 잘못 작성
    • 하드웨어, 네트워크 오작동
    • 시스템 과부하
  • JAVA에서 모든 예외가 발생하면 xxxException 객체를 생성한다. 예외를 처리하는 방법에는 크게 2가지가 있다.
    • 직접 try catch를 이용해서 예외에 대한 최종적인 책임을 지고 처리하는 방식
    • throws Exception을 이용해서 발생한 예외를 호출하는 쪽이 책임지는 방식

Throwable 클래스

https://gyoogle.dev/blog/computer-language/Java/Error%20&%20Exception.html

  • Throwable 클래스는 예외처리를 할 수 있는 최상위 클래스이다. Exception과 Error는 Throwable의 상속을 받는다.

Error

https://gyoogle.dev/blog/computer-language/Java/Error%20&%20Exception.html

  • 시스템 레벨에서 발생하여, 개발자가 어떻게 조취할 수 없는 수준을 의미
    • OutOfMemory : JVM에서 설정된 메모리의 한계를 벗어난 상황일 때 발생. 힙 사이즈가 부족하거나 너무 많은 class를 로드할때, 가용가능한 swap이 없을때 큰 메모리의 native 메서드가 호출될때 등이 있다. 이를 해결하기 위해 dump파일분석, jvm 옵션 수정 등이 있다.

Exception

https://gyoogle.dev/blog/computer-language/Java/Error%20&%20Exception.html

  • 예외는 개발자가 구현한 로직에서 발생하며 개발자가 다른 방식으로 처리가능한 것들로 JVM은 정상 동작한다.

Exception의 2가지 종류

  • Checked Exception
    • 예외처리가 필수이며, 처리하지 않으면 컴파일되지 않는다. JVM 외부와 통신할 때 주로 쓰인다.
      • RuntimeException 이외에 있는 모든 예외
      • IOException, SQLException 등
  • Unchecked Exception
    • 컴파일 때 체크되지 않고, Runtime에 발생하는 Exception을 말한다.
      • RuntimeException 하위의 모든 예외
      • NullPointerException, IndexOutOfBoundException 등

Throw 구문

  • 예외처리를 할때 현재 메서드가 직접 처리하지 않고 호출한 곳에 발생 여부를 통보한다. 호출한 메서드는 이걸 다시 throw할지 처리할지 정해야 한다. return 보다 강력

try-with-resource

  • try()에 자원 객체를 선언해 사용하면, try 블록이 끝날 때 해당 자원을 자동으로 종료 해준다. 다만 AutoCloseable 인터페이스가 구현된 객체여야 사용 가능하다.

'Computer science > JAVA' 카테고리의 다른 글

Composition  (0) 2023.03.22
Interned String in Java  (0) 2023.03.22
GC  (0) 2023.03.16
JVM  (0) 2023.03.16
Instrinsic Lock  (0) 2023.01.08

Garbage Collection

  • 사용하지 않는 객체는 메모리에서 삭제하는 작업을 GC라고 부르며 JVM에서 GC를 수행한다.

stop-the-world

  • GC를 실행하기 위해 JVM이 애플리케이션 실행을 멈추는 것.
  • 어떤 GC 알고리즘을 사용하더라도 stop-the-world는 발생하게 된다.
    • 이 시간을 줄이는 것이 바로 GC튜닝!!
  • GC를 해도 더이상 사용 가능한 메모리 영역이 없는데 계속 메모리를 할당하려고 하면
    • OutOfMemoryError가 발생하여 WAS가 다운될 수 도 있다.
  • 따라서 규모있는 JAVA 어플리케이션을 효율적으로 개발하기 위해선 GC에 대해서 잘 알고 있어야 한다.

GC의 대상

  1. 객체가 NULL인 경우
  2. 블럭 실행 종료 후, 블럭 안에서 생성된 객체
  3. 부모 객체가 NULL인 경우, 포함하는 자식 객체

GC의 메모리 해제 과정(Mark and Sweap)

  1. Marking
    • 프로세스는 마킹을 호출 → GC가 메모리가 사용되는지 아닌지를 찾아낸다.
    • 모든 오브젝트는 마킹단계에서 결정을 위해 스캔되어 진다. 모든 오브젝트를 스캔하기 때문에 매우 많은 시간이 소모된다.
  2. Normal Deletion
    • 참조되지 않는 객체를 제거하고, 메모리를 반환한다. 메모리 Allocator는 반환되어 비어진 블럭의 참조 위치를 저장해 두었다가 새로운 오브젝트가 선언되면 할당되도록 한다.
  3. Compacting
    • 퍼포먼스를 향상시키기 위해, 참조되지 않는 객체를 제거하고 또한 남은 참조되어지는 객체들을 묶는다. 이들을 묶음으로서 공간이 생기므로 새로운 메모리 할당 시에 더 쉽고 빠르게 진행할 수 있다.

Generational Garbage Collection 배경

  • 위의 과정과 같이 모든 객체를 Mark&Compact 하는 JVM은 비효율 적이다.
  • Y축은 할당된 바이트 수, X축은 바이트가 할당될 때의 시간. 시간이 갈수록 적은 객체만이 남는 다는 것을 볼 수 있고, 위와같은 그래프에 기반한 것이 Weak Generational Hypothesis이다.

Weak Generational Hypothesis

  • 신규로 생성한 객체의 대부분은 금방 사용하지 않는 상태가 되고, 오래된 객체에서 신규 객체로의 참조는 매우 적게 존재한다는 가설이다.
  • 이 가설에 기반하여 자바는 Young 영역과 Old 영역으로 메모리를 분할하고, 신규로 생성되는 객체는 Young영역에 보관하고, 오랫동안 살아남은 객체는 Old 영역에 보관한다.

Generational Garbage Collection

  1. Young 영역
    • 새롭게 생성한 객체의 대부분이 이곳에 위치한다.
    • 대부분의 객체가 금방 접근 불가능 상태가 되기 때문에 많은 객체가 Young영역에 생성되고 사라진다. 이 영역에서 객체가 사라질때 Minor GC가 발생한다고 말한다.
  2. Old 영역
    • 접근 불가능 상태로 되지 않아 Young 영역에서 살아늠은 객체가 이곳으로 복사된다. 대부분의 Young 영역보다 크게 할당하며, 크기가 큰 만큼 Young 영역보다 GC는 적게 발생한다. 이 영역에서 객체가 사라질때 Major GC가 발생한다고 말한다.
  3. Permanent 영역
    • Method Area라고도 한다. JVM이 클래스들과 메서드들을 설명하기 위해 필요한 메타데이터들을 포함하고 있다. JDK8부터는 PermGen 은 Metaspace로 교체

'Computer science > JAVA' 카테고리의 다른 글

Interned String in Java  (0) 2023.03.22
Error & Exception  (0) 2023.03.16
JVM  (0) 2023.03.16
Instrinsic Lock  (0) 2023.01.08
Thread  (1) 2023.01.02

JVM

JVM 이란?

  • 시스템 메모리를 관리하면서 자바 기반 애플리케이션을 위해 이식 가능한 실행 환경을 제공한다. 한마디로 JVM은 자바 애플리케이션을 실행하는 프로그램이다. 이를 통해 자바는 OS 에 종속되지 않고, JVM이 존재한다면 실행 가능하다. Stack 기반의 가상 머신이다.

실행과정

  1. 프로그램이 실행되면 JVM은 OS로부터 이 프로그램이 필요로하는 메모리를 할당받는다. JVM은 이 메모리를 용도에 따라 여러 영역으로 나누어 관리한다.
  2. 자바 컴파일러가 자바 소스코드를 읽고 바이트 코드로 변환한다.
  3. 변환된 바이트 코드를 클래스 로더를 통해 JVM 메모리 영역으로 로딩된다.
  4. 로딩된 class 파일들은 Excution Engine을 통해 해석되고
  5. 해석된 바이트 코드는 메모리 영역에 배치되어 실질적인 수행이 이뤄진다. 이러한 과정 속 JVM은 필요에 따라 스레드 동기화난 가비지 컬렉션 같은 메모리 관리 작업을 수행한다.

JVM 런타임 데이터 영역

모든 스레드가 공유하는 영역

  • 메서드(스태틱) 영역
    • 크게 두가지 영역으로 나뉠 수 있음
      • 코드 세그먼트
        • 바이트코드로 변환되어 이 영역에 들어간다.
      • 데이터 세그먼트
        • static 데이터가 정의되는 영역
    • 메서드의 바이트코드, static 변수가 할당된다.
  • 힙 영역
    • 배열과 모든 인스턴스 객체가 할당되는 영역으로 자동 초기화가 진행된다.

스레드마다 가지는 영역

  • 스택 영역
    • 지역변수, 매개변수가 할당되는 영역으로 초기화가 진행되지 않는다.
  • PC Resisters
    • 현재 실행되고 있는 주소를 저장하고 있는 영역
  • Native Method Stacks
    • c나 c++로 작성된 메서드를 실행할 때 사용되는 스택

  • 스레드는 하나의 heap, method Area를 공유하고있다.
    그러나 하나의 스레드는 다른 스레드의 내부 데이터에 접근 할 수 없다.
    우리가 하나의 메서드 안에서 지역변수의 동시성 문제를 걱정하지 않아도 되는 이유가 바로 이것!

'Computer science > JAVA' 카테고리의 다른 글

Error & Exception  (0) 2023.03.16
GC  (0) 2023.03.16
Instrinsic Lock  (0) 2023.01.08
Thread  (1) 2023.01.02
Casting  (0) 2023.01.01

Intrinsic Lock

  • 자바는 멀티스레드 환경에서 동기화를 지원하기 위해 가장 기초적인 장치인 ’고유 락(Intrinsic Lock)’을 지원한다. 개발자는 synchronized 키워드를 이용해서 특정 객체의 고유락을 사용해 여러 스레드를 동기화 시킬 수 있다.
  • Synchronized 블록은 Intrinsic Lock을 이용해서, Thread의 접근을 제어한다.

Java의 synchronized

  • 동일한 객체에 대해 synchronized 블록을 사용하는 두 스레드는 한 번에 하나의 스레드만 내부로 들어갈 수 있고, 이 것이 자바가 제공하는 가장 기본적인 ‘상호배제(Mutual Exclusion)’ 장치이다.
  • synchronized에는 4가지의 사용법이 있다.
    • synchronized method
    • synchronized block
    • static synchronized method
    • static synchronized block
      • 4가지 방식의 차이인 lock이 적용되는 범위를 알아보자

1. synchronized method

  • synchronized method는 클래스의 인스턴스에 대하여 lock을 건다.
  • 하나의 인스턴스에 대하여 2개의 thread가 경합하는 상황
public class Main {
    public static void main(String[] args) {
        A a = new A();
        Thread thread1 = new Thread(()->{
            a.run("t1");
        });
        Thread thread2 = new Thread(()->{
            a.run("t2");
        });
        thread1.start();
        thread2.start();
    }
}

public class A {
    public synchronized void run(String name){
        System.out.println(name + "lock");
        try{
            Thread.sleep(1000);
        } catch(InterruptedException e){
            e.printStackTrace();
        }
        System.out.println(name + "unlock");
    }
}
  • 순서대로 lock을 획득하고 반납함
    • 실행 결과
      • t1lock t1unlock t2lock t2unlock
  • 각각의 인스턴스를 만들고 실행 해보자
public class Main {
    public static void main(String[] args) {
        A a = new A();
        A a1 = new A();
        Thread thread1 = new Thread(()->{
            a.run("t1");
        });
        Thread thread2 = new Thread(()->{
            a1.run("t2");
        });
        thread1.start();
        thread2.start();
    }
}
  • lock을 공유하지 않기 때문에 동기화가 발생하지 않음
    • 실행 결과
      • t1lock t2lock t1unlock t2unlock
  • 결과
    • synchronized method는 인스턴스에 대하여 lock을 건다.
    • 인스턴스에 대해서 lock을 건다라는 표현이 인스턴스 접근 자체가 lock이 걸리는 걸까?? 확인해보자
public class Main {
    public static void main(String[] args) throws InterruptedException {
        A a = new A();
        Thread thread1 = new Thread(()->{
            a.run("t1");
        });
        Thread thread2 = new Thread(()->{
            a.print("t2");
        });
        thread1.start();
        Thread.sleep(500);
        thread2.start();
    }
}

public class A {
    public void print(String name){
        System.out.println(name + " hi");
    }
    public synchronized void run(String name){
        System.out.println(name + "lock");
        try{
            Thread.sleep(1000);
        } catch(InterruptedException e){
            e.printStackTrace();
        }
        System.out.println(name + "unlock");
    }
}
  • synchronized가 적용되지 않은 print() 메서드를 추가하고 lock이 걸린 중간에 호출 해보았더니 run() 메서드의 lock과 상관없이 정상적으로 호출됨
    • 실행 결과
      • t1lock t2 hi t1unlock
  • print() 메서드도 sychronized가 적용되어있다면?
    • 동기화 발생
    • 실행 결과
      • t1lock t1unlock t2 hi
  • 결과
    • synchronized 메서드는 인스턴스 단위로 lock을 건다.
    • 인스턴스에 lock을 거는 synchronized 키워드는 synchronized가 적용된 메서드끼리 일괄적으로 lock을 공유한다.

2. static synchronized method

  • static이 포함된 synchronized method방식은 우리가 일반적으로 생각하는 static 성질을 갖는다. 인스턴스가 아닌 클래스 단위로 lock이 발생한다.
public class Main {
    public static void main(String[] args) throws InterruptedException {
        A a1 = new A();
        A a2 = new A();
        Thread thread1 = new Thread(()->{
            a1.run("t1");
        });
        Thread thread2 = new Thread(()->{
            a2.run("t2");
        });
        thread1.start();
        thread2.start();
    }
}
public class A {
    public static synchronized void run(String name){
        System.out.println(name + "lock");
        try{
            Thread.sleep(1000);
        } catch(InterruptedException e){
            e.printStackTrace();
        }
        System.out.println(name + "unlock");
    }
}
  • 실행 결과
    • t1lock t1unlock t2lock t2unlock
  • 다른 인스턴스 이지만 클래스 단위로 lock이 발생했다.
  • 만약 static synchronized method와 synchronized method가 섞여있다면 어떨까?
public class Main {
    public static void main(String[] args) throws InterruptedException {
        A a1 = new A();
        A a2 = new A();
        Thread thread1 = new Thread(()->{
            a1.run("t1");
        });
        Thread thread2 = new Thread(()->{
            a2.print("t2");
        });
        thread1.start();
        thread2.start();
    }
}
public class A {
    public synchronized void print(String name){
        System.out.println(name + " hi");
    }
    public static synchronized void run(String name){
        System.out.println(name + "lock");
        try{
            Thread.sleep(1000);
        } catch(InterruptedException e){
            e.printStackTrace();
        }
        System.out.println(name + "unlock");
    }
}
  • 실행 결과
    • t1lock t2 hi t1unlock
  • 인스턴스 단위의 lock과 클래스 단위의 lock은 공유되지 않았다.
  • static synchronized method를 정리해보면
    • 클래스 단위로 lock을 걸지만
    • 인스턴스 단위의 synchronized method와 lock을 공유하지 않는다.

3. synchronized block

  • synchronized block은 인스턴스의 block단위로 lock을 건다. 이 때, lock 객체를 지정해줘야 한다.
public class Main {
    public static void main(String[] args) throws InterruptedException {
        A a1 = new A();
        Thread thread1 = new Thread(()->{
            a1.run("t1");
        });
        Thread thread2 = new Thread(()->{
            a1.run("t2");
        });
        thread1.start();
        thread2.start();
    }
}
public class A {
    public void run(String name) {
        synchronized (this) {
            System.out.println(name + "lock");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(name + "unlock");
        }
    }
}
  • 실행 결과
    • t1lock t1unlock t2lock t2unlock
  • block의 인자로 this를 주었다.
    • this는 해당 인스턴스를 의미하고 위 코드에서 method 전체가 block으로 감싸져 있으므로 메서드 선언부에 synchronized 키워드를 붙인 것과 똑같이 동작한다.
    • 하지만 여러 로직이 섞여 있는 사이 부분만 lock을 걸 수 있다. lock은 synchronized block에 진입할 때 획득하고 빠져나오면서 반납하므로 block으로 범위를 지정하는 것이 효율적이다.
    • synchronized block도 method와 동일하게 인스턴스에 대해서 적용된다.
  • 지금까지는 block에 인자로 this를 사용해서 Synchronized를 메서드 선언부에 붙인 것과 별반 다를 것 없이 사용함
    • 이제는 block에 자원을 명시하고 사용
public class Main {
    public static void main(String[] args) throws InterruptedException {
        A a = new A();
        Thread thread1 = new Thread(() -> {
            a.run("thread1");
        });

        Thread thread2 = new Thread(() -> {
            a.run("thread2");
        });

        Thread thread3 = new Thread(() -> {
            a.print("자원 B와 상관 없는 thread3");
        });

        thread1.start();
        thread2.start();
        thread3.start();
    }
}
public class A {
    B b = new B();
    public void run(String name) {
        synchronized (b){
            System.out.println(name + " lock");
            b.run();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(name + " unlock");
        }
    }

    public synchronized void print(String name) {
        System.out.println(name + " hello");
    }
}
public class B extends Thread{
    @Override
    public synchronized void run() {
        System.out.println("B lock");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("B unlock");
    }
}
  • 실행 결과
    • thread1 lock 자원 B와 상관 없는 thread3 hello B lock B unlock thread1 unlock thread2 lock B lock B unlock thread2 unlock
  • 매우 복잡한 상황이다. lock 객체를 A클래스가 아닌 B클래스의 인스턴스로 사용하고 있다.
  • thread1, thread2는 b를 사용하는 method를 호출하고 있지만
  • thread3은 b와 상관없는 method이기 때문에 b의 lock과 상관없이 출력되었다.
  • 즉 인스턴스 단위 lock과 B를 block한 lock은 공유되지 않고 별도로 관리되는 것을 확인할 수 있다.
  • 이번에는 block에 인스턴스가 아니라 class를 명시해보았다.
public class Main {
    public static void main(String[] args) {
        A a = new A();
        Thread thread1 = new Thread(() -> {
            a.run("thread1");
        });

        Thread thread2 = new Thread(() -> {
            a.run("thread2");
        });

        thread1.start();
        thread2.start();
    }
}
public class A {
    B b = new B();

    public void run(String name) {
        synchronized (B.class){
            System.out.println(name + " lock");
            b.run();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(name + " unlock");
        }
    }

    public synchronized void print(String name) {
        System.out.println(name + " hello");
    }
}
public class B extends Thread{
    @Override
    public synchronized void run() {
        System.out.println("B lock");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("B unlock");
    }
}
  • 실행 결과
    • thread1 lock B lock B unlock thread1 unlock thread2 lock B lock B unlock thread2 unlock
  • block의 인자로 인스턴스가 아닌 .class를 주었다.
  • 출력 결과를 보면 lock을 공유하고 있는 것을 확인할 수 있다.
  • block에는 객체를 넘기면 인스턴스 단위로 lock을 걸고, .class형식으로 넘기면 클래스 단위의 lock을 건다.

4. static synchronized block

  • static method안에 synchronized block을 지정할 수 있다. static의 특성 상 this같이 현재 객체를 가르키는 표현을 사용할 수 없다. static synchronized method 방식과 차이는 lock 객체를 지정하고 block으로 범위를 한정지을 수 있다는 점이다. 이외에 클래스 단위로 lock을 공유한다는 점은 같다.
public class Main {

    public static void main(String[] args) {
        A a1 = new A();
        A a2 = new A();
        Thread thread1 = new Thread(() -> {
            a1.run("thread1");
        });

        Thread thread2 = new Thread(() -> {
            a2.run("thread2");
        });

        thread1.start();
        thread2.start();
    }
}
public class A {

    public static void run(String name) {
        synchronized (A.class){
            System.out.println(name + " lock");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(name + " unlock");
        }
    }
}
  • 실행 결과
    • thread1 lock thread1 unlock thread2 lock thread2 unlock

동기화 순서

  • synchronized를 통해 lock을 물고 있을 때 여러 개의 스레드가 접근 요청을 한다고 가정하면
  • 첫 진입 이후에 동기화 순서가 보장되지는 않는다.

정리

  • synchronized method
    • 인스턴스 단위로 lock이 걸림
    • 메서드의 시작→종료 까지 동기화 발생
    • 동일한 인스턴스 내 synchronized 키워드가 적용된 메서드끼리 lock을 공유
  • synchronized block
    • 인스턴스 단위로 lock이 걸림
    • this를 명시하면 synchronized method와 동일하게 동작하면서 synchronized method와 lock을 공유
    • 특정 객체를 명시하면 해당 객체에만 특정 lock을 걸면서 해당 객체에 lock을 거는 block끼리만 lock을 공유
    • .class 형식 명시하면 해당 클래스에만 특정 lock을 걸면서 해당 클래스에 lock을 거는 block끼리만 lock을 공유
  • static synchronized method
    • 클래스 단위로 lock이 걸림
    • 메서드의 시작→종료 까지 동기화 발생
    • static synchronized와 synchronized가 혼용되어있을 때 각자의 lock으로 관리
  • static synchronized block
    • 클래스 단위 lock로 lock이 걸림.
    • block의 인자로 정적 인스턴스나 클래스만 사용
  • synchornized는 Thread의 동기화 순서를 보장하지 않는다.

Atomic

  • 래퍼 클래스의 일종으로 참조 자료형과 기본 자료형 두 종류 변수에 모두 적용 가능
  • 사용시 내부적으로 CAS 알고리즘을 사용해 lock 없이 동기화 처리가 가능

 

https://beststar-1.tistory.com/21#synchronized%EC%99%80_Lock%EC%9D%98_%EC%B0%A8%EC%9D%B4%EC%A0%90_-_%EA%B3%B5%EC%A0%95%EC%84%B1(Fairness)

  • 멀티 스레드 환경, 멀티 코어 환경에서 각 cpu는 메인 메모리에서 변수값을 참조하는 게 아닌 각 cpu의 캐시 영역에서 메모리 값을 참조하게 된다.
  • 이때, 메모리에 저장된 값과 cpu 캐시에 저장된 값이 다른 경우가 존재
    • 이를 가시성 문제라고 한다.
      • 그래서 사용되는 것이 CAS 알고리즘
  • 현재 스레드에서 저장된 값과 메인 메모리에 저장된 값을 비교하여 일치하는 경우 새로운 값으로 교체, 일치하지 않는다면 실패하고 재시도를 한다. 이렇게 처리되면 cpu 캐시에서 잘못된 값을 참조하는 가시성 문제가 해결된다.

Volatile

  • 자바 변수를 메인 메모리에 저장하겠다고 명시하는 키워드이다.
  • 매번 변수의 값을 읽을 때 마다 CPU 캐시에 저장된 값이 아니라 메인 메모리에서 읽는 것이며, 또한 변수의 값을 쓸 때 마다 메인 메모리에 작성하는 것이다.
    • 이 또한 가시성 문제를 해결하는 방법으로 볼 수 있다.

락(lock)

  • 모든 객체에는 lock이 하나씩 있는데 이 lock을 가지고 있는 스레드만 해당 객체의 임계 영역 코드와 관련된 작업을 할 수 있다.
  • 그렇지만 여러 스레드가 경쟁 상태에 있을 때 스레드가 진입권한을 획득할지 순서를 보장하진 않는다. 이를 암시적(Implicit)락 이라고 한다.(Intrinsic Lock) 이라고도 부른다.
  • Lock 클래스는 lock() 메서드와 unlock() 메서드를 호출함으로써 어떤 스레드가 먼저 락을 획득하게 될지 순서를 지정할 수 있다. 이를 명시적(explicit)락 이라고 한다.( Reentrant Lock)

💡 경쟁 상태

  • 공유하는 자원이 있는데 공유하는 자원에 접근하는 여러 스레드 중 어떤 것이 먼저 접근하냐에 따라 결과가 달라질 수 있는 경우가 있다. 이를 경쟁상태에 의해 발생되었다고 하기도 한다.

synchronized와 Lock의 차이점 - 공정성(Fairness)

  • synchronized와 Lock을 구분 짓는 키워드는 공정성(Fairness)이다.
  • 공정성이란 모든 스레드가 자신의 작업을 수행할 기회를 공평하게 갖는 것을 의미한다.
  • 공정한 방법에선 큐 안에서 스레드들이 무조건 순서를 지켜가며 락을 확보한다.
  • 불공정한 방법에선 만약 특정 스레드에 락이 필요한 순간 release가 발생하면 대기열을 건너뛰는 새치기 같은 일이 벌어지게 된다.

💡 기아 상태

  • 다른 스레드들에게 우선순위가 밀려 자원을 계속해서 할당받지 못하는 스레드가 존재하는 상황을 말하며, 이 기아 상태를 해결하기 위해 공정성이 필요하다.
  • synchronized는 공정성을 지원하지 않아서 후순위인 스레드의 실행이 안될 수 있는 반면에
  • ReentrantLock은 생성자의 boolean 인자를 통해 공정/불공정을 설정할 수 있다.

ReentrantLock

  • 가장 일반적인 락이며 재진입이 가능한 락이다.
  • 'Reentrant(재진입할 수 있는)'이라는 단어가 앞에 붙은 이유는
  • 특정 조건에서 락을 풀고 나중에 다시 락을 얻고 임계 영역으로 들어와서 작업을 수행할 수 있기 때문이다.

ReentrantReadWriteLock

  • 읽기에는 공유적이고, 쓰기에는 배타적인 락이다.
  • ReentrantReadWriteLock은 이름에서 알 수 있듯 읽기를 위한 락과 쓰기를 위한 락을 제공한다.
  • ReentrantLock은 배타적인 락이라서 무조건 락이 있어야만 임계 영역의 코드를 수행할 수 있으나,
  • ReentrantReadWriteLock은 읽기 락이 걸려있으면 다른 스레드가 읽기 락을 중복해서 걸고 읽기를 수행할 수 있다.
  • 읽기는 내용을 변경하지 않음으로 동시에 여러 스레드가 읽어도 문제 되지 않는다.
    • 그래서 읽기 락이 걸린 상태에서 쓰기 락은 허용되지 않는다.

StampedLock

  • ReentrantReadWriteLock에 '낙관적 읽기 락(Optimistic Reading Lock)'을 추가한 것이다.
  • StampedLock은 Java 8부터 추가되었으며, 다른 락과 달리 Lock 클래스를 구현하지 않았다.
  • 일반적으론 읽기 락이 걸려있으면 쓰기 락을 얻기 위해서는 읽기 락이 풀릴 때까지 기다려야 하는데,
  • 낙관적 읽기 락은 쓰기 락에 의해 바로 풀린다.
    • 그래서 낙관적 읽기에 실패하면 읽기 락을 얻어서 다시 읽어와야 한다.
  • 무조건 읽기 락을 걸지 않고 쓰기와 읽기가 충돌할 때만 쓰기가 끝난 후에 읽기 락을 거는 것이다.
  • StampedLock은 락을 걸거나 해지할 때 '스탬프(long 타입의 정수 값)'를 사용한다.
  • 스탬프에 해당하는 값이 둘다 long형의 숫자고 사전 연산에서 받아놓은 값이 이후 연산에서 비교 해봤을 때 변경되었다면 이후 연산은 실패가 된다.

'Computer science > JAVA' 카테고리의 다른 글

GC  (0) 2023.03.16
JVM  (0) 2023.03.16
Thread  (1) 2023.01.02
Casting  (0) 2023.01.01
Object Class  (0) 2023.01.01

스레드 구현

자바에서 스레드 구현 방법은 2가지가 있다.
  1. Runnable 인터페이스 구현
    1. Runnable 인터페이스를 구현한 경우는, 해당 클래스를 인스턴스화해서 Thread 생성자에 argument로 넘겨줘야 한다.
  2. Thread 클래스 상속
  • 둘다 run() 메소드를 오버라이딩 하는 방식이다.
  • 자바는 단일 상속 만을 지원하기 때문에 Runnable 인터페이스를 구현하는 것이 많은 이점을 가져갈 수 있다.

스레드 실행

스레드의 실행은 run()이 아닌 start()호출로 해야한다.
  • 분명 run() 메서드를 재정의 했지만 실제 스레드 작업을 시키려면 start()로 작업해야한다.
  • run()으로 작업지시를 하면 스레드를 사용하는 것이 아니게 된다.
  • Java에는 콜 스택이 있다. 이 영역이 실질적인 명령어를 담고 있는 메모리로 , 하나씩 꺼내서 실행시키는 역할을 한다.
    • 만약 동시에 두 가지 작업을 하기 위해선, 두 개 이상의 콜스택이 필요함
    • 스레드를 이용한다는 것은, JVM이 다수의 콜 스택을 번갈아가면 처리를 하는 것이고 사용자에겐 동시에 작업하는 것 처럼 보이게 된다.
    • 즉 run() 메서드를 이용하는 것은 main()의 콜스택에 이용하게 되는 것이고
    • start() 메서드를 호출하면 JVM은 알아서 스레드를 위한 콜 스택을 새로 만들어 주고 새로운 콜스택에 run()을 호출하게 된다.

스레드의 I/O블락킹

  • 입출력시 작업 중단되는 현상
  • 싱글스레드의 경우 I/O을 기다리면서 아무일도 하지 않는다.
  • 멀티스레드의 경우 I/O을 기다리는 것과 동시에 다른 일을 할 수 있다.

스레드의 우선순위

작업의 중요도에 따라 스레드의 우선순위를 다르게 하여 특정 스레드가 더 많은 작업시간을 갖게 할 수 있다.
  • 자바에서는 1~10까지 부여할 수 있고 default를 5이다.
    • setPriority(int newPriority)함수를 통해 변경할 수 있다.
    • 실제로는 참고만 할 뿐 무조건 먼저 실행되지는 않는다.
      • OS 스케줄러가 담당
      • 확률을 높이는 것을 기대함

데몬 스레드

일반 스레드의 작업을 돕는 보조적인 역할을 수행
  • 일반 스레드
  • 데몬 스레드
    • 일반 스레드의 작업을 돕는 보조적인 역할을 수행
    • 일반 스레드가 모두 종료되면 자동으로 종료
    • GC, 자동저장, 화면 자동갱신 등에 사용된다.
    • 무한루프와 조건문을 이용해서 실행 후 대기하다가 특정 조건이 만족되면 작업을 수행하고 다시 대기하도록 작성한다.
    void setDaemon(boolean on)
    //스레드를 데몬 스레드로 또는 사용자 스레드로 변경
    //매개변수 on을 true -> 데몬스레드가 된다.
    
    • setDaemon(boolean on)은 반드시 start()를 호출하기 전에 실행되어야 한다. 그렇지 않으면 IllegalTreadStateException이 발생.

스레드 그룹

서로 관련된 스레드를 그룹으로 묶어서 다루기 위한 것
  • 모든 스레드는 반드시 하나의 스레드 그룹에 포함되어 있어야 한다.
  • 지정하지 않고 생성하면 main스레드 그룹에 속한다.
  • 자신을 생성한 스레드의 그룹과 우선순위를 상속받는다.

스레드의 상태

스레드의 상태는 5가지가 있다.
  • NEW
    • 스레드가 생성되고 아직 start()가 호출되지 않은 상태
  • RUNNABLE
    • 실행 중 또는 실행 가능 상태
  • BLOCKED
    • 동기화 블럭에 의해 일시정지된 상태
  • WAITING, TIME_WAITING
    • 실행가능하지 않은 일시정지 상태
  • TERMINATED
    • 스레드 작업이 종료된 상태

https://jongwoon.tistory.com/14

스레드의 실행제어

스레드의 실행을 제어할 수 있는 메서드가 제공된다.

  • static sleep(long m) static sleep(long m, int n)
    • 지정된 시간동안 스레드를 일시정지시킴. 지정된 시간이 지나면 자동으로 실행대기상태가 됨.
    • 특정 스레드를 지정해서 멈추게 하는 것은 불가능하다.
  • void join() void join(long m) void join(long m, int n)
    • 지정된 시간동안 스레드가 실행되도록 한다. 지정된 시간이 지나거나 작업이 종료되면 join()을 호출한 스레드로 다시 돌아와 실행을 계속함.
  • void interrupt()
    • sleep()이나 join()에 의해 일시정지상태인 스레드를 깨워 실행대기 상태로 만든다. 해당 스레드에서는 interrupted exception이 발생함으로써 일시정지 상태를 벗어나게 된다.
  • void stop()
    • 스레드를 즉시 종료시킨다.
  • void suspend()
    • 스레드를 일시정지시킨다. resume()을 호출하면 다시 실행대기상태가 된다.
  • void resume()
    • suspend()에 의해 일시정지상태에 있는 스레드를 실행대기상태로 만든다.
  • static void yield()
    • 실행중에 자신에게 주어진 실행시간을 다른 스레드에게 양보하고 자신은 실행대기상태가 된다.
    • yield, interrupt 메서드를 적절히 사용하면, 응답성과 효율을 높일 수 있다.
  • suspend(), resume(), stop()
    • 이 메서드들은 dead-lock을 발생시키기 쉽기 때문에 사용을 지양하길 권장하고 있다.

스레드의 동기화(synchronization)

멀티 스레드 프로세스에서는 다른 스레드의 작업에 영향을 미칠 수 있다.

  • 진행중인 작업이 다른 스레드에게 간섭받지 않게 하려면 ‘동기화’가 필요
    • 스레드의 동기화 - 한 스레드가 진행중인 작업을 다른 스레드가 간섭하지 못하게 막는 것.
  • 동기화 하려면 간섭받지 않아야 하는 문장들을 ‘임계 영역’으로 설정
    • 임계영역은 lock을 얻은 단 하나의 스레드만 출입가능(객체 1개에 lock 1개)

synchronized를 이용한 동기화

  • synchronized로 임계영역(lock이 걸리는 영역)을 설정하는 방법 2가지
    1. 메서드 전체를 임계영역으로 지정
    public synchronized void calSum(){
    //...
    }
    
    1. 특정한 영역을 임계 영역으로 지정
    synchronized(객체의 참조변수){
    //...
    }
    
  • 임계 영역은 많으면 많을수록 성능이 떨어지기 때문에 최소화 하는 것이 좋다.

wait()와 notify()

동기화를 하면 효율이 떨어진다. 동기화의 효율을 높이기 위해 wait(), notify()를 사용.

  • Object클래스에 정의되어 있으며, 동기화 블록 내에서만 사용할 수 있다.
    • wait()
      • 객체의 lock을 풀고 스레드를 해당 객체의 waiting pool에 넣는다.
    • notify()
      • waiting pool에서 대기중인 쓰레드 중의 하나를 깨운다.
    • notifyAll()
      • waiting pool에서 대기중인 모든 스레드를 깨운다.

'Computer science > JAVA' 카테고리의 다른 글

JVM  (0) 2023.03.16
Instrinsic Lock  (0) 2023.01.08
Casting  (0) 2023.01.01
Object Class  (0) 2023.01.01
String Class  (0) 2023.01.01

Casting

형변환 변수 또는 상수의 타입을 다른 타입으로 변환하는 것

Primitive 타입

  • 묵시적 형 변환
    • 작은 데이터 타입에서 큰 데이터 타입으로 형 변환
    • 값에 대한 손실이 발생할 가능성이 없음
    • 따로 연산자가 필요없다.(JVM이 서비스 해줌)
  • 명시적 형 변환
    • 큰 데이터 타입에서 작은 데이터 타입으로 형 변환
    • 값에 대한 손실이 발생할 가능성이 있음
    • 명시적으로 형변환 연산자를 써줘야 한다.

byte → short → int → long → float → double

             char

Reference 타입

  • 사용할 수 있는 멤버의 갯수를 조절하는 것
    • 값이 달라지는 것이 아님
  • 조상 , 자손 관계의 참조변수는 서로 형 변환 가능

업캐스팅(UpCasting)

  • 업캐스팅은 자식 클래스가 부모 클래스 타입으로 캐스팅 되는 것이다.
  • 업캐스팅은 캐스팅 연산자 괄호를 생략할 수 있다
  • 단, 부모 클래스로 캐스팅 된다는 것은 멤버의 갯수 감소를 의미한다.이는 곧 자식 클래스에서만 있는 속성과 메서드는 실행하지 못한다는 뜻이다.
  • 업캐스팅을 하고 메소드를 실행할때, 만일 자식 클래스에서 오버라이딩한 메서드가 있을 경우, 부모 클래스의 메서드가 아닌 오버라이딩 된 메서드가 실행되게 된다.

다운 캐스팅(DownCasting)

  • 다운캐스팅은 거꾸로 부모 클래스가 자식 클래스 타입으로 캐스팅 되는 것이다.
  • 다운캐스팅은 캐스팅 연산자 괄호를 생략할 수 없다
  • 다운캐스팅의 목적은 업캐스팅한 객체를 다시 자식 클래스 타입의 객체로 되돌리는데 목적을 둔다. (복구)

instanceof 연산자

  • 참조 캐스팅을 잘못했다가 런타임 환경에서 에러가 나 프로그램이 종료 되버리면 서비스에 크나큰 차질이 생기게 된다.
  • 따라서 코드 디버깅을 많이 하여 미리 예방하는 것이 베스트이지만, 이마저도 부족하면 직접 업캐스팅 / 다운캐스팅 유무를 확인하여 참조 캐스팅 동작을 결정하면 된다.
  • 이때 사용되는 것이 instanceof 연산자인데, 이 연산자는 어느 객체 변수가 어느 클래스 타입인지 판별해 true/false를 반환해준다.

'Computer science > JAVA' 카테고리의 다른 글

Instrinsic Lock  (0) 2023.01.08
Thread  (1) 2023.01.02
Object Class  (0) 2023.01.01
String Class  (0) 2023.01.01
Serialization(직렬화)  (0) 2022.12.25

Object Class

Java의 최상위 클래스

  • 부모가 없는 클래스는 자동적으로 Object 클래스를 상속받게 된다.
  • 모든 클래스는 Object클래스에 정의된 11개의 메서드를 상속받는다.
    • toString(), equals(Object obj), hashCode(),…

toString()

  • 객체의 문자 정보를 리턴한다.
    • 객체 문자 정보 : 객체를 문자열로 표현한 값
    • 기본적으로 Object 클래스의 toString() 메서드는 “클래스명@16진수해시코드” 로 구성된 문자 정보를 리턴
    • Object의 하위 클래스는 toString() 메서드를 오버라이딩 하여 간결하고 유익한 정보를 리턴하도록 되어 있다.
    • System.out.println() 메서드의 매개변수로 참조타입을 넘겨주면 객체의 toString() 메서드를 호출하여 리턴값을 받아서 출력하도록 되어 있다.

equals(Object obj)

  • 객체를 비교하여 참 거짓을 리턴한다.
    • 객체의 주소를 비교
  • 객체의 인스턴스 변수의 값을 비교하고자 한다면 오버라이딩 해야한다.

hashCode()

  • 실행중에 객체의 유일한 integer값을 반환한다.
  • Object클래스에서는 heap에 저장된 객체의 메모리 주소를 반환하도록 되어있다.
  • public native int hashCode();
  • native란?
    • OS가 가지고 있는 메서드(C언어로 작성되어있는 경우가 많음)
    • JNI
      • OS가 가진 메서드를 java 메서드와 연결해준다.
        • java메서드를 호출할 시 , C나 C++로 작성된 함수가 실행됨
  • OS가 가지고 있는 메서드가 객체의 주소를 int 값으로 반환해준다! 자바에서는 그걸 사용할 뿐!
  • 그러므로 객체의 지문이라고함
    • 객체마다 다른 값을 가지기 때문
  • equals()를 오버라이딩 하면, hashCode()도 오버라이딩 해야한다.
    • 두 함수의 공통점은 객체의 주소를 가지고 작업을 한다는 것이다.
    • equals는 보통 주소가 아니라 객체의 멤버변수를 가지고 작업하도록 오버라이딩 한다.
      • 그렇게 되면 hashCode()도 멤버변수를 가지고 작업하도록 오버라이딩 해야한다.
        • equals() 결과가 true 이면 두 객체의 해시코드는 같아야한다.

왜 그럴까?

hash 값을 사용하는 Collection(HashSet, HashMap, HashTable)을 사용할 때 문제가 발생한다.

hash값을 사용하는 Collection은 객체가 논리적으로 같은지 비교할 때 위의 과정을 거친다.

  • hashCode 메서드의 리턴값이 우선 일치하고 equals 메서드의 리턴 값이 true여야 논리적으로 같은 객체라고 판단함.

그렇다면 무조건 같이 재정의 해줘야할까?

  • hash 값을 사용하는 Collection을 사용하지 않는다면 equals와 hashCode를 같이 재정의 하지 않아도 된다라고 생각할 수 있지만
  • 요구사항은 항상 변하고,협업환경에서 무조건(hashSet, HashMap, HashTable 사용안할거야!!!)이라는건 없다.
  • 굳이 위험한 코드를 안고 가지말고 항상 같이 재정의 해줘야 한다고 생각한다.

'Computer science > JAVA' 카테고리의 다른 글

Thread  (1) 2023.01.02
Casting  (0) 2023.01.01
String Class  (0) 2023.01.01
Serialization(직렬화)  (0) 2022.12.25
오토 박싱 & 오토 언박싱  (0) 2022.12.25

String클래스

  • 문자열을 다루기 위한 클래스.
  • String클래스 = 데이터(char[]) + 메서드(문자열 관련)

특징

  • 내용을 변경할 수 없는 불변(immutable) 클래스
  • 문자열 연산시 새로 객체를 만드는 Overhead 발생(성능이 떨어진다.)
  • 문자열의 결합이나 변경이 잦다면, 내용을 변경가능한 StringBuffer, StringBuilder를 사용

문자열 비교

String str1 = "abc";
String str2 = "abc";
String str3 = new String("abc");
String str4 = new String("abc");

  • str1 ,str2 는 문자열 abc를 가리킨다.
  • str3, str4 는 각각의 인스턴스를 가리킨다.
  • 문자열 리터럴로 문자열을 생성 시
    • 하나의 문자열을 여러 참조변수가 공유한다.
  • new 연산자를 사용 시
    • 항상 새로운 문자열이 만들어진다.
  • 문자열은 내용변경이 불가하기 때문에 여러 참조변수가 공유해도 문제가 생기지 않는다.
  • 문자열 변수는 "==" 연산자로 비교 시 주소를 비교하기 때문에 값을 비교하기 위해선 equals함수를 써야한다.

문자열 리터럴

  • 문자열 리터럴은 프로그램 실행시 자동으로 생성된다.(constant pool에 저장)
  • constant pool
    • 프로그램에 있는 모든 상수를 저장하는 곳
  • JVM은 문자열 상수 풀에 객체를 생성하고 해당 참조를 스택에 저장한다.
  • JVM은 문자열 상수 풀에 리터럴의 복사본을 하나만 저장하여 문자열에 할당된 메모리 양을 최적화 할 수 있다.
    • interning 이라고함!
    • String변수를 만들고 값을 할당하면 JVM이 상수풀에서 동일한 String을 검색하고, 발견되면 메모리 주소값을 반환하고 그렇지 않으면 풀에 등록하고(interning) 해당 주소값을 반환한다.

StringBuffer클래스, StringBuilder클래스

  • 공통점
    • new 연산으로 클래스를 한 번만 만듬 (Mutable)
    • 문자열 연산시 새로 객체를 만들지 않고, 크기를 변경시킴
    • StringBuffer와 StringBuilder 클래스의 메서드가 동일함.
  • 차이점
    • StringBuffer는 동기화되어 있다. 멀티 쓰레드에 안전함
    • StringBuilder는 동기화 되어 있지 않아서. 멀티 쓰레드에 안전하지 않음.
    • 멀티 쓰레드 프로그램이 아닌 경우, 동기화는 불필요한 성능저하를 일으킨다. 그럴때 StringBuilder를 쓰자.

'Computer science > JAVA' 카테고리의 다른 글

Casting  (0) 2023.01.01
Object Class  (0) 2023.01.01
Serialization(직렬화)  (0) 2022.12.25
오토 박싱 & 오토 언박싱  (0) 2022.12.25
Primitive type & Reference type  (0) 2022.12.24

+ Recent posts