并发编程多线程基础(草稿6)--锁机制

第一节
  • 基本的锁的分类:

(1)重入锁(也叫作递归锁),为了避免我们递归时候发生的异常,我们常用的synchronized和lock都是递归锁。
(2)乐观锁:总是认为不会发生并发问题,每一次去取数据的时候不认为其他线程对数据进行修改,因此不会上锁,但是在更新的时候会判断其他线程在这之前是否对其进行修改,一般会用版本号机制或CAS操作实现。
– 2.1 version方式:一般是在数据表中加上一个数据版本号version子弹,表示数据被修改的次数,当数据被修改的时候,version值加1。当线程A要更新数据的时候,在读取数据的时候也会读取version的值,在提交更新的时候,若刚才读取到的version值为当前数据库中的version值相等的时候,才会更新,否则就一直进行重试,直到更新成功。

update table set x=x+1, version=version+1 where id= #{id}  and version=#{version}

– 2.2 CAS操作方式:即compare and swap 或者 compare and set,涉及到三个操作数,数据所在的内存值,预期值,新值。当需要更新的时候,判断当前内存中的内存值与之前取到的值是否相等,若相等,则更新新值,若失败则重试,一般情况下是一个自旋操作,即不断的重试。
内存值:当前内存中读到的值,不管是否修改,就是当前变量的值(修改了则为修改后的)
预期值:我们预期的值,即预期是没有被任何其他线程修改,就是我们要更新前的值,理想下线程安全的值
新值:最新的数据,将要被更新的数据
(3)悲观锁:总是假设最坏的情况,每次取数据都认为其他线程会被修改,所以都会加锁,当其他线程想要访问数据的时候,都需要阻塞挂起。
(4)读写锁:如果两个线程同时读一个资源,没有任何写操作的时候,两个线程可以共同读取。但是只要有一个线程执行了写的操作的时候,就不应该有其他线程对其进行读或者写的操作。
(5)AQS锁
(6)自旋锁
(7)公平锁
(8)非公平锁
(9)排它锁和互斥锁:同一时刻只能允许一个线程进入
(10)CAS无锁
(11)布式锁

读锁:
ReentranReadWriterLock cwl = new ReentranReadWriterLock();
Lock r = cwl.readLock();
Lock w = cwl.writeLock();

  • CAS无锁机制效率高于synchronized,且永远不可能发生死锁。
  • 常用的原子类
    (1)AtomicBoolean
    (2)AtomicInteger
    (3)AtomicLong
    (4)AtomicReference
    上面的常用的原子类都是用了无锁的概念,有的地方直接使用了CAS操作的线程安全。
第二节 Synchronized的重入锁
  • 轻量级锁:lock锁
  • 重量级锁:synchronzied
  • 轻量级锁和重量级锁的区别:轻量级锁灵活性高,需要自己手动加锁和释放锁。
  • 重入锁:又叫递归锁,有可重入性,目的就是为了避免死锁,A方法请求B方法,A,B方法都加同一把锁,如果一个线程进入了A方法,那么也可以调用A方法中B方法。具有锁的传递性,指的是同一个线程外层函数获得锁之后,内层递归函数仍然有获取该锁的代码,但不受影响,内层函数仍然可以获得该锁。
  • Synchronized重入锁:
    代码测试如下:
public class Test implements Runnable{
    public synchronized void get(){
        System.out.println("name: " + Thread.currentThread().getName() + " get()");
        set();
    }

    public synchronized void set() {
        System.out.println("name: " + Thread.currentThread().getName() + " set()");
    }

    public void run() {
        get();
    }

    public static void main(String[] args) {
        Test t = new Test();
        new Thread(t).start();
        new Thread(t).start();
        new Thread(t).start();
        new Thread(t).start();
    }
}

结果是:

name: Thread-0 get()
name: Thread-0 set()
name: Thread-3 get()
name: Thread-3 set()
name: Thread-2 get()
name: Thread-2 set()
name: Thread-1 get()
name: Thread-1 set()
  • 重入锁什么时候释放?

