Java并发编程概述

基本概念

多线程业务场景

  • 异步任务

1、用户注册后的异步通知,短信/邮箱

2、异步记录日志

  • 定时任务

定期备份日志、数据库

  • 分布式计算

分片计算/Hadoop的map-reduce

  • 服务器编程

Servlet编程模型

进程、线程、协程

  • 基本概念

进程: 本质上是一个独立执行的程序,进程是操作系统进行资源分配和调度的基本概念,操作系统进行行资源分配和调度的一个独立单位

线程:是操作系统能够进⾏运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。 ⼀个进程中可以并发多个线程,每条线程执行不同的任务,切换受系统控制。

协程: 又称为微线程,是⼀种⽤户态的轻量级线程,协程不像线程和进程需要进行系统内核上的上下文切换,协程的上下文切换是由⽤户⾃⼰决定的,有自⼰的上下文,所以说是轻量级的线程,也称之为用户级别的线程就叫协程,⼀个线程可以多个协程,线程进程都是同步机制,⽽协程则是异步。 Java的原生语法中并没有实现协程,⽬前python、Lua和GO等语言支持

  • 关系

一个进程可以有多个线程,它允许计算机同时运行两个或多个程序。线程是进程的最⼩执行单 位,CPU的调度切换的是进程和线程,进程和线程多了了之后调度会消耗大量的CPU,CPU上真正运行的是线程,线程可以对应多个协程

  • 协程的优缺点

优点:

非常快速的上下文切换,不⽤系统内核的上下文切换,减⼩开销 单线程即可实现高并发,

单核CPU可以支持上万的协程 由于只有⼀个线程,也不存在同时写变量的冲突,在协程中控制共享资源不需要加锁

缺点:

协程⽆法利⽤多核资源,本质也是个单线程

协程需要和进程配合才能运行在多CPU上 ⽬前java没成熟的第三方库,存在⻛险 调试debug存在难度,不利于发现问题

并发与并行

  • 并发

本质上只是通过cpu的切换,交替的执行任务。

但是由于切换速度过快,导致看上去是多个任务一起在执行一样

  • 并行

真正的多任务执行

Java实现多线程的若干种方式

案例:多线程输出字符串

  • 继承Thread类
@Slf4j
public class HelloThread extends Thread {
    @Override
    public void run() {
        log.info("😬 我的线程号:{}", Thread.currentThread().getName());
    }
}

调用

        HelloThread thread = new HelloThread();
        HelloThread thread1 = new HelloThread();
        thread.start();
        thread1.start();
  • 实现Runnable接口
@Slf4j
public class HelloThread implements Runnable {
    @Override
    public void run() {
        log.info("😬 我的线程号:{}", Thread.currentThread().getName());
    }
}

调用

        HelloThread thread = new HelloThread();

        Thread thread1 = new Thread(thread);
        Thread thread2 = new Thread(thread);

        thread1.start();
        thread2.start();
  • Stream(JDK8+)
        List<String> jobList = new ArrayList<>();
        jobList.add("job1");
        jobList.add("job2");
        jobList.add("job3");

        jobList.parallelStream()
                .forEach(item -> {
                    System.out.println("当前线程号:" + Thread.currentThread().getName() + "\t\t" + item);
                });

输出结构如下

当前线程号:main		job2
当前线程号:ForkJoinPool.commonPool-worker-1		job1
当前线程号:ForkJoinPool.commonPool-worker-2		job3
  • Callable和FutureTask方式

JDK1.5+支持

public class MyTaskTest {


