javaEE-多线程(3)

目录

一,常见的锁策略

1,乐观锁/悲观锁

2,重量级锁/轻量级锁

3,挂起等待锁/自旋锁

4,公平锁/非公平锁

5,可重入锁/不可重入锁

6,读写锁

synchronized原理

偏向锁

锁消除

锁粗化

二,CAS

1)基于CAS实现"原子类"

2)ABA问题

三,JUC(java.util.concurrent)

1,Callable 接⼝

2,ReentrantLock

3,信号量Semaphore

4,CountDownLatch

四,线程安全的集合类

解决方案:

1)自己加锁

2)使用带锁的List

3)使用CopyOnWrite集合类

3)多线程下使用队列

4)​编辑ConcurrentHashMap


一,常见的锁策略

1,乐观锁/悲观锁

乐观锁:加锁时候,假设出现锁冲突的概率不大,接下来围绕锁要做的工作就会很少
悲观锁:加锁时候,假设出现锁冲突的概率较大,接下来围绕锁要做的工作就会很多

2,重量级锁/轻量级锁

重量级锁:开销更大(时间开销),要做的工作更多,往往是悲观锁

轻量级锁:开销较小(时间开销),要做的工作较少,往往是乐观锁

3,挂起等待锁/自旋锁

挂起等待锁:属于悲观锁/重量级锁的典型代表,会让出CPU资源,过了一段时间之后,通过其他途径得知锁被释放了,然后再去获得锁(这种场景往往是锁竞争特别激烈,拿到锁的概率本身也不大,所以不妨将CPU让出来)

自旋锁:属于乐观锁/轻量级锁的典型代表,"忙等"等待的过程中不会释放CPU,会不停的检测锁是否被释放,一旦锁被释放就立刻有机会获得锁了

4,公平锁/非公平锁

公平锁/非公平锁:先来后到的方式定义为"公平",本身上操作系统针对锁的处理是以非公平的方式,如果需要实现公平锁,就需要额外的操作(比如:引入队列,记录每一个线程加锁的顺序)

5,可重入锁/不可重入锁

可重入锁/不可重入锁:对于一个线程,针对一把锁,连续加锁两次,就有可能出现"死锁",如果设置为可重入锁,就可以避免死锁了

1)记录当前是哪个线程持有这把锁

2)在加锁的的时候判定,申请的锁是否是之前申请锁的线程.

3)计数器,记录就锁的次数,从而确定何时真正释放锁

6,读写锁

读写锁:把"加锁操作"分成了两个情况提供了两种加锁的API,读加锁,写加锁,解锁的的API是一样的.如果两个线程都是按照读方式加锁,此时不会产生锁冲突(相当于对读操作进行了一个优化),如果两个线程都是加写锁,此时就会产生冲突,如果一个线程一个是读锁,一个是写锁,也会产生冲突

----------------------------------------------------------------------------------------

Java标准库提供了一个类ReentrantReadWriteLock实现的读写锁,可重入锁/读写锁.这个类里面又包含了两个内部类

 ReentrantReadWriteLock.ReadLock 类表⽰⼀个读锁. 这个对象提供了 lock / unlock ⽅法 进⾏加锁解锁. • ReentrantReadWriteLock.WriteLock 类表⽰⼀个写锁. 这个对象也提供了 lock / unlock ⽅法进⾏加锁解锁.

synchronized原理

synchronized原理:只是锁中的一种典型实现,是自适应的,一开始乐观锁,但是当锁冲突到一定程度时,就会变成悲观锁.轻量级锁就是基于自旋的方式实现的(JVM内部,用户态代码实现的),重量级锁就是基于挂起等待的方式实现的(调用操作系统的API在内核中实现的).可重入锁,属于非公平锁.非读写锁

synchronized的加锁过程:锁升级,刚开始使用synchronized加锁,首先是"偏向锁"状态,遇到线程的时候会升级到"轻量级锁",进一步统计竞争出现的频次,到达一定程度后升级为"重量级锁",锁升级的过程是为了适应不同的场景,降低程序员的负担,锁升级对于JVM是一个不可逆的过程(不能降级).

偏向锁

偏向锁:一开始就只是做了一个标记,并不是真正加锁,如果线程来竞争锁,就会加上锁.本质上是在推迟加锁的时机(懒汉模式思想)

总结:偏向锁->轻量级锁:出现竞争.轻量级锁->重量级锁:竞争加剧

锁消除

锁消除(编译器优化策略):编译器会对synchronized的代码做出判断,判断这个地方那个是否需要加锁,如果不需要就会把这个锁给取消掉(这只是一个辅助,并不能无脑加锁!!)

锁粗化

锁粗化(编译器优化策略):锁的粒度,synchronized的代码块,执行的代码越多,粒度越粗.锁粗化,就是把多个"细粒度"的锁合成一个"粗粒度"的锁(每次加锁都涉及到阻塞等待)

二,CAS

