当前位置: 首页 > article >正文

JAVA EE(10)——线程安全——synchronized JUC(java.util.concurrent) 的常见类 线程安全的集合类

一.synchronized锁升级

上篇博文介绍了各种锁策略,那么在此基础上我再对常用的synchronized的优化策略进行讲解

synchronized (锁对象) {
	//其他代码
}

当我们使用synchronized对某一代码块加锁的时候,synchronized并不会在第一时间加锁,而是经历了(偏向锁——>轻量级锁——>重量级锁)这样的锁升级过程。
偏向锁
当一个线程第一次访问同步块时,JVM会尝试将该线程的ID记录在锁对象的对象头中,并标记为偏向锁。之后,当该线程再次进入同步块时,直接进入同步块
在这里插入图片描述
偏向锁——>轻量级锁
如果有其他线程尝试获取该锁,偏向锁会被撤销,升级为轻量级锁
在这里插入图片描述
轻量级锁——>重量级锁
当thread1等待了很久(自旋了很多次)lock依然没有释放,或者此时又来了很多线程。JVM看到这里的竞争太大,会考虑把lock升级为重量级锁

二.JUC(java.util.concurrent) 的常见类

2.1ReentrantLock

可重入互斥锁,和synchronized定位类似,都是用来实现互斥效果,保证线程安全。ReentrantLock需要手动上锁和解锁,而synchronized是自动加锁和解锁。
(1)更加灵活

//拿不到锁就死等
public void lock() {
	sync.lock();
}
//尝试获取锁,获取失败后立即返回false,不会阻塞当前线程
public boolean tryLock() {
	return sync.nonfairTryAcquire(1);
}
//尝试在指定的时间内获取锁,如果在超时之前成功获取到锁,则返回true,否则返回false
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

因为需要手动释放锁,为了避免忘记释放锁,一般搭配finally使用

ReentrantLock lock = new ReentrantLock(); 
-----------------------------------------
lock.lock();   
try {    
 //其他代码
} finally {    
 lock.unlock()    
}  

(2)可实现公平锁

//通过构造方法可以选择实例化非公平锁还是公平锁
public ReentrantLock(boolean fair) {
	sync = fair ? new FairSync() : new NonfairSync();
}

(3)更强大的唤醒机制
synchronized是通过Object的wait/notify实现等待-唤醒,每次唤醒的是一个随机等待的线程ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程

2.2原子类

原子类内部用的是CAS实现,所以性能要比加锁实现 i++高很多,原子类有以下几个

AtomicBoolean
AtomicInteger
AtomicIntegerArray
AtomicLong
AtomicReference
AtomicStampedReference

以AtomicInteger为例,常用方法有:
addAndGet(int delta); i += delta;
decrementAndGet(); --i;
getAndDecrement(); i–;
incrementAndGet(); ++i;
getAndIncrement(); i++;

2.3 Semaphore

信号量,用来表示 “可用资源的个数”,本质上就是一个计数器
可以用它来代替阻塞队列,来实现生产者——消费者模式

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Semaphore;

class producer_consumer_semaphore_Test{
    private static final ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(1000);
    private static final int capacity = 5;
    //未占用资源5个
    private static final Semaphore empty = new Semaphore(capacity);
    //已占用资源0个
    private static final Semaphore fill = new Semaphore(0);
    //实现互斥锁,非0即1
    private static final Semaphore mutex = new Semaphore(1);

    private static class producer implements Runnable {
        @Override
        public void run() {
            int n = 0;
            try {
                while (true) {
                    System.out.println("producer:" + ++n);
                    empty.acquire();
                    mutex.acquire();
                    queue.add(n);
                    mutex.release();
                    fill.release();
                    Thread.sleep(1000);
                }
            }catch (InterruptedException e){
                throw new RuntimeException(e);
            }
        }
    }
    private static class consumer implements Runnable{
        @Override
        public void run() {
            try {
                while (true) {
                    fill.acquire();
                    mutex.acquire();
                    System.out.println("consumer:" + queue.take());
                    mutex.release();
                    empty.release();
                    Thread.sleep(1000);
                }
            }catch (InterruptedException e){
                throw new RuntimeException(e);
            }
        }
    }
    public void start(){
        Thread ThreadProducer = new Thread(new producer());
        Thread ThreadConsumer = new Thread(new consumer());
        ThreadProducer.start();
        ThreadConsumer.start();
    }
}

public class Producer_Consumer_Semaphore {
    public static void main(String[] args) {
        producer_consumer_semaphore_Test producerConsumerSemaphore = new producer_consumer_semaphore_Test();
        producerConsumerSemaphore.start();
    }
}

2.4 CountDownLatch

同时等待 N 个任务执行结束

import java.util.Random;
import java.util.concurrent.CountDownLatch;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        int count = 10;
        //一共十个任务
        CountDownLatch latch = new CountDownLatch(count);
        for (int i = 1; i <= count; i++) {
            int n = i;
            Thread thread = new Thread(()->{
                Random random = new Random();
                int time = (random.nextInt(5) + 1) * 1000;
                System.out.println("线程:" + n + "开始执行");
                System.out.println("线程:" + n + "执行完毕");
                try {
                    Thread.sleep(time);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                //相当于count--
                latch.countDown();
            });
            thread.start();
        }
        //等待count减为0
        //await方法无参数就是死等
        //有参数可以传入时间
        latch.await();
        System.out.println("所有线程执行完毕");
    }
}

