目录
从锁的粒度,我们可以分成两大类:
不同的存储引擎支持的锁粒度是不一样的:
表锁下又分为两种模式:
乐观锁悲观锁应用
机制
实现方式
使用场景
实际应用
CAS
CAS与synchronized的使用情景
InnoDB只有通过索引条件检索数据才使用行级锁,否则,InnoDB将使用表锁
从上面已经看到了:读锁和写锁是互斥的,读写操作是串行。
max_write_lock_count
和low-priority-updates
值得注意的是:
- MyISAM可以支持查询和插入操作的并发进行。可以通过系统变量
concurrent_insert
来指定哪种模式,在MyISAM中它默认是:如果MyISAM表中没有空洞(即表的中间没有被删除的行),MyISAM允许在一个进程读表的同时,另一个进程从表尾插入记录。- 但是InnoDB存储引擎是不支持的!
悲观锁
是一种悲观思想,它总认为最坏的情况可能会出现,它认为数据很可能会被其他人所修改,所以悲观锁在持有数据的时候总会把资源
或者 数据
锁住,这样其他线程想要请求这个资源的时候就会阻塞,直到等到悲观锁把资源释放为止。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。悲观锁的实现往往依靠数据库本身的锁功能实现。
乐观锁的思想与悲观锁的思想相反,它总认为资源和数据不会被别人所修改,所以读取不会上锁,但是乐观锁在进行写入操作的时候会判断当前数据是否被修改过。乐观锁的实现方案一般来说有两种: 版本号机制
和 CAS实现
。乐观锁多适用于多度的应用类型,这样可以提高吞吐量。
乐观锁,version方式:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。sql实现代码:
update table
set x=x+1, version=version+1
where id=#{id} and version=#{version};
CAS操作方式:即compare and swap 或者 compare and set,
悲观锁,是由数据库自己实现的,要用的时候,我们直接调用数据库的相关语句就可以了(原理:共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程),如行锁、读锁和写锁等,都是在操作之前加锁。
乐观锁:比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。
在Java中
java.util.concurrent.atomic
包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。
悲观锁:比较适合写入操作比较频繁的场景,如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。
Java 中的
Synchronized
和ReentrantLock
等独占锁(排他锁)也是一种悲观锁思想的实现,因为 Synchronzied 和 ReetrantLock 不管是否持有资源,它都会尝试去加锁,生怕自己心爱的宝贝被别人拿走。
悲观锁:
一般来说,悲观锁不仅会对写操作加锁还会对读操作加锁,一个典型的悲观锁调用:
select * from student where name="cxuan" for update
这条 sql 语句从 Student 表中选取 name = "cxuan" 的记录并对其加锁,那么其他写操作再这个事务提交之前都不会对这条数据进行操作,起到了独占和排他的作用。
悲观锁因为对读写都加锁,所以它的性能比较低,对于现在互联网提倡的三高
(高性能、高可用、高并发)来说,悲观锁的实现用的越来越少了,但是一般多读的情况下还是需要使用悲观锁的,因为虽然加锁的性能比较低,但是也阻止了像乐观锁一样,遇到写不一致的情况下一直重试的时间。
乐观锁:
乐观锁用于读多写少的情况,即很少发生冲突的场景,这样可以省去锁的开销,增加系统的吞吐量。
乐观锁的适用场景有很多,典型的比如说成本系统,柜员要对一笔金额做修改,为了保证数据的准确性和实效性,使用悲观锁锁住某个数据后,再遇到其他需要修改数据的操作,那么此操作就无法完成金额的修改,对产品来说是灾难性的一刻。
CAS 即 compare and swap(比较与交换)
,是一种有名的无锁算法。即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步
CAS 中涉及三个要素:
- 需要读写的内存值 V
- 进行比较的值 A
- 拟写入的新值 B
仅当 预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
JAVA对 CAS 的支持:在JDK1.5 中新添加 java.util.concurrent (J.U.C) 就是建立在 CAS 之上的。对于 synchronized 这种阻塞算法,CAS是非阻塞算法的一种实现。所以J.U.C在性能上有了很大的提升。
我们以 java.util.concurrent 中的AtomicInteger
为例,看一下在不用锁的情况下是如何保证线程安全的
public class AtomicCounter {
private AtomicInteger integer = new AtomicInteger();
public AtomicInteger getInteger() {
return integer;
}
public void setInteger(AtomicInteger integer) {
this.integer = integer;
}
public void increment(){
integer.incrementAndGet();
}
public void decrement(){
integer.decrementAndGet();
}
}
public class AtomicProducer extends Thread{
private AtomicCounter atomicCounter;
public AtomicProducer(AtomicCounter atomicCounter){
this.atomicCounter = atomicCounter;
}
@Override
public void run() {
for(int j = 0; j < AtomicTest.LOOP; j++) {
System.out.println("producer : " + atomicCounter.getInteger());
atomicCounter.increment();
}
}
}
public class AtomicConsumer extends Thread{
private AtomicCounter atomicCounter;
public AtomicConsumer(AtomicCounter atomicCounter){
this.atomicCounter = atomicCounter;
}
@Override
public void run() {
for(int j = 0; j < AtomicTest.LOOP; j++) {
System.out.println("consumer : " + atomicCounter.getInteger());
atomicCounter.decrement();
}
}
}
public class AtomicTest {
final static int LOOP = 10000;
public static void main(String[] args) throws InterruptedException {
AtomicCounter counter = new AtomicCounter();
AtomicProducer producer = new AtomicProducer(counter);
AtomicConsumer consumer = new AtomicConsumer(counter);
producer.start();
consumer.start();
producer.join();
consumer.join();
System.out.println(counter.getInteger());
}
}
经测试可得,不管循环多少次最后的结果都是0,也就是多线程并行的情况下,使用 AtomicInteger 可以保证线程安全性。 incrementAndGet 和 decrementAndGet 都是原子性操作。本篇文章暂不探讨它们的实现方式。而普通的消费者生产者无法保证每次都是0,因为
原因是 count -= 1 和 count += 1 都是非原子性操作,它们的执行步骤分为三步:
如果要把证它们的原子性,必须进行加锁,使用 Synchronzied
或者 ReentrantLock。
#并发执行1000次加减
import junit.framework.Test;
public class Counter {
int cnt = 0;
public int getCnt(){
return cnt;
}
public void setCnt(){
this.cnt = cnt;
}
public void add(){
cnt += 1;
}
public void dec(){
cnt -= 1;
}
public static class Consumer extends Thread{
Counter counter;
public Consumer(Counter counter){
this.counter = counter;
}
@Override
public void run() {
for(int j = 0; j < Test.LOOP; j++){
counter.dec();
System.out.println("consumer is :" + counter.getCnt());
}
}
}
public static class Producer extends Thread{
Counter counter;
public Producer(Counter counter){
this.counter = counter;
}
@Override
public void run() {
for(int i = 0;i < Test.LOOP;++i){
counter.add();
System.out.println("producer is :" + counter.getCnt());
}
}
}
public static class Test{
final static int LOOP = 1000000;
public static void main(String[] args) throws InterruptedException{
Counter counter = new Counter();
Producer producer = new Producer(counter);
Consumer consumer = new Consumer(counter);
producer.start();
consumer.start();
producer.join();
consumer.join();
System.out.println(counter.getCnt());
}
}
}
简单的来说 CAS 适用于写比较少的情况下(多读场景,冲突一般较少),synchronized 适用于写比较多的情况下(多写场景,冲突一般较多)