1.引子
昨天是1024程序员节日,看到很多朋友发了朋友圈,在这里我想到了问一个问题:1024是2的多少此方?关于这个问题,如果你还需要默算而不是靠潜意识就能回答上来,那说明你不是一个合格的程序员,开个玩笑!
下面进入正题,今天开始在高级并发编程系列中,我们正式进入第二个小节系列:锁 。说起锁你一定不会陌生。日常生活中,我们经常谈及的很多都与锁有关,你比如说:出门的时候记得把门锁 好、锁 保险柜、装个防盗锁 等等。这些都是我们生活中的锁,锁好门、锁好保险柜都是为了安全 ;它其实与我们程序中锁目的是一致的,在程序中加锁的目的也是为了安全,这又是一个证明世间万物道理想通的铁证。
我们暂且记住,加锁的目的是为了:安全。这个系列中我们准备分享这么几个内容:
- 锁定义
- 常见锁分类
- Lock接口和它的常见实现类分析
- ReentrantLock使用案例
- ReentrantReadWriteLock使用案例
今天这一篇,让我们先从入门开始,那么来吧
            
            
              bash
              
              
            
          
          #考考你:
1.你能用自己的话,结合你的理解给锁下一个定义吗
2.你能说出常见的锁分类有哪些吗2.案例
2.1.第一个加锁案例
我们先看一个大家都熟悉,而可能又陌生的案例操作。在看之前,你需要先思考一下:在大多数编程语言中,都提供了:++自曾的操作,关于这个操作它是线程安全的吗?
2.1.1.案例版本一:不加锁
2.1.1.1.任务线程
            
            
              csharp
              
              
            
          
          /**
 * 任务线程
 */
class AddITask implements Runnable{
    @Override
    public void run() {
        // for循环,让add_i变量自增操作:10000次
        for (int i = 0; i < 10000; i++){
            NeedDoLockDemo.addI();
        }
    }
}2.1.1.2.主程序
            
            
              csharp
              
              
            
          
          /**
 * 演示需要加锁的基本操作
 *
 * @author ThinkPad
 * @version 1.0
 * @date 2020/10/24 21:35
 */
public class NeedDoLockDemo {
    /**
     * 自曾操作变量add_i
     */
    public static int add_i = 0;
    /**
     * addI方法,实现add_i变量自曾操作
     * 注意:这里没有加锁
     */
    public  static void addI(){
        add_i ++;
    }
    public static void main(String[] args) {
        // 创建任务线程对象
        Runnable r1 = new AddITask();
        // 用20个线程,并行执行自增操作
        for(int i = 0; i < 20; i++){
            new Thread(r1).start();
        }
        // 循环等待子线程执行结束
        while (Thread.activeCount() > 2){
            ;
        }
        // 打印add_i变量值,预期结果:200000
        System.out.println("20个线程并行自增后,结果:" + add_i);
    }
}2.1.1.3.执行结果
结果分析:
- 
add_i变量初始值是:0 arduinopublic static int add_i = 0
- 
通过20个线程,并行执行自增操作 scss// 用20个线程,并行执行自增操作 for(int i = 0; i < 20; i++){ new Thread(r1).start(); }
- 
每个线程,自增10000次 ini// for循环,让add_i变量自增操作:10000次 for (int i = 0; i < 10000; i++){ NeedDoLockDemo.addI(); }
- 
预期结果:20 * 10000 = 200000 
- 
实际结果:198359 
- 
执行结果与预期结果不符,发生了并发不安全问题 
2.1.2.案例版本二:加锁
在案例版本一中,由于没有加锁,发生了并发不安全的问题。这里有一个小知识点:对于jvm来说,++操作不是一个线程安全的操作,因为自增++操作,它不是一个原子性的操作。
那如何实现自增++的放心操作,人畜无害呢?答案是:加锁。我们再来看加锁的版本。
2.1.2.1.方法addI加锁
            
            
              arduino
              
              
            
          
          /**
* addI方法,实现add_i变量自曾操作
* 注意:这里的synchronized关键字
*/
public synchronized static void addI(){
      add_i ++;
}2.1.2.2.执行结果
结果分析:
- 加锁后,add_i变量的最终执行结果是:200000,符合预期
2.2.锁定义及常见锁分类
2.2.1.锁定义
我们知道了,在多线程并发操作下,需要考虑线程安全的问题。这是因为有共享资源的存在,所谓共享资源即是每个线程都会去操作的资源,你需要,我也需要,可以这么去理解 ,比如说上面案例中的add_i变量。
那么问题来了,在并发操作的情况下,是不管先来后到的,即便后来,但是可能却先把活干了。这样下去,世界就乱了,没有规则没有秩序,重新陷入混沌状态!
所以需要一种规则,建立一套秩序,让程序世界不能乱!锁就是编程世界中,解决混乱的规则和秩序,它的本质是在整体并行执行的应用程序中,实现局部串行有序执行。
到了这里,我们可以用一句简洁的话来描述锁:锁是一种工具,用于控制对共享资源访问的工具。
2.2.2.锁分类
关于锁的分类,如果要细分会非常多,非常难记,且没有必要。我们关注几类常见的吧:
- 
乐观锁与悲观锁 - 
乐观锁 bash#1.所谓乐观锁,它是典型的乐天派,认为在操作共享资源的时候,永远都只有自己一个人(一个线程)在操作,不会有其它线程与自己争 #2.因此乐观锁本质上不加锁,而是引入了冲突检测的机制。关于冲突检测机制,在后面分享CAS的时候,我们在详细讨论 #3.乐观锁适用于:读多写少,冲突小的业务场景
- 
悲观锁 
 bash#1.所谓悲观锁,它是典型的悲观派,时时刻刻都认为:在操作共享资源的时候,永远都有别人(别的线程),在跟自己抢! #2.因此不管三七二十一,先锁了再说,让你抢不着 #悲观锁适用于:写多读少,容易发生冲突的业务场景
- 
- 
可重入锁与非可重入锁 - 
可重入锁 bash#1.所谓可重入锁,一个线程拿到该锁以后,在释放以前,可以重复多次获取到该锁 #2.在juc中提供的ReentrantLock即是可重入锁
- 
非可重入锁 bash#1.所谓非可重入锁,一个线程拿到该锁以后,在释放以前,不能重复多次获取到该锁 #2.在jdk中提供的synchronized锁,即是非可重入锁
 
- 
- 
共享锁与排它锁 - 
共享锁 bash#1.所谓共享锁,表示该锁比较大方,独乐乐不如众乐乐,一个锁可以被多个线程同时获取拥有 #2.在juc中提供的ReentrantReadWriteLock锁中的读锁,即是共享锁
- 
排它锁 bash#1.所谓排它锁,表示有你没我,有我没你。即一个锁被一个线程获取以后,便不能再被其他线程获取 #2.我们平常见的较多的锁,都是排它锁。比如ReentrantLock、synchronized、ReentrantReadWriteLock中的写锁等
 
-