강의 초반부 스레드는 start해서 작업이 끝나면 재사용할 수 없다고 하셨고 자바독에서도 아래와 같이 start()가 재사용되는 것은 legal하지 않다고 표현하고 있습니다 It is never legal to start a thread more than once. I n particular, a thread may not be restarted once it has completed execution. 그런데 스레드풀 재사용 시에는 뭔가 다른 작업이 있을까 해서 강의와 함깨 디버깅해보니 addWorker()에서도 Worker에 할당된 스레드의 start()메소드로 호출하고 있었습니다. 스레드 풀의 경우 start()호출 후 메소드가 종료되었음에도 스레드를 재사용할 수 있는 이유는 무엇인가요? 질문을 작성하고 나서 조금 더 고민해보니 아래와 같은 결론에 도달했습니다.(혹시 틀린 내용이 있다면 수정 의견 부탁드립니다) 비슷한 고민을 하신 분이 있을 것 같아 글을 남겨둡니다. 생성된 스레드의 start() 실행 -> Worker의 run()실행 -> Worker의 runWorker()실행 -> while()조건에 의해 무한 루프 무한루프가 되는 이유 : getTask()를 통해 큐에서 작업을 꺼내와 실행하고 큐에 작업이 없을 경우 블록킹되므로 블로킹이 해제된 시점에서는 task변수에 작업이 할당되어 while조건문이 true가 됨
https://www.inflearn.com/questions/655574 위에 질문에서 saveAndFlush를 사용한 이유에 "save 메소드를 사용하게 된다면 데이터베이스에 바로 flush 가 되는것이 아니기때문에 synchronized 를 이용한 방법을 테스트할 때 오류가 날것입니다." 라고 답변해주셨는데 save를 사용하든 saveAndFlush를 사용하든 문제가 발생하지 않나요? flush를 바로 해준다고 데이터베이스에 커밋이 되는게 아니기때문에 충분히 동시성 문제가 생길 수 있다고 생각됩니다. 결국 아래 코드에서 saveAndFlush를 사용하든 save를 사용하든 실패하는것인데 왜 saveAndFlush를 사용하면 오류가 안난다고 하신건지 궁금합니다. @Transactional public synchronized void decrease(Long id, Long quantity) { Stock stock = stockRepository.findById(id).orElseThrow(); stock.decrease(quantity); stockRepository.saveAndFlush(stock); }
안녕하세요 강사님 강의를 듣다가 이상한 것을 발견하여 문의 드립니다. Java Thread Fundamentals - 스레드 기본 API > interrupt() 강의중 InterruptExample 부분의 샘플 코드를 작성하고 테스트 및 정리를 하고 있습니다. 제가 현업에서 사용하는 JDK 11 을 사용해서 연습중에 이상한 현상을 발견하였습니다. JDK 17에서는 문제 없음을 확인하였습니다. 먼저 코드 구현체 부분입니다. public class TreadInterrupt { public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { System.out.println("thread 1 start"); System.out.println("thread 1 isInterrupted() = " + Thread.currentThread().isInterrupted()); }); Thread t2 = new Thread(() -> { System.out.println("thread 2 start"); t1.interrupt(); System.out.println("thread 1 isInterrupted() = " + t1.isInterrupted()); System.out.println("thread 2 isInterrupted() = " + Thread.currentThread().isInterrupted()); }); t2.start(); Thread.sleep(1000); t1.start(); t1.join(); t2.join(); System.out.println("작업 완려"); } } Thread#interrupt() , Thread#isInterrupted() 의 jdk 11과 17 버전입니다. // jdk 11 public class Thread { public boolean isInterrupted() { return isInterrupted(false); } @HotSpotIntrinsicCandidate private native boolean isInterrupted(boolean ClearInterrupted); public void interrupt() { if (this != currentThread()) { this.checkAccess(); synchronized(this.blockerLock) { Interruptible b = this.blocker; if (b != null) { this.interrupt0(); b.interrupt(this); return; } } } this.interrupt0(); } private native void interrupt0(); } // jdk 17 public class Thread { private volatile boolean interrupted; public boolean isInterrupted() { return interrupted; } public void interrupt() { if (this != Thread.currentThread()) { checkAccess(); // thread may be blocked in an I/O operation synchronized (blockerLock) { Interruptible b = blocker; if (b != null) { interrupted = true; interrupt0(); // inform VM of interrupt b.interrupt(this); return; } } } interrupted = true; // inform VM of interrupt interrupt0(); } } 위 코드와 같이 jdk 17 은 Thread 객체 내 interrupted 필드를 둠으로써 인터럽트의 상태를 관리하지만 jdk 11 같은 경우에는 native 메서드를 이용해서 인터럽트의 상태를 관리하는 것을 확인할 수 있었습니다. 이 때 jdk 11에서 위 코드를 실행시키면 결과가 아래 와 같이 출력되는것을 확인하였습니다. thread 2 start thread 1 isInterrupted() = false thread 2 isInterrupted() = false thread 1 start thread 1 isInterrupted() = false 작업 완려 프로세스 실행을 반복하거나 sleep 의 시간을 늘려보아도 항상 아래와 같은 동일한 결과가 나왔습니다. 하지만 이 코드를 디버그 모드를 이용해서 동작시킨 경우 완전히 다른 결과가 나왔습니다. break point 는 t1.interrupt() 부분에 걸었으며 실제 인터럽트 내부 코드 동작 부분은 확인하지 않고 F8 을 이용해서 코드라인을 프로세스가 종료할 때 까지 넘김 thread 2 start thread 1 start thread 1 isInterrupted() = true thread 1 isInterrupted() = true thread 2 isInterrupted() = false 위는 실제로 디버그 모드로 동작 시켰을때 처리 결과입니다. 아무리 생각해도 동일한 코드, 동일한 로직인데 디버그 모드일때 정상적으로 출력되고 일반적으로 실행하였을때 인터럽트의 상태가 모두 false 가 나온다는 것이 이해되지 않아 문의 남겨봅니다.
NamedLock을 사용한 예제실습 중 질문입니다 NamedLockStockFacade의 decrease 메소드에 @Transactional 을 걸고 StockService의 decrease 메소드에는 @Transactional 을 걸지 않아도 잘 작동해야하는거 같은데, 데드락에 걸리는거 같습니다. 그 이유가 궁금합니다 제 생각에는 NamedLockStockFacade의decrease 메소드에 @Transactional 을 걸면, lockRepository.getLock(id.toString()), stockService.decrease(id, quantity), lockRepository.releaseLock(id.toString()) 이 3 메소드가 모두 한 트랜잭션 안에서 처리되므로 lock을 걸고 lock을 해제하는 그 사이에 재고를 감소하므로 아무 문제가 없어보여서 질문드립니다.
Optimistic Lock은 충돌이 빈번할 경우 적절하지 않다고 하는데, 충돌이 기준이 어떤건지 궁금합니다. 별도로 프로젝트에서 Mybatis랑 같이 쿠폰발급 프로젝트에서 응용중입니다. 전 쿠폰 500개를 기준으로 발급된 수량을 합산하는 로직입니다. 스레드 100개기준으로 동시성테스트를 진행했을때 69개만 적용되더라구요. 재시도 로직에서 Thread.sleep(1000)으로 하면 98개까지 적용됩니다. 그런데 스레드 10개를 기준으로 동시성 테스트를 진행했을때는 정합성이 맞고, 스레드1000개를 돌려버리면 500개가 전부다 발급됩니다. 1000개 중에 몇개는 실패하고, 몇개는 성공하고 그래서 500개 전부 다 발급된 상황이라고 예상됩니다. Q1. 충돌이 많은 상황이라는게 동시에 여러 스레드가 접근하는 걸 의미하는게 맞나요? 충돌의 기준은 무엇인지, 어플리케이션 내부 로직마다 다른걸까요? Q2. 충돌이 빈번하지 않을때 낙관적락을 사용하는 경우가 어떤 경우인지 궁금합니다.. 정합성이 많이 떨어진다고 생각하는데 실제로 현업에서 많이 사용하나요? Q3. 데이터베이스에 락을 걸지 않아서 성능상 이점이 있다고 하셨는데, 제가 테스트해봤을때는 pessimisticLock보다 Mysql에 더 부하가 많이가더라구요. 재시도로직으로 I/O작업이 더 많이 일어나서 그런것 같은데, 말씀하시는 '성능상 이점'이라는건 어떤건지 궁금합니다.
다대일에서 한 스레드가 I/O 발생 시 프로세스 자체를 블록하기 때문에 모든 스레드들이 Block이 발생하는데 다대다에서는 한 스레드가 I/O 발생 시 커널이 다른 스레드의 수행을 스케줄할 수 있는 이유가 궁금합니다. 다대일도 다대다처럼 커널이 다른 스레드의 수행을 스케줄하면 안되나요? 왜 다대일만 프로세스 블록시키고 다대다는 프로세스를 블록시키지 않고 모든 스레드들이 Block이 발생하지 않나요?
위의 질문과 똑같은 질문입니다! @SpringBootTest class stockServiceTest { @Autowired private StockService stockService; @Autowired private StockRepository stockRepository; @BeforeEach public void setUp(){ stockRepository.saveAndFlush(new Stock(1L,100L)); } @AfterEach public void after(){ stockRepository.deleteAll(); } @Test @DisplayName("동시에 100개 요청") void 동시에_100개의_요청() throws InterruptedException { //given int threadCount = 100; // ExecutorService는 비동기로 실행하는 작업을 단순화하여 사용할 수 있게하는 자바 api ExecutorService executorService = Executors.newFixedThreadPool(32); // CountDownLatch는 다른 쓰레드에서 수행중인 작업이 완료될때까지 대기할 수 있도록 도와주는 클래스 CountDownLatch latch = new CountDownLatch(threadCount); for (int i = 0; i < threadCount; i++) { executorService.submit(()->{ try { stockService.decrease(1L,1L); }finally { latch.countDown(); } }); } latch.await(); //when //then Stock stock = stockRepository.findById(1L).orElseThrow(); assertThat(stock.getQuantity()).isEqualTo(0); } @Test @DisplayName("재고감소") void decreaseStock(){ //given //when stockService.decrease(1L,1L); Stock stock = stockRepository.findById(1L).orElseThrow(); //then assertThat(stock.getQuantity()).isEqualTo(99); } } 1번테스트와 2번테스트를 각각 테스트를 실행할 경우 테스트가 성공하지만 전체 테스트를 실행하게 하여 1번테스트와 2번테스트를 돌리면 No value present java.util.NoSuchElementException: No value present 라는 예외가 뜹니다. @BeforeEach와 @AfterEach가 있어서 문제가 없을거라 생각하는데 왜 전체 테스트중 동시에_100개의_요청이라는 테스트가 실패하는건가요?
@Transactional(isolation = Isolation.SERIALIZABLE) 이렇게 해도 동시성 제어 테스트에 실패해서 검색해보니까 SERIALIZABLE 격리 단계는 락이 걸려 있는 동안 update를 못하는거지 select는 가능하다 라는 식의 글을 봤습니다. 그러면 각 스레드들이 접근해서 select만 한 상태에서 대기하고 있다가 락이 풀리면 update를 시도하는 건가요? 그래서 테스트에 실패하는 건가요?
안녕하세요 강사님 강의(싱글스레드 & 멀티스레드)를 듣다가 궁금한 점이 생겼습니다.예제 코드에서 1 ~ 1000을 더하는 for문의 실행시간은 약 2초 정도가 나오고 이를 2개의 스레드로 처리하면 약 1초 정도가 나왔는데요.synchronized키워드를 이용해서 lock을 걸 경우 해당 자원에는 하나의 스레드만 접근하게 되므로 예제의 경우 사실상 병렬처리가 불가능함에도 처리속도가 더 빠른 이유는 무엇인가요? 사양은 다음과 같습니다 : 인텔 ® 코어 ™ i5 - 7500 프로세서 ; 스레드 수. 4 ; GPT 도움을 받아 아래와 같이 추측해 봤는데 맞을까요? 싱글스레드 프로그램 : sleep(1)초에 의해 for루프 안에서 sleep 상태 발생 멀티스레드 프로그램 : thread1의 sleep동안 thread2가 연산 수행 -> 싱글스레드와 다르게 sleep 상태에 의해 지연되는 시간이 없음
NamedLock을 사용하는 경우에 DataSource를 분리하는 것이 좋다고 하여 시도해봤으나 DataSource 설정의 문제인지 정상적으로 동작하지 않아 질문드리게 되었습니다. Main이 JPA에서 사용하게 될 데이터소스이고, Lock이 JdbcTemplate를 사용하기 위해서 사용하는 데이터소스입니다. spring: datasource: main: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3308/stock_example?serverTimezone=Asia/Seoul username: root password: 1234 lock: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3308/stock_example?serverTimezone=Asia/Seoul username: root password: 1234 hikari: maximum-pool-size: 20 jpa: database: mysql database-platform: org.hibernate.dialect.MySQLDialect hibernate: ddl-auto: create logging: level: root: INFO org: hibernate: SQL: DEBUG type.descriptor.sql.BasicBinder: TRACE 애플리케이션 자체에서 사용해야 할 커넥션 풀만 관리를 하면 될 것이라고 생각하여 동일한 데이터베이스를 사용하였습니다. 먼저, 메인으로 사용하는 NamedLock을 사용하지 않는 데이터소스설정클래스입니다. @Configuration @EnableJpaRepositories( basePackages = "com.example.stock.stock", entityManagerFactoryRef = "mainEntityManager", transactionManagerRef = "mainTransactionManager") public class MainDataSourceConfiguration { @Bean @Primary @ConfigurationProperties("spring.datasource.main") public DataSourceProperties mainDataSourceProperties() { return new DataSourceProperties(); } @Bean @Primary public DataSource mainDataSource() { return mainDataSourceProperties() .initializeDataSourceBuilder() .type(HikariDataSource.class) .build(); } @Bean(name = "mainEntityManager") @Primary public LocalContainerEntityManagerFactoryBean mainEntityManager( EntityManagerFactoryBuilder builder) { return builder.dataSource(mainDataSource()) .packages("com.example.stock.stock") .build(); } @Bean(name = "mainTransactionManager") @Primary public PlatformTransactionManager mainTransactionManager( @Qualifier("mainEntityManager") LocalContainerEntityManagerFactoryBean entityManagerFactoryBean) { return new JpaTransactionManager(Objects.requireNonNull(entityManagerFactoryBean.getObject())); } } 다음으로 Lock을 사용하기 위한 데이터소스 설정 클래스입니다. @Configuration @EnableTransactionManagement public class LockDataSourceConfiguration { @Bean @ConfigurationProperties("spring.datasource.lock") public DataSourceProperties lockDataSourceProperties() { return new DataSourceProperties(); } @Bean public DataSource lockDataSource() { return lockDataSourceProperties() .initializeDataSourceBuilder() .type(HikariDataSource.class) .build(); } @Bean(name = "lockTransactionManager") public JdbcTransactionManager lockTransactionManager(@Qualifier("lockDataSource") DataSource dataSource) { return new JdbcTransactionManager(dataSource); } } @Configuration public class JdbcTemplateConfiguration { @Bean public NamedParameterJdbcTemplate namedParameterJdbcTemplate(@Qualifier("lockDataSource") DataSource dataSource) { return new NamedParameterJdbcTemplate(dataSource); } } 그리고 테스트를 돌려봤는데, 재고가 정상적으로 감소가 되지 않습니다. 이상한 점은 업데이트 쿼리가 발생을 하지 않는다는 점입니다. 또 매번, 생성되는 스톡의 id가 1씩 증가한다는 점입니다. 혹시 제가 놓친 부분이 있을까요..?
synchronized를 사용하면 1개의 쓰레드만 이용한다고 말씀해주셨는데 startTransaction(); stockService.decrease(id, quantity); endTransation(); 에서 왜 다른 쓰레드가 먼저 호출되는 상황이 발생하는건가요? 이 부분이 이해가 잘 안됩니다
Redisson을 사용할때 waitTime과 leaseTime 값 설정이 잘못되면 서비스의 큰 문제가 발생할 수 있지 않나요? 예를들어, 서비스가 서버 상황에 따라 특정 로직을 처리하는데 걸리는 시간이 다를 거라고 생각합니다. 이때 leaseTime이 처리 시간보다 적다면 동시성 문제가 추가로 발생하지 않나요? 또한 강의에서처럼 waitTime 값에 따라 동시성 문제 발생 여부가 달라진다면 이 또한 실제 현업에서는 상황에 따라 다르기 때문에 문제가 되지 않을까요? 위와 같은 이유로, Redisson이 너무 위험하다고 생각이 드는데 제가 잘못이해한건지 궁금합니다.
어플리케이션 서버가 여러개 라는 뜻일까요? DB 서버가 여러개라는 뜻일까요? 멀티 스레드 테스트를 통해 여러 애플리케이션 서버가 동시에 요청이 들어오는 경우는 비관,낙관적 락으로도 해결이 가능한걸 확인했고 DB가 분산서버일때 분산락을 이용해야한다고 이해했는데 맞을까요? namelock도 결국엔 문자열에 건다해도 그 문자열 == 특정 데이터 인데 특정 데이터에 락== 비관적 락이랑 다를게 없는데 정확한 차이를 모르겠습니다. 문자열로 하면 뭐가 다른건지...
SpringBoot 에서 멀티 스레드로 크롤러가 돌아가는 프로젝트 입니다. junit 테스트 코드를 통해 크롤러가 10개 정도 게시판 페이지를 정상적으로 가져오는지 확인하려고 하는데요. 현재 작성된 테스트 케이스는 아래와 같습니다. @Test void crawlerTest() { // 크롤링 시작 crawlerRun(); while(스레드풀 작업 종료됐나??) { // 작업이 안끝났으면 Thread.sleep(5000); } } while 문이 있으면 정상적으로 10개의 게시판 페이지를 크롤링한 이후 DB에 적재됩니다. 다만 강의를 보면서 while 문이 없어도 되지 않을까 생각해서 삭제하거나 주석처리하면 올바르게 동작하지 않습니다. [while 문 제거 후] 디버깅을 통해 값이 제대로 들어오는지 확인하면서 테스트 케이스를 돌리면 4~5 개정도만 DB 에 크롤링한 데이터가 저장되고 갑자기 오류도 없이 테스트가 종료됩니다. 디버깅없이 그냥 실행시키면 6개 정도 DB에 저장되고 테스트가 종료됩니다. crawlerTest() 라는 테스트 코드가 Main Thread 라고 쳤을 때 다른 Thread 들은 정상적으로 동작 중이어야 하지 않나요..? 아니면 Spring 환경이라서 이런 문제가 발생하는 것인지 junit 테스트 코드라서 이런 문제가 발생하는건지 궁금합니다! ( Daemon Thread 는 아닙니다! )