乐观锁
乐观锁:它认为一般情况下不会发生并发冲突,所以只有在进行数据更新的时候,才会检测并发冲突,如果没有冲突,则直接修改,如果有冲突,则返回失败。
CAS(Compare And Swap 比较并且交换):
V(内存值)
A(旧值)
B(新值)
具体流程:V==A?true(没有并发冲突)->V=B; : false(并发冲突)
线程的解决方案:加锁、ThreadLocal、Atomic*
乐观锁 Atomic*
我们先来看一个示例:
public class ThreadDemoGJ1 {
private static int count = 0;
private static final int maxSize = 100000; //最大循环次数
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < maxSize; i++) {
count++;
}
}
});
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < maxSize; i++) {
count--;
}
}
});
t2.start();
t1.join();
t2.join();
System.out.println("结果: " + count);
}
}
发现结果不为0,因为有并发冲突
接下来我们采用乐观搜:
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadDemoGJ2 {
private static AtomicInteger count = new AtomicInteger(0);
private static final int maxSize = 100000; //最大循环次数
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < maxSize; i++) {
//count++;
count.getAndIncrement();
}
}
});
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < maxSize; i++) {
//count--;
count.getAndDecrement();
}
}
});
t2.start();
t1.join();
t2.join();
System.out.println("结果: " + count);
// AtomicInteger count = new AtomicInteger(0);
// count.getAndIncrement(); //i++
// count.incrementAndGet(); //++i
}
}
CAS底层实现原理:
答:Java层面CAS的实现是UnSafe类
Unsafe类调用了C++的本地方法,通过调用操作系统的Atomic::cmpxchg来实现CAS
优点:性能比高
缺点:ABA问题 ->上面代码中的AtomicInteger 是存在ABA问题的 A:预期旧值 B:新值
演示一下ABA问题:
import java.util.concurrent.atomic.AtomicReference;
/**
* ABA 问题演示
*/
public class ThreadDemoGJ4 {
private static AtomicReference money = new AtomicReference(100);
public static void main(String[] args) throws InterruptedException {
//转账线程1(-100)
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
//转账
boolean result = money.compareAndSet(100,0);
System.out.println("转账线程1(-100)"+result);
}
});
t1.start();
t1.join();
//转入100元
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
//+100
boolean result = money.compareAndSet(0,100);
System.out.println("转入100(+100)"+result);
}
});
t3.start();
t3.join();
//转账线程2(-100)
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
//转账
boolean result = money.compareAndSet(100,0);
System.out.println("转账线程2(-100)"+result);
}
});
t2.start();
}
}
发现t3转入的一百元影响了t2的执行。本来预想结果第二次转账应该是是失败的,因为余额没有100元。
那么如何解决这个问题呢?
ABA统一解决方案:增加版本号,每次修改之后,更新版本号。(AtomicStampedReference)添加了版本号
演示:
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* 解决ABA问题
*/
public class ThreadDemoGJ5 {
private static AtomicStampedReference money = new AtomicStampedReference(100,1);
public static void main(String[] args) throws InterruptedException {
//转账线程1(-100)
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
//转账
boolean result = money.compareAndSet(100,0,1,2);
System.out.println("转账线程1(-100)"+result);
}
});
t1.start();
t1.join();
//转入100元
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
//+100
boolean result = money.compareAndSet(0,100,2,3);
System.out.println("转入100(+100)"+result);
}
});
t3.start();
t3.join();
//转账线程2(-100)
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
//转账
boolean result = money.compareAndSet(100,0,1,2);
System.out.println("转账线程2(-100)"+result);
}
});
t2.start();
}
}
这次发现第二次转账失败了,因为两次转账的版本号不同。
那如果把金额改为1000呢?我们来看看
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* 解决ABA问题
*/
public class ThreadDemoGJ6 {
private static AtomicStampedReference money = new AtomicStampedReference(1000,1);
public static void main(String[] args) throws InterruptedException {
//转账线程1(-100)
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
//转账
boolean result = money.compareAndSet(1000,0,1,2);
System.out.println("转账线程1(-1000)"+result);
}
});
t1.start();
t1.join();
//转入100元
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
//+100
boolean result = money.compareAndSet(0,1000,2,3);
System.out.println("转入1000(+1000)"+result);
}
});
t3.start();
t3.join();
//转账线程2(-100)
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
//转账
boolean result = money.compareAndSet(1000,0,1,2);
System.out.println("转账线程2(-1000)"+result);
}
});
t2.start();
}
}
发现三次操作都失败了,那是因为Integer高速缓存(-127 -> +128)
设置应用程序参数(-D) ,设置Integer高速缓存的最大值:
悲观锁
悲观锁:他认为通常情况下会出现并发冲突,所以在进入方法之后就会进行检测。
!!!synchronized是悲观锁。
怎么理解乐观锁和悲观锁的,具体怎么实现呢?
答:乐观锁->CAS->Atomic*,CAS组成是由三个东西组成:V (内存值)A(预期旧值) B(新值)组成。执行的时候使用V==A,如果为true,则表明没有并发冲突,则可以直接修改,否则不能修改。CAS通过C++提供的UnSafe中的本地方法实现的(CompareAndSwapXXX)。C++是通过调用系统的Atomic::cmpxchg(原子指令)来实现的
悲观锁:->synchronized 在Java中是将锁的id存放到到对象头中。synchronized在jvm层面是通过监视器锁来实现,在操作系统层面是通过互斥锁mutex实现。
synchronized优化(锁消除):
JDK1.6锁升级过程:
无锁
偏向锁(第一个线程第一次访问)将线程id放在对象头中的偏向锁标识。
轻量级锁(自旋)
重量级锁
共享锁与非共享锁
共享锁:一把可以被多个线程拥有,这就叫共享锁
读写锁中的读锁就是共享锁。
非共享锁(独占锁):一把锁只能被一个线程拥有,这就叫非共享锁。
synchronized是非共享锁
读写锁
读写锁:就是将一把锁分成两个,一个用于读数据的锁,另一把锁叫做写锁。读锁可以被多个线程同时拥有,而写锁只能被一个线程拥有。
读写锁的具体实现:
import java.util.Date;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ThreadDemoGJ7 {
public static void main(String[] args) {
//创建一个读写锁
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
//读锁
ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
//写锁
ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
//线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(10,10,0, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000));
//任务1:读锁
executor.execute(new Runnable() {
@Override
public void run() {
//加锁
readLock.lock();
try{
//业务逻辑处理
System.out.println(Thread.currentThread().getName() + "执行了读锁操作" + new Date());
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
readLock.unlock();
}
}
});
//任务2:读锁
executor.execute(new Runnable() {
@Override
public void run() {
//加锁
readLock.lock();
try{
//业务逻辑处理
System.out.println(Thread.currentThread().getName() + "执行了读锁操作" + new Date());
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
readLock.unlock();
}
}
});
//任务3:写锁
executor.execute(new Runnable() {
@Override
public void run() {
writeLock.lock();
try{
System.out.println(Thread.currentThread().getName()+ "执行了写锁操作" + new Date());
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
}
});
//任务4:写锁
executor.execute(new Runnable() {
@Override
public void run() {
writeLock.lock();
try{
System.out.println(Thread.currentThread().getName()+ "执行了写锁操作" + new Date());
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
}
});
}
}
可以发现,两个线程的读操作的时间是一致的,而写操作不一致,说明了读锁是共享锁,而写锁是非共享锁。
读写锁的优势:锁的力度更加的小,性能也更高。
注意事项:读写锁中的读锁和写锁是互斥的,防止同时读写所产生的脏数据。
公平锁与非公平锁
公平锁:锁的获取顺序必须和线程访问的先后顺序保持一致,就叫做公平锁
非公平锁:锁的获取顺序,和线程线程获取锁的前后顺序无关,就叫做非公平锁(默认锁策略)
非公平锁的优点:性能比较高。
公平锁优点:执行时有序的,所以结果也是可以预期的。
公平锁:new ReentrantLock(true)
非公平锁:new ReentrantLock()/new ReentrantLock(false)synchronized
自旋锁
自旋锁:通过死循环一直尝试获取锁。
缺点:如果发生死锁,则会一直循环,所以会带来一定的额外开销。
while(true){
if(尝试获取锁) return;
}
可重入锁
可重入锁:当一个线程获取一个锁之后,可以重复进入。
public class ThreadDemoGJ8 {
//创建锁
private static Object lock = new Object();
public static void main(String[] args) {
synchronized (lock){
System.out.println("第一次进入锁");
synchronized (lock){
System.out.println("第二次进入锁");
}
}
}
}