Java高并发-学习笔记 上

并行与并发

  • 单核 cpu 下,线程实际是串行执行的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片,分给不同的线程使用,只是由于 cpu 在线程间(时间片很短)的切换非常快,人类感觉是同时运行的。 总结为一句话就是:微观串行,宏观并行,一般会将这种线程轮流使用 cpu 的做法称为并发(concurrent)
    在这里插入图片描述

  • 多核 cpu 下,每个核(core)都可以调度运行线程,这时候线程可以是
    在这里插入图片描述
    如何理解并发?
    在这里插入图片描述

  • 大家排队在一个咖啡机上接咖啡,交替执行,是并发;两台咖啡机上面接咖啡, 是并行

  • 从严格意义上来说,并行的多任务是真的同时执行,而对于并发来说,这个过程只是交替执行的,一会执行任务 A,一会执行任务 B,系统会不停地在两者之间切 换。

  • 并发说的是在一个时间段内,多件事情在这个时间段内交替执行

  • 并行说的是多件事情在同一个时刻同事发生

多线程

Java 是最先支持多线程的开发的语言之一,Java 从一开始就支持了多线程 能力。由于现在的 CPU 已经多是多核处理器了,是可以同时执行多个线程的.

多线程优点

多线程技术使程序的响应速度更快 ,可以在进行其它工作的同时一直处于活动 状态,程序性能得到提升. 性能提升的本质 就是榨取硬件的剩余价值(硬件利用率).

多线程带来的问题?

  • 安全性(访问共享变量)
  • 性能(切换开销等)

JMM(Java内存模型)

硬件设备之间的速度差异带来的问题?

  • 硬件的发展中,一直存在一个矛盾,CPU、内存、I/O 设备的速度差异。
  • 速度排序:CPU > 内存 > I/O 设备

为了平衡这三者的速度差异,做了如下优化:

  • CPU 增加了缓存,以均衡内存与 CPU 的速度差异;
  • 操作系统以线程分时复用 CPU,进而均衡 I/O 设备与 CPU 的速度差异;
  • 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。

JMM

Java 内存模型(Java Memory Model,JMM)规范了 Java 虚拟机与计算 机内存是如何协同工作的。Java 虚拟机是一个完整的计算机的一个模型,因此这个模型自然也包含一个内存模型——又称为 Java 内存模型。
注意 : 是java内存模型 不是 JVM模型
Java 内存模型,用于屏蔽掉各种硬件和操作系统的内存访问差异,以实现 让 Java 程序在各种平台下都能达到一致的并发效果,JMM 规范了 Java 虚拟机 与计算机内存是如何协同工作,规定了一个线程如何以及何时可以看到由其他线 程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量

计算机在高速的 CPU 和相对低速的存储设备之间使用高速缓存,作为内存 和处理器之间的缓冲。将运算需要使用到的数据复制到缓存中,让运算能快速运 行,当运算结束后再从缓存同步回内存之中。

多处理器的系统中(或者单处理器多核的系统),每个处理器内核都有自己 的高速缓存,它们有共享同一主内存(Main Memory)。

当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致

JVM 主内存与工作内存

Java 内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量(线程共享的变量)存储到内存和从内存中取出变量这样底层细节。

JMM中规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。

这里的工作内存是 JMM 的一个抽象概念,也叫本地内存,其存储了该线程以 读 / 写共享变量的副本。

就像每个处理器内核拥有私有的高速缓存,JMM 中每个线程拥有私有的本地内存。

不同线程之间无法直接访问对方工作内存中的变量,线程间的通信一般有两种方式进行,一是通过消息传递,二是共享内存Java 线程间的通信采用的是共享内存方式,线程、主内存和工作内存的交互关系如下图所示:
在这里插入图片描述
若将JVM内存区域与JMM主内存工作内存勉强对应起来,那从变量、主内存、工作内存的定义来看,主内存主要对应于 Java 堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域

并发编程核心问题–可见性,原子性,有序性

1.可见性

缓存不能及时刷新导致了可见性问题。
一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为可见性。

对于如今的多核处理器,每颗 CPU 都有自己的缓存,而缓存仅仅对它所在的处理器可见,CPU 缓存与内存的数据不容易保证一致

为了避免处理器停顿下来等待向内存写入数据而产生的延迟,处理器使用写缓冲区来临时保存向内存写入的数据。写缓冲区合并对同一内存地址的多次写, 并以批处理的方式刷新,也就是说写缓冲区不会即时将数据刷新到主内存中

