❓
물음표살인마 블로그
  • README
  • ALGORITHM
    • Sieve of Eratosthenes
    • Round Up
    • Binary Search
    • Union Find
    • Sorting Array
    • Lcm, Gcd
  • TechTalk Review
    • Template
  • Books
    • CS Note for Interview
      • Ch1. Design Pattern & Programming paradigm
        • 1.1.1 Singleton Pattern
        • 1.1.2 Factory Pattern
        • 1.1.4 Observer Pattern
        • 1.1.5 Proxty Pattern & Proxy Server
        • 1.1.8 Model-View-Controller Pattern
        • 1.2.1 Declarative and Functional Programming
        • 1.2.2 Object Oriented Programming
      • Ch2. Network
        • 2.2.1 TCP/IP Four-Layer Model
        • 2.2.1-1 TCP 3, 4 way handshake
        • 2.3 Network Devices L4, L7
        • 2.4.1 ARP, RARP
        • 2.4.2 Hop By Hop Communication
        • 2.4.3 IP Addressing Scheme
      • Ch3. Operating System
        • 3.1.1 Roles and Structure of Operating Systems
        • 3.2.1 Memory Hierarchy
        • 3.2.2 Memory Management
        • 3.3.1 Processes and Compilation Process
        • 3.3.3 Memory Structure of a Process
        • 3.3.4 Process Control Block (PCB)
        • 3.3.5 Multiprocessing
        • 3.3.6 Threads and Multithreading
        • 3.3.7 Shared Resources and Critical Sections
        • 3.3.8 Deadlock
        • 3.4 CPU Scheduling Algorithm
      • Ch4. Database
        • 4.1 Database Basic
        • 4.2 Normalization
        • 4.3 Transaction and Integrity
        • 4.4 Types of Databases
        • 4.5 Indexes
        • 4.6 Types of Joins
        • 4.7 Principles of Joins
      • Ch5. Data Structure
    • Learning the Basics of Large-Scale System Design through Virtual Interview Cases
      • 1. Scalability based on user counts(1/2)
      • 1. Scalability based on user counts(2/2)
      • 2.Back-of-the-envelope estimation
      • 3. Strategies for System Design Interviews
      • 4. Rate Limiter
      • 5. Consistent Hashing
      • 6. Key-Value System Design
      • 7. Designing a Unique ID Generator for Distributed Systems
      • 8. Designing a URL Shortener
      • 9. Designing a Web Crawler
      • 10. Notification System Design
      • 11. Designing a News Feed System
      • 12. Chat System Design
      • 13. AutoComplete
      • 14. Design YouTube
      • 15. Design Google Drive
      • Loadbalancer Algorithms
      • Cache tier
      • CDN, Content Delivery Network
      • Stateless Web tier
    • Computer System A programmer's perspective
    • Effective Java
      • Item 1. Consider Static Factory Methods Instead of Constructors
      • Item 2. Consider a Builder When Faced with Many Constructor Parameters
      • Item 3. Ensure Singleton with Private Constructor or Enum Type
      • Item 4. Enforce Noninstantiability with a Private Constructor
      • Item 5. Prefer Dependency Injection to Hardwiring Resources
      • Item 6. Avoid Creating Unnecessary Objects
      • Item 7. Eliminate Obsolete Object References
      • Item 8. Avoid Finalizers and Cleaners
      • Item 9.Prefer try-with-resources to try-finally
      • Item10. Adhering to General Rules When Overriding equals
        • Handling Transitivity Issues
        • Ensuring Consistency
      • Item11. Override hashCode When You Override equals
      • Item12. Always Override toString
        • Always Override toString
      • Item13. Override Clone Judiciously
      • Item14. Consider Implementing Comparable
      • Item15. Minimize the Accessibility of Classes and Members
      • Item16. Accessor Methods Over Public Fields
      • Item17. Minimize Mutability
      • Item18. Composition over inherentance
      • Item19. Design and Document for Inheritance, or Else Prohibit It
      • Item20. Prefer Interfaces to Abstract Classes
      • Item21. Design Interfaces with Implementations in Mind
      • Item22. Use Interfaces Only to define Types
      • Item23. Prefer Class Hierarchies to Tagged Classes
      • Item24. Favor Static Member Classes Over Non-Static
      • Item28. Use Lists Instead of Arrays
      • Item29. Prefer Generic Types
      • Item30. Favor Generic Methods
    • Head First Design Patterns
      • Ch1. Strategy Pattern
      • Ch2. Observer Pattern
        • Ver1. Ch2. Observer Pattern
      • Ch3. Decorator Pattern
        • Ch3. Decorator Pattern
      • Ch4. Factory Pattern
      • Ch5. Singleton Pattern
      • Ch6. Command Pattern
      • Ch7. Adapter and Facade Pattern
      • Ch8. Template Method Pattern
    • Digging Deep into JVM
      • Chapter 2. Java Memory Area & Memory Overflow
      • Chapter 3. Garbage Collector & Memory Allocation Strategy (1/2)
      • Chapter 3. Garbage Collector & Memory Allocation Strategy (2/2)
      • Chapter 5. Optimization Practice
      • Chapter 6. Class file structure
      • Chapter 8. Bytecode Executor Engine (1/2)
  • Interview Practices
    • Restful API Practices
      • Url Shortener API
      • Event Ticket Reservation API
      • Course Management API
      • Search posts by tags API
      • Online Code platform API
      • Simple Task Management API
      • Event Participation API
      • Review System API
      • Car management API
      • Online Library
    • Tech Review
      • if(kakao)
        • Kakao Account Cache Migration / if(kakao)2022
        • Improving the Anomaly Detection System for KakaoTalk Messaging Metrics / if(kakao) 2022
        • Standardizing API Case Handling Without Redeployment / if(kakaoAI)2024
        • JVM warm up / if(kakao)2022
    • Naver Computer Science
      • Process & Thread
      • TCP & UDP
      • Spring & Servlet
      • Filter & Interceptor & AOP
      • Equals() & ==
      • Dependency Injection
      • Object Oriented Programming
  • F-Lab
    • Week1
      • Client & Server
      • HTTP
      • TCP/UDP
      • REST API
      • Questions
        • Object Oriented Programming
        • HTTP
        • Process & Thread
        • Data Structure
    • Week2
      • OSI 7 layer
      • Web vs WAS
    • Week3
      • RDB vs NoSQL
      • RDB Index
      • Cache
      • Redis
      • Messaging Queue
    • Week4
      • Project - Ecommerce
    • Week5
      • ERD - 1
    • Week6
      • Ecommerce - 2
      • Role
      • pw hashing && Salt
      • CreatedAt, ModifiedAt
      • JWT
      • Copy of ERD - 1
    • Week7
      • Vault (HashiCorp Vault)
    • Week 8
      • Api Endpoints
    • Week10
      • Product Create Workflow
  • TOY Project
    • CodeMentor
      • Implementation of Kafka
      • Project Improvement (Architectural Enhancements)
      • Communication between servers in msa
  • JAVA
    • MESI protocol in CAS
    • CAS (Compare and Set)
    • BlockingQueue
    • Producer & Consumer
    • Synchronized && ReentrantLock
    • Memory Visibility
    • Checked vs Unchecked Exception
    • Thread
    • Batch delete instead of Cascade
    • Java Questions
      • Week 1(1/2) - Basic Java
      • Week 1(2/2) - OOP
      • Week 2(1/2) - String, Exception, Generic
      • Week2(2/2) Lambda, Stream, Annotation, Reflection
      • Week3(1/2) Collections
      • Week3(2/2) Threads
      • Week4 Java Concurrency Programming
      • Week5 JVM & GC
    • Java 101
      • JVM Structure
      • Java Compiles and Execution Method
      • Override, Overload
      • Interface vs Abstract Class
      • Primitive vs Object Type
      • Identity and equality
      • String, StringBuilder, StringBuffer
      • Checked Exceptions and Unchecked Exceptions
      • Java 8 methods
      • Try-with-reources
      • Strong Coupling and Loose Coupling
      • Serialization and Deserialization
      • Concurrency Programming in Java
      • Mutable vs Immutable
      • JDK vs JRE
  • SPRING
    • DIP. Dependency Inversion Principal
    • Ioc container, di practice
    • @Transactional
    • Proxy Pattern
    • Strategy Pattern
    • Template Method Pattern
    • using profile name as variable
    • Spring Questions
      • Spring Framework
      • Spring MVC & Web Request
      • AOP (Aspect-Oriented Programming)
      • Spring Boot
      • ORM & Data Access
      • Security
      • ETC
  • DATABASE
    • Enhancing Query Performance & Stability - User list
    • Ensuring Data Consistency, Atomicity and UX Optimization (feat.Firebase)
    • Redis: Remote Dictionary Server
    • Database Questions
      • Week1 DBMS, RDBMS basics
      • Week2 SQL
      • Week3 Index
      • Week4 Anomaly, Functional Dependency, Normalization
      • Week5 DB Transaction, Recovery
    • Normalization
      • 1st Normal Form
      • 2nd Normal Form
      • 3rd Normal Form
  • NETWORK
    • HTTP & TCP head of line blocking
    • HTTP 0.9-3.0
    • Blocking, NonBlocking and Sync, Async
    • Network Questions
      • Week1 Computer Network Basic
      • Week2(1/3) Application Layer Protocol - HTTP
      • Week2(2/3) Application Layer Protocol - HTTPS
      • Week2(3/3) Application Layer Protocol - DNS
      • Week3 Application Layer
      • Week4 Transport Layer - UDP, TCP
      • Week5 Network Layer - IP Protocol
    • Network 101
      • https://www.google.com
      • TCP vs UDP
      • Http vs Https
      • TLS Handshake 1.2
      • HTTP Method
      • CORS & SOP
      • Web Server Software
  • OS
    • Operating System Questions
      • Week1 OS & How Computer Systems Work
      • Week2(1/2) Process
      • Week2(2/2) Thread
      • Week3 CPU Scheduling
      • Week4 Process Synchronize
      • Week5 Virtual Memory
    • Operating System 101
      • Operating system
        • The role of the operating system
        • The composition of the operating system.
      • Process
        • In Linux, are all processes except the initial process child processes?
        • Zombie process, orphan process
        • (Linux) Daemon process
        • Process address space
        • Where are uninitialized variables stored?
        • Determination of the size of the Stack and Heap
        • Access speed of Stack vs Heap
        • Reason for memory space partitioning
        • Process of compiling a process
        • sudo kill -9 $CURRENT_PID
      • Thread
        • Composition of a thread's address space
      • Process vs Thread
        • Creation of processes and threads in Linux
      • Multiprocessing
        • Web Browser
        • Implementation of multiprocessing
        • Application areas of multiprocessing
      • Multithreading
        • Application areas of multithreading
      • Interrupt
        • HW / SW Interrupt
        • Method of handling interrupts
        • Occurrence of two or more interrupts simultaneously
      • Polling
      • Dual Mode
        • Reason for distinguishing between user mode and kernel mode
      • System call
        • Differentiation between system calls
        • Types of system calls
        • Execution process of a system call
      • Process Control Block (PCB)
        • PCB의 구조
        • 쓰레드는 PCB를 갖고 있을까?
        • 프로세스 메모리 구조
      • Context switching
        • Timing of context switching
        • Registers saved during context switching
        • Context switching in processes
        • Context switching in threads
        • Difference between context switching in processes and threads
        • Information of the current process during context switching
      • Interprocess Communication (IPC)
        • Cases where IPC is used
        • Process address space in IPC Shared Memory technique
        • Types of IPC
  • COMPUTER SCIENCE
    • Computer Architecture 101
      • 3 components of a computer
      • RAM vs ROM
      • CPU vs GPU
      • SIMD
      • Two's complement
      • Harvard Architecture vs. von Neumann Architecture
      • The structure of a CPU.
      • Instruction cycle (CPU operation method)
      • Instruction pipelining
      • Bus
      • Memory area
      • Memory hierarchy structure
        • Reason for using memory hierarchy structure
      • Cache memory
      • L1, L2, L3 Cache
      • Locality of reference (cache)
      • Fixed-point vs Floating-point
        • epresentation of infinity and NaN (Not a Number) in floating-point
      • RISC vs CISC
      • Hamming code
      • Compiler
      • Linking
      • Compiler vs Interpreter
      • Mutex vs Semaphore
      • 32bit CPU and 64bit CPU
      • Local vs Static Variable
      • Page
  • Programming Paradigm
    • Declarative vs Imperative
  • JPA, QueryDsl
    • why fetchResults() is deprecated
  • PYTHON
    • Icecream
  • FASTAPI
    • Template Page
  • LINUX
    • Template Page
  • DATA STRUCTURE
    • Counting Sort
    • Array vs Linked List
  • GIT, Github
    • git clone, invalid path error
  • INFRA
    • Template Page
  • AWS
    • Server Log Archive Pipeline
    • Image Processing using Lambda
  • DOCKER
    • Docker and VM
    • Python Executable Environment
    • Docker commands
  • docker-compose
    • Kafka, Multi Broker
  • KUBERNATES
    • !Encountered Errors
      • my-sql restarts
      • kafka producer: disconnected
    • Kubernetes Components
    • Helm
      • Helm commands
    • Pod network
    • Service network
      • deployment.yaml
      • services.yaml
    • Service type
      • Cluster IP
      • NodePort
    • service-name-headless?
    • kube-proxy
  • GraphQL
    • Template Page
  • WEB
    • Template Page
  • Reviews
    • Graphic Intern Review
    • Kakao Brain Pathfinder Review
    • JSCODE 자바 1기 Review
  • 😁Dev Jokes
    • Image
      • Plot twist
      • Priorities
      • SQL join guide
      • Google is generous
      • Genie dislikes cloud
      • buggy bugs
      • last day of unpaid internship
      • what if clients know how to inspect
      • its just game
      • how i wrote my achievement on resume
      • self explanatory
      • chr(sum(range(ord(min(str(not))))))
