多线程2--相关理论知识

目录

常见的锁策略

(1)乐观锁和悲观锁

(2)重量级锁和轻量级锁

(3)自旋锁和挂起等待锁

(4)互斥锁和读写锁

(5)公平锁和非公平锁

(6)可重入锁和不可重入锁

CAS

1.CAS的介绍

 2.CAS的应用场景

(1)实现原子类

(2)实现自旋锁

3.CAS的ABA问题

synchronized原理 

1.升级锁/锁膨胀:

2.锁消除

3.锁粗化

JUC

1.Callable接口

2.ReentrantLock

3.原子类

 4.信号量semaphore

5.CountDownLatch

线程安全的集合类

1.多线程环境使用ArrayList

2.多线程使用哈希表


常见的锁策略

(1)乐观锁和悲观锁

乐观锁:预测锁竞争不是很激烈

悲观锁:预测锁竞争会很激烈

(2)重量级锁和轻量级锁

轻量级锁加锁开锁开销比价小,效率更高

重量级锁加锁解锁开销比较大,效率更低

大多数情况下,乐观锁也是一个轻量级锁。多数情况下,悲观锁也是一个重量级的锁。但是两者都不能完全保证到。

(3)自旋锁和挂起等待锁

自旋锁是一种典型的轻量级锁;挂起等待锁是一种典型的重量级锁。

(4)互斥锁和读写锁

互斥锁:就是前面的像synchronized这样的锁提供加锁和解锁两个操作,如果一个线程加锁了,另一个线程也尝试加锁,就会阻塞等待。

读写锁:分为三种操作

1.争对读加锁

2.争对写加锁

3.解锁

多线程争对同一个变量并发读,这个时候没有线程安全问题,也不需要加锁控制。

读锁和读锁之间,没有互斥;写锁和写锁之间,存在互斥;写锁和读锁之间,存在互斥。

代码中,如果只是读操作,加读锁即可。如果有写操作,加写锁。假设当前有一组线程都去读(加读锁),这些线程之间是没有锁竞争的。也没有线程安全问题(又快、又准);如果当前的一组操作有读也有写,才会产生竞争。

(5)公平锁和非公平锁

公平锁:遵守“先来后到”,B比C先来,当A释放锁后,B会优先于C获取到锁。

非公平锁:不遵循先来后到,B和C都有可能获取到锁。

操作系统和Java synchronized 原生都是“非公平锁”。

(6)可重入锁和不可重入锁

不可重入锁:一个线程针对一把锁,连续加锁两次,出现死锁。

可重入锁:一个线程争对一把锁,连续加锁多次都不会死锁。

举例:synchronized

1.synchronized既是一个悲观锁,也是一个乐观锁。(synchronized默认是一个乐观锁,但是当出现了锁竞争比较激烈,这个时候就会变成悲观锁)

2.synchronized既是轻量级锁,也是一个重量级锁(默认是轻量级锁,如果发现当前锁竞争比较激烈,就会转换成重量级锁)

3.synchronized这里的轻量级锁,是基于自旋锁的方式实现的。

   synchronized这里的重量级锁,是基于挂起等待锁的方式实现的。

4.synchronized不是读写锁

5.synchronized是非公平锁

6.synchronized是可重入锁

上述谈到的六种锁策略,可以视为是“锁的形容词”。

CAS

1.CAS的介绍

CAS:全称compare and swap“比较和交换”。

CAS可以理解为是CPU给提供的一个特殊指令,通过这个指令,可以一定程度的处理线程安全问题。

给出CAS的伪代码:

 2.CAS的应用场景

(1)实现原子类

Java标准库里提供的类

(2)实现自旋锁

3.CAS的ABA问题

CAS在运行中的核心,检查value和oldValue是否一致,如果一致,就视为value中途没有被修改过。

事实上:这里的一致,可能是没有改过,也有可能是改过但是还原回来了。

把value的值设为A的话,cas判断value为A,此时可能value确实是始终是A.

也可能是value本来是A,被改成了B,又还原成了A

实际场景中比较小概率会遇到该事件

解决方案:加入一个版本号,初始版本号是1,每次修改加1,然后进行CAS的时候,不是以金额为基准,而是以版本号为基准。要是版本号没有改变,那就是一定没有发生改变。(版本号是只能增长,不能降低的)

synchronized原理 

两个线程,针对同一个对象加锁,就会产生阻塞等待。

