简单聊聊面试最爱问的线程安全

线程安全聊到数据库安全

1. 什么是线程安全?

1.1 先说说那三个背烂了的性质

  • 原子性 :对共享数据的操作必须是要么全部执行直到执行结束,且中间过程不能被任何外部因素打断,要么就不执行,和数据库的事务原子性很像。其实还是举例子容易明白,A给B转账100元,读取的B的余额为0,理论上转完应该是100元,但是还没有当A把数据写入的时候,C读取了B的余额0元,也向B转帐了100元,实际上最终B的余额应该为200元,但是因为线程不安全的问题,最终余额为100元,少了100元。
  • 可见性 :可见性就是,共享数据的实时变化是对多个线程可见的,啥是可见的呢?共享数据变了,每个线程都知道他变了,立马能看到,这就是可见的。实际上,每个线程都会有自己的缓存,当某个线程从主内存中读取数据并修改时,并不会立即将修改后的数据同步到主内存中,所以,当其他的线程读取共享数据时,读取的数据还是旧的,这就是不可见。
  • 顺序性 :按照代码的顺序进行执行。其实在单线程实际情况时,JVM都会对代码进行优化重排,在保证结果正确的情况下,优化代码的执行顺序。

1.2 如何保证以上三点性质?

1.2.1 sychronized

我们要直到,每个对象都有一个内部锁,你看不见,也不用你自己来声明锁,当对象调用sychronized修饰的方法或代码块时,它会自动获取锁,执行完毕,自己又释放锁。保证的是原子性可见性,为什么也有可见性,因为一次只有一个线程能访问共享变量,线程访问结束,对共享变量的修改对其他线程都是可见的。

1.2.2 lock

lock需要我们自己声明锁对象,调用lock()来锁住下面的代码,执行完要释放锁unlock()。同样,保证的也是原子性可见性

ReetrantLock myLock = new ReetrantLock();
myLock.lock();
try{
	...
}finally{
	myLock.unlock();
}

1.2.3 volatile

Volatile关键字只对基本类型 (byte、char、short、int、long、float、double、boolean) 的赋值操作和对象的引⽤赋值操作有效,保证的是可见性和顺序性,不能保证数据的读取,翻转和写入不被中断。只有在对共享数据只进行赋值时,可以用volatile标记。

1.2.4 java.util.concurrent.atomic

我们拿这个包下的类就来弥补volatile的不足,来保证原子性 我们拿AtomicInteger来举个例子。其本质是利用了CPU级别的CAS指令。由于是CPU级别的指令,其开销比需要操作系统参与的锁的开销小,它属于乐观指令,不会加锁。

public static AtomicInteger num = new AtomicInteger;
//它下面的increaseAndGet方法,将以原子的方式对num进行自增的操作(num++)
num.increaseAndGet();

2. 归纳整理的一些常问的问题

锁和synchronized即可保证原子性也可保证可见性,为何还需要volatile?

synchronized和锁需要通过操作系统来仲裁谁获得锁,开销比较高,而volatile开销小很多。因此在只需要保证可见性的条件下,使用volatile的性能要比使用锁和synchronized高得多。

既然锁和synchronized可以保证原子性,为什么还需要AtomicInteger这种的类来保证原子操作?

锁和synchronized需要通过操作系统来仲裁谁获得锁,开销比较高,而AtomicInteger是通过CPU级的CAS操作来保证原子性,开销比较小。所以使用AtomicInteger的目的还是为了提高性能。

还有没有别的办法保证线程安全?

有。尽可能避免引起非线程安全的条件——共享变量。如果能从设计上避免共享变量的使用,即可避免非线程安全的发生,也就无须通过锁或者synchronized以及volatile解决原子性、可见性和顺序性的问题。

synchronized即可修饰非静态方式,也可修饰静态方法,还可修饰代码块,有何区别?

synchronized修饰非静态同步方法时,锁住的是当前实例;synchronized修饰静态同步方法时,锁住的是该类的Class对象;synchronized修饰静态代码块时,锁住的是synchronized关键字后面括号内的对象。

参考

Synchronized和Lock的区别

  1. 首先synchronized是java内置关键字在jvm层面,Lock是个java类。
  2. synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁。
  3. 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了。
  4. synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)
  5. Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
  6. synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁,并且可以主动尝试去获取锁。

2. 说说数据库的悲观锁和乐观锁

2.1 悲观锁

悲观锁:正如其名,它的态度比较悲观,具有强烈的独占和排他特性。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度。因此,在整个数据处理过程中,将数据处于锁定状态。当我们认为数据被并发修改的概率比较大的时候,要加悲观锁。

2.1.1 共享锁或排他锁

  • 共享锁(S):用法lock in share mode,又称读锁,允许多个事务对读同一个数据共享一把锁,都能访问到数据,但是只能读不能修改。若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
  • 排他锁(X):用法for update,又称写锁,排他锁就是不能与其他锁并存,如果一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据行读取和修改。若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。在没有索引的情况下,InnoDB只能使用表锁。

2.1.2 悲观锁的使用

//需要关闭自动提交
select quantity from products where id = 1 for update;
update products set quanntity = 2 where id = 1;
commit

2.2 乐观锁

乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测.

2.2.1 乐观锁的使用

使用乐观锁就不需要借助数据库的锁机制,而是在修改数据之前,先对数据进行查询检测,然后再是数据更新.典型方式是CAS技术.
CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
但是会存在传说中的ABA问题

避免ABA问题的方法:
通过一个单独的可以顺序递增的version字段或者添加时间戳

但是乐观锁的弊端也很明显,不能解决高并发问题,所以一定要控制好乐观锁的粒度.

2.3 事务的并发问题

  • 脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据
  • 不可重复读:事务A多次读取同一数据,事务B在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果不一致。
  • 幻读:A事务读取了B事务已经提交的新增数据。注意和不可重复读的区别,这里是新增,不可重复读是更改(或删除)。select某记录是否存在,不存在,准备插入此记录,但执行 insert 时发现此记录已存在,无法插入,此时就发生了幻读。例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。
原创文章 34 获赞 8 访问量 1161

猜你喜欢

转载自blog.csdn.net/qq_46225886/article/details/105588971
今日推荐