Powered by GitBook
On this page
  • 1. 출금 예제
  • 1.1. 공유 자원에 대한 동시 접근 시나리오
  • 1.2. 임계영역과Synchronized
  • 1.3. Synchronized 코드 블록
  • 1.4. Synchronized의 단점
  • 2. concurrent.Lock
  • 2.1. LockSupport
  • 2.2. Interrupt 사용
  • 2.3. 시간 대기
  • 3. Lock 인터페이스와 ReentrantLock
  • 3.1. Lock 인터페이스
  • 3.2. ReentrantLock - 이론
  • 3.3. ReentrantLock - 활용
  • 3.4. ReentrantLock - 대기 중단
  1. JAVA

Synchronized && ReentrantLock

Synchronized && ReentrantLock

1. 출금 예제

package thread.sync;

public interface BankAccount {
    boolean withdraw(int amount);
    int getBalance();
}
package thread.sync;

public class WithdrawTask implements Runnable {
    private BankAccount account;
    private int amount;

    public WithdrawTask(BankAccount account, int amount) {
        this.account = account;
        this.amount = amount;
    }

    @Override
    public void run() {
        account.withdraw(amount);
    }
}
package thread.sync;

import static util.MyLogger.log;
import static util.ThreadUtils.sleep;

public class BankMain {
    public static void main(String[] args) throws InterruptedException {
        BankAccount account = new BankAccountV1(1000);

        Thread t1 = new Thread(new WithdrawTask(account, 800), "t1");
        Thread t2 = new Thread(new WithdrawTask(account, 800), "t2");
        t1.start();
        t2.start();

        sleep(500);
        log("t1 state: " + t1.getState());
        log("t2 state: " + t2.getState());

        t1.join();
        t2.join();
        log("최종 잔액: " + account.getBalance());
    }
}