缓存不能及时刷新导致了可见性问题。

举例:
在这里插入图片描述

假设线程 1 和线程 2 同时开始执行,那么第一次都会将 a=0 读到各自的 CPU 缓存里,线程 1 执行 a++之后 a=1,但是此时线程 2 是看不到线程 1 中 a 的值的,所以线程 2 里 a=0,执行 a++a=1

线程 1 和线程 2 各自 CPU 缓存里的值都是 1,之后线程 1 和线程 2 都会将 自己缓存中的 a=1 写入内存,导致内存中 a=1,而不是我们期望的 2

2.有序性

编译优化带来了有序性问题

有序性指的是程序按照代码的先后顺序执行
编译器为了优化性能,有时候会改变程序中语句的先后顺序
在这里插入图片描述

3.原子性

原子的意思代表着——“不可分”;

一个或多个操作在 CPU 执行的过程中不被中断的特性,我们称为原子性

原子性是拒绝多线程交叉操作的,不论是多核还是单核,具有原子性的量,同一 时刻只能有一个线程来对它进行操作.

CPU 能保证的原子操作是 CPU 指令级别的,而不是高级语言的操作符(+、-、*、/、++等)。线程切换导致了原子性问题

在这里插入图片描述
Java 并发程序都是基于多线程的,自然也会涉及到任务切换,任务切换的时机大多数是在时间片结束的时候。我们现在基本都使用高级语言编程,高级语言里一条语句往往需要多条 CPU 指令完成
count++,至少需要三条 CPU 指令

  • 指令1:首先,需要把变量 count 从内存加载到工作内存;
  • 指令 2:之后,在工作内存执行 +1 操作;
  • 指令 3:最后,将结果写入内存;

线程切换带来的原子性问题


如上图,两个线程 A 和 B 同时执行 count++, 即便 count 使用 volatile 修饰,我们预期的结果值是 2,但实际可能是 1。

如何保证可见性和有序性?

volatile关键字

一个共享变量被volatile修饰后

  • 一个线程对volatile变量修改后,结果对其他线程是立即可见的(保证了可见性)
  • 禁止进行指令重排序(保证了有序性)

线程写入volatile变量值时 等效于 线程退出sync代码块(将写入工作内存的变量值同步到主内存)
线程读取volatile变量值时 等效于 线程进入sync代码块(先清空本地内存变量值,再从主内存获取最新值)

使用volatile的场景
  • 修改变量值不依赖当前值时,如果依赖当前值,也就是类似i++这种操作
    i++的操作可以分为三步,不是原子性的
  1. 获取:从主存中读取i的值
  2. 进行+1操作
  3. 将更新的值写入主存
  • 读写变量时没有加锁

如何保证原子性?

保证原子性需要保证对共享变量的修改是互斥的

  • 加锁是阻塞式实现
  • 原子变量是非阻塞式实现

加锁

synchronized关键字

  • 是独占锁/互斥锁,其他线程要获取锁只能等待
  • 保证原子性.synchronized同步方法or代码块时,只能有一个线程可以执行该代码
  • synchronized也可以保证可见性和有序性

原子变量

JUC中的atomic和locks包下的类,可以解决原子性问题

  • java.util.concurrent.atomic
  • java.util.concurrent.locks

在这里插入图片描述

原子类

原子类的原子性是通过 volatile + cas 实现原子操作的
如:AtomicInteger类中的value是volatile修饰的,保证了可见性和有序性,cas需要用到可见性

低并发下可以使用原子类

为什么会有原子类?

在单线程中,经常有类似i++++i的操作,那么它是不是线程安全的?
写一段代码来测试

public class Atomic {

    static int num=0;

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(){
                @Override
                public void run() {
                   System.out.println(this.getName()+":"+(++num));
                }
            }.start();
        }
    }
}

循环10次,每次++,预期结果应该是10,计算的结果却是9,得出结论i++,++i这种运算不是原子性的
在这里插入图片描述

使用原子类AtomicInteger
public class Atomic {

    private  static AtomicInteger atomicInteger = new AtomicInteger(0);

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(){
                @Override
                public void run() {
                   System.out.println(this.getName()+":"+atomicInteger.incrementAndGet());
                }
            }.start();
        }
    }
}

在这里插入图片描述
使用了原子类,得到的结果是正确的,内部是线程安全的

总结:

  • 缓存刷新不及时导致了可见性问题(volatile、synchronized、原子类可以解决)
  • 编译优化,指令重排,导致了有序性问题(volatile、加锁可以解决)
  • 线程切换导致原子性问题(加锁、原子变量)

