并发编程

什么是并发编程

并行:在同一个时间节点上,多个线程同时执行(是真正意义上的同时执行)

并发:一个时间段内,多个线程依次执行。

并发编程:在例如买票、抢购、秒杀等等场景下,有大量的请求访问同一个资源。会出现线程安全的问题,所以需要通过编程来解决多个线程依次访问资源,称为并发编程。

并发编程的根本原因:

  1. 多核cpu的出现,真正意义上可以做到并行执行
  2. java内存模型(JMM)

java内存模型,规范了Java虚拟机与计算机内存是如何协同工作的。

将内存分为主内存和工作内存。两个线程同时操作,会导致出错,本质原因在于内存模型设计。

共享数据存储在主内存中,每个线程都有各自的工作内存。操作共享数据时,会将主内存中的数据复制一份到工作内存中操作,操作完成后,再写回到主内存中。

但是一旦两个线程同时进行操作,读取共享数据,两个线程各自在工作内存中修改后,同时又写到主内存,这样就会与预期的结果不同。(AB两个线程同时操作变量n)

一、并发编程核心问题

由于java内存模型的设计,多线程操作一些共享的数据时,出现以下3个问题:

(1)不可见性:A线程在工作内存中操作共享数据时,B线程不知道A线程已经修改了数据。

(2)无序性:为了优化性能,有时候会改变程序中语句的先后顺序,以提高速度。

int a = 10;

io.read();//从其他地方读数据

int b = 5;

int c=a+b;

但是为了优化,第2行需要从其他地方读数据 需要时间;系统可能将3行代码乱序执行,例如 1、3、2的顺序执行。

有时,看似没有关系的代码乱序执行,可能会对后面的代码产生影响。

(3)非原子性

一个或多个操作在CPU执行的过程中不被中断的特性,我们称为原子性。 原子性是拒绝多线程交叉操作的,同一时刻只能有一个线程来对它进行操作**。**

高级语言里一条语句往往需要多条CPU指令完成。如 count++,至少需要三条CPU指令。

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

解决办法

  1. 让不可见变为可见
  2. 让无序变为有序
  3. 非原子执行变为原子(加锁),由于线程切换执行导致

缓存(工作内存) 带来了不可见性;指令重排优化带来了无序性;线程切换带来了非原子性。

volatile可以解决前两个问题,加锁可以解决所有问题。

二、volatile关键字

volatile修饰的共享变量(类的成员变量、类的静态成员变量),被一个线程修改后,可以同步更新到其他线程,让其他线程中立即可见。volatile修饰的共享变量,指令是有顺序的。

但是volatile不能解决原子性问题,原子性问题由于线程切换执行导致。

volatile底层实现原理:

使用内存屏障(指令)进行控制。

  • 有序性实现 :volatile修饰的变量,在操作前添加内存屏障,来禁止指令重排序。
  • 可见性实现 :volatile修饰的变量添加内存屏障 之外,还通过缓存一致性协议(MESI)将数据写回到主内存,其他工作内存嗅探后,如果自己工作内存中的数据过期,重新从主内存读取最新的数据。

三、如何保证原子性

同一时刻只有一个线程执行,称之为互斥。如果我们能够保证对共享变量的修改是互斥的,那么就能保证原子性了。

1、锁

只有通过加锁的方式,让线程互斥执行,来保证一次只有一个线程对共享资源进行访问

synchronized:关键字;修饰代码块、方法;自动获取锁,自动释放锁

ReentrantLock:类;只能对某段代码修饰;需要手动加锁,手动释放锁

2、原子变量

在java中还提供一些原子类,在低并发情况下使用,是一种无锁实现。

JUC(java.util.concurrent包)中,里面的locks包和atomic包,它们可以解决原子性问题。

1.原子类原理(AtomicInteger 为例)

原子类的原子性是通过volatile+CAS实现原子操作的。

低并发情况下:使用原子类 AtomicInteger,底层有一个变量通过volatile关键字修饰的,结合CAS机制实现。

2.CAS(重点)

采用CAS机制(Compare-And-Swap比较并交换),是一种无锁实现,在低并发情况下使用。CAS是乐观锁的方式,采用的是自旋的思想。

采用自旋思想:

(1)第一次从内存中读到内存值V

(2)对数据进行修改,将改变后的值写入到内存时,需要重新读取内存中最新的值,作为预期值A