(초기 셋업 코드)

1.1. 공유 자원에 대한 동시 접근 시나리오

package thread.sync;

import static util.MyLogger.log;
import static util.ThreadUtils.sleep;

public class BankAccountV1 implements BankAccount {
    private int balance;

    public BankAccountV1(int balance) {
        this.balance = balance;
    }

    @Override
    public boolean withdraw(int amount) {
        log("거래 시작: " + getClass().getSimpleName());
        log("[검증 시작] 출금액: " + amount + ", 잔액: " + balance);
        if (balance < amount) {
            log("[검증 실패] 출금액: " + amount + ", 잔액: " + balance);
            return false;
        }
        log("[검증 완료] 출금액: " + amount + ", 잔액: " + balance);
        sleep(1000); // 출금에 걸리는 시간으로 가정
        balance = balance - amount;
        log("[출금 완료] 출금액: " + amount + ", 변경 잔액: " + balance);
        log("거래 종료");
        return true;
    }

    @Override
    public int getBalance() {
        return balance;
    }
}
// 실행 결과
12:16:55:211 [       t1] 거래 시작: BankAccountV1
12:16:55:210 [       t2] 거래 시작: BankAccountV1
12:16:55:221 [       t1] [검증 시작] 출금액: 800, 잔액: 1000
12:16:55:221 [       t2] [검증 시작] 출금액: 800, 잔액: 1000
12:16:55:222 [       t1] [검증 완료] 출금액: 800, 잔액: 1000
12:16:55:222 [       t2] [검증 완료] 출금액: 800, 잔액: 1000
12:16:55:688 [     main] t1 state: TIMED_WAITING
12:16:55:688 [     main] t2 state: TIMED_WAITING
12:16:56:237 [       t1] [출금 완료] 출금액: 800, 변경 잔액: 200
12:16:56:237 [       t2] [출금 완료] 출금액: 800, 변경 잔액: -600
12:16:56:238 [       t1] 거래 종료
12:16:56:238 [       t2] 거래 종료
12:16:56:242 [     main] 최종 잔액: -600

