程序猿必须要掌握的多线程安全问题之锁策略详解

一、常见的锁策略

1.1 乐观锁

乐观锁:乐观锁假设认为数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正 式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如 何去做。乐观锁的性能比较高。
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会 上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
悲观锁的问题:总是需要竞争锁,进而导致发生线程切换,挂起其他线程;所以性能不高。 乐观锁的问题:并不总是能处理所有问题,所以会引入一定的系统复杂度。

乐观锁的使用场景:

import java.util.concurrent.atomic.AtomicInteger;

public class happylock {
    private  static  AtomicInteger count = new AtomicInteger(0);
    private  static  final  int MAXSIZE = 100000;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0;i<MAXSIZE;i++){
                    count.getAndIncrement();
                }
            }
        });
        t1.start();
        t1.join();
        Thread t2= new Thread(new Runnable() {
            @Override
            public void run() {
                for(int j = 0;j<MAXSIZE;j++){
                    count.getAndDecrement();
                }
            }
        });
        t2.start();
        t2.join();
        System.out.println("结果"+count);
    }
//结果是0,如果不加AtomicInteger,那么线程执行完以后不会是0,存在线程不安全!

}

1.2 悲观锁

悲观锁:他认为通常情况下会出现并发冲突,所以它在一开始就加锁;
synchronized 就是悲观锁

1.3 读写锁

多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需  要进行互斥。如果两种场景下都用同一个锁,
就会产生极大的性能损耗。所以读写锁因此而产生。

读写锁(readers-writer lock),看英文可以顾名思义,在执行加锁操作时需要额外表明读写意图,复数读者之间并不互斥,而写者则要求与任何人互斥。
把锁分成两个锁,一个是读锁,一个是写锁,其中读锁可以多个线程拥有,而写锁是一个线程拥有。读锁是共享锁,而写锁是非公享锁。
读写锁的应用方法:

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Readerlock {
    //读写锁的具体实现
    public static void main(String[] args) {
        //创建读写锁
        ReentrantReadWriteLock  reentrantReadWriteLock = new ReentrantReadWriteLock();
        //分离读锁
        ReentrantReadWriteLock.ReadLock readLock=  ReadWriteLock.ReadLock();
        //分离写锁
        ReentrantReadWriteLock.WriteLock readLock=  ReadWriteLock.WriteLock();
    }

}

1.4 公平锁与非公平锁

公平锁:锁的获取顺序必须合线程方法的先后顺序是保存一致的,就叫公平锁 优点:执行时顺序的,所以结果是可以预期的
非公平锁:锁的获取方式循序和线程获取锁的顺序无关。优点:性能比较高

1.5 自旋锁(Spin Lock)

按之间的方式处理下,线程在抢锁失败后进入阻塞状态,放弃 CPU,需要过很久才能再次被调度。但经过测算,实际的生活中,大部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。基于这个 事实,自旋锁诞生了。
你可以简单的认为自旋锁就是下面的代码

只要没抢到锁,就死等。

自旋锁的缺点:
缺点其实非常明显,就是如果之前的假设(锁很快会被释放)没有满足,则线程其实是光在消耗 CPU 资源,长期在做无用功的。

1.6 可重入锁

可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。比如一个递归函数 里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁(因为这个原因 可重入锁也叫做递归锁)。
Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括
synchronized关键字锁都是可重入的。

1.7 相关题目

面试题:

1.你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?

乐观锁——> CAS ——> Atomic.(CAS是由v(内存值) A(预期值)B(新值))组成,然后执行的时候是使用V=A对比,如果结果为true,这表明没有并发冲突,则可以直接进行修改,否则返回错误信息。*

2.有了解什么读写锁么?

多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需 要进行互斥。如果两种场景下都用同一个锁,就会产生极大的性能损耗。所以读写锁因此而产生。
读写锁(readers-writer lock),看英文可以顾名思义,在执行加锁操作时需要额外表明读写意图,复数读者之间并不互斥,而写者则要求与任何人互斥。
把锁分成两个锁,一个是读锁,一个是写锁,其中读锁可以多个线程拥有,而写锁是一个线程拥有

