8.并发

并发

一个CPU同一时刻只能执行一个进程,比如当你打开浏览器时,被覆盖掉的记事本就自动挂起;在你眼中是同时运行了两个程序,实际上是CPU在两个程序之间的切换速度相当的快,这个就是单核CPU工作的最基本原理(当然现在我们的电脑都是8核,16核)。

执行一个应用程序就是执行一个进程,而一个进程下面会有一个或多个线程,线程是一个进程中不同的执行路径。

那么进程和线程的区别是什么呢?每个进程都有独立的内存空间,而线程是共享数据

什么是线程

如何新建一个线程:

通过实现Runnable函数式接口,然后构造一个Thread对象,该对象调用一个start方法

Runnable r = () -> {/*代码块*/};
Thread t = new Thread(r);
t.start();

也可以自定义一个继承了Thread类的类,不过不提倡

线程状态

通过观察ThreadTest.java可以发现,当CPU执行两个线程时,两个线程交错输出,并不同步。由此我们引出线程的6种状态:

  • New(新建)
  • Runnable(可运行)
  • Blocked(阻塞)
  • Waiting(等待)
  • Timed Waiting(计时等待)
  • Terminated(终止)

新建线程

为了降低模块之间的耦合性:使用lambda表达式是最佳的方法:

new Thread(() -> {
    /* code block */
           }, "线程名");

可运行线程

一旦调用start方法,线程就处于可运行状态。

阻塞和等待线程

当线程处于阻塞或等待状态时,它暂时是不活动的。线程试图获取一个内部的对象锁,但该锁被其他线程占用,那么该线程就处于阻塞状态;当某些线程需要一定的条件才能运行时,那么该线程就处于等待状态;若该线程使用了一个含时间参数的方法,则是超时等待

终止线程

线程会由于以下两个原因之一而终止:

  • run方法正常退出,线程自然终止
  • 因为一个没有捕获的异常终止了run方法,使线程意外终止

线程属性

中断线程

interrupt方法可以请求终止一个线程,当对一个线程调用interrupt方法时,就会设置线程的中断状态。

while (!Thread.currentThread.isInterrupted() && ...) {
    /*代码块*/
}

但是当线程被阻塞时,就无法检查中断状态。这里需要引入InterruptedException异常

我们需要了解的是:当使用sleep方法时,一定要捕获该异常

守护线程

当只剩下守护线程时,程序会退出

线程名

可以直接在构造器中设置线程名

也可以使用setName()方法来设置

线程优先级

在Java程序中,每一个线程都有一个优先级。可以使用setPriority()方法来提高线程的优先级,常值位于MIN_PRIORITYMAX_PRIORITY之间

虽然会首先选择具有较高优先级的线程,但线程优先级依赖于系统,故并不能反映线程优先级

同步

多线程是对同一数据的存取,如果对每一个线程都实现了一个修改对象的方法,那么该对象就会被破坏。这种情况被称为竞态条件

竞态条件的一个例子

为了避免多线程破坏共享数据,需要对线程进行同步

例子就不打出来了,见书p566

锁对象

两种机制可以防止并发访问代码块:

  • synchronized关键字
  • 引入了ReentrantLock

当实现锁之后,如果一个线程实现了对象,若第二个线程也想要实现该对象,由于第二个线程不s能获得锁,那么第二个线程处于阻塞状态,当第一个线程释放锁之后,第二个线程才能开始运行。

条件对象

一般在程序设计中,线程需要满足某一个条件才能执行。可以使用条件对象来管理那些已经获得了一个锁却不能做有用工作的线程

举个栗子:

当一个账户(线程1)向另一个账户(线程2)转账时(转账就是调用一个方法),账面上的余额低于需要转出的余额,我们就需要等待账户3(线程3)向账户1转账。但是此时账户1获得了一个锁,这时账户3永远不能被激活。我们就引入了条件对象:

我们可以使用newCondition方法获得一个条件对象

Condition condition = new Condition();
condition.await();
condition.signalAll();

