标签:感知 多资源 edr 轻量级 long 访问 double 获取值 false
自旋其实就是当一个线程获取到锁之后,其他的线程会进行阻塞等待,一直到这个线程释放锁后才能进入
锁重入即在一个对象中对两个方法都加锁了,那么在一个线程获取到其中一个方法的锁后,再执行另外一个方法时就不再需要获取锁了;同时如果一个线程获取到了其中一个方法的锁,那么其他的线程既不能执行这个方法,也不能执行另一个方法。
当一个线程永远的持有这把锁而其他线程都尝试获取这把锁的时候就形成了死锁
死锁问题最简单的演示:
public class DeadLock {
    private Object obj1 = new Object();
    private Object obj2 = new Object();
    
    public void a() throws Exception {
        synchronized(obj1) {
            Thread.sleep(1000);
            synchronized(obj2) {
                System.out.println("a");
            }
        }
    }
    
    public void b() throws Exception {
        synchronized(obj2) {
            Thread.sleep(1000);
            synchronized(obj1) {
                System.out.println("b");
            }
        }
    }
}
/**
 * 上面a跟b方法形成了死锁
 * 当a执行后会获取obj1的锁,b执行后会获取obj2的锁
 * 这时a想要再获取obj2的锁已经不可能了 而b想要获取obj1的锁也不可能了
 * 因此会一直阻塞,谁也不能执行
 */synchronized力度是作用于对象上的,有三种用法
java的synchronized关键字会被翻译成字节码指令monitorenter跟monitorexit,指令之间就是synchronized同步代码块之间执行的代码。
既然任何对象都可以作为锁,那么锁信息又存在对象什么地方呢?答案是存在对象头中。主要是存在于对象头中的Mark Word中的,markword中存储锁的机制如下图所示:

由上面的图可知,jvm内置锁又分为很多个,其实在不断地优化中,jvm的内置锁已经不再是当初那个笨重的锁了,它会根据不同的情况来自动升级,大致的过程是:无锁 ---> 偏向锁 --> 轻量级锁 ---> 重量级锁
在很多情况下,竞争锁的不是由多个线程,而是由一个线程在使用,这时候如果还是像多线程那样去获取锁再释放锁,会浪费很多资源。因此偏向锁非常适用于同一个线程反复进入同一同步块的场景
开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
关闭偏向锁:-XX:-UseBiasedLocking
无锁状态
对象一开始是没有加锁的,当一个线程访问同步块的时候会检查标志位,如果是01表示要么是偏向锁要么是无锁,如果不是偏向锁,那么会CAS修改对象头MarkWord中的偏向位为1,变成偏向锁,然后会将对象头的Mark Word中前23位放入当前线程的ID,最后执行同步块当中的代码,等到达安全点后会暂停线程,这里的安全点是指cpu分配给当前线程的时间片用完,这个时候会进行判断,看是否已经执行完了同步块中的代码,如果执行完了就没有必要加锁了,那么偏向锁会被释放,Mark Word中的线程ID会被清除,偏向会置为0;如果没有执行完则会进一步将锁升级为轻量级锁。

有锁状态
当一个线程访问同步块,检查偏向为1,表示是偏向锁,这时候它会使用CAS修改对象头前23位为自己的ID,但是由于线程ID已经有了,所以一定会修改失败,这时候它会等待占用锁的线程到达安全点后撤销偏向锁,将锁升级为轻量级锁。下图红色部分就是新的线程进入时的流程。

适用于线程交替执行的场景。
再轻量级锁的状态下,每个线程都会将markword的信息复制到自己线程栈的栈帧中,然后尝试修改markword中的锁标志位为轻量级锁,并且对象的markword中会留下获取到锁的线程的信息,这个信息是一个指针,指向刚刚这个线程复制的markword信息。随后的线程会尝试修改markword中的内容,但是由于第一个线程正在占用,所以是修改不了的,只能不断的等待,不断的重新尝试修改,最终等到第一个线程释放锁后才能修改成功。这个过程叫做自旋。在第二个线程获取到锁后就会将锁升级到重量级锁。
重量级锁的性能非常低,适合高并发场景。因为高并发的场景最终的结果一定是会升级到重量级锁,所以不如一开始就使用重量级锁,以免锁升级的过程中造成过多的资源浪费。
要理解volatile的作用,需要先了解java的内存模型jmm