3.什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么?

按之间的方式处理下,线程在抢锁失败后进入阻塞状态,放弃 CPU,需要过很久才能再次被调度。但经过测算,实际的生活中,大部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。基于这个 事实,自旋锁诞生了。
你可以简单的认为自旋锁就是下面的代码
只要没抢到锁,就死等。
自旋锁的缺点:
缺点其实非常明显,就是如果之前的假设(锁很快会被释放)没有满足,则线程其实是光在消耗 CPU 资源,长期在做无用功的。

4.synchronized 是可重入锁么?

synchronized 是可重入锁,

代码如下:

public class Chonglock {
    private  static   Object lock = new Object();

    public static void main(String[] args) {
        //第一次进入锁
        synchronized (lock){
            System.out.println("第一次进入锁");
            synchronized (lock){
                System.out.println("第二次进入锁");
            }
        }
    }
}

二、CAS问题

2.1 什么是CAS问题

CAS: 全称Compare and swap,字面意思:”比较并交换“,一个 CAS 涉及到以下操作:
我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。 1. 比较 A 与 V 是否相等。(比较) 2. 如果比较相等,将 B 写入 V。(交换) 3. 返回操作是否成功。
当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。可见 CAS 其实是一个乐观锁。

2.2 CAS 是怎么实现的

针对不同的操作系统,JVM 用到了不同的 CAS 实现原理,简单来讲:
java 的 CAS 利用的的是 unsafe 这个类提供的 CAS 操作;
unsafe 的 CAS 依 赖 了 的 是 jvm 针 对 不 同 的 操 作 系 统 实 现 的 Atomic::cmpxchg(一个原子性的指令)

/Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 cpu 硬件提供的 lock 机制保证其原子性。
简而言之,是因为硬件予以了支持,软件层面才能做到。

2.3 CAS 有哪些应用

2.3.1 实现自旋锁

public class SpinLock {
private AtomicReference<Thread> sign =new AtomicReference<>();

public void lock(){
Thread current = Thread.currentThread();

// 不放弃 CPU,一直在这里旋转判断
while(!sign .compareAndSet(null, current)){
}
}

public void unlock (){
Thread current = Thread.currentThread(); sign.compareAndSet(current, null);
}
}

用于实现原子类

示例代码:

public class AtomicInteger {
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
}

public class Unsafe {
public final int getAndAddInt(Object var1, long var2, int var4) { int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

return var5;
}
}

三、ABA问题

3.1 什么是ABA问题

ABA 的问题,就是一个值从A变成了B又变成了A,而这个期间我们不清楚这个过程。

3.2 实现ABA问题场景

我来举一个例子,如果你向别人转钱,你需要转100元,但是你点击了两次转钱,第一次会成功,但是第二次肯定会失败,但是,在你点击第二次转钱的同一时刻,你的公司给你转了100元工资,那么你就会莫名其妙的把100又转了出去,你丢失了100,别人也没有获得100.
代码演示:

1.正常转钱流程

import java.util.concurrent.atomic.AtomicReference;

public class Aba {
    //ABA问题的演示

    private  static AtomicReference money = new AtomicReference(100);//转账

    public static void main(String[] args) {
        //转账线程1
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
               boolean result =  money.compareAndSet(100,0);
                System.out.println("点击第一次转出100"+result);
            }
        });
        t1.start();
        //转账线程2
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
               boolean result =  money.compareAndSet(100,0);
                System.out.println("点击第二次转出100"+result);
                if(!result){
                    System.out.println("余额不足,无法转账!");
                }
            }
        });
        t2.start();

    }
}

2.错误操作后:

import java.util.concurrent.atomic.AtomicReference;

public class ABas {

    private  static AtomicReference money = new AtomicReference(100);//转账