위 상황에서는 동시에 2개의 스레드를 통해서 출금을 하고 있음

  • t1, t2 스레드가거의 동시의 실행됨

과정:

t1: 잔액(1000)이 출금액(800)보다 많으므로 검증 로직 통과

t2: 잔액(1000)이 출금액(800)보다 많으므로 검증 로직 통과

t1: balance(1000) - amount(800) = 200

t2: balance(200) - amount(800) = -600

다른 경우가 발생할 수도 있다.

12:31:10.885 [       t2] 거래 시작: BankAccountV1
12:31:10.885 [       t1] 거래 시작: BankAccountV1
12:31:10.892 [       t1] [검증 시작] 출금액: 800, 잔액: 1000
12:31:10.892 [       t2] [검증 시작] 출금액: 800, 잔액: 1000
12:31:10.893 [       t1] [검증 완료] 출금액: 800, 잔액: 1000
12:31:10.893 [       t2] [검증 완료] 출금액: 800, 잔액: 1000
12:31:11.370 [     main] t1 state: TIMED_WAITING
12:31:11.370 [     main] t2 state: TIMED_WAITING
12:31:11.898 [       t2] [출금 완료] 출금액: 800, 변경 잔액: 200
12:31:11.898 [       t1] [출금 완료] 출금액: 800, 변경 잔액: 200
12:31:11.899 [       t2] 거래 종료
12:31:11.899 [       t1] 거래 종료
12:31:11.905 [     main] 최종 잔액: 200

이러한 경우에는 balance = balance - amount부분에서 문제가 발생.

  • t1: 잔액(1000)을 읽음

  • t2:잔액(1000)을 읽음

  • t1: 출금(800) 후 잔액(200) 저장

  • t2: 출금(800) 후 잔액(200) 저장

정리하자면 두가지 케이스로 나뉜다.

  1. t1 쓰레드의 출금 후 잔액 저장 전에, t2 쓰레드의 출금 여부 확인 통과.

  2. t1 쓰레드의 출금 후 잔액 저장 전에, t2 쓰레드가 잔액을 불러왔을때.

1.2. 임계영역과Synchronized

1.2.1. 임계영역(Critical Section)

  • 여러 스레드가 동시에 접근하면 데이터 불일치나 예상치 못한 동작이 발생할 수 있는 코드 부분

  • 여러 스레드가 동시에 접근해서는 안되는 공유 자원을 접근하거나 수정하는 부분 (공유 변수나 공유 객체 수정)

1.2.2. Synchronized

위 상황을 피하기 위해서는 synchronized 키워드를 사용할 수 있다.

package thread.sync;

import static util.MyLogger.log;
import static util.ThreadUtils.sleep;

public class BankAccountV2 implements BankAccount {
    private int balance;

    public BankAccountV2(int balance) {
        this.balance = balance;
    }

    @Override
    public synchronized boolean withdraw(int amount) {
        log("거래 시작: " + getClass().getSimpleName());
        log("[검증 시작] 출금액: " + amount + ", 잔액: " + balance);
        if (balance < amount) {
            log("[검증 실패] 출금액: " + amount + ", 잔액: " + balance);
            return false;
        }
        log("[검증 완료] 출금액: " + amount + ", 잔액: " + balance);
        sleep(1000); // 출금에 걸리는 시간으로 가정
        balance = balance - amount;
        log("[출금 완료] 출금액: " + amount + ", 변경 잔액: " + balance);
        log("거래 종료");
        return true;
    }

    @Override
    public int getBalance() {
        return balance;
    }
}
  • 단순히 메서드 앞에 synchronized를 붙여주면 된다.

  • withdraw 메서드 전체가 "보호받는 임계영역"이 된다. 한번에 하나의 스레드만 접근 가능.

// 실행 결과
13:58:39:633 [       t1] 거래 시작: BankAccountV2
13:58:39:643 [       t1] [검증 시작] 출금액: 800, 잔액: 1000
13:58:39:643 [       t1] [검증 완료] 출금액: 800, 잔액: 1000
13:58:40:108 [     main] t1 state: TIMED_WAITING
13:58:40:108 [     main] t2 state: BLOCKED
13:58:40:645 [       t1] [출금 완료] 출금액: 800, 변경 잔액: 200
13:58:40:645 [       t1] 거래 종료
13:58:40:646 [       t2] 거래 시작: BankAccountV2
13:58:40:646 [       t2] [검증 시작] 출금액: 800, 잔액: 200
13:58:40:647 [       t2] [검증 실패] 출금액: 800, 잔액: 200
13:58:40:650 [     main] 최종 잔액: 200
  • t1이 모두 수행될떄까지 t2는 메서드를 시작할 수 없게 된다.

  • t1이 실행될동안 t2는 BLOCKED 상태이다.

  • 현재는 두개의 스레드만 존재하지만, 여러 스레드가 있을때 락을 획득하는 다음 스레드는 랜덤이다.

  • synchronized안에서 접근하는 변수의 메모리 가시성 문제는 volatile이 없어도 해결됨(JMM의 Happens-Before)

