多线程中,共享变量存放于共享内存,各个线程会将其copy到自己的线程内存中,更改数据,最后把线程内存中的数据复制给共享内存。
应用于多线程的乐观锁
乐观的以为每次拿取数据时别人都不会更改数据,但在更新的时候也会以某种方法判断一下有没有人更新这个数据。乐观锁应用于多读少写的场景。
判断有没有人更新数据有两种方法:版本号控制 和 CAS(compare and swap)比较和交换
版本号控制
比如,给数据加一个版本字段,凡线程对其进行数据更新后,版本号+1,当另一个线程对其更新时,会比对一下,共享内存中该数据的版本号,和当前线程中的版本号是否一致,不一致则说明有其他线程对其进行了更新,之后他要重新从内存中获取数据,再进行后续操作。
这个套路也可以用于数据库中数据的更改。
CAS
CAS有三个参考值
V:变量在共享内存中的值
A:线程本地内存的值
B:线程模拟修改变量后的值
CAS机制在更新一个变量的时候,只有当变量在线程本地内存的值A和共享内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。它与版本号控制原理类似,只不过它比较的是值而不是版本号。CAS的开销很大,它的底层有一个自旋锁(其实就是一个while判断),如果V不等于A的时候,它会一直循环,直到V等于A。CAS不能保障代码块的原子性。
CAS会引起ABA问题
假设,三个线程,一个共享变量A,线程1、3会把A变成C,如果线程1执行完后,共享变量变为C,线程3发现共享内存的值C 与 我线程内存的值A 不相等,所以线程3就不会执行,但是这时出现了一个线程2,它在线程1之后、线程3之前,将C又变回了A,那么之后线程3在执行时,发现原本内存的值A又和线程3内存值A相等了,线程3又会执行了,它将A变成了C,但这个C不是通过线程1得到的C。
实际场景,假设有100元,我开启了两个线程,他们执行的内容是,当有100元时,减去50元,这样,理论上两个线程都执行完后,还剩下50元,也就是只扣了一个50元。如果执行当中恰巧有另一个线程乱入了,这个线程执行的内容是增加50元,这个线程刚好排在了那两个线程中间执行,那么,最后一个线程执行时,他发现,-50又+50,共享内存中是100,那他符合执行条件,V=A,它也会执行一遍,最终得到的结果却是,100元被扣了2次50元。
ABA问题 通常的解决办法是添加版本号或时间戳,每次修改操作时版本号加一,这样数据对比的时候就不会出现 ABA 的问题了。
CAS在JAVA中的应用:java.util.concurrent.atomic包
比如 i++ 在多线程中的操作,如不使用sychronize修饰,将会出现问题。
假设100个线程执行对共享内存变量A=0的+1操作,得到结果往往会小于100,因为执行过程中,某个线程+1后,可能后面另一个线程已将其结果存在了共享内存中,当前线程再去存结果值,这样就会导致共享内存中的结果值变小(因为它有可能覆盖了另一个的结果值,这导致结果值比真实结果少了1)
public class Counter {
public volatile static int count = 0;
public static void inc(){
try{
Thread.sleep(1); //延迟1毫秒
}catch (InterruptedException e){ //catch住中断异常,防止程序中断
e.printStackTrace();
}
count++;//count值自加1
}
public static void main(String[] args) throws InterruptedException {
final CountDownLatch latch = new CountDownLatch(100);
for(int i=0;i<100;i++){
new Thread(new Runnable() {
@Override
public void run() {
Counter.inc();
latch.countDown();
}
}).start();
}
latch.await();
System.out.println("运行结果:"+Counter.count);
}
}
运行结果:98
由于atomic包中的方法是基于CAS来实现的,所以在线程执行时,它会判断V与A值是否相等,然后才会执行,如果不等则while自旋,直到它符合条件,才执行。
public class Counter {
public static AtomicInteger count = new AtomicInteger(0);
public static void inc(){
try{
Thread.sleep(1); //延迟1毫秒
}catch (InterruptedException e){ //catch住中断异常,防止程序中断
e.printStackTrace();
}
count.getAndIncrement();//count值自加1
}
public static void main(String[] args) throws InterruptedException {
final CountDownLatch latch = new CountDownLatch(100);
for(int i=0;i<100;i++){
new Thread(new Runnable() {
@Override
public void run() {
Counter.inc();
latch.countDown();
}
}).start();
}
latch.await();
System.out.println("运行结果:"+Counter.count);
}
}
结果:100
CAS与sychronize的比较
CAS适用于写操作少的、资源竞争较少的情况,syschronize适用于写操作多、线程冲突严重的情况。
注意:如果CAS涉及的线程很多,比如说有1000个线程去处理i++自增,那么在一个线程执行后,就有可能有999个线程在自旋(资源竞争多),过多的线程自旋会严重销毁CPU资源,这种情况下就不适合再使用CAS了,应改用sychronize关键字。
JAVA后期版本对sychronize进行了优化,使之在线程竞争少的情况下,也能获得和CAS类似的性能了。
ReadWriteLock读写锁
ReentrantReadWriteLock是ReadWriteLock接口的具体实现类
读写锁的特点是:同一时刻允许多个线程对共享资源进行读操作;同一时刻只允许一个线程对共享资源进行写操作;当进行写操作时,同一时刻其他线程的读操作会被阻塞;当进行读操作时,同一时刻所有线程的写操作会被阻塞。对于读锁而言,由于同一时刻可以允许多个线程访问共享资源,进行读操作,因此称它为共享锁;而对于写锁而言,同一时刻只允许一个线程访问共享资源,进行写操作,因此称它为排他锁。
简单来说就是,有读锁的时候,不能写,但能读,有写锁的时候,不能读也不能写
可以降锁但不能升锁,比如以下的升锁会锁死。
ReadWriteLock rtLock = new ReentrantReadWriteLock();
rtLock.readLock().lock();
rtLock.writeLock().lock();
它常被应用于构建共享缓存类中
class CachedData {
Object data; //缓存的数据
volatile boolean cacheValid; //缓存是否存在
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); //读写锁
public void processCachedData() {
rwl.readLock().lock(); //开启读锁
if (!cacheValid) {
rwl.readLock().unlock(); //关闭读锁
rwl.writeLock().lock(); //开启写锁
try {
if (!cacheValid) {
data = ...
cacheValid = true;
}
// 在释放写锁之前通过获取读锁降级写锁(注意此时还没有释放写锁)
rwl.readLock().lock();
} finally {
rwl.writeLock().unlock(); // 释放写锁而此时已经持有读锁
}
}
try {
use(data);
} finally {
rwl.readLock().unlock();
}
}
}
悲观锁
悲观锁:使用synchronized来解决多线程并发问题,以保证事务的完整性。线程在访问加锁的代码块,只会让一个线程进入,其他线程只能等待或者去执行其他没有加锁的代码块。这个线程执行完成会释放锁,由下一个线程来执行。性能慢。
如果是实例对象方法加锁的话,要对同一个实例才有效,不然不同对象各锁各的,相当于没锁。
public class TestSyn implements Runnable {
public static final Object lockHelper = new Object();
static int count = 0;
public synchronized void increase() {
for (int i = 0; i < 10000; i++) {
count++;
}
}
@Override
public void run() {
increase();
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new TestSyn());
Thread t2 = new Thread(new TestSyn());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
sychronize的三种用法
修饰实例方法,针对同一对象的被修饰方法时会对实例上锁
修饰静态方法
修饰代码块
线程安全的单例模式
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() { }
public static Singleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进入加锁代码
if (uniqueInstance == null) {
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
其中voliate关键字的作用:
保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
禁止进行指令重排序。
变量声明为 volatile,这就指示 JVM,这个变量是不稳定的,每次使用它都到主存(即共享内存)中进行读取,而不是从线程内存中读取。
可重入锁
可重入锁就是一个类的A、B两个方法,A、B都有获得统一把锁,当A方法调用时,获得锁,在A方法的锁还没有被释放时,调用B方法时,B方法也获得该锁。
sychronize和ReentrantLock都是可重入锁。
可重入锁可以防止死锁。(比如子类方法中调用父类方法,可重入锁保证了被调用的父类方法也获得了子类方法涉及的锁)
不可重入锁
不可重入锁就是一个类的A、B两个方法,A、B都有获得统一把锁,当A方法调用时,获得锁,在A方法的锁还没有被释放时,调用B方法时,B方法也获得不了该锁,必须等A方法释放掉这个锁。
线程池的作用
1.减少资源消耗(因为重复的创建、销毁会造成过多的资源消耗)
2.提高相应速度(有需求时直接拿来一个现成的线程)
3.提高线程可管理性
原理:线程池本质是一个hashSet。多余的任务会放在阻塞队列中
AQS (AbstractQueuedSynchronizer)队列同步器
Java 中常用的并发包的底层技术,想像共享内存中的值被一个线程调用,那么这个值就会被加锁,如果有其他线程此时访问该值,则将这个线程放到一个阻塞的队列中,等待锁释放,然后再执行这个线程
synchronized 和 Lock 锁之间的区别(Lock是并发包)
并发量很多的场景,Lock锁更稳定。
并发量很少的场景,sychronize与Lock锁差不多,sychronize胜在应用简单。
线程池需实现Runnable或Callable接口,前者不会返回结果,后者会。相对应的 excute() 方法提交不需返回值的任务,submit()提交有返回值的任务,返回的是future对象。
三种线程池
通过Executor 框架的工具类Executors来实现
1.FixedThreadPool ,固定大小的线程池
2.SingleThreadExecutor, 池内只有一个,多余的会报存在一个队列中
3.CachedThreadPool,根据实际情况调整大小的线程池