Java ReentrantReadWriteLocks-안전하게 쓰기 잠금을 획득하는 방법은 무엇입니까?
현재 내 코드에서 ReentrantReadWriteLock 을 사용하여 트리와 같은 구조에 대한 액세스를 동기화하고 있습니다. 이 구조는 크고 작은 부분을 가끔 수정하여 한 번에 여러 스레드에서 읽으므로 읽기-쓰기 관용구에 잘 맞는 것 같습니다. 이 특정 클래스를 사용하면 읽기 잠금을 쓰기 잠금으로 높일 수 없으므로 Javadocs에 따라 쓰기 잠금을 얻기 전에 읽기 잠금을 해제해야합니다. 이전에 재진입이 아닌 컨텍스트에서이 패턴을 성공적으로 사용했습니다.
그러나 내가 찾은 것은 영원히 차단하지 않고는 쓰기 잠금을 안정적으로 얻을 수 없다는 것입니다. 읽기 잠금이 재진입 가능하고 실제로 그대로 사용하고 있으므로 간단한 코드
lock.getReadLock().unlock();
lock.getWriteLock().lock()
재진입으로 readlock을 획득 한 경우 차단할 수 있습니다. 잠금을 해제하기위한 각 호출은 보류 카운트를 줄이며 잠금은 보류 카운트가 0에 도달 할 때만 실제로 해제됩니다.
내가 처음에 너무 잘 설명하지 않았다고 생각하기 때문에 이것을 명확히하기 위해 편집하십시오 .이 클래스에는 기본 제공 잠금 에스컬레이션이 없으며 읽기 잠금을 해제하고 쓰기 잠금을 획득해야한다는 것을 알고 있습니다. 내 문제는 다른 스레드가 수행하는 작업에 관계없이 호출 이 재진입으로 획득 한 경우이 스레드의 잠금을 getReadLock().unlock()
실제로 해제하지 않을 수 있다는 것입니다.이 경우 호출 은이 스레드가 여전히 읽기에 보류를 가지고 있기 때문에 영원히 차단됩니다. 잠금 및 따라서 자체 차단.getWriteLock().lock()
예를 들어,이 코드 조각은 잠금에 액세스하는 다른 스레드가없는 단일 스레드로 실행되는 경우에도 println 문에 도달하지 않습니다.
final ReadWriteLock lock = new ReentrantReadWriteLock();
lock.getReadLock().lock();
// In real code we would go call other methods that end up calling back and
// thus locking again
lock.getReadLock().lock();
// Now we do some stuff and realise we need to write so try to escalate the
// lock as per the Javadocs and the above description
lock.getReadLock().unlock(); // Does not actually release the lock
lock.getWriteLock().lock(); // Blocks as some thread (this one!) holds read lock
System.out.println("Will never get here");
그래서이 상황을 처리 할 수있는 좋은 관용구가 있습니까? 특히, 읽기 잠금 (재진입 가능성이 있음)을 보유한 스레드가 쓰기 작업을 수행해야 함을 발견하고 쓰기 잠금을 선택하기 위해 자체 읽기 잠금을 "일시 중단"하려고 할 때 (다른 스레드에서 필요에 따라 차단) 읽기 잠금에 대한 보류를 해제 한 다음 나중에 동일한 상태에서 읽기 잠금에 대한 보류를 "선택"하시겠습니까?
이 ReadWriteLock 구현은 재진입을 위해 특별히 설계 되었기 때문에 재진입으로 잠금을 획득 할 수있을 때 읽기 잠금을 쓰기 잠금으로 승격시킬 수있는 합리적인 방법이 있습니까? 이것은 순진한 접근 방식이 작동하지 않는다는 것을 의미하는 중요한 부분입니다.
나는 이것에 대해 약간의 진전을 이루었습니다. 잠금 변수 ReentrantReadWriteLock
를 단순히 a ReadWriteLock
(이상적이지 않지만이 경우에는 필요한 악) 대신 명시 적으로 선언 하여 getReadHoldCount()
메서드를 호출 할 수 있습니다 . 이를 통해 현재 스레드에 대한 보류 수를 얻을 수 있으므로 readlock을 여러 번 해제 할 수 있습니다 (나중에 동일한 수로 다시 획득). 따라서 이것은 빠르고 더러운 테스트에서 알 수 있듯이 작동합니다.
final int holdCount = lock.getReadHoldCount();
for (int i = 0; i < holdCount; i++) {
lock.readLock().unlock();
}
lock.writeLock().lock();
try {
// Perform modifications
} finally {
// Downgrade by reacquiring read lock before releasing write lock
for (int i = 0; i < holdCount; i++) {
lock.readLock().lock();
}
lock.writeLock().unlock();
}
그래도이게 내가 할 수있는 최선일까요? 그다지 우아하지 않은 느낌이 들고, 덜 "수동적 인"방식으로 이것을 처리 할 수있는 방법이 여전히 있기를 바라고 있습니다.
당신이 원하는 것은 가능해야합니다. 문제는 Java가 읽기 잠금을 쓰기 잠금으로 업그레이드 할 수있는 구현을 제공하지 않는다는 것입니다. 특히 javadoc ReentrantReadWriteLock은 읽기 잠금에서 쓰기 잠금으로의 업그레이드를 허용하지 않는다고 말합니다.
어쨌든 Jakob Jenkov는이를 구현하는 방법을 설명합니다. 자세한 내용은 http://tutorials.jenkov.com/java-concurrency/read-write-locks.html#upgrade 를 참조하십시오.
읽기-쓰기 잠금 업그레이드가 필요한 이유
읽기에서 쓰기 잠금으로 업그레이드하는 것은 유효합니다 (다른 답변의 반대 주장에도 불구하고). 교착 상태가 발생할 수 있으므로 구현의 일부는 교착 상태를 인식하고 스레드에서 예외를 발생시켜 교착 상태를 해제하는 코드입니다. 즉, 트랜잭션의 일부로 작업을 다시 수행하여 DeadlockException을 처리해야합니다. 일반적인 패턴은 다음과 같습니다.
boolean repeat;
do {
repeat = false;
try {
readSomeStuff();
writeSomeStuff();
maybeReadSomeMoreStuff();
} catch (DeadlockException) {
repeat = true;
}
} while (repeat);
이 기능이 없으면 일관되게 데이터를 읽은 다음 읽은 내용을 기반으로 무언가를 쓰는 직렬화 가능한 트랜잭션을 구현하는 유일한 방법은 시작하기 전에 쓰기가 필요할 것으로 예상하여 모든 데이터에 대한 WRITE 잠금을 얻는 것입니다. 작성해야 할 내용을 쓰기 전에 읽으십시오. 이것은 Oracle이 사용하는 KLUDGE입니다 (SELECT FOR UPDATE ...). 또한 트랜잭션이 실행되는 동안 다른 사람이 데이터를 읽거나 쓸 수 없기 때문에 실제로 동시성을 줄입니다!
특히 쓰기 잠금을 얻기 전에 읽기 잠금을 해제하면 일관성없는 결과가 생성됩니다. 중히 여기다:
int x = someMethod();
y.writeLock().lock();
y.setValue(x);
y.writeLock().unlock();
someMethod () 또는 호출하는 메서드가 y에 대한 재진입 읽기 잠금을 생성하는지 여부를 알아야합니다! 당신이 알고 있다고 가정합시다. 그런 다음 먼저 읽기 잠금을 해제하는 경우 :
int x = someMethod();
y.readLock().unlock();
// problem here!
y.writeLock().lock();
y.setValue(x);
y.writeLock().unlock();
다른 스레드는 읽기 잠금을 해제 한 후 쓰기 잠금을 획득하기 전에 y를 변경할 수 있습니다. 따라서 y의 값은 x와 같지 않습니다.
테스트 코드 : 읽기 잠금을 쓰기 잠금 블록으로 업그레이드 :
import java.util.*;
import java.util.concurrent.locks.*;
public class UpgradeTest {
public static void main(String[] args)
{
System.out.println("read to write test");
ReadWriteLock lock = new ReentrantReadWriteLock();
lock.readLock().lock(); // get our own read lock
lock.writeLock().lock(); // upgrade to write lock
System.out.println("passed");
}
}
Java 1.6을 사용한 출력 :
read to write test
<blocks indefinitely>
이것은 오래된 질문이지만 여기에 문제에 대한 해결책과 몇 가지 배경 정보가 있습니다.
다른 사람들이 지적했듯이 기존의 독자-작성기 잠금 (예 : JDK ReentrantReadWriteLock )은 교착 상태에 취약하기 때문에 읽기 잠금을 쓰기 잠금으로 업그레이드하는 것을 본질적으로 지원하지 않습니다.
읽기 잠금을 먼저 해제하지 않고 안전하게 쓰기 잠금을 획득해야하는 경우 더 나은 대안이 있습니다 . 대신 읽기-쓰기- 업데이트 잠금을 살펴보십시오 .
ReentrantReadWrite_Update_Lock을 작성했으며 여기 에서 Apache 2.0 라이선스에 따라 오픈 소스로 릴리스했습니다 . 또한 JSR166 동시성 관심 메일 링리스트 에 대한 접근 방식에 대한 세부 정보를 게시 했으며 해당 접근 방식은 해당 목록에있는 구성원이 앞뒤로 조사한 후에도 살아 남았습니다.
접근 방식은 매우 간단하며 동시성 관심에 대해 언급했듯이 적어도 2000 년까지 Linux 커널 메일 링 목록에서 논의 되었기 때문에 아이디어가 완전히 새로운 것은 아닙니다. 또한 .Net 플랫폼의 ReaderWriterLockSlim 은 잠금 업그레이드를 지원합니다. 또한. 따라서이 개념은 지금까지 Java (AFAICT)에서 구현되지 않았습니다.
아이디어는 읽기 잠금 및 쓰기 잠금 외에도 업데이트 잠금 을 제공하는 것입니다 . 업데이트 잠금은 읽기 잠금과 쓰기 잠금 사이의 중간 유형의 잠금입니다. 쓰기 잠금과 마찬가지로 한 번에 하나의 스레드 만 업데이트 잠금을 획득 할 수 있습니다. 그러나 읽기 잠금과 마찬가지로이를 보유하고있는 스레드와 일반 읽기 잠금을 보유하는 다른 스레드에 대한 읽기 액세스를 동시에 허용합니다. 주요 기능은 업데이트 잠금을 읽기 전용 상태에서 쓰기 잠금으로 업그레이드 할 수 있으며, 한 스레드 만 업데이트 잠금을 보유하고 한 번에 업그레이드 할 수있는 위치에 있기 때문에 교착 상태에 취약하지 않습니다.
이는 잠금 업그레이드를 지원하며 더 짧은 시간 동안 읽기 스레드를 차단하므로 쓰기 전 읽기 액세스 패턴이 있는 애플리케이션에서 기존의 리더-라이터 잠금보다 효율적 입니다.
사이트 에서 사용 예가 제공됩니다 . 라이브러리는 100 % 테스트 범위를 가지며 Maven 중앙에 있습니다.
당신이하려는 것은 단순히 이런 식으로 가능하지 않습니다.
읽기에서 쓰기로 문제없이 업그레이드 할 수있는 읽기 / 쓰기 잠금을 가질 수 없습니다. 예:
void test() {
lock.readLock().lock();
...
if ( ... ) {
lock.writeLock.lock();
...
lock.writeLock.unlock();
}
lock.readLock().unlock();
}
이제 두 개의 스레드가 해당 함수에 들어간다고 가정합니다. (그리고 동시성을 가정하고 있지 않습니까? 그렇지 않으면 처음에 잠금에 대해 신경 쓰지 않을 것입니다 ....)
두 스레드가 동시에 시작 하고 똑같이 빠르게 실행 된다고 가정합니다 . 즉, 둘 다 완벽하게 합법적 인 읽기 잠금을 획득하게됩니다. 그러나 둘 다 결국 쓰기 잠금을 얻으려고 시도 할 것입니다.이 중 누구도 얻을 수 없습니다. 각각의 다른 스레드는 읽기 잠금을 유지합니다!
읽기 잠금을 쓰기 잠금으로 업그레이드 할 수있는 잠금은 정의에 따라 교착 상태가 발생하기 쉽습니다. 죄송합니다. 접근 방식을 수정해야합니다.
당신이 찾고있는 것은 잠금 업그레이드이며 표준 java.concurrent ReentrantReadWriteLock을 사용하여 (적어도 원자 적으로는) 불가능합니다. 가장 좋은 방법은 잠금 해제 / 잠금 후 그 사이에 아무도 수정하지 않았는지 확인하는 것입니다.
모든 읽기 잠금을 강제로 차단하는 것은별로 좋은 생각이 아닙니다. 읽기 잠금은 쓰면 안되는 이유가 있습니다. :)
편집 :
Ran Biron이 지적했듯이 문제가 기아라면 (읽기 잠금이 항상 설정되고 해제되며 0으로 떨어지지 않음) 공정한 대기열을 사용해 볼 수 있습니다. 그러나 귀하의 질문이 이것이 귀하의 문제인 것처럼 들리지 않았습니까?
편집 2 :
이제 문제를 확인하고 실제로 스택에서 여러 읽기 잠금을 획득했으며이를 쓰기 잠금 (업그레이드)으로 변환하고 싶습니다. 이것은 읽기 잠금의 소유자를 추적하지 않기 때문에 JDK 구현에서는 실제로 불가능합니다. 볼 수없는 읽기 잠금을 보유한 다른 사람이있을 수 있으며 현재 호출 스택은 말할 것도없고 읽기 잠금이 얼마나 많은 스레드에 속하는지 알 수 없습니다 (즉, 루프가 모든 읽기 잠금을 종료합니다. 당신의 쓰기 잠금은 동시 독자가 끝날 때까지 기다리지 않고 손에 엉망이 될 것입니다)
나는 실제로 비슷한 문제가 있었고, 누가 어떤 읽기 잠금을 가지고 있는지 추적하고이를 쓰기 잠금으로 업그레이드하는 내 자신의 잠금을 작성했습니다. 이것은 또한 Copy-on-Write 종류의 읽기 / 쓰기 잠금 (독자에 따라 한 명의 작성자를 허용) 이었지만 여전히 약간 달랐습니다.
자바 8은 이제이있다 java.util.concurrent.locks.StampedLock
로모그래퍼 tryConvertToWriteLock(long)
API
http://www.javaspecialists.eu/archive/Issue215.html 에서 자세한 정보
이건 어때요?
class CachedData
{
Object data;
volatile boolean cacheValid;
private class MyRWLock
{
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
public synchronized void getReadLock() { rwl.readLock().lock(); }
public synchronized void upgradeToWriteLock() { rwl.readLock().unlock(); rwl.writeLock().lock(); }
public synchronized void downgradeToReadLock() { rwl.writeLock().unlock(); rwl.readLock().lock(); }
public synchronized void dropReadLock() { rwl.readLock().unlock(); }
}
private MyRWLock myRWLock = new MyRWLock();
void processCachedData()
{
myRWLock.getReadLock();
try
{
if (!cacheValid)
{
myRWLock.upgradeToWriteLock();
try
{
// Recheck state because another thread might have acquired write lock and changed state before we did.
if (!cacheValid)
{
data = ...
cacheValid = true;
}
}
finally
{
myRWLock.downgradeToReadLock();
}
}
use(data);
}
finally
{
myRWLock.dropReadLock();
}
}
}
나는 ReentrantLock
트리의 재귀 적 순회에 의해 동기가 부여 되었다고 가정합니다 .
public void doSomething(Node node) {
// Acquire reentrant lock
... // Do something, possibly acquire write lock
for (Node child : node.childs) {
doSomething(child);
}
// Release reentrant lock
}
재귀 외부로 잠금 처리를 이동하도록 코드를 리팩터링 할 수 없습니까?
public void doSomething(Node node) {
// Acquire NON-reentrant read lock
recurseDoSomething(node);
// Release NON-reentrant read lock
}
private void recurseDoSomething(Node node) {
... // Do something, possibly acquire write lock
for (Node child : node.childs) {
recurseDoSomething(child);
}
}
So, Are we expecting java to increment read semaphore count only if this thread has not yet contributed to the readHoldCount? Which means unlike just maintaining a ThreadLocal readholdCount of type int, It should maintain ThreadLocal Set of type Integer (maintaining the hasCode of current thread). If this is fine, I would suggest (at-least for now) not to call multiple read calls within the same class, but instead use a flag to check, whether read lock is already obtained by current object or not.
private volatile boolean alreadyLockedForReading = false;
public void lockForReading(Lock readLock){
if(!alreadyLockedForReading){
lock.getReadLock().lock();
}
}
to OP: just unlock as many times as you have entered the lock, simple as that:
boolean needWrite = false;
readLock.lock()
try{
needWrite = checkState();
}finally{
readLock().unlock()
}
//the state is free to change right here, but not likely
//see who has handled it under the write lock, if need be
if (needWrite){
writeLock().lock();
try{
if (checkState()){//check again under the exclusive write lock
//modify state
}
}finally{
writeLock.unlock()
}
}
in the write lock as any self-respect concurrent program check the state needed.
HoldCount shouldn't be used beyond debug/monitor/fast-fail detect.
Found in the documentation for ReentrantReadWriteLock. It clearly says, that reader threads will never succeed when trying to acquire a write lock. What you try to achieve is simply not supported. You must release the read lock before acquisition of the write lock. A downgrade is still possible.
Reentrancy
This lock allows both readers and writers to reacquire read or write locks in the style of a {@link ReentrantLock}. Non-reentrant readers are not allowed until all write locks held by the writing thread have been released.
Additionally, a writer can acquire the read lock, but not vice-versa. Among other applications, reentrancy can be useful when write locks are held during calls or callbacks to methods that perform reads under read locks. If a reader tries to acquire the write lock it will never succeed.
Sample usage from the above source:
class CachedData {
Object data;
volatile boolean cacheValid;
ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) {
// Must release read lock before acquiring write lock
rwl.readLock().unlock();
rwl.writeLock().lock();
// Recheck state because another thread might have acquired
// write lock and changed state before we did.
if (!cacheValid) {
data = ...
cacheValid = true;
}
// Downgrade by acquiring read lock before releasing write lock
rwl.readLock().lock();
rwl.writeLock().unlock(); // Unlock write, still hold read
}
use(data);
rwl.readLock().unlock();
}
}
Use the "fair" flag on the ReentrantReadWriteLock. "fair" means that lock requests are served on first come, first served. You could experience performance depredation since when you'll issue a "write" request, all of the subsequent "read" requests will be locked, even if they could have been served while the pre-existing read locks are still locked.
'IT TIP' 카테고리의 다른 글
iOS 8-엔터프라이즈 앱을 설치할 수 없음 (0) | 2020.11.24 |
---|---|
npm을 사용하여 여러 버전의 패키지를 설치하는 방법 (0) | 2020.11.24 |
boxing / unboxing과 type casting의 차이점은 무엇입니까? (0) | 2020.11.24 |
CallTarget에 매개 변수를 전달하는 MSBuild (0) | 2020.11.24 |
현재 브라우저 창이 닫힐 때 Chrome 개발자 도구가 닫히지 않도록하려면 어떻게하나요? (0) | 2020.11.24 |