[Java] 25. Object 클래스 (notify, wait 사용법)


Study/Java  2020. 5. 27. 11:30

안녕하세요. 명월입니다.


이 글은 Java에서 Object 클래스에 대한 글입니다.


예전에 제가 재정의 대한 설명하던 중 hashCode과 equals, toString,에 대해서 간략하게 설명한 적이 있습니다.

링크 - [Java] 11. String의 hashCode과 equals, 그리고 toString의 재정의(override)


Java에서는 최소 단위가 클래스로 이루어져 있습니다. 실행 함수인 main만 실행하려고 해도 클래스를 만들어서 그 안에 static void main으로 실행 함수를 만듭니다.

그런 최소 단위의 클래스는 기본적으로 Object 클래스를 상속받습니다. extents을 하지 않아도 기본적으로 Object를 상속받습니다.

여기서 Java는 다중 상속이 안된다고 했는데, 그럼 다른 클래스를 상속 받을 때 다중 상속이 되지 않을까 하는 의문이 생깁니다만, 모든 클래스가 Object클래스를 상속 받으니 다중 상속이 아니고 그냥 상속의 상속이 된 것 뿐입니다.

그럼 모든 클래스가 Object 클래스를 상속받으니 Object 클래스에 대해서 정리할 필요가 있습니다.

Object는 총 11개의 함수로 구성되어 있습니다. 내부의 맴버 변수는 없습니다.

함수 설명
public final native Class<?> getClass() 객체의 클래스 형을 반환한다.
public native int hashCode(); 객체의 주소값을 반환한다.
public boolean equals(Object obj) 객체의 주소 값을 비교를 하여 같으면 true, 다르면 false를 반환한다.
protected native Object clone() 객체를 복사한다. 단순한 주소 값의 복사가 아닌 클래스 객체를 복사하는 것이다.
클래스 복제를 위해서는 인터페이스 Cloneable를 상속받아야 한다.
public String toString() 현재 객체의 문자열 스트링을 반환한다.
기본값은 getClass().getName() + "@" + Integer.toHexString(hashCode())이다.
public final native void notify() wait된 스레드를 재개한다.
public final native void notifyAll() wait된 모든 스레드를 재개한다.
public final void wait() 스레드를 일시적으로 중지한다.
public final native void wait(long timeoutMillis) 주어진 시간만큼 스레드를 일시적으로 중지한다. 값이 0일 경우는 notify가 호출될 때까지 중지한다.
public final void wait(long timeoutMillis, int nanos) 주어진 시간만큼 스레드를 일시적으로 중지한다. 값이 0일 경우는 notify가 호출될 때까지 중지한다.
protected void finalize() GC(Garbage collection)에서 객체를 삭제하면 호출된다. Java 9 이상에서는 더이상 사용되지 않는다.(Deprecated)

먼저 getClass와 hashCode, equals, clone, toString에 대해 설명하겠습니다.

public class Example implements Cloneable{
  // Object clone은 접근 제한자가 protected이기 때문에 public으로 재정의 
  public Example clone() {
    try {
      // 캐스팅 변환을 해서 리턴한다.
      return (Example) super.clone();
    } catch (CloneNotSupportedException e) {
      // Cloneable를 상속받지 않으면 CloneNotSupportedException 에러가 발생한다.
      return null;
    }
  }
  // 실행 함수
  public static void main(String[] args) {
    // 인스턴스 선언, 모든 클래스는 Object를 상속받기 때문에 Object타입으로 받는 것이 가능하다.
    Object a = new Example();
    // getClass는 Reflection에서 사용되는 Class<?> 타입으로 반환되고 그 Class 타입은 선언된 클래스 힙에 있는 클래스 타입입니다.
    System.out.println("getClass = " + a.getClass().getName());
    // 주소값이 hash code로 출력된다.
    System.out.println("hashCode = " + a.hashCode());
    // 이것은 주소 값을 복사
    Example b = (Example)a;
    // 이것은 클래스를 복사
    Example c = b.clone();
    // b의 hash code, a의 hash code와 값이 같다.
    System.out.println("b hashCode = " + b.hashCode());
    // c의 hash code, clone을 했기 때문에 hash code가 다르다. 
    System.out.println("c hashCode = " + c.hashCode());
    // a와 b는 주소값이 같은 같은 클래스인가?
    System.out.println("a.equals(b) = " + a.equals(b));
    // a와 c는 주소값이 같은 같은 클래스인가?
    System.out.println("a.equals(c) = " + a.equals(c));
    // toString은 getClass().getName() + "@" + Integer.toHexString(hashCode())
    System.out.println("toString = " + a.toString());
  }
}

여기서 주의해야 할 것은 equals와 연산자 ==는 hashCode의 값을 보는 것입니다. 즉, 클래스 안의 데이터가 같아도 할당된 클래스가 다르면 false를 리턴합니다.

