并发编程

并发编程基础

Java线程模型

发生了系统调用的锁,就是重量锁

MMU: 虚拟地址映射

线程类型

用户线程:使用Java代码创建的线程。

内核线程:操作系统对应的线程

对应关系

一对一

一个用户线程对应一个内核线程

优点:简单,几乎所有对线程的操作都交给了内核线程。

缺点:

  1. 对用户线程的大部分操作会映射到内核线程上,引起用户态和内核态的频繁切换。

  2. 创建大量线程对系统性能有影响。

多对一

多个用户线程对应一个内核线程

优点:用户线程的很多操作对内核来说是透明的,不需要进行频繁的用户态和内核态的切换,线程的创建、调度、同步非常快。

缺点:

  1. 其中一个线程阻塞,其他用户线程也无法执行。

  2. 内核不知道用户有哪些线程,无法像内核线程一样实现完整的调度、优先级等。

多对多

用户态和内核态

linux系统虚拟地址映射

物理地址空间 :物理地址就是真实地址,对应真实的内存条。内存像一个数组,每个存储单元被分配了一个地址,就是物理地址。

虚拟地址空间 : 每个进程拥有一个巨大的连续内存空间,甚至比内存空间更大,这是一个“假象”。

CPU使用虚拟地址像内存寻址,通过内存管理单元MMU硬件,把虚拟地址转换成真实的物理地址。

CPU有四种不同的执行级别0-3,linux用0表示内核态,3表示用户态

切换方式

  • 系统调用 (关注重点)
  • 异常(不是Java中的异常)
  • 外围设备中断

CPU上下文

CPU寄存器和程序计数器就是CPU的上下文,都是CPU在运行任务前,必须的依赖环境。

CPU寄存器:CPU内置的容量小、速度极快的内存。

程序计数器:用来存储CPU正在执行的指令位置,或者即将执行的下一条指令位置。

CPU上下文切换

将前一个任务的CPU上下文保存,然后加载新任务的上下文到寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新的任务。

CPU上下文切换类型

进程上下文切换
  1. 系统调用:从用户态到内核态的转变,通过系统调用来完成。 属于同进程内的CPU上下文切换。一次系统调用过程,发生两次CPU上下文切换(用户态 —> 内核态 —> 内核态)

  2. 进程间上下文切换:进程的上下文不仅包括了虚拟内存、栈、全局变量等用户空间资源,还包括了内核堆栈、寄存器等内核空间资源。比系统调用多了一步,在保存内核态资源之前,需要把该进程的用户态资源保存下来,加载了下一进程的内核态后,需要刷新新进程的虚拟内存和用户栈。

进程上下文切换场景:

  • CPU轮转,进程分配的时间片耗尽。
  • 系统资源不足
  • 调用sleep等方法将自己主动挂起
  • 有优先级更高的进程运行
线程上下文切换
  1. 两个线程属于不同进程: 资源不共享,切换过程和进程上下文切换一样。
  2. 属于同一进程:虚拟内存共享,只需要切换线程的私有数据、寄存器等不共享的数据。

并发相关知识

对象头

基本对象布局:对象头(96bit)+ [实例数据] + [对齐填充数据]

实例数据就是定义的全局变量所占的内存。

对象的大小必须为8字节(byte)的整数倍。1byte = 8 bit

添加这个依赖,可以打印出对象的信息ClassLayout.parseClass(A.class).toPrintable();

<dependency> 
  <groupId>org.openjdk.jol</groupId> 
  <artifactId>jol-core</artifactId> 
  <version>0.9</version> 
</dependency>

大小端模式

小端模式:高字节存在高地址,低字节存在低地址。所以打印出来是反的。

如 1的字节码为:00000000 00000000 00000000 00000001

打印出来就是:00000001 00000000 00000000 00000000

对象头:

mark world 占64bit

对象头
mark word存储数据

Klass point 指向对象的元数据。开启了指针压缩,4位。没开启,就占8位

偏向锁 01 轻量锁 00 重量锁 10 GC 11

偏向锁:

101 可偏向

001 不可偏向 计算了hashCode之后就不能偏向了

公平锁和非公平锁

总结:

一朝排队,永远排队

基于JDK1.8的解释:

非公平锁

  1. 首先会在加锁的时候去抢锁(公平锁不会上来就拿锁)

  2. 如果加锁失败,判断锁是否被人持有了,没有被人持有的话,会直接去进行加锁(公平锁会判断是否有人排队),成功进入代码块。失败则进入队列

  3. 进入队列后,如果前面那个是head,则再次尝试加锁,失败则park,进行真正的排队。

