코딩딩/Java

ArrayList, Generics, 멀티 쓰레딩, 임계영역을 이용한 동기화, 쓰레드 사이의 통신을 이용한 동기화

전낙타 2023. 7. 8. 21:55

 

학습 목표

 

  • ArrayList 클래스에 대해 설명할 수 있다.
  • Generics에 대해 설명할 수 있다.
  • 멀티 쓰레딩(multi-threading)에 대해 설명할 수 있다.
  • 임계영여(critical section)을 이용한 동기화(synchronization)에 대해 설명할 수 있다.
  • 쓰레드 사이의 통신을 이용한 동기화에 대해 설명할 수 있다.

 

학습 내용

 

ArrayList 클래스

 

Collections 클래스는 오브젝트 데이트의 관리를 지원하는 클래스이며, ArrayList 클래스는 그 중 하나를 말함

오브젝트들을 연결구조로 저장 가능, 배열과 달리 크기가 사용에 따라 자동적으로 조절됨

오브젝트 생성시 <> 안에 저장한 데이터의 자료형을 지정함

버젼 6 이후에서는 <자료형>을 생략해도 됨

자료 구조에서 다루는 linked list 구조와 유사함

index는 0부터 시작

 

ArrayList 클래스에서 사용할 수 있는 주요 메소드는 다음과 같음

파이썬의 list와 아주 유사하다, 또 자바의 배열과는 다른 개념이라고 한다.

그래서 요소를 배열에 저장하는 메서드가 제공되는 것

 

  • 차이점 알아보기
    1. 배열(Array): 배열은 고정 크기의 연속된 메모리 블록으로 구성됩니다. 자바에서는 원시 타입(int, double, char 등)과 객체 타입(Object) 모두를 배열로 선언할 수 있습니다. 배열은 선언 시에 크기를 지정하며, 크기는 변경할 수 없습니다. 즉, 한 번 배열을 생성한 후에는 크기를 동적으로 조절할 수 없습니다. 배열은 인덱스를 사용하여 각 요소에 직접 접근할 수 있습니다. 배열은 빠른 접근 속도를 가지지만, 크기가 고정되고 중간에 요소를 삽입하거나 삭제하는 것이 비효율적입니다.
    1. ArrayList: ArrayList는 자바의 컬렉션 프레임워크에 속하는 동적 배열 구조입니다. ArrayList는 내부적으로 배열을 사용하여 요소를 저장하며, 크기를 동적으로 조절할 수 있습니다. 요소를 추가하거나 삭제할 때, ArrayList는 내부 배열의 크기를 조정하고 요소를 복사하는 작업을 수행합니다. ArrayList는 배열에 비해 크기 조절이 유연하며, 중간에 요소를 삽입하거나 삭제하는 것이 효율적입니다. 또한, ArrayList는 제네릭을 사용하여 다양한 타입의 요소를 저장할 수 있습니다.

    따라서, 배열은 크기가 고정되고 요소를 중간에 삽입/삭제하기 어려운 반면, ArrayList는 크기가 동적으로 조절 가능하고 요소의 삽입/삭제가 편리한 동적 배열 구조입니다.

 

예시

 

Generics 제네릭

 

클래스의 정의 내에서 이용되는 자료형을 미리 정해놓지 않고 파라미터(인수)로 처리함

오브젝트를 생성하기 전에는 자료형을 추상적으로 표현하고 오브젝트 생성시 구체적인 자료형을 인수로 전달받아 지정함

객체를 생성함과 동시에 자료형을 정해주는것

미리 자료형을 지정해 두면, 콜렉션 클래스의 요소를 참조할 때 캐스팅을 피할 수 있음 (형 변환 과정을 생략할 수 있음)

 

예시

// 전체코드