(3)在写入前比较预期值与内存值,看是否一致:

  • 如果一致,说明其他线程没有修改内存中的值,将更新后的值,写入到内存;
  • 如果不一致,说明其他线程修改了主内存中的值,就需要重新计算变量值,反复这一过程。--->自旋

优点:

  • 不加锁,所有的线程都可以对共享数据操作;
  • 适合低并发使用,因为所有线程不会进入阻塞状态

缺点:

  • 大并发时,不停自旋判断,导致cpu占用率高
3.ABA问题

ABA问题,即线程1读取到内存值,线程2将内存值由A改为了B,再由B改为了A。当线程1去判断时,预期值与内存值相同,无法分辨内存值是否发生过变化。

通过设置版本号,每次操作改变版本号**,** 来避免ABA问题。如原先的内存值为(A,1),线程修改为(B,2),再修改为(A,3)。此时另一个线程使用预期值(A,1)与内存值(A,3)进行比较,只需要比较版本号1和3,即可发现该主内存中的数据被更新过了。

四、java中的锁

一些锁的名称指的是锁的特性、设计、状态,并不是都是锁。

1、乐观锁/悲观锁

乐观锁 :没有加锁,不加锁的方式是没有问题的。例如CAS机制

悲观锁 :必须加锁。悲观的认为,不加锁的并发操作一定会出问题。

2、可重入锁

synchronized和ReentrantLock是可重入锁,可以避免死锁。

A方法和B方法是两个同步方法,在同一个类中,用同一把锁,先进入到同步方法A中,锁被使用,在方法A调用方法B依然可以进入到方法B。(此时方法A还没有释放锁)

如果不是可重入锁的话,方法B不会被当前线程执行。

3、读写锁

ReentrantReadWriteLock,里面有一个读锁和写锁。

  • 读读不互斥:只有读没有写,可以多个线程同时读
  • 读写互斥:一旦有写操作,读写不同同时进行。
  • 写写互斥:多个写互斥
4、分段锁

不是锁,是一种锁实现思想:用于将数据分段,并在每个分段上都会单独加锁,以提高并发效率。

举例:Hashtable是将整合hash表格锁住了,一次只能有一个线程操作并发量低,效率低。

ConcurrentHashMap将每个哈希位置当做一个锁,可以有多个线程对map进行操作,一次只能有一个线程操作一个位置.

5、自旋锁

不是锁。是自己重试,当线程抢锁失败后,重试几次,如果抢到锁了就继续,如果抢不到就阻塞线程。

6、共享锁/独占锁

共享锁:一个锁可被多个线程共享,例如读写锁中的 读锁。

独占锁:一次只能有一个线程操作。例如:Synchronized、ReentrantLock,读写锁中的 写锁。

7、公平锁/非公平锁

公平锁:按照请求的顺序执行(排队,先来来执行)。

非公平锁:不按照请求顺序执行,谁先抢到谁先执行。

synchronized是一种非公平锁。ReentrantLock默认是非公平锁,但是底层可以通过AQS来实现线程调度,使其变成公平锁。

五、synchronized锁

1、锁的状态

synchronized 锁的底层实现中,提供4种锁的状态 ,又来区别对待。(锁的状态在同步锁对象的对象头 中,有一个区域叫Mark Word中存储)

  1. 无锁状态:没有线程进入。
  2. 偏向锁:始终只有一个线程访问同步代码快,记录线程的编号,快速的获取锁。
  3. 轻量级锁 :当锁状态为偏向锁时,还有其他线程访问,此时升级为轻量级锁。特点:当一个线程获取锁之后,其他线程不会阻塞,会通过自旋方式获取锁,提高效率。
  4. 重量级锁:当锁的状态为轻量级锁时,线程自旋达到一定的次数,还没有获取到锁,就会进入到阻塞状态,锁状态升级为重量级锁,等待操作系统调度。

2、对象结构

在Hotspot虚拟机中,对象在内存中分为三块区域:对象头、实例数据和对齐填充;synchronized使用的锁对象是存储在对象头里。

对象头中有一块为Mark Word,用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程ID等等。

32位操作系统Mark Word为32bit,64 位操作系统Mark Word为64bit。下面就是对象头的一些信息:

3、synchronized锁实现

synchronized锁是依赖底层编译后的指令,添加锁的监视器实现,需要我们提供一个同步对象,来记录是否加锁、以及锁的状态。

六、AQS

