프로젝트를 진행 중 좋아요를 누르는 API가 있는데 문득 동시에 여러사람이 동시에 하나의 게시글에 좋아료를 눌렀을 때 어떻게 될까?라는 의문을 가지게 되었다..
현재 코드에서는 만약 2명의 유저가 하나의 게시글에 좋아요를 동시에 눌렀을 때 좋아요 갯수가 하나만 올라간다...! 😪
물론 좋아요갯수가 중요도가 높은 API는 아니긴 하지만 동시성에 관심을 갖고 한번 해결해보고 싶었다.
우선 동시성을 하기 전에 Thread개념에 대해 알아야한다.
https://wnstjr120422.tistory.com/entry/JAVA-Thread-Runnable-Multi-Thread%EB%9E%80
public class Main extends Thread {
public static void main(String[] args) {
final Book book = new Book();
for (int i = 1; i <= 10; i++) {
new Thread(new SellingBook(book), "jun" + i).start();
}
}
}
public class Book {
private int bookCount = 0;
public void sell() {
bookCount += 1;
}
public void show() {
System.out.println(Thread.currentThread().getName() + " 현재 팔린 수량: " + bookCount);
}
}
public class SellingBook implements Runnable {
private final Book book;
public SellingBook(Book book) {
this.book = book;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
book.sell();
}
book.show();
}
}
위의 예제는 단순한 책 판매를 하는 예제이다.
위의 예제를 실행했을 때 내가 예상한 결과는 10, 20, 30, ~ 100이렇게 순서대로 나오는 것이다..!
결과는 나의 예상과 달리 엉망진창으로 나왔다!
이렇게 나온 이유는 바로 동시성 이슈가 발생했기 때문이다....
그래서 난 이걸 해결하기 위해 자바에서 많이 사용되는 synchronized를 사용하여 해결을 하려 했다!
public class Book {
private int bookCount = 0;
public void sell() {
synchronized (this) {
bookCount += 1;
}
}
public void show() {
synchronized (this) {
System.out.println(Thread.currentThread().getName() + " 현재 팔린 수량: " + bookCount);
}
}
}
그래서 위와 같이 코드 블럭 단에 synchronized 를 붙여서 동시성 이슈를 해결할 수 있다고 확신,,했다!!
어,,,,근데 뭔가 된거 같은데 10개씩이 아니라 이상하게 출력이 된 걸 확인했다,,, 정말 왜 이런지 이해가 안됐다ㅜㅜㅜ
synchronized는 객체 단위로 락을 거는 것이다.
내가 생각한 바로는 락의 범위가 객체인데 SellingBook클래스에서 book.sell()과 book.show()메소드를 호출할 때 synchronized가
각각 달려있기에 다른 락이 걸리고 sell()과 show()에 각각 스레드가 접근해서 저런 결과가 나왔다고 생각했다..
그럼 저 sell(), show()메소드를 하나의 메소드로 묶은 다음 실행을 하면 잘 나오지 않을까 생각했다
위 사진처럼 Book클래스에 이렇게 메소드를 생성하고 실행을 하였고 결과는,.,!
해결이 되었다,,!
근데 과연 전 코드가 왜 안되었을까? 라는 생각이 들었고, 락에 대해 알아보라는 조언을 받았다!!!
그래서 알아보니
📌 메소드 단에 synchronized를 붙인 경우
해당 메소드가 포함된 객체(클래스단위로)에 락이 걸린다.
📌 코드 블럭 단에 synchronized을 붙인 경우
괄호안에 인자로 넣는 객체가 락이 걸린다.
내가 생각하는 코드가 예상대로 안됐던 이유는 synchronized가 sell(), show()에 따로 걸려있었기 때문에라고 생각한다( 틀렸다면 바로 지적해주세요!!!!!😊)
그래서 해결한 것이 sell(), show()을 하나로 묶어서 메소드단에 synchronized를 붙여 클래스 단위로 락을 걸어서 해결을 하였다!!
하지만 진짜 내가 궁금한 것은 jun1이 먼저 공유자원에 접근했을 때 순차적으로 자원을 사용하는 방법이 무엇일까라는 의문이 들었다..
그래서 떠오른게 ReentrantLock이다.
내가 생각하는 ReentrantLock의 가장 큰 특징은 '공정함' 이라고 생각한다.
이 공정함이라는 것은 말 그대로 대기 큐에 가장 오래 대기한 스레드를 먼저 임계영역에 넣어주는 것을 의미한다.
synchronized는 대기 큐에 오래있던 말던 무작위로 임계영역에 접근하였는데 이를 방지하기 위해 ReentrantLock를 사용한 것이다.
우선 원리를 간단하게 보면 한 스레드가 공유자원을 사용중이면 다른 쓰레드는 waiting pool이라는 곳으로 들어가 대기를 하게 된다.
그런다음 임계영역에 있던 스레드가 나오면 nofify()메소드를 이용하여 waiting pool에 있던 스레드를 깨워서 임계영역에 넣는데,,!
여기서 synchronized는 무슨 스레드가 임계영역으로 나올지 알 수 없다는 단점이 있다.(이것이 내가 궁금했던 순차적이랑 비슷함,,)
이때 스레드를 선별할 수 있는 것이 Condition이다.
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class Book {
private int bookCount = 0;
ReentrantLock lock=new ReentrantLock(true);
Condition condition=lock.newCondition();
Queue<Thread> threadQueue=new LinkedList<>();
public void sell() {
bookCount += 1;
}
public void show() {
System.out.println(Thread.currentThread().getName() + " 현재 팔린 수량: " + bookCount);
}
public void sellAndShow() {
lock.lock();
try{
threadQueue.add(Thread.currentThread());
while (threadQueue.peek()!=Thread.currentThread()){
try {
condition.await();
} catch (InterruptedException e) {}
}
for (int i = 0; i < 10; i++) {
sell();
}
show();
threadQueue.remove();
condition.signalAll();
}finally {
lock.unlock();
}
}
}
우선 내가 작성한 예제 코드는 다음과 같다.
이제 한번 뜯어보자,,,,!
ReentrantLock lock=new ReentrantLock(true);
Condition condition=lock.newCondition();
ReentrantLock를 생성할 때 true라는 값을 인자로 넘겨서 가장 오래된 wait한 스레드에게 락을 부여한다.
false로 하면 synchronized처럼 무작위로 스레드에게 락을 부여한다(?)
그 다음 Condition객체는 스레드를 선별적으로 중지하거나 재진입할 수 있게끔 도와주는 인터페이스이다.
여기서 사용한 await()와 singalAll()은 synchronized의 wait(), notify()의 역할을 한다고 보면 된다.
자 그럼 다시 위의 코드로 돌아가서 스레드를 담기 위한 큐를 먼저 선언해준다.
큐를 선언한 이유는 아무리 ReentrantLock의 생성자에 공정성을 위해 boolean변수를 인자로 넘겨도 CPU와 시스템 상황에 따라 정확한 순서가 보장되지 않을 수 있기 때문에 선언을 해주었다!!
그 다음 현재 스레드를 큐에 담고 while을 통해서 큐에서 꺼낸 스레드가 현재 스레드가 아니면 await()를 통해 waiting pool에 집어넣는다.
여기서 sellAndShow()메소드 앞부분에 lock을 거는 부분이 있는데 Condition의 await(), singalAll()을 이용하려면 객체에 락이 걸려있어야 하기 때문이다.
ReentrantLock의 lock의 범위는 lock이 선언된 블록, 메소드에 해당한다.
스레드가 작업을 수행을 다했다면 remove()를 통해 대기열 큐에서 제거를 해주고 singalAll()를 통해 waiting pool에 있는 스레드를 전부 깨워준다.
이렇게 하면 과연 결과는 어떻게 나올까?????
내가 원했던 결과가 나온다ㅜㅜㅜㅜㅜㅜㅜㅜ
동시성을 해결하는 방법은 정말 많은 것 같다..!
하지만 지금 내가 한 건 코드 블럭 단에서 동시성 이슈를 제어하려고 했다
과연 이렇게 하면 내가 진행하는 프로젝트에서 정상적으로 작동을 할까?
만약 내가 한대의 서버를 이용하고 있다면 상관 없지만 다수의 서버를 이용한다고 한다면 다시 동시성 이슈가 발생할 것이다........
그럼 무슨 방법이 있을까??
바로 DB에 락을 거는 방법이다..
이건 다음 포스팅에서 설명하겠다!!!!!
'JAVA' 카테고리의 다른 글
JAVA_단위 테스트&통합 테스트 (1) | 2023.10.16 |
---|---|
[JAVA] I/O스트림 (0) | 2023.05.23 |
[JAVA] 제네릭 < > (0) | 2023.05.23 |
[JAVA] Enum(열거 타입) (0) | 2023.05.17 |
[JAVA] Thread & Runnable & Multi Thread란? (0) | 2023.05.03 |