static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;

    /**
     * Performs lock.  Try immediate barge, backing up to normal
     * acquire on failure.
     */
    final void lock() {
        // 第1步 加锁的时候就去获取锁
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }

    /**
     *acquire(1)方法会调用到该方法,再次尝试获取锁
     *
     */
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

abstract static class Sync extends AbstractQueuedSynchronizer {
  final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
      // 第2步 锁没别人持有,会去再次尝试拿锁
      if (compareAndSetState(0, acquires)) {
        setExclusiveOwnerThread(current);
        return true;
      }
    }
    // 重入锁
    else if (current == getExclusiveOwnerThread()) {
      int nextc = c + acquires;
      if (nextc < 0) // overflow
        throw new Error("Maximum lock count exceeded");
      setState(nextc);
      return true;
    }
    return false;
  }
  
  ...
}

公平锁

  1. 调用lock的时候不会去尝试加锁,回去查看队列中有没有排队的节点Node,如果有则进入队列(并不等于排队),
  2. 会再次进行查看前面那个节点是否为head,如果是,则再次尝试拿锁。失败则排队park
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);
        }

        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                // 会判断是否有节点在排队
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }


public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
  
      final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
}

是否开启了偏向模式?涉及到偏向的撤销。

偏向延迟的时间是4秒钟。JVM自身的代码,基本上不存在偏向锁,所以把偏向锁给禁用掉了,因为偏向的撤销很耗费性能。而过了4秒钟之后,JVM认为自己已经执行完成了,而用户写的代码不知道是不是偏向锁,所以开启偏向锁。自己可以在启动的时候设置JVM的参数,设置启动延迟为0秒。-XX:BiasedLockingStartupDelay=0 -XX:+UseBiasedLocking //启用偏向锁

轻量锁00加锁成功一定是要无锁的状态

pthread_mutex_lock 重量锁

JMH 基准测试 java性能测试

public class A {
    static boolean  isRunning = true;
    static int i;

     void  test(){
        log.info("t1.start-----");
        while (isRunning){
            // 代码中不做什么事情
            i++;
            i = i + 3;
        }
        log.error("t1.end-----");

    }

    public static void main(String[] args) throws InterruptedException {

        A a = new A();
        new Thread(a::test,"t1").start();

        Thread.sleep(200);
        // 睡眠过后,更改这个值,不会让t1线程停止运行。isRunning改成volite可以实现,不是因为不可见性,而是因为禁止了指令重排
        // 其实是Jvm对代码进行了优化,发现while循环中并没有做什么操作,创建了一个临时变量等于isRunning,来控制了循环
        isRunning = false;
    }
}

happend before JVM对代码进行了优化,如果发现

两条CAS指令不能保证原子性

synchronize发生异常,如果没有进行try catch 会释放锁。 monitorExit指令

Lock特点

  1. 可打断(lock.lockInterruptibly() 获取锁),可重入
  2. 可以设置超时时间
  3. 可以设置为公平锁
  4. 支持多个条件变量
  5. 支持读写锁

打断之后是在异常中处理的,自己可以在打断的异常中做自己的逻辑处理。

线程的顺序执行:

  1. wait和notify实现(while循环)
  2. LockSupport.park 和 LockSupport.unpark(t1)
  3. Join(),future()

并发编程基础知识

高并发的好处

  1. 充分利用CPU资源
  2. 加快响应用户时间
  3. 代码模块化,异步化,简单化

多线程注意事项

  1. 线程之间的安全性
  2. 线程之间的死锁
  3. 线程对服务器资源的消耗

线程的启动与中止

启动:

  1. 继承Thread类,调用start方法
  2. 实现Runnable,交给Thread运行
  3. 实现Callable,通过FutureTask把Callable包装成Runnable,交给Thread运行,可以通过FutureTask拿到Callable运行后的返回值。

中止:

  1. run方法执行完成,或者抛出一个未处理的异常导致线程提前结束
  2. 调用suspend()resume()stop()这些过期方法,不建议使用,调用后不会释放占有的资源

建议使用中断式的操作,调用线程的interrupt()方法对其进行中断操作,这个是协作式的。该线程通过isInterrupted()方法进行判断是否被中断。调用静态方法Thread.interrupted()会将中断标识改为false,也可以作为判断。

处于死锁的线程无法被中断

线程

线程中的方法

run()方法,本质是就是一个成员方法, 可重复执行,也可被单独调用

start()方法,只有执行了start()方法,才真正启动线程,只能执行一次。

yield()方法,让出CPU的执行权,但是让出时间不可设定,也不会释放锁资源。

join()方法,把指定线程A加入到当前线程,知道A线程执行完成后,才会继续执行当前线程。两个交替的线程可以合并为顺序执行。

sleep()方法,调用后,当前线程会休眠,不会释放锁。

