diff --git a/study/src/main/resources/application.yml b/study/src/main/resources/application.yml index db798e1815..59ffbf7bbd 100644 --- a/study/src/main/resources/application.yml +++ b/study/src/main/resources/application.yml @@ -1,7 +1,7 @@ server: tomcat: accept-count: 1 - max-connections: 1 + max-connections: 3 threads: min-spare: 2 max: 2 diff --git a/study/src/test/java/thread/stage0/SynchronizationTest.java b/study/src/test/java/thread/stage0/SynchronizationTest.java index 0333c18e3b..6297f3dfdf 100644 --- a/study/src/test/java/thread/stage0/SynchronizationTest.java +++ b/study/src/test/java/thread/stage0/SynchronizationTest.java @@ -1,29 +1,24 @@ package thread.stage0; -import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.stream.IntStream; - -import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.Test; /** - * 다중 스레드 환경에서 두 개 이상의 스레드가 변경 가능한(mutable) 공유 데이터를 동시에 업데이트하면 경쟁 조건(race condition)이 발생한다. - * 자바는 공유 데이터에 대한 스레드 접근을 동기화(synchronization)하여 경쟁 조건을 방지한다. - * 동기화된 블록은 하나의 스레드만 접근하여 실행할 수 있다. - * - * Synchronization - * https://docs.oracle.com/javase/tutorial/essential/concurrency/sync.html + * 다중 스레드 환경에서 두 개 이상의 스레드가 변경 가능한(mutable) 공유 데이터를 동시에 업데이트하면 경쟁 조건(race condition)이 발생한다. 자바는 공유 데이터에 대한 스레드 접근을 + * 동기화(synchronization)하여 경쟁 조건을 방지한다. 동기화된 블록은 하나의 스레드만 접근하여 실행할 수 있다. + *

+ * Synchronization https://docs.oracle.com/javase/tutorial/essential/concurrency/sync.html */ class SynchronizationTest { /** - * 테스트가 성공하도록 SynchronizedMethods 클래스에 동기화를 적용해보자. - * synchronized 키워드에 대하여 찾아보고 적용하면 된다. - * - * Guide to the Synchronized Keyword in Java - * https://www.baeldung.com/java-synchronized + * 테스트가 성공하도록 SynchronizedMethods 클래스에 동기화를 적용해보자. synchronized 키워드에 대하여 찾아보고 적용하면 된다. + *

+ * Guide to the Synchronized Keyword in Java https://www.baeldung.com/java-synchronized */ @Test void testSynchronized() throws InterruptedException { @@ -41,7 +36,7 @@ private static final class SynchronizedMethods { private int sum = 0; - public void calculate() { + public synchronized void calculate() { setSum(getSum() + 1); } diff --git a/study/src/test/java/thread/stage0/ThreadPoolsTest.java b/study/src/test/java/thread/stage0/ThreadPoolsTest.java index 238611ebfe..afe5ff6db2 100644 --- a/study/src/test/java/thread/stage0/ThreadPoolsTest.java +++ b/study/src/test/java/thread/stage0/ThreadPoolsTest.java @@ -1,23 +1,19 @@ package thread.stage0; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import static org.assertj.core.api.Assertions.assertThat; import java.util.concurrent.Executors; import java.util.concurrent.ThreadPoolExecutor; - -import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** - * 스레드 풀은 무엇이고 어떻게 동작할까? - * 테스트를 통과시키고 왜 해당 결과가 나왔는지 생각해보자. - * - * Thread Pools - * https://docs.oracle.com/javase/tutorial/essential/concurrency/pools.html - * - * Introduction to Thread Pools in Java - * https://www.baeldung.com/thread-pool-java-and-guava + * 스레드 풀은 무엇이고 어떻게 동작할까? 테스트를 통과시키고 왜 해당 결과가 나왔는지 생각해보자. + *

+ * Thread Pools https://docs.oracle.com/javase/tutorial/essential/concurrency/pools.html + *

+ * Introduction to Thread Pools in Java https://www.baeldung.com/thread-pool-java-and-guava */ class ThreadPoolsTest { @@ -31,8 +27,8 @@ void testNewFixedThreadPool() { executor.submit(logWithSleep("hello fixed thread pools")); // 올바른 값으로 바꿔서 테스트를 통과시키자. - final int expectedPoolSize = 0; - final int expectedQueueSize = 0; + final int expectedPoolSize = 2; + final int expectedQueueSize = 1; assertThat(expectedPoolSize).isEqualTo(executor.getPoolSize()); assertThat(expectedQueueSize).isEqualTo(executor.getQueue().size()); @@ -46,7 +42,7 @@ void testNewCachedThreadPool() { executor.submit(logWithSleep("hello cached thread pools")); // 올바른 값으로 바꿔서 테스트를 통과시키자. - final int expectedPoolSize = 0; + final int expectedPoolSize = 3; final int expectedQueueSize = 0; assertThat(expectedPoolSize).isEqualTo(executor.getPoolSize()); diff --git a/study/src/test/java/thread/stage0/ThreadTest.java b/study/src/test/java/thread/stage0/ThreadTest.java index 3ffef18869..9eee240c49 100644 --- a/study/src/test/java/thread/stage0/ThreadTest.java +++ b/study/src/test/java/thread/stage0/ThreadTest.java @@ -5,24 +5,19 @@ import org.slf4j.LoggerFactory; /** - * 자바로 동시에 여러 작업을 처리할 때 스레드를 사용한다. - * 스레드 객체를 직접 생성하는 방법부터 알아보자. - * 진행하면서 막히는 부분은 아래 링크를 참고해서 해결한다. - * - * Thread Objects - * https://docs.oracle.com/javase/tutorial/essential/concurrency/threads.html - * - * Defining and Starting a Thread - * https://docs.oracle.com/javase/tutorial/essential/concurrency/runthread.html + * 자바로 동시에 여러 작업을 처리할 때 스레드를 사용한다. 스레드 객체를 직접 생성하는 방법부터 알아보자. 진행하면서 막히는 부분은 아래 링크를 참고해서 해결한다. + *

+ * Thread Objects https://docs.oracle.com/javase/tutorial/essential/concurrency/threads.html + *

+ * Defining and Starting a Thread https://docs.oracle.com/javase/tutorial/essential/concurrency/runthread.html */ class ThreadTest { private static final Logger log = LoggerFactory.getLogger(ThreadTest.class); /** - * 자바에서 직접 스레드를 만드는 방법은 2가지가 있다. - * 먼저 Thread 클래스를 상속해서 스레드로 만드는 방법을 살펴보자. - * 주석을 참고하여 테스트 코드를 작성하고, 테스트를 실행시켜서 메시지가 잘 나오는지 확인한다. + * 자바에서 직접 스레드를 만드는 방법은 2가지가 있다. 먼저 Thread 클래스를 상속해서 스레드로 만드는 방법을 살펴보자. 주석을 참고하여 테스트 코드를 작성하고, 테스트를 실행시켜서 메시지가 잘 + * 나오는지 확인한다. */ @Test void testExtendedThread() throws InterruptedException { @@ -30,15 +25,14 @@ void testExtendedThread() throws InterruptedException { Thread thread = new ExtendedThread("hello thread"); // 생성한 thread 객체를 시작한다. - thread.start(); + thread.start(); // thread의 작업이 완료될 때까지 기다린다. - thread.join(); + thread.join(); } /** - * Runnable 인터페이스를 사용하는 방법도 있다. - * 주석을 참고하여 테스트 코드를 작성하고, 테스트를 실행시켜서 메시지가 잘 나오는지 확인한다. + * Runnable 인터페이스를 사용하는 방법도 있다. 주석을 참고하여 테스트 코드를 작성하고, 테스트를 실행시켜서 메시지가 잘 나오는지 확인한다. */ @Test void testRunnableThread() throws InterruptedException { @@ -46,10 +40,10 @@ void testRunnableThread() throws InterruptedException { Thread thread = new Thread(new RunnableThread("hello thread")); // 생성한 thread 객체를 시작한다. - thread.start(); + thread.start(); // thread의 작업이 완료될 때까지 기다린다. - thread.join(); + thread.join(); } private static final class ExtendedThread extends Thread { @@ -80,3 +74,4 @@ public void run() { } } } + diff --git a/study/src/test/java/thread/stage1/UserServlet.java b/study/src/test/java/thread/stage1/UserServlet.java index b180a84c32..e894497176 100644 --- a/study/src/test/java/thread/stage1/UserServlet.java +++ b/study/src/test/java/thread/stage1/UserServlet.java @@ -7,12 +7,17 @@ public class UserServlet { private final List users = new ArrayList<>(); - public void service(final User user) { + public synchronized void service(final User user) { join(user); } private void join(final User user) { if (!users.contains(user)) { + try { + Thread.sleep(1); // Expected context switching to another thread + } catch (InterruptedException e) { + throw new RuntimeException(e); + } users.add(user); } } diff --git a/tomcat/src/main/java/org/apache/catalina/connector/Connector.java b/tomcat/src/main/java/org/apache/catalina/connector/Connector.java index 3b2c4dda7c..a86ea189e3 100644 --- a/tomcat/src/main/java/org/apache/catalina/connector/Connector.java +++ b/tomcat/src/main/java/org/apache/catalina/connector/Connector.java @@ -1,22 +1,31 @@ package org.apache.catalina.connector; -import org.apache.coyote.http11.Http11Processor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.IOException; import java.io.UncheckedIOException; import java.net.ServerSocket; import java.net.Socket; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.apache.coyote.http11.Http11Processor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class Connector implements Runnable { private static final Logger log = LoggerFactory.getLogger(Connector.class); private static final int DEFAULT_PORT = 8080; - private static final int DEFAULT_ACCEPT_COUNT = 100; + private static final int DEFAULT_ACCEPT_COUNT = 500; + + private static final int DEFAULT_MIN_SPARE_COUNT = 10; + private static final int DEFAULT_MAX_THREAD_COUNT = 250; + private static final int DEFAULT_MAX_QUEUE_COUNT = 100; private final ServerSocket serverSocket; + private final ExecutorService executorService; private boolean stopped; public Connector() { @@ -24,8 +33,15 @@ public Connector() { } public Connector(final int port, final int acceptCount) { + this(port, acceptCount, DEFAULT_MIN_SPARE_COUNT, DEFAULT_MAX_THREAD_COUNT); + } + + public Connector(final int port, final int acceptCount, final int minSpareCount, final int maxThreadCount) { this.serverSocket = createServerSocket(port, acceptCount); this.stopped = false; + this.executorService = new ThreadPoolExecutor(minSpareCount, maxThreadCount, + 60L, TimeUnit.SECONDS, + new ArrayBlockingQueue<>(DEFAULT_MAX_QUEUE_COUNT)); } private ServerSocket createServerSocket(final int port, final int acceptCount) { @@ -57,7 +73,7 @@ public void run() { private void connect() { try { process(serverSocket.accept()); - } catch (IOException e) { + } catch (IOException | RejectedExecutionException e) { log.error(e.getMessage(), e); } } @@ -67,13 +83,14 @@ private void process(final Socket connection) { return; } var processor = new Http11Processor(connection); - new Thread(processor).start(); + executorService.execute(processor); } public void stop() { stopped = true; try { serverSocket.close(); + executorService.shutdown(); } catch (IOException e) { log.error(e.getMessage(), e); } diff --git a/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java b/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java index d576d18130..a51d5bc2b7 100644 --- a/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java +++ b/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java @@ -2,15 +2,15 @@ import com.techcourse.model.User; import jakarta.servlet.http.HttpSession; -import java.util.HashMap; import java.util.Map; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import org.apache.catalina.Manager; import org.apache.coyote.http11.request.HttpRequest; public class SessionManager implements Manager { - private static final Map SESSIONS = new HashMap<>(); + private static final Map SESSIONS = new ConcurrentHashMap<>(); private static final SessionManager SESSION_MANAGER = new SessionManager(); private static final String ATTRIBUTE_USER_NAME = "user"; diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java index 5d68784e1b..fc7cda3d53 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -25,7 +25,7 @@ public Http11Processor(final Socket connection) { @Override public void run() { -// log.info("connect host: {}, port: {}", connection.getInetAddress(), connection.getPort()); + log.info("connect host: {}, port: {}", connection.getInetAddress(), connection.getPort()); process(connection); } diff --git a/tomcat/src/test/java/org/apache/catalina/connector/ConnectorTest.java b/tomcat/src/test/java/org/apache/catalina/connector/ConnectorTest.java new file mode 100644 index 0000000000..0fb4a6467c --- /dev/null +++ b/tomcat/src/test/java/org/apache/catalina/connector/ConnectorTest.java @@ -0,0 +1,51 @@ +package org.apache.catalina.connector; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.http.HttpResponse; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import support.TestHttpUtils; + +class ConnectorTest { + + @Test + @DisplayName("켜져있는 서버 메인 페이지를 600개 요청후 성공이 스레드 개수 250과 큐 100개 합친 350인지 확인한다.") + void request600() throws InterruptedException { + // given + int threadCount = 600; + int expectedCount = 350; + ExecutorService pool = Executors.newFixedThreadPool(threadCount); + List> futures = new ArrayList<>(); + + // when + for (int i = 0; i < threadCount; i++) { + futures.add(pool.submit(() -> { + HttpResponse response = TestHttpUtils.send("/index.html"); + return response.statusCode(); + })); + } + + pool.shutdown(); + pool.awaitTermination(5, TimeUnit.MINUTES); + + long successCount = futures.stream() + .filter(future -> { + try { + return future.get() == 200; + } catch (Exception e) { + return false; + } + }) + .count(); + + // then + assertThat(successCount).isEqualTo(expectedCount); + } +} diff --git a/tomcat/src/test/java/support/TestHttpUtils.java b/tomcat/src/test/java/support/TestHttpUtils.java new file mode 100644 index 0000000000..67f9cf92e8 --- /dev/null +++ b/tomcat/src/test/java/support/TestHttpUtils.java @@ -0,0 +1,29 @@ +package support; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; + +public class TestHttpUtils { + + private static final HttpClient httpClient = HttpClient.newBuilder() + .version(HttpClient.Version.HTTP_1_1) + .connectTimeout(Duration.ofSeconds(1)) + .build(); + + public static HttpResponse send(final String path) { + final var request = HttpRequest.newBuilder() + .uri(URI.create("http://localhost:8080" + path)) + .timeout(Duration.ofSeconds(1)) + .build(); + + try { + return httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + } catch (IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } +}