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);
+ }
+ }
+}