并发编程学习

一、并发编程Bug源头

1. 缓存一致性问题

       缓存导致的可见性问题:具体来说就是缓存不一致问题,例如下面的例子:

        单核时代下,所有线程用一个缓存,因此不会产生缓存一致性问题。但是在多核时代下 

        同一个内存中的变量v在缓存中可能有不同的副本,缓存不一致问题存在,但是有许多协议能够保证缓存的一致性。 有MESI等协议。如下面的代码:

public class Test {
  private long count = 0;
  private void add10K() {
    int idx = 0;
    while(idx++ < 10000) {
      count += 1;
    }
  }
  public static long calc() {
    final Test test = new Test();
    // 创建两个线程,执行 add() 操作
    Thread th1 = new Thread(()->{
      test.add10K();
    });
    Thread th2 = new Thread(()->{
      test.add10K();
    });
    // 启动两个线程
    th1.start();
    th2.start();
    // 等待两个线程执行结束
    th1.join();
    th2.join();
    return count;
  }
}

两个线程分别循环给count加一,但是由于两个线程可能会交替运行,去读取内存中可能已经失效的值,导致最终结果20000以下。

2.原子性问题

cpu对于每个程序都有着时间片,时间片一到就会被中断,因此不具有原子性。
一个或者多个操作在 CPU 执行的过程中不被中断的特性称为原子性。

3.编译优化带来的有序性问题

双重检查创建单例对象:

public class Singleton {
  static Singleton instance;
  static Singleton getInstance(){
    if (instance == null) {
      synchronized(Singleton.class) {
        if (instance == null)
          instance = new Singleton();
        }
    }
    return instance;
  }
}

这个双重检查单例对象创建看起来貌似没有什么问题,但是值得注意的是,假如有两个线程线程A成功的检查到了instance==null,此时新创建一个对象赋值给instance,但是如果new的顺序是,分配内存,创建&M变量,将地址赋值给instance则不会产生异常。但是new的实际执行为:

1.分配内存 2.地址赋值 3.初始化对象

如果在2之后线程被中断则会导致此时虽然instance不是一个空的指针,但是Instance指向了一个没有初始化的对象。

 二、JAVA内存模型:解决可见性有序性问题

        最早诞生于C语言原始的意义就是禁用cpu缓存。
        java 1.5 Happens-Before规则:前一个操作的结果对于后续操作具有可见性

① 程序顺序性规则:

        同一线程中操作顺序不变(例如,线程的操作是a,b,c,d,则最终的顺序也必须是a,b,c,d)

② volatile变量规则:

        写操作必须可见与后面的读操作
③ 传递性

        

        已知 A线程中x = 42 应当先于v = true 变量赋值执行,读v变量在写v变量之后执行,因此最终顺序如上图所示。

④ 管程中的锁

解锁必须在加锁之后,这一点在Java中是自动实现的。

synchronized (this) { // 此处自动加锁
  // x 是共享变量, 初始值 =10
  if (this.x < 12) {
    this.x = 12; 
  }  
} // 此处自动解锁

⑤ 线程start( )规则

主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。

⑥ 线程join() 规则

指主线程 A 等待子线程 B 完成(主线程 A 通过调用子线程 B 的 join() 方法实现),当子线程 B 完成后(主线程 A 中 join() 方法返回),主线程能够看到子线程的操作。

其实就是同步,整个并行处理的过程无非就是同步并行同步并行等等。

被忽视的final关键字:

三、互斥锁

简易锁模型:

 Java 语言提供的锁技术:synchronized

默认:当修饰静态方法的时候,锁定的是当前类的 Class 对象,在上面的例子中就是 Class X;
当修饰非静态方法的时候,锁定的是当前实例对象 this。

class SafeCalc {
  long value = 0L;
  synchronized long get() {
    return value;
  }
  synchronized void addOne() {
    value += 1;
  }
}

这里保证了同一时刻只有一个线程才可以调用addone()方法,synchronized关键字给this加锁,其他线程如果要调用这个方法首先需要获得this对象。

由于get方法也在一个保护区里面所以和addone()是互斥的。 

class SafeCalc {
  long value = 0L;
  long get() {
    synchronized (new Object()) {
      return value;
    }
  }
  void addOne() {
    synchronized (new Object()) {
      value += 1;
    }
  }

这里每次加锁的不是一个对象,因此不能做到互斥。

 

  • 12
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
嗨!很高兴回答你关于Java并发编程的问题。请问你想知道什么方面的内容呢?我可以分享一些学习笔记和建议给你。 1. 并发编程基础:了解并发编程的基本概念,如线程、进程、锁、同步等。学习Java中的并发编程模型以及相关的API,如Thread、Runnable、Lock、Condition等。 2. 线程安全性:学习如何保证多线程环境下的数据安全性,了解共享资源的问题以及如何使用同步机制来防止数据竞争和并发问题。 3. 线程间的通信:掌握线程间的通信方式,如使用wait/notify机制、Lock/Condition等来实现线程的协调与通信。 4. 并发容器:学习并发容器的使用,如ConcurrentHashMap、ConcurrentLinkedQueue等。了解它们的实现原理以及在多线程环境下的性能特点。 5. 并发工具类:熟悉Java提供的并发工具类,如CountDownLatch、CyclicBarrier、Semaphore等,它们可以帮助你更方便地实现线程间的协作。 6. 并发编程模式:学习一些常见的并发编程模式,如生产者-消费者模式、读者-写者模式、线程池模式等。了解这些模式的应用场景和实现方式。 7. 性能优化与调试:学习如何分析和调试多线程程序的性能问题,了解一些性能优化的技巧和工具,如使用线程池、减少锁竞争、避免死锁等。 这些只是一些基本的学习笔记和建议,Java并发编程是一个庞大而复杂的领域,需要不断的实践和深入学习才能掌握。希望对你有所帮助!如果你有更具体的问题,欢迎继续提问。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值