    public static void main(String[] args) throws InterruptedException {
        //转账出线程1
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                boolean result =  money.compareAndSet(100,0);
                System.out.println("第一次"+result);
            }
        });
        t1.start();
        t1.join();
        //转入100
        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                boolean result =  money.compareAndSet(0,100);
                System.out.println("转账"+result);
            }
        });
        t3.start();
        //转账线程2
        t3.join();
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                boolean result =  money.compareAndSet(100,0);
                System.out.println("第二次"+result);
            }
        });
        t2.start();

    }
}

解决ABA方法

解决方法:加入版本信息,例如携带 AtomicStampedReference 之类的时间戳作为版本信息,保证不会
出现老的值。

代码实现:

import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;

public class Abaack {

    //private  static AtomicReference money = new AtomicReference(100);//转账
private  static AtomicStampedReference money = new AtomicStampedReference(100,1);

//
    public static void main(String[] args) throws InterruptedException {
        //转账出线程1
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                boolean result =  money.compareAndSet(100,0,1,2);
                System.out.println("第一次转账100:"+result);
            }
        });
        t1.start();
        t1.join();
        //转入100
        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                boolean result =  money.compareAndSet(0,100,2,3);
                System.out.println("其他人给你转账了100:"+result);
            }
        });
        t3.start();
        //转账线程2
        t3.join();
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                boolean result =  money.compareAndSet(100,0,1,2);
                System.out.println("第二次点击转账100:"+result);
            }
        });
        t2.start();

//Integer的高速缓存是-128--127(AtomicStampedReference)
        //如果大于127,那么就开始new对象了
        /*
        * 解决方法,调整边界值*/
    }

}

四、总结

以上就是今天要讲的内容,本文仅仅简单介绍了锁策略,解决线程安全。

