锁与线程安全

synchronized的4种应用方式

synchronized关键字最主要有以下3种应用方式,都是作用在对象上

  1. 修饰类,作用范围:synchronized括号内, 作用对象:类的所有对象;synchronized(Service.class){ }
  2. 修改静态方法,作用范围:整个静态方法, 作用对象:类的所有对象;
  3. 修饰方法,被修饰的同步方法,作用范围:整个方法, 作用对象:调用这个方法的对象;
    1. 缺点:A线程执行一个长时间任务,B线程必须等待
  4. 修饰代码块,被修饰的代码块同步语句块,作用范围:大括号内的代码, 作用对象:调用这个代码块的对象;
    1. 优点:减少锁范围,耗时的代码放外面,可以异步调用

synchronized底层 每个对象有一个监视器锁(monitor),当monitor被占用时处于锁定状态

1.线程执行monitor enter指令时尝试获取monitor的所有权:

1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。

2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数+1.

3、如果其他线程已经占用monitor,该线程进入阻塞状态,直到monitor的进入数为0,再尝试获取monitor的所有权

2.线程执行monitor exit指令

monitor的进入数-1,如果-1后进入数为0,线程退出monitor;其他被monitor阻塞的线程尝试获取 monitor

每个线程获得锁后,要完成变量 copy到工作内存->   修改->   刷新主存   的过程,才会释放它得到的锁,达到线程安全。

  1. 锁住(lock)
  2. 主->从 将需要的数据从主内存拷贝到自己的工作内存(read and load)
  3. 修改 根据程序流程读取或者修改相应变量值(use and assign)
  4. 从->主 将自己工作内存中修改了值的变量拷贝回主内存(store and write)
  5. 释放对象锁(unlock)

线程安全:

  1. 当多个线程访问某个类,其始终能表现出正确的行为
  2. 采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,限制其他线程访问,直到锁释放

 

synchronized和volatile区别 锁的目标:互斥和可见性

1、锁提供了两种主要特性:互斥性(mutual exclusion) 和可见性(visibility)。

  互斥即一次只允许一个线程持有某个锁,使用该共享数据。

  可见性确保新值立即同步到主存,每次使用前立即从主内存刷新

2、在Java中,为了保证多线程读写数据时保证数据的一致性,可以采用两种方式:

  synchronized同步:释放锁之前会将对变量的修改刷新到主存当中;阻塞

  volatile关键字:确保新值立即同步到主存,每次使用前立即从主内存刷新;非阻塞

3.区别

1)volatile非阻塞,synchronized只有当前线程可以访问修饰的变量,其他线程阻塞

2)volatile仅能修饰变量,synchronized则可以使用在变量,方法.

3)volatile仅能实现变量的修改可见性,而synchronized则可以保证变量的修改可见性和原子性(操作不可分割)

volatile详解

1.实现对所有线程的可见性

volatile保证新值立即同步到主存,每次使用前立即从主内存刷新

   编译器为了加快程序运行的速度,对一些变量的写操作会先在(工作内存)寄存器或者是CPU缓存上进行,最后才写入内存,这个过程中,变量的新值对其他线程是不可见的

volatile适用场景: 某线程修改某个状态变量,通知其他线程做别的事

确保只有单一的线程修改变量的值 或 运算结果不依赖当前变量值

变量不需要与其他的状态变量共同参与不变约束

2.实现禁止指令重排序优化

  1. java不能保证变量赋值操作的顺序与代码中一致
  2. volatile修饰的变量相当于生成内存屏障,重排序时不能把后面的指令排到屏障之前

3.无法实现 i++ 原子操作

A读取 i 后,B也读取 i ,此时A进行 +1,B的 i 就变了

原子类如何解决:CAS,B在进行+1时,检查此时的 i 跟主存的 i 是否一致,一致才+1

Java中的锁优化 编码、JDK

编码 锁优化

  1. 减少锁持有时间 
    1. 使用同步代码块,而非同步方法;
  2. 减小锁粒度
    1. JDK1.6中 ConcurrentHashMap采取对segment加锁而不是整个map加锁,提高并发性;
  3. 锁分离  读锁之间不互斥
    1. 根据同步操作的性质,把锁划分为的读锁和写锁,读锁之间不互斥,提高了并发性

JDK1.6 锁优化 synchronized底层

1.引入偏向锁、轻量级锁

  1. 锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级
  2. 锁可以升级不可降级,提高 获得锁和释放锁 效率
  3. “轻量级锁”和“偏向锁”作用:减少 获得锁和释放锁 的性能消耗

优点

缺点

适用场景

偏向锁

记录线程iD,加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。

如果线程间存在锁竞争,会带来额外的锁撤销的消耗。

适用于只有一个线程访问同步块场景。

轻量级锁

自旋方式竞争,竞争的线程不会阻塞,提高了程序的响应速度CAS尝试将头部的二进制位修改00

如果始终得不到锁竞争的线程使用自旋会消耗CPU。

追求响应时间。

同步块执行速度非常快。

重量级锁

线程竞争不使用自旋,不会消耗CPU。

线程阻塞,响应时间缓慢。

追求吞吐量。

同步块执行速度较长。

 

2.锁粗化 

如果一系列的连续操作都对同一个对象反复加锁和解锁,如循环体内,很耗性能

       加锁同步的范围扩展到整个操作序列的外部:第一个append到最后一个append

3.锁消除 逃逸分析的数据的支持

        编译器判断到一段代码中,堆上的数据不会逃逸出当前线程,可以认为是线程安全的,不必加锁

4.自旋与自适应自旋:想要获取锁的线程做几个空循环 10

.为什么引入:

轻量级锁失败后,线程会在操作系统层面挂起,

操作系统实现线程之间的切换时,需要从用户态转换到核心态,状态转换耗时

.解决方法:

假设不久当前的线程可以获得锁,虚拟机会让当前想要获取锁的线程做几个空循环,可能是50个循环或100循环

.结果:

如果得到锁,就顺利进入临界区;如果不能,就将线程在操作系统层面挂起,升级为重量级锁

.JDK做的优化:自适应自旋

当线程在获取轻量级锁时CAS操作失败时,通过自旋让线程等待,避免线程切换的开销

自旋是需要消耗CPU的,如果一直获取不到锁,线程一直自旋,浪费CPU资源

线程如果自旋成功了,下次自旋的次数会更多,自旋失败了,自旋的次数就会减少。

CAS底层实现原理

CAS:Compare and Swap, 翻译成比较并交换 

CAS需要在:操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,最终都会返回内存地址,且是原子操作

需要3个操作数:内存地址V,旧预期值A、新值B

当且仅当V符合预期值A时(即V存储的值无变化),用B更新A,否则不执行更新,最终都返回内存地址V

死锁

多个线程因竞争资源而造成僵局(互相等待),无法向前推进

产生的原因

1) 系统资源的竞争

系统不可剥夺资源,数量不足以满足多个进程运行,使得进程在运行过程中,因竞争资源而陷入僵局

2) 进程推进顺序非法

请求和释放资源的顺序不当,也同样会导致死锁。如,互相申请各占有的资源。

信号量使用不当也会造成死锁。进程间彼此相互等待消息,结果也会使得这 些进程间无法继续向前推进。

3) 死锁产生的必要条件

产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生。

  • 资源互斥条件:资源互斥,即某资源仅为一个进程占有
  • 资源不可剥夺条件:进程所获得的资源在未使用完毕之前,只能是主动释放,不能被其他进程强行夺走
  • 保持和请求条件:进程已经保持了一个资源,又提出了新的资源请求,而该资源已被其他进程占有
  • 循环等待条件:进程资源循环等待

如何避免死锁

  1. 加锁顺序(线程按照一定的顺序加锁)
    1. 按照顺序加锁是一种死锁预防机制,需要事先知道所有会用到的锁
  2. 加锁时限(超时则放弃)
    1. 获取锁时加上时限,超过时限则放弃请求,并释放锁,等待一段随机的时间再重试
  3. 死锁检测与恢复
    1. 操作系统中:系统为进程分配资源,不采取任何限制性措施,提供检测和解脱死锁的手段