1.3. Synchronized 코드 블록

위 코드를 자세히 보면, 보호가 필요없는 임계영역이 존재한다. 메서드 전체가 아닌 특정 코드 부분만 보호되는 임계영역으로 지정할 수 있다.

package thread.sync;

import static util.MyLogger.log;
import static util.ThreadUtils.sleep;

public class BankAccountV3 implements BankAccount {
    private int balance;

    public BankAccountV3(int balance) {
        this.balance = balance;
    }

    @Override
    public boolean withdraw(int amount) {
        log("거래 시작: " + getClass().getSimpleName());

        synchronized(this) {
            log("[검증 시작] 출금액: " + amount + ", 잔액: " + balance);
            if (balance < amount) {
                log("[검증 실패] 출금액: " + amount + ", 잔액: " + balance);
                return false;
            }
            log("[검증 완료] 출금액: " + amount + ", 잔액: " + balance);
            sleep(1000); // 출금에 걸리는 시간으로 가정
            balance = balance - amount;
            log("[출금 완료] 출금액: " + amount + ", 변경 잔액: " + balance);
        }

        log("거래 종료");
        return true;
    }

    @Override
    public int getBalance() {
        return balance;
    }
}
  • synchronized(this)를 사용하여 코드 영역을 지정할 수 있다.

// 실행 결과
14:17:46:387 [       t1] 거래 시작: BankAccountV3
14:17:46:387 [       t2] 거래 시작: BankAccountV3
14:17:46:397 [       t1] [검증 시작] 출금액: 800, 잔액: 1000
14:17:46:397 [       t1] [검증 완료] 출금액: 800, 잔액: 1000
14:17:46:863 [     main] t1 state: TIMED_WAITING
14:17:46:863 [     main] t2 state: BLOCKED
14:17:47:399 [       t1] [출금 완료] 출금액: 800, 변경 잔액: 200
14:17:47:399 [       t1] 거래 종료
14:17:47:399 [       t2] [검증 시작] 출금액: 800, 잔액: 200
14:17:47:400 [       t2] [검증 실패] 출금액: 800, 잔액: 200
14:17:47:404 [     main] 최종 잔액: 200
  • "거래 시작" 로그는 동시에 여러 스레드가 접근해도 문제가 없기때문에, 이 부분을 제외했다.

  • 로그를 봤을때 t2의 "거래 시작" 까지는 실행되는것을 볼 수 있다.

1.4. Synchronized의 단점

  • 무한 대기: BLOCKED 상태의 스레드는 락이 풀릴 때 까지 무한 대기

    • 특정 시간까지만 대기하는 타임아웃 불가

    • 중간에 인터럽트 불가

  • 공정성: 락이 돌아왔을 때 BLOCKED 상태의 여러 스레드 중에 어떤 스레드가 락을 획득할 지 알 수 없다. 최악의 경우 스레드가 너무 오랜기간 락을 획득하지 못할 수 있다.

조금 더 정교한 설정이 필요하면 java.util.concurrent 동시성 문제 해결을 위한 패키지를 사용한다.

2. concurrent.Lock

java.util.concurrent 라이브러리 패키지는 synchronized의 단점들을 보안하기 위해 만들어졌다.

2.1. LockSupport

LockSupport는 기본적인 스레드 동기화 메커니즘을 제공하는 클래스입니다. 이 클래스는 낮은 레벨의 잠금을 처리하며 스레드를 잠들게 하거나 깨우는 메서드를 제공합니다.

LockSupport의 주요 기능들입니다:

  • park(): 현재 스레드를 WAITING 상태로 만듭니다. 다른 스레드에 의해 깨워질 때까지 멈춰 있습니다.

  • unpark(Thread thread): 지정된 스레드를 WAITING 상태에서 RUNNABLE로 변경

  • parkNanos(long nanos): 주어진 시간(나노 초) 동안 스레드를 TIMED_WAITING상태로 변경합니다. 시간이 완료되면 자동으로 깨어납니다.

  • parkUntil(long deadline): 지정된 시점까지 스레드를 멈춥니다. 이 기능은 주로 타임스탬프 기반으로 스레드를 일시 중지하고 싶을 때 유용합니다.

package thread.sync.lock;

import java.util.concurrent.locks.LockSupport;

import static util.MyLogger.log;
import static util.ThreadUtils.sleep;

public class LockSupportMainV1 {
    public static void main(String[] args) {
        Thread thread1 = new Thread(new ParkTask(), "Thread-1");
        thread1.start();

        // 잠시 대기하여 Thread-1이 park 상태에 빠질 시간을 준다.
        sleep(100);
        log("Thread-1 state: " + thread1.getState());

        log("main -> unpark(Thread-1)");
        LockSupport.unpark(thread1); // 1. unpark 사용
        //thread1.interrupt();  // 2. interrupt() 사용
    }

