线程安全
多线程下同时对共享数据进行读写操作造成的数据混乱,称之为线程安全问题。
当多线程并发访问临界资源时,如果破坏其可见性、原子性、有序性时可能会造成数据不一致。
临界资源:共享资源,同一时刻只允许只有一个线程进行读写,才能保证数据一致性。
可见性:
当多个线程修改一个共享变量时候,其中一个线程修改了共享变量的值,其他线程可以感知到变量的值的变化,这叫可见性。
其实可见性与内存模型有关系,简单一点描述,就是,每个线程会拷贝一份共享变量到自己的线程的缓存中,当每个线程修改变量也就是修改的是当前线程缓存中的变量的值,如果想要其他线程也要感知到变量的变化的话,就需要当前线程将修改过的变量的值同步到主内存中,其他线程再从主内存中重新获取一次新的值。
java中的关键字volatile可以保证共享变量的可见性,另外,synchronized和lock锁也可以保证共享变量的可见性,那是因为锁可以保证只能有一个线程在同一时刻操作共享变量,操作完以后会将线程缓存和主内存中的变量进行同步。
volatile的两个作用就是可见性和禁止指令重排序。单例模式中静态属性声明为volatile的作用就是禁止指令重排序,防止对象半初始化。
原子性:
同一时刻允许有且只有一个线程进行操作。单一不可分割的操作。
比如:i++就不是原子性的操作,其实i++可以分为3步走
第一步:获取 i 的本身的值;
第二步:i+1
第三步:将+1之后的新值赋值给 i;
所以要想保证原子性,可以给大的方法加锁来实现。CAS(CompareAndSet)也是通过锁来保证原子性的。
有序性:
有序性是指让CPU按照代码的顺序依次执行。
造成不是有序性的原因是因为编译器和CPU为了提高运行效率,会对执行进行重排序,不会按照代码的顺序依次执行。
可以对操作进行synchronized和lock加锁来保证有序性。也可以利用happens-before原则来保证程序的有序性。
happens-before 表达的并不是说前面一个操作发生在后面一个操作的前面,尽管从程序员编程角度来看也并不会出错,但它其实表达的是,前一个操作的结果对后续操作是可见的。
如何解决线程不安全的问题
1、取消共享
共享资源取消共享,局部变量代替
共享资源只读
2、ThreadLocal
ThreadLocal其实是利用Map实现的,key是当前的线程,value是存放的值。
ThreadLocal用完以后为了防止内存泄漏需要清除保存的数据。因为ThreadLocal中的key是弱引用,value是强引用,很容易造成内存泄露。
JMM内存模型
内存模型
JMM在线程缓存和主内存之间数据的同步操作:
保证可见性的原理:
内存屏障
内存屏障就是防止指令重排序。
Java中规范定义的内存屏障。
原子类
一个操作不可中断,不可分割;
原子类的作用和锁的作用相同;
通常,原子类比加锁粒度更细,效率更高。
java提供了一些原子类,在java.util.concurrent.atomic包下;
private static AtomicInteger value = new AtomicInteger(10);
private static int[] origin = new int[]{1,2,3};
private static AtomicIntegerArray aia = new AtomicIntegerArray(origin);
public static void main(String[] args) throws Exception {
value.compareAndSet(value.get(),10);
value.decrementAndGet();
value.getAndDecrement();
value.incrementAndGet();
value.getAndIncrement();
value.set(10);
value.lazySet(10);
value.getAndAdd(5);
value.addAndGet(5);
//原子数组比原子类多了一个下标
//创建原子数组的时候是根据原数组拷贝了一个新的数组,所以修改原子数组的时候,不会影响到原数组origin
aia.getAndAdd(1,5);
}
累加器
LongAdder longAdder = new LongAdder();
longAdder.add(5);
longAdder.increment();
System.out.println(longAdder.longValue());
LongAccumulator longAccumulator = new LongAccumulator((x,y)->x+y,1);
System.out.println(longAccumulator.get());
longAccumulator.accumulate(1);
System.out.println(longAccumulator.get());
longAccumulator.accumulate(2);
System.out.println(longAccumulator.get());
锁
乐观锁和悲观锁
悲观锁
乐观锁
CAS就是乐观锁的例子。
CAS
CAS缺点
如果一直设置失败,会一直尝试,这样就会造成很大的资源开销。
自旋锁的核心思想就是CAS。
Sleep和wait方法的区别
wait()方法如果不指定时间,则需要被动唤醒,如果指定了等待时间,则不需要唤醒。
锁升级
JDK6之前,java重量级锁(也就是常见的synchronized锁)效率比较低,在用户态和内核态之间切换来进行锁的互斥等操作。synchronized是基于Monitor(管程)实现的。JDK6之后对锁进行了升级。对synchronized的锁状态分为了4种。因为synchronized锁是对象锁,所以在对象的堆内存上有固定的位置标志对应的锁状态。
无锁:
未上锁的状态。
偏向锁:
没有线程竞争的情况下,第一个获得锁的线程会把线程id写入堆内存中,同时锁会升级为偏向锁。如果同一个线程重复上锁的话,依旧是偏向锁。jvm中在jvm启动4s之后创建的对象才会启动偏向锁。
轻量级锁:
当两个以上的线程串行(交替,不是并发)的情况下获取锁,锁会升级为轻量级锁。基于CAS自旋锁。
重量级锁:
多个线程并发的获取锁的情况下锁会升级为重量级锁。用户态切换为内核态。
可重入锁(可递归锁)
ReentrantLock
ReentrantLock锁手动显示的上锁和释放锁,更灵活
//ReentrantLock锁手动显示的上锁和释放锁,更灵活
private ReentrantLock reentrantLock = new ReentrantLock();
new Thread(() -> {
try {
reentrantLock.lock();
//do something
} finally {
if (reentrantLock.isHeldByCurrentThread()) {
reentrantLock.unlock();
}
}
}).start();
tryLock()
ReentrantLock.tryLock()方法如果锁被别的线程占用,则获取锁失败。
tryLock(time)传入参数表示获取锁最大等待时间,在这个时间内锁被释放了,也是可以获取到的。
锁中断
ReentrantLock.lockInterruptibly()
如果当前线程已经持有此锁,则持有计数增加1,并且该方法立即返回。如果锁由另一个线程持有,那么当前线程将出于线程调度目的而被禁用,并处于休眠状态,直到发生以下两种情况之一:
当前线程获得锁;
或者其他线程中断了当前线程。
如果锁是由当前线程获取的,则锁持有计数设置为1。如果当前线程在进入此方法时设置了中断状态;或者在获取锁时被中断,则抛出InterruptedException,并清除当前线程的中断状态。
公平锁和非公平锁
ReentrantLock锁是可以设置公平还是非公平锁的,默认是false非公平锁,设置为true就是公平锁。
ReentrantLock reentrantLock = new ReentrantLock(true);