Java 심층 학습(5): 잠금

21458 단어
다시 잠금 가능:
간단하게 말하면 중복 잠금 지원, 중용성
특징: 자물쇠 전달 가능, 방법 귀속 전달
목적: 잠금 현상을 피했다
코드:
public class Test implements Runnable {

    @Override
    public void run() {
        method1();
    }

    public synchronized void method1() {
        System.out.println("method1");
        method2();
    }

    public synchronized void method2() {
        System.out.println("method2");
    }

    public static void main(String[] args) {
        new Thread(new Test()).start();
    }

}

인쇄:
method1
method2

분석: 만약 자물쇠를 다시 사용할 수 없다면 여기에 사라진 자물쇠 문제가 발생할 것이다
 
ReentrantLock 잠금 사용:
public class TestLock implements Runnable {
    //   
    private Lock reentrantLock = new ReentrantLock();

    @Override
    public void run() {
        method1();
    }

    public void method1() {
        try {
            reentrantLock.lock();
            System.out.println("method1");
            method2();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            reentrantLock.unlock();
        }
    }

    public void method2() {
        try {
            reentrantLock.lock();
            System.out.println("method2");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            reentrantLock.unlock();
        }
    }

    public static void main(String[] args) {
        new Thread(new TestLock()).start();
    }

}

 
읽기 및 쓰기 잠금:
높은 병발 시 쓰기 작업과 동시에 읽기 작업을 허용하지 않아야 한다
(다중 스레드 중: 읽기-읽기 공존, 읽기-쓰기-쓰기 공존 불가)
코드:읽기 및 쓰기 작업의 라인 보안 문제 만들기
public class TestWriteLock {

    Map cache = new HashMap<>();

    //    
    public void put(String key, String value) {
        try {
            System.out.println("    key : " + key + " value : " + value);
            Thread.sleep(50);
            cache.put(key, value);
            System.out.println("    key : " + key + " value : " + value);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //    
    public String get(String key) {
        System.out.println("    key : " + key);
        String value = cache.get(key);
        System.out.println("    key : " + key + " value : " + value);
        return value;
    }

    public static void main(String[] args) {
        TestWriteLock test = new TestWriteLock();
        Thread readThread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    test.put("i", i + "");
                }
            }
        });

        Thread writeThread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    test.get("i");
                }
            }
        });

        readThread.start();
        writeThread.start();
    }
}

관찰 인쇄: 불합리한 발견
    key : i value : 0
    key : i
    key : i value : null
.................................

분석: 쓰기가 완료되지 않았을 때 읽기 시작했는데 결과는 비어 있습니다
 
해결 방법:
1.synchronized를 사용하면 해결할 수 있지만 효율이 낮아 쓰기를 동시에 읽을 수 없어 막힌다
2. 읽기 및 쓰기 자물쇠 사용
public class TestWriteLock {

    Map cache = new HashMap<>();

    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
    ReentrantReadWriteLock.ReadLock readLock = lock.readLock();

    //    
    public void put(String key, String value) {
        try {
            writeLock.lock();
            System.out.println("    key : " + key + " value : " + value);
            Thread.sleep(50);
            cache.put(key, value);
            System.out.println("    key : " + key + " value : " + value);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            writeLock.unlock();
        }
    }

    //    
    public String get(String key) {
        String value = "";
        try {
            readLock.lock();
            System.out.println("    key : " + key);
            value = cache.get(key);
            System.out.println("    key : " + key + " value : " + value);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readLock.unlock();
        }
        return value;
    }

    public static void main(String[] args) {
        TestWriteLock test = new TestWriteLock();
        Thread readThread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    test.put("i", i + "");
                }
            }
        });

        Thread writeThread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    test.get("i");
                }
            }
        });
        readThread.start();
        writeThread.start();
    }
}

관찰 인쇄: 완벽한 해결
 
낙관적 잠금:
쉽게 말하면 낙관적 자물쇠는 자물쇠가 없고 막히지 않고 기다림이 없다
SQL 문의 예:
UPDATE TABLE SET X=X+1,VERSION=VERSION+1 WHERE ID=#{id} AND VERSION=#{version}

