자료구조와 멀티쓰레드

hyunn·2021년 8월 10일
0

Java-basic

목록 보기
26/26
post-thumbnail

자료구조

이전에는 Object type만 사용 - 자료구조의 내용물을 알기 어려움

=> 자료구조 내에 들어가는 자료의 type을 제한! = 제네릭


제네릭

public class GenericEx {
  public static void main(String[] args) {
    //    ArrayList list = new ArrayList(); //이전의 사용 - 기본자료형 사용 불가
    ArrayList<String> list = new ArrayList(); //문자열만 넣고싶을때
    list.add("AAA");
    //list.add(new Integer(1)); 
    //위에서 문자열로 type을 제한했기때문에 Integer에 사용불가표시뜬것 = 제네릭

    //제네릭 = 타입제한
  }
}

public class Sample1<E> { //클래스자체를 제네릭화
    
  private E inner;
  //E = type을 나중에 결정하고싶을때

  public static void main(String[] args) {

    Sample1<String> obj = new Sample1(); //E = String 문자열로 지정해준것
    obj.inner = "AAA"; //문자열을 넣었을때 에러X

    Sample1<Integer> obj2 = new Sample1(); //E = String 문자열로 지정해준것
    obj2.inner = new Integer(10); //가능은한데 취소선왜지

    //똑같은 객체를 그때그때 필요한 type에 맞춰서 사용가능 
    // = 제네릭의 진짜 용도 = 유연한 코드 생성 가능
    //하지만 직접 제네릭 선언하는 경우는 거의 없음 - 알아만두기
  }

}

제네릭의 용도

보통 제네릭(Generic)은 클래스 내부에서 지정하는 것이 아닌 외부에서 사용자에 의해 지정되는 것을 의미

1. 특정 타입만 입력 가능하도록 제한

2. 아직 타입을 특정하지 않았을때 나중에 지정해줄 수 있도록 열어두는? 개념

하지만 2번은 잘 사용하지 않아서 보통 1번기능 타입제한자로 사용함
보통 HashMap을 만들때도 제네릭을 사용해서 특정 타입만 입력 가능하도록 제한



제네릭이용해 쪽지 List 출력

public class ListTest {
    public static void main(String[] args) {
        //기본자료형은 안되고 Object만 가능함
        //이 list 내에는 NoteDTO type만 들어갈 수 있음
        ArrayList<NoteDTO> list = new ArrayList<>();

        //배열의 경우 추가할때 인덱스 번호 알아야만 추가가능
        // ArrayList는 순서 따로 없어서 그냥 추가하면됨

        NoteDTO obj = NoteDTO.builder().build();
        list.add(obj);
        //위의 두 코드를 줄여서 아래와 같이 표현
        list.add(NoteDTO.builder().no(1).who("A").whom("B").build());
        list.add(NoteDTO.builder().no(2).who("A").whom("B").build());
        list.add(NoteDTO.builder().no(3).who("B").whom("A").build());
        list.add(NoteDTO.builder().no(4).who("B").whom("A").build());
        list.add(NoteDTO.builder().no(5).who("A").whom("B").build());

        //몇개나 찼는지 알 수 있음 (배열의 length와 같은 역할)
        System.out.println(list.size());

        //특정 값만 뽑고싶을때 = filter사용 true인 값만 추출
        //현재 쪽지프로그램에서는 특정 사용자에게 도착한 쪽지만 추출할 때 사용하면됨
        List<NoteDTO> blist
                = list.stream().filter(noteDTO -> noteDTO.getWhom().equals("B")).collect(Collectors.toList());

        System.out.println(blist);
    }
}

결과

5
[NoteDTO(no=1, who=A, whom=B, content=null), NoteDTO(no=2, who=A, whom=B, content=null), NoteDTO(no=5, who=A, whom=B, content=null)]

쪽지 삭제

ArrayList에는 remove 기능 있음