全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面。抽象同步队列,是java代码实现线程同步非常重要的一个底层实现类。

思路:

  • 在类中定义了一个state变量(初始化为0,表示有没有线程访问共享资源)和一个双向链表队列(head结点代表当前占用的线程)。
  • 有线程访问时,第一个抢到执行权的线程放在头节点,将state加1。期间如果有其他的线程访问时,如果state=1,将其他线程添加到队列中,等待锁的释放。

state 由于是多线程共享变量,所以定义成volatile,以保证state的可见性,但不能保证原子性,所以AQS提供了对state的原子操作方法,保证了线程安全。

队列由Node对象组成,Node是AQS中的内部类。

AQS 的锁模式分为:独占和共享

独占锁:每次只能有一个线程持有锁,比如ReentrantLock是以独占方式实现的。

共享锁:允许多个线程同时获取锁,并发访问共享资源,比如ReentrantReadWriteLock

ReentrantLock锁实现

ReentrantLock是java.util.concurrent.locks包下的类,实现Lock接口。

java 复制代码
public class ReentrantLock implements Lock, java.io.Serializable{ }

ReentrantLock基于AQS,在并发编程中可以实现公平锁和非公平锁来对共享资源进行同步。ReentrantLock类内部总共存在Sync、NonfairSync、FairSync三个,NonfairSync与FairSync类继承自Sync类,Sync类继承自AbstractQueuedSynchronizer抽象类。

ReentrantLock构造方法

  • 无参构造方法默认是非公平实现
  • 有参构造方法可以选择,true---公平实现,false---非公平实现

NonfairSync类继承了Sync类,表示采用非公平策略获取锁,其实现了Sync类中抽象的lock方法。

java 复制代码
static final class NonfairSync extends Sync {
//若通过 CAS 设置变量 state 成功,就是获取锁成功,则将当前线程设置为独占线程。
//若通过 CAS 设置变量 state 失败,就是获取锁失败,则进入 acquire 方法进行后续处理。
    final void lock() {
        if (compareAndSetState(0, 1))//每个线程进入到lock方法时,会尝试获取锁,有可能获取到了
            setExclusiveOwnerThread(Thread.currentThread());
        else//获取不到,将线程添加到队列中,排队获取锁
            acquire(1);
    }
	//尝试获取锁,无论是否获得都立即返回
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

FairSync类也继承了Sync类,表示采用公平策略获取锁,其实现了Sync类中的抽象lock方法。

java 复制代码
static final class FairSync extends Sync {
    final void lock() {//公平锁,默认排队获取锁
        acquire(1);
    }
}

七、JUC常用类

1、ConcurrentHashMap

HashMap是线程不安全的,不能在多线程环境下使用

Hashtable是线程安全的,但是synchronized直接锁住的是整个方法,效率低(public synchronized V put(K key,V value{}))

ConcurrentHashMap是线程安全的,效率高于Hashtable。

不像Hashtable将整个方法锁起来,将每个位置的第一个节点当做锁对象,将锁的力度减小,进而提高了效率;同时可以有多个线程对ConcurrentHashMap进行操作,如果多个线程操作的是同一个位置,那么必须等待,因为用的是同一把锁。当算出的位置,第一个节点为null时,采用CAS机制添加。

Hashtable和ConcurrentHashMap不支持存储null键和null值。源码中看到为null,就报空指针异常。为什么这样设计呢?

为了消除歧义,因为无法分辨key的值为null还是key不存在返回的null,这在多线程里面是模糊不清的,所以压根就不让 put null。

2、CopyOnWriteArrayList

ArraayList是线程不安全的,在高并发情况下可能会出现问题;

Vector是线程安全的,get、add方法都加锁,读读都互斥,效率低。

CopyOnWriteArrayList在读的时候不加锁,写入也不会阻塞读取操作,只有同时写入和写入之间需要进行同步等待,提高了读的效率。

CopyOnWriteArrayList在进行add、set等修改操作时,是通过底层数组的副本实现的。先将底层数组进行复制,修改复制出来的数组,修改后将数据赋值给原来的底层数组。写入时,不影响其他线程读

3、CopyOnWriteArraySet

CopyOnWriteArraySet线程安全的,底层使用的是CopyOnWriteArrayList不能存储重复数据

4、辅助类 CountDownLatch

CountDownLatch允许一个线程 等待其他线程各自执行完毕后再执行。底层实现是通AQS来完成的,创建CountDownLatch对象时指定一个初始值(线程的数量)。每当一个线程执行完毕后,AQS内部的state就-1,当state的值为0时,表示所有线程都执行完毕,然后等待的线程就可以恢复工作了。

八、对象引用

在JDK1.2版之后,Java对引用的概念进行了扩充,将引用分为:

  • 强引用
  • 软引用(SoftReference)
  • 弱引用(WeakReference)
  • 虚引用(PhantomReference)

这4种引用强度依次逐渐减弱。除强引用外,其他3种引用均可以在java.lang.ref包中找到它们的身影。

1、强引用(不是垃圾)

有引用指向该对象,Object obj = new Object(); 这种情况下new出来的对象不能被垃圾回收的。

软引用、弱引用、虚引用都是用来标记对象的一种状态。当一些对象称为垃圾后,通过不同的状态来判断什么时候被清理。可以继承SoftReference、WeakReference、PhantomReference或者把自己的对象添加到软、弱、虚的对象中。

2、软引用(内存不足时回收)

被软引用关联的对象,被判定为垃圾时,可以不用立即回收;直到垃圾回收后内存仍然不够用时,才会回收软引用关联的对象。

java 复制代码
Object obj = new Object();// 声明强引用
SoftReference<Object> sf = new SoftReference<>(obj);
obj = null; //销毁强引用

3、弱引用(发现时回收)

弱引用管理的对象,只能存活到下一次垃圾回收。

4、虚引用(对象回收跟踪)

最弱的引用,对对象的生命周期没有任何的影响,跟踪对象是否被回收(如果对象被回收后,会给队列返回信息)

java 复制代码
Object obj = new Object();
ReferenceQueue phantomQueue = new ReferenceQueue();//声明引用队列
PhantomReference<Object> sf = new PhantomReference<>(obj,phantomQueue);//声明虚引用(还需要传入引用队列),如果对象被回收后,会给队列返回信息
obj = null;

九、线程池

1、池的概念

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,频繁创建线程和销毁线程需要时间。 可以事先创建出一些连接对象,每次使用时,从集合中直接获取,用完不销毁。减少频繁创建、销毁。

在 JDK5 版本中增加了内置线程池实现 ThreadPoolExecutor,同时提供了Executors来创建不同类型的线程池。

池的好处:减少频繁创建销毁时间,统一管理线程,提高速度。

2、ThreadPoolExecutor类

Java.uitl.concurrent.ThreadPoolExecutor类是线程池中最核心的一个类,因此如果要透彻地了解Java中的线程池,必须先了解这个类。

ThreadPoolExecutor继承了AbstractExecutorService类,并提供了四个构造器,但是前三个构造器都是调用的第四个构造器进行的初始化工作。

3、构造器中各个参数的含义

1.corePoolSize

核心池的大小,一旦创建不会被销毁的;非核心池中的线程,在没有被使用时,可以被回收。

2.maximumPoolSize

线程池最大线程数量,包含核心池中的数量。

3.keepAliveTime

非核心线程池中的线程,在不被使用后,多久就终止。(假如核心线程池5个,最大数量10,但是任务少的情况下,核心线程池够用了,等多长时间,就把非核心线程池中的线程终止)

4.unit

为keepAliveTime设置时间单位,有7种取值。

5.workQueue

一个阻塞队列,用来存储执行的任务。有以下工作队列:

  1. **ArrayBlockingQueue:**数组实现的有界阻塞队列,创建时必须设置长度,按FIFO排序。
  2. **LinkedBlockingQueue:**链表结构的阻塞队列,按FIFO排序任务,容量可以选择进行设置,不设置是一个最大长度为 Integer.MAX_VALUE;
6.threadFactory

创建线程的工厂

7.handler

拒绝策略。当线程池中的核心池、阻塞队列、非核心池已满时,如果有任务继续到达,如何执行。有以下四种拒绝策略:

  1. AbortPolicy();直接抛出异常,拒绝执行。
  2. CallerRunsPolicy();交由当前提交任务的线程执行(如果任务被拒绝了,则由提交任务的线程(例如:main)直接执行此任务)
  3. DiscardOldestPolicy();丢弃等待时间最长的任务。
  4. DiscardPolicy();直接丢弃,不执行。

4、线程池的执行

创建完成ThreadPoolExecutor之后,当向线程池提交任务时,通常使用execute方法。 execute方法的执行流程图如下:

当请求到来时,如果核心线程池没有满,就提交到核心线程池,如果核心线程池已满,则添加到队列中(前提是队列没有满);如果队列中已满,则在非核心线程中创建线程,直到到达最大线程数量;如果非核心线程池也已经满了,那么则使用适当的拒绝策略处理。

execute与submit的区别