在上面的图中可以看到每个线程都会从主内存备份一份数据到自己的工作内存中,但是现在有一个问题,假设某个变量在线程B中被修改了,而由于线程B修改后的数据只会同步到主内存中,而不会影响到线程A中工作内存的数据,这就使得数据同步出现了问题,具体看下面代码:
public class Volicity {
    private static boolean flag = false;
    public static void main(String[] args) {
        // 线程A等待线程B的数据
        new Thread(() -> {
            System.out.println("等待准备数据。。。");
            while(!flag) {
            }
            System.out.println("启动系统。。。");
        }).start();
        new Thread(() -> {
            System.out.println("准备数据。。。");
            flag = true;
            System.out.println("数据准备完成。。。");
        }).start();
    }
}在上面的程序中线程A会等待线程B修改数据,修改完成后会继续往下执行,但是结果是即便线程B修改了数据,线程A仍然会停在循环处不会执行,这就是因为线程B修改后的数据不会影响到线程A的工作内存中的数据。
上面的问题的解决办法就是在变量前面添加volatile关键字
private static volatile boolean flag = false;在说明这个问题之前需要先了解JMM将数据从主内存读到工作内存以及再同步回来的原理
JMM主要是通过如下原子操作实现的:
JMM就是通过如上的一些方法来实现主内存与各个线程之间的工作内存进行数据同步的
下面可以将上面的程序执行流程捋一遍:
具体流程图如下:

