synchronized常见锁策略

这里提到的锁策略仅仅是一种“策略模式”,并不是真正的锁,同理这里的锁策略不仅仅局限于java,所有编程语言都适用于这里的锁策略。

锁策略

1.乐观锁 ,悲观锁

乐观锁,悲观锁并不是一把真正的锁,他描述的是一个抽象的概念,是一类锁的集合

  • 锁冲突: 几个线程同时竞争一把锁,一个线程拿到锁,其余线程处于阻塞等待的状态.

乐观锁: 预测该场景中不太会出现锁冲突的情况.(后续做的工作会更少)
悲观锁: 预测该场景中会频繁出现锁冲突的情况.(后续会做更多的工作)

例如:当我们去追我们学校的校花的时候,这就是一种高并发问题了,当你尝试给校花加锁的时候,大概率会有其他很多“线程”来和你抢这一把锁.所以,我们要想成功加锁. 就需要多做很多很多的工作。比如好好学习,以后开着豪车到校花的面前来。
但是当我们去追一个长相平平的女生时,可能并发量相对来说会减少很多,我们去尝试对其加锁的时候,也是很大概率能加上锁的,所以我们后续做的工作就会少很多.就比如看看电影,喝喝奶茶可能就成了.

2.轻量级锁,重量级锁

和这里的名字相同
轻量级锁:  加锁开销比较小的锁(花的时间少,占用系统资源少)
重量级锁:  加锁开销很大的锁,(花的时间多,占用系统资源多)
所以上述的乐观锁也通常为轻量级锁,悲观锁也通常被叫为重量级锁.  (不绝对大部分情况下可能)

3.自旋锁,挂起等待锁

自旋锁是一种轻量级锁的典型实现. 

自旋锁表示在用户态下,通过自旋(while循环)的方式来实现的类似于加锁的效果.

这种锁,会以最快时间拿到锁,但同时会占用和消耗部分cpu资源.

例如: 当我们去追我们女神的时候,在一次偶然中得知我们女神竟然有对象了. (女神已经被加锁了)但是我们每天仍然像女神发消息,每天都发早安,午安,晚安等,这样做的好处就是,在得知后面女神分手之后(锁释放),我们可以马上发现女神分手了,并最快的抓住机会。

挂起等待锁是一种重量级锁的典型实现

挂起等待锁表示通过内核态,借助系统提供的锁机制,来解决锁冲突问题。(涉及到内核对线程的调度,我也不太明白)

这种锁,无法保证第一时间拿到锁,但只需占用更少的CPU资源

例如:还是我们去追女神啊,同样也是在一次偶然中我们知道女神竟然有对象了。但这时候,我们采取了和上述方法完全不同的策略,知道女神有对象之后,我们就不去关心女神那边的事情了,自己做自己喜欢的事情去了。但是在一次偶然中,我们得知了女神竟然分手了,我们才继续去尝试追求女神,这样做,可以减少很多内耗,但是不能第一时间发现女神分手了(当你得知消息的时候,女神对象不知道都换多少个了).

4.读写锁

读写锁:就是将读操作和写操作分开了

  • 读加锁:读的时候不能写
  • 写加锁:写的时候不能读

多个线程操作对同一个变量进行写操作的时候是不安全的 详情参考一下文章
http://t.csdnimg.cn/M95oF

读加锁,写加锁的一些应用与解决,参靠数据库中的事务操作http://t.csdnimg.cn/QPMHA

  • 一个线程读加锁,另一个线程读加锁 不会产生锁竞争。
  • 一个线程写加锁,另一个线程写加锁 会产生锁竞争。
  • 一个线程写加锁,另一个线程读加锁 会产生锁竞争。

在java的代码中synchronized只是提供了一个加锁的操作,并没有对读写操作进行一个细化。但是在java的标准库中提供了相应的读写锁的操作。ReentrantReadWriteLock中就封装了相应的读写锁。

5. 公平锁,非公平锁

公平锁:当锁释放之后,遵循锁的先来后到原则。

例如:我们平时在追我们女神的时候,既然都是女神了,追求的人肯定不会少。当我们一群人得知女神已经有对象了,我们还是坚持追求,当女神分手的时候,A追了一年,B追了一个月,追求时间最长的人就直接上位。这就是公平锁。