public class ListTest {
    public static void main(String[] args) {
        ArrayList<NoteDTO> list = new ArrayList<>();
        list.add(NoteDTO.builder().no(1).who("A").whom("B").build());
        list.add(NoteDTO.builder().no(2).who("A").whom("B").build());
        list.add(NoteDTO.builder().no(3).who("B").whom("A").build());
        list.add(NoteDTO.builder().no(4).who("B").whom("A").build());
        list.add(NoteDTO.builder().no(5).who("A").whom("B").build());
        System.out.println(list.size());
        
        //삭제
        //ArrayList에는 remove기능이 있음
        list.remove(2); //3rd
        System.out.println(list);
        System.out.println(list.size());
    }
}

remove와 인덱스 번호를 이용해서 list의 3번째 data 삭제


결과

5
[NoteDTO(no=1, who=A, whom=B, content=null), NoteDTO(no=2, who=A, whom=B, content=null), NoteDTO(no=4, who=B, whom=A, content=null), NoteDTO(no=5, who=A, whom=B, content=null)]
4

속도면에서 비교

평소에는 ArrayList가 더 잘 쓰이지만

삭제같은 경우엔 ArrayList보다는 LinkedList가 더 빨라서 많이 쓰임






Thread

public class Ex1 {
  public static void main(String[] args) {
    //자바의 모든 프로그램은 쓰레드를 통해 실행
    //기본적으로 싱글쓰레드 쓰레드1개 = main
    //쓰레드는 객체가 아님
    
    //Thread.currentThread().getName() 현재 실행되고있는 쓰레드의 이름을 출력
    //기본은 main Thread 이기때문에 루프A, B 둘 다 똑같이 출력된다

    //자바에서는 쓰레드를 코드를 통해 새로 생성가능함

    //Multi-Thread
    //매번 CPU의 상황이 달라져서 실행순서가 달라진다 -> 막 섞여서 실행됨
    //멀티쓰레드는 실행의 순서를 예측할 수 없다

    //근데 거의 다 main이 먼저 실행됨 

    new Thread(() -> {
      for (int i = 0; i < 10; i++) {
        System.out.print(Thread.currentThread().getName() + "A" + i + "  ");
      }
    }).start();


    for (int i = 0; i < 10; i++) {
      System.out.print(Thread.currentThread().getName() + "B" + i + "  ");
    }
  }
}

Thread.sleep

public class Horse {
  private String name; //말이름
  private int pos; //말위치

  //모든 말의 포지션은 0부터 시작 이름만 다름
  public Horse(String name) {
    this.name = name;
    this.pos = 0;
  }

  public void gallop() {
    for (int i = 0; i < 100; i++) {

      int range = (int)(Math.random()*10) + 1;

      this.pos += range; //말의 위치가 경력

      System.out.println(this.name + ": " + this.pos);

      try {
        Thread.sleep(100);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      //Thread.sleep - Thread를 쉰다 =  CPU를 차지하는 것을 쉬겠다는 의미
    }//end for
  }
}

Race Condition

여러개의 thread가 서로 CPU를 점유하려고 경쟁하는 것

Thread.sleep 실행되는 동안 해당 thread는 CPU점유를 쉼

-> 다른 thread가 CPU점유

-> 서로 CPU를 점유하려고 경쟁하는 것 race condition






Multi-Thread

멀티쓰레드 예제 - 경마

public class Horse extends Thread{
  private String name; //말이름
  private int pos; //말위치

  //모든 말의 포지션은 0부터 시작 이름만 다름
  public Horse(String name) {
    this.name = name;
    this.pos = 0;
  }

  @Override
  public void run() {
    this.gallop();      
  }