当满足条件时,条件对象调用await方法,暂停当前线程(线程1),并且放弃锁,允许账户3执行。线程1并不会因为线程3释放锁之后立即进入RUNNABLE状态,当线程3调用signalAll方法后,会激活线程1(并不是立即激活,而是解除线程1的阻塞)。如果没有其他线程调用signalAll方法,那么线程1就永远不会运行,进入了死锁(dead lock)状态;如果其他线程都被阻塞(只有线程3获得锁),线程3调用了await方法,但没有解除其他线程的阻塞,这是线程3也进入了阻塞状态,这时程序就被永远挂起。因此要注意调用signalAll方法的时机。

同步机制会付出额外的内存

synchronized关键字

synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:

1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对 象;
  2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
  3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
  4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。

总结一下锁和条件对象:

  • 锁用来保护代码片段,一次只能有一个线程执行被保护的代码
  • 锁可以管理试图进入被保护代码段的线程
  • 一个锁可以有一个或多个相关联的条件对象
  • 每个条件对象管理那些已经进入被保护代码段但还不能运行的线程

从**jdk1.0**开始,每个对象都有一个内部锁。如果一个方法声明时有synchronized关键字,那么对象的锁将会保护整个方法

内部对象锁只有一个关联条件。调用wait方法或notifyAll方法(这两个方法位于Object类)等价于调用await或者signalAll

同步块

Java除了调用同步方法获得锁,还可以进入一个同步块获得锁:

synchronized(object) {
    // protected code
}

监视器概念

JMM

Java内存模型,是一种虚构的概念

一些约定:

  1. 线程解锁前,必须把共享变量立刻刷新回主存
  2. 线程加锁前,必须读取主存中最新值到工作内存中
  3. 加锁和解锁必须是同一把锁

volatile字段

对于实例字段来说,使用synchronized关键字有点浪费,Java提供了volatile关键字为实例字段的同步访问提供了一种免锁机制:如果声明一个字段为volatile,那么编译器和虚拟机就知道该字段可能被另一个线程并发更新。

  1. 保证可见性
  2. 不保证原子性
  3. 禁止指令重排:使用了内存屏障

原子性

即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。

可以使用原子类(java.util.courrent)来处理原子性问题

常用的类

CountDownLatch

允许一个或者多个线程等待到某一线程完成一组操作的辅助类

CountDownLatch(int count)
// 构造一个用给定计数初始化的 CountDownLatch。

// 使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断。
void await()
// 使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断或超出了指定的等待时间。
boolean await(long timeout, TimeUnit unit)
// 递减锁存器的计数,如果计数到达零,则释放所有等待的线程。
void countDown()
// 返回当前计数。
long getCount()
// 返回标识此锁存器及其状态的字符串。
String toString()

每次有线程调用countDown()方法,计数器都会减1。

CyclicBarrier

CyclicBarrier是一个同步辅助类,允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。因为该 barrier 在释放等待线程后可以重用,所以称它为循环 的 barrier。

CyclicBarrier(int parties)
// 创建一个新的 CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,但它不会在启动 barrier 时执行预定义的操作。
CyclicBarrier(int parties, Runnable barrierAction)
// 创建一个新的 CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,并在启动 barrier 时执行给定的屏障操作,该操作由最后一个进入 barrier 的线程执行。

int await()
// 在所有参与者都已经在此 barrier 上调用 await 方法之前,将一直等待。
int await(long timeout, TimeUnit unit)
// 在所有参与者都已经在此屏障上调用 await 方法之前将一直等待,或者超出了指定的等待时间。
int getNumberWaiting()
// 返回当前在屏障处等待的参与者数目。
int getParties()
// 返回要求启动此 barrier 的参与者数目。
boolean isBroken()
// 查询此屏障是否处于损坏状态。
void reset()
// 将屏障重置为其初始状态。

Semaphore

定义:同一时间下只能有指定数量的线程,其他线程处于等待状态,只有某一线程释放时,才会处于可运行状态

作用:多个共享资源互斥使用,并发限流,控制最大线程数x

