✔ 쓰레드의 이해와 쓰레드의 생성
쓰레드의 이해
- 쓰레드란 실행 중인 프로그램 내에서 또 다른 실행의 흐름을 형성하는 주체를 의미
- 현재까지 작성한 예제들은 하나의 쓰레드를 생성해서 main 메소드를 실행해 온 것
public static void main(String[] args) {
Thread ct = Thread.currentThread();
String name = ct.getName();
System.out.println(name); // 쓰레드의 이름인 main 반환
}
쓰레드의 생성
- main 쓰레드 이외의 쓰레드를 생성하면 추가한 수만큼 프로그램 내에서 다른 실행의 흐름이 만들어지게 됨
- 쓰레드의 생성을 위해 제일 먼저 할 일은 Runnable 인터페이스를 구현하는 클래스의 인스턴스를 생성하는 것
- 이후 이를 전달하여 Thread 인스턴스를 생성하게 되고
start 메소드를 호출하면 가상머신을 쓰레드를 생성해서 Thread 인스턴스 생성 시 전달된 run 메소드를 실행하게 함 - 생성된 쓰레드는 자신의 일을 마치면 자동으로 소멸되며, 모든 쓰레드가 일을 마치고 소멸되어야 프로그램이 종료됨
- 쓰레드의 실행을 잠깐 멈추고 잠을 자게 하는 sleep 메소드를 호출해 실행 속도를 늦출 수 있음
- 쓰레드는 각각 독립적으로 자신의 일을 실행해나가기 때문에 항상 같은 결과를 보장할 수 없음
public Thread(Runnable target, String name)
public static void sleep(long millis) throws InterruptedException
public static void main(String[] args) {
Runnable task = () -> { // 쓰레드가 실행하게 할 내용
try {
int n1 = 10;
int n2 = 20;
String name = Thread.currentThread().getName();
System.out.println(name + ": " + (n1 + n2)); // Thread-0: 30
Thread.sleep(100); // 0.1초간 잠을 잠
}
catch(InterruptedException e) {
e.printStackTrace();
}
};
Thread t = new Thread(task);
t.start(); // 쓰레드 생성 및 실행
System.out.println("End " + Thread.currentThread().getName()); // End main
}
- 앞의 Runnable을 구현한 인스턴스를 생성하고, Thread 인스턴스를 생성한 후, start 메소드를 호출하는 과정 말고도
Thread를 상속하는 클래스의 정의와 인스턴스를 생성하고 start 메소드를 호출하여 쓰레드를 생성하는 과정도 존재
class Task extend Thread {
public void run() { // 쓰레드가 실행하게 할 내용
int n1 = 10;
int n2 = 20;
String name = Thread.currentThread().getName();
System.out.println(name + ": " + (n1 + n2));
}
}
public static void main(String[] args) {
Task t1 = new Task();
t1.start(); // 쓰레드 생성 및 실행
System.out.println("End " + Thread.currentThread().getName());
}
✔ 쓰레드의 동기화
쓰레드의 메모리 접근 방식
- 둘 이상의 쓰레드가 하나의 메모리 공간(변수)에 접근했을 때 문제가 발생할 수 있음
- 값의 증가를 위해 두개의 쓰레드가 변수에 저장된 값을 동시에 가져간 후, 각각 증가시킨 값을 가져다 놓게 될 경우
99+1+1을 원하였지만, 99+1, 99+1이 되어 101이 아닌 100이 저장되게 됨 - 그러므로 둘 이상의 쓰레드가 동일한 메모리 공간에 접근해도 문제가 발생하지 않도록 동기화를 해야 함
- 값의 증가를 위해 두개의 쓰레드가 변수에 저장된 값을 동시에 가져간 후, 각각 증가시킨 값을 가져다 놓게 될 경우
public final void join() throws InterruptedException
class Counter {
int count = 0;
public void increment() {
count++;
}
public void decrement() {
count--;
}
public int getCount() {
return count;
}
}
class MutualAccess {
public static Counter cnt = new Counter();
public static void main(String[] args) {
Runnable task1 = () -> {
for(int i = 0; i < 1000; i++) {
cnt.increment();
}
};
Runnable task2 = () -> {
for(int i = 0; i < 1000; i++) {
cnt.decrement();
}
};
Thread t1 = new Thread(task1);
Thread t2 = new Thread(task2);
t1.start();
t2.start();
t1.join(); // t1이 참조하는 쓰레드의 종료를 기다림
t2.join(); // t2가 참조하는 쓰레드의 종료를 기다림
System.out.println(cnt.getCount()); // 실행할 때마다 출력되는 결과가 다름
}
}
동기화 메소드
- 동기화 방법에는 동기화 메소드인 synchronized 선언을 추가하는 것이 존재
- 이 메소드는 한 순간에 한 쓰레드의 접근만을 허용하게 되므로
두 쓰레드가 동시에 호출되면 조금이라도 빨리 호출한 쓰레드가 메소드를 실행하게 되고
다른 한 쓰레드는 대기하고 있다가 먼저 호출된 쓰레드가 실행을 마쳐야 비로소 메소드를 실행하게 됨
- 이 메소드는 한 순간에 한 쓰레드의 접근만을 허용하게 되므로
class Counter {
int count = 0;
sychronized public void increment() {
count++;
}
sychronized public void decrement() {
count--;
}
public int getCount() {
return count;
}
}
동기화 블록
- 앞의 동기화 메소드 기반의 동기화는 사용하기는 편하지만 메소드 전체에 동기화를 걸어야 한다는 단점이 존재
- 그러므로 동기화가 불필요한 부분을 실행하는 동안에도 다른 쓰레드의 접근을 막는 일이 발생하게 됨
- 또다른 동기화 방법에는 동기화 블록이 존재
- 동기화 블록은 둘 이상의 쓰레드에 의해 동시에 실행될 수 없도록 함께 동기화됨
class Counter {
int count = 0;
public void increment() {
sychronized(this) {
count++;
}
System.out.println("카운트 값이 1 증가하였습니다."); // 동기화 불필요한 문장
}
public void decrement() {
sychronized(this) {
count--;
}
System.out.println("카운트 값이 1 감소하였습니다."); // 동기화 불필요한 문장
}
public int getCount() {
return count;
}
}
✔ 쓰레드를 생성하는 더 좋은 방법
더 좋은 방법의 쓰레드의 생성
- 자바 5에서 쓰레드 관련 concurrent 패키지를 추가하여 더 간단히 쓰레드를 생성하고 더 강력하게 쓰레드를 활용할 수 있게 됨
- 쓰레드의 생성과 소멸을 시스템에 부담을 주는 일이므로 처리해야 할 일이 있을 때마다 쓰레드를 생성하는 것은 성능의 저하
- 그러므로 쓰레드 풀이라는 것을 만들고 그 안에 미리 제한된 수의 쓰레드를 생성해 두고 이를 활용할 수 있도록 함
- 처리해야 할 작업이 있을 때 풀에서 쓰레드를 꺼내 그 작업을 처리한 후, 작업을 끝낸 쓰레드는 다시 풀로 돌아가 대기
- 다양한 유형의 쓰레드 풀을 생성할 수 있음
- newSingleThreadExecutor : 풀 안에 하나의 쓰레드만 생성하고 유지
- newFixedThreadPool : 풀 안에 인자로 전달된 수의 쓰레드를 생성하고 유지
- newCachedThreadPool : 풀 안의 쓰레드의 수를 작업의 수에 맞게 유동적으로 관리
- 그러므로 쓰레드 풀이라는 것을 만들고 그 안에 미리 제한된 수의 쓰레드를 생성해 두고 이를 활용할 수 있도록 함
public static void main(String[] args) {
Runnable task = () -> { // 쓰레드가 실행하게 할 내용
int n1 = 10;
int n2 = 20;
String name = Thread.currentThread().getName();
System.out.println(name + ": " + (n1 + n2));
};
ExecutorService exr = Executors.newSingleThreadExecutor(); // 쓰레드 풀 생성
exr.submit(task); // 쓰레드 풀에 작업을 전달
System.out.println("End " + Thread.currentThread().getName());
exr.shutdown(); // 쓰레드 풀과 그 안에 있는 쓰레드 소멸
}
Callable & Future
- 앞선 Runnable 인터페이스를 기반으로 쓰레드를 작성하면
Runnable에 위치한 추상 메소드 run의 반환형이 void이기 때문에 작업의 결과를 return문을 통해 반환하는 것이 불가능 - Callable 인터페이스를 기반으로 작업을 구성하면 작업의 끝에서 값을 반환하는 것이 가능
- 특히 반환형도 결정할 수 있음
- 이후 메소드의 반환 값을 Future<V>형 참조변수에 저장하여 쓰레드가 실행한 메소드의 반환 값을 얻을 수 있게 됨
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
public static void main(String[] args) throws InterruptedException, ExecurtionException {
Callable<Integer> task = () -> {
int sum = 0;
for(int i = 0; i < 10; i++) {
sum += i;
return sum;
}
ExecutorService exr = Executors.newSingleThreadExecutor();
Future<Integer> fur = exr.submit();
Integer r = fur.get(); // 쓰레드의 반환 값 획득
System.out.println("result: " + r);
exr.shutdown();
}
synchronized를 대신하는 ReentrantLock
- 동기화 블록과 동기화 메소드를 대신할 수 있는 ReentrantLock
- 한 쓰레드가 lock 메소드를 호출하고, 이어서 다음 문장을 실행하기 시작한 상태에서
다른 쓰레드가 lock 메소드를 호출하면 이 쓰레드는 lock 메소드를 반환하지 않고
먼저 lock 메소드를 호출한 쓰레드가 unlock 메소드를 호출할 때까지 그 자리에서 대기하게 됨 - 이때 shutdown 메소드는 쓰레드 풀에 전달된 작업이 마무리되면 풀을 폐쇄하라고 명령할 뿐 기다려주지 않으므로
쓰레드 풀에 전달된 작업의 최종 결과를 확인하기 위해서는 awaitTermination 문장을 넣어
쓰레드 풀에 전달된 모든 작업이 완료되거나, 작업이 완료되지는 않았지만 초를 기준으로 100을 세도록 하여
최종 결과를 확인하도록 할 수 있음
- 한 쓰레드가 lock 메소드를 호출하고, 이어서 다음 문장을 실행하기 시작한 상태에서
class Counter {
int count = 0;
ReentrantLock criticObj = new ReentrantLock();
public void increment() {
criticObj.lock();
try {
count++;
} finally {
criticObj.unlock();
}
}
public void decrement() {
criticObj.lock();
try {
count--;
} finally {
criticObj.unlock();
}
}
public int getCount() {
return count;
}
}
class MutualAccess {
public static Counter cnt = new Counter();
public static void main(String[] args) {
Runnable task1 = () -> {
for(int i = 0; i < 1000; i++) {
cnt.increment();
}
};
Runnable task2 = () -> {
for(int i = 0; i < 1000; i++) {
cnt.decrement();
}
};
ExecutorService exr = Executors.newFixedThreadPool(2);
exr.submit(task1);
exr.submit(task2);
exr.shutdown();
exr.awaitTermination(100, TimeUnit.SECONDS); // 쓰레드 풀에 전달된 작업이 끝나기를 100초간 기다림
}
}
컬렉션 인스턴스 동기화
- 동기화는 성능의 저하를 수반하므로 불필요한 동기화를 진행하지 않도록 하기 위해
컬렉션 프레임워크의 클래스 대부분도 동기화 처리가 되어 있지 않음- 따라서 쓰레드의 동시 접근에 안전하지 않음
- 대신에 Collecions의 메소드를 통해 동기화 방법을 제공하고 있음
- 또한 동기화된 컬렉션 인스턴스에 반복자를 통해 접근할 때도 동기화를 추가해야 함
public static <T> Set<T> synchronizedSet(Set<T> s)
public static <T> List<T> synchronizedList(List<T> list)
public static <K, V> Map<K, V> synchronizedMap(Map<K, V> m)
public static <T> Collection<T> synchronizedCollection(Collection<T> s)
List<String> list = Collections.synchronizedList(new ArrayList<String>());
Runnable task = () -> {
synchronized(list) {
ListIterator<String> itr = list.listIterator();
while(itr.hasNext())
itr.set(itr.next());
};
'Java-Spring > 열혈 Java 프로그래밍' 카테고리의 다른 글
[Java] 열혈 Java 프로그래밍 - 목차 (0) | 2023.08.19 |
---|---|
[Java] 자바 프로그램의 원리와 메모리 모델 (0) | 2023.08.19 |
[Java] NIO 그리고 NIO.2 (0) | 2023.08.19 |
[Java] I/O 스트림 (0) | 2023.08.19 |
[Java] 시각과 날짜의 처리 (0) | 2023.08.18 |
✔ 쓰레드의 이해와 쓰레드의 생성
쓰레드의 이해
- 쓰레드란 실행 중인 프로그램 내에서 또 다른 실행의 흐름을 형성하는 주체를 의미
- 현재까지 작성한 예제들은 하나의 쓰레드를 생성해서 main 메소드를 실행해 온 것
public static void main(String[] args) {
Thread ct = Thread.currentThread();
String name = ct.getName();
System.out.println(name); // 쓰레드의 이름인 main 반환
}
쓰레드의 생성
- main 쓰레드 이외의 쓰레드를 생성하면 추가한 수만큼 프로그램 내에서 다른 실행의 흐름이 만들어지게 됨
- 쓰레드의 생성을 위해 제일 먼저 할 일은 Runnable 인터페이스를 구현하는 클래스의 인스턴스를 생성하는 것
- 이후 이를 전달하여 Thread 인스턴스를 생성하게 되고
start 메소드를 호출하면 가상머신을 쓰레드를 생성해서 Thread 인스턴스 생성 시 전달된 run 메소드를 실행하게 함 - 생성된 쓰레드는 자신의 일을 마치면 자동으로 소멸되며, 모든 쓰레드가 일을 마치고 소멸되어야 프로그램이 종료됨
- 쓰레드의 실행을 잠깐 멈추고 잠을 자게 하는 sleep 메소드를 호출해 실행 속도를 늦출 수 있음
- 쓰레드는 각각 독립적으로 자신의 일을 실행해나가기 때문에 항상 같은 결과를 보장할 수 없음
public Thread(Runnable target, String name)
public static void sleep(long millis) throws InterruptedException
public static void main(String[] args) {
Runnable task = () -> { // 쓰레드가 실행하게 할 내용
try {
int n1 = 10;
int n2 = 20;
String name = Thread.currentThread().getName();
System.out.println(name + ": " + (n1 + n2)); // Thread-0: 30
Thread.sleep(100); // 0.1초간 잠을 잠
}
catch(InterruptedException e) {
e.printStackTrace();
}
};
Thread t = new Thread(task);
t.start(); // 쓰레드 생성 및 실행
System.out.println("End " + Thread.currentThread().getName()); // End main
}
- 앞의 Runnable을 구현한 인스턴스를 생성하고, Thread 인스턴스를 생성한 후, start 메소드를 호출하는 과정 말고도
Thread를 상속하는 클래스의 정의와 인스턴스를 생성하고 start 메소드를 호출하여 쓰레드를 생성하는 과정도 존재
class Task extend Thread {
public void run() { // 쓰레드가 실행하게 할 내용
int n1 = 10;
int n2 = 20;
String name = Thread.currentThread().getName();
System.out.println(name + ": " + (n1 + n2));
}
}
public static void main(String[] args) {
Task t1 = new Task();
t1.start(); // 쓰레드 생성 및 실행
System.out.println("End " + Thread.currentThread().getName());
}
✔ 쓰레드의 동기화
쓰레드의 메모리 접근 방식
- 둘 이상의 쓰레드가 하나의 메모리 공간(변수)에 접근했을 때 문제가 발생할 수 있음
- 값의 증가를 위해 두개의 쓰레드가 변수에 저장된 값을 동시에 가져간 후, 각각 증가시킨 값을 가져다 놓게 될 경우
99+1+1을 원하였지만, 99+1, 99+1이 되어 101이 아닌 100이 저장되게 됨 - 그러므로 둘 이상의 쓰레드가 동일한 메모리 공간에 접근해도 문제가 발생하지 않도록 동기화를 해야 함
- 값의 증가를 위해 두개의 쓰레드가 변수에 저장된 값을 동시에 가져간 후, 각각 증가시킨 값을 가져다 놓게 될 경우
public final void join() throws InterruptedException
class Counter {
int count = 0;
public void increment() {
count++;
}
public void decrement() {
count--;
}
public int getCount() {
return count;
}
}
class MutualAccess {
public static Counter cnt = new Counter();
public static void main(String[] args) {
Runnable task1 = () -> {
for(int i = 0; i < 1000; i++) {
cnt.increment();
}
};
Runnable task2 = () -> {
for(int i = 0; i < 1000; i++) {
cnt.decrement();
}
};
Thread t1 = new Thread(task1);
Thread t2 = new Thread(task2);
t1.start();
t2.start();
t1.join(); // t1이 참조하는 쓰레드의 종료를 기다림
t2.join(); // t2가 참조하는 쓰레드의 종료를 기다림
System.out.println(cnt.getCount()); // 실행할 때마다 출력되는 결과가 다름
}
}
동기화 메소드
- 동기화 방법에는 동기화 메소드인 synchronized 선언을 추가하는 것이 존재
- 이 메소드는 한 순간에 한 쓰레드의 접근만을 허용하게 되므로
두 쓰레드가 동시에 호출되면 조금이라도 빨리 호출한 쓰레드가 메소드를 실행하게 되고
다른 한 쓰레드는 대기하고 있다가 먼저 호출된 쓰레드가 실행을 마쳐야 비로소 메소드를 실행하게 됨
- 이 메소드는 한 순간에 한 쓰레드의 접근만을 허용하게 되므로
class Counter {
int count = 0;
sychronized public void increment() {
count++;
}
sychronized public void decrement() {
count--;
}
public int getCount() {
return count;
}
}
동기화 블록
- 앞의 동기화 메소드 기반의 동기화는 사용하기는 편하지만 메소드 전체에 동기화를 걸어야 한다는 단점이 존재
- 그러므로 동기화가 불필요한 부분을 실행하는 동안에도 다른 쓰레드의 접근을 막는 일이 발생하게 됨
- 또다른 동기화 방법에는 동기화 블록이 존재
- 동기화 블록은 둘 이상의 쓰레드에 의해 동시에 실행될 수 없도록 함께 동기화됨
class Counter {
int count = 0;
public void increment() {
sychronized(this) {
count++;
}
System.out.println("카운트 값이 1 증가하였습니다."); // 동기화 불필요한 문장
}
public void decrement() {
sychronized(this) {
count--;
}
System.out.println("카운트 값이 1 감소하였습니다."); // 동기화 불필요한 문장
}
public int getCount() {
return count;
}
}
✔ 쓰레드를 생성하는 더 좋은 방법
더 좋은 방법의 쓰레드의 생성
- 자바 5에서 쓰레드 관련 concurrent 패키지를 추가하여 더 간단히 쓰레드를 생성하고 더 강력하게 쓰레드를 활용할 수 있게 됨
- 쓰레드의 생성과 소멸을 시스템에 부담을 주는 일이므로 처리해야 할 일이 있을 때마다 쓰레드를 생성하는 것은 성능의 저하
- 그러므로 쓰레드 풀이라는 것을 만들고 그 안에 미리 제한된 수의 쓰레드를 생성해 두고 이를 활용할 수 있도록 함
- 처리해야 할 작업이 있을 때 풀에서 쓰레드를 꺼내 그 작업을 처리한 후, 작업을 끝낸 쓰레드는 다시 풀로 돌아가 대기
- 다양한 유형의 쓰레드 풀을 생성할 수 있음
- newSingleThreadExecutor : 풀 안에 하나의 쓰레드만 생성하고 유지
- newFixedThreadPool : 풀 안에 인자로 전달된 수의 쓰레드를 생성하고 유지
- newCachedThreadPool : 풀 안의 쓰레드의 수를 작업의 수에 맞게 유동적으로 관리
- 그러므로 쓰레드 풀이라는 것을 만들고 그 안에 미리 제한된 수의 쓰레드를 생성해 두고 이를 활용할 수 있도록 함
public static void main(String[] args) {
Runnable task = () -> { // 쓰레드가 실행하게 할 내용
int n1 = 10;
int n2 = 20;
String name = Thread.currentThread().getName();
System.out.println(name + ": " + (n1 + n2));
};
ExecutorService exr = Executors.newSingleThreadExecutor(); // 쓰레드 풀 생성
exr.submit(task); // 쓰레드 풀에 작업을 전달
System.out.println("End " + Thread.currentThread().getName());
exr.shutdown(); // 쓰레드 풀과 그 안에 있는 쓰레드 소멸
}
Callable & Future
- 앞선 Runnable 인터페이스를 기반으로 쓰레드를 작성하면
Runnable에 위치한 추상 메소드 run의 반환형이 void이기 때문에 작업의 결과를 return문을 통해 반환하는 것이 불가능 - Callable 인터페이스를 기반으로 작업을 구성하면 작업의 끝에서 값을 반환하는 것이 가능
- 특히 반환형도 결정할 수 있음
- 이후 메소드의 반환 값을 Future<V>형 참조변수에 저장하여 쓰레드가 실행한 메소드의 반환 값을 얻을 수 있게 됨
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
public static void main(String[] args) throws InterruptedException, ExecurtionException {
Callable<Integer> task = () -> {
int sum = 0;
for(int i = 0; i < 10; i++) {
sum += i;
return sum;
}
ExecutorService exr = Executors.newSingleThreadExecutor();
Future<Integer> fur = exr.submit();
Integer r = fur.get(); // 쓰레드의 반환 값 획득
System.out.println("result: " + r);
exr.shutdown();
}
synchronized를 대신하는 ReentrantLock
- 동기화 블록과 동기화 메소드를 대신할 수 있는 ReentrantLock
- 한 쓰레드가 lock 메소드를 호출하고, 이어서 다음 문장을 실행하기 시작한 상태에서
다른 쓰레드가 lock 메소드를 호출하면 이 쓰레드는 lock 메소드를 반환하지 않고
먼저 lock 메소드를 호출한 쓰레드가 unlock 메소드를 호출할 때까지 그 자리에서 대기하게 됨 - 이때 shutdown 메소드는 쓰레드 풀에 전달된 작업이 마무리되면 풀을 폐쇄하라고 명령할 뿐 기다려주지 않으므로
쓰레드 풀에 전달된 작업의 최종 결과를 확인하기 위해서는 awaitTermination 문장을 넣어
쓰레드 풀에 전달된 모든 작업이 완료되거나, 작업이 완료되지는 않았지만 초를 기준으로 100을 세도록 하여
최종 결과를 확인하도록 할 수 있음
- 한 쓰레드가 lock 메소드를 호출하고, 이어서 다음 문장을 실행하기 시작한 상태에서
class Counter {
int count = 0;
ReentrantLock criticObj = new ReentrantLock();
public void increment() {
criticObj.lock();
try {
count++;
} finally {
criticObj.unlock();
}
}
public void decrement() {
criticObj.lock();
try {
count--;
} finally {
criticObj.unlock();
}
}
public int getCount() {
return count;
}
}
class MutualAccess {
public static Counter cnt = new Counter();
public static void main(String[] args) {
Runnable task1 = () -> {
for(int i = 0; i < 1000; i++) {
cnt.increment();
}
};
Runnable task2 = () -> {
for(int i = 0; i < 1000; i++) {
cnt.decrement();
}
};
ExecutorService exr = Executors.newFixedThreadPool(2);
exr.submit(task1);
exr.submit(task2);
exr.shutdown();
exr.awaitTermination(100, TimeUnit.SECONDS); // 쓰레드 풀에 전달된 작업이 끝나기를 100초간 기다림
}
}
컬렉션 인스턴스 동기화
- 동기화는 성능의 저하를 수반하므로 불필요한 동기화를 진행하지 않도록 하기 위해
컬렉션 프레임워크의 클래스 대부분도 동기화 처리가 되어 있지 않음- 따라서 쓰레드의 동시 접근에 안전하지 않음
- 대신에 Collecions의 메소드를 통해 동기화 방법을 제공하고 있음
- 또한 동기화된 컬렉션 인스턴스에 반복자를 통해 접근할 때도 동기화를 추가해야 함
public static <T> Set<T> synchronizedSet(Set<T> s)
public static <T> List<T> synchronizedList(List<T> list)
public static <K, V> Map<K, V> synchronizedMap(Map<K, V> m)
public static <T> Collection<T> synchronizedCollection(Collection<T> s)
List<String> list = Collections.synchronizedList(new ArrayList<String>());
Runnable task = () -> {
synchronized(list) {
ListIterator<String> itr = list.listIterator();
while(itr.hasNext())
itr.set(itr.next());
};
'Java-Spring > 열혈 Java 프로그래밍' 카테고리의 다른 글
[Java] 열혈 Java 프로그래밍 - 목차 (0) | 2023.08.19 |
---|---|
[Java] 자바 프로그램의 원리와 메모리 모델 (0) | 2023.08.19 |
[Java] NIO 그리고 NIO.2 (0) | 2023.08.19 |
[Java] I/O 스트림 (0) | 2023.08.19 |
[Java] 시각과 날짜의 처리 (0) | 2023.08.18 |