volatile

volatile是一种稍弱的同步机制,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型之后,编译器与运行时都会注意到这个变量是共享的。访问volatile变量时,不会执行加锁操作,因此不会使线程发送阻塞。

volatile变量只能保证内存中的可见性,而不能保证互斥性。根据这个特性,volatile变量常用来表示状态。

synchronized

  • 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;

  • 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;

  • 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;

  • 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象。

synchronized锁是可重入的。synchronized既能保证互斥性,也能保证内存可见性。

Lock

Lock是java中的显示锁。

ReentrantLock

ReentrantLock实现了Lock接口,并提供了与synchronized相同的互斥性和内存可见性。ReentrantLock锁也是可重入锁。

public interface Lock {

    void lock();
	
    void lockInterruptibly() throws InterruptedException;
    
    boolean tryLock();
    
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
	
    void unlock();
	
    Condition newCondition();
}

公平锁与非公平锁

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
  • 公平锁:线程按照他们发出请求的顺序获取锁

  • 非公平锁: 当一个线程请求非公平锁时,如果在发出请求的同时该锁变成可用状态,那么这个线程会跳过队列中所有的等待线程而获得锁。

在公平的锁中,如果有另一个线程持有锁或者有其他线程在等待队列中等待这个所,那么新发出的请求的线程将被放入到队列中。而非公平锁上,只有当锁被某个线程持有时,新发出请求的线程才会被放入队列中。 非公平锁的性能要优于公平锁。

读写锁

一个资源可以被多个读操作访问,或者被一个写操作访问,但不能两者同时进行。

public interface ReadWriteLock {

    Lock readLock();
	
    Lock writeLock();
}

对于在多处理器系统上被频繁读取饿数据结构,读写锁能提高性能;而在其他场景下,读写锁的性能要差于独占锁,因为读写锁的复杂性更高。

synchronized与Lock的比较

类别

synchronized

Lock

实现机制

悲观锁

乐观锁

操作层次

Java的关键字,JVM层面

Java代码层面

锁的释放

1、以获取锁的线程执行完同步代码,释放锁;2、线程执行发生异常,jvm会让线程释放锁

在finally中必须释放锁,不然容易造成线程死锁

锁的状态

无法判断

可以判断

锁的类型

互斥性 内存可见性 可重入 不可中断 非公平

互斥性 内存可见性 可重入 可判断 可公平 可非公平

分布式锁

redis实现分布式锁

redis通常可以使用setnx来实现分布式锁。setnx来创建一个key,并设置过期时间。如果key不存在则创建成功返回1,如果key已经存在则返回0。依照上述来判定是否获取到了锁获取到锁的执行业务逻辑,完毕后删除lock_key,来实现释放锁其他未获取到锁的则进行不断重试,直到自己获取到了锁

public void lock(){
    while(true){
        ret = set lock_key identify_value nx ex lock_timeout
        if(ret){
            //获取到了锁
            return;
        }
        sleep(100);
    }
}

public void release(){
    value = get lock_key
    if(identify_value == value){
        del lock_key
    }
}

问题

1、 lock timeout的存在也使得失去了锁的意义,即存在并发的现象。一旦出现锁的租约时间,就意味着获取到锁的客户端必须在租约之内执行完毕业务逻辑,一旦业务逻辑执行时间过长,租约到期,就会引发并发问题。所以有lock timeout的可靠性并不是那么的高。

2、 redis单机情况下,还存在redis单点故障的问题。如果为了解决单点故障而使用redis的sentinel或者cluster方案,则更加复杂,引入的问题更多。

ZooKeeper实现分布式锁

获取锁
public void lock(){
    path = 在父节点下创建临时顺序节点
    while(true){
        children = 获取父节点的所有节点
        if(path是children中的最小的){
            代表获取了节点
            return;
        }else{
            添加监控前一个节点是否存在的watcher
            wait();
        }
    }
}

watcher中的内容{
    notifyAll();
}
释放锁
public void release(){
    删除上述创建的节点
}

锁的占用时间限制:redis就有占用时间限制,而ZooKeeper则没有,最主要的原因是redis目前没有办法知道已经获取锁的客户端的状态,是已经挂了呢还是正在执行耗时较长的业务逻辑。而ZooKeeper通过临时节点就能清晰知道,如果临时节点存在说明还在执行业务逻辑,如果临时节点不存在说明已经执行完毕释放锁或者是挂了。

是否单点故障:redis本身有很多中玩法,如客户端一致性hash,服务器端sentinel方案或者cluster方案,很难做到一种分布式锁方式能应对所有这些方案。而ZooKeeper只有一种玩法,多台机器的节点数据是一致的,没有redis的那么多的麻烦因素要考虑。

总体上来说ZooKeeper实现分布式锁更加的简单,可靠性更高。

Last updated