  public void gallop() {
    for (int i = 0; i < 10; i++) {
      int range = (int)(Math.random()*10) + 1;
      
      this.pos += range; //말의 위치가 경력
      System.out.println(this.name + ": " + this.pos);

      try {
        Thread.sleep(100);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      //Thread.sleep - Thread를 쉰다 =  CPU를 차지하는 것을 쉬겠다는 의미
    }//end for
  }

  public static void main(String[] args) {
    Horse h1 = new Horse("가");
    Horse h2 = new Horse("나");
    Horse h3 = new Horse("다");
    Horse h4 = new Horse("라");

    h1.start();
    h2.start();
    h3.start();
    h4.start();
  }
}

for 문이 돌면서 1에서 10 사이의 랜덤숫자 하나가 나오고 그 숫자가 pos에 누적되어 말들의 위치를 나타내는 경마프로그램
각 말을 나타내는 Horse에 Thread.start()를 통해 시작하게 하고 thread.sleep(100)을 통해 0.1초의 휴식을 주어 그 시간동안 다른 쓰레드가 시작하도록 함


결과

나: 4
라: 3
다: 7
가: 5
나: 14
가: 11
다: 12
라: 9
나: 20
다: 22
가: 20
라: 15
나: 30
다: 23
...



멀티쓰레드 상황에서 Thread 간섭