synchronized内部还有一些优化机制,存在目的就是为了让这个锁更高效更好用。

1.升级锁/锁膨胀:

1)无锁 

2)偏向锁 

3)轻量级锁 

4)重量级锁

代码执行到 synchronized(locker){

                  }这个代码块里的时候,加锁过程会出现前面的几个阶段。

进入到加锁时,首先会进入到偏向锁状态,偏向锁不是正真的加锁,而只A是占个位置,有需要再真加锁,没有需要就不加锁了。

synchronized的时候,并不是真正的加锁,先偏向锁状态,做个标记(这个过程是非常轻量的),如果整个使用锁的过程中,都没有出现锁竞争,在synchronized执行完之后,取消偏向锁即可。没有锁冲突,这样做开销最低。

但是如果使用过程中,另一个线程也尝试加锁,在它加锁之前,迅速的把偏向锁升级成正真的加锁状态,另一个线程也就只能是阻塞等待。

当synchronized发生锁竞争的时候,就会从偏向锁升级为轻量级锁。此时synchronized相当于是通过自旋的方式来进行加锁。

如果很快别人就释放锁了,自旋是划算的,但是如果迟迟拿不到锁,一直自旋,并不划算。synchronized自旋不是无休止的自旋,自旋到一定程度后,就会再次升级成重量级锁(挂起等待锁),这次加锁是基于操作系统原生的API来进行加锁了,此时这个锁会影响到线程的调度。此时如果线程进行了重量级的加锁,并发生锁竞争,此时线程就会被放到阻塞队列中,暂时不参与CPU调度,知道锁被释放,这个线程才有机会获取到锁。(一旦线程被切出CPU,将会变成一个低效的事情)

2.锁消除

编译器智能的判断,看当前代码是否是真的要加锁,如果这个场景不需要加锁,程序员也加了,就自动把锁消除掉。

3.锁粗化

锁的粒度:synchronized包含的代码越多,粒度就越粗,包含的代码越少,粒度就越细。

通常情况下,认为锁的粒度细一点,比较好,加锁部分的代码是不能并发执行的。锁的粒度越细,能并发的代码越多,反之就越少。(类比筛子)

但在有的情况下,锁的粒度粗一些反而更好(两次加锁解锁之间间隔非常小,此时不如直接一次大锁搞定。

JUC

java.util.concurrent(并发)放了并发编程(多线程)相关的组件

各种集合类:scanner,random...

1.Callable接口

类似于Runnable一样,用来描述一个任务。

区别:Runnable描述的任务没有返回值,Callable描述的任务有返回值。

如果要使用一个线程单独计算出某个结果来,用Callable比较合适。

例:使用Callable来计算1+2+3+....+1000
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class ThreadDemo28 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //使用Callable来计算1+2+3+....+1000
        Callable<Integer> callable=new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                //这里的call()方法相当于Runnable中的run()方法
                //run()方法的返回值是void,call()方法的返回值是泛型参数
                int sum=0;
                for (int i = 0; i <=1000; i++) {
                    sum+=i;
                }
                return sum;
            }
        };
       //  Thread t=new Thread(callable) 这里不能把callable直接传到Thread里面
        FutureTask<Integer> futureTask=new FutureTask<>(callable);
        Thread t =new Thread(futureTask);
        t.start();
        Integer result=futureTask.get();//get()获取结果
        System.out.println(result);
    }
}

2.ReentrantLock

可重入的,词根:entry 条目/入口

ReentrantLock是标准库提供的另一种锁,“可重入锁”。

synchronized是基于代码块的方式来加锁解锁的。

ReentrantLock更传统,使用了lock方法和unlock方法加锁解锁

 这样的写法最大的问题就是unlock可能执行不到,这里使用try.....finally,unlock放在finally中,最后一定会执行unlock。

ReentrantLock的优势:

1.ReentrantLock提供了公平锁版本的实现。

可以根据构造方法的参数来确定使用公平锁还是非公平锁(true/false)

2.对于synchronized来说,提供的加锁方式就是“死等”,只要获取不到锁就一直阻塞等待,ReentrantLock提供了更加灵活的等待方式 。

 无参数版本,能加锁就加,加不上就放弃。

有参数版本,指定了超时时间,加不上锁就等待一会儿,如果时间到了也没有加上锁就放弃。

3.ReentrantLock提供了更强大的方便的一个等待通知机制

synchronized搭配的是wait notify.notify的时候是随机唤醒一个wait的线程

