线程
线程的daemons熟悉:默认false 用户线程 true:守护线程,随着主线程结束而结束
jdk1.6之前:syncchronized 加锁对象为操作系统的monitor,其他线程加锁不成功 丢入队列(重量级锁,(线程阻塞、上下文切换、操作系统线程调度)性能问题)
1.6之后 引入一种偏向锁,当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁;多个线程竞争变为轻量级锁,当重度竞争(自适应自旋)变为重量级锁
公平锁与非公平锁:
java的ReentrantLock,通过构成函数默认是非公平锁,优点在于吞吐量比公平锁大,对于Synchronized,也是一种非公平锁
公平锁::并发环境下,如果当前线程是等待队列的第一个,就占有锁,否者就会加入到等待队列中,按照fifo的规则从队列获取
非公平锁:比较暴力,上来就尝试占有锁,如果尝试失败,在采用公平锁的方式,
公平锁:判断state ==0,判断是否去要排队,在判断是否可重入
非公平锁:直接cas修改state未1 ,修改不了 再去判断state ==0 等于0再去cas修改为1,修改不了去入队 最后在排队是否可重入
可重入锁(递归锁):
可重入锁:(递归锁)线程可以进入任何一个它已经拥有锁的同步代码块。
生活案例:
家里的大门有一把锁,厕所没有上锁。我进了大门了,就不用在厕所上锁了。sychronized,ReentrantLock就是把可重入锁
(源码 在判断锁的state 变量 在变更时 ,再去判断 获取锁的线程是不是当前线程)
同一线程 获取锁成功后,再去加锁 可直接获取锁 (可重入锁)
自旋锁,互斥锁
spinlock,尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,有点 减少线程上下文切换(CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再次加载这个任务的状态,从任务保存到再加载的过程就是一次上下文切换。)的消耗,缺点循环会消耗cpu。
自旋锁加锁失败后,线程会忙等待,直到它拿到锁
互斥锁是一种独占锁,比如当线程 A 加锁成功后,此时互斥锁已经被线程 A 独占了,只要线程 A 没有释放手中的锁,线程 B 加锁就会失败,于是就会释放 CPU 让给其他线程,既然线程 B 释放掉了 CPU,自然线程 B 加锁的代码就会被阻塞。对于互斥锁加锁失败而阻塞的现象,是由操作系统内核实现,互斥锁加锁失败后,线程会释放 CPU ,给其他线程
读写锁
在并发场景中用于解决线程安全的问题,我们几乎会高频率的使用到独占式锁,通常使用java提供的关键字synchronized(关于synchronized可以看这篇文章)或者concurrents包中实现了Lock接口的ReentrantLock。它们都是独占式获取锁,也就是在同一时刻只有一个线程能够获取锁。而在一些业务场景中,大部分只是读数据,写数据很少,如果仅仅是读数据的话并不会影响数据正确性(出现脏读),而如果在这种业务场景下,依然使用独占锁的话,很显然这将是出现性能瓶颈的地方。针对这种读多写少的情况,java还提供了另外一个实现Lock接口的ReentrantReadWriteLock(读写锁)。读写所允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞。在分析WirteLock和ReadLock的互斥性时可以按照WriteLock与WriteLock之间,WriteLock与ReadLock之间以及ReadLock与ReadLock之间进行分析。更多关于读写锁特性介绍大家可以看源码上的介绍(阅读源码时最好的一种学习方式,我也正在学习中,与大家共勉),这里做一个归纳总结:
公平性选择:支持非公平性(默认)和公平的锁获取方式,吞吐量还是非公平优于公平;
重入性:支持重入,读锁获取后能再次获取,写锁获取之后能够再次获取写锁,同时也能够获取读锁;
锁降级:遵循获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁
乐观锁与悲观锁
悲观:悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。Java 中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。悲观锁通常多用于写多比较多的情况下(多写场景),避免频繁失败和重试影响性能。
乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。在 Java 中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。观锁通常多于写比较少的情况下(多读场景),避免频繁加锁影响性能,大大提升了系统的吞吐量。
CAS 的全称是 Compare And Swap(比较与交换) ,用于实现乐观锁,被广泛应用于各大框架中。CAS 的思想很简单,就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。
CAS 是一个原子操作,底层依赖于一条 CPU 的原子指令。
原子操作 即最小不可拆分的操作,也就是说操作一旦开始,就不能被打断,直到操作完成。
CAS 涉及到三个操作数:
V :要更新的变量值(Var)
E :预期值(Expected)
N :拟写入的新值(New)
当且仅当 V 的值等于 E 时,CAS 通过原子方式用新值 N 来更新 V 的值。如果不等,说明已经有其它线程更新了V,则当前线程放弃更新。
表锁和行锁、全局锁
全局锁: flush table with read lock;
释放: unlock tables;
全局锁让整个数据库(所有表)处于只读状态,使用这个命令后,数据库表的增删改(DML)、表结构的更改(DDL)、更新类事物的提交都会被阻塞;
上面看到了全局锁会让数据库只处于可读的状态,这种状态会使数据库处于一个多么低效率的状态,那么为什么还需要它呢?
低效率的原因:
如果你在主库上备份,那么在备份期间都不能执行更新,业务基本上就得停摆;
如果你在从库上备份,那么备份期间从库不能执行主库同步过来的binlog,会导致主从延迟;
因为在以前,全局锁的主要作用就是:做全库逻辑备份
针对于表级的锁 表锁;元数据锁(mete data lock,MDL)
表锁:
加锁命令: lock tables 表名 read/write
释放锁的命令:unlock tables;
特点:
当还没有出现更细粒度的锁时,表锁是常用的处理并发问题的方式,而对于InnoDB这种支持行锁的引擎,一般不适用表锁,因为表锁的影响效率还是很大;对某个表加表锁,不仅影响其他线程对该表的对应操作,也会影响当前线程对这张表的操作。 当对某个表执加上写锁后(lock table t1 write),该线程可以对这个表进行读写,其他线程对该表的读和写都受到阻塞
MDL锁:
特点:在访问一个表的时候会自动加上,它的作用是保证读写的正确性;
当对一个表做增删查改的时候,加MDL读锁,当对表结构做更改操作的时候,加MDL写锁;
读锁之间不互斥,所以可以多个线程同时对一张表增删查改;
读写锁之间,写锁之间是互斥的,如果有多个线程要同时给一个表加字段,其中一个要等待另外一个执行完成才能开始执行;
事物中的MDL锁,在语句执行时开始申请,但是语句结束后并不会马上释放,而是等到这个事物提交后才释放; (⭐)
行锁:
行锁是在引擎层由各个引擎自己实现的,有的引擎并不支持行锁,比如MyISAM就不支持行锁,这意味着:并发控制只能使用表锁,对于这种引擎(MyISAM)的表,同一张表上任何时刻只能有一个更新在执行,这严重影响了并发度;
InnoDB是支持行锁的,这也是MyISAM被InnoDB代替的主要原因;
1. 行锁特性
首先注意:InnoDB的行锁是针对索引加的锁,不是针对记录加的锁,并且该索引不能失效,否则都会从行锁升级为表锁
分布式锁
基于redis实现
获取锁使用命令:
SET resource_name my_random_value NX PX 30000
线程池