死锁检测:当一个线程请求锁失败时,遍历锁的关系图检测死锁。

死锁恢复

  1. 撤消进程,剥夺资源
  2. 线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁
  3. 死锁发生的时候设置随机的优先级;如果赋予这些线程的优先级是固定不变的,同一批线程总是会拥有更高的优先级。

锁模式包括: 

  1. 共享锁:(读取)用户可以并发读取数据,但不能获取写锁,直到释放所有读锁。
  2. 排他锁(写锁):加上写锁后,其他线程无法加任何锁;写锁可以读和写
  3. 更新锁: 防止死锁而设立,转换读锁为写锁之前的准备,仅一个线程可获得更新锁

乐观锁:认为数据一般情况下不会造成冲突,在数据提交更新时,才进行数据的冲突检测;

如果冲突,返回I信息让用户决定如何去做。实现方式:记录数据版本。

悲观锁:操作数据时上锁保护,限制其他线程访问,直到该锁释放。关系型数据库锁机制,行锁、页锁、表锁,都是在做操作之前先上锁。

锁的粒度: 都是悲观锁

  1. 行锁: 粒度最小,并发性最高
  2. 页锁:锁定一页。25个行锁可升级为一个页锁。
  3. 表锁:粒度大,并发性低
  4. 数据库锁:控制整个数据库操作

重入锁 Reentrantlock

  1. 无阻塞的同步机制,实现了独占功能
  2. 加锁和解锁都需要显式写出,实现了Lock接口,注意一定要在适当时候unlock
  3. 添加了轮询锁、定时锁等候和可中断锁特性;
  4. 提供了一个Condition(条件)类,用来实现分组唤醒线程
  5. 默认使用非公平锁,可插队跳过对线程队列的处理
    1. ReentrantLock的内部类Sync继承了AQS,分为公平锁FairSync和非公平锁NonfairSync。
  • 公平锁:线程获取锁的顺序和调用lock的顺序一样,FIFO;
  • 非公平锁:线程获取锁的顺序和调用lock的顺序无关

公平锁为了保证线程规规矩矩地排队,需要增加阻塞和唤醒的时间开销;直接插队获取非公平锁,跳过了对队列的处理,速度会更快

不要抛弃 synchronized

  1. 易忘记 finally 块释放锁,对程序有害
  2. synchronized 管理锁定和释放时,能标识死锁或者其他异常行为的来源,利于调试
  3. Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多

使用场景

对锁进行更精确的控制,分组唤醒

轮询锁、定时锁、可中断锁

 

介绍Condition    Java多线程系列--“JUC锁”06之 Condition条件

对锁进行更精确的控制

Condition中的await()方法相当于Object的wait()方法

Condition中的signal()方法相当于Object的notify()方法

Condition中的signalAll()相当于Object的notifyAll()方法

Condition函数列表

造成当前线程在接到信号或被中断之前一直处于等待状态 void await() // 造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态 boolean await(long time, TimeUnit unit) // 造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态 long awaitNanos(long nanosTimeout) // 造成当前线程在接到信号之前一直处于等待状态 void awaitUninterruptibly() // 造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态 boolean awaitUntil(Date deadline) // 唤醒一个等待线程 void signal() // 唤醒所有等待线程 void signalAll()

介绍AbstractQueuedSynchronizer  一个用来构建锁和同步工具的框架

  1. 使用int类型的volatile变量维护同步状态(state),
  2. 使用Node实现FIFO队列存放阻塞的等待线程,来完成线程的排队执行
  3. 围绕state提供两种基本操作“获取”和“释放”
  4. 组合AQS对象的方式实现锁的语义

AQS与锁(如Lock)的对比:

  • 锁是面向使用者的,锁定义了用户调用的接口,隐藏了实现细节;
  • AQS是锁的实现者,通过用AQS简化了锁的实现屏蔽了同步状态管理,线程的排队,等待唤醒的底层操作。
  • 简而言之,锁是面向使用者,AQS是锁的具体实现者。

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值