Java并发编程3---锁和同步工具类(未完成)

Java并发编程3—锁和同步工具类

文章目录

  • Java并发编程3---锁和同步工具类
    • 锁的意义
      • 并发、并行与同步
      • 锁的特性
        • 原子性/互斥性
        • 可见性
    • 锁的分类
      • 乐观锁
      • 悲观锁/互斥锁
      • 自旋锁
      • 可重入锁
      • 读写锁
      • 数据库中的锁
        • 事务的ACID特性
        • 事务隔离级别
          • 脏读、不可重复读、幻读的含义
        • 锁分类
        • MVCC
        • InnoDB的MVCC实现方式
    • 内置锁Synchronized
      • Java对象的内存布局
        • 一个字(Word)对应多少字节(byte)?
        • 同步方法和同步代码块
        • 对象头
      • 锁膨胀
        • Object_Monitor:对象监视器
    • 显式锁
      • Lock接口
      • ReentrantLock
      • ReentrantReadWriteLock
    • 队列同步器AQS
      • 同步队列
      • Condition
      • Semphore

锁的意义

并发、并行与同步

  • 并发:在单核CPU中,应用进程为了提高CPU的利用率,将多个线程交替执行,如背10分钟单词,然后读10分钟古诗,并发不要求线程同时执行,实际上在单核CPU中同一时刻只有一个线程在执行;
  • 并行:现代处理器中,通常有多核CPU,这些CPU可以同时执行多个线程,如一个可以一心二用的人同时背单词和读古诗,这样提高多核CPU的利用率;并行是并发在多CPU环境的体现;
  • 同步:但无论是多核CPU的并行执行还是单核CPU的并发执行,它们都共享一个主内存(每个CPU中都有其私有的寄存器、缓冲区等),如何防止并发(并行)的线程们交替(同时)读写同一个内存块(变量)时读到失效值,即控制多个线程的执行顺序就是同步的意义所在了;同步还有另一个与异步相对的概念:线程等待调用的结束后才能进行下一步的执行;
  • 异步:线程不等待调用的结果,继续做下面的工作,该调用结束后通过某种方式回调,告诉线程执行的结果。异步执行的意义在于不阻塞线程的流程,提高CPU利用率和应用执行速度。

我们在Java并发编程1中就指出了并发编程的缺点:

  • 上线文切换的开销很大
  • 共享内存带来线程安全性问题

对于第一个问题,我们建议限制并发的线程的数量来减少线程的上下文切换;而对于第二个问题,通过某些手段控制线程的执行顺序来解决,即线程同步。

在Java中,实现线程同步有多种方式:

  • 同步原语(隐式锁):Synchronized
  • 显式锁:JUC包的Lock接口

它们通过设置同步代码块的临界区来控制多个线程对共享变量的访问顺序:

  • 当有一个线程A获得了临界区的访问权限(即获得了锁)时,会使CPU中的缓存行(寄存器)失效,重新从主内存中读取该变量;
  • 在该线程退出临界区(即释放锁)之前,其他线程不可再次进入该临界区;
  • 线程A对共享变量的写入会在释放锁的时候回写到主内存中;
  • 后续其他线程在获得了临界区的访问权限时,会读取到线程A写入主内存的数据;
  • 从而实现了多线程之间共享变量的互斥性可见性。此即Java锁的意义。

锁的特性

原子性/互斥性

原子性: 操作的原子性指该操作的所有步骤要么全做要么全都不做,不存在中间状态;

互斥性:一个线程在进行临界区操作时,其他线程不可进入临界区。

可见性

可见性:线程A对共享变量写入后的值,可以被其他线程使用该共享变量时读取。

原子性/互斥性和可见性共同组成了锁的特性,正是由于这些特性的存在,才能使共享变量在并发的线程中被安全正确地使用。

锁的分类

乐观锁

乐观锁:在读取共享变量时,乐观地认为没有其他线程和其竞争写入该共享变量,所以不加锁,只会在更新时才会判断一下该共享变量是否被其他线程修改过,如果没有修改过则更新成功;否则更新失败抛出异常或返回;

在Java中,乐观锁的一种实现方式叫做CAS(Compare And Swap),其通过sun.misc.Unsafe类进行比较并交换操作,该类的方法都为native方法,具体的代码通过C语言实现。

乐观锁是一种无锁同步的思想;CAS是这种思想的实现方式。

悲观锁/互斥锁

悲观锁:在读取共享变量时,悲观地认为一定会有其他线程和其竞争写入该变量,所以会在进入临界区时会加锁,以阻止其他线程进入该临界区,其他线程在进入该临界区时发现已被其他线程上锁了,会阻塞在该临界区的监视器(Monitor)上,直到获取锁的线程释放了锁,然后所有阻塞在该监视器(Monitor)上的线程进行锁的竞争,此即为悲观锁的互斥性。