  • execute() 提交任务,没有返回值
  • submit() 提交任务,可以有返回值(任务需要实现callable接口)

关闭线程池

  • shutdownNow() 直接关闭,对还未开始执行的任务全部取消
  • shutdown() 等待任务执行完关闭
java 复制代码
//任务
public class MyTask implements Runnable {
    private int taskNum;

    public MyTask(int num) {
        this.taskNum = num;
    }

    @Override
    public void run() {
        try {
            Thread.currentThread().sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+":task "+taskNum+"执行完毕");
    }
}
java 复制代码
public class Test {
    public static void main(String[] args) {
        //创建线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(2,
                                     5, 200,
                                     TimeUnit.MILLISECONDS,
                                     new ArrayBlockingQueue<>(2),
                                     Executors.defaultThreadFactory(),
                                     new ThreadPoolExecutor.CallerRunsPolicy());
        executor.prestartAllCoreThreads();

        for(int i=1;i<=8;i++){
            MyTask myTask = new MyTask(i);
            executor.execute(myTask);//添加任务到线程池
           //Future<?> submit = executor.submit(myTask);
                        //submit.get();//返回值
        }
        executor.shutdown();
    }
}

十、ThreadLocal

本地线程变量,可以为每个线程都创建一个属于自己的变量副本,使得多个线程之间隔离,不影响。(在每一个线程里都有一个自己的localNum)

java 复制代码
package com.ffyc.javapro.thread.threadlocal;

public class ThreadLocalDemo {

    //创建一个ThreadLocal对象,复制保用来为每个线程会存一份变量,实现线程封闭
    private  static ThreadLocal<Integer> localNum = new ThreadLocal<Integer>(){
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    public static void main(String[] args) {
          new Thread(){
              @Override
              public void run() {
                   localNum.set(1);
                  try {
                      Thread.sleep(2000);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  localNum.set(localNum.get()+10);
                  System.out.println(Thread.currentThread().getName()+":"+localNum.get());//11
              }
          }.start();

        new Thread(){
            @Override
            public void run() {
                localNum.set(3);
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                localNum.set(localNum.get()+20);
                System.out.println(Thread.currentThread().getName()+":"+localNum.get());//23
            }
        }.start();
        System.out.println(Thread.currentThread().getName()+":"+localNum.get());//0(main线程)
    }
}

ThreadLocal底层实现:

在一个线程中使用ThreadLocal时,为每个当前线程创建了一个ThreadLocalMap,看似用唯一的ThreadLocal对象作为键,其实每个线程中都有一个属于自己的ThreadLocalMap,所以每个线程中都有一个自己的变量副本。

ThreadLocal会造成内存泄漏:

由于ThreadLocal被弱引用关联,有可能在下一次垃圾回收时被回收掉,会导致key为null,而value还存在着强引用。但是value却被Entry对象关联,Entry又被ThreadLocalMap关联,ThreadLocalMap又被Thread关联,要是当前线程长期不结束,value就不能被销毁,但是key有可能已被回收,就获取不到value造成内存泄漏。

正确的使用:不再使用这个本地线程变量后,将其主动删除掉,调用remove方法删除。

相关推荐
Swift社区1 小时前
在 Swift 中实现字符串分割问题:以字典中的单词构造句子
开发语言·ios·swift
没头脑的ht1 小时前
Swift内存访问冲突
开发语言·ios·swift
没头脑的ht1 小时前
Swift闭包的本质
开发语言·ios·swift
wjs20241 小时前
Swift 数组
开发语言
吾日三省吾码2 小时前
JVM 性能调优
java
stm 学习ing2 小时前
FPGA 第十讲 避免latch的产生
c语言·开发语言·单片机·嵌入式硬件·fpga开发·fpga
湫ccc3 小时前
《Python基础》之字符串格式化输出
开发语言·python
弗拉唐3 小时前
springBoot,mp,ssm整合案例
java·spring boot·mybatis
oi774 小时前
使用itextpdf进行pdf模版填充中文文本时部分字不显示问题
java·服务器