  public void gallop() {
    for (int i = 0; i < 10; i++) {
      int range = (int)(Math.random()*10) + 1;

      this.pos += range; //말의 위치가 경력
      int count = this.pos/10;
      for(int j = 0; j < count; j++) {
        System.out.print("*");
      }      

      System.out.println(this.name + ": " + this.pos);
      try {
        Thread.sleep(100);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }//end for
  }

누적된 pos의 수에 /10 을 해서 그 숫자만큼 *이 찍히도록함
하지만 원하는 대로 출력이 안되고 출력되는 중간에 다른 쓰레드가 시작하여 쓰레드끼리 간섭현상이 일어나는 것을 볼 수 있음


결과

*라: 15
가: 9
*나: 6
다: 15
****라: 19
*나: 13
다: 25
가: 18
*********나: 23
다: 35
라: 25
가: 24
***********나: 29
가: 25
다: 43
라: 31
*********라: 38
나: 36
*****가: 34
다: 53
*****************라: 45
...

하나의 thread 작업이 채 끝나기 전에 다른 thread가 실행되어 서로 간섭이 일어나 원하는 작업을 다 끝내지 못하게됨



synchronized - Thread 간섭 해결

public void gallop() {
    for (int i = 0; i < 10; i++) {
      int range = (int)(Math.random()*10) + 1;
      this.pos += range; //말의 위치가 경력

      //syschronize를 통한 동기화로 쓰레드사이 간섭이 일어나는 것을 방지  
      //synchronized 블럭으로 특정 코드를 임계영역으로 설정할 수 있음
      synchronized(System.out) {
        int count = this.pos/10;
        for(int j = 0; j < count; j++) {
          System.out.print("*");
        }      
        System.out.println(this.name + ": " + this.pos);
      }

      try {
        Thread.sleep(100);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }//end for
  }

결과

가: 4
다: 4
라: 9
나: 1
*라: 15
가: 8
나: 3
*다: 14
가: 9
**라: 22
*다: 16
나: 5
**라: 27
*가: 17
**다: 25
*나: 10
***다: 34
*나: 17
***라: 36
**가: 20
**나: 21
**가: 27
...

synchronize를 사용하게되면 쓰레드 하나가 진행되는 동안 다른 쓰레드의 접근을 막아 원하는 작업이 완전하게 끝나지 못하는 현상을 방지할 수 있음

동기화는 특정 메소드에도 설정할 수 있지만 이렇게 특정 코드를 임계영역으로도 선언 가능함






Multi-Thread 이용한 채팅프로그램 완성

OneToOneServer Client

public class OneToOneServer {
  //bad code
  public static void main(String[] args) throws Exception{
    ServerSocket server = new ServerSocket(9999);
    System.out.println("Ready...");

    Socket client = server.accept();

    DataInputStream din = new DataInputStream(client.getInputStream());

    while(true) {
      String clientMsg = din.readUTF();
      System.out.println(clientMsg);
    }
  }
}

public class OneToOneClient {
  //bad code
  public static void main(String[] args) throws Exception{
    Scanner keyScanner = new Scanner(System.in);
    Socket socket = new Socket("192.168.50.4", 9999);
    DataOutputStream dos = new DataOutputStream(socket.getOutputStream());

    while(true) {
      System.out.println("INSERT YOUR MESSAGE");
      String msg = keyScanner.nextLine();
      dos.writeUTF(msg);
    }
  }
}

결과

image


OneToOneServer / Client -> Thread 분기

public class OneToOneServer {
  //bad code
  public static void main(String[] args) throws Exception{
    ServerSocket server = new ServerSocket(9999);
    System.out.println("Ready...");
    Socket client = server.accept();

    DataInputStream din = new DataInputStream(client.getInputStream());
    DataOutputStream dos = new DataOutputStream(client.getOutputStream());

    while(true) {
      String clientMsg = din.readUTF();
      System.out.println(clientMsg);
      for (int i = 0; i < 5; i++) {        
        Thread.sleep(1000);
        dos.writeUTF(clientMsg);
      }//end for
    }//end while
  }
}

public class OneToOneClient {
  //bad code
  public static void main(String[] args) throws Exception{

    Scanner keyScanner = new Scanner(System.in);
    Socket socket = new Socket("192.168.50.4", 9999);
    DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
    DataInputStream din = new DataInputStream(socket.getInputStream());

    new Thread(() -> {

      while(true) {
        String serverMsg;
        try {
          serverMsg = din.readUTF();
          System.out.println("SERVER: " + serverMsg);
        } catch (IOException e) {
          e.printStackTrace();
        }
      }//end while

    }).start();

    //원래는 unreachable code -> Thread를 두개로 분기해서 같이 돌리면 동시에 돌릴수있음 
    while(true) {
      System.out.println("INSERT YOUR MESSAGE");
      String msg = keyScanner.nextLine();
      dos.writeUTF(msg);
    }//end while
  }
}

원래 while(true) 문을 두개를 돌리면 아래에 있는 코드는 unreachable code로 도달할 수 없는 코드라 에러가 발생하지만 이렇게 new Thread를 이용해 thread를 분리시키면 동시에 thread가 진행될 수 있기때문에 에러가 발생하지않음

그래서 하나 진행되는 동안에 다른 문장 입력해도 다른 코드와 엉켜서 꼬이거나 하지않고 각각 잘 싱행되는 모습을 볼 수 있음


결과

image


OneToOneServer쪽에도 Thread 추가

public class OneToOneServer {
  //bad code
  public static void main(String[] args) throws Exception{
    Scanner keyScanner = new Scanner(System.in);
    ServerSocket server = new ServerSocket(9999);

    System.out.println("Ready...");
    Socket client = server.accept();

    DataInputStream din = new DataInputStream(client.getInputStream());
    DataOutputStream dos = new DataOutputStream(client.getOutputStream());

    new Thread(() -> {
      while(true) {
        String serverMsg = keyScanner.nextLine();
        try {
          dos.writeUTF(serverMsg);
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    }).start();

    while(true) {
      String clientMsg = din.readUTF();
      System.out.println(clientMsg);
    }//end while
  }
}

결과

image

최종적으로 Server와 Client 양쪽 모두에 In, Out Loop 를 개별 Thread로 돌아가도록 구성해서 메세지를 주고받음에 끊김이 없도록 구성함

받기를 하는 동안에도 보내기가 가능하고 보내는 동안에도 받는 것이 가능해짐

1개의 댓글

comment-user-thumbnail
2023년 1월 26일

멀티쓰레드에 관한 내용이 확실하게 이해되었습니다. 감사합니다!

답글 달기

관련 채용 정보