一.什么是原子操作?如何实现原子操作?
原子操作即为操作的最小单元,比如i=1,这样一个简单的赋值操作便是原子操作。再例如i=i+1就不是一个原子操作,因为这个语句包括了读取i,i+1,将结果写入内存。这三个操作单元。保证原子操作即保证这三个单元在操作中具有原子性,也就是保证这个操作单元在操作中,不能因线程竞争等情况而被打断。如果操作单元被打断,那么计算结果会发生变化,也就无法保证线程的安全。
那么如何实现原子操作?大家也许会第一时间想到synchronized,它的本质是阻塞锁,执行持有锁的线程,阻塞其它线程的执行。这样在多且小的原子操作中会面临几个问题。(1)被阻塞的线程优先级很高怎么办?(2)拿到锁的原子操作不释放锁怎么办?(3)因为多,可能造成大量的竞争,消耗cpu的性能而且可能带来死锁和线程安全问题。那么CAS是一个很好的选择。
二.CAS
CAS,Compare And Swap,即比较并交换。
CAS原理:利用计算机处理器都有的CAS指令,进行循环,直到成功为止
CAS工作:CAS会有三个操作数,分别是内存值V(Java中可以理解为地址),旧的预期值A(在计算前保存内存的值),以及新的值B(新的运算出来的值).首先会将就预期值和内存值V进行比较,相同才会更新内存值为新值B,否则会一直进行循环。直到相同后更新为止。但是无论是否更新了V的值,都会返回V的旧值,这个处理过程是一个原子操作,由硬件来保证。
CAS面临的问题:
(1)循环时间太长
如果CAS一直不成功呢?这种情况绝对有可能发生,如果CAS自旋长时间地不成功,则会给CPU带来非常大的开销。
(2)只能保证一个共享变量原子操作
看了CAS的实现就知道这只能针对一个共享变量,如果是多个共享变量就只能使用锁了。
(3)ABA问题
CAS中存在这样一种场景:如果一个变量V初次读取的时候是A值,如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS检查就会误认为它从来没有被改变过,但是实质上它已经发生了改变,这就是CAS操作的"ABA"问题。就像你倒了一杯水放在桌子上后去上了个厕所,同学给你喝了然后又给你接了一杯,这样你会误认为这杯就是你刚才接好的。对于ABA问题其解决方案是加上版本号,即在每个变量都加上一个版本号,每次改变时加1,即A —> B —> A,变成1A —> 2B —> 3A。
更新基本类型AtomicInteger.getAndIncrement()与atomicInteger.incrementAndGet()极易产生理解偏差。一个是先获取值后再进行+1,一个是先+1后再获取。
public class TestAutomicIntege {
//初始化一个10的变量
public static AtomicInteger atomicInteger=new AtomicInteger(10);
public static void main(String[] args) {
System.out.println(atomicInteger.getAndIncrement());
System.out.println(atomicInteger.incrementAndGet());
}
}
输出结果
10
12
更新引用类型中,AtomicMarkableReference、AtomicstampedReference。一个是判断变量是否被更新过返回boolean,一个是判断更新了多少次。
例:
public class TestAutomicReference {
public static void main(String[] args) throws InterruptedException {
//第一个参数是reference(引用,强引用,弱引用等) ,第二个参数是初始化版本号
AtomicStampedReference asr=new AtomicStampedReference("sjw",0);
//先记录下更新前的时间版本戳
int oldStamp=asr.getStamp();
//记录下更新前的引用,以便作为期望引用
Object oldReference= asr.getReference();
//进行一个正确的数据更新(期望值和期望版本号不错误)
Thread rightReference=new Thread(new Runnable() {
@Override
public void run() {
//在更新方法.compareAndSet方法中第一个参数是期望引用对象,第二个是要更新到的值
//第三个是期望版本号,第四个是想要更新到的值
System.out.println("当前版本戳:"+oldStamp+" 当前引用:"+oldReference
+"结果:"+asr.compareAndSet(oldReference,oldReference
+"ringht",oldStamp,oldStamp+1));
}
});
//进行一个线程,让其当前期望值与期望值不一致
Thread errorReference=new Thread(new Runnable() {
@Override
public void run() {
System.out.println("当前版本戳:"+asr.getStamp()+" 当前引用:"+asr.getReference()
+"结果:"+asr.compareAndSet(oldReference,oldReference+"error",
oldStamp,oldStamp+1));
}
});
rightReference.start();
rightReference.join();
errorReference.start();
errorReference.join();
}
}
输出结果:
当前版本戳:0 当前引用:sjw结果:true
当前版本戳:1 当前引用:sjwringht结果:false
AtomicInterArray变更属于在内部封装了个新的数组变更,不会更改最初原始数组的数据
例:
public class TestAtomicArray {
public static void main(String[] args) {
int[] arrys = {1, 2, 6, 9, 12};
//实例化的时候把数组放入
AtomicIntegerArray ata = new AtomicIntegerArray(arrys);
//给索引为0的数组变化为3
System.out.println(ata.getAndSet(0,3));
//输出
System.out.println(ata.get(0));
System.out.println(arrys[0]);
}
}
输出结果:
1
3
1
三、Lock
1.Lock锁和synchronized关键字的区别?
(1).Lock是一个类,而synchronized是一个关键字
(2).synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
(3).synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
(4).用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
(6).Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。如果说没有需要获取锁可以被中断,需要可以超时的获取锁,以及实现尝试获取锁这三种情况,那么建议使用synchornized
2.什么是公平锁和非公平锁?
公平锁:根据请求的时间的顺序,进行获取锁。
非公平锁:允许线程进行不按请求的顺序去获取锁,可以抢先获取锁。
例如:线程A获取锁执行后,因为B比A的请求时间晚了些,所以在A执行中,B的状态变成了阻塞,当A执行完释放锁后,阻塞转变需要时间,在转变的时候线程C发送了请求,这时它是没有阻塞状态的,所以抢先B获取了锁。所以非公平锁比公平锁在效率上更好,因为阻塞转变成可执行状态是需要时间的。
3.什么是可重入锁?
当一个锁允许一个获取锁的线程调用其子过程(递归等),即可代表这个锁是可重入的。
synchronized就是常见的可重入锁,调用其子过程时候,依旧可以进入获取锁,在Lock接口中,其实现类ReentrantLock也是可重入锁,前提是设置了允许可重入,如果没有设置则代表不可重入。
4.ReadWriteLock接口下的实现类ReentrantReadWriteLock是一个读写锁,什么是读写锁?什么时候用?
首先说一下排他锁,ReentrantLock和synchronized都是排他锁,同一时刻只允许一条线程获得锁进行操作,但是读写锁不同,它允许读的线程同时可以有多条获得锁进行访问,但是写锁访问的时候,所有读线程和写线程都会被阻塞。在读多写少的环境下使用,可以有效的提高性能。
在Java中ReadWriteLock的主要实现为ReentrantReadWriteLock,其提供了以下特性:
公平性选择:支持公平与非公平(默认)的锁获取方式,吞吐量非公平优先于公平。
可重入:读线程获取读锁之后可以再次获取读锁,写线程获取写锁之后可以再次获取写锁
可降级:写线程获取写锁之后,其还可以再次获取读锁,然后释放掉写锁,那么此时该线程是读锁状态,也就是降级操作。
读写是如何在一个变量内,确定state呢?一个state整形变量是32个字节,它会分为32位字节,读占前16位,写状态占后16位。
测试读写锁和syn关键字在读写操作中的时间效率
读写锁锁定读写.class
public class Bean {
private int age;
private ReadWriteLock lock=new ReentrantReadWriteLock();
//获取读锁
private final Lock readLock=lock.readLock();
//获取写锁
private final Lock writeLock=lock.writeLock();
//读操作
public int getAge() {
readLock.lock();
try {
return age;
}finally {
//释放锁
readLock.unlock();
}
}
//写操作
public void setAge(int age) {
writeLock.lock();
try {
this.age = age;
}finally {
writeLock.unlock();
}
}
}
syn关键字修饰读写线程.class
public class Bean {
private int age;
//读操作
public synchronized int getAge() {
return age;
}
//写操作
public synchronized void setAge(int age) {
this.age=age;
}
}
运行读线程和写线程.class
public class TestReadAnaWrite {
//读线程
static class Read extends Thread{
public void run(){
try {
sleep(60);
new Bean().getAge();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//写线程
static class Write extends Thread{
public int age;
public Write(int age){
this.age=age;
}
public void run(){
try {
sleep(60);
new Bean().setAge(age);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
long start=System.currentTimeMillis();
//读写比例10比1
for (int i=0;i<200;i++){
//记录开始时间
new Read().start();
for (int j=0;j<10;j++){
new Write(j).start();
}
//记录结束时间
}
long end=System.currentTimeMillis();
System.out.println("总共用时: "+(end-start));
}
}
读写锁用时
syn用时
在数据量越大效果会越明显。
5.Lock书写的范式?
采用 try{ } finally { }的格式。因为这样可以确保锁肯定会被释放
writeLock.lock();
try {
this.age = age;
}finally {
writeLock.unlock();
}
四、配合Lock的condition接口
切记锁都是对同一对象而言的,如果new出一个对象去上锁,另外new出一个对象去开锁,这种开锁是无效的
condition接口中的方法,为Lock提供了线程的辅助功能,相当于wait()和notify()对于synchronized的作用。
(1)await() ,线程等待。
(2)awaitUninterruptibly(),线程等待不可被打断
(3)signal(),相当于notify()的功能。
(4)signalAll(),相当于notifyAll()的功能。
Lock+condition实现等待通知。
例:当改变数值大于100,启动signal,查看被await()的线程变化情况。
public class Express {
//定义一个显示锁
public Lock Kmlock=new ReentrantLock();
public Lock SiteLock=new ReentrantLock();
//定义两个condition接口,一个辅助KM线程,一个辅助Site线程
public Condition Kmcondition=Kmlock.newCondition();
public Condition Sitecondition=SiteLock.newCondition();
public String site="chengdu";
public int Km=100;
//改变KM的方法
public void changeKm() throws InterruptedException {
Kmlock.lock();
try {
this.Km=101;
//唤醒此锁的Kmcondition对应的等待线程
Kmcondition.signal();
}finally {
Kmlock.unlock();
}
}
public void changeSite(String site) throws InterruptedException {
SiteLock.lock();
try {
this.site=site;
//唤醒此锁的sitecondition对应的等待线程
Sitecondition.signal();
}finally {
SiteLock.unlock();
}
}
public void waitSite() throws InterruptedException {
SiteLock.lock();
try {
while (site=="shanghai"||site.equals("shanghai")) {
Sitecondition.await();
System.out.println("地点线程被唤醒,启动...");
}
}finally {
SiteLock.unlock();
}
}
public void waitKm() throws InterruptedException {
Kmlock.lock();
try {
while (this.Km<=100){
Kmcondition.await();
System.out.println("路程线程等待被唤醒,启动...");
}
}finally {
Kmlock.unlock();
}
}
}
启动线程.class
public class Test {
//切记锁都是对同一对象而言的,如果new出一个对象去上锁,另外new出一个对象去开锁,这种开锁是无效的
public static Express express=new Express();
public static void main(String[] args) throws InterruptedException {
//启动三个修改路程命令线程
for (int i=0;i<3;i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
express.waitKm();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
//启动三个修改地点命令线程
for (int i=0;i<3;i++){
new Thread(new Runnable() {
@Override
public void run() {
try {
express.waitSite();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
Thread.sleep(100);
express.changeKm();
}
}
输出结果:
在syn关键字搭配wait()和notfiy()使用的时候,尽量用notifyAll()而不是notify(),但在显示锁中,尽可能使用sinal()而不是signalAll()。因为wait()相对于的是类,很可能这个方法间会互相影响,导致通知信号丢失(没有到达预期位置)。但是在显示锁中sinal会明确指定针对的是哪个锁,甚至一个锁可以new出很多个condication从而保证一个锁的不同的线程也可以精确的指定休眠和唤醒状态。所以没有这种顾虑。
五.LockSupport
LockSupport 工具可以帮助我们阻塞或唤醒一个线程,也是构建同步组件的基础工具。
AQS中 对于节点的阻塞和唤醒就是通过LockSupport的park和unpark实现的。
park():堆线程产生阻塞作用
unpark():对阻塞线程进行唤醒操作
六.AQS(AbstractQueuedSynchronizer)
AQS使用的设计模式是模板方法模式。
模板方法模式:由父类定义一个所谓的框架方法,这个方法会设置方法的运行流程。但是这些方法的具体实现交给子类进行实现。
下面笔者给大家举一个例子来介绍模板方法设计模式:加入一个公司想要实现一个邮件发送的程序,项目经理把抽象的流程抽象类写了出来,里面包含了这些方法的具体流程,但是实现的任务交给了项目组成员。如下sendMessage()就是一个框架方法。
public abstract class AbstractModel {
//首先定义一个写邮件的功能
public abstract void write();
//定义一个内容
public abstract void context();
//定义一个发送功能
public abstract void send();
//定义一个输出当前日期的功能
public void date(){
System.out.println(new Date());
}
//定义一个框架方法
public void sendMessage(){
write();
context();
send();
date();
}
}
首先介绍一下独占锁和共享锁,独占锁也就是悲观锁,在某一时间段只可以被一个线程锁占有,典型的有synchronized 关键字,和ReentrantLock锁,共享锁,简而言之就是可以被多个线程去共享,典型的有ReentrantReadWriteLock的读锁。
独占锁和共享锁,在java源码中是如何实现的呢?独占锁state初始是0,当有线程获取的时候变为1,表示已经被获取,共享锁的state初始并不是0,而是一个可以被共享的量,比如是10,每个线程去获取它就会在初始值减去1,当0的时候,共享锁共享完毕,其它线程就会被阻塞。
AQS中的模板方法
独占式:
acquire()
acquireInterruptibly()
tryAcquireNanos()
共享式:
acquireShared()
acquireSharedInterruptibly()
tryAcquireSharedNanos()
独占式的释放方法:
release()
共享式的释放方法:
releaseShared()
需要实现的方法
独占式:
tryAcquire()
独占式释放:
tryRelease()
共享式:
tryAcquireShared()
共享式释放:
tryReleaseShared()
状态设置的方法(状态可以理解是否获得锁)
状态为1,代表锁被占用,状态为0代表锁没有被占用。
设置状态:
setState()
获取状态:
getState()
保证状态的原子性操作:
compareAndSetState()
7.自己实现一个类似ReentrantLock锁
首先我们先观察一下,ReentrantLock是怎么实现的,它首先继承于Lock,然后回创建一个内部类去继承AQS。
ReentrantLock是独占式锁,如果要实现独占锁,首先要实现独占锁的方法acquire()、tryRelease()、isHeldExclusively() (判断当前锁是否被占用)
实现自己的锁.class
public class MyLock implements Lock {
static class syn extends AbstractQueuedSynchronizer{
//覆写AQS判断是否占用的方法
@Override
protected boolean isHeldExclusively() {
//状态1代表已占用,返回1
return getState()==1;
}
@Override
protected boolean tryAcquire(int arg) {
//运用CAS去改变状态,独占锁获取前必须是不被占用的状态,所以期望值是0
if (compareAndSetState(0,1)){
//传入当前占入的线程
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
//如果已被占用,返回的是false
return false;
}
@Override
protected boolean tryRelease(int arg) {
//说明当前线程没有被占用,也就无法释放
if (getState()==0){
throw new UnsupportedOperationException();
}
//让当前线程占用线程变为Null
setExclusiveOwnerThread(null);
//让当前状态变为0,那为什么设置的时候用CAS,而释放的时候不用呢?
//因为拿锁的时候是众多线程竞争,但是释放的时候,只有拿到锁的才能释放,不存在竞争关系
setState(0);
return true;
}
//实现一个 Condition
public Condition newCondition() {
return new ConditionObject();
}
}
private final syn syn=new syn();
@Override
public void lock() {
//参数1代表独占式锁
syn.tryAcquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
syn.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return syn.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return syn.tryAcquireNanos(1,time);
}
@Override
public void unlock() {
syn.release(1);
}
@Override
public Condition newCondition() {
return syn.newCondition();
}
}
8.AQS的内部结构
首先AQS内部就是一个同步队列,数据结构底层是用双向链表进行储存的,在AQS的源码中,Node(节点)被重点写入。
CANCELLED:线程等待时间超时或者被中断,需要从队列中移除
SIGNAL:后续节点的等待状态,当前节点可以去通知后面的节点去运行
CONDITION :当前节点处于等待队列
PROPAGATE:共享状态,表示当前节点的状态要向后方节点传播
waitstatus:表示等待状态,前面所介绍的变量根据情况,赋值于waitstatus
prev:表示当前节点的前驱节点
next:表示当前节点的后继节点
同步队列的更新操作
当1号线程获取锁的时候,所有的等待线程会组成一个同步队列等待线程1去释放锁,当再有新的线程想要获取锁的时候,就会被加入到等待队列,增加尾节点。
当1号线程释放锁后,头节点2会被唤醒,让其脱离同步队列去获取锁。
增加尾节点才用CAS,因为获取锁是竞争关系,CAS会保证安全。但是设置首节点,因为只有这一个需要去获取,所以不需要CAS。
AQS流程
获取同步状态就相当于获取锁
前置节点在被获取锁前,也会同时去唤醒后面的节点,让其准备进入头节点位置。
9.Condition与AQS的联系
Condition本质上也是去维护一个队列,这个队列配合AQS队列去实现线程的等待和唤醒,但Condition维护的等待队列与AQS同步队列不同的是,它是单链表。
await方法只有获取锁后才可以调用,说明头节点如果调用了await方法就会被放置Condition的等待队列中,当期使用signal方法后,又会将其从等待队列拿出,放入同步队列的末尾。
为什么在syn关键字中,最好用notifyAll()而不是notify(),在condition中要用signal()而不是signalAll()?
因为在syn中,它也是存在等待队列的,但是只有一个等待队列,你无法保证你唤醒的线程就是你想要的线程(可能是实现其它功能的线程),而在condition中有多个队列,你可以指定哪个条件(也就是功能)唤醒,这样就无需用singnalAll(),用singnal()性能更好。
10.非公平锁和公平锁是如何设计的?
在公平锁中,会有一个hasQeuedPredeceessors()方法去判断同步队列前方是否还有节点,如果有,就会等前方节点去获取。
而非公平锁不会判断前方是否还有节点,只要判断当前锁是否被释放,释放后,直接去CAS获取锁,然后其它线程就会被阻塞。