(Compare and swap)比较内存和CPU寄存器中的内容,如果发现相同,就进行交换(交换的是内存和另一个寄存器的内存)

一个内存数据和两个寄存器进行操作:比较内存和寄存器1的内容是否相同,如果相同就将寄存器2的内容与内存中与寄存器1相同的内容进行交换(我们更关心内存里面的内容,因此与其说交换,不如说赋值给内存).CAS的关键不在于这个逻辑能干嘛,而在于这是通过"一个CPU指令"完成的(原子的),因此这会给程序员带来新的思路,实现"无锁化编程"

运用场景:

1)基于CAS实现"原子类"

int/long在进行的时候都不是原子的,但是可以通过CAS来实现"原子类",本质上就是对int/long++--进行封装.从而完成++--这种操作

原子类在Java标准库中也有

这就是JVM对CAS进行封装,native修饰的方法是"本地方法"这个方法实现是在JVM内部通过c++代码实现的

这里的不安全(这里的代码,偏底层逻辑,需要程序员对操作系统和硬件有一定的了解,才能够使用这里的代码逻辑,一般不建议直接使用unsafe)

如何通过CAS实现原子类:

oldValue代表寄存器1的值,value代表内存里面的值,oldValue+1代表寄存器2的值.首先将内存的值赋值到寄存器1中(value赋值到oldValue中),进行CAS操作,这时会先对比value和oldValue里面的值是否相同,如果相同则意味着赋值和while循环操作中没有插入其他线程.此时就可以直接修改内存的值.但是如果value和oldValue里面的值不相同,就意味着上方赋值和CAS中间穿插了其他线程,这时CAS就会返回true,从而进入循环,重新进行复制操作(value再次赋值到oldValue中).

2)ABA问题

CAS之所以安全,是因为在通过CAS比较的过程,来确认是否在执行过程中有别的线程插入了该线程,但是当两个线程同时穿插的改变一个变量的时候,其中线程2将这个变量由A变成了B,之后又变成了A.这时候线程1的CAS进行比较内存和寄存器中的值的时候,这个两个值还是相同的.就会继续原来的操作.虽然变量A的值没有改变,但是整个过程中还是产生了一些其他的操作.这些操作就有可能产生的问题,这种由于变量在CAS中反复横跳产生的问题就是ABA问题

⼤部分的情况下, t2 线程这样的⼀个反复横跳改动, 对于 t1 是否修改 num 是没有影响的. 但是不排除⼀ 些特殊情况.比如银行转账,先进行一个转出500的操作,由于按了两次转账按键所以产生了两个线程,与此同时有一笔转入,金额恰好也是500,这是就会发生以下问题:

相当于进行了一次转出账操作,却进行了两次.

解决方案:设置版本号

三,JUC(java.util.concurrent)

1,Callable 接⼝

Callable->call方法=>带有返回值

Runnable->run方法=>返回void

Callable<Integer> callable=new Callable<Integer>() {
    @Override
    public Integer call() throws Exception {
       int sum=0;
        for (int i = 0; i < 100; i++) {
            sum+=i;
        }
        return sum;
    }
};
FutureTask<Integer> futureTask=new FutureTask<>(callable);
Thread thread=new Thread(futureTask);
thread.start();
System.out.println(futureTask.get());

Thread不能直接Callable的引用,但是可以借助FutureTask来间接实现使用.futureTask.get()带有阻塞功能,当前Thread没有执行完的时候,get就会阻塞,线程执行完后,get才能返回

2,ReentrantLock

ReentrantLock 也是可重⼊锁

private static int count=0;
public static void main(String[] args) throws InterruptedException {
    ReentrantLock locker=new ReentrantLock();
    Thread thread=new Thread(()->{
        for (int i = 0; i < 100; i++) {
            locker.lock();
            count++;
            locker.unlock();
        }
    });
    Thread thread2=new Thread(()->{
        for (int i = 0; i < 100; i++) {
            locker.lock();
            count++;
            locker.unlock();
        }
    });
    thread.start();
    thread2.start();
    thread.join();
    thread2.join();
    System.out.println(count);
}

synchronized与ReentrantLock的区别:

1)synchronized是属于关键字(底层是通过JVM的c++代码实现的),但是ReentrantLock是一个右Java标准库提供的类,它是由Java实现的

2)synchronized是通过代码块实现加锁解锁的,ReentrantLock是通过调用lock/unlock来实现的,这就导致可能会忘记解锁

3)ReentrantLock提供了tryLock这样的加锁风格,之前的锁,都是发现别人占用了之后等待阻塞,但是tryLock在加锁失败的情况下,不会阻塞,而是直接返回值来反馈是加锁成功还是失败了

4)ReentrantLock提供了公平锁的机制,默认也是非公平锁,但可以在构造方法中传入参数,设定为公平的

5)ReentrantLock有更强的"等待通知机制",基于Condition类,能力比wait/notify强大