三.线程安全的集合类

我们学过的大多数集合类时=是线程不安全的,线程安全的只有Stack,Vector,Hashtable这三个。但是这三个缺点很明显,一旦实例出对象,无论在单线程还是多线程环境下,所进行的操作都需要加锁和解锁,这就严重降低了性能。那么,如何保证线程安全的同时又保证性能不受到严重影响,我总结出以下几点。

3.1多线程环境使用ArrayList

(1)使用ArrayList进行增删查改的时候手动加锁(synchronized),这个方式很好理解,这里就不过多赘述。

synchronized(锁对象){
	arrayList.add(元素)
}

(2)Collections.synchronizedList(new ArrayList)
该方法可以将List接口的实现类从线程不安全转换为线程安全,例如下面的代码:

public class Main {
    public static void main(String[] args) {
        //实例化普通的顺序表
        List<Integer> array = new ArrayList<>();
        //将array转换为线程安全的safeArray
        List<Integer> safeArray = Collections.synchronizedList(array);
    }
}

因为读写操作都进行了加锁,所以适用于读和写操作频率相对均衡的场景
(3)CopyOnWriteArrayList
CopyOnWrite的意思是:写时拷贝
学到现在,我们知道触发线程安全问题需要多线程对同一个共享变量进行"写"操作,而并发"读"操作是线程安全的
通过CopyOnWriteArrayList实例化出来的对象在进行读操作时不会加锁,当进行增删查改等写操作时会在原数组基础上拷贝一个副本,在副本上进行写操作,执行完毕后修改引用指向副本
在这里插入图片描述
读操作没有额外开销,但频繁地写操作会频繁地拷贝数组,不加锁的本意是节省加锁和解锁的开销,但如果拷贝的开销都大于加锁解锁那就得不偿失了,所以CopyOnWriteArrayList适用于读多写少的环境

3.2多线程环境使用队列

1.使用队列进行增删查改的时候手动加锁(synchronized)
2.BlockingQueue阻塞队列

3.3多线程环境使用哈希表

HashMap线程不安全,Hashtable性能不行,Java标准库就引入ConcurrentHashMap,下面来讲讲ConcurrentHashMap到底做出了哪些优化
(1)缩小锁的粒度
在这里插入图片描述
基于以上的问题,ConcurrentHashMap缩小了锁的粒度
在这里插入图片描述
(2)合理使用CAS
哈希表的增加/删除元素的操作会让useSize++/–,这个自增/自减操作就可以使用CAS来代替
(3)针对扩容进行优化
当负载因子>0.75(默认情况下)时,会把哈希表扩容为原来的2倍,这涉及到两个操作。第一步,创建一个2倍大的哈希表;第二部,将原哈希表上面的元素重新哈希到新的表上,当元素过多时,这一步会非常耗时。
因为哈希表的增删查改操作的时间复杂度理论上是O(1),这样的速度是十分快的,假如此时一共有一千万个元素,如果插入某一元素时触发扩容操作,那么这一次插入操作就非常耗时。站在用户层面,大部分时间运行流程,但时不时就非常卡顿,这不利于用户的体验。

所以,针对扩容操作,ConcurrentHashMap采用的是分布扩容的方式。(这里的数据都是假设而已,理解思想即可)
例如,要对拥有一千万元素的哈希表进行扩容,每一次增删查改操作都只重新哈希一万个元素,进行一千次操作就扩容完毕了。

那么这会不会导致哈希表一直在扩容的路上?
答案是:不会,因为分布扩容总共进行一千次就完毕了,而触发一次扩容操作则需要百万数量级的数据,这显然是低频的。
当进行分布扩容时,此时存在旧/新两张哈希表,那么这个时候的增删查改操作就怎么进行的?
增加:直接添加到新表
删除:将旧/新表的目标元素都删除
修改:通常发生在旧表
查询:旧/新表的进行查询

四.小结

线程安全问题介绍到这里就差不多了,从下节开始进入文件IO的学习


http://www.kler.cn/a/586934.html

相关文章:

  • 机器学习编译器(二)
  • Java中的访问修饰符有哪些
  • Swagger 从 .NET 9 中删除:有哪些替代方案
  • 洛谷 P4933 大师
  • LRU(最近最少使用)算法实现
  • 探索Maas平台与阿里 QWQ 技术:AI调参的魔法世界
  • 车载软件刷写工具vFlash --- 自动化接口(Automation API)应用简介
  • 德语A1学习
  • 批量ip反查域名工具
  • 删除有序数组中的重复项(26)
  • [网络] 网络基础概念--socket编程预备
  • Ubuntu 24 常用命令方法
  • 【Git】配置Git
  • 按钮权限的设计及实现
  • uniapp-x vue 特性
  • 在线 SQL 转 SQLAlchemy:一键生成 Python 数据模型
  • AcWing--870.约数个数
  • Windows环境下安装部署dzzoffice+onlyoffice的私有网盘和在线协同系统
  • Java中的I/O
  • 通过qemu仿真树莓派系统调试IoT固件和程序