    static class ParkTask implements Runnable {
        @Override
        public void run() {
            log("park 시작");
            LockSupport.park();
            log("park 종료, state: " + Thread.currentThread().getState());
            log("인터럽트 상태: " + Thread.currentThread().isInterrupted());
        }
    }
}
// 실행 결과
14:29:32:062 [ Thread-1] park 시작
14:29:32:150 [     main] Thread-1 state: WAITING
14:29:32:151 [     main] main -> unpark(Thread-1)
14:29:32:151 [ Thread-1] park 종료, state: RUNNABLE
14:29:32:155 [ Thread-1] 인터럽트 상태: false
  • main 스레드가 Thread-1을 start() 하면 Thread-1은 RUNNABLE 상태가 된다.

  • Thread-1은 Thread-park()를 호출. Thread-1은 RUNNABLE -> WAITING 상태가 된다.

  • main 스레드가 Thread-1을 깨운다. Thread-1은 대기 상태에서 실행 가능 상태로 변한다. WAITING -> RUNNABLE

  • park()와 unpark()의 차이는, park는 해당 스레드가 실행하고, unpark는 다른 스레드에 의해서 실행되어야한다.

2.2. Interrupt 사용

14:32:31:506 [ Thread-1] park 시작
14:32:31:580 [     main] Thread-1 state: WAITING
14:32:31:580 [     main] main -> unpark(Thread-1)
14:32:31:581 [ Thread-1] park 종료, state: RUNNABLE
14:32:31:586 [ Thread-1] 인터럽트 상태: true

LockSupport.unpark(thread1) 대신, thread1.interrupt()를 사용하게되어도 깨울 수 있지만 인터럽트 상태가 true인걸 확인할 수 있다.

2.3. 시간 대기

parkNanos(nanos)를 사용한 경우.

package thread.sync.lock;

import java.util.concurrent.locks.LockSupport;

import static util.MyLogger.log;
import static util.ThreadUtils.sleep;

public class LockSupportMainV2 {
    public static void main(String[] args) {
        Thread thread1 = new Thread(new ParkTask(), "Thread-1");
        thread1.start();

        // 잠시 대기하여 thread1이 park 상태에 빠질 시간을 준다.
        sleep(100);
        log("Thread-1 state: " + thread1.getState());
    }

    static class ParkTask implements Runnable {
        @Override
        public void run() {
            log("park 시작, 2초 대기");
            LockSupport.parkNanos(2000_000000); // parkNanos 사용
            log("park 종료, state: " + Thread.currentThread().getState());
            log("인터럽트 상태: " + Thread.currentThread().isInterrupted());
        }
    }
}
14:34:49:069 [ Thread-1] park 시작, 2초 대기
14:34:49:149 [     main] Thread-1 state: TIMED_WAITING
14:34:51:084 [ Thread-1] park 종료, state: RUNNABLE
14:34:51:088 [ Thread-1] 인터럽트 상태: false
  • 따로 unpark를 해주지 않아도, 2초후에 꺠어난다.

BLOCKED vs WAITING

WAITING 상태에 특정한 시간까지만 대기하는 기능이 포함된 것이 TIMED_WAITING. 둘을 묶어서 WAITING 상태라고 가정.

  • 인터럽트

    • BLOCKED 상태는 인터럽트가 걸려도 대기 상태를 빠져나오지 못한다. 여전히 BLOCKED 상태

    • WAITING, TIMED_WAITING 상태는 인터럽트가 걸리면 대기 상태를 빠져 나온다. RUNNABLE 상태로 변경.

  • 용도

    • BLOCKED 상태: 자바의 synchronized에서 락을 획득하기 위해 대기할 때 사용.

    • WAITING, TIMED_WAITING 상태: 스레드가 특정 조건이나 시간 동안 대기할 때 발생하는 상태

    • WAITING 상태: 다양한 상황에서 사용된다. 예를 들어, Thread.join(), LockSupport.park(), Object.wait()과 같은 메서드 호출시 WAITING 상태가 된다.

    • TIMED_WAITING 상태: Thread.sleep(ms), Object.wait(long timeout), Thread.join(long millis), LockSupport.parkNanos(ns) 등과 같은 시간 제한이 있는 대기 메서드 호출시 발생.

  • Thread.join() <--> Thread.join(millis)

  • Thread.park() <--> Thread.parkNanos(long millis)

  • Object.wait() <--> Object.wait(long timeout)

이러한 기능을 직접 구현하기는 어렵다. 여러 스레드가 대기하고 있는 상태에서 다음 스레드를 선정하는 것, 우선순위를 부여하는 것 등의 세밀한 작업은 LockSupport로 하기에는 synchronized 보다 더 저수준의 기능이다. 이를 해결하기 위해 Lock 인터페이스와 ReentrantLock이 존재한다.

3. Lock 인터페이스와 ReentrantLock

3.1. Lock 인터페이스

자바1.0 Synchronized와 BLOCKED 상태를 통한 임계 영역 관리의 한계를 극복하기 위해 자바1.5부터 Lock 인터페이스와 ReentrantLock 구현체가 등장했다.

Lock 인터페이스를 통해서 synchronized의 단점인 무한 대기 문제 해결

여기서의 Lock은 객체 내부에 있는 모니터 락이랑 다른 락이다. Lock 인터페이스와 ReentrantLock이 제공하는 기능이다.

Lock 인터페이스는 아래와 같다:

