悲观锁(Pessimistic Lock)
在对一条数据进行修改时,为了避免其他人同时对这一条数据进行更改,我们就可以通过锁机制,对数据进行上锁防止以并发问题;
线程认为线程的安全问题是很容易发生的,会对代码上锁。
因为悲观锁的上锁和释放锁的过程是需要消耗很多资源的,因此会降低程序的性能。
(锁机制看上篇)
乐观锁(Optimistic Locking)
相对于悲观锁来说,乐观锁认为线程问题是不容易发生的,因此不会对代码上锁。
乐观锁本身是不会对操作的数据进行加锁,而是通过一些业务的操作实现的,如果线程过多,也不建议使用;
实现的方式:
-
版本号机制
利用版本号记录数据更新的次数,一旦更新版本号加1,线程修改数据后会判断版本号是否是自己更新的次数,如果不是就不更新数据。
-
CAS (Compare And Swap)比较和交换算法
-
通过内存的偏移量获得数据的值
-
计算出一个预计的值
-
将提交的实际值和预计值进行比较,如果相同执行修改,如果不同就不修改
-
悲观锁和乐观锁的对比
-
悲观锁更加重量级,占用资源更多,应用线程竞争比较频繁的情况,多写少读的场景
-
乐观锁更加轻量级,性能更高,应用于线程竞争比较少的情况,多读少写的场景
public class AtomicDemo {
static int count = 0;
public static void main(String[] args) {
for (int i = 0; i < 100000; i++) {
new Thread(() ->{
count++;
}).start();
}
System.out.println(count);
}
}
问题:多线程同时执行++操作,最后结果少了
分析:
count++ 分解为三个指令:
-
从内存中读取count的值
-
计算count+1的值
-
将计算结果赋值给count
这三个指令不是原子性的,A线程读取count值10,加1后得到11,准备赋值给count;B线程进入读取count也是10,加1得到11,赋值给count为11;切换会A线程,赋值count为11。
解决方案:
-
悲观锁,使用同步方法、同步块、同步锁
-
乐观锁
使用原子整数
悲观锁解决方案:
public class Demo {
static Integer count=0;
static Object object=new Object();
Lock lock=new ReentrantLock();
public void aa(){
//同步锁
lock.lock();
try{
count++;
}finally {
lock.unlock();
}
}
// 同步方法
public synchronized void cc(){
count++;
}
// 同步代码块
public void dd(){
synchronized (object){
count++;
}
}
public static void main(String[] args) {
Demo demo = new Demo();
for (int i = 0; i < 100000; i++) {
new Thread(()->{
demo.dd();
}).start();
}
System.out.println("count:"+count);
}
}
乐观锁的解决方案:
原子类
java.util.concurrent.atomic包下的类大多是使用 CAS 操作来实现的;
如AtomicInteger类:
常用方法:
-
incrementAndGet 原子递增
-
decrementAndGet 原子递减
public class AtomicDemo {
static AtomicInteger atomicInteger=new AtomicInteger(0);
//乐观锁
public void bb(){
count++;
atomicInteger.incrementAndGet();
}
public static void main(String[] args) {
AtomicDemo atomicDemo = new AtomicDemo();
for (int i = 0; i < 100000; i++) {
new Thread(()->{
atomicDemo.bb();
}).start();
}
System.out.println("count:"+count);
System.out.println("atomic:"+atomicInteger.get());
}
}
CAS算法存在的问题
1.ABA问题 :
count=3;Thread1获取count=3
Thread2获取count=3 再修改成 count=4;再将count=4 =>count=3;
Thread1再进行操作,然后执行成功了,但这并不代表这个过程没有问题;
2.如果预期值和实际值不一致处于循环等待状态,对CPU的消耗比较大
编写懒汉式的单例模式,创建100个线程,每个线程获得一个单例对象,看是否存在问题(打印对象的hashCode,看是否相同)
public class SingletonDemo {
private static SingletonDemo singleton = null;
private SingletonDemo() {
System.out.println("创建对象");
}
public static SingletonDemo getInstance(){
if(singleton==null){
singleton = new SingletonDemo();
}
return singleton;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"-----"+SingletonDemo.getInstance().hashCode());
}).start();
}
}
}
我们会发现:
多线程的情况下,不止一次调用了构造方法,产生了多个对象,就不是单例了(如果没有发生这种情况,是因为第一次创建的过程太快了,让这个线程sleep,就会发现了)
这是因为第一次在创建对象过程中时 又执行了别的线程别的线程也进行了判断,也创建了对象
if(singleton==null){ singleton = new SingletonDemo(); }
解决问题:
可以通过同步方法、同步代码块、同步锁的方式的方式都可已解决
public synchronized static SingletonDemo getInstance(){
if(singleton==null){
singleton = new SingletonDemo();
}
return singleton;
}
但是这种方法就会产生一个问题:
每次执行的时候都需要大量上锁、释放锁;我们知道大量的上锁、释放锁会消耗资源,而单例模式用的是一个对象,因此这样的方式显然也不是最佳解。
那么我们的单例类的方法这样写:
public synchronized static SingletonDemo getInstance(){
//如果为空,再执行代码,提升性能
if(singleton==null){
//同步代码块 保证原子性
synchronized (SingletonDemo.class){
//创建对象
if(singleton==null){
singleton = new SingletonDemo();
}
}
}