Java并发14:Lock&Condition-1


Java并发14:Lock&Condition-1

Java并发包中提供Lock和Condition实现管程,Lock作为锁,Condition作为同步。

为什么有了synchronized还要Lock和Condition?

举例:

死锁-破坏不可抢占条件:

```作为互斥锁是做不到的。
原因是``` synchronized``` 申请资源的时候,如果申请不到,线程直接进入阻塞状态,而线程进入阻塞状态,啥都干不了,也释放不了线程已经占有的资源。 >对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。 因此设计了```Lock```和```Condition```解决这种问题。 ### Lock的设计 解决```synchronized```的问题有3种方案,根据这3种方案设计了新的互斥锁```Lock```。 + 能够响应中断: ```synchronized```是一旦获取不到资源,直接阻塞,没法唤醒。 如果可以让获取互斥锁的线程响应中断,就可以从阻塞中通过响应中断唤醒。 + 支持超时: ```synchronized```是一旦获取不到资源,直接阻塞,并且没超时限制。 如果可以让获取互斥锁的线程支持超时,就可以从阻塞中超时设置唤醒。 + 非阻塞地获取锁: 如果可以让线程获取锁失败的时候,不进入阻塞状态,而是直接返回,这样有机会让线程释放自己的资源。 ##### 相关API ```java // 支持中断的API void lockInterruptibly() throws InterruptedException; // 支持超时的API boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 支持非阻塞获取锁的API boolean tryLock();

Lock如何保证可见性

/**
* The synchronization state.
*/
private volatile int state;
/**
* Performs lock.  Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}
/**
* Performs non-fair tryLock.  tryAcquire is implemented in
* subclasses, but both need nonfair try for trylock method.
*/
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();//获取当前线程实例
    int c = getState();//获取state变量的值,即当前锁被重入的次数
    if (c == 0) { //state为0,说明当前锁未被任何线程持有
        if (compareAndSetState(0, acquires)) { //以cas方式获取锁
            setExclusiveOwnerThread(current); //将当前线程标记为持有锁的线程
            return true;//获取锁成功,非重入
        }
    }
    else if (current == getExclusiveOwnerThread()) { //当前线程即持有锁线程,说明该锁被重入
        int nextc = c + acquires;//计算state变量要更新的值
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);//非同步方式更新state值
        return true; //获取锁成功,重入
    }
    return false; //走到这里说明尝试获取锁失败
}

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

```state```是```volatile```修饰的变量。

总体逻辑为:```lock()``` 会读写state中的值,```unlock()```会读写state中的值。

根据Happens-Before规则进行梳理:

+ **顺序性规则**:对于线程 T1,```value+=1```**Happens-Before**释放锁的操作```unlock()```;
+ **volatile 变量规则**:由于 ```state = 1``` 会先读取 ```state```,所以线程 ```T1``` 的 ```unlock()``` 操作 ```Happens-Before``` 线程``` T2``` 的 ```lock()``` 操作;
+ **传递性规则**:线程 ```T1``` 的 ```value+=1```**Happens-Before**线程 T2 的 ```lock()``` 操作。

### 可重入锁

**可重入锁即线程可以重复获取同一把锁。**

**可重入函数即多个线程可以同时调用该函数**。

>```synchronized```拥有强制原子性的内部锁机制,是一个可重入锁。
>
>因此,在一个线程使用```synchronized```方法时调用该对象另一个```synchronized```方法,即一个线程得到一个对象锁后再次请求该对象锁,是**永远可以拿到锁的**。
>
>在Java内部,同一个线程调用自己类中其他```synchronized```方法/块时不会阻碍该线程的执行,同一个线程对同一个对象锁是可重入的,同一个线程可以获取同一把锁多次,也就是可以多次重入。
>
>**原因是Java中线程获得对象锁的操作是以线程为单位的,而不是以调用为单位的。**
>
>可重入函数的条件
>
>- 不在函数内使用静态或全局数据。
>- 不返回静态或全局数据,所有数据都由函数的调用者提供。
>- 使用本地数据(工作内存),或者通过制作全局数据的本地拷贝来保护全局数据。
>- 不调用不可重入函数。
>
>可重入与线程安全
>
>一般而言,可重入的函数一定是线程安全的,反之则不一定成立。
>
>在不加锁的前提下,如果一个函数用到了全局或静态变量,那么它不是线程安全的,也不是可重入的。
>
>如果加以改进,对全局变量的访问加锁,此时它是线程安全的但不是可重入的,因为通常的加锁方式是针对不同线程的访问(如Java的```synchronized```),当同一个线程多次访问就会出现问题。
>
>只有当函数满足可重入的四条条件时,才是可重入的。
>
>——**CieloSun**

```java
class X {
  private final Lock rtl = new ReentrantLock();
  int value;
  public int get() {
    // 获取锁
    rtl.lock();         ②
    try {
      return value;
    } finally {
      // 保证锁能释放
      rtl.unlock();
    }
  }
  public void addOne() {
    // 获取锁
    rtl.lock();  
    try {
      value = 1 + get(); ①
    } finally {
      // 保证锁能释放
      rtl.unlock();
    }
  }
}

当线程 T1 执行到 ① 处时,已经获取到了锁 rtl ,当在 ① 处调用 get() 方法时,会在 ② 再次对锁 rtl 执行加锁操作。此时,如果锁 rtl 是可重入的,那么线程 T1 可以再次加锁成功;如果锁 rtl 是不可重入的,那么线程 T1 此时会被阻塞。

公平&非公平锁

构造时默认非公平锁,传一boolean值,true为公平锁,false为非公平锁。

公平:有先来后到,先来的线程,抢锁的时候能先抢。

非公平:没有先来后到,所有队列中的线程大家一起抢锁。

//无参构造函数:默认非公平锁
public ReentrantLock() {
    sync = new NonfairSync();
}
//根据公平策略参数创建锁
public ReentrantLock(boolean fair){
    sync = fair ? new FairSync() : new NonfairSync();
}

用锁的最佳实践

  • 永远只在更新对象的成员变量时加锁
  • 永远只在访问可变的成员变量时加锁
  • 永远不在调用其他对象的方法时加锁

可能其他方法里面有线程sleep()的调用,也可能会有奇慢无比的 I/O 操作,这些都会严重影响性能。更可怕的是,其他类的方法可能也会加锁,然后双重加锁就可能导致死锁。

性能上:ReentrantLock和Synchronized基本持平,场景上能用Synchronized,就尽量用。

思考

是否存在死锁问题?

class Account {
  private int balance;
  private final Lock lock= new ReentrantLock();
  // 转账
  void transfer(Account tar, int amt){
    while (true) {
      if(this.lock.tryLock()) {
        try {
          // myway:lock.tryLock((int)Math.random()*1000, TimeUnit.NANOSECONDS);
          if (tar.lock.tryLock()) {
            try {
              this.balance -= amt;
              tar.balance += amt;
              //break;
            } finally {
              tar.lock.unlock();
            }
          }//if
        } finally {
          this.lock.unlock();
        }
      }//if
     //bestway:sleep一个随机时间避免活锁 
     //Thread.sleep(随机时间);
    }//while
  }//transfer
}

有可能活锁:

若A和B同时向对方转账,同时执行到第7行,双方都持有了自己的锁,接着同时执行到第9行,都获取不到对方的锁,接着又到第18行同时释放自己的锁,再次尝试重新获取。会导致双方一直取不到锁。

增加随机等待时间,转账成功过后应跳出循环。


文章作者: Wendell
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Wendell !
评论
  目录