본문 바로가기
Back-end

2023.08.19 TIL

by 신재권 2023. 8. 19.

Java

Set 이란?

Set은 자바 Colleciton 프레임워크로, 중복된 요소를 허용하지 않는 컬렉션입니다.

순서가 정해져 있지 않으며, 각 요소는 고유한 값이어야 합니다.

대표적인 구현체는 HashSet, LinkedHashSet, TreeSet이 있습니다.

Set은 중복을 막기 위해 내부적으로 요소들을 저장할 때 equals()와 hashCode() 메서드를 사용합니다.

중복을 판별하기 위해 equals() 메서드로 객체의 동등성을 비교하고, hashCode() 메서드로 해시값을 계산하여 저장합니다.

장점

  • 중복된 요소를 허용하지 않아 고유한 값만 저장할 수 있습니다.
  • 검색 속도가 빠릅니다.

단점

  • 요소의 순서가 보장되지 않습니다.
  • 추가적인 연산 비용이 들 수 있습니다.

사용해야 하는 경우

  • 고유한 값만을 저장하고, 순서가 중요하지 않을때

구현체별 특징

  • HashSet : 해시테이블을 사용하여 구현되며, 빠른 검색 속도를 제공하지만, 순서가 보장되지 않습니다.
  • LinkedHashSet : 연결된 목록과 해시테이블을 사용하여 구현되며, 요소의 삽입 순서를 유지합니다.
  • TreeSet : 레드-블랙 트리를 사용하여 구현되며, 요소들이 자동으로 정렬되어 자장되며, 검색 속도가 느릴 수 있지만 정렬된 순서를 유지합니다.

즉, 구현체 별로 장단점이 다르기 때문에, 상황에 따라 선택하면 됩니다.

Map이란?

Map은 키-값 쌍을 저장하는 자료구조로, 키는 고유해야 하며 각 키에 대응하는 값이 저장됩니다.

대표적인 구현체는 HashMap, LinkedHashMap, TreeMap, Hashtable 입니다.

Map의 키 중복을 막기 위해 키들 간이 equals()와 hashCode() 메서드를 사용합니다.

키가 중복될 경우, 이전 값을 덮어쓰기도 합니다.

장점

  • 키-값 쌍을 사용하여 데이터를 저장하므로 데이터 검색이 빠릅니다.
  • 키를 기준으로 고유한 값을 저장할 수 있습니다.

단점

  • 순서가 보장되지 않습니다.
  • 추가적인 연산 비용이 들 수 있습니다.

구현체별 특징

  • HashMap : 해시테이블을 사용하여 구현되며, 빠른 검색 속도를 제공하고 순서가 보장되지 않습니다.
  • LinkedHashMap : 연결된 목록과 해시 테이블을 사용하여 구현되며, 요소의 삽입 순서를 보존합니다.
  • TreeMap : 레드 블랙 트리를 사용하여 구현되며, 키들이 정렬되어 저장되며 검색 속도가 느릴 수 있지만, 정렬된 순서를 유지합니다.

HashMap은 키의 해시코드를 사용하여 해당 키와 값을 쌍으로 저장하는데, 동일한 해시코드를 가지는 키들이 있을 수도 있습니다. 이것을 해시충돌이라 합니다.

해시 충돌이 일어나면 동일한 해시코드를 가진 여러 키들이 같은 버킷에 저장됩니다. 이러한 상황을 해결하기 위해 키-값 쌍은 연결리스트로 관리하고 이것을 Separate Chaining 이라합니다.

값 검색 시에도 동일한 해시 코드를 가진 키가 여러개 있을 수 있는데, 먼저 버킷을 찾고, 버킷 내부의 연결 리스트에서 선형 검색을 수행하여 값을 찾습니다.

해시 충돌이 자주 발생하면 성능 저하가 일어날 수 있기 때문에 효율적인 해시 함수 선택 및 충돌 해결 전략이 중요합니다.

스레드란?

하나의 프로세스 내에서 여러 스레드가 수행된다. 역으로 여러 프로세스가 공유하는 하나의 스레드가 수행되지는 않는다. 어떤 프로세스 간에 스레드는 반드시 하다 이상 수행됩니다.