    public static void main(String[] args) {
        FutureTask<Object> futureTask = new FutureTask<>(() -> {
            System.out.println("通过Callable实现多线程:" + Thread.currentThread().getName());
            return "返回值1:" + Thread.currentThread().getName();
        });
        FutureTask<Object> futureTask2 = new FutureTask<>(() -> {
            System.out.println("通过Callable实现多线程:" + Thread.currentThread().getName());
            return "返回值2:" + Thread.currentThread().getName();
        });
        Thread thread = new Thread(futureTask);
        Thread thread2 = new Thread(futureTask2);
        thread.start();
        thread2.start();
        try {
            System.out.println(futureTask.get());
            System.out.println(futureTask2.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}
  • 线程池的方式
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            executorService.execute(()->{
                System.out.println("当前线程号:"+Thread.currentThread().getName());
            });
        }
        executorService.shutdown();//线程池中的活跃线程处理完任务后,会自动关闭线程池

多任务并发执行实用代码

package com.zhangln.shanhaijing.thread;

import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.*;

@Slf4j
public class Demo01CompleteFutureMain {
    public static void main(String[] args) throws InterruptedException {
//        任务列表
        List<Integer> jobList = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14);
        int nThreads = 3;
//        工作线程池
        ExecutorService executorService = new ThreadPoolExecutor(nThreads, nThreads,
                0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>());
//        接收每个任务的执行结果
        List<CompletableFuture<String>> completableFutures = new ArrayList<>(jobList.size());

//        任务分派
        for (int i = 0; i < jobList.size(); i++) {

            try {
                Integer jobId = jobList.get(i);

                CompletableFuture<String> integerCompletableFuture = CompletableFuture.supplyAsync(() -> {

                    log.info("模拟任务执行,执行任务:{},线程号:{}", jobId, Thread.currentThread().getName());
//                    睡几秒
                    return jobId + "任务执行结果"+Thread.currentThread().getName();
                }, executorService);
//            保存本次任务执行结果
                completableFutures.add(integerCompletableFuture);
            } catch (Exception e) {
                log.error(e.getMessage(), e);
            }

        }

        log.info("任务分派结束");

//        等待全部任务执行完毕
        CompletableFuture[] completableFutures1 = new CompletableFuture[completableFutures.size()];
        CompletableFuture<Void> voidCompletableFuture =
                CompletableFuture.allOf(completableFutures.toArray(completableFutures1));
        try {
            voidCompletableFuture.get();
            log.info("打印执行结果");
            completableFutures.stream()
                    .forEach(tmp -> {
                        try {
                            log.info("执行结果:{}", tmp.get());
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    });
        } catch (Exception e) {
            e.printStackTrace();
        }

        executorService.shutdown();
        log.info("任务执行完毕");

        TimeUnit.SECONDS.sleep(2);

    }
}

线程状态

JDK的线程状态有6种,JVM里面有9种。

一般我们说的线程状态是JDK的线程状态

常见状态

  • 新建 new:生成线程对象,未调用该对象的start方法,如 new Thread()
  • 就绪 runnable:新建的对象调用start方法后,线程处于就绪状态

就绪状态的来源不仅仅是new的对象调用start方法。还可以来自运行装和阻塞状态。

所谓就绪,就是说我已经准备好了,只要CPU有空了,就可以调度我进行线程执行。

  • 运行 running:获取到cpu使用权的线程,就是处于running状态

注意:在jdk中,是没有running状态的,见 Thread类的State内部枚举类。

它把runnable和running两种状态合并了

  • 阻塞 blocked

    • 等待阻塞

    进入该状态的线程需要等待其他线程做出一定动作,如 通知、中断。此状态CPU不进行分配。

    可能需要被唤醒,也可能无限等待下去。

    • 同步阻塞:线程执行需要的资源被锁住,此时就处于同步阻塞状态
  • 终止 terminate

    线程的run执行完毕

  • 线程常见API

    • sleep

      线程方法,交出CPU使用权;等待预计时间后再恢复;进行阻塞状态(TIME_WAITING);睡眠结束变为就绪状态

    • yield

      线程方法,暂停当前线程,让CPU执行其他线程

      注意:yield操作不会让线程处于阻塞状态,而是变为就绪。只需要重新获得CPU使用权就会回到运行状态

    • join

      线程方法

      在主线程调用该方法,会让主线程休眠,不会释放已经持有的对象锁。

      让调用join方法的线程先执行,再执行其他线程

    • wait

      对象方法,释放对象的锁,进入线程的等待队列。

      需要靠notify/notifyAll唤醒。或者wait(timeout后自动唤醒)

      sleep和wait的区别就在于是否释放锁

    • notify

      对象方法;唤醒对象监视器上等待的单个线程(随机抽取一个)

    • notifyAll

      对象方法,唤醒对象监视器上等待的所有线程

线程状态切换

CCNE87

Java中保证线程安全的方式

  • 加锁

synchronize/ReentrantLock

  • 线程安全类

CopyOnWriteArrayList/ConcurrentHashMap

  • ThreadLocal

volatile关键字

了解volatile关键字不?能否解释下,然后这和synchronized有什么大的区别

答:

volatile是轻量级的synchronized,保证了共享变量的可见性,被volatile关键字修饰的变量,如果值发生了变化,其他线程⽴立刻可见,避免出现脏读现象

volatile:保证可见性,但是不能保证原子性 synchronized:保证可见性,也保证原子性

使用场景

1、不能修饰写入操作依赖当前值的变量,⽐如num++、num=num+1,不是原⼦操作,肉眼看起来是,但是JVM字节码层⾯不⽌一步

2、由于禁止了指令重排,所以JVM相关的优化没了,效率会偏弱

什么是指令重排?

指令重排分为编译器重排和运行期重排

JVM在编译java代码,或者cpu在执行jvm字节码的是,在不改变执行结果的情况下对指令执行顺序进行调整,以优化执行效率。

  • 为什么会出现脏读

JAVA内存模型简称 JMM ;

JMM规定所有的变量存在主内存,每个线程有⾃己的工作内存,线程对变量的操作都在工作内存中进行,

不能直接对主内存进行操作

使用volatile修饰变量 每次读取前必须从主内存属性最新的值 每次写入需要立刻写到主内存中

volatile关键字修饰的变量随时看到的自己的最新值,假如线程1对变量v进⾏行修改,那么线程2 是可以马上看⻅

7A4C7329-5D58-43FF-BF7B-452627FCBEB4.png

  • 什么是happens-before

因为jvm会对代码进行编译优化,指令会出现重排序的情况,为了避免编译优化对并发编程安全性的影响,需要happens-before规则定义一些禁止编译优化的场景,保证并发编程的正确性。

https://www.jianshu.com/p/9464bf340234

并发编程三要素

  • 原子性:即一些操作,要么全部成功,要么全部失败。执行期间不能被打断。类似于数据库的事务的概念
  • 有序性:程序的执行顺序按照代码的先后顺序执行(因为处理器有可能通过指令重排进行重新排序)
  • 可见性:线程A、B之间,A对共享变量的写操作,B能够马上看到

以上三特性,是并发编程中需要保证的

进程/线程调度策略

先来先服务

短作业优先

高响应比优先

时间片轮询

优先级调度

常用锁

悲观锁

当线程去操作数据的时候,总认为别的线程会去修改数据。

所以每次拿数据的时候,都会上锁。别的线程去拿数据的时候就会阻塞。

如:synchronized

乐观锁

每次去拿数据的时候都认为别人不会修改,更新的时候判断别人是否会去更新数据,通过数据版本号进行控制

如果数据被更改了,就拒绝更新。如 CAS是乐观锁,但是,严格意义来说,并不是锁。

公平锁

多个线程按照申请锁的顺序来获取锁。

简单来说,如果一个线程组中,能够保证每个线程都能拿到锁,比如:ReentranLock(底层是同步队列)

悲观锁适合写操作多的场景

乐观锁适合读操作多的场景

非公平锁

获取锁的方式是随机的,保证不了每个线程都能拿到锁。

会存在有的线程一直拿不到锁的情况。如:synchronized、ReentrantLock

ReentrantLock公平与否,由构造函数控制

可重入锁

也叫递归锁。

在外层使用锁之后,内层仍然可以使用,并且不发生死锁。

即:在调用方法A的时候,获取了锁,方法A在执行过程中,调用了方法B,在B中继续获取该锁。在可重入锁中,这种操作是被允许的。

可重入锁在一定程度上避免了死锁。

不可重入锁

若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞。

自旋锁

一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待。

然后不断判断锁是否能够被成功获取,直到获取到锁才会退出循环。

任何时刻最多只有一个执行单元获取到锁

不会发生线程状态的切换,一直处于用户态;减少了线程的上下文切换,但同时也消耗了CPU

其他

  • 共享锁

这种锁添加后,多线程可以读,但无法写。

该锁可被多个线程持有,用于资源数据共享

  • 互斥锁

该锁只能被一个线程持有。

加锁后其他线程试图获取此锁,都会阻塞,知道当前线程解锁

  • 死锁

两个或两个以上的线程在执行过程中,由于资源竞争,导致阻塞

  • 偏向锁–>轻量级锁–>重量级锁

这三种锁,是JVM为了提高锁的获取和释放效率而做的优化。

针对synchronized的锁升级。锁的状态通过对象监视器在对象头中的字段来表名,是不可逆的过程。

  • 分段锁/行锁/表锁

死锁案例与解决

写一个死锁,并解决

  • 死锁的引发
import java.util.concurrent.TimeUnit;

public class DeadLockDemo {
    //这两个对象就代表锁
    private static String locka = "locala";
    private static String lockb = "localb";

    public void methodA() {
        //以同步代码块的方式使用锁
        synchronized (locka) {
            System.out.println("我是A方法,获取锁A" + Thread.currentThread().getName());
            try {
//                让出CPU执行权,不释放锁
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            synchronized (lockb) {
                System.out.println("我是A方法,获取锁B" + Thread.currentThread().getName());
            }
        }
    }

    public void methodB() {
        synchronized (lockb) {
            System.out.println("我是B方法,获取锁B" + Thread.currentThread().getName());
            try {
//                让出CPU执行权,不释放锁
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            synchronized (locka) {
                System.out.println("我是B方法,获取锁A" + Thread.currentThread().getName());
            }
        }
    }

    public static void main(String[] args) {
        DeadLockDemo deadLockDemo = new DeadLockDemo();
        new Thread(() -> {
            deadLockDemo.methodA();
        }).start();

        new Thread(() -> {
            deadLockDemo.methodB();
        }).start();

        System.out.println("我是主线程:" + Thread.currentThread().getName());
    }
}

1、线程1拿到锁a

2、线程1睡眠

3、线程2拿到锁b

4、线程2睡眠

5、线程1要拿锁b,线程2要拿锁a,但两把锁都未释放,就堵死了

以上,我们总结发生死锁的4个必要条件

1、互斥条件:资源不能共享, 只能由一个线程使用

2、请求与保持条件:线程已经获得一些资源,但因请求其他资源发生阻塞,对已经获得的资源保持不释放

3、不可抢占:有些资源不可抢占,当某个线程获得资源后,系统不能强行回收,只能由线程自己用完释放

4、循环等待条件:多个线程下形成环形链,每个都占用了对方申请的下个资源

以上条件,只要有一个不成立,就不会发生死锁

  • 解决死锁问题

常见方法

1、调整申请锁的范围

对于上述的案例中,只需要将锁的范围调整为

    public void methodA() {
        synchronized (locka) {
            System.out.println("我是A方法,获取锁A" + Thread.currentThread().getName());
            try {
//                让出CPU执行权,不释放锁
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        synchronized (lockb) {
            System.out.println("我是A方法,获取锁B" + Thread.currentThread().getName());
        }
    }

就不会发生死锁

锁的范围越小越好。

如无必要,尽量不要让代码在锁中

调整申请锁的顺序

不可重入锁的设计

  • 什么是不可重入锁?

当前线程执行某个方法的时候获取到了锁A,在锁A释放前,如果在方法中再次尝试获取锁A,此时会获取不到被阻塞。

这就是不可重入锁

package com.zhangln.shanhaijing.thread;

/**
 * 不可重入锁
 *
 * @author sherry
 * @description
 * @date Create in 2020/8/10
 * @modified By:
 */

public class UnreentrantLock {

    private boolean isLocked = false;


    public synchronized void lock() throws InterruptedException {
        System.out.println("进入lock加锁" + Thread.currentThread().getName());
        while (isLocked) {
            System.out.println("进入wait等待:" + Thread.currentThread().getName());
            wait();
        }
        isLocked = true;
    }

    public synchronized void unlock() {
        System.out.println("进入unlock解锁" + Thread.currentThread().getName());
        isLocked = false;
//        唤醒当前对象锁等待队列中的某个线程
        notify();
    }
}

  • 使用
    private UnreentrantLock unreentrantLock = new UnreentrantLock();

    @Test
    public void test3() {
        try {
            unreentrantLock.lock();
            System.out.println("1 获取锁");
            unreentrantLock.lock();
            System.out.println("2 获取锁");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            unreentrantLock.unlock();
        }
    }

可重入锁设计

  • 什么是可重入锁

在外层获取到锁后,内层可以继续获取锁

package com.zhangln.shanhaijing.thread;

/**
 * @author sherry
 * @description
 * @date Create in 2020/8/12
 * @modified By:
 */

public class ReentrantLockDemo {
    private boolean isLocked = false;
    //    用于记录是不是重入的线程
    private Thread lockOwner = null;
    //    累计加锁次数
    private int lockedCount = 0;

    public synchronized void lock() throws InterruptedException {
        System.out.println("进入lock加锁" + Thread.currentThread().getName());
        Thread thread = Thread.currentThread();
//        判断是否是同一个线程获取锁
        while (isLocked && lockOwner != thread) {
            System.out.println("进入wait等待:" + Thread.currentThread().getName());
            System.out.println("当前锁状态:" + isLocked);
            System.out.println("当前加锁次数:" + lockedCount);
            wait();
        }
        isLocked = true;
        lockOwner = thread;
        lockedCount++;
    }

    public synchronized void unlock() {
        System.out.println("进入unlock解锁" + Thread.currentThread().getName());
        Thread thread = Thread.currentThread();
//        自己加的锁只有自己才能解
        if (thread == lockOwner) {
            lockedCount--;
            if (lockedCount == 0) {
                lockOwner = null;
                isLocked = false;
//        唤醒当前对象锁等待队列中的某个线程
                notify();
            }

        }

    }

}

  • 测试调用
    @Test
    public void test5() {
        try {
            reentrantLockDemo.lock();
            reentrantLockDemo.lock();

        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            reentrantLockDemo.unlock();
        }
    }

Synchronized深入理解

  • 基本理解

rJExMM

  • jdk6以后的优化

8SErII

jqXDZ9

CAS

  • 概念

Compare And Swap,即 比较再交换

是实现并发技术的一种

底层通过Unsafe类实现原子性操作,操作包含三个数:内存地址(V)、预期原值(A)和新值(B)。

如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值,如果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能执行。

整个CAS的过程,就是一个乐观锁的过程

AtomicXXX等原子类,底层就是CAS实现的。一定程度上比synchronized性能好,因为synchronized是悲观锁。

注意:线程多的时候,由于CAS自旋,性能反而不好

  • ABA问题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KGUgU1DC-1597558446071)(http://tuchuang.zhangln.com/s31CQC.png)]

AQS

  • 基本概念

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9eLYLOV8-1597558446072)(http://tuchuang.zhangln.com/nyfooX.png)]

AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks 包下面。它是一个Java提高的底层同步工具类,比如CountDownLatch、ReentrantLock, Semaphore,ReentrantReadWriteLock,SynchronousQueue,FutureTask等皆是基于 AQS的

只要搞懂了了AQS,那么J.U.C中绝大部分的api都能轻松掌握

简单来说:是用一个int类型的变量表示同步状态,并提供了一系列的CAS操作来管理这个同步状态 对象
一个是 state(⽤于计数器,类似gc的回收计数器)
一个是线程标记(当前线程是谁加锁的)

一个是阻塞队列(用于存放其他未拿到锁的线程)

解析ReentranLock

  • 通过参数控制ReentranLock是公平锁还是非公平锁,默认是非公平锁
  • 内部重写了AQS,自定义了锁

ReentranLock与synchronized差别

  • 两者都是独占锁
  • synchronized是悲观锁,会引起其他线程阻塞
  • synchronized无法判断锁状态,可重入、不可中断,非公平锁
  • 加锁过程是隐式的
  • 一般的并发场景足够用了

  • ReentranLock是悲观锁,实现了Lock接口
  • 可判断是否获取到锁,可重入,通过参数控制是否公平锁
  • 需要手动加锁/解锁,解锁操作尽量放在finaly代码块中,保证线程正确释放锁
  • 在复杂并发场景下,确保获取锁的次数与释放锁的次数一致,否则会导致其他线程无法获取锁

读写锁ReentrantReadWriteLock

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l2TZPBFC-1597558446073)(http://tuchuang.zhangln.com/20200816133132_uaWsk3_325C598A-64DA-421A-A6A9-B940D36F6D13.png)]

  • ReentrantReadWriteLock实现了读写分离锁,适合并发读多的场景
  • 支持公平锁与非公平锁,底层也是AQS的实现
  • 允许从写锁降级为读锁

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cRwSFBJQ-1597558446075)(http://tuchuang.zhangln.com/20200816133614_nbdSmU_0B969139-E2EC-471F-9D18-286621C5D54F.png)]

BlockingQueue

核心:保证生产者不在缓冲区满的时候放入数据,消费者不在缓冲区空时消耗数据

常见的同步方法是采用信号量或加锁机制

4D001781-C09C-43CA-A834-E86BB301DB72.png

6E112931-3397-43C9-9259-7323F924CEAB.png

并发最佳实践

  • 不同模块取不同的线程名称,便于后续问题排查
  • 使用同步代码块或同步方法时,尽量减小同步范围
  • 多比那个发集合少使用同步集合
  • 线程业务需要使用多线程时,优先考虑线程池

C85BD632-B3B9-41CE-9CDD-BD1E4CCDA20E.png

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值