并发编程的艺术(一)

这是我参与更文挑战的第20天,活动详情查看: 更文挑战

前言

  • 学完了JUC的大概后,现在准备看书学习,深入了解一些底层的东西,更好的理解并发编程的概念。并发编程的目的是为了程序运行的更快,但是是不是线程越多执行的就越快呢?其实并不然,并发编程的世界里面临着很多挑战:比如上下文切换,死锁问题,以及受限于硬件和软件的资源限制问题。

一、上下文切换

  • cpu通过给每个线程分配CPU时间片,时间片是CPU分配给各个线程的时间,一般是几十毫秒。不同的线程去抢夺这个是时间片,CPU也不停的切换线程,让我们感觉是同时执行的。

  • CPU通过时间分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务,但是在切换前会保存上一个任务的状态,以便下次切换回到这个任务时,可以再加载这个任务的状态,所以保存任务到再加载的过程就是一次上下文切换。

  • cs(content Switch):上下文切换次数

二、如何减少上下文切换

  • 1、无锁并发编程:避免使用锁。
  • 2、CAS算法:Java的Atomic包下使用CAS算法来更新数据,而不需要加锁。效率比加锁高
  • 3、使用较少线程:如果任务较少,就不要创建这么多线程,造成资源浪费
  • 4、协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。(协程不是进程也不是线程,而是一个特殊的函数,这个函数可以在某个地方挂起,并且可以重新在挂起处外继续运行)

三、死锁

  • 我的理解就是两个线程互相占有对方线程所需要的锁又去相互获取锁的过程,使得两个线程互相等待对方释放锁。
  • 避免死锁的几个方法:
    • 1、避免一个线程同时获取多个锁
    • 2、避免一个线程在锁内同时占用多个资源,降低锁的粒度
    • 3、尝试使用定时锁,使用lock.tryLock来替代使用内部锁机制
  • 手写一个死锁:
public class DeathLockDemo {
    public static void main(String[] args) {
        Death death1 = new Death();
        Death death2 = new Death();

        new Thread(()->{
            death1.setFlag(1);
            death1.deathMethod();
        },"线程1").start();
        new Thread(()->{
            death2.setFlag(2);
            death2.deathMethod();
        },"线程2").start();
    }
}

class Death{
    private int flag=1;
    private static final Object o1=new Object();
    private static final Object o2=new Object();

    public void setFlag(int flag){
        this.flag=flag;
    }

    public void deathMethod(){
        if(flag==1){
            synchronized (o1){
                System.out.println(Thread.currentThread().getName()+"o1");
                try {
                    Thread.sleep(800);//等待时间让其他线程获取
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o2){
                    System.out.println(Thread.currentThread().getName()+"o2");
                }
            }
        }
        if(flag==2){
            synchronized (o2){
                System.out.println(Thread.currentThread().getName()+"o2");
                try {
                    Thread.sleep(800);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o1){
                    System.out.println(Thread.currentThread().getName()+"o1");
                }
            }
        }
    }

}
复制代码

四、资源限制的挑战

  • 比如我们启动一个微服务项目,很可能内存就100%,一个服务几百兆,多线程编程也会有这点问题,会有资源的限制,所以要在资源限制下进行编程。

五、Jmm引入

  • 说到volatile 就得提及jmm。jmm是java内存模型,是一种约定,实际并不存在。
  • 约定:
    • 1、线程解锁前,必须把共享变量立刻刷回主存
    • 2、线程加锁前,必须读取主存的最新值到工作内存
    • 3、加锁和解锁是同一把锁

image

  • 出现的问题:一个线程改变了主存的值,另一个线程不知道它发生了概念。内存不可见。所以引出了volatile
lock     (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
   unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
   read    (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
   load     (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
   use      (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
   assign  (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
   store    (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
   write  (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
复制代码

六、volatile的原理

  • 它在多处理器开发中保证了共享变量的可见性。如果volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。
分析:
instance =new Singleton()  //instance是volatile变量

# 转为汇编代码后:会多出第二行,是以lock开头的
0x01a3deld: movb &0x0 .....;
lock addl $0x0... ;
复制代码
  • Lock前缀的指令会引发两件事:

    • 1、将当前处理器缓存行的数据写回到系统内存
    • 2、这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效
  • 为了提高处理速度,处理器不直接和内存进行通信,而是将系统内存的数据读到内部缓存后再操作。如果没加volatile的话,它不知道何时会写入内存。而对声明了volatile的变量,jvm会向处理器发送一条Lock前缀的指令,将这变量的缓存值写入内存;利用多处理下的缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,如果发现这个值被修改了,就会把当前缓存值设为无效,重新去主存获取新的数据。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享