02 加锁方法.txt
UP 返回
1 Synchronized原理与使用 (重量级锁。其他线程必须在外等待)
1.1 代码写法
内置锁
互斥锁
修饰普通方法 内置锁就是当前类的实例
修饰静态方法 内置锁是当前class字节码对象
修饰代码块 内置锁可以是任意对象
1.2 jvm层面的原理
使用monitorenter monitorexit指令来获取锁 释放锁 (这是字节码文件中的内容)
有的同步方法块中并没有这些指令,那是使用了别的同步机制,这里暂不表
任何对象都可以作为锁,关于锁的信息存在对象头中。对象头含有这些信息:
Mark Word 存储对象的hash值,锁等信息
Class Metadata Address 指向对象类型的地址
Array Length 如果是数组的话,数组对象要比普通对象多一个长度信息
jdk1.6以后引入了其他几种锁:
偏向锁 每次获取锁和释放锁会浪费资源。很多情况下,竞争锁不是由多个线程,而是由一个线程在使用。在只有一个线程访问同步代码块的场景下效率会更高
偏向锁的Mark Word中会含有这些字段:
线程id
Epoch
对象的分代年龄信息
是否是偏向锁
锁标志位
线程第一次进来时,会查看锁标志,如果是偏向锁,再查看线程id。如果线程相同,则不再获取锁,直接进入,否则才会获取锁。线程第二次进来时其实是不用获取也不释放锁的;只有其他线程过来尝试获取锁时才会释放锁(等到竞争出现才释放的机制)
轻量级锁 (不是很理解未做笔记,参视频15)
重量级锁
2 单例模式与线程安全性问题
饿汉式 没有线程安全性问题
public class Singleton {
private Singleton() {}
private static Singleton singleton = new Singleton();
public static Singleton getInstance() {
return singleton; //原子操作,多线程不会有问题
}
}
懒汉式 双重检查加锁解决线程安全性问题 (饿汉式不论有没有使用都会创建对象,会更多的消耗资源)
public class Singleton2 {
private Singleton2() {}
private static Singleton2 singleton;
public static Singleton2 getInstance()
if (singleton == null) { //非原子操作
return new Singleton2();
}
return singleton;
}
}
如果改成:
public static synchronized Singleton2 getInstance() { //在这里加同步会浪费很大的性能,不论是改为偏向锁还是轻量级锁都没多大意义(轻量级锁会在进入同步块后检查是否有线程正在执行,有的话会发生自旋浪费性能。参视频16)
if (singleton == null) {
return new Singleton2();
}
return singleton;
}
需要改成双重检查加锁:
public static Singleton2 getInstance() {
if (singleton == null) {
synchronized (Singleton2.class) {
if (singleton == null) {
singleton = new Singleton2(); //在这个地方,可能会发生指令重排序的问题,导致达不到预期的效果。
//事实上这句指令发生的行为为:1.申请一块内存空间 2.在这块空间中实例化对象 3.引用指向该空间
//虚拟机可能会对这三条指令重排序,导致单例的实现出现问题
//所以这个地方还需要将变量变为volatile : private static volatile Singleton2 singleton; 这个关键字加上以后就不会发生指令重排序等优化行为
}
}
}
return singleton;
}
3 关于锁的相关概念 (参视频17)
3.1 锁重入
需要同一个锁对象的方法之间的相互调用。调用方拿到锁以后,被调用方可以直接进入。避免了死锁问题
3.2 自旋锁
相当于线程拿到锁以后一直等待,类似于while(true),直到被唤醒(轻量级锁中)
3.3 死锁
4 volatile (参视频18)
Volatile称之为轻量级锁,被volatile修饰的变量,在线程之间是可见的。
可见:一个线程修改了这个变量的值,在另外一个线程中能够读到这个修改后的值。
Synchronized除了线程之间互斥意外,还有一个非常大的作用,就是保证可见性
添加了volatile修饰的变量,编译汇编以后会多一条lock指令。lock指令的效果是:在多处理器的系统上,将当前处理器缓存行(CPU缓存的最小单位)的内容写回到系统内存;同时这个写回到内存的操作会使在其他CPU里缓存了该内存地址的数据失效
volatile只能保证可见性,但是并不能保证原子操作;synchronized可以保证原子操作。所以如果同步的代码本身就是原子性的,就可以用volatile,更轻量。大量的使用volatile会使缓存无效,以及优化无效,所以不能多用
5 JDK提供的原子类原理及使用 (参视频19)
在util.concurrent.atomic包下提供了多个原子类:
原子更新基本类型 AtomicInteger
原子更新数组 AtomicIntegerArray
原子更新抽象类型 AtomicReference<V>
原子更新字段 AtomicIntegerFieldUpdater<User>
代码演示:
public class AtomicTest {
private AtomicInteger value = new AtomicInteger(0);// 原子基础类型
private int[] s = { 1, 5, 6, 8 };
AtomicIntegerArray array = new AtomicIntegerArray(s);// 原子数组
AtomicReference<User> user = new AtomicReference<>();// 原子引用类型
AtomicIntegerFieldUpdater<User> old = AtomicIntegerFieldUpdater.newUpdater(User.class, "old");// 原子更新字段
public int getNext() {
array.getAndIncrement(2);// 给数组某个位置值自增
array.getAndAdd(2, 10);// 给数组某个位置值加上10
User user = new User();
old.getAndIncrement(user);
return value.getAndIncrement();// 相当于自加操作
}
public static void main(String[] args) {
AtomicTest atomicTest = new AtomicTest();
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
while (true) {
System.out.println(atomicTest.getNext());
try {
Thread.sleep(500);
} catch (Exception e) {
// TODO: handle exception
}
}
}
}).start();
}
}
User类
public class User {
private String name;
public volatile int old; //设置成public volatile才能执行AtomicIntegerFieldUpdater.newUpdater(User.class, "old")
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getOld() {
return old;
}
public void setOld(int old) {
this.old = old;
}
}
实现原理:首先获得当前值prev和预期的下一个值next,再根据cas原子性操作判断是否只有本线程对其做修改,如果是则返回修改过的值,如果不是则循环执行判断
public final int getAndUpdate(IntUnaryOperator updateFunction) {
int prev, next;
do {
prev = get();
next = updateFunction.applyAsInt(prev);
} while (!compareAndSet(prev, next)); //该操作将next赋给prev的同时会判断是否仅该线程对变量做了修改
return prev;
}
6 Lock接口 (参视频20)
6.1 使用代码
开始会出现并发问题的代码:
public class LockTest {
private int value;
public int getNext() {
return value++;
}
public static void main(String[] args) {
LockTest lockTest = new LockTest();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + " " + lockTest.getNext());
try {
Thread.sleep(100);
} catch (Exception e) {
// TODO: handle exception
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + " " + lockTest.getNext());
try {
Thread.sleep(100);
} catch (Exception e) {
// TODO: handle exception
}
}
}
}).start();
}
}
修改LockTest的代码:
private int value;
Lock lock = new ReentrantLock();
public int getNext() {
lock.lock(); //加锁
int a = value++;
lock.unlock(); //释放锁
return a;
}
6.2 Lock需要显示地获取和释放锁,繁琐但能让代码更灵活。(比如再添加多个Lock对象,随时加锁释放锁)
Synchronized不需要显示地获取和释放锁,简单
使用Lock可以方便的实现公平性(Lock本身也是使用了Synchronized)
使用Lock的好处:
非阻塞的获取锁
能被中断的获取锁
超时获取锁
6.3 自己实现可重入锁: (参视频21)
测试类代码:
public class Sequence {
private MyLock myLock = new MyLock();
private int value;
public int getNext() {
myLock.lock();
value++;
myLock.unlock();
return value;
}
public static void main(String[] args) {
Sequence sequence = new Sequence();
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
while (true) {
System.out.println(sequence.getNext());
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
while (true) {
System.out.println(sequence.getNext());
}
}
}).start();
}
}
锁代码:
public class MyLock implements Lock { //★此处省略其他无关的需要实现的方法
private boolean isLocked = false;
@Override
public synchronized void lock() {
// 自旋
while (isLocked) {
try {
wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
isLocked = true;
}
@Override
public synchronized void unlock() {
notify();
isLocked = false;
}
}
上述代码测试可以实现多线程获得的value可以不重复,但是无法解决可重入的问题,当用涉及到锁重入的测试类来进行时,会卡在a的打印上,因为方法b无法获得锁:
public class Demo {
private MyLock lock = new MyLock();
public void a() {
lock.lock();
System.out.println("a"); //卡住
b(); //无法获得锁
lock.unlock();
}
public void b() {
lock.lock();
System.out.println("b");
lock.unlock();
}
public static void main(String[] args) {
Demo demo = new Demo();
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
demo.a();
}
}).start();
}
}
这个时候需要对锁进行修改,记录加锁的线程并维护加锁的数量,同时释放锁时也需要比较,这样才能实现可重入:
public class MyLock implements Lock {
private boolean isLocked = false;
Thread lockBy = null; //★记录当前加锁的线程
int lockCount = 0; //★记录加锁数量
@Override
public synchronized void lock() {
Thread currenThread = Thread.currentThread();
// 自旋
while (isLocked && currenThread != lockBy) { //★如果当前锁是被占有的,同时占有的线程不是当前线程,则自旋等待
try {
wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
isLocked = true; //否则加锁,并记录加锁线程,加锁数量自增
lockBy = currenThread;
lockCount++;
}
@Override
public synchronized void unlock() {
if (lockBy == Thread.currentThread()) { //解锁时,必须当前线程是加锁线程才有响应。如果条件成立,加锁数量自减
lockCount--;
if (lockCount == 0) { //当加锁数量为0时锁释放,并通知其他线程
notify();
isLocked = false;
}
}
}
}
7 使用AQS重写自定义锁 (参视频23)
7.1 测试类不变,锁代码更换:
public class MyLock2 implements Lock { // 自己定义的锁
private Helper helper = new Helper();
// 根据帮助文档,应将具体的状态设置定义为锁的一个内部帮助类并继承AbstractQueuedSynchronizer
// 同时重写tryAcquire和tryRelease
private class Helper extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
// 第一个线程进来可以拿到锁,返回true
// 第二个线程拿不到锁,返回false。但是如果是线程重入,则应该拿到锁,并更新状态值
int state = getState();
Thread t = Thread.currentThread();
if (state == 0) {
if (compareAndSetState(0, arg)) {// 保证原子操作
setExclusiveOwnerThread(t);
return true;
}
} else if (getExclusiveOwnerThread() == t) {// 重入
setState(state + 1);
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
// 锁的获取和释放肯定是一一对应的,那么调用此方法的线程一定是当前线程
if (Thread.currentThread() != getExclusiveOwnerThread()) {
throw new RuntimeException();
}
int state = getState() - arg;
boolean flag = false;
if (getState() == 0) {
setExclusiveOwnerThread(null);
flag = true;
}
setState(state);
return flag;
}
// 该方法是为了使用AbstractQueuedSynchronizer的内部类ConditionObject,方便锁newCondition接口的重写,暂时不管
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();
}
}
8 LongAdder 1.8出现(DoubleAdder同理)。详细代码解释参看视频56
AtomicLong在多线程的表现性能很差,所以出现了LongAdder
当多个线程争夺一个AtomicLong时自然会出现很多cas自旋降低性能,但是LongAdder使用一个base来保存基础值,当只有单线程访问时直接将这个值使用即可;当有多个线程访问,LongAdder会将base值拆分为多个小一点的值存放于一个cell数组中
于是每一个线程就会抢占其中的每一个cell,这样就允许多线程同时处理;而这个cell数组的初始容量为2,一旦有线程抢占不到cell元素时,就会以*2的方式扩容
获取具体的值的时候,是直接将整个cell数组的值求和返回,就保证了一致性。(比如加锁的值是5,执行+1操作时,将其拆分成2+3两个元素,对任意一个+1并不会影响最后得到的值)
LongAdder中具体的cell细分方式来源于继承的Striped64,具体代码在这个类里可以看到
cell数组存在一个伪共享的概念,视频并未具体说明
9 StampedLock 参视频57
9.1 出现原因
传统的读写锁,读和写是互斥的,有可能读线程非常多导致写线程一直抢占失败而饥饿(虽然公平锁可以防止饥饿,但是相对于非公平锁性能会更差)。StampedLock就是为了提高读写互斥导致的性能差,即StampedLock的读不会阻塞写,如果有写操作直接重新读。
StampedLock在获取锁的时候可以拿到票据,根据票据是否变化即可知道读时是否发生了
9.2 示例代码:
public class Demo {
private int balance;
private StampedLock lock = new StampedLock();
// 这种读锁和之前的没有区别
public void read() {
long stamp = lock.readLock();
int c = balance;
// 读出来后一系列操作...
lock.unlockRead(stamp);
}
// 获取乐观锁
public void optimisticRead() {
long stamp = lock.tryOptimisticRead();
int c = balance;
// 这里可能会出现了写操作,因此要进行判断
if (!lock.validate(stamp)) {
// 要重新读
long readStamp = lock.readLock();
c = balance;
// 更新票据,用于后面释放锁
stamp = readStamp;
}
lock.unlockRead(stamp);
}
// 适用于读出一个值后进行判断,判断成功再写入
public void conditionReadWrite(int value) {
long stamp = lock.readLock();
while (balance > 0) {// 假设用于更新的判断条件
// 尝试转化为写锁
long writeStamp = lock.tryConvertToWriteLock(stamp);
if (writeStamp != 0) {// 转化成功,直接更新数据,完成操作
stamp = writeStamp;
balance += value;
break;
} else {// 没有转化成功,则直接释放读锁,然后获取写锁
lock.unlockRead(stamp);
stamp = lock.writeLock();
}
}
lock.unlock(stamp);
}
public void write(int value) {
long stamp = lock.writeLock();
balance += value;
lock.unlockWrite(stamp);
}
}
9.3 为什么两种锁同时存在
读写锁比StampedLock更加纯粹,获取锁释放锁;为了兼容以前广泛使用的读写锁
DOWN 返回