package java.util.concurrent.locks;

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}
  • void lock()

    • 락을 획득. 만약 다른 스레드가 이미 락을 획득 했다면, 락이 풀릴때까지 대기(WAITING) 한다. 인터럽트에 응답하지 않음

    • 맛집에 줄을 서면 끝까지 기다린다. 친구가 다른 맛집을 찾았다고 중간에 연락해도 포기하지 않고 기다림.

    • WAITING 상태의 스레드에 인터럽트가 발생하면 WAITING 상태를 빠져나오는게 정상. 근데 lock()의 경우에는 다시 WAITING으로 변경시킨다. 결국 인터럽트를 무시

  • void lockInterruptibly()

    • 락 획득을 시도하되, 다른 스레드가 인터럽트 할 수 있다. 락을 획득할 수 없다면 획득할때까지 대기한다. 대기중 인터럽트가 발생하면 InterruptException이 발생하며 락 획득을 포기

    • 맛집에 줄을 서서 기다린다. 친구가 다른 맛집을 찾았다고 중간에 연락하면 포기한다.

  • boolean tryLock()

    • 락 획득을 시도하고, 즉시 성공 여부 반환. 다른 스레드가 이미 락을 획득하면 false를 반환

    • true 반환 -> 락 획득 -> 임계 구역 코드 실행

    • false 반환 -> 포기

    • 맛집에 대기 줄이 없으면 바로 들어가고, 대기 줄이 있으면 즉시 포기

  • boolean tryLock(long time, TimeUnit unit)

    • 주어진 시간동안 락 획득 시도. 주어진 시간 안에 락을 획득하면 true 반환. 주어진 시간이 지나도 락을 획득하지 못한 경우 false를 반환.

    • 인터럽트가 발생하면 InterruptedException이 발생하며 락 획득 포기

    • 맛집에 줄을 서지만 특정 시간 만큼만 기다린다. 특정 시간이 지난 후에도 계속 줄을 서야 하면 포기. 친구가 다른 맛집을 찾았다고 중간에 연락해도 포기

  • Condition newCondition()

    • Condition 객체를 생성하여 반환. Condition 객체는 락과 결합되어 사용되며, 스레드가 특정 조건을 기다리거나 신호를 받을 수 있도록 한다. Object 클래스의 wait, notify, notifyAll 메서드와 유사함

3.2. ReentrantLock - 이론

공정성을 해결하기 위해 사용된다. Lock 인터페이스의 구현체 중 가장 대표적. 공정하게 스레드가 락을 얻을 수 있도록 한다.

