概念、理论

并发:多个线程操作相同的资源,优点:效率高、资源利用率高,缺点:线程可能不安全、数据可能不一致,需要使用一些方式保证线程安全、数据一致

高并发:服务器能同时处理大量请求

线程安全:当多个线程访问某个类,不管采用何种调度方式、线程如何交替执行,这个类都能表现出正确的行为。

 

造成线程不安全的原因

  • 存在共享资源
  • 多个线程同时操作同一共享资源,操做不具有原子性

 

如何实现线程安全?

  • 使多线程不同时操作同一共享资源:eg. 只使用单线程、必要的部分加锁、使用juc的并发容器、并发工具类
  • 使对共享资源的操作具有原子性:eg.使用原子类
  • 不共享资源:eg. 使用ThreadLocal
  • 用final修饰共享资源,使之只读、不可修改

只要实现以上任意一点,即可实现线程安全

 

互斥锁的特性

  • 互斥性:同一时刻只能有1个线程对这部分数据进行操作,互斥性也常叫做操作的原子性
  • 可见性:如果多个线程同时操作相同的数据(读、写),对数据做的修改能及时被其它线程观测到。可见性用happens-before原则保证

 

锁的实现原理

在这里插入图片描述
获取锁:把主内存中对应的共享资源读取到本地内存中,将主内存中的该部分共享资源置为无效
释放锁:把本地内存中的资源刷到主内存中,作为共享资源,把本地内存中的该部分资源置为无效

 

juc包简介

juc包提供了大量的支持并发的类,包括

  • 线程池executor
  • 锁locks,locks包及juc下一些常用类CountDownLatch、Semaphore基于AQS实现。jdk将同步的通用操作封装在抽象类AbstractQueuedSynchronizer中,acquire()获取资源的独占权(获取锁),release()释放资源的独占权(释放锁)
  • 原子类atomic,atomic包基于CAS实现,实现了多线程下无锁操作
  • 并发容器(集合)collections
  • 并发工具类tools

 

实现线程安全的常用方式

synchronized

synchronized的用法
// 修饰普通方法
public synchronized void a(){

}

// 修饰静态方法
public static synchronized void b(){

}


public static Object lock = new Object();

public void c(){
    // 修饰代码块。同步代码块,锁住一个对象
    synchronized (lock){
        
    }

}

synchronized可以修饰方法、代码块,修饰的操作是原子性的,同一时刻只能有1个线程访问、执行

  • 修饰普通方法,加的是对象锁,执行该方法时会自动锁住该方法所属的对象
  • 修饰静态方法,加的是类锁,执行该方法时会锁住所在类的class对象,即锁住该类所有实例
  • 修饰代码块,加的是对象锁,会锁住指定对象

如果要修饰方法,尽量用普通方法,因为静态方法因为会锁住类所有的实例,严重影响效率。

 

synchronized的实现原理

synchronized使用对象作为锁,对象在内存的布局分为3部分:对象头、实例数据、对齐填充,对象头占64位

  • 前32位是Mark Word,存储对象的hashCode、gc分代年龄、锁类型、锁标志位等信息
  • 后32位是类型指针,存储对象所属的类的元数据的引用,jvm通过类型指针确定此对象是哪个类的实例
     

Mark Work结构如下
在这里插入图片描述
每个对象都关联了一个Monitor(这也是为什么每个对象都可以作为锁的原因),锁的指针指向对象对应的Monitor,当某个线程持有锁时,Monitor处于锁定状态

 

synchronized的4种锁状态及膨胀方向

无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁

  • 无锁:没有线程要获取锁,未加锁
  • 偏向锁:大多数情况下,锁不存在多线程竞争,很多时候都是同一线程多次申请锁。偏向锁简化了线程再次申请锁的流程,减少了同一线程多次获取同一个锁的代价。偏向锁只适用于锁竞争不激烈的情况
  • 轻量级锁:适用于锁竞争一般的情况,等待过程中会自旋等待锁的释放
  • 重量级锁:适用于锁竞争激烈的情况
    在这里插入图片描述

 

使用Lock接口

synchronized使用前自动加锁、使用完自动释放锁,很方便。synchronized是悲观锁的实现,每次操作共享资源前都要先加锁;以前是重量级锁,性能低,经过不断优化,量级轻了很多,性能和Lock相比差距不再很大。

Lock需要自己加锁、用完需要自己释放。Lock是乐观锁的实现,每次先操作共享资源,提交修改时再验证共享资源是否被其它线程修改过;Lock是轻量级锁,性能很高。