到此这篇关于程序猿必须要掌握的多线程安全问题之锁策略详解的文章就介绍到这了,更多相关java锁内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Java线程安全和锁Synchronized知识点详解

    一.进程与线程的概念 (1)在传统的操作系统中,程序并不能独立运行,作为资源分配和独立运行的基本单位都是进程. 在未配置 OS 的系统中,程序的执行方式是顺序执行,即必须在一个程序执行完后,才允许另一个程序执行:在多道程序环境下,则允许多个程序并发执行.程序的这两种执行方式间有着显著的不同.也正是程序并发执行时的这种特征,才导致了在操作系统中引入进程的概念. 自从在 20 世纪 60 年代人们提出了进程的概念后,在 OS 中一直都是以进程作为能拥有资源和独立运行的基本单位的.直到 20 世纪 8

  • java多线程创建及线程安全详解

    什么是线程 线程被称为轻量级进程,是程序执行的最小单位,它是指在程序执行过程中,能够执行代码的一个执行单位.每个程序程序都至少有一个线程,也即是程序本身. 线程的状态 新建(New):创建后尚未启动的线程处于这种状态 运行(Runable):Runable包括了操作系统线程状态的Running和Ready,也就是处于此状态的线程有可能正在执行,也有可能正在等待着CPU为它分配执行时间. 等待(Wating):处于这种状态的线程不会被分配CPU执行时间.等待状态又分为无限期等待和有限期等待,处于无

  • Java基于redis实现分布式锁

    为了保证一个在高并发存场景下只能被同一个线程操作,java并发处理提供ReentrantLock或Synchronized进行互斥控制.但是这仅仅对单机环境有效.我们实现分布式锁大概通过三种方式. redis实现分布式锁 数据库实现分布式锁 zk实现分布式锁 实际上这三种和java对比看属于一类.都是属于程序外部锁. 原理剖析 上述三种分布式锁都是通过各自为依据对各个请求进行上锁,解锁从而控制放行还是拒绝.redis锁是基于其提供的setnx命令. setnx当且仅当key不存在.若给定key已

  • 浅谈Java由于不当的执行顺序导致的死锁

    我们来讨论一个经常存在的账户转账的问题.账户A要转账给账户B.为了保证在转账的过程中A和B不被其他的线程意外的操作,我们需要给A和B加锁,然后再进行转账操作, 我们看下转账的代码: public void transferMoneyDeadLock(Account from,Account to, int amount) throws InsufficientAmountException { synchronized (from){ synchronized (to){ transfer(fr

  • 详解Java中的悲观锁与乐观锁

    一.悲观锁 悲观锁顾名思义是从悲观的角度去思考问题,解决问题.它总是会假设当前情况是最坏的情况,在每次去拿数据的时候,都会认为数据会被别人改变,因此在每次进行拿数据操作的时候都会加锁,如此一来,如果此时有别人也来拿这个数据的时候就会阻塞知道它拿到锁.在Java中,Synchronized和ReentrantLock等独占锁的实现机制就是基于悲观锁思想.在数据库中也经常用到这种锁机制,如行锁,表锁,读写锁等,都是在操作之前先上锁,保证共享资源只能给一个操作(一个线程)使用. 由于悲观锁的频繁加锁,

  • Java synchronize线程安全测试

    线程的运行是与当前CPU的资源调度与时间片是有关系的,当一个线程中的执行到某一部分方法的时候轮到另外一个线程来执行相应的代码,所以还没有等到第一个线程执行完那么CPU有切换到另外一个线程来运行其相应的代码,所以这个时候假如操作公共的数据部分就会出现错误 为了解决这个问题,可以使用 synchronized 同步代码块来对公共部分进行同步操作 在用synchronize关键字修饰同步代码块时,运行代码发现不能交替卖票. 以下是初始代码 package com.itheima.Test; publi

  • java关于并发模型中的两种锁知识点详解

    1.悲观锁 悲观锁假设最坏的情况(如果果你不锁门,那么捣蛋鬼就会闯入并搞得一团糟),只有在确保其他线程不受干扰(获得正确的锁)的情况下才能执行. 一般实现如独占锁等. 安全性更高,但中低并发性效率更低. 2.乐观锁 乐观锁通过冲突检查机制判断更新过程中是否存在其他线程干扰.如果存在,操作将失败,重试(也可以不重试). CAS等常见实现. 一些乐观锁削弱了一致性,但在中低并发性下效率大大提高. 知识点扩展: 并行与分布式编程 关注的是复杂软件系统的构造,"复杂"是指多线程.分布式与GUI

  • Java8新特性之线程安全日期类

    LocalDateTime Java8新特性之一,新增日期类. 在项目开发过程中经常遇到时间处理,但是你真的用对了吗,理解阿里巴巴开发手册中禁用static修饰SimpleDateFormat吗 通过阅读本篇文章你将了解到: 为什么需要LocalDate.LocalTime.LocalDateTime[java8新提供的类] Java8新的时间API的使用方式,包括创建.格式化.解析.计算.修改 可以使用Instant代替 Date,LocalDateTime代替 Calendar,DateTi

  • java枚举是如何保证线程安全的

    前言 写在前面:Java SE5提供了一种新的类型-Java的枚举类型,关键字enum可以将一组具名的值的有限集合创建为一种新的类型,而这些具名的值可以作为常规的程序组件使用,这是一种非常有用的功能.本文将深入分析枚举的源码,看一看枚举是怎么实现的,他是如何保证线程安全的,以及为什么用枚举实现的单例是最好的方式. 枚举是如何保证线程安全的 要想看源码,首先得有一个类吧,那么枚举类型到底是什么类呢?是enum吗?答案很明显不是,enum就和class一样,只是一个关键字,他并不是一个类,那么枚举是

  • 如何测试Java类的线程安全性

    这篇文章主要介绍了如何测试Java类的线程安全性,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 线程安全性是Java等语言/平台中类的一个重要标准,在Java中,我们经常在线程之间共享对象.由于缺乏线程安全性而导致的问题很难调试,因为它们是偶发的,而且几乎不可能有目的地重现.如何测试对象以确保它们是线程安全的? 假如有一个内存书架 package com.mzc.common.thread; import java.util.Map; impo

随机推荐