class CName<T>{
    private T t; // 제네릭 타입 필드
    void set(T t){ // 제네릭 타입의 파라미터 메소드
        this.t = t;
    }
    T get() { // 제네릭 타입의 반환 메소드
        return t; // int가 들어오면 int를 반환 String이 들어오면 String을 반환
    }
    // static이 붙은 경우 T 타입은 제네릭 클래스의 T 타입과 다른 독립적인 타입임
    // <T>는 매개인자고 T타입은 인수의 형식에 따라 결정됨
    static <T> T SMethod(T obj){
        return obj;
    }
// 제네릭 타입이 들어간 static 메서드는 오버로딩을 지원하지 않음
//    T SMethod(T ob){
//        return ob;
//    }
}


public class GenericsTest {
    public static void main(String[] args) {

        CName<String> obs = new CName<String>(); // 생성자 T에 Stirng 지정
        CName<Integer> obn = new CName<Integer>(); // 생성자 T에 Int 지정

        obs.set("123"); // 각각 string과 int만 넣을 수 있음
        obn.set(123);

        // 반환된 변수가 어떤 타입인지 출력
        System.out.println("obs field = " + obs.get());
        System.out.println("obs <T> Type = " + obs.get().getClass().getName());

        // 반환된 변수가 어떤 타입인지 출력
        System.out.println("obn field = " + obn.get());
        System.out.println("obn <T> Type = " + obn.get().getClass().getName());

        // 제네릭 정적 메소드의 T는 String
        System.out.println("SMethod <T> Type = " + CName.SMethod("ABC").getClass().getName());

        // 제네릭 정적 메소드의 T는 Integer
        System.out.println("SMethod <T> Type = " + CName.SMethod(123).getClass().getName());

        // 제네릭 정적 메소드의 T는 CName
        System.out.println("SMethod <T> Type = " + CName.SMethod(obs).getClass().getName());

        // 제네릭 정적 메소드의 T는 Double
        System.out.println("SMethod <T> Type = " + obs.SMethod(123.456).getClass().getName());
    }
}

 

멀티 쓰레딩(multi-threading)

 

시작점과 종료점을 갖는 하나의 작업흐름

프로그램 실행시 main 메소드가 호출되어 하나의 작업흐름(메인 쓰레드)이 시작됨

쓰레드가 하나인 프로그램을 싱글 쓰레드 프로그램이고, 여러개의 쓰레드를 갖는 경우 멀티 쓰레드 프로그램이라 함