CAS

Compare-And-Swap比较并交换,硬件对并发的支持

自旋:不断的循环尝试,对cas这里极速不断尝试判断内存值是否已经被更新过

CAS 是一种乐观锁(不加锁),采用自旋的思想,轻量级锁。不会阻塞线程

  • 内存值 V
    第一次从内存中读到的值
  • 预估值 A
    (对读到的值进行操作,准备将操作结果写入前) 读到的内存值
  • 更新值 B
    操作后的值

比较第一次读入的值和写入前读入的值是否相等,相等就 V = B
不相等,就继续循环,不会阻塞线程

CAS 的 ABA问题

  1. 一个线程读到的内存值为A
  2. 在写入前,被其他线程将A修改为了B,又将B修改为了A
  3. 此时该线程要将结果写入内存值,第二次读入的值也是A,无法确定是否被其他线程修改过
public static void main(String[] args) throws InterruptedException {
    AtomicInteger atomicInteger = new AtomicInteger(100);//默认值为100
    new Thread(() -> {
        System.out.println(atomicInteger.compareAndSet(100, 101));//设置预期值是100  修改值为101   true
        System.out.println(atomicInteger.get());// 101   
        System.out.println(atomicInteger.compareAndSet(101, 100));;//设置预期值是101  修改值为100   true
        System.out.println(atomicInteger.get());//  100
    }).start();
    Thread.sleep(1000);
    new Thread(() -> {
        System.out.println(atomicInteger.compareAndSet(100, 101));//返回true 说明修改成功  发生ABA问题  true
        System.out.println(atomicInteger.get());// 101
    }).start();
}

ABA 解决

给当前使用的类添加版本号

  1. 读入(A,1)
  2. 其他线程:(A,1)->(B,2); (B,2)->A(A,3)
  3. 写入前读入的(A,3)与内存中(A,1)不相同,不执行更新
public static void main(String[] args) throws InterruptedException {
	AtomicStampedReference stampedReference = new AtomicStampedReference(100, 0);
	new Thread(() -> {
	   try {
	       Thread.sleep(50);
	   } catch (InterruptedException e) {
	       e.printStackTrace();
	   }
	   stampedReference.compareAndSet(100,101,stampedReference.getStamp(),stampedReference.getStamp() + 1);
	   stampedReference.compareAndSet(101,100,stampedReference.getStamp(), stampedReference.getStamp() + 1);
	}).start();
	
	new Thread(() -> {
	   int stamp = stampedReference.getStamp();
	   try {
	       Thread.sleep(2000);
	   } catch (InterruptedException e) {
	       e.printStackTrace();
	   }
	   boolean result = stampedReference.compareAndSet(100, 101, stamp, stamp + 1);
	   System.out.println(String.format("  >>> 修改 stampedReference :: %s ", result));
	}).start();
}

在这里插入图片描述

锁的分类

乐观锁、悲观锁

乐观锁

  • 对并发情况持乐观态度,不加锁。
  • 采用CAS思想自旋,不断去尝试。
  • java.util.concurrent.automic包下的原子类采用了CSA的自旋思想实现原子操作的更新
  • 适合并发读取

悲观锁

  • 对并发情况持悲观的态度,加锁
  • 适合并发写入

独占锁、共享锁

独占锁

  • 独占锁一次只能被一个线程获得
  • synchronized、ReentrantLock、 ReentrantReadWriteLock.WriteLock 都是独占锁

共享锁

  • 共享锁可以被多个线程获得,并发地访问共享资源
  • ReentrantReadWriteLock.ReadLock 是共享锁

公平锁、非公平锁

公平锁

  • 按请求锁的顺序来分配,ReentrantLock是公平锁实现,维护了一个队列,按请求锁的顺序排队
  • 性能比非公平锁低

非公平锁

  • 由线程自己去抢锁,不按请求顺序排队。
  • synchronized是非公平锁
  • ReentrantLock默认是非公平锁,可以通过AQS方式实现线程调度,变成公平锁
/**
 * ReentrantLock默认非公平
 */
public ReentrantLock() {
    sync = new NonfairSync();
}

/**
 * 传入参数true 指定为 公平锁
 * 传入参数false指定为 非公平锁
 */
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

可重入锁

  • 可以重新进入的锁,避免了线程死锁
  • 也叫“递归锁”,当一个线程进入外层方法获取锁时,方法内部调用了另一个同步方法,那么线程是可以进入的
  • synchronized和ReentrantLock都是可重入锁

