线程安全问题先决条件
当同时满足以下三个条件时,才可能引发线程安全问题。
多线程环境
有共享数据
有多条语句操作共享数据/单条语句本身非原子操作
并发编程的三个重要特性
1.原子性 :所谓原子性就是指一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。synchronized 可以保证代码片段的原子性。
2.可见性 :可见性是指多个线程访问一个资源时,该资源的状态、值信息等对于其他线程都是可见的。如果当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。volatile 关键字可以保证共享变量的可见性。
3.有序性 :代码在执行的过程中的先后顺序,Java 在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。volatile 关键字可以禁止指令进行重排序优化。
什么是指令重排?
Java允许编译器和处理器对指令进行重排,但是指令重排并不会影响单线程的顺序,它影响的是多线程并发执行的顺序性。
举例解释
uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:
为 uniqueInstance 分配内存空间
初始化 uniqueInstance
将 uniqueInstance 指向分配的内存地址
由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。
指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。
使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
java中的锁
Java 各种锁的小结
Java中的锁主要用于保障多并发线程情况下数据的一致性。
在多线程编程中为了保障数据的一致性,我们通常需要在使用对象或者方法之前加锁,这时如果有其他线程也需要使用该对象或者该方法,则首先要获得锁,如果某个线程发现锁正在被其他线程使用,就会进入阻塞队列等待锁的释放,直到其他线程执行完成并释放锁,该线程才有机会再次获取锁进行操作。这样就保障了在同一时刻只有一个线程持有该对象的锁并修改对象,从而保障数据的安全。
1. synchronized关键字
(使) 同步,在时间上一致,同速进行;
synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行
在 JDK 1.6 之前,synchronized 是重量级锁,效率低下。
从 JDK 1.6 开始,synchronized 做了很多优化,减少锁操作的开销。
特性:
深入理解synchronized底层原理,一篇文章就够了!
1. 原子性
java中原子操作:
对基本数据类型的变量的读取和赋值操作是原子性操作;
synchronized修饰的类或对象的所有操作都是原子的;
java中非原子操作:
像i++、i+=1等操作字符就不是原子性的,它们是分成读取、计算、赋值几步操作。
2. 可见性
synchronized如何保证可见性的?
synchronized对一个类或对象加锁时,这个锁的状态对于其他任何线程都是可见的,并且在释放锁之前会将对变量的修改刷新到主存当中,保证资源变量的可见性。
3.有序性
synchronized保证了每个时刻都只有一个线程访问同步代码块,也就确定了线程执行同步代码块是分先后顺序的,保证了有序性。
4. 可重入性
当一个线程试图操作一个由其他线程持有的对象锁的资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的资源时,这种情况属于重入锁。通俗一点讲就是说一个线程拥有了锁仍然还可以重复申请锁。
synchronized 关键字最主要的三种使用方式:
类锁:使用 synchronized 修饰静态方法以及 synchronized(class) 同步代码块使用的锁是类锁。
对象锁:使用 synchronized 修饰非静态方法以及 synchronized(this) 同步代码块使用的锁是对象锁。
私有锁:在类内部声明一个私有属性如private Object lock,在需要加锁的同步块使用 synchronized(lock)
Object lock = new Object();
synchronized (lock) {
while (条件不满足) {
lock.wait();
}
}
synchronized (lock) {
条件发生改变
lock.notify();
每个类只有一个类锁,每一个对象对应一个对象锁。
类锁和对象锁不会产生竞争。
对象锁和私有锁也不会产生竞争。
类锁和私有锁也不会产生竞争。
使用私有锁可以减小锁的细粒度,减少由锁产生的开销。
synchronized 使用情况----学习笔记
构造方法可以使用 synchronized 关键字修饰么?
构造方法本身就属于线程安全的,不存在同步的构造方法一说。
2. volatile关键字:
背景知识:
CPU 缓存模型的数据不一致性 --> 自
volatile 关键字 除了防止 JVM 的指令重排 ,还有一个重要的作用就是保证变量的可见性。
说说 synchronized 关键字和 volatile 关键字的区别
volatile 关键字是线程同步的轻量级实现,所以 volatile 性能肯定比synchronized关键字要好
volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块
volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。
volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。
java中锁的种类
从乐观和悲观的角度可分为乐观锁和悲观锁;
从获取资源的公平性角度可分为公平锁和非公平锁;
从是否共享资源的角度可分为共享锁和独占锁;
从锁的状态的角度可分为偏向锁、轻量级锁和重量级锁;
同时,在JVM中还巧妙设计了自旋锁以更快地使用CPU资源。
乐观锁与悲观锁
面试必备之深入理解自旋锁.
乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,
使用场景:乐观锁适用于多读的应用类型,这样可以提高吞吐量
比如数据库提供的类似于write_condition机制,其实都是提供的乐观锁;
实现方式:用版本号机制或CAS算法实现
1. 版本号机制
一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加1。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
2. CAS算法
即compare and swap(比较与交换),是一种有名的无锁算法;
乐观锁的缺点
ABA 问题
ABA问题指第1个线程从内存的V位置取出A,这时第2个线程也从内存中取出A,并将V位置的数据首先修改为B,接着又将V位置的数据修改为A,这时第1个线程在进行CAS操作时会发现在内存中仍然是A,然后第1个线程操作成功。尽管从第1个线程的角度来说,CAS操作是成功的,但在该过程中其实V位置的数据发生了变化,只是第1个线程没有感知到罢了,这在某些应用场景下可能出现过程数据不一致的问题。
2 循环时间长开销大
3 只能保证一个共享变量的原子操作
悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)
使用场景:一般多写的场景下用悲观锁就比较合适
比如数据库的行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。