// 创建具有给定的许可数和非公平的公平设置的 Semaphore。
Semaphore(int permits)
// 创建具有给定的许可数和给定的公平设置的 Semaphore。
Semaphore(int permits, boolean fair)

// 从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断。
void acquire()
// 从此信号量获取给定数目的许可,在提供这些许可前一直将线程阻塞,或者线程已被中断。
void acquire(int permits)
// 从此信号量中获取许可,在有可用的许可前将其阻塞。
void acquireUninterruptibly()
// 从此信号量获取给定数目的许可,在提供这些许可前一直将线程阻塞。
void acquireUninterruptibly(int permits)
// 返回此信号量中当前可用的许可数。
int availablePermits()
// 获取并返回立即可用的所有许可。
int drainPermits()
// 返回一个 collection,包含可能等待获取的线程。
protected Collection<Thread> getQueuedThreads()
// 返回正在等待获取的线程的估计数目。
int getQueueLength()
// 查询是否有线程正在等待获取。
boolean hasQueuedThreads()
// 如果此信号量的公平设置为 true,则返回 true。
boolean isFair()
// 根据指定的缩减量减小可用许可的数目。
protected void reducePermits(int reduction)
// 释放一个许可,将其返回给信号量。
void release()
// 释放给定数目的许可,将其返回到信号量。
void release(int permits)
// 返回标识此信号量的字符串,以及信号量的状态。
String toString()
// 仅在调用时此信号量存在一个可用许可,才从信号量获取许可。
boolean tryAcquire()
// 仅在调用时此信号量中有给定数目的许可时,才从此信号量中获取这些许可。
boolean tryAcquire(int permits)
// 如果在给定的等待时间内此信号量有可用的所有许可,并且当前线程未被中断,则从此信号量获取给定数目的许可。
boolean tryAcquire(int permits, long timeout, TimeUnit unit)
// 如果在给定的等待时间内,此信号量有可用的许可并且当前线程未被中断,则从此信号量获取一个许可。
boolean tryAcquire(long timeout, TimeUnit unit)

读写锁

主要是使用了ReadWriteLock类。

在数据进行读和写进行原子性操作时,我们需要使读和读是共享的(多个线程可以同时读取一个数据),读和写是互斥的(在写的时候不能读,防止读到写的中间值),写和写是互斥的。

为什么需要读锁

既然我们读和读是共享的,为什么还需要读锁(ReadWriteLock.readLock())呢?为了防止读和写是互斥的。。。。

读锁也叫做共享锁

为什么需要写锁

当然是为了确保写操作的原子性,一个线程写入完毕后,另一个线程才会开始写入。

写锁也叫做排他锁

线程安全的集合

阻塞队列

FIFO原则:First In First Out

写入:当队列满了,就必须阻塞等待

读取:如果队列为空,必须阻塞生产

方式抛出异常返回布尔值,不抛出异常等待,一直阻塞超时等待
添加add(Object obj)offer(Object obj)put(Object)offer(Object obj, long timeout, TimeUnit timeunit)
移除remove()poll()take()poll(long timeout, TimeUnit timeunit)
查看队首元素element()peek()

同步队列:SynchronousQueue

没有容量,元素进入队列之后,必须等待它取出来之后,才能进入第二个元素

高效的映射、集和队列

java.util.concurrent包提供了线程安全的集合类:ConcurrentHashMap,CopyOnWriteArrayList,CopyOnWriteArraySet

用法和Collection接口下的类差不多

任务和线程池

构造一个新的线程开销有些大,因为这设计系统的交互。如果你的程序中创建了大量的生命期很短的线程,那么不应该把每个任务映射到一个单独的线程,而是应该使用线程池(thread pool)。线程池中包含许多准备运行的线程。为线程池提供一个Runnable,就会有一个线程调用run方法。当run方法退出时,这个线程不会死亡,而是留在池中为下一个请求提供服务

池化技术

程序的运行,本质:占用系统的资源,优化资源的使用

池化技术:一次创建,多次使用