아무런 스레드를 생성하지 않아도 JVM을 관리하기 위한 여러 스레드가 존재합니다.

프로세스가 시작하려면 많은 자원이 필요한데, 하나의 작업을 동시에 수행하려고 하면, 여러 개의 프로세스를 띄어서 실행하면 각각 메모리를 할당해야 한다.

자바로 프로세스를 띄운다는 것은 JVM을 실행한다는 것이고, JVM을 여러개 띄우는거에 비해 스레드는 적은 메모리를 점유한다. 그래서 스레드를 경량 프로세스라고도 한다.

즉 작업을 단일 스레드로 실행하는 것보다는 다중 스레드로 실행하는 것이 더 빠른 시간에 결과를 제공해준다.

Runnable 인터페이스

스레드를 생성하는 방법은 크게 2가지 방법이 존재합니다.

하나는 Runnable 인터페이스를 사용하는 것이고, 다른 하나는 Thread 클래스를 사용하는 것입니다..

Thread는 Runnable 인터페이스를 구현한 클래스입니다.

둘다 java.lang 패키지에 있어 별도로 import 할 필요가 없습니다.

Runnable에 있는 메서드는 run() 메서드 하나입니다.

Thread 클래스는 매우 많은 생성자와 메서드를 제공합니다.

스레드 작업이 수행되는 곳은 run() 메서드 이고, 스레드를 시작하는 메서드는 start() 입니다.

두 가지 방법이 있는 이유는 자바는 다중 상속을 지원하지 않기 때문에, 대체할 수 있는 인터페이스도 존재하는 것입니다.

스레드를 start() 메서드로 실행했다는 것은, 프로세스가 아닌, 하나의 스레드를 JVM에 추가하여 실행하는 것 이기 때문에, 여러 스레드를 실행한다면 메서드를 실행 시키고 끝날때 까지 기다리지 않는다. 또한 순서를 보장하지 않는다. 즉, run() 메서드가 끝나야 스레드가 종료된다.

Thread 클래스

Thread 생성자

  • Thread() : 새로운 스레드 생성
  • Thread(Runnable target) : 매개 변수로 받은 target 객체의 run() 메서드를 수행하는 스레드를 생성
  • Thread(Runnable target, String name) : 위와 동일하며, name 이름을 갖는 스레드를 생성한다.
  • Thread(String name) : name 이름을 갖는 스레드를 생성한다.
  • Thread(ThreadGroup group, Runnable target) : 매개 변수로 받은 group의 스레드 그룹에 속하는 target 객체의 run() 메서드를 수행하는 스레드를 생성한다.
  • Thread(ThreadGroup group, Runnable target, String name, long stackSize) : 매개 변수로 받은 group의 스레드 그룹에 속하는 target 객체의 run() 메서드를 수행하고 name이라는 이름을 갖는 스레드를 생성한다. 해당 스레드의 스택 크기는 stackSize 만큼 할당된다.
  • Thread(ThreadGroup group, String name) : 매개변수로 받은 group의 스레드 그룹에 속하는 name이라는 이름을 갖는 스레드를 생성한다.

모든 스레드는 이름이 있다. 아무런 이름을 지정하지 않으면 해당 스레드 이름은 Thread-n 이다.

n은 스레드가 생성된 순서에 따라 증가한다.

스레드 이름이 겹쳐도 예외는 발생하지 않는다.

스레드를 생성할 때 스레드를 묶을 수 있는데, ThreadGroup을 사용하면 된다.

Thread 클래스의 static 메서드는 주로 해당 스레드를 위해 존재하는 것이 아닌, JVM에 있는 스레드를 관리하기 위한 용도로 사용된다.

run() 메서드가 끝나지 않으면 해당 스레드는 종료되지 않는다.