ReentrantLock搭配一个Condition类,进行唤醒的时候可以唤醒指定的线程。

注意:虽然ReentrantLock有一定的优势,但是在实际开发中大部分情况下还是使用的synchronized

import java.util.concurrent.locks.ReentrantLock;

public class ThreadDemo29 {
    public static void main(String[] args) {
        ReentrantLock reentrantLock=new ReentrantLock(true);
       boolean result=reentrantLock.tryLock();
       try{
           reentrantLock.tryLock();
       }finally {
           if(result) {
               reentrantLock.unlock();
           }
       }
    }
}

3.原子类

 4.信号量semaphore

信号量本质上就是一个计数器,描述了可用资源的个数。

p操作:申请一个可用资源,计数器就要-1

V操作:释放一个可用操作,计数器就要+1

P操作如果要是计数器为0了,继续P操作,就会出现阻塞等待的情况。

import java.util.concurrent.Semaphore;

public class ThreadDemo30 {
    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore=new Semaphore(3);
        semaphore.acquire();
        System.out.println("执行一次P操作");
        semaphore.acquire();
        System.out.println("执行一次P操作");
        semaphore.acquire();
        System.out.println("执行一次P操作");
        semaphore.acquire();
        System.out.println("执行一次P操作");

        semaphore.release();
    }
}

5.CountDownLatch

举例:一场跑步比赛的开始时间是明确的(裁判的发令时间),结束时间是不明确的(所有选手都冲过终点。为了等待比赛结束,就引入了CountDownLatch

主要是两个方法:

1.await(wait是等待,a=>all)主线程来调用这个方法

2.countDown 表示选手冲过了终点线

3.CountDownLatch在构造的时候,指定一个计数(选手的个数)

例如:指定四个选手进行比赛。初始情况下调用await就会阻塞。每个选手都冲过终点,都会调用countDown方法。前三次调用countDown,await没有影响。第四次调用countDown,await就会被唤醒,返回。(解除阻塞)此时就可以认为是整个比赛都结束了。

实际开发中:CountDownLatch也有很多的使用场景。

比如下载一个大文件:迅雷,steam,idm,fdm等方式下载。多线程下载,把一个大的文件,切分成多个小块文件,安排多个线程分别下载。

线程安全的集合类

Java标准库里大部分集合类都是“线程不安全的”,多个线程使用同一个集合类对象,可能会出现问题。

1.多线程环境使用ArrayList

1)自己加锁,自己使用synchronized或者ReentrantLock

2)Collections,synchronizedList使用这个方法把集合类套一层

3)CopyOnWriteArraryList,简称为COW,也叫做”写时拷贝“(很多服务器提供热加载的功能,通过这样的功能就可以不重启服务器,实现配置的更新,热加载的实现,可以使用刚才的写时拷贝思路。

新的配置放到新的对象中,加载过程里,请求仍然基于旧的配置工作,新的对象加载完毕,使用新对象代替旧对象(替换完成后,旧对象就可以释放了)

2.多线程使用哈希表

HashMap是线程不安全的

HashTable是线程安全的。给关键方法,加了synchronized

更推荐使用ConcurrentHashMap

考点:ConcurrentHashMap进行了哪些优化?比HashTable好在哪里?和HashTable之间的区别是什么?

1.最大的优化之处:ConcurrentHashMap相比于HashTable大大缩小了锁冲突的概率,把大锁转换成多把小锁。(HashTable的做法是直接在方法上加synchronized,等于给this加锁,只要操作哈希表上的任意元素,都会产生加锁,也就都可能会发生冲突。

2. ConcurrentHashMap做了一个比较激进的操作,针对读操作不加锁,只针对写操作加锁。

读和读之间没有冲突,写和写之间有冲突,读和写之间也没有冲突,(很多场景下读和写之间不加锁控制,可能会读到一个写了一半的结果,如果写操作不是原子的,此时读就可能读到写了一半的数据,相当于脏读

3.ConcurrentHashMap内部充分使用了CAS,通过这个来进一步的削减加锁操作的数量。

4.针对扩容,采取了“化整为零”的方式。

HashTable/HashMap扩容:创建一个更大的数组空间,把旧数组上的链表的每个元素都搬运到新的数组上。耗时并且用户体验不好。

ConcurrentHashMap扩容采取每次搬运一小部分元素的方式。创建新的数组,旧的数组也保留。等所有元素都搬运好了,最终再释放旧的数组。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Roylelele

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值