现在问题出现了:当线程B将数据推送到主内存后,线程A并不知道,它仍旧使用的是自己工作内存中没有更新的数据,所以会出问题
由此可知其实volatile关键字的作用就是当主内存中的数据改变后及时的将主内存的数据同步到线程A的工作内存中,那么它是如何做到的呢?
为了解决上面的问题,在不同的时期使用了不同的方法,早期的时候使用的是总线加锁,但是由于性能太低,后来使用了MESI缓存一致性协议
volatile缓存可见性实现原理
底层实现主要是通过汇编lock前缀指令,它会锁定这块内存区域的缓存(缓存行锁定)并会写到主内存
下面是对lock指令的解释:
其实除了能够使用volatile解决可见性问题外,还能够使用synchronized解决可见性问题,只需要将程序修改为如下即可:
public class Demo01 {
    private static boolean flag = false;
    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println("等待准备数据。。。");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (Demo01.class) {
                while (!flag) {
                }
                System.out.println("启动系统。。。");
            }
        }).start();
        new Thread(() -> {
            System.out.println("准备数据。。。");
            synchronized (Demo01.class) {
                flag = true;
            }
            System.out.println("数据准备完成。。。");
        }).start();
    }
}上面在对flag变量进行读写时都加了锁,其实道理很简单,这也是synchronized的特性导致的:
其实对比volatile的做法(利用cpu的嗅探机制嗅探主内存的值改变,进而使工作内存中的变量失效,从而重新去主内存中获取值),这种方式只是人为的在读取变量前强制程序去主内存中读取变量。
上面说到了volatile的一个作用,保证可见性,其实除了可见性,volatile还能够保证程序的有序性,当我们写下的程序交给jvm去执行的时候,jvm并非是按照我们写下的顺序去执行的,而是会先进行一些指令重排,在保证程序正确执行的情况下做到尽可能的优化,例如下面这段例子:
public void test() {
    int a;
    int b;
    int c;
    a = 1;
}上面的代码原本的执行是这样的:首先jvm会在当前线程栈中开辟一块内存作为test方法的栈帧,然后将在栈帧中的局部变量表中为a变量开辟一块内存,然后为b变量开辟一片内存,然后为c变量开辟一片内存;最后将常量1压入栈帧中的操作数栈中,然后从操作数栈中将1弹出,并且存放到局部变量表中a变量所在的区域。
经过指令重排后:其实从上面的过程中我们就可以看出一个问题,在jvm为a变量开辟出内存后,为什么不直接执行a=1的操作呢?这样就能避免后面再去寻找a变量的地址时形成的开销,因此jvm会对指令重排,重排后的代码如下
public void test() {
    int a;
    a = 1;
    int b;
    int c;
}这样做的本意是好的,但是有一个问题,在有些情况下做指令重排会导致一些问题,最著名的就是单例模式中利用双重检测锁创建单例时出现的问题,如下:
public class Singleton {
    private Singleton singleton;
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized(singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
             }
        }
        return singleton;
    }
}在上面的代码中,看似很完美的实现了单例模式,但是由于jvm会进行指令重排,所以最终的结果或许并不如期待的那样,但是如果稍作修改,在singleton变量前面加上volatile关键字,就可以很完美的解决这个问题。
由此可见,volatile保证程序有序性的原因就是能够阻止jvm对指令的重排序
在对数据使用volatile后,虽然能够保证数据在各个线程之间的可见性,但是并不能保证原子性,想要保证数据的原子性,需要使用juc包下面的原子类
原子类大致分为四类:
基本类型有AtomicInteger、AtomicBoolean、AtomicLong这几个,基本的使用方法如下:
private AtomicInteger value = new AtomicInteger(0);
value.getAndIncrement();   // 获取值再自加
value.incrementAndGet();   // 自加再获取值
value.getAndAdd(10);   // 获取值再加10jdk1.8之后又推出了一个LongAdder,首先,这个类实现的功能其实是跟AtomicLong一样的,那么为什么有了AtomicLong了还要有LongAddr呢?原因就是AtomicLong性能不是特别好,同一时间只能允许一个线程修改。
那么LongAddr是怎样提升效率的呢?我们可以看到原先的AtomicLong是所有线程去修改一个数,这样自然同一时间只能允许一个线程修改,但是LongAddr是将这个数拆分为了几个数,单个的数还是只能同时允许一个线程修改。譬如6拆分成1 2 3,那么现在有三个线程,它们就可以同时去修改,线程1修改数字1,线程2修改数字2,线程3修改数字3,最后改变的结果是2 3 4,如果用户去获取结果就把这几个部分的数字加起来,也就是9。但是如果再来一个线程,就继续拆分,因此不会存在自旋现象。
DoubleAddr跟LongAddr解决的问题是相同的
有例如AtomicIntegerArray等类,基本操作如下:
private AtomicIntegerArray value = new AtomicIntegerArray(new int[] {1, 2, 5});
value.getAndIncrement(2);   // 获取数组第三个元素再加一
value.getAndAdd(2, 10);   // 获取数组第三个元素再加10使用AtomicReference类更新引用类型
private AtomicReference<User> user = new AtomicReference<>();
// 这时更新的时整个user对象对于字段的要求有如下三点:
// 后面两个参数是要进行原子操作的类以及要修改类中的哪一个字段
AtomicIntegerFieldUpdater<User> old = AtomicIntegerFieldUpdater.newUpdater(User.class, "age");
User user = new User("nick", 12);
old.getAndIncrement(user);
System.out.println(user.getAge());  // 此时age字段变为13前面已经了解了解决线程安全问题的三个方式,分别是使用synchronized、Volatile以及使用原子类,使用synchronized是可以解决所有线程安全性问题的,但是由于比较笨重,使用了volatile替代,但是volatile只能解决可见性跟有序性问题,不能解决原子性问题,于是出现了原子类,但是原子类只能保证单个数据修改的原子性,当要进行一系列的操作的时候仍旧不能够保证原子性,于是就出现了Lock接口。
首先注意下面代码的问题:
public class Sequence {
    private int value;
    public int getNext() {
        return value++;    // 线程不安全的
    }
    public static void main(String[] args) {
        Sequence s = new Sequence();
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                while (true) {
                    System.out.println(Thread.currentThread().getName() + "==" + s.getNext());
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
}上面的代码执行value++的操作是线程不安全的,想要解决只需要再value++前后上锁即可
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Sequence {
    private int value;
    private Lock lock = new ReentrantLock();
    public int getNext() {
        lock.lock();  // 上锁
        value++;
        lock.unlock();  // 释放锁
        return value;
    }
    public static void main(String[] args) {
        Sequence s = new Sequence();
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                while (true) {
                    System.out.println(Thread.currentThread().getName() + "==" + s.getNext());
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
}使用lock.lock()以及lock.unlock()方法即可对操作进行上锁,需要注意的是这时的锁对象lock必须是同一个,也就是多个线程使用同一把锁,否则是没有用的
Lock的好处
AQS即AbstractQueuedSynchronizer,是实现各种阻塞锁以及各种同步容器的基础。
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedLongSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
public class MyLock2 implements Lock {
    private Helper helper = new Helper();
    private class Helper extends AbstractQueuedLongSynchronizer {
        @Override
        protected boolean tryAcquire(long arg) {
            // 如果是第一个线程进来 可以拿到锁 返回true
            // 第二个线程进来拿不到锁 返回false
            int state = (int) getState();
            if (state == 0) {
                if (compareAndSetState(0, arg)) {
                    setExclusiveOwnerThread(Thread.currentThread());
                    return true;
                }
            } else if (getExclusiveOwnerThread() == Thread.currentThread()) {
                setState(state + arg);
                return true;
            }
            return false;
        }
        @Override
        protected boolean tryRelease(long arg) {
            if (Thread.currentThread() != getExclusiveOwnerThread()) {
                throw new RuntimeException("所被其他线程占用");
            }
            int state = (int) (getState() - arg);
            setState(state);
            if (state == 0) {
                setExclusiveOwnerThread(null);
                return true;
            }
            return true;
        }
        public Condition newCondition() {
            return new ConditionObject();
        }
    }
    @Override
    public void lock() {
        helper.acquire(1);
    }
    @Override
    public void lockInterruptibly() throws InterruptedException {
        helper.acquireInterruptibly(1);
    }
    @Override
    public boolean tryLock() {
        return helper.tryAcquire(1);
    }
    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return helper.tryAcquireNanos(1, unit.toNanos(time));
    }
    @Override
    public void unlock() {
        helper.release(1);
    }
    @Override
    public Condition newCondition() {
        return helper.newCondition();
    }
}前面所了解的锁都是排他锁,也就是同一个时间里面只能允许一个线程进行访问,但是在有些时候并不需要如此,例如在读操作的时候可以同时多个线程访问,这时候的锁可以设置为共享锁。
对于读写锁有:读跟读是互斥的、读跟写是互斥的、读跟读是不互斥的
下面简单实现读写锁的用法:
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockDemo {
    private Map<String, Object> map = new HashMap<>();
    private ReadWriteLock lock = new ReentrantReadWriteLock();
    private Lock readLock = lock.readLock();
    private Lock writeLock = lock.writeLock();
    public Object get(String key) {
        readLock.lock();
        System.out.println(Thread.currentThread() + "读操作开始..");
        Object o = map.get(key);
        readLock.unlock();
        System.out.println(Thread.currentThread() + "读操作结束..");
        return o;
    }
    public void put(String key, Object value) {
        writeLock.lock();
        System.out.println(Thread.currentThread() + "写操作开始..");
        map.put(key, value);
        writeLock.unlock();
        System.out.println(Thread.currentThread() + "写操作结束..");
    }
}锁降级是指写锁降级为读锁,原理就是在写锁还没释放的时候将锁设置为读锁,以致让别的写线程没办法竞争到写锁
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockDemo1 {
    private Map<String, Object> map = new HashMap<>();
    private ReadWriteLock lock = new ReentrantReadWriteLock();
    private Lock readLock = lock.readLock();
    private Lock writeLock = lock.writeLock();
    private volatile boolean isUpdate;
    public void readWrite() {
        if (isUpdate) {
            writeLock.lock();  // 在此处添加写锁
            map.put("xxx", "xxx");
            readLock.lock();  // 在这里进行锁降级
            /*
             * 在这里释放写锁后其他的写线程会来竞争写锁继续写数据,为了不让其他的线程来写数据
             * 应该在写锁释放前将锁降级为读锁 这样其他的写线程就没办法竞争到写锁了
             * 因为读锁与写锁互斥
             */
            writeLock.unlock();
        }
        System.out.println(map.get("xxx"));
        readLock.unlock();
    }
}这是jdk1.8出现的一个锁,是对ReentrantReadWriteLock进行的一个增强,之所以出现这个类是因为读写锁经常会遇到一个问题,再高并发的环境下,读的线程远远大于写的线程,由于读写互斥,可能导致写线程饥饿问题,如果使用读写锁的公平模式又会导致性能问题,因此急需要一个类对读写锁进行增强。
在StampedLock中读锁是不会阻塞写锁的,那么如何保证读写一致性呢?解决的方法很简单,就是在读的过程中如果发现了写操作就重新读。
在StamptedLock中分乐观锁跟悲观锁,悲观锁跟读写锁没什么区别,都是读写互斥。只有乐观锁是读写不互斥的。
悲观锁演示:
import java.util.concurrent.locks.StampedLock;
public class StamptedLockDemo {
    private StampedLock stampedLock = new StampedLock();
    private int balance;
    public void read() {
        long stampted = stampedLock.readLock();
        int c = balance;
        System.out.println(c);
        stampedLock.unlockRead(stampted);
    }
    public void write(int value) {
        long stampted = stampedLock.writeLock();
        balance += value;
        stampedLock.unlockWrite(stampted);
    }
}乐观锁演示:
乐观锁主要是在读锁上进行更改,只需要在读取后进行一次判断,如果判断结果是写锁修改了数据,就重新读一次
import java.util.concurrent.locks.StampedLock;
public class StamptedLockDemo {
    private StampedLock stampedLock = new StampedLock();
    private int balance;
    // 读锁示例
    public void read() {
        long stampted = stampedLock.tryOptimisticRead();
        int c = balance;
        // 这里可能会出现写操作,因此要进行判断
        if (!stampedLock.validate(stampted)) {
            try {
                // 发生了写操作 重新读取
                stampted = stampedLock.readLock();
                c = balance;
            } finally {
                // 释放锁
                stampedLock.unlockRead(stampted);
            }
        }
        System.out.println(c);
    }
    
    
    
    /**
     * 读写锁转换
     * @param value
     */
    public void conditionReadWrite(int value) {
        long stampted = stampedLock.readLock();   // 拿到悲观的读锁,方便下面判断数据
        while (balance > 0) {
            // 将读锁转换为写锁修改数据
            stampted = stampedLock.tryConvertToWriteLock(stampted);
            if (stampted != 0) {   // 成功转换为写锁
                // 进行修改操作
                balance += value;
                break;
            } else {  // 没有转换成功
                // 需要先释放读锁,然后再拿到写锁
                stampedLock.unlockRead(stampted);
                // 获取写锁
                stampted = stampedLock.writeLock();
            }
        }
        stampedLock.unlock(stampted);   // 释放任何的锁
    }
}出现线程安全性问题的条件
解决线程安全性问题的途径
认识的锁
标签:感知 多资源 edr 轻量级 long 访问 double 获取值 false
原文地址:https://www.cnblogs.com/Myarticles/p/12046011.html