多线程进阶【二】

Synchronized原理

Synchronized的优化手段

锁膨胀/锁升级

体现了Synchronized能够“自适应”这样的能力。
在这里插入图片描述

锁粗化

粗化的反面是细化
此处的粗细指的是“锁的粒度”。指的是加锁代码涉及到的范围。加锁代码的范围越大,认为锁的粒度越粗,范围越小,则认为粒度越细。
在这里插入图片描述
到底锁的粒度是粗好还是细好?各有各的好
如果锁的粒度比较细,多个线程之间的并发性就更高
如果锁的粒度比较粗,加锁解锁的开销就更小
编译器就会有一个优化,就会自动判定说,如果某个地方的代码锁的粒度太细了,就会进行粗化。

锁消除

有些代码,不用加锁,结果给加锁了。编译器就会发现这个加锁没必要,就直接把锁给去掉了。
像StringBuffer 、 Vector…在标准库中进行了加锁操作,在单个线程中用到了上述的类,就是单线程进行了加锁解锁。

Java中的JUC

iava.util.concurrent

Callable 接口

Callable 是一个interface,也是一种创建线程的方式。
Runnable创建线程,但是不太适合让线程计算出一个结果,这样的代码。
例如:像创建一个线程,让这个线程计算1+2+3+…+1000,如果基于Runnable来实现,就会比较麻烦。

Callable就是要解决Runnable不方便返回结果这个问题的

public class Demo28 {
    public static void main(String[] args)  {
        //通过Callable来描述一个这样的任务
        Callable<Integer> callable=new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
               int sum=0;
               for(int i=0;i<=1000;i++){
                   sum += i;
               }
               return sum;
            }
        };
        //为了让线程执行callable中的任务,光有构造方法还不够,还需要一个辅助的类
        FutureTask<Integer> task=new FutureTask<>(callable);
        //创建线程。来完成这样的计算工作
        Thread t=new Thread(task);
        t.start();

        //如果线程的任务没有执行完,get就会阻塞
        //一致阻塞到,任务完成了,结果算出来了
        try {
            System.out.println(task.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }


    }
}

执行结果
在这里插入图片描述

ReentrantLock

可重入锁

基础用法

提供了两种方法
lock()
unlock()
把加锁和解锁两个操作分开了(这种做法不太好,很容易遗漏unlock)

public class Demo29 {
    public static void main(String[] args) {
        ReentrantLock  locker=new ReentrantLock();
        //加锁
        locker.lock();
        //如果在这里抛出异常了,就容易导致unlock执行不到
        //解锁
        locker.unlock();
    }
}

在这里插入图片描述

和Synchronized的区别

(1)Synchronized是一个关键字(背后的逻辑是JVM内部实现的,c++代码写的),ReentrantLock是一个标准库中的类(背后的逻辑是Java代码写的)。
(2)Synchronized不需要手动释放锁,出了代码块,锁自然释放。ReentrantLock必须要手动释放锁,要谨防忘记释放。
🎈(3)Synchronized如果竞争锁失败,就会阻塞等待,但是ReentrantLock除了阻塞等待,还会trylock,失败了直接返回。

trylock给了更多回旋余地

🎈(4)Synchronized是非公平锁,ReentrantLock提供了公平锁和非公平锁两个版本,在构造方法中,通过参数来指定当前十公平还是非公平。
(5)基于Synchronized衍生出来的等待机制是wait notify,功能相对有限; 基于ReentrantLock衍生出来的等待机制是Condition类,功能要更丰富一系。

信号量 Semaphore

是一个更广义的锁
锁是信号量里一种特殊情况,叫做“二次元信号量”。

举例:
开车的时候,经常会遇到一种情况—停车
停车场入口一般会有一个牌子,上面写着“当前空闲**个车位”
每次有车开进去,车位数-1
每次有车开出来,车位数+1
这个牌子就是信号量,描述了可用资源(车位)的个数
每次申请一个可用资源,计时器就-1(称为P操作)
每次释放一个可用资源,计数器就+1(称为V操作)
当信号量的计数器已经是0了,再次进行P操作,就会阻塞等待

public class Demo30 {
    public static void main(String[] args) throws InterruptedException {
        //初始化的值表示可用资源有4个
        Semaphore semaphore=new Semaphore(4);
        //申请资源  P操作
        semaphore.acquire();
        System.out.println("申请成功");
        semaphore.acquire();
        System.out.println("申请成功");
        semaphore.acquire();
        System.out.println("申请成功");
        semaphore.acquire();
        System.out.println("申请成功");
        semaphore.acquire();
        System.out.println("申请成功");


        //释放资源,V操作
        //semaphore.release();
    }
}

CountDownLatch

类似于终点线

countDown给每个线程里面去调用,就表示到达终点了
await是给等待线程去调用,当所有的任务都到达终点了,await就从阻塞中返回,就表示任务结束。

在这里插入图片描述

public class Demo31 {
    public static void main(String[] args) throws InterruptedException {
        //构造方法的参数表示有几个选手参赛
        CountDownLatch latch=new CountDownLatch(10);
        for(int i=0;i<10;i++){
            Thread t=new Thread(() ->{
                try {
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName()+"跑完了");
                    latch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }) ;
            t.start();
        }
        //裁判就要等待所有的线程到达
        //当这些线程没有执行完的时候,await就阻塞,所有的线程都执行完了,await才返回
        latch.await();
        System.out.println("比赛结束");

    }
}

执行效果
在这里插入图片描述

copyOnWriteArrayList

写时拷贝,在修改的时候,会创建一个副本出来。
适合于读多写少的情况,也适合于数据小的情况

例如:有一个ArrayList
如果是多线程读这个ArrayList,此时没有线程安全问题,完全不需要加锁,也不需要其他方面的控制,如果是多线程写,就是把这个ArrayList复制了一份,先修改副本。
要想将 [1,2,3,4]中的1修改为100,先修改,在让副本转正。
这样做的好处,就是修改的同时对于读操作,是没有任何影响的。读的时候优先读旧的版本,不会说出现读到了一个“修改了一半”的中间状态。

多线程下使用哈希表

HashMap本身线程不安全
1.HashTable 【不推荐】
HashTable 是如何保证线程安全的?就是给关键方法加锁

在这里插入图片描述

在这里插入图片描述

2.ConcurrentHashMap
在这里插入图片描述
ConcurrentHashMap改进之处
(1)ConcurrentHashMap减少了锁冲突,就是让锁加到每个链表的头节点上
(2)ConcurrentHashMap只是针对写操作加锁了,读操作没加锁,只是使用Volatile
(3)ConcurrentHashMap中更广泛的使用CAS,进一步提高效率
(4)ConcurrentHashMap针对扩容,进行了巧妙的化整为零
对于HashTable来说,只要这次put触发了扩容就一口气搬运完,会导致这次put非常卡顿。对于ConcurrentHashMap,每次操作只搬运一点点,通过多次操作完成整个搬运的过程。
同时维护一个新的HashMap和一个旧的,查找的时候既需要查找旧的,也需要查找新的,插入的时候只插入新的,直到搬运完在销毁旧的。

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值