Lock接口有很多实现类,常用的有ReentrantLock 可重入锁、ReadWriteLock 读写锁,也可以自己实现Lock接口来实现自定义的锁。要注意是否可能发生异常,需不需要把释放锁放在finally中。

 

ReentrantLock 可重入锁

重入:一个线程再次获取自己已持有的锁

public class Xxx{
    public final static ReentrantLock lock=new ReentrantLock(); //锁对象都可以加个final防止被修改
    //public final static ReentrantLock lock=new ReentrantLock(true);  //可指定是否是公平锁,缺省时默认false

    public void a() {
        lock.lock();  //获取锁,如果未获取到锁,会一直阻塞在这里
        // lock.tryLock();  //只尝试1次,如果未获取到锁,直接失败不执行后面的代码
        
        //....   //操作共享资源
        
        lock.unlock();  //释放锁
    }
    
    public void b() {
        try {
            lock.tryLock(30, TimeUnit.SECONDS);  //如果获取锁失败,会在指定时间内不停尝试。此句代码可能会抛出异常
            //.... //操作共享资源
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            if (!lock.isFair()){
                lock.unlock();  //如果获取到锁,最终要释放锁
            }
        }
    }

    public void c() {
        lock.lock();
        try {
            //.... //操作共享资源
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            lock.unlock();  //如果操作共享资源时可能发生异常,最终要释放锁
        }
    }
    
}

 

ReentrantLock如何实现公平锁、非公平锁?

使用链表存储等待同一把锁的线程,将等待锁的线程添加到链表尾部,释放锁后

  • 公平锁:将锁分配给链表头部的线程
  • 非公平锁:将锁分配个链表中的任意一个线程

将获得锁的线程从链表中移除

 

synchronized、ReentrantLock的比较
  • synchronized是关键字,ReentrantLock是类
  • 机制不同,synchronized是操作对象的Mark Word,ReentrantLock是使用Unsafe类的park()方法加锁
  • synchronized是非公平锁,ReentrantLock可以设置是否是公平锁
  • ReentrantLock可以实现比synchronized更细粒度的控制,比如设置锁的公平性
  • 锁竞争不激烈时,synchronized的性能往往要比ReentrantLock高;锁竞争激烈时,synchronized膨胀为重量级锁,性能不如ReentrantLock
  • ReentrantLock可以设置获取锁的等待时间,避免死锁

 

ReadWriteLock 读写锁

ReadWriteLock将锁细粒度化分为读锁、写锁,synchronized、ReentrantLock 同一时刻最多只能有1个线程获取到锁,读锁同一时刻可以有多个线程获取锁,但都只能进行读操作,写锁同一时刻最多只能有1个线程获取锁进行写操作,其它线程不能进行读写操作。

读写锁做了更加细致的权限划分,加读锁时多个线程可以同时对共享资源进行读操作,相比于synchronized、ReentrantLock,在以读为主的情况下可以提高性能。

ReadWriteLock是接口,常用的实现类是ReentrantReadWriteLock 可重入读写锁。

public class Xxx {
    public static ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); 
    public static ReentrantReadWriteLock.ReadLock readLock = lock.readLock();  //从读写锁获取读锁
    public static ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();  //从读写锁获取写锁
    //.....

    public void a(){
        //....

        readLock.lock();
        //..... 操作共享资源
        readLock.unlock();
        
        //....
    }

}

读锁、写锁的操作方式和ReentrantLock完全相同,都可以设置超时,这3种锁都是可重入锁

 

锁降级

在获取写锁后,写锁可以降级为读锁

public class Xxx {
    public static ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); 
    public static ReentrantReadWriteLock.ReadLock readLock = lock.readLock();  //读锁
    public static ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();  //写锁
    //.....

    public void a(){
        //....

        writeLock.lock();  //获取写锁
        //..... 对共享资源进行写操作
        readLock.lock();  //获取读锁(仍然持有写锁)
        writeLock.unlock();  //释放写锁(只持有读锁,写锁->读锁,锁降级)
        //.....  //对共享资源进行读操作
        readLock.unlock();  //释放读锁
        
        //....
    }

}
  • 锁降级后,线程仍然持有写锁,需要自己释放写锁
  • 锁降级的意义在于:后续对共享资源只进行读操作,及时释放写锁可以让其它线程也能获取到读锁、进行读操作
  • 锁降级的应用场景:对数据比较敏感,在修改数据之后,需要校验数据
  • 写锁可以降级为读锁,但读锁不能升级为写锁

 

AQS如何用int值表示读写状态

AbstractQueuedSynchronizer,抽象类

int,4字节32位,高位(前16位)表示读锁状态,低位(后16位)表示写锁状态。状态指的是重入次数,最大为2^16-1=65536

 

StampedLock

StampedLock是jdk1.8新增的类,可以获取读写锁、读锁、写锁,可以选择悲观锁、乐观锁,但StampedLock是不可重入的,且API比其他方式复杂,使用难度稍高。

 

ThreadLocal

ThreadLocal使用ThreadLocalMap存储所在线程中的线程私有数据,一个线程对应一个ThreadLocalMap。ThreadLocal可以实现各个线程私有数据的隔离,并发场景下可以实现无状态调用,常用于存储可能会被多个线程访问、但线程之间需要独立操作的变量。

//如果不指定初始值,默认为null(泛型不能为基本类型)
private ThreadLocal<Integer> threadLocal1 = new ThreadLocal<>();
//可以指定初始值
private ThreadLocal<String> threadLocal2 = ThreadLocal.withInitial(() -> "aaa");
private ThreadLocal<User> threadLocal3 = ThreadLocal.withInitial(() -> new User());

public void test() {
    //设置|更新值
    threadLocal1.set(100);
    threadLocal2.set("bbb");
    threadLocal3.set(new User("chy"));

    //获取值
    Integer integer = threadLocal1.get();
    String string = threadLocal2.get();
    User user = threadLocal3.get();

    //重置为初始值
    threadLocal1.remove();
    threadLocal2.remove();
    threadLocal3.remove();
}

 
在这里插入图片描述

  • 每个线程自己维护一个ThreadLocalMap,用于存储线程自身所有的ThreadLocal变量。
  • map自然是以键值对的形式存储,键值对对应的内部类是Entry(ThreadLocal<?> k, Object v),key是ThreadLocal对象,value是该ThreadLocal对象对应的值。

 

使用ThreadLocal的注意点

  • 每个线程的ThreadLocal都是线程私有的,主线程的ThreadLocal也是主线程自身私有的,不会共享给子线程。
public void test() {
	ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

    //此处操作的是主线程自己的ThreadLocal
    threadLocal.set(100);
    threadLocal.get();

    //lambda表达式可以访问外部变量,可以使用主线程中定义的threadLocal
    //但创建线程时,是把最初的ThreadLocal深拷贝一份,放在线程自身的ThreadLocalMap中,拷贝的是初值,不是主线程自身的ThreadLocal
    new Thread(() -> {
        //此处取到的是初始值null
        threadLocal.get();
        threadLocal.set(200);
    }).start();

    new Thread(() -> {
        //取到的也是初始值null
        threadLocal.get();
    }).start();
    
}
  • 线程池会复用线程,在提交给线程池的任务中使用ThreadLocal时,最后一定要使用remove()清除对ThreadLocal做的修改、重置到初始值,防止影响到此线程后续执行的使用了该ThreadLocal的任务。
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
        6,
        20,
        10, TimeUnit.MINUTES,
        new LinkedBlockingQueue<Runnable>(100),
        new ThreadPoolExecutor.AbortPolicy()
);

ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

threadPoolExecutor.execute(() -> {
    threadLocal.set(100);
    //...
    threadLocal.remove();
});

threadPoolExecutor.execute(() -> {
    threadLocal.set(200);
    //...
    threadLocal.remove();
});

 

volatile

public static volatile boolean flag = true;
  • volatile只能修饰变量
  • 2个作用:实现了所修饰变量的可见性、禁止指令重排序。可见性指的是变量值被某个线程修改时,其它使用此变量的线程可以观测到这个变化。
  • volatile只实现了可见性,没有实现原子性,严格来说并没有实现线程安全。
     

volatile、synchronized的比较
在这里插入图片描述
 

Atomic系列原子类

i++、++i、i–、–i、+=、-=等操作都不是原子性的,juc的atomic包下的类提供了自增、自减、比较赋值、取值修改等原子性方法,可以线程安全地进行操作

AtomicInteger atomicInteger = new AtomicInteger(0);
AtomicLong atomicLong = new AtomicLong(0);
AtomicBoolean atomicBoolean = new AtomicBoolean(false);
//引用
AtomicReference<User> user = new AtomicReference<>(new User());

//以上4种内部都维护了一个目标类型的成员变量,都可以使用带参构造器给该成员变量设置初始值
//也可以使用无参构造器,使用无参构造器时,AtomicInteger、AtomicLong默认初始值0,AtomicBoolean默认初始值false,AtomicReference默认初始值null


//数组
AtomicIntegerArray atomicIntegerArray1 = new AtomicIntegerArray(new int[]{1, 23});
//参数可以是数组,也可以是数组的元素个数
AtomicIntegerArray atomicIntegerArray2 = new AtomicIntegerArray(10);

AtomicLongArray atomicLongArray1 = new AtomicLongArray(new long[]{1, 23});
AtomicLongArray atomicLongArray2 = new AtomicLongArray(10);


//对象的引用型字段,此处的引用型字段是 orderList
AtomicReferenceFieldUpdater<User, List> userOrderList = AtomicReferenceFieldUpdater.newUpdater(User.class, List.class, "orderList");
//对象的Integer型字段,相当于 AtomicReferenceFieldUpdater<User, Integer>
AtomicIntegerFieldUpdater<User> atomicIntegerUserAge = AtomicIntegerFieldUpdater.newUpdater(User.class, "age");
//对象的Long型字段,相当于 AtomicReferenceFieldUpdater<User, Long>
AtomicLongFieldUpdater<User> atomicLongUserId = AtomicLongFieldUpdater.newUpdater(User.class, "id");
  • 原子类适合多线程读写简单数据类型的变量。只能保证单个变量的原子性,只能进行简单操作,如果要保证多个变量、稍微复杂点的操作的原子性,要用其它方式来实现线程安全(一般是加锁)。
  • 原子类使用CAS实现乐观锁,并发支持好、效率高
  • CAS提交修改失败时会while循环进行重试,如果重试时间过长,会给cpu带来很大开销
  • 可能发生ABA问题。有2个原子类解决了ABA问题 :AtomicMarkableReference、AtomicStampedReference,使用标记、邮戳实现乐观锁,和版本号、时间戳机制差不多,避免了ABA问题。

我们自己实现原子性操作时,也可以使用CAS算法。

 

并发容器

ArrayList、LinkedList、HashSet、HashMap等常见的普通集合容器都不是线程安全的,多线程并发读写同一个普通集合时(多个线程同时修改同一个集合,或者有的线程读、有的线程写),会抛出并发修改异常。

多线程并发读写集合时,不能使用普通集合,需要使用线程安全的集合

  • Vector、Hashtable 的方法都使用synchronized修饰,是线程安全的,但缺点较多,基本不使用这2个类。
  • Collections.synchronizedXxx()可以将集合转换为线程安全的集合,是使用synchronized锁住整个集合,读写都加锁,同一时刻只能单线程操作该集合,效率低下。
  • juc提供了常用的并发容器,线程安全,性能也不错。

 

Copy-On-Write集合容器CopyOnWriteArrayList、CopyOnWriteArraySet
//有序,按照插入顺序排列,内部使用Object[]存储元素
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();  

//无序,CopyOnWriteArraySet内部使用CopyOnWriteArrayList存储元素
CopyOnWriteArraySet<String> set = new CopyOnWriteArraySet<>(); 
//写操作都要先获取锁|加锁
public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        //从原数组复制得到新数组
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        //往新数组中添加元素
        newElements[len] = e;
        //指向新数组
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

//读操作都不加锁
public E get(int index) {
    return get(getArray(), index);
}
  • 线程进行写操作时,需要先获取锁,避免多个线程同时进行写操作。获取到锁后,复制内置数组得到一个新数组,在新数组上进行修改,最后将引用指向新数组。
  • 某个线程进行写操作时,其它线程不能进行写操作,但依然可以进行读操作,可以读取旧数组中的内容。
     

Copy-On-Write总结

  • 设计思想:读写分离,最终一致性
  • 优点:多线程并发读的时候不加锁,性能好
  • 缺点:写的时候需要复制内置数组,元素多的时候复制时间长,同时存在2个数组,内用占用大,容易触发GC;且写的时候其它线程可以对旧数组进行读操作,可能存在数据不一致的情况。
  • 应用场景:适合读多写少的场景,比如黑名单、白名单。
  • 使用建议:每次调用修改的方法时,都会复制数组,要操作多个元素时,尽量使用addAll()、removeAll()之类的方法,一次性修改,只复制数组一次,可以提升性能。

 

ConcurrentHashMap、ConcurrentLinkedQueue
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();  //map

ConcurrentLinkedQueue<String> queue1 = new ConcurrentLinkedQueue<>();  //基于链表的队列

 

阻塞队列LinkedBlockingQueue、ArrayBlockingQueue

常用于在生产者/消费者的线程协作模式中。

//基于链表的阻塞队列,如果参数指定了元素个数,则有界、不能扩容,如果未指定,则无界
LinkedBlockingQueue<String> queue2 = new LinkedBlockingQueue<>();  

//基于数组的阻塞队列,指定容量,不能扩容(有界)
ArrayBlockingQueue<String> queue3 = new ArrayBlockingQueue<>(20);  
//可以指定是否是公平锁,默认false
ArrayBlockingQueue<String> queue4 = new ArrayBlockingQueue<>(20,true);  
LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>();  
        
//入队的3个方法

//返会操作结果,boolean,如果队列满了放不下,返回false
queue.offer(""); 
//返会操作结果,boolean,如果队列满了放不下,会抛出异常
queue.add("");  
try {
	//如果队列满了,会阻塞线程,直到队列有空位可以放进去
    queue.put("");
} catch (InterruptedException e) {
    e.printStackTrace();
}


//出队的3个方法

//如果队列中没有元素,返回null
queue.poll();
//如果队列中没有元素,直接抛出异常
queue.remove();
try {
	//如果队列中没有元素,会阻塞线程,直到有元素可弹出
    queue.take();
} catch (InterruptedException e) {
    e.printStackTrace();
}

 

并发工具类

CountDownLatch

CountDownLatch是一个计数器,常用于等待多条线程都执行完毕

//指定次数
CountDownLatch countDownLatch = new CountDownLatch(2); 

new Thread(()->{
    //.....
    countDownLatch.countDown();  //次数-1
}).start();


new Thread(()->{
    //......
    countDownLatch.countDown();
}).start();


try {
    countDownLatch.await();  //阻塞当前线程,直到次数为0时才继续往下执行,即等待2个线程执行完毕
    //......
} catch (InterruptedException e) {
    e.printStackTrace();
}

 

CyclicBarrier 栅栏

CountDownLatch用于等待一些线程全部执行完毕,CyclicBarrier用于一些线程在执行中的某个阶段互相等待,等待的线程都执行到执行阶段时才继续往下执行

//指定await要应用的线程数
CyclicBarrier cyclicBarrier = new CyclicBarrier(3);

new Thread(()->{
    //......
    try {
        cyclicBarrier.await();  //执行到此开始阻塞线程,等待其它线程也执行到这句代码
        //.....
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (BrokenBarrierException e) {
        e.printStackTrace();
    }
}).start();


new Thread(()->{
    //......
    try {
        cyclicBarrier.await();  //执行到此开始阻塞线程,等待其它线程也执行到这句代码
        //.....
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (BrokenBarrierException e) {
        e.printStackTrace();
    }
}).start();


//.....
try {
    cyclicBarrier.await();  //执行到此开始阻塞线程,等待其它线程也执行到这句代码
    //.....
} catch (InterruptedException e) {
    e.printStackTrace();
} catch (BrokenBarrierException e) {
    e.printStackTrace();
}

线程执行到await()处会阻塞,停下来,直到指定数量的线程都执行到await()才会继续往下执行。

 

Semaphore 信号量

常用于限流

//指定可用的信号量
Semaphore semaphore = new Semaphore(2);  
// Semaphore semaphore = new Semaphore(2,true);  //可指定是否使用公平锁(先来后到依次获取信号量),默认false

new Thread(() -> {
    //......
    try {
        semaphore.acquire();  //获取|消耗1个信号量,信号量-1。如果信号量已经是0,没有可用的信号量,会阻塞线程直到获取到一个信号量
        //....
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        semaphore.release();  //操作完释放信号量,信号量+1
    }
}).start();

 

Exchanger

交换机,用于2条线程之间交换数据,只能用于2条线程之间,即一个Exchanger对象只能被2条线程使用(成对)

Exchanger<String> stringExchanger = new Exchanger<>();  //泛型指定交换的数据类型

new Thread(()->{
    try {
        String data = stringExchanger.exchange("are you ok?");
        System.out.println("线程1接收到的数据:" + data);  //ok
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}).start();

new Thread(()->{
    try {
        String data = stringExchanger.exchange("ok");
        System.out.println("线程2接收到的数据:" + data);  //are you ok
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}).start();

 

锁的分类

  • 自旋锁:多线程切换上下文会消耗系统资源,频繁切换上下文不值得,线程在没获取到锁时暂时执行空循环等待获取锁,即自旋,循环次数即自旋次数;如果在指定自旋次数内没获取到锁,则挂起线程,切换上下文,执行其它线程。锁默认是自旋的。

  • 自适应自旋锁:自旋次数不固定,由上一次获取该锁的自旋时间及锁持有者的状态决定,更加智能
     

  • 阻塞锁:阻塞锁会改变线程的运行状态,让线程进入阻塞状态进行等待,当获得相应信号(唤醒或阻塞时间结束)时,进入就绪状态
     

  • 重入锁:已持有锁的线程,在未释放锁时,可以再次获取到该锁

public class Xxx{
    public final static ReentrantLock lock=new ReentrantLock();

    public void a() {
        lock.lock();
        //.....
        b();  //如果锁是可重入的,则b()直接获取到锁;如果锁不是可重入的,则b()需要单独获取获取锁,但锁还没被a()释放,b()会一直获取不到锁
        //.....
        lock.unlock();
    }

    public void b() {
        lock.lock();
        //......
        lock.unlock();
    }

}

 

  • 读锁:是一种共享锁 | S锁(share),多条线程可同时操作共享资源,但都只能进行读操作、不能进行写操作

  • 写锁:是一种排它锁 | 互斥锁 | 独占锁 | X锁,同一时刻最多只能有1个线程可以对共享资源进行读写操作,其它线程不能对该资源进行读写
     

  • 悲观锁:每次操作共享资源时,认为期间其它线程一定会修改共享资源,每次操作共享数据之前,都要给共享资源加锁

  • 乐观锁:每次操作共享资源时,认为期间其它线程一般不会修改共享资源,操作共享资源时不给共享资源加锁,只在提交修改时验证数据是否被其它线程修改过,常用版本号等方式实现乐观锁
     

  • 公平锁:等待锁的线程按照先来先得顺序获取锁

  • 非公平锁:释放锁后,等待锁的线程都可能获取到锁,不是先来先得

非公平锁可能导致某些线程长时间甚至一直获取不到锁,但这种情况毕竟是极少数;使用公平锁,为保证公平性有额外的开销,会降低性能,所以一般使用非公平锁
 

  • 偏向锁:初次获取锁后,锁进入偏向模式,当获取过锁的线程再次获取该锁时会简化获取锁的流程,即锁偏向于曾经获取过它的线程

 

锁消除:编译时会扫描上下文,自动去除不可能存在线程竞争的锁

锁细化:如果只操作共享资源的一部分,不用给整个共享资源加锁,只需给要操作的部分加锁即可。使用细粒度的锁可以让多个线程同时操作共享资源的不同部分,提高效率。

锁粗化:要操作共享资源的多个部分,如果每次只给部分加锁,频繁加锁、释放锁会影响性能,可以扩大锁的作用范围,给整个共享资源加锁,避免频繁加锁带来的开销。

 

指令重排序

指令重排序:编译器、处理器会对指令序列重新排序,提高执行效率、优化程序性能

int a=1;
int b=1;

以上2条指令会被重排序,可能2条指令并发执行,可能int a=1;先执行,可能int b=1;先执行。
 

指令重排序遵循的2个原则

1、 数据依赖性,不改变存在数据依赖关系的两个操作的执行顺序。

int a=1;
int b=a;

b依赖于a,重排序不能改变这2个语句的执行顺序
 

2、as-if-serial原则,重排序不能改变单条线程的执行结果

int a=1;
int b=a;

执行结果是a=1、b=1,重排序后执行得到的也要是这个结果

 

数据同步接口

有时候需要对接第三方的项目,或者公司大部门之间对接业务,不能直接连接、操作他们的数据库,一般是建中间库|中间表,把我们|他们需要的数据放到中间库|表中,去中间库|表获取数据。更新数据库时需要同步更新中间库|表。
 

中间表的设计

  • 只存储要使用的字段即可
  • 需要用一个字段记录该条数据的状态:已入库、正在处理、处理时发生异常、已处理
  • 需要用一个字段记录数据入库时间
  • 需要用一个字段记录处理时间

记录时间是为了日后好排查问题、统计分析

 

对中间表的处理

可以使用生产者/消费者的线程协作模式

  • 生产者分批读取中间表中未处理的数据 where status=‘xxx’,放到仓库中。因为数据量一般很大,所以通常要分批读取,防止仓库装不下。如果要操作多张表,很多操作都差不多,可以抽象出接口
  • 消费者处理仓库中的数据

操作时需要更新中间表中的数据状态、处理时间


版权声明:本文为chy_18883701161原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/chy_18883701161/article/details/106572229