멀티 쓰레드는 CPU를 시분할해서 작동하기 떄문에 여러 개의 쓰레드를 동시에 작업하는 것처럼 보임 (

자바에서 쓰레드를 생성하기 위해서는 Thread 클래스를 상속받거나, Runnable 인터페이스를 구현해야함

 

Thread 클래스를 이용한 멀티 쓰레딩

 

  1. 쓰레드를 상속받는 클래스를 만듦
  1. run() 메소드를 오버라이딩 해서 내용을 작성함
  1. main 메소드에서 쓰레드를 상속받는 클래스의 오브젝트를 생성함
  1. 해당 오브젝트의 start() 메소드를 호출함

 

예시

// 전체코드

public class ThreadTest extends Thread {
    private String word;
    private int time;
    private int count;
    public ThreadTest(String w, int t, int c){ // 생성자
        word = w;
        time = t;
        count = c;
    }
    public void run(){
        for (int n = 0; n < count; n++) {
            System.out.println(word);
            try{
                Thread.sleep(time); // time 밀리초 만큼 현재 쓰레드 대기
            }
            catch (Exception e) {
            }
        }
    }
    public static void main(String[] args) {
        ThreadTest tick = new ThreadTest("tick",1000,3); // 출력 예상 틱텍틱텍틱
        ThreadTest tack = new ThreadTest("tack",1300,2);
        
        tick.start();
        tack.start();
    }
}

두개의 쓰레드가 동시에 작동하는 모습을 확인할 수 있다.

 

Runnable 인터페이스를 이용한 멀티 쓰레딩

 

  1. Runnable 인터페이스를 구현하는 클래스를 만듦
  1. run() 메소드를 오버라이딩해서 내용을 작성함
  1. main() 메소드에서 Runnable 인터페이스를 구현한 클래스의 오브젝트를 생성함
  1. 3의 오브젝트를 Thread 생성자의 인수로 해서 Thread 오브젝트를 생성함
  1. Thread 오브젝트의 start() 메소드를 호출함

 

// 전체코드

public class ThreadTest2 implements Runnable {
    private String word;
    private int time;
    private int count;
    public ThreadTest2(String w, int t, int c){ // 생성자
        word = w;
        time = t;
        count = c;
    }
    public void run(){
        for (int n = 0; n < count; n++) {
            System.out.println(word);
            try{
                Thread.sleep(time); // time 밀리초 만큼 현재 쓰레드 대기
            }
            catch (Exception e) {
            }
        }
    }
    public static void main(String[] args) {
        ThreadTest tick = new ThreadTest("tick",1000,3); // 출력 예상 틱텍틱텍틱
        ThreadTest tack = new ThreadTest("tack",1300,3);
        ThreadTest tock = new ThreadTest("tock",1500,3);

				Thread tickThread = new Thread(tick);
        Thread tackThread = new Thread(tack);
        Thread tockThread = new Thread(tock);

        tickThread.start();
        tackThread.start();
        tockThread.start();
    }
}

 

  • 예제를 작성하다 든 의문점
     public static void main(String[] args) {
            ThreadTest tick = new ThreadTest("tick",1000,3); // 출력 예상 틱텍틱텍틱
            ThreadTest tack = new ThreadTest("tack",1300,3);
            ThreadTest tock = new ThreadTest("tock",1500,3);
    
    				Thread tickThread = new Thread(tick);
            Thread tackThread = new Thread(tack);
            Thread tockThread = new Thread(tock);
    
            tickThread.start();
            tackThread.start();
            tockThread.start();
        }

    이 부분의 코드를

    public static void main(String[] args) {
            ThreadTest tick = new ThreadTest("tick",1000,3); // 출력 예상 틱텍틱텍틱
            ThreadTest tack = new ThreadTest("tack",1300,3);
            ThreadTest tock = new ThreadTest("tock",1500,3);
    
            tick.start();
            tack.start();
            tock.start();
        }

    이런 식으로 작성해도 같은 동작을 수행하더라

     

    그래서 gpt 선생에서 여쭤봤다.

    • GPT 선생 왈

      Thread tickThread = new Thread(tick);, Thread tackThread = new Thread(tack);, Thread tockThread = new Thread(tock); 부분은 생략할 수 있습니다.

      주어진 코드에서 tick, tack, tock 인스턴스는 Runnable을 구현한 ThreadTest2 클래스의 인스턴스입니다. Thread 클래스의 생성자에 Runnable을 매개변수로 전달하여 새로운 스레드를 생성할 수 있습니다.

      하지만 Runnable 인터페이스를 구현한 객체를 Thread 객체의 생성자에 바로 전달하고 start()를 호출하는 방식을 사용하면 더 간결하게 코드를 작성할 수 있습니다. 이렇게 하면 중간에 Thread 객체를 변수에 할당할 필요가 없습니다.

      따라서, Thread tickThread = new Thread(tick);, Thread tackThread = new Thread(tack);, Thread tockThread = new Thread(tock); 부분을 생략하고 바로 tick.start();, tack.start();, tock.start();를 호출하는 것도 가능합니다. 이렇게 하면 동일한 동작을 수행할 수 있습니다.

       

      라고 하신다.

     

 

마찬가지로 쓰레드가 동시에 작동하는 모습을 확인할 수 있다.

 

데몬(daemon) 쓰레드/독립 쓰레드

 

데몬 쓰레드는 main 쓰레드가 종료되면, 자신의 실행을 멈추고 main쓰레드와 함께 종료됨

독립 쓰레드는 끝까지 실행함

쓰레드를 데몬 쓰레드로 설정하려면 setDaemon(true) 메소드를 실행하고, 독립 쓰레드로 설정하려면 setDaemon(false) 메소드를 실행함

 

예시

// 전체 코드

class A extends Thread {
    public void run() {
        System.out.println("Daemon Thread A 시작");
        try {
            Thread.sleep(700);
        }
        catch (Exception e) {
        }
        System.out.println("Daemon Thread A 끝");
        System.out.println(getName());
    }
}
class B extends Thread {
    public void run() {
        System.out.println("독립 Thread B 시작");
        try {
            Thread.sleep(5000);
        }
        catch (Exception e) {
        }
        System.out.println("독립 Thread B 끝");
    }
}
public class DaemonThreadTest {
    public static void main(String[] args) {
        System.out.println("main Thread 시작");

        A threadA = new A();
        B threadB = new B();

        threadA.setDaemon(true);
        threadB.setDaemon(false);

        threadA.start();
        threadB.start();

        try{
            Thread.sleep(500);
        }
        catch (Exception e){
        }
        System.out.println("main Thread 끝");
    }
}

Daemon 쓰레드는 main 쓰래드가 끝남과 동시에 끝나지만 독립 쓰레드는 끝까지 실행을 마친다.

 

임계영역(critical section)을 이용한 동기화(synchronization)

 

여러 쓰레드가 공유하는 데이터를 상호 배타적으로 접근하기 위한 방법

임계 영역이란 여러 쓰레드가 접근 가능한 영역이면서, 한 순간에는 하나의 쓰레드만 사용할 수 있는 영역임

자바에서는 임계영역 지정을 위한 synchronized 메소드를 제공함

쓰레드는 임계영역인 synchronized 메소드에 들어가면 lock 을 얻고 벗어나면 lock을 양보함

하나의 쓰레드가 synchronized 메소드를 실행중이면 다른 쓰레드는 lock이 양보될 때까지 대기해야함

(학교 급식실)

 

임계영역을 이용한 멀티 쓰레드 프로그램 구성

 

  1. 임계영역을 갖는 클래스 작성
  1. 멀티 쓰레드를 갖는 클래스 작성(임계영역을 인자로 하는 생성자를 갖음)
  1. 메인 메소드를 갖는 클래스 작성
  1. 메인 메소드에서 임계영역을 갖는 클래스의 오브젝트 생성
  1. 메인 메소드에서 (멀티) 쓰레드를 갖는 클래스의 오브젝트 생성
  1. 메인 메소드에서 (멀티) 쓰레드를 갖는 오브젝트에 임계영역 전달
  1. 메인 메소드에서 (멀티) 쓰레드를 start() 메소드를 실행해서 시작
  1. 멀티 쓰레드에서 run 메소드 실행, sleep 메소드를 이용해서 다른 쓰레드 실행기회 제공
  1. 메인 메소드에서 모든 쓰레드 종료를 join 메소드를 실행해서 확인

 

농구 예제 프로그램

 

5명의 선수가 게임에서 램덤하게 골을 넣고 총 20점 이상이 되면 게임 종료하는 프로그램을 작성함

 

예제

// 전체 코드

package ch13;

class Point {
    private int total = 0; // 임계영역 특성상 private으로 함
    synchronized void goalin(int point){ // 임계영역
        total = total + point;
    }
    int gettotal(){
        return total;
    }
}
class Player extends Thread{
    Point goal; // goal과 main에서 생성된 임계 영역 아직 연결 안됨
    Player (Point number){
        goal = number; // 임계영역과 연결됨
    }
    public void run(){
        try {
            for(int n=0; n<10;n++){
                int pt = (int)(Math.random()*10)%3+1; // 점수생성
                goal.goalin(pt); // 임계영역에 점수 전달
                System.out.println("선수("+getName()+") = "+pt+"점");

                int sleep_time = (int)(Math.random()*10); // 점수생성
                sleep(sleep_time); // 다른 쓰레드 실행을 위해 잠시 대기
                if (goal.gettotal() >=20) break;
            }
        }
        catch (Exception e) {
            System.out.println(e);
        }
    }
}
public class Basketball {
    public static void main(String[] args) {
        Point score = new Point(); // 임계영역을 갖는 오브젝트 생성
        Player [] player = new Player[5]; // 배열을 선언함

        for(int n = 0; n < 5; n++){
            player[n] = new Player(score); // player 오브젝트 생성(임계영역 전달)
            player[n].start();
        }
        for (int n = 0; n < 5; n++) {
            try {
                player[n].join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("총 득점은 : " + score.gettotal() + "점");
    }
}

 

쓰레드 사이의 통신을 이용한 동기화

 

응용 분야에 따라 여러개의 쓰레드가 임계영역을 반드시 교대로 수행하기 위해서는 쓰레드 사이에 통신이 필요함

 

예시

생산자 vs 소비자

  • 생산자는 사과를 상자에 놓고 사과깃발을 올린 후 대기하던 소비자에게 알림
    • 생산자는 사과가 상자에 있으면 대기
    • 소비자가 사과를 가져가고 빈 깃발을 올릴때까지 대기
  • 소비자는 사과를 가져가고 빗 깃발을 올려 대기하던 생산자에게 알림
  • 생산자가 아직 사과를 생산하지 못해 빈 상자면 소비자는 대기

 

예시2

데이터 생산지(쓰레드 1)가 데이터 창고(버퍼)에 데이터를 저장한 후에만 데이터 소비자(쓰레드 2)가 데이터를 가져가는 경우

데이터 생산속도와 데이터 소비속도가 다르기 때문에 생산자가 데이터를 창고에 저장했다는 것을 소비자에게 알려주어야 함

두개의 쓰레드 사이에 통신이 필요하게 됨

wait() 메소드를 실행하는 쓰레드는 실행이 중지되어 무한 대기 상태이므로 다른 쓰레드가 notify() 또는 notifyAll() 메소드를 실행해서 깨워야 함

일반적으로 notify() 또는 notifyAll() 메소드는 synchronized 메소드 내에서 호출함

 

생산자 vs 소비자 예제 프로그램

// 전체 코드

class Buffer {
    private int contents;
    private boolean available = false; // 데이터 유무 플래그

    public synchronized void put(int value){ // 임계영역
        while (available == true){
            System.out.println("창고가 찼음. 생산자 : 대기");
            try{
                wait(); // 다른 쓰레드에서 notify()를 실행해 줄 때까지 대기
            }
            catch(InterruptedException e){}
        }
        contents = value;
        available = true; // 창고에 데이터 있음을 알림
        System.out.println("생산자 : 생산 "+contents);
        notify(); // 대기 상태의 쓰레드에게 신호를 보냄
    }
    public synchronized int get(){ // 임계영역
        while (available == false){
            System.out.println("창고가 비었음. 소비자 : 대기");
            try{
                wait(); // 다른 쓰레드에서 notify()를 실행해 줄 때까지 대기
            }
            catch(InterruptedException e){}
        }
        System.out.println("소비자 : 소비 "+contents);
        available = false; // 창고에 데이터 있음을 알림
        notify(); // 대기 상태의 쓰레드에게 신호를 보냄
        return contents;
    }
}
class Producer extends Thread {
    private Buffer b;
    public Producer(Buffer blank){ // 생성자
        b = blank; // 임계영역과 연결됨
    }
    public void run(){
        for (int i = 1; i <= 5; i++) {
            b.put(i);
        }
    }
}
class Consumer extends Thread {
    private Buffer b;
    public Consumer(Buffer blank){ // 생성자
        b = blank; // 임계영역과 연결됨
    }
    public void run(){
        int value = 0;
        for (int i = 1; i <= 5; i++) {
            value = b.get();
        }
    }
}

public class ProducerConsumer {
    public static void main(String[] args) {
        Buffer buff = new Buffer(); // 임계영역 갖는 오브젝트 생성
        Producer pro = new Producer(buff); // 오브젝트 생성(임계영역 전달)
        Consumer con = new Consumer(buff); // 오브젝트 생성(임계영역 전달)

        pro.start();
        con.start();
    }
}

생산자와 소비자 사이의 통신이 이루어지는 모습이다.