读写锁

读锁:多个线程可以获得锁
写锁:只能有一个线程获得锁(同一时间,只能有一个线程可以写操作)

  • 写锁优先级大于读锁
  • 写写互斥、读写互斥

分段锁

在每个分段上加锁,将锁的粒度细化,提高并发效率(ConcurrentHashMap)
在这里插入图片描述

自旋锁

不会放弃CPU时间片,通过自旋不断尝试去获取锁,不会阻塞线程,比较消耗CPU

加锁时间短的场景适合使用自旋锁

synchronized几种锁状态

  1. 无锁

  2. 偏向锁:只有一个线程访问,该线程自动获取锁

  3. 轻量级锁:锁状态为偏向锁时,又有线程访问,锁升级为轻量级锁,其他线程会自旋尝试获取锁(自旋到一定次数就阻塞了),不会阻塞线程,提升了效率

  4. 重量级锁
    锁状态为轻量级锁时,其他线程自旋到一定次数,线程阻塞,锁升级为重量级锁
    获取不到锁的线程将阻塞,等待操作系统调度

AQS

java.util.concurrent.locks包下
AQS – AbstractQueuedSynchronizer抽象同步队列
AQS是JUC中实现线程安全的一个核心组件

  • 内部维护了一个锁状态:volatile 修饰(保证了可见性和有序性)的state,初始为0
  • 提供了对state的原子操作方法,保证了原子性
  • 维护了一个队列,保存等待获取锁的线程
  • 维护了一些获取锁、添加线程到等待队列、释放锁的方法

多个线程来访问,如果有一个线程访问到了state,将state + 1
其他线程不能访问了,加到等待队列中
该线程结束后,state变回0,队列中下一个等待获取锁的线程才能进来

ReentrantLock锁实现

public class ReentrantLock implements Lock, java.io.Serializable

核心结构–3个内部类

//继承AQS的同步类
abstract static class Sync extends AbstractQueuedSynchronizer{}
//公平锁
static final class FairSync extends Sync {}
//非公平锁
static final class NonfairSync extends Sync {}

lock()方法

public void lock() {
	//调用内部类Sync的lock()方法,
   sync.lock();
}

synchronized锁实现

JUC常用类

ConcurrentHashMap

JDK5 增加 多线程并发安全的HashMap(锁分段/独占锁)
JDK8 修改为 CAS + synchronized

ConcurrentHashMap不像HashTable一样对整个put方法加锁,而是将每一个位置(Node)作为一个独立空间对其加锁,锁粒度变小,提高了并发访问效率

ConcurrentHashMap如何保证线程安全?

  1. 在进入put方法后,通过hash值计算添加的位置
  2. 若位置上没有元素,采用cas机制判断,添加元素
  3. 若位置上有元素了,使用链表第一个Node作为锁标记的对象(使用synchronized)

HashMap、HashTable、ConcurrentHashMap

  • HashMap线程不安全,允许键和值为null

  • HashTable线程安全,在put方法上加锁了,并发下只能有一个线程进入put,不允许键和值为null

    在这里插入图片描述

  • ConcurrentHashMap线程安全,不允许键和值为null
    在这里插入图片描述

ConcurrentHashMap为什么不能存key和value为null?

  • 不能存key为null:不能判断key为null 还是key没找到
  • 不能存value为null:get()时不能判断是存入的value为空,还是map中没有改key-value的映射
public class ConcurrentHashMapDemo {
    /*
       HashMap是线程不安全的,不能并发操作的
       ConcurrentModificationException  并发修改异常   遍历集合,并删除集合中的数据

       Hashtable 是线程安全的 public synchronized V put(K key, V value)-->独占锁
            锁直接加到了put方法上,锁粒度比较大,效率比较低
            用在低并发情况下可以

       Map<String,Integer> map = Collections.synchronizedMap(new HashMap<>());
       ConcurrentHashMap
     */
    public static void main(String[] args) {

        Map<String, Integer> map = new ConcurrentHashMap<>();
        //Map<String,Integer> map = new HashMap<>();
        //Map<String,Integer> map = new Hashtable<>();
        //模拟多个线程对其操作
        for (int i = 0; i < 20; i++) {
            new Thread(
                    () -> {
                        map.put(Thread.currentThread().getName(), new Random().nextInt());
                        System.out.println(map);
                    }
            ).start();
        }
    }
}

CopyOnWriteArrayList