setPriority()方法,设置线程的优先级,高优先级的线程分配的时间片数量要多于低优先级的线程。默认优先级为5,范围为1-10。高优先级的线程并不能保证一定会比低优先级的线程先执行。因为线程的调度最终取决于操作系统。

守护线程

Daemon守护线程是一种支持型线程,主要用作程序中后台调度和支持性工作。当Java虚拟机中不存在非Daemon线程的时候,Java虚拟机就会退出。使用Thread.setDaemon(true)将线程设置为守护线程。虚拟机退出时守护线程的finally块不一定会执行,所以守护线程不能依靠finally块来确保执行关闭或清理资源。垃圾回收线程就是守护线程。

等待通知机制

线程A调用了对象o的wait()方法进入等待状态,而线程B调用了对象o的notify()或者notifyAll()方法,线程A收到通知后,从o.wait()方法返回,执行后续操作。这几个方法都是针对对象的,都是Object中的方法。

等待通知范式:

// 等待方遵循的范式
synchronized(对象){// 1.获取对象的锁
  while(条件不满足){// 2.条件不满足,调用对象的wait方法,被通知后仍要检查条件
    对象.wait();// 会在这进行阻塞
  }
  对应的逻辑处理 // 3.条件满足则执行对应的逻辑
}

// 通知方范式
synchronized(对象){// 1.获得对象的锁
  改变条件 // 2.改变条件
  对象.notifyAll(); // 3.通知所有等待在线程上的线程
}
  

wait()、notify()、notifyAll()方法,线程必须要获得该对象的锁,只能在同步方法或者同步代码块中调用。进入wait方法后,当前线程就会释放锁。调用了notifyAll()后其它线程就会去竞争锁。但是锁释放是在同步代码块执行完成后释放,所以一般notifyAll方法都是在同步代码块的最后一行执行。

线程的并发工具类

Fork-Join

分而治之,设计思想是将一个难以解决的大问题,分割成一些规模较小的相同问题,各个击破,分而治之,最后进行join汇总。快速排序,归并排序,二分查找,大数据中M/R都是分而治之的思想。

CountDownLatch

闭锁,能够使一个线程等待其它线程完成各自的工作后再执行。通过一个计数器来实现,计数器的初始值为初十任务的数量,每完成一个任务后,计数器减1,当计数器到达0时,表示所以任务已完成。在闭锁上等待 CountDownLatch.await()方法的线程就可以恢复执行任务。