주요 메서드

  • run() : 스레드 실행 메서드
  • getId() : 스레드 고유 id 리턴, JVM에서 자동 생성
  • getName() : 스레드 이름 리턴
  • setName(String name) : 스레드 이름 지정
  • getPriority() : 스레드 우선순위 확인
  • setPriority(int newPriority) : 스레드 우선순위 지정
  • isDaemon() : 데몬스레드인지 확인
  • setDaemon(boolean on) : 스레드를 데몬으로 설정할지 아닌지 설정
  • getStackTrace() : 스레드의 스택 정보를 확인
  • getState() : 스레드 상태 확인
  • getThreadGroup() : 스레드 그룹 확인

데몬 스레드란?

데몬 스레드가 아닌 사용자 스레드는 JVM이 해당 스레드가 끝날 떄 까지 기다린다. 어떤 스레드를 데몬 스레드로 지정하면, 그 스레드가 수행되고 있든, 수행되지 않고 있든 상관없이 JVM이 끝날 수 있다.

단, 해당 스레드가 시작하기 전 데몬 스레드로 지정해야 한다. 스레드가 시작한 다음은 데몬으로 지정할 수 없다.

모니터링 등 부가적인 작업을 수행하는 스레드를 선언할 때 데몬 스레드를 만든다.

부가적인 작업이 실제 프로세스에 영향을 미치면 안되기 때문이다.

synchronized

synchronized는 스레드와 연관이 깊다.

어떤 클래스나 메서드가 스레드에 안전하려면 synchronized 를 사용해야 한다.

여러 스레드가 한 객체에 선언된 메서드에 접근하여 데이터를 처리하려고 할 때 동시에 연산을 수행하여 값이 꼬이는 경우가 발생할 수도 있다.

메서드에서 인스턴스 변수를 수정하려고할 때만 위 문제가 생긴다. 지역변수에서는 각 스레드가 개별적으로 적용되므로 문제가 생기지 않는다.

synchronized는 두 가지 방법으로 사용이 가능하다.

  • 메서드 자체를 synchronized로 선언
  • 메서드의 특정 문장 자체만 synchronized 감싸는 방법

synchronized 키워드가 있다면 동일한 객체의 이 메서드에 하나의 스레드만 접근이 가능하다.

synchronized 메서드를 전체를 감싸면 메서드 자체에 접근이 1개의 스레드만 가능하기 때문에 동기화가 필요 없는 문장을 수행할 때 필요없는 대기시간이 생길 수도 있다.

이때는 동기화가 필요한 부분(값을 공유하는 부분)만 synchronized(){} 을 사용하여 처리가 가능하다.

또한 블록을 사용할 때는 lock 객체가 필요한데, 각 필요한 만큼 효율적으로 만드는 것이 좋다.

하나만 만든다면, 다른 synchronized 부분에 접근 못할 수도 있기 떄문이다.

synchronized은 여러 스레드에서 하나의 객체에 있는 인스턴스 변수를 동시에 처리할 때 발생할 수 있는 문제를 해결하기 위해 필요한 것이므로, 객체에 있는 인스턴스 변수를 공유할 필요가 없다면 synchronized 키워드도 필요하지 않다.

StringBuffer은 synchronized 으로 동기화 처리가 되어 있어 스레드 안전하고, StringBuilder는 동기화 처리가 되어 있지 않아 스레드 안전하지 않다.

즉, StringBuffer는 하나의 문자열을 여러 스레드에서 공유해야 할 때 사용하고, StringBuilder는 싱글 스레드 환경에서 사용한다.

스레드 통제

스레드를 통제해야 하는 경우도 있다.

스레드는 상태를 가지고 있는데, 이 상태를 토대로 통제가 가능하다.

  • getState() : 스레드 상태 확인
  • join() : 수행중인 스레드가 중지할 때 까지 대기
  • join(long millis) : 매개변수에 지정된 시간만큼 대기
  • join(long millis, int nanos)
  • interrupt() : 수행중인 스레드에 중지 요청

스레드 상태

  • NEW : 스레드 객체는 생성되었지만, 아직 시작되지 않은 상태
  • RUNNABLE : 스레드가 실행 중인 상태
  • BLOCKED : 스레드가 실행 중지 상태이며, 모니터 락이 풀리기를 기다리는 상태
  • WAITING : 스레드가 대기중인 상태
  • TIMED_WAITING : 특정 시간 만큼 스레드가 대기중인 상태
  • TERMINATED : 스레드가 종료된 상태