ArrayList是线程不安全的;
Vector是线程安全的;但是给add()和get()都加了锁,一个线程写的时候or读的时候,其他线程不能去读,效率低下

CopyOnWriteArrayList只有写-写时,才会阻塞,可以并发读取(读操作没有锁)

可变操作 add()、set()等修改数据的操作,会先创建一个底层数组的副本,将要添加或修改的数据写入副本,用副本替换底层数组
在这里插入图片描述

/**
 * ArrayList 并发情况下不能使用,不能同时有多个下次对其操作
 * Vector 是线程安全的  对 读/取 方法加了锁
 * 读和写用的同一把锁  <-- synchronized修饰非静态方法,默认同步对象是this
 * CopyOnWriteArrayList
 * 对 读 和 写 操作分离,读操作不用加锁,
 * 写操作加锁,写时不影响读操作
 * 多个线程同时添加时会互斥
 * 添加时先将原来的数组复制一个副本,将数据添加到副本中,不影响读操作,最后再用副本替换原数组
 */
public class CopyOnWriteArrayListDemo {
    public static void main(String[] args) {
        //List<Integer> list = new ArrayList();//java.util.ConcurrentModificationException
        List<Integer> list = Collections.synchronizedList(new ArrayList<Integer>());//转为线程安全的集合
        //List<Integer> list = new Vector();
        //List<Integer> list = new CopyOnWriteArrayList<>();
        for (int i = 0; i < 10; i++) {//模拟10个线程对其操作
            new Thread(
                    () -> {
                        list.add(new Random().nextInt());
                        System.out.println(list);
                    }
            ).start();
        }
    }
}

CopyOnWriteArraySet

底层用CopyOnWriteArrayList实现
在这里插入图片描述
在添加数据时,会控制不出现重复数据
在这里插入图片描述

/**
 * CopyOnWriteArraySet
 * 不允许重复数据出现的单列集合
 * 底层是CopyOnWriteArrayList实现
 * 添加时判断是否重复
 */
public class CopyOnWriteArraySetDemo {
    public static void main(String[] args) {
        Set<Integer> set = new CopyOnWriteArraySet<>();
        for (int i = 0; i < 10; i++) {
            new Thread(
                    () -> {
                        set.add(new Random().nextInt());
                        System.out.println(set);
                    }
            ).start();
        }
    }
}

CountDownLatch

  • 减法计数辅助类,允许一个线程等待其他线程执行完后再执行
  • 底层使用AQS实现
  1. new CountDownLatch(线程数量),指定的线程数量将会传给AQS中的state,每执行完一个线程,计数器-1–>AQS的state - 1
  2. state为0时,所有线程执行完了,关闭计数器,再去执行指定的线程
public class CountDownLatchDemo {
    /**
     * 辅助类
     * 使一个线程 等待其他线程执行完毕后再执行
     * 相当于一个程序计数器 是一个递减的计数器
     * 先指定一个(线程)数量,当有一个线程执行结束后,就减一 ,直到减为0 关闭计数器
     * 此时线程就可以执行了
     */
    public static void main(String[] args) throws InterruptedException {

        CountDownLatch countDownLatch = new CountDownLatch(10);
        for (int i = 0; i < 10; i++) {
            new Thread(
                    () -> {
                        System.out.println(Thread.currentThread().getName());
                        countDownLatch.countDown();//计数器减一操作
                    }
            ).start();
        }
        countDownLatch.await();//关闭计数器
        System.out.println("main线程");
    }
}

在这里插入图片描述
如果不使用辅助类CountDownLatch,线程将是随机执行的

CycliBarrier

  • 加法计数辅助类,让一组线程都到达某一屏障时,必须等最后一个线程到达屏障,才开始执行
    在这里插入图片描述
public class CyclicBarrierDemo {

    /*
       CyclicBarrier 让一组线程到达一个屏障时被阻塞,直到最 后一个线程到达屏障时,屏障才会开门
         是一个加法计数器,当线程数量到达指定数量时,开门放行
     */

    public static void main(String[] args) {
        CyclicBarrier c = new CyclicBarrier(5, () -> {
            System.out.println("大家都到齐了 该我执行了");
        });

        for (int i = 0; i < 5; i++) {
            new Thread(
                    () -> {
                        System.out.println(Thread.currentThread().getName());
                        try {
                            c.await();//加一计数器
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        } catch (BrokenBarrierException e) {
                            e.printStackTrace();
                        }
                    }
            ).start();
        }
    }
}

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值