线程池的优点:

  1. 降低资源的消耗
  2. 提升响应的速度
  3. 方便管理

CallableFuture

Runnable可以看作没有参数和返回值的异步方法,Callable可以看作有返回值的异步方法,返回一个异步计算结果。

Future保存异步计算的结果。

执行器

执行器类(Executors)有许多静态方法,用来构造线程池。但是在阿里Java开发手册中,明确规定不能用Executors类创建线程池

在这里插入图片描述

方法名描述
newSingleThreadExecutor只有一个线程的线程池
newFixedThreadPool线程池中包含固定数目的线程;空闲线程会一直保留
newCachedThreadPool必要时创建新线程
ExecutorService executorPool = Executors.newSingleThreadExecutor();

ThreadPoolExecutor

七个重要的参数

public ThreadPoolExecutor(int corePoolSize,// 核心线程池大小
                          int maximumPoolSize,// 最大核心线程池大小,最大核心线程池大小减去核心线程池大小就是非核心线程池大小
                          long keepAliveTime,// 超时了没有人调用就会释放
                          TimeUnit unit,// 超时单位
                          BlockingQueue<Runnable> workQueue// 阻塞队列
                          ThreadFactory threadFactory,// 线程工厂
                          RejectedExecutionHandler handler// 拒绝策略) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

自定义线程池

自定义线程池就是填入上述7个参数

4种拒绝策略

AbortPolicy         -- 当任务添加到线程池中被拒绝时,它将抛出 RejectedExecutionException 异常。
CallerRunsPolicy    -- 当任务添加到线程池中被拒绝时,会在线程池当前正在运行的Thread线程池中处理被拒绝的任务。
DiscardOldestPolicy -- 当任务添加到线程池中被拒绝时,线程池会放弃等待队列中最旧的未处理任务,然后将被拒绝的任务添加到等待队列中。
DiscardPolicy       -- 当任务添加到线程池中被拒绝时,线程池将丢弃被拒绝的任务。

线程池中的默认策略是AbortPolicy

最大线程数设置

  1. CPU 密集型 适合C语言多线程,主要是消耗在计算上
  2. IO 密集型 适合脚本语言开发多线程,CPU大部分在进行读写操作

fork-join框架

可以将一个处理任务分解为子任务,提高线程的效率。(分而治之的思想)

特点:工作密取。每个工作线程都有一个双端队列来完成任务。一个工作线程将子任务压入双端队列的对头。一个工作线程空闲时,它会从另一个双端队列的队尾“密取”一个任务。

典例:

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;
import java.util.concurrent.RecursiveTask;
import java.util.function.DoublePredicate;

public class ForkJoinTest {
    public static void main(String[] args) {
        final int SIZE = 10_000_000;
        double[] numbers = new double[SIZE];
        for (int i = 0; i < SIZE; i++) {
            numbers[i] = Math.random();
            Counter counter = new Counter(numbers, 0, numbers.length, x -> x > 0.5);// 函数式接口
            ForkJoinPool pool = new ForkJoinPool();
            pool.invoke(counter);
            System.out.println(counter.join());
        }
    }
}

class Counter extends RecursiveTask<Integer> {

    public static final int THRESHOLD = 1000;
    private double[] values;
    private int from;
    private int to;
    private DoublePredicate filter;

    public Counter(double[] values, int from, int to, DoublePredicate filter) {
        this.values = values;
        this.from = from;
        this.to = to;
        this.filter = filter;
    }

    @Override
    protected Integer compute() {
        if (to - from < THRESHOLD) {
            int count = 0;
            for (int i = from; i < to; i++) {
                if (filter.test(values[i])) {
                    count++;
                }
            }
            return count;
        } else {
            int mid = (to + from) >> 2;
            Counter first = new Counter(values, from, mid, filter);
            Counter second = new Counter(values, mid, to, filter);
            invokeAll(first, second);
            return first.join() + second.join();
        }
    }
}

异步计算

当一个线程阻塞时, 一直等待有点降低效率;我们可以在等待时间内运行其它线程

可完成Future(异步回调)

使用CompletableFuture个类实现了Future接口

//TODO
/**
  * 待补充
  */

CAS与ABA

CAS :Compare and Swap,即比较再交换。

jdk5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronized同步锁的一种乐观锁。JDK 5之前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁。

核心格式:

public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!weakCompareAndSetInt(o, offset, v, v + delta));
    return v;
}

