多个线程访问同一个数据,并且对数据的操作在底层并不是一步完成的。因此有可能被其他线程打断。
在现实生活中,多个人使用同一个设施,比如厕所或试衣间,都会加一把锁。
线程安全需要从语句的原子性,变量的可见性,代码的有序性三个方面进行考虑。
具体的解决办法就是利用锁对象或者volatile关键字。
线程可以共享数据,共享任务(多个窗口卖票),自然也可以共享一把锁。
虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了贯彻面向对象的思想,JDK5以后提供了锁对象Lock。
一般会在finally块中写unlock()以防死锁。
数据库也有锁机制。它的并发场景更多。
数据库本质上只有赋值操作,要么读数据,要么写数据吧。代码块儿没法加乐观锁吧,含有共享数据的代码就得从头锁到尾吧。
悲观锁
悲观锁(Pessimistic Lock),顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。
悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。
Java synchronized 就属于悲观锁的一种实现,每次线程要修改数据时都先获得锁,保证同一时刻只有一个线程能操作数据,其他线程则会被block。
乐观锁
乐观锁(Optimistic Lock),顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在提交更新的时候会判断一下在此期间别人有没有去更新这个数据。乐观锁适用于读多写少的应用场景,这样可以提高吞吐量。
乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。
乐观锁一般来说有以下2种方式:
使用数据版本(Version)记录机制实现。
这是乐观锁最常用的一种实现方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。
使用时间戳(timestamp)。
乐观锁定的第二种实现方式和第一种差不多,同样是在需要乐观锁控制的table中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp), 和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。
Java JUC中的atomic包就是乐观锁的一种实现,AtomicInteger 通过CAS(Compare And Set)操作实现线程安全的自增。
乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制主要采用的技术手段。
悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。在只读型事务处理中由于不会产生冲突,也没必要使用锁,这样做只能增加系统负载;还有会降低了并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数。
乐观锁( Optimistic Locking ) 相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。
相对于悲观锁,在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。一般的实现乐观锁的方式就是记录数据版本。
数据版本,为数据增加的一个版本标识。当读取数据时,将版本标识的值一同读出,数据每更新一次,同时对版本标识进行更新。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的版本标识进行比对,如果数据库表当前版本号与第一次取出来的版本标识值相等,则予以更新,否则认为是过期数据。
实现数据版本有两种方式,第一种是使用版本号,第二种是使用时间戳。
原子性
比如 a++
这一条语句在编译后实际是多条语句:先读a的值,再加1操作,再写操作,执行了3个原子操作。并发情况下,另外一个线程就有可能读到了变量 a 的中间状态,导致结果不正确。
Java内存模型中定义了8中操作都是原子的,其中6条可以满足基本数据类型的访问读写具备原子性。还剩下lock和unlock两条原子操作,用于保证更大范围的原子性操作,反应到java代码中就是利用 synchronized 实现原子性。
可见性
多个线程共享的数据需要同步更新,否则总的计算结果就会出错。
Thread visibility problems may occur in a code that isn't properly synchronized according to the java memory model. Due to compiler & hardware optimizations, writes by one thread aren't always visible by reads of another thread.
The Java Memory Model is a formal model that makes the rules of "properly synchronized" clear, so that programmers can avoid thread visibility problems.
Happens-before is a relation defined in that model, and it refers to specific executions.
当线程获取锁时会从主内存中获取共享变量的最新值,释放锁的时候会将共享变量同步到主内存中。因此,synchronized具有可见性。
volatile会通过在指令中添加lock指令,以实现可见性。
有序性
程序员希望内存模型易于理解、易于编程,希望基于一个强内存模型来编写代码。
编译器和处理器希望内存模型对它们的束缚越少越好,希望实现一个弱内存模型,这样它们就可以做尽可能多的优化来提高性能。一般重排序可以分为如下三种:
- 编译器优化的重排序
- 指令级并行的重排序
- 内存系统的重排序
最终,JMM向程序员提供了happens-before规则:
- 可以根据规则去推断代码的执行顺序,不需要理解底层重排序的规则。
- 规则的实现:JMM的编译器重排序规则会禁止一些特定类型的编译器重排序,通过插入内存屏障指令来禁止某些特殊的处理器重排序。
happens-before规则
Happens-before relationship is a guarantee that action performed by one thread is visible to another action in different thread.
一共8条,只讨论比较重要的3条。
-
程序次序规则:一段代码在单线程中执行是有序的。即单线程执行代码不用担心重排序。
-
锁定规则:一个线程释放了锁,另一个线程才能获取到锁。保证 synchronized 锁得住,另一个线程不会破门而入。
-
共享变量被volatile修饰:如果一个线程先去写一个变量,然后一个线程去进行读取,那么写入操作肯定会保证修改的值会立即被更新到主存,从而保证后续读操作的正确性,即可见性。
synchronized: 具有原子性,有序性和可见性;相当于把多线程变成了单线程,效率降低了。
volatile:具有有序性和可见性。
synchronized
//锁是实例对象
public synchronized void method () {}
synchronized (this) {}
//锁是类对象
public static synchronized void method () {}
synchronized (类.class) {}
//自己造把锁
String lock = "";
synchronized (lock) {}
volatile
volatile包含禁止指令重排序的语义,保证有序性。在单例模式的实现上有一种双重检验锁定的方式(Double-checked Locking)。
public static Singleton getSingleton() {
if (instance == null) { //Single Checked
synchronized (Singleton.class) {
if (instance == null) { //Double Checked
//包含了三个操作:1.分配对象的内存空间;2.初始化对象;3.设置instance指向刚分配的内存地址。
//但是步骤2,3可能被重排序。
instance = new Singleton();
}
}
}
return instance ;
}
死锁就是互相等待。
死锁.jpg
网友评论