非公平锁:当锁释放之后,其余等待的线程拿到该锁的概率均等。

例如:   还是在刚刚追求女生的过程中,我们得知女神分手的消息之后,此时A追了一年,B追了一个月,但此时A和B加锁女神的概率是相等的,这就是非公平锁。

在java中synchronized就是一把非公平锁,想让synchronized实现公平锁的特性,就需要额外实现一个数据结构来记录线程的先后顺序

6. 可重入锁,不可重入锁

不可重入锁:如果一个线程针对同一把锁连续加锁2次就会出现死锁的情况就是不可重入锁。
可重入锁:一个线程针对同一把锁加锁2次不会出现死锁。

既然这里谈到了死锁,我们接下来针对死锁问题,重点讲一讲。

在加锁过程中如果我们用 synchronized对一个非静态方法进行加锁,默认表示的是对方法中的this进行加锁.

public class demo1 {
    private int num  = 0;
    private synchronized void f(){
            num++;
    }
}

这2段代码表示了同一个含义,只不过加锁的位置不同,但本质都是对this对象进行了加锁

public class demo1 {
    private int num  = 0;
    private void f(){
        synchronized (this) {
            num++;
        }
    }
}


那我们来瞅瞅以下这段代码

public class demo1 {
    private int num  = 0;
    private synchronized void f(){
        synchronized (this){
            num++;
        }
    }
}

可以发现上述代码同时对方法和this进行了一个加锁。接下来我们详细解析一下不可重入锁的加锁原理

 我们假设执行上述f方法的过程中,一开始加锁成功了,然后当前线程拿到了锁this,此时该执行f方法中的内容了,但是此时方法中又遇到了一个锁,锁的对象也是this,且我此时发现this线程已经被取走了,一般遇到锁被取走的时候,线程会进行阻塞等待,等待有锁的方法执行完毕释放锁之后,我在进行一个锁的拿取,但是此时阻塞等待的方法也是拿取锁的方法。所以就造成了一个死循环,也就是上述的不可重入锁对一个线程进行2次加锁会出现死锁的原因。

那不可重入锁是否合理呢?这个一直是一个蛮有争议的话题,有些程序员可能会说,我怎么会写出如此“si山”的代码一个方法里加上了2个琐。但其实不然,在平时写代码的过程中,会经常出现写多个锁的情况。

public class demo1 {
    private int num  = 0;
    private void f(){
        synchronized (this) {
            f1();
        }
    }
    private void f1() {
        f2();
    }
    private synchronized void f2() {
        num++;
    }
}

像上述代码结构中,就出现了一个方法进行了多次加锁,但是在实际更复杂的业务逻辑中,可能丝毫不会察觉出来。所以站在我的角度来将其实不可重入锁还是蛮不合理的。

例如在我们平时的生活中,我去像我的女神说我喜欢你,试图对我的女神进行一个加锁,女神也答应了我的请求,表明我加锁成功了,在热恋期中,我又向我的女神说一句我爱你之类的话,难道就得失败了嘛。

所以设计JAVA的JVM虚拟机的程序员也考虑到了这一点,JAVA中的synchronized就是一把可重入锁,相比于隔壁的C++的同学std::mutex就是一把不可重入的锁.

所以可重入锁,会标记当前持有锁的线程,要是后续是同一个线程进行的加锁,也就不会出现死锁问题了。同理
不可重入锁就不会记录当前那个线程持有的锁了,他处于了一个加锁的状态,后面只要收到加锁的请求,就会拒绝掉该请求。

就像我们平时生活中,虽然女神接受了我们的表白,但如果我们此时开了一个小号去试探我们女神,在计算机中,女神是会拒绝掉小号的,也就是他不清楚这个小号是我的,所以选择了拒绝。这也就是一把典型的不可重入锁了。但是在实际生活中,上述只是一个理想情况,大概率是.....(不要去考验人性!!!!)

后续感觉写在一篇中会造成篇幅长太冗余的情况,后面博客打算分开写了下一篇博客专门谈死锁问题

你可能感兴趣的:(java,开发语言)