外层函数执行完之后,方法执行完之后,锁释放。

第三节 ReentrantLock锁
  • lock锁必须在try-catch-finally中,因为需要在finally中释放锁
    锁是可以传递的,递归传递。外层函数可以将锁传递给内层函数。
    可重入锁的目的就是为了让我们避免死锁现象
第四节 ReentrantReadWriteLock读写锁
  • 如果读方法和写方法不加锁,启动两个线程分别对其进行读操作和写操作,此时,可能没有写完的时候,就开始读数据,造成脏读的现象。
  • 读写锁,在写的时候,读的方法也不能进入,就是写的时候不能读,读的时候不能写。
  • 读写锁的目的

都是读的操作,不会加锁,但凡有写的操作,就会加锁,只允许有一个线程进入。

代码如下:


package com.xiyou.mayi.thread5.duxiesuo;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 读写锁
 */
public class Cache {
    private static Map<String, Object> map = new HashMap<>();
    // 定义读写锁
    private static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    // 定义读锁
    private static Lock r = rwl.readLock();
    // 定义写锁
    private static Lock w = rwl.writeLock();

    /**
     * 获取一个key对应的value
     * @param key
     * @return
     */
    public static final Object get(String key){
        // 读加锁
        r.lock();
        try {
            System.out.println("正在做读的操作, key: " + key + " 开始");
            Thread.sleep(100);
            Object object = map.get(key);
            System.out.println("正在做读的操作,key:" + key + " 结束");
            System.out.println();
            return object;
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            // 读锁的释放
            r.unlock();
        }
        return key;
    }

    /**
     * 赋值操作,写操作
     * @param key
     * @param value
     * @return
     */
    public static final Object put(String key, Object value) {
        // 写加锁
        w.lock();
        try {

            System.out.println("正在做写的操作,key:" + key + ",value:" + value + "开始.");
            Thread.sleep(100);
            Object object = map.put(key, value);
            System.out.println("正在做写的操作,key:" + key + ",value:" + value + "结束.");
            System.out.println();
            return object;
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 释放写锁
            w.unlock();
        }
        return value;
    }

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    Cache.put(i + "", i + "");
                }
            }
        }).start();

        new Thread(new Runnable() {

            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    Cache.get(i + "");
                }

            }
        }).start();

    }
}


第五节 乐观锁
  • 乐观锁的本质上其实是没有锁的,效率比较高,无阻塞,无等待,若乐观锁失败可以重试,直至成功获取锁。
  • 在设计表的时候有一个version字段,该字段就是用来实现乐观锁的,判断version是否满足要求,因为每一次修改操作都会更新version字段的值。
 update table set x=x+1, version=version+1 where id= #{id}  and version=#{version}
  • version字段若没有发生改变就可以更新,version字段发生了改变就不能更新,版本发生了改变,值已经被别人改了,此时应该重试进行更新。
第六节 悲观锁
  • 悲观锁是什么?

总是假设最坏的情况,每次取数据时都认为其他线程会修改,所以都会加锁(读锁,写锁,行锁等),当其他线程想要访问数据的时候,都需要阻塞挂起。可以依靠数据库实现,如行锁,读锁和写锁,都是在操作之前加锁,在java中,synchronized的思想也是悲观锁。

  • 悲观锁是一个重量级锁,会发生阻塞。
  • 乐观锁执行失败会进行重试,不会阻塞,不会等待。
  • 乐观锁的效率会很高。
第七节 AtomicInteger的原子类
  • 预期值?

指的是本地内存中的值。

举例说明:
两个线程同时操作一个全局变量,演示线程安全问题:


private static AtomicInteger atomic = new AtomicInteger();
// 加1 做自增操作
atomic.incrementAndGet();

  • AtomicInteger天然的线程安全,用了无锁的概念,底层用了CAS无锁。完全不会阻塞,没有加锁
第八节和第九节 CAS无锁机制
  • 原子类的底层实现原理是CAS无锁技术。就是做比较。
  • 什么是CAS的无锁技术?

