异步
相对的概念:线程等待调用的结束后才能进行下一步的执行;我们在Java并发编程1中就指出了并发编程的缺点:
对于第一个问题,我们建议限制并发的线程的数量来减少线程的上下文切换;而对于第二个问题,通过某些手段控制线程的执行顺序来解决,即线程同步。
在Java中,实现线程同步有多种方式:
它们通过设置同步代码块的临界区来控制多个线程对共享变量的访问顺序:
互斥性
和可见性
。此即Java锁的意义。原子性: 操作的原子性指该操作的所有步骤要么全做要么全都不做,不存在中间状态;
互斥性:一个线程在进行临界区操作时,其他线程不可进入临界区。
可见性:线程A对共享变量写入后的值,可以被其他线程使用该共享变量时读取。
原子性/互斥性和可见性共同组成了锁的特性,正是由于这些特性的存在,才能使共享变量在并发的线程中被安全正确地使用。
乐观锁:在读取共享变量时,乐观地认为没有其他线程和其竞争写入该共享变量,所以不加锁,只会在更新时才会判断一下该共享变量是否被其他线程修改过,如果没有修改过则更新成功;否则更新失败抛出异常或返回;
在Java中,乐观锁的一种实现方式叫做CAS(Compare And Swap)
,其通过sun.misc.Unsafe
类进行比较并交换
操作,该类的方法都为native方法,具体的代码通过C语言实现。
乐观锁是一种无锁同步的思想;CAS是这种思想的实现方式。
悲观锁:在读取共享变量时,悲观地认为一定会有其他线程和其竞争写入该变量,所以会在进入临界区时会加锁,以阻止其他线程进入该临界区,其他线程在进入该临界区时发现已被其他线程上锁了,会阻塞在该临界区的监视器(Monitor)上,直到获取锁的线程释放了锁,然后所有阻塞在该监视器(Monitor)上的线程进行锁的竞争,此即为悲观锁的互斥性。
在Java中,悲观锁为同步原语Synchronized
和Lock
接口。
与悲观锁类似,差别在于,线程在进入临界区时如果发现已被加锁,不会进入阻塞在对象监视器上,而是自旋等待获取锁的机会。有以下问题:
可重入锁:顾名思义,即同一个线程在获取某个锁后,可以再次获取该锁,进入到临界区中;如果一个锁不支持重入,那么在递归时会出现死锁。
在Java中,Synchronized
和ReentrantLock
等都是可重入锁。
读写锁: 一个共享变量或临界区可以被多个读线程访问,或者被一个写线程访问,但两者不可同时进行。读写锁维护了一对锁:写锁和读锁,通过分离读写锁使得并发性相比一般的互斥锁有了很大的提升,因为在大部分场景下都是读操作。
在Java中,``ReentrantReadWriteLock是读写锁的具体实现,支持线程重入和公平性选择。使用方式和Lock接口的一致,在临界区开始时进行
l.lock(),在finally块中进行
l.unlock()`释放。
该锁支持锁的降级:从写锁降级为读锁,即写锁释放后,其他读线程可以都获取读锁或者写线程获取写锁;但是不支持锁升级,因为有多个读线程同时持有读锁时进行升级,这些读线程都不会释放锁,这样有可能导致死锁。
特性 | 含义 | 举例 |
---|---|---|
原子性(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才会使用行锁,否则将使用表锁,会导致大量的锁冲突,带来严重的并发性能问题。
Next-Key:间隙锁:当使用范围条件进行检索数据并加行锁时,InnoDB除了锁定符合条件的已有记录以外,还会对范围条件内不存的记录(即间隙:GAP)加锁。
InnoDB引擎默认是RR(可重复读)的隔离级别,在这种隔离级别下会对范围条件加上间隙锁,以防止幻读。
MVCC:多版本并发控制,是乐观锁思想的一种实现方式。
乐观锁假设多用户并发的事务在处理时不会相互影响,各事务在不加锁的情况下处理各自的那部分数据;只会在提交数据更新时,去检查当前数据有没有被其他事务修改过,如果没有被修改过则更新更改,否则进行事务回滚。
这样的好处在于:读不加锁,读写不冲突,在读多写少的场景下极大地增加了系统的并发性能。
MVCC使用多版本的数据进行并发控制,数据在初始化时指定一个版本号,每次更新操作会对版本号进行+1操作,相邻版本之间有指针关联,即新的版本数据指针指向上一版的数据。
在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语句可能触发唯一键检查,先读取当前键值是否存在。
相关参考资料:
Java对象在堆内存中分为三个部分:
具体参看Java对象占用内存大小
这个取决于虚拟机的位数,32位机的字长就是4个字节32位;64位机的字长就是8个字节64位。
所谓位数,即CPU一次读取内存的大小,32位机,一次读取32位(bit)的内存数据,CPU的位数对应着地址总线的宽度,地址总线的宽度决定了寻址空间的大小,如32位机的寻址大小即内存的大小为2^32=4GB。
相关参考资料: