并发、线程安全


 

概念、理论

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

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

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

 

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

 

如何实现线程安全?
  • 使多线程不同时操作同一共享资源: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’,放到仓库中。因为数据量一般很大,所以通常要分批读取,防止仓库装不下。如果要操作多张表,很多操作都差不多,可以抽象出接口
  • 消费者处理仓库中的数据

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

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Java高并发线程安全集合是指在多线程环境下能够保证数据一致性和线程安全的数据结构。Java提供了许多高并发线程安全集合,包括ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、CopyOnWriteArrayList、CopyOnWriteArraySet等。 ConcurrentHashMap是一个线程安全的哈希表,它允许多个线程同时读取并修改其中的元素。它使用分段的方式来实现并发访问,不同的线程可以同时访问不同的分段,从而提高了并发性能。 ConcurrentSkipListMap是一个基于跳表的并发有序映射,它可以提供较好的并发性能,且支持按照键的顺序进行遍历。它的实现是通过通过多层链表实现的,每一层链表中的节点按照键的顺序排列。 ConcurrentSkipListSet是一个基于ConcurrentSkipListMap的并发有序集合,它实现了Set接口,并且保证元素的有序性和线程安全性。 CopyOnWriteArrayList是一个线程安全的ArrayList,它通过每次修改时创建一个新的副本来实现线程安全。虽然在插入和删除操作时需要复制整个数组,但读取操作非常高效,适用于读操作远多于写操作的场景。 CopyOnWriteArraySet是一个线程安全的Set,它是基于CopyOnWriteArrayList实现的。它通过复制整个数组来实现线程安全,保证了元素的唯一性和线程安全。 这些高并发线程安全集合在多线程环境中保证了数据的一致性和线程安全性,能够提高并发性能和效率,适用于高度并发和需要频繁读写的场景。但需要注意的是,并发集合在某些操作上可能会损失一些性能,因此在选择使用时需根据具体需求进行权衡和选择。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值