어떤 스레드든 NEW → 상태 → TERMINATED의 라이프 사이클을 갖는다.

스레드가 시작하기전이나, 종료상태여도 interrupt() 를 수행해도 예외나 에러 없이 다음 문장으로 넘어간다.

만약 실행 중인 스레드를 interrupt() 로 중지하면 InterruptedException을 발생시키면서 중단시킨다.

  • checkAccess() : 현재 수행중인 스레드가 해당 스레드를 수정할 수 있는 권한이 있는지 확인, 없다면 SecurityException 예외 발생
  • isAlive() : 스레드가 살아 있는지 확인한다. 해당 스레드의 run() 메서드가 종료되었는지 안되었는지를 확인
  • isInterrupted() : run() 메서드가 정상적으로 종료되지 않고, interrupt() 메서드의 호출을 통해 종료되었는지 확인하는데 사용된다.
  • static interrupted() :현재 스레드가 중지되었는지 확인
  • static activeCount() : 현재 스레드가 속한 스레드 그룹의 스레드 중 살아 있는 스레드의 개수를 리턴한다.
  • static currentThread() : 현재 수행중인 스레드의 객체를 리턴한다.
  • static dumpStack() : 콘솔 창에 현재 스레드의 스택 정보를 출력

Object 클래스에 선언된 스레드 메서드

  • wait() : 다른 스레드가 object 객체에 대한 notify() 메서드나 notifyAll() 메서드를 호출할 때 까지 현재 스레드가 대기하고 있도록 한다.
  • wait(long timeout)
  • wait(long timeout, int nanos)
  • notify() : Object 객체의 모니터에 대기하고 있는 단일 스레드를 깨운다.
  • notifyAll() : Object 객체의 모니터에 대기하고 있는 모든 스레드를 깨운다.

즉, wait() 메서드를 사용하면 스레드 대기 상태가 되며, notify()나 notifyAll() 메서드를 사용하면 스레드 대기상태가 해제된다.

ThradGroup

ThreadGroup은 스레드 관리를 용이하게 하기 위한 클래스이다.

하나의 애플리케이션에는 여러 종류의 스레드가 있을 수 있으며, ThreadGroup 클래스가 없으면 용도가 다른 여러 스레드를 관리하기 어렵다.

스레드 그룹은 트리 구조를 가진다. 하나의 그룹이 다른 그룹에 속할 수도 있고, 그 아래 또 다른 그룹을 포함할 수 있다.

  • activeCount() : 실행중인 스레드의 개수 리턴
  • activeGroupCount() :실행중인 스레드 그룹의 개수를 리턴
  • enumerate(Thread[] list) : 현재 스레드 그룹에 있는 모든 스레드를 매개변수로 넘어온 스레드 배열에 담는다.
  • enumerate(Thread[] list, boolean recurse) : 두번째 매개변수가 true이면 하위에 있는 스레드 그룹에 있는 스레드 목록도 포함한다.
  • enumerate(ThreadGroup[] list) : 스레드 그룹에 있는 모든 스레드 그룹을 매개변수로 넘어온 스레드 그룹 배열에 담는다.
  • enumerate(ThreadGroup[] list, boolean recurse) : true이면 하위에 있는 스레드 그룹 목록도 포함된다.
  • getName() : 스레드 그룹의 이름을 리턴한다.
  • getParent() :부모 스레드 그룹을 리턴한다.
  • list() : 스레드 그룹의 상세 정보를 출력한다.
  • setDaemon(boolean daemon) :지금 스레드 그룹에 속한 스레드들을 데몬으로 지정한다.

'Back-end' 카테고리의 다른 글

2023.08.20 TIL  (0) 2023.08.21
2023.08.20 TIL  (0) 2023.08.20
static lazy 로딩  (0) 2023.08.19
정적 vs 동적 in Java  (0) 2023.08.18
클래스 로더  (0) 2023.08.18