CAS: Compare And Swap,即比较之后再进行交换
jdk5增加了并发包java,util,concurrent.*,其下面的类使用了CAS算法实现了区别于synchronized同步锁的一种乐观锁,jdk5之前用的是synchronized关键字来保证同步的,这是一种独占锁,也是悲观锁的一种。

  • 原子性指的是线程安全,可见性指的是本地内存修改之后,立刻刷新到工作内存中,本地内存的值永远和工作内存一致。
  • JAVA内存模型(JMM,定义了一个线程对另一个线程的可见性)

JMM将内存分为主内存(共享内存)和本地内存(各自线程中的内存),本地内存也叫作工作内存。
共享内存存放了一个我们的全局变量i,本地内存主要存放共享内存的副本,也就是i全局变量的副本,也就是每个本地内存都存了一个i = 0,假设有两个本地内存t1线程的和t2线程的。假设两个线程都做了i++操作,两个线程都执行了,此时各自本地内存中的值都是1,因为各自本地内存中的值i初始是0,这明显不对,做了两次对全局变量i的++操作,此时我们将本地内存都刷新到主内存后,主内存的值是1,那么这种情况怎么解决?此时我们可以使用volatile保证可见性(就是本地内存中的值一修改就进行刷新),保证了工作内存修改后的值,永远和主内存一致。或用synchronized关键字,保证了原子性和可见性,同一时刻只能一个线程操作,将本地内存刷新到主内存之后,才可以让另一个线程执行,那么有没有别的方法呢?有的,就是 使用CAS的无锁机制。
在这里插入图片描述

  • CAS无锁机制

CAS的无锁机制,包含了三个参数,V, E, N
V:表示的是要去更新的变量。表示需要去更新的变量。表示主内存的值(对照着上图)
E:表示的是预期值。表示本地内存的值(对照着上图)
N:表示的是新值。表示变量的最新值。需要去修改成的值。
我们仅仅当V值等于E值的时候,才会将V值的值设置为N,如果V值和E值不同,则说明已经有线程对其作出了更新,则当前线程什么都不做,不停的重试,直至可以进行更新。
简单理解,就是主内存的值和本地内存的值一致的情况下,说明了没有人对变量做操作,这样就可以将主内存的值修改为最新的值,如果不一致,表示已经修改了,主内存的值应该重新备份到本地内存中,重新操作。

  • CAS缺点:

ABA问题无法确定,即最先开始主内存的值为A,备份到工作内存中为A,将工作内存又修改成了B,最后又改成A,这时候判断的时候,主内存最先开始是A,此时还是A,可以修改,但是实际上我们已经将A修改过了一次

  • 如何解决CAS的ABA问题?

JAVA并发包中提供了一个带有标记的原子引用类AtomicStampedReference,他可以通过控制变量值的版本来保证CAS的正确性。

自旋锁
  • CAS已经用到了自旋锁,如果自旋锁失败了(即本地内存的值和主内存的值不一致,无法修改新值),则其会一直循环重试,直至成功。这里的不停的循环重试实际上就是已经使用了自旋锁的机制。
  • 自旋锁和互斥锁的区别?

synchronized就是一个互斥锁,同一个时刻只能有一个线程获取锁,没有获取到锁的线程会进行等待
(1)互斥锁与自旋锁的区别就是,互斥锁中未获得锁的线程会进行等待,自旋锁中未获得锁的线程则不停的尝试获取,不会等待和阻塞
(2)互斥锁是属于悲观锁的一种,自旋锁属于乐观锁的一种。
(3)互斥锁的效率很低,因为有上下为的切换,CPU的抢占。

排它锁与共享锁
  • 共享锁就相当于是读锁
  • 排它锁相当于是写锁
公平与非公平锁
  • 什么是公平锁和非公平锁?

简单的理解就是公平锁与请求顺序有关,先请求锁的先得到,而非公平锁,就是直接去争,不按照顺序进行。

补充知识点:
  • lock的原理使用的是AQS。AQS使用的是一个双向链表。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值