3,信号量Semaphore

信号量就是一个"计数器,通过计数器衡量"可用资源"的个数,申请(acquire)资源让计数器加1,也称为"P 操作".释放(release)资源让计数器减1,也称之为"V操作",如果资源为0,继续申请,就会出现阻塞.

Semaphore semaphore=new Semaphore(3);
semaphore.acquire();
System.out.println("申请一个资源");
semaphore.acquire();
System.out.println("申请一个资源");
semaphore.acquire();
System.out.println("申请一个资源");
semaphore.release();
System.out.println("释放一个资源");
semaphore.acquire();
System.out.println("申请一个资源");

当信号量为1 的时候,就相当于"锁",这时资源数为1/0,也称二元信号量

private static int count=0;
public static void main(String[] args) throws InterruptedException {
    Semaphore semaphore=new Semaphore(1);
    Thread thread=new Thread(()->{
        for (int i = 0; i < 100; i++) {
            try {
                semaphore.acquire();
                count++;
                semaphore.release();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

        }
    });
    Thread thread2=new Thread(()->{
        for (int i = 0; i < 100; i++) {
            try {
                semaphore.acquire();
                count++;
                semaphore.release();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    });
}

4,CountDownLatch

需要把一个大的任务,拆分成小的任务,通过多线程/线程池执行,如何衡量所有任务都执行完毕了

比如:多线程下载,网络上下载的是单线程,但是想要提高下载的总速率,就可以使用专门的下载工具,通过和服务器建立多个网络连接,创建出多个线程就可以大大提高下载的总速率

public static void main(String[] args) throws InterruptedException {
    ExecutorService executorService= Executors.newFixedThreadPool(4);
    CountDownLatch countDownLatch=new CountDownLatch(20);//参数为任务数
    for (int i = 0; i < 20; i++) {
        int id=i;
        executorService.submit(()->{
            System.out.println("下载任务"+id+"开始");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("下载任务"+id+"结束");
            countDownLatch.countDown();
        });
    }
    countDownLatch.await();//相当于all wait.收到20个"完成,代表所有任务都结束了
    System.out.println("所有任务都完成了");
}

CountDownLatch由此,就可以衡量出当前任务是否完整执行结束

四,线程安全的集合类

Vector, Stack, HashTable...是线程安全的(不建议⽤), 其他的集合类不是线程安全的

解决方案:

1)自己加锁

2)使用带锁的List

如果需要使用ArrayList/LinkedList这样的结构标准库中提供了了带锁的List

3)使用CopyOnWrite集合类

这个集合类是不加锁的,但是通过"写实拷贝"来实现线程安全(来避免两个线程同时改变一个变量).

例如一个ArrayList,如果只是对这个进行读操作,则不进行任何改变,但是如果有其他线程要改变表中的数据,那么就会将这个ArrayList上面的元素重新拷贝到一个新的空间上,然后对新的ArrayList进行修改.(修改的时候,读操作还是读取的旧ArrayList里面的数据),修改完成后,将ArrayList的引用指向新的建立ArrayList的空间(这个赋值的操作是原子的).由此操作能够确保读到的是有效的数据,要么是新数据,要么是旧数据,不会读到只修改了一半的数据.

缺点:

(1)如果针对多个线程同时写的话,该方法就难以应对,这种方法主要针对一个线程读,一个线程写的场景.

(2)如果涉及的数据量很大,拷贝起来就会非常慢

3)多线程下使用队列

4)ConcurrentHashMap

多线程环境下使用哈希表HashMap/hashtable,推荐使用ConcurrentHashMap,相比前两者改进力度非常大.

(1)优化了锁的粒度(核心).

hashtable的加锁,就是直接给put/get等方法直接加上synchronized.整个哈希表对象就是一把锁,就会引起的锁竞争

ConcurrentHashMap是给每一个hash表中的"链表"进行加锁(不是一把锁,而是多把锁),这种方法可以保证线程是安全的,大大降低锁冲突的概率,只有同时进行两次修改,恰好修改的值在同一个链表上,才会触发锁冲突.

(2)ConcurrentHashMap引入了CAS这样的操作,针对修改size这样的操作,这样就不会加锁了

(3)针对读操作,做了特殊的处理,上述加锁,只针对写操作加锁,对于读操作,只是通过volatile以及一些精巧的代码实现,确保读操作不会读到"修改一半的数据"

(4)针对hash表的扩容进行了优化

普通的hash表扩容,是需要建立新的hash表,把元素搬过去,这一系列操作很肯能再一次普通中就完成了,这就会使out的开销非常的大,耗时长.但是ConcurrentHashMap是进行了"化整为零",不会在一次操作中将所有数据搬运,而是一次只搬运一部分.此后,每一次操作都会触发一部分key的搬运,最终把所有key都搬完成

当新旧表同时在的时候,插入操作是直接插入到新的空间中,查询/修改/删除,都是需要同时查询新旧表

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值