static CountDownLatch countDownLatch = new CountDownLatch(5);
public static void countDownLatchTest() {
    for (int i = 0; i < 5; i++) {
        int finalI = i;
        new Thread(() -> {
            System.out.println("子线程运行... " + finalI);
            countDownLatch.countDown();// 计数器减1
        }).start();
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public static void main(String[] args) {
   countDownLatchTest();
   try {
       countDownLatch.await();// 等计数器为0之后,停止阻塞,恢复执行
   } catch (InterruptedException e) {
       e.printStackTrace();
   }
   System.out.println("主线程运行完成...");
}

并非说任务数量和线程数量一点要相等,一个线程可以多吃调用countDownLatch.countDown();进行计数器减1

CycilcBarrier

可循环使用的屏障。让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会开放,所有被屏蔽拦截的线程才会继续执行。

CyclicBarrier 默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用 CyclicBarrier.await 方法告诉 CyclicBarrier 我已经到达了屏障,然 后当前线程被阻塞。

CyclicBarrier 还提供一个更高级的构造函数 CyclicBarrie(r int parties,Runnable barrierAction),用于在线程到达屏障时,优先执行 barrierAction,方便处理更复 杂的业务场景。

Semaphore

信号量用来控制同时访问特定资源的线程数量。可以用于做流量控制。Semaphore 的构造方法 Semaphore(int permits)接受一个整型的数字, 表示可用的许可证数量。线程使用 Semaphore的 acquire()方法获取一个许可证,使用完之后调用 release()方法归还许可证。还可以用 tryAcquire()方法尝试获取许可证。

Semaphore 还提供一些其他方法,具体如下。

  • intavailablePermits():返回此信号量中当前可用的许可证数。

  • intgetQueueLength():返回正在等待获取许可证的线程数。

  • booleanhasQueuedThreads():是否有线程正在等待获取许可证。

  • void reducePermits(int reduction):减少 reduction 个许可证,是个protected 方法。

  • Collection getQueuedThreads():返回所有等待获取许可证的线程集合,是个protected 方法。

Exchange

Exchanger交换者是一个用于线程间协作的工具类。用于进行线程间数据交换。提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。通过exchange()方法交换数据,第一个线程先执行exchange方法,会等待第二个线程执行exchange方法,两个线程都到达同步点时,就可以交换数据。

原子操作CAS

如果有两个操作A和B,如果从执行 A 的线程来看,当另一个线程执行 B 时,要么将 B 全部执行完,要么完全不执行 B,那么 A 和 B 对彼此来说是原子的。

锁实现原子操作存在的问题:被阻塞的线程优先级很高,获得锁的线程一直不释放锁,大量线程竞争支援,导致CPU资源浪费,死锁问题。

CAS基本思路:内存地址V上的值和期望的值A相等,则给其赋予新值B,否则不做任何事,只返回原值。

CAS实现原子操作的3大问题

  1. ABA问题

    一个值原来是A,后面变成了B,又修改成了A,使用CAS进行检查时发现其值没有发生变化,但是实际上是已经变化过了的。

    解决思路就是使用版本号,在变量面前加上版本号。

  2. 循环时间长开销大

    自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。

  3. 只能保证一个共享变量的原子操作

Jdk中原子相关操作类

  1. AtomicInteger
  2. AtomicIntegerArray
  3. AtomicReference
  4. AtomicStampedRefrence 利用版本戳形式记录了每次改变的版本号,避免ABA问题
  5. AtomicMarkableReference 带有标记位的引用类型
  6. AtomicIntegerFieldUpdater
  7. AtomicLongFieldUpdater
  8. AtomicReferenceFieldUpdater

显示锁

synchronized关键字会隐式的获取锁,它将锁的获取和释放固化了,就是先获取再释放。

显示锁提供了隐式锁很多没有的功能,比如尝试非阻塞的获取锁,能被中断的获取锁,超时获取锁等。

显示锁的特性

Lock的标准用法

lock.lock();// 获取锁
try{
  ...
}finally{
  lock.unlock();// 释放锁
}

在finally语句块中执行unlock()方法,保证最终能够释放锁。

Lock的常用API

Lock常用api

公平和非公平锁

先对锁获取的请求一定先被满足,那么这个锁就是公平锁。也就是等待时间最长的线程优先获取锁。反之就是非公平锁。非公平锁效率比公平锁高。

恢复一个被挂起的线程与该线程真正开始执行之间存在严重的延迟,有可能在这个延迟时间段内有另外一个线程获取到锁并且执行完成,被挂起的线程还没有被唤醒。正因为这个情况,非公平锁比公平锁效率更高。

ReentrantLock可重入锁

定义:同一个线程对于已经获得到的锁,可以多次继续申请到该锁的使用权。Synchronized隐式的支持可重入。ReentrantLock实现Lock接口和序列化接口,构造函数传入ture表示使用公平锁。默认为非公平锁。

读写锁ReentrantReadWriteLock

排它锁:在同一时刻,只允许一个线程进行访问。

读写锁在同一个时刻可以允许多个读线程访问。但是写线程访问时,所有的读线程和其它线程均被阻塞。

维护了一个读锁和一个写锁,通过分离读锁和写锁,提升并发性。

Condition接口

Condition 接口提供 了类似 Object 的监视器方法,与 Lock 配合可以实现等待/通知模式。

Condition常用方法

使用范式:

Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();

public void conditionWaiter() throws  InterruptedException{
    lock.lock();
    try {
        condition.await();
    } finally {
        lock.unlock();
    }
}

public void conditionSignal() {
    lock.lock();
    try {
        condition.signal();
    }finally {
        lock.unlock();
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,386评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,142评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,704评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,702评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,716评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,573评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,314评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,230评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,680评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,873评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,991评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,706评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,329评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,910评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,038评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,158评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,941评论 2 355

推荐阅读更多精彩内容

  • 最近自己在整理关于并发编程相关的知识点,要细致的了解每个知识背后产生的原因和相关处理并发的底层原理,确实还...
    Android开发_Hua阅读 124评论 0 1
  • 锁 锁分为 类锁 对象锁 显示锁image.png写在函数上的锁,不用去设置锁的谁,会自动去寻找一把锁,并且如果是...
    星宇V阅读 858评论 0 3
  • 1、什么是线程安全,怎么保证线程安全? 线程安全可以简单理解为一个方法或者一个实例可以在多线程环境中使用而不会出现...
    技术灭霸阅读 172评论 0 0
  • 1.并发编程三要素 原子性 原子,即一个不可再被分割的颗粒。在Java中原子性指的是一个或多个操作要么全部执行成功...
    huang_liang_a阅读 1,100评论 0 0
  • 本文是我自己在秋招复习时的读书笔记,整理的知识点,也是为了防止忘记,尊重劳动成果,转载注明出处哦!如果你也喜欢,那...
    波波波先森阅读 11,264评论 4 56