참고로 String의 경우는 hashCode가 재정의되어 있기 때문에 hashCode 함수는 반드시 메모리 참조 값은 아닙니다. 즉, hashCode를 재정의하면 다른 값을 리턴하는 함수가 됩니다.


그리고 notify와 wait의 기능입니다.

예전에 스레드에 대해서 설명한 적이 있습니다.

링크 - [Java] 22. 스레드(Thread)를 사용하는 방법

링크 - [Java] 24. 동기화(synchronized) 그리고 교착 상태(Deadlock)

이 스레드 환경에서 값을 동기화 할 때 synchronized를 많이 사용합니다. 그런데 이 동기화를 좀 더 세밀하게 프로그래밍을 할 때가 있습니다.

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

public class Example implements Runnable {
  // 날짜 포멧
  private DateFormat format = new SimpleDateFormat("hh:MM:ss");
  // print 동기화 함수
  public synchronized void print() {
    try {
      // 콘솔 출력 현재 날짜.
      System.out.println(Thread.currentThread().getName() + " " + format.format(new Date()));
      // wait을 하면 다른 synchronized가 풀리게 된다.
      // 즉, synchronized 내부에서 wait을 했지만 synchronized가 잡혀있는 상태가 아닌 lock이 해제되는 상태로 변한다.
      super.wait();
      // 콘솔 출력
      System.out.println(Thread.currentThread().getName() + " wait");
    } catch (InterruptedException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
  }
  // 실행
  @Override
  public void run() {
    // 반복문
    for (int i = 0; i < 10; i++) {
      // 동기화 영역
      synchronized (this) {
        // 콘솔 출력
        System.out.println(Thread.currentThread().getName() + " notify ");
        // notify를 호출한다.
        notify();
      }
      // print 함수 호출
      print();
    }
  }
  // 실행 함수
  public static void main(String[] args) {
    Example a = new Example();
    // 두개의 스레드에 a 인스턴스를 실행한다.
    new Thread(a).start();
    new Thread(a).start();
    try {
      // 마지막은 wait으로 끝나기 때문에
      Thread.sleep(2000);
      synchronized (a) {
        // 모든 스레드를 깨워서 종료해야 한다.
        a.notifyAll();
      }
    } catch (Throwable e) {
      e.printStackTrace();
    }
  }
}

위 보시면 두개의 스레드가 차례로 호출되는 것을 확인할 수 있습니다.

먼저 Thread1에서 먼저 notify를 하고 print 함수로 호출되어 콘솔에 출력이 됩니다. 그리고 synchronized 스택 영역 안에서 wait를 호출합니다. 참고로 notify와 wait은 synchronized 스택 영역 안에서 호출하는 함수입니다.

그 사이에 Thread2는 for의 synchronized (this)의 대기 상태에 있을 것입니다. Thread1에서 wait을 호출하면 lock은 풀리고 그 상태로 스래드는 멈춥니다. 여기서 Thread2가 synchronized (this)로 진입하고 notify를 호출합니다.

Thread1에서 멈춰있던 스레드는 깨어나고 그대로 synchronized에 의해 대기 상태에 들어갑니다. 다시 Thread2에서 wait이 호출되면 바로 스레드는 정지하고 Thread1이 움직이게 됩니다.


notify와 notifyAll의 차이는 notify는 큐의 순서에 따라 가장 앞에 wait된 스레드 한개가 깨어나는 것이고 notifyAll는 모든 스레드가 깨어납니다.

wait에 시간을 두고 멈추는 상태를 할 수 있습니다.

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

public class Example implements Runnable {
  // 날짜 포멧
  private DateFormat format = new SimpleDateFormat("hh:MM:ss");
  // print 동기화 함수
  public synchronized void print() {
    try {
      // 콘솔 출력 현재 날짜.
      System.out.println(Thread.currentThread().getName() + " " + format.format(new Date()));
      // wait을 하면 다른 synchronized가 풀리게 된다.
      // 즉, synchronized 내부에서 wait을 했지만 synchronized가 잡혀있는 상태가 아닌 lock이 해제되는 상태로 변한다.
      // 대기 시간을 넣으면 notify를 하지 않아도 일정 시간 후에 스레드는 깨어난다.
      super.wait(1000);
    } catch (InterruptedException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
  }
  // 실행
  @Override
  public void run() {
    // 반복문
    for (int i = 0; i < 10; i++) {
      // print 함수 호출
      print();
    }
  }
  // 실행 함수
  public static void main(String[] args) {
    Example a = new Example();
    // 두개의 스레드에 a 인스턴스를 실행한다.
    new Thread(a).start();
    new Thread(a).start();
  }
}

Thread.sleep와 비슷합니다만 wait은 다른 동기화에 영향이 가는 함수이기 때문에 차이가 있습니다.

참고 - https://docs.oracle.com/javase/7/docs/api/java/lang/Object.html


여기까지 Java에서 Object 클래스에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.