比较当前工作内存中的值和主内存中的值:如果这个值是期望的,那么执行操作;如果不是,就一直循环

虽然CAS是CPU指令级的原子操作,但是仍然有一定的缺陷

  1. 循环会耗时
  2. 一次操作只能 保证一次原子性
  3. ABA(当一个线程将原值修改为新值后又修改回来的问题,就被称为ABA问题)

可以通过版本控制来解决ABA问题:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;

public class ABASolution {
    public static void main(String[] args) {
        AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1, 1);

        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println("线程A的版本号01:" + stamp);

            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            atomicStampedReference.compareAndSet(1,2,atomicStampedReference.getStamp(),atomicStampedReference.getStamp() + 1);
            System.out.println("线程A的版本号02:" + atomicStampedReference.getStamp());

            System.out.println(atomicStampedReference.compareAndSet(2, 1, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
            System.out.println("线程A的版本号03:" + atomicStampedReference.getStamp());
        }, "a").start();

        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println("线程B的版本号01:" + stamp);

            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(atomicStampedReference.compareAndSet(1, 5, stamp, stamp + 1));
            System.out.println("线程B的版本号02:" + atomicStampedReference.getStamp());

        },"b").start();
    }
}

常见的锁

非公平锁,公平锁

公平锁是严格按照CPU调度而决定获取锁的顺序,先执行的线程先获得锁,可能会导致效率降低

非公平锁则没有这个保障,具体的执行顺序看源码(比较复杂,以后再深度分析)

RenntrantLock锁重载了一个boolean参数的构造器来设置它是公平锁还是非公平锁

public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

乐观锁、悲观锁

  1. 悲观锁:当要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。这种借助数据库锁机制,在修改数据之前先锁定,再修改的方式被称之为悲观并发控制

    主要类型有共享锁排他锁。在前面读写锁部分有介绍不再赘述

  2. 乐观锁:乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做。乐观锁适用于读操作多的场景,这样可以提高程序的吞吐量。

    一般是通过版本号来控制的,在前面检测ABA问题就有所涉及

可重入锁(递归锁)

可重入锁有:synchronizedReentrantLock(这两者的之间的区别不再赘述)

主要要注意的是锁的对象是一样的。所以能够存在可重入对象

要注意的是上锁的个数和释放锁的个数要一致,不然会导致死锁现象

public class WhatReentrant {
	public static void main(String[] args) {
		ReentrantLock lock = new ReentrantLock();
		
		new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					lock.lock();
					System.out.println("第1次获取锁,这个锁是:" + lock);

					int index = 1;
					while (true) {
						try {
							lock.lock();
							System.out.println("第" + (++index) + "次获取锁,这个锁是:" + lock);
							
							try {
								Thread.sleep(new Random().nextInt(200));
							} catch (InterruptedException e) {
								e.printStackTrace();
							}
							
							if (index == 10) {
								break;
							}
						} finally {
							lock.unlock();
						}

					}

				} finally {
					lock.unlock();
				}
			}
		}).start();
	}
}

Thread(new Runnable() {
@Override
public void run() {
try {
lock.lock();
System.out.println(“第1次获取锁,这个锁是:” + lock);

				int index = 1;
				while (true) {
					try {
						lock.lock();
						System.out.println("第" + (++index) + "次获取锁,这个锁是:" + lock);
						
						try {
							Thread.sleep(new Random().nextInt(200));
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
						
						if (index == 10) {
							break;
						}
					} finally {
						lock.unlock();
					}

				}

			} finally {
				lock.unlock();
			}
		}
	}).start();
}

}


### 自旋锁
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值