높은 병발지 상황에서 초기 버젼이 1이라고 가정하면 요청 1이 오고 id와 버젼에 따라 찾을 수 있기 때문에 업데이트를 허용합니다
요청 2가 동시에 작동하지만 id와version에 따라 찾을 수 없습니다 (요청 1이 수정되었습니다). 업데이트를 허용하지 않습니다.
 
비관 잠금:
간단하게 말하면 웨이트급 자물쇠가 막히고 기다린다
자물쇠를 잠근 후 한 라인만 조작할 수 있다는 뜻으로 이해할 수 있다. 즉 자바의synchronized이다
 
원자 클래스:
아날로그 스레드 보안 문제의 코드:
public class ThreadTest implements Runnable {

    private static int count = 1;

    @Override
    public void run() {
        while (true) {
            Integer count = getCount();
            if (count >= 100) {
                break;
            }
            System.out.println(count);
        }
    }

    public synchronized Integer getCount() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return count++;
    }

    public static void main(String[] args) {
        ThreadTest t = new ThreadTest();
        new Thread(t).start();
        new Thread(t).start();
    }

}

인쇄를 관찰한 결과 과연 라인 안전에 문제가 생겼음을 발견하였다
 
일종의 수정 방식: 효율이 비교적 낮다
    public synchronized Integer getCount() {

 
원자류 사용: 낙관적 잠금, 밑바닥 잠금 없음, CAS 잠금 없음 기술 사용
public class ThreadTest implements Runnable {

    //     
    private AtomicInteger atomicInteger = new AtomicInteger();

    @Override
    public void run() {
        while (true) {
            Integer count = getCount();
            if (count >= 100) {
                break;
            }
            System.out.println(count);
        }
    }

    public Integer getCount() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return atomicInteger.incrementAndGet();
    }

    public static void main(String[] args) {
        ThreadTest t = new ThreadTest(); 
        new Thread(t).start();
        new Thread(t).start();
    }

}

 
CAS 잠금 해제 기술(Compare And Swap):
비교 재교환
로컬 메모리에 공유 메모리의 복사본 저장
예를 들어 메인 메모리에 i=0이 있으면 두 라인의 로컬 메모리로 복사합니다
두 라인이 i++를 실행하면 로컬 메모리가 i=1로 바뀌고 메인 메모리로 리셋됩니다
 
CAS 알고리즘:
CAS(V, E, N) 매개변수는 다음과 같습니다.
V는 업데이트할 변수(기본 메모리)를 나타냅니다.
E는 예상 값(로컬 메모리)을 나타냅니다.
N은 새 값(새 값)을 나타냅니다.
V 값이 E 값인 경우에만 (기본 메모리=로컬 메모리) V 값을 N으로 설정합니다.
만약 V 값과 E 값이 다르다면 (메인 메모리! = 로컬 메모리), 이미 다른 라인이 업데이트되었음을 의미하며, 현재 라인은 아무것도 하지 않습니다.
마지막으로 CAS는 현재 V의 실제 값을 반환합니다.
 
원자류의 원본 코드 관찰하기
    /** 
     * Atomically increments by one the current value. 
     * 
     * @return the updated value 
     */  
    public final int incrementAndGet() {  
        for (;;) {  
            //       
            int current = get();  
            //       
            int next = current + 1;  
            //  Native  compareAndSet,  CAS    
            if (compareAndSet(current, next))  
                //
                return next;  
        }  
    }  

 
CAS 잠금 해제 메커니즘의 단점:
1. 데드사이클
2. ABA 문제: 변수 V가 처음 읽을 때 A이고 값을 부여하려고 준비할 때 A인 것을 검사하면 값이 다른 라인에서 수정되지 않았음을 설명할 수 있습니까
(이 기간 동안 B로 변경된 후에 A로 다시 바뀌면 CAS 작업은 수정된 적이 없다고 오해할 수 있습니다. 이 경우 자바 패키지는 표시된 원자 인용류인 Atomic Stamped Reference를 제공합니다. 변수 값을 제어하는 버전을 통해 CAS의 정확성을 확보할 수 있습니다.)
 
JVM 데이터 동기화: 분산 잠금
 
자전거 자물쇠와 상호 배척 자물쇠의 차이점:
비관과 낙관 자물쇠의 차이, 자전거 자물쇠는 사순환으로 막히지 않으며, 상호 배척 자물쇠는 같은 시간에 한 라인만 데이터에 접근한다.

좋은 웹페이지 즐겨찾기