일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 카프카
- 웹개발
- WEB SOCKET
- AWS
- @jsonproperty
- visualvm
- emqx
- 항해99
- 스프링의 정석
- CentOS
- java
- 스파르타코딩클럽
- DB
- MYSQL
- docker
- Kafka
- 쇼트유알엘
- Spring
- EC2
- Spring Security
- 생성자 주입
- 남궁성과 끝까지 간다
- JavaScript
- 시큐리티
- 패스트캠퍼스
- JWT
- 데이터베이스
- 스웨거
- 개인프로젝트
- 프로그래머스
- Today
- Total
Nellie's Blog
[Java] 동시성 이슈를 유발해보고 해결하기 (feat. VisualVM, synchronized) 본문
[Java] 동시성 이슈를 유발해보고 해결하기 (feat. VisualVM, synchronized)
Nellie Kim 2024. 12. 14. 17:56아주 간단한 코드로 동시성 이슈를 유발해보고, synchronized를 사용해 초간단 해결을 해보는 과정을 통해 모니터링을 관찰하며 쓰레드와 리소스에 어떤 변화가 있는지 확인해보려고 한다. 실무에서 매우 중요한 부분이라서 실습을 해보겠다!
동시성 이슈 유발 코드
Producer 클래스
package com.example.demo;
import java.util.Random;
import java.util.logging.Logger;
/**
프로듀서 스레드는 리스트에 값을 추가한다.
*/
public class Producer extends Thread{
private Logger log = Logger.getLogger(Producer.class.getName());
public Producer(String name) {
super(name);
}
public void run() {
final Random random = new Random();
while (true) {
if (DemoApplication.list.size() < 100) {
int x = random.nextInt();
DemoApplication.list.add(x);
log.info("프로듀서 "+Thread.currentThread().getName() + "는 + " + x +" 를 추가함.");
}
}
}
}
Consumer 클래스
package com.example.demo;
import java.util.logging.Logger;
/**
컨슈머 스레드는 리스트에 값을 삭제한다.
*/
public class Consumer extends Thread {
private Logger log = Logger.getLogger(Producer.class.getName());
public Consumer(String name) {
super(name);
}
public void run() {
while (true) {
if (DemoApplication.list.size() > 0) { // 리스트에 값이 하나라도 있으면
int x = DemoApplication.list.get(0);
DemoApplication.list.remove(0); // 첫번째 값을 삭제
log.info("컨슈머 "+Thread.currentThread().getName() + "는 + " + x +" 를 삭제함.");
}
}
}
}
Main 클래스
package com.example.demo;
import java.util.ArrayList;
import java.util.List;
//@SpringBootApplication
public class DemoApplication {
public static List<Integer> list= new ArrayList<>();
public static void main(String[] args) {
// SpringApplication.run(DemoApplication.class, args);
new Producer("_Producer 1").start();
new Producer("_Producer 2").start();
new Consumer("_Consumer 1").start();
new Consumer("_Consumer 2").start();
}
}
4개의 스레드(_Producer 1, _Producer 2, _Consumer 1, _Consumer 2)를 생성하여 돌려봄
VisualVM으로 모니터링을 해보자.
상단 좌측에는 CPU 모니터링, 상단 우측에는 메모리 (Heap) 모니터링이다.
그래프를 보면, CPU는 20% 가량을 사용하지만, 메모리는 거의 사용하지 않는다. 100 MB 정도….
메모리를 거의 사용하지 않기 때문에 GC도 할 일이 없다.
결국 이 앱은 아무것도 하지 않는 셈이다.
이제 쓰레드 모니터링을 해보자.
맨 위에서부터 3개의 쓰레드를 주목하자.
스레드 4개가 생성이 되었으나 하나(_Consumer 2)가 죽어버렸다. 경쟁 상태로 예외가 발생해서 죽어버린 것.
3개의 스레드는 좀비 스레드가 되었다. 아무 일도 안하면서 무지성으로 실행 상태로 남아있는 것이다.
CPU 리소스를 축내는 애들이다.
이렇게 뜨고 멈춘다.
synchronized를 사용하여 동기화로 해결해보자.
이 문제를 해결해보자.
스레드 간의 동시 액세스와 경쟁 상태를 방지하기 위해 동기화(syncronized) 블록을 추가했다.
컨슈머/프로듀서 모두 list 인스턴스를 동기화 코드 블록의 스레드 모니터로 사용한다.
Producer 클래스
package com.example.demo;
import java.util.Random;
import java.util.logging.Logger;
/**
프로듀서 스레드는 리스트에 값을 추가한다.
*/
public class Producer extends Thread{
private Logger log = Logger.getLogger(Producer.class.getName());
public Producer(String name) {
super(name);
}
public void run() {
final Random random = new Random();
while (true) {
synchronized (DemoApplication.list) { // 추가 🎯
if (DemoApplication.list.size() < 100) {
int x = random.nextInt();
DemoApplication.list.add(x);
log.info("프로듀서 "+Thread.currentThread().getName() + "는 + " + x +" 를 추가함.");
}
}
}
}
}
Consumer 클래스
package com.example.demo;
import java.util.logging.Logger;
/**
컨슈머 스레드는 리스트에 값을 삭제한다.
*/
public class Consumer extends Thread {
private Logger log = Logger.getLogger(Producer.class.getName());
public Consumer(String name) {
super(name);
}
public void run() {
while (true) {
synchronized (DemoApplication.list) { // 추가 🎯
if (DemoApplication.list.size() > 0) { // 리스트에 값이 하나라도 있으면
int x = DemoApplication.list.get(0);
DemoApplication.list.remove(0); // 첫번째 값을 삭제
log.info("컨슈머 "+Thread.currentThread().getName() + "는 + " + x +" 를 삭제함.");
}
}
}
}
}
코드를 수정하고 모니터링을 해보니, CPU 사용량이 20%에서 10% 정도로 절반 가량 줄었다. 정상적인 앱은 CPU를 많이 소비하지 않는다.
메모리도 정상적으로 사용하고 있다. 앱이 메모리를 사용한다는 것은 실제로 어떤 일을 하고 있다는 뜻이다.
GC도 활동하여 톱니바퀴형의 그래프가 관찰된다.
이렇게 정상적으로 코드를 동기화하게 되면 CPU 소비는 줄고 앱은 메모리를 약간 사용하는 형태로 리소스 소비 패턴 자체가 달라진다.
쓰레드 모니터링에서도 맨 위부터 4개가 정상적으로 번갈아가며 실행 중이다.
스레드가 더이상 연속적으로 실행되지 않고 모니터에 의해 차단되었다가 실행되었다를 반복한다.
하늘색과 연두색의 조화가 예쁘다.
동기화 블록이 있어서 모니터가 한 번에 하나의 스레드만 실행되도록 수시로 실행을 중단시키는 모습이 확인된다...신기!
멈추지 않고 로그가 계속 올라온다. 굿!
출처 : 자바 잘 읽는 법 - 자바 코드를 이해하고, 디버깅하고, 최적화하는 요령 (Ch 6)
'Back-end > java' 카테고리의 다른 글
[Java] OOM Error (OutOfMemoryError) 유발해보고 모니터링 분석하기 (feat. VisualVM) (1) | 2024.12.14 |
---|---|
스트림의 중간연산과 최종연산 목록 표 (0) | 2024.01.01 |
@RequestBody 호출 시 일어나는 일을 간단히 알아보기 (feat. MessageConverter, ObjectMapper) (1) | 2023.12.06 |
AOP를 사용하여 에러 로그 찍기 (0) | 2023.09.26 |
로컬 디스크에 자바 로깅 기록하기 (spring.log) (0) | 2023.09.26 |