在Java中,悲观锁为同步原语SynchronizedLock接口。

自旋锁

与悲观锁类似,差别在于,线程在进入临界区时如果发现已被加锁,不会进入阻塞在对象监视器上,而是自旋等待获取锁的机会。有以下问题:

  • 自旋锁获得到CPU时间片时,使CPU空转,会造成CPU的浪费;
  • 自旋锁不可重入,所以递归调用时不可使用自旋锁。

可重入锁

可重入锁:顾名思义,即同一个线程在获取某个锁后,可以再次获取该锁,进入到临界区中;如果一个锁不支持重入,那么在递归时会出现死锁。

在Java中,SynchronizedReentrantLock等都是可重入锁。

读写锁

读写锁: 一个共享变量或临界区可以被多个读线程访问,或者被一个写线程访问,但两者不可同时进行。读写锁维护了一对锁:写锁和读锁,通过分离读写锁使得并发性相比一般的互斥锁有了很大的提升,因为在大部分场景下都是读操作。

在Java中,``ReentrantReadWriteLock是读写锁的具体实现,支持线程重入和公平性选择。使用方式和Lock接口的一致,在临界区开始时进行l.lock(),在finally块中进行l.unlock()`释放。

  • int getReadLockCount():返回当前读锁被获取的次数,但并不等于获取读锁的线程数,因为可能有线程重入了读锁;
  • int getReadHoldCont():返回当前线程获取读锁的次数。使用ThreadLocal保存当前线程获取读锁的次数;
  • int getWriteHoldCount():返回当前写锁被获取的次数;
  • Boolean isWriteLock():判断写锁是否被获取

该锁支持锁的降级:从写锁降级为读锁,即写锁释放后,其他读线程可以都获取读锁或者写线程获取写锁;但是不支持锁升级,因为有多个读线程同时持有读锁时进行升级,这些读线程都不会释放锁,这样有可能导致死锁。

数据库中的锁

事务的ACID特性

特性 含义 举例
原子性(Atomiciy) 事务中的操作要么全做,要么全不做,不存在只做一部分的情况 小明给小红转账分两步:
1. 小明账户金额-500;
2. 小红账户+500
这两步操作,要么是都执行了,要么是都不执行。
一致性(Consistency) 事务执行前后的数据状态保持一致 小明给小红转账前后的两个账户的总金额保持不变
隔离性(isolation) 一个事物的执行过程,一般对其他事务不可见 小明给小红转账过程中,其他转账事务看不到小明和小红的账户金额变化
持久性(durability) 事务执行完成后,数据会被保存起来,即便系统发生了故障,数据也不会丢失 小明给小红转账成功后,不会因为系统崩溃而导致转账失败

事务隔离级别

隔离级别 脏读 不可重复读 幻读 隔离方式(争议,待确认)
未提交读(READ UNCOMMITTED) 读不加锁,写加排他锁
提交读(READ COMMITTED) MVCC:快照读
可重复读(REPEATABLE READ) MVCC:当前读
序列化(SERIALIZABLE) 事务排队,单线程执行
脏读、不可重复读、幻读的含义
名称 含义 举例 原因
脏读 事务读取到了其他事务未提交的中间状态 事务T1 update table set a = 5 where a=4; 且未提交
事务T2 select a from table,得到a=5
写操作加排他锁,读操作不加锁,所以读操作不会被排他锁阻塞
不可重复读 一个事务多次查询相同条件,得到的结果不一致 事务T1第一次 select b from table,得到b =5,未提交;
事务T2 update table set b = 6;并提交;
事务T1第二次 select b from table,得到b = 6。
MVCC:快照读,读取当前事务之前事务提交的数据,防止了脏读;但未对满足条件的行加锁,于是其他事务可以对这些行进行写操作(update、delete),当其他事务提交后,该事务再次执行时发现查询结果发生了变化;
幻读 一个事务多次查询相同的条件,得到的结果条数增加 事务T1第一次 select count(*) from table where a = 5,得到3行记录,未提交;
事务T2 insert table(a) values (5)并提交;
事务T1第二次 select count(*) from table where a = 5,得到4行记录;
多出来的一行记录就像幻觉
MVCC:当前读,读取记录的最新版本,且对满足条件的行及相邻的行(next-key)加锁,防止其他事务可以对这些行进行写操作(update、delete);但是其他事务进行插入后,该事务再次执行时发现结果记录条数发生了变化;
序列化(即串行化)后,所有事务排队串行执行,防止了某一事务对其他事务的干扰。

锁分类

锁定内容 优点 缺点 支持的引擎
表锁 一次锁定整张表,可以同时读,只能获得锁的事务写 开销小,加锁快,锁粒度大,不会出现死锁; 锁冲突概率高;并发度低 MyISAM、Memory、InnoDB
行锁 一次锁定满足条件的记录 锁粒度小,锁冲突概率低,并发度高 开销大,加锁慢,会出现死锁 InnoDB
页锁 一次锁定相邻的一组记录 锁粒度、开销和加锁速度一般 并发度一般 BDB

InnoDB的行锁实现方式:通过给索引上的索引项加锁来实现的,所以只有通过索引条件进行检索数据,InnoDB才会使用行锁,否则将使用表锁,会导致大量的锁冲突,带来严重的并发性能问题。

  • 由于InnoDB是对索引而不是记录加的锁,所以在访问不同行记录时,如果使用的相同的索引,也会出现锁冲突;
  • 当表有多个索引时,不同的事务可以使用不同的索引锁定不同的记录;
  • 即便在条件中使用了索引字段,如果MySQL在进行分析时发现全表扫描效率更高(比如很小的表),也会不使用行锁而是表锁,这就会导致并发事务的锁冲突。

Next-Key:间隙锁:当使用范围条件进行检索数据并加行锁时,InnoDB除了锁定符合条件的已有记录以外,还会对范围条件内不存的记录(即间隙:GAP)加锁。

InnoDB引擎默认是RR(可重复读)的隔离级别,在这种隔离级别下会对范围条件加上间隙锁,以防止幻读。

MVCC

MVCC:多版本并发控制,是乐观锁思想的一种实现方式。

乐观锁假设多用户并发的事务在处理时不会相互影响,各事务在不加锁的情况下处理各自的那部分数据;只会在提交数据更新时,去检查当前数据有没有被其他事务修改过,如果没有被修改过则更新更改,否则进行事务回滚。

这样的好处在于:读不加锁,读写不冲突,在读多写少的场景下极大地增加了系统的并发性能。

MVCC使用多版本的数据进行并发控制,数据在初始化时指定一个版本号,每次更新操作会对版本号进行+1操作,相邻版本之间有指针关联,即新的版本数据指针指向上一版的数据。

InnoDB的MVCC实现方式

在MySQL的InnoDB中,实现的是非完全的多版本并发控制:读操作分为的快照读和当前读。

  • 快照读:读取记录的可见版本,可能是历史版本,且不对记录加锁,实现非阻塞;
  • 当前读:读取记录的最新版本,并且加上行锁,阻止其他事务的并发修改。
名称 SQL 加锁
快照读 普通的select语句:
select * from table where ?
不加锁
当前读 显式加锁的select语句:
select * from table where ? lock in share mode
select * from table where ? for update
DML语句:
insert into table values (…);
update table set ? where ?;
delete from table where ?; 行锁

其中,DML语句之所以是当前读,是因为在MySQL中,类似update操作,都是MySQL server读取一条记录,加锁返回,然后再进行update操作,一条数据操作完成再操作下一条记录;insert语句可能触发唯一键检查,先读取当前键值是否存在。

相关参考资料:

  1. MySQL学习之——锁(行锁、表锁、页锁、乐观锁、悲观锁等)
  2. 深入理解Mysql——锁、事务与并发控制
  3. MySqL 事务与锁的深入学习笔记
  4. MySQL 加锁处理分析

内置锁Synchronized

Java对象的内存布局

Java对象在堆内存中分为三个部分:
借用图片

  1. 对象头(2或3个字长)
  • Mark Word:标记字,存储对象的hashcode或锁信息等,一个字长
  • Class Metadata Address:类元素地址,存储指向对象类型数据的指针,一个字长
  • Array length:如果是数组的话,还有一个字来存储数组的长度
  1. 实例变量:如int,占用4个字节,long占用8个字节;
  2. 填充数据:补齐对象头和实例变量占用内存的。CPU从内存中读取数据是以字(Word)为基本单位,将对象的内存补齐为字的整数倍会大大提高CPU存取数据的性能。

具体参看Java对象占用内存大小

一个字(Word)对应多少字节(byte)?

这个取决于虚拟机的位数,32位机的字长就是4个字节32位;64位机的字长就是8个字节64位。
所谓位数,即CPU一次读取内存的大小,32位机,一次读取32位(bit)的内存数据,CPU的位数对应着地址总线的宽度,地址总线的宽度决定了寻址空间的大小,如32位机的寻址大小即内存的大小为2^32=4GB。

同步方法和同步代码块

对象头

锁膨胀

  • 无锁状态
  • 偏向锁
  • 轻量级锁
  • 重量级锁

Object_Monitor:对象监视器

相关参考资料:

  1. 深入理解Java并发之synchronized实现原理
  2. 《Java并发编程实战》—Brian Goetz等著
  3. 《Java并发编程的艺术》—方腾飞等著

显式锁

Lock接口

ReentrantLock

ReentrantReadWriteLock

队列同步器AQS

同步队列

Condition

Semphore

你可能感兴趣的:(Java并发编程总结和浅析)