// 사용 예시
package thread.sync.locks.reentrantLock;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockEx {
    // 비공정 모드 락
    private final Lock nonFairLock = new ReentrantLock();
    // 공정 모드 락
    private final Lock fairLock = new ReentrantLock(true);
    public void nonFairLockTest() {
        nonFairLock.lock();
        try {
            // 임계 영역
        } finally {
            nonFairLock.unlock();
        }
    }
    public void fairLockTest() {
        fairLock.lock();
        try {
            // 임계 영역
        } finally {
            fairLock.unlock();
        }
    }
}
```

ReentrantLock은 공정성(fairness) 모드와 비공정성(non-fair) 모드로 설정 가능.

  • 공정 모드(fair mode)

    • 생성자에 true를 전달.

    • 락을 요청한 순서대로 스레드가 락을 획득할 수 있게 한다. FIFO 방식으로 공정성을 보장하지만 성능 저하

  • 비공정 모드(non-fair mode)

    • 공정모드보다 락을 획득하는 속도가 빠르다

    • 새로운 스레드가 기존 대기 스레드보다 먼저 락을 획득할 수 있다.

    • starvation이 발생할 수도 있다 (다만 내부적으로는 queue가 사용되어서, 정확히 락을 반환한 시점에 새로운 스레드가 오지 않는 이상 크게 문제는 없다고 함)

  • 정리: 비공정 모드 vs 공정모드 - 성능 중시 vs 순서 보장

3.3. ReentrantLock - 활용

이를 출금 예제에 활용해본다.

package thread.sync;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import static util.MyLogger.log;
import static util.ThreadUtils.sleep;

public class BankAccountV4 implements BankAccount {
    private int balance;

    private final Lock lock = new ReentrantLock();

    public BankAccountV4(int balance) {
        this.balance = balance;
    }

    @Override
    public boolean withdraw(int amount) {
        log("거래 시작: " + getClass().getSimpleName());

        lock.lock();
        try {
            log("[검증 시작] 출금액: " + amount + ", 잔액: " + balance);
            if (balance < amount) {
                log("[검증 실패] 출금액: " + amount + ", 잔액: " + balance);
                return false;
            }
            log("[검증 완료] 출금액: " + amount + ", 잔액: " + balance);
            sleep(1000); // 출금에 걸리는 시간으로 가정
            balance = balance - amount;
            log("[출금 완료] 출금액: " + amount + ", 변경 잔액: " + balance);
        } finally {
            lock.unlock(); // ReentrantLock을 사용할 때는 반드시 unlock()을 호출해야 한다.
        }

        log("거래 종료");
        return true;
    }

    @Override
    public int getBalance() {
        lock.lock();
        try {
            return balance;
        } finally {
            lock.unlock();
        }

    }
}
// 실행 결과
17:10:50:919 [       t1] 거래 시작: BankAccountV4
17:10:50:919 [       t2] 거래 시작: BankAccountV4
17:10:50:929 [       t1] [검증 시작] 출금액: 800, 잔액: 1000
17:10:50:930 [       t1] [검증 완료] 출금액: 800, 잔액: 1000
17:10:51:397 [     main] t1 state: TIMED_WAITING
17:10:51:397 [     main] t2 state: WAITING
17:10:51:945 [       t1] [출금 완료] 출금액: 800, 변경 잔액: 200
17:10:51:945 [       t1] 거래 종료
17:10:51:946 [       t2] [검증 시작] 출금액: 800, 잔액: 200
17:10:51:946 [       t2] [검증 실패] 출금액: 800, 잔액: 200
17:10:51:949 [     main] 최종 잔액: 200
  • private final Lock lock = new ReentrantLock()을 사용하도록 선언

  • 임계 영역이 끝나면 반드시 락을 반납해야한다. lock.unlock()은 반드시 finally에 작성.

  • t2는 WAITING 상태이다. BLOCKED 상태는 synchronized에서만 사용된다.

  • t2는 락을 획득하지 못해서 x002 대기큐에 들어가게 된다(이때 락은 모니터 락이 아님)

3.4. ReentrantLock - 대기 중단

ReentrantLock을 사용하면 락을 무한 대기하지 않고, 중간에 빠져나오는게 가능. 락을 얻을 수 없다면 즉시 빠져나오기도 가능.

  • boolean tryLock()

    • 락 획득을 시도하고 즉시 성공 여부를 반환. 만약 다른 스레드가 이미 락을 획득했다면 false 반환. 그렇지 않으면 락을 획득하고 true 반환.

    • 맛집에 대기 줄이 없으면 바로 들어가고, 대기 줄이 있으면 즉시 포기

  • boolean tryLock(long time, TimeUnit unit)

    • 주어진 시간 동안 락 획득을 시도한다. 주어진 시간 안에 락을 획득하면 true 반환. 주어진 시간이 지나도 락을 획득하지 못한 경우 false를 반환. 대기중 인터럽트가 발생하면 InterruptedException이 발생하며 락 획득을 포기한다.

    • 맛집에 줄을 서지만 특정 시간 만큼 기다린다. 특정 시간이 지나도 계속 줄을 서야 한다면 포기한다. 친구가 다른 맛집을 찾았다고 중간에 연락해도 포기한다.

tryLock 예시

package thread.sync;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import static util.MyLogger.log;
import static util.ThreadUtils.sleep;

public class BankAccountV5 implements BankAccount {
    private int balance;

    private final Lock lock = new ReentrantLock();

    public BankAccountV5(int balance) {
        this.balance = balance;
    }

    @Override
    public boolean withdraw(int amount) {
        log("거래 시작: " + getClass().getSimpleName());

        if (!lock.tryLock()) {
            log("[진입 실패] 이미 처리중인 작업이 있습니다.");
            return false;
        }
        
        try {
            log("[검증 시작] 출금액: " + amount + ", 잔액: " + balance);
            if (balance < amount) {
                log("[검증 실패] 출금액: " + amount + ", 잔액: " + balance);
                return false;
            }
            sleep(1000);
            balance = balance - amount;
            log("[출금 완료] 출금액: " + amount + ", 변경 잔액: " + balance);
        } finally {
            lock.unlock();
        }

        log("거래 종료");
        return true;
    }

    @Override
    public int getBalance() {
        lock.lock();
        try {
            return balance;
        } finally {
            lock.unlock();
        }

    }
}
// 실행 결과
17:18:47:025 [       t1] 거래 시작: BankAccountV5
17:18:47:025 [       t2] 거래 시작: BankAccountV5
17:18:47:029 [       t2] [진입 실패] 이미 처리중인 작업이 있습니다.
17:18:47:036 [       t1] [검증 시작] 출금액: 800, 잔액: 1000
17:18:47:503 [     main] t1 state: TIMED_WAITING
17:18:47:503 [     main] t2 state: TERMINATED
17:18:48:051 [       t1] [출금 완료] 출금액: 800, 변경 잔액: 200
17:18:48:051 [       t1] 거래 종료
17:18:48:054 [     main] 최종 잔액: 200
  • t2의 상태가 TERMINATED인것을 볼 수 있다.

tryLock(시간) 예시

package thread.sync;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import static util.MyLogger.log;
import static util.ThreadUtils.sleep;

public class BankAccountV6 implements BankAccount {
    private int balance;

    private final Lock lock = new ReentrantLock();

    public BankAccountV6(int balance) {
        this.balance = balance;
    }

    @Override
    public boolean withdraw(int amount) {
        log("거래 시작: " + getClass().getSimpleName());

        try {
            if (!lock.tryLock(500, TimeUnit.MILLISECONDS)) {
                log("[진입 실패] 이미 처리중인 작업이 있습니다.");
                return false;
            }
        } catch (InterruptedException e) {
            log("[진입 실패] 다른 스레드에 의해 인터럽트 되었습니다.");
            return false;
        }


        try {
            log("[검증 시작] 출금액: " + amount + ", 잔액: " + balance);
            if (balance < amount) {
                log("[검증 실패] 출금액: " + amount + ", 잔액: " + balance);
                return false;
            }
            sleep(1000);
            balance = balance - amount;
            log("[출금 완료] 출금액: " + amount + ", 변경 잔액: " + balance);
        } finally {
            lock.unlock();
        }

        log("거래 종료");
        return true;
    }

    @Override
    public int getBalance() {
        lock.lock();
        try {
            return balance;
        } finally {
            lock.unlock();
        }

    }
}
// 실행 결과
17:22:01:143 [       t2] 거래 시작: BankAccountV6
17:22:01:143 [       t1] 거래 시작: BankAccountV6
17:22:01:154 [       t2] [검증 시작] 출금액: 800, 잔액: 1000
17:22:01:620 [     main] t1 state: TIMED_WAITING
17:22:01:620 [     main] t2 state: TIMED_WAITING
17:22:01:651 [       t1] [진입 실패] 이미 처리중인 작업이 있습니다.
17:22:02:161 [       t2] [출금 완료] 출금액: 800, 변경 잔액: 200
17:22:02:161 [       t2] 거래 종료
17:22:02:165 [     main] 최종 잔액: 200
  • t1는 TIMED_WAITING 상태로 있다가, TERMINATED가 된다.

  • 0.5초간 락을 획득하지 못함으로 false를 반환하게 된다.

PreviousProducer & ConsumerNextMemory Visibility

Last updated 5 months ago