并发编程总结

一、创建线程的十种方式

继承Thread类:
这是最普通的方式,继承Thread类,重写run方法,如下:

public class ExtendsThread extends Thread {
    @Override
    public void run() {
        System.out.println("1......");
    }

    public static void main(String[] args) {
        new ExtendsThread().start();
    }
}

实现Runnable接口:
这也是一种常见的方式,实现Runnable接口并重写run方法,如下:

public class ImplementsRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("2......");
    }

    public static void main(String[] args) {
        ImplementsRunnable runnable = new ImplementsRunnable();
        new Thread(runnable).start();
    }
}

实现Callable接口:
和上一种方式类似,只不过这种方式可以拿到线程执行完的返回值,如下

public class ImplementsCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        System.out.println("......");
        return "123456";
    }

    public static void main(String[] args) throws Exception {
        ImplementsCallable callable = new ImplementsCallable();
        FutureTask<String> futureTask = new FutureTask<>(callable);
        new Thread(futureTask).start();
        System.out.println(futureTask.get());
    }
}

Callable如何与Runnable联系在一起?

Runnable的一个实现类FutureTask(Future详解)再创建时能够指定传入一个给定的Callable。

可以参考这篇了解Future详解

在这里插入图片描述
在这里插入图片描述

使用ExecutorService线程池:
这种属于进阶方式,可以通过Executors创建线程池,也可以自定义线程池,如下:

public class UseExecutorService {
    public static void main(String[] args) {
        ExecutorService poolA = Executors.newFixedThreadPool(2);
        poolA.execute(()->{
            System.out.println("4A......");
        });
        poolA.shutdown();

        // 又或者自定义线程池
        ThreadPoolExecutor poolB = new ThreadPoolExecutor(2, 3, 0,
                TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(3),
                Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
        poolB.submit(()->{
            System.out.println("4B......");
        });
        poolB.shutdown();
    }
}

使用CompletableFuture类:

CompletableFuture是JDK1.8引入的新类,可以用来执行异步任务,如下:

public class UseCompletableFuture {
    public static void main(String[] args) throws InterruptedException {
        CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
            System.out.println("5......");
            return "zhuZi";
        });
        // 需要阻塞,否则看不到结果
        Thread.sleep(1000);
    }
}

基于ThreadGroup线程组:
Java线程可以分组,可以创建多条线程作为一个组,如下:

public class UseThreadGroup {
    public static void main(String[] args) {
        ThreadGroup group = new ThreadGroup("groupName");

        new Thread(group, ()->{
            System.out.println("6-T1......");
        }, "T1").start();

        new Thread(group, ()->{
            System.out.println("6-T2......");
        }, "T2").start();

        new Thread(group, ()->{
            System.out.println("6-T3......");
        }, "T3").start();
    }
}

使用FutureTask类:
这个和之前实现Callable接口的方式差不多,只不过用匿名形式创建Callable,如下:

public class UseFutureTask {
    public static void main(String[] args) {
        FutureTask<String> futureTask = new FutureTask<>(() -> {
            System.out.println("7......");
            return "zhuZi";
        });
        new Thread(futureTask).start();
    }
}

使用ForkJoin线程池或Stream并行流:
ForkJoin是JDK1.7引入的新线程池,基于分治思想实现。而后续JDK1.8的parallelStream并行流,默认就基于ForkJoin实现,如下:

public class UseForkJoinPool {
    public static void main(String[] args) {
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        forkJoinPool.execute(()->{
            System.out.println("10A......");
        });

        List<String> list = Arrays.asList("10B......");
        list.parallelStream().forEach(System.out::println);
    }
}

二、说说线程的生命周期和状态?

Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态:

  • NEW: 初始状态,线程被创建出来但没有被调用 start() 。
  • RUNNABLE: 运行状态,线程被调用了 start()等待运行的状态。
  • BLOCKED: 阻塞状态,需要等待锁释放。
  • WAITING: 等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
  • TIME_WAITING: 超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
  • TERMINATED: 终止状态,表示该线程已经运行完毕。

在这里插入图片描述

三、JMM(Java 内存模型)

具体参考JMM详解

四、volatile 关键字

4.1 如何保证变量的可见性?

在 Java 中,volatile 关键字可以保证变量的可见性,如果我们将变量声明为 volatile ,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。 volatile底层实现主要是通过lock前缀指令,它会锁定这块内存区域的缓存(缓存行锁定)并回写到主内存
在这里插入图片描述

IA-32和Intel 64架构软件开发者手册对lock指令的解释:

  • 会将当前处理器缓存行的数据立即写回到系统内存.
  • 这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效(MESI协议)
  • 提供内存屏障功能,使lock前后指令不能重排序

volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。

4.2 如何禁止指令重排序?

在 Java 中,volatile 关键字除了可以保证变量的可见性,还有一个重要的作用就是防止 JVM 的指令重排序即保证了变量的有序性。 如果我们将变量声明为 volatile ,在对这个变量进行读写操作的时候,会通过插入特定的 内存屏障 的方式来禁止指令重排序。

指令重排序:在不影响单线程程序执行结果的前提下,计算机为了最大限度的发挥机器性能,会对机器指令重排序优化

重排序会遵循as-if-serial与happens-before原则

在 Java 中,Unsafe 类提供了三个开箱即用的内存屏障相关的方法,屏蔽了操作系统底层的差异

public native void loadFence();
public native void storeFence();
public native void fullFence();

理论上来说,你通过这个三个方法也可以实现和volatile禁止重排序一样的效果,只是会麻烦一些。
下面演示一下基于volatile关键字实现双重校验锁实现对象单例(线程安全)

public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

    public  static Singleton getUniqueInstance() {
       //先判断对象是否已经实例过,没有实例化过才进入加锁代码
        if (uniqueInstance == null) {
            //类对象加锁
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

uniqueInstance = new Singleton();这段代码其实是分为三步执行:

  1. uniqueInstance 分配内存空间
  2. 初始化 uniqueInstance
  3. uniqueInstance 指向分配的内存地址

但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。

4.3 volatile 可以保证原子性么?

volatile 关键字能保证变量的可见性,但不能保证对变量的操作是原子性的,保证原子性需要借助synchronized这样的锁机制
我们通过下面的代码即可证明:


public class VolatileAtomicityDemo {
    public volatile static int k = 0;
    public void increase() {
        k++;
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService threadPool = Executors.newFixedThreadPool(5);
        VolatileAtomicityDemo volatileAtomicityDemo = new VolatileAtomicityDemo();
        for (int i = 0; i < 5; i++) {
            threadPool.execute(() -> {
                for (int j = 0; j < 200; j++) {
                    volatileAtomicityDemo.increase();
                }
            });
        }
        // 等待2秒,保证上面程序执行完成
        Thread.sleep(2000);
        System.out.println(c);
        threadPool.shutdown();
    }
}

正常情况下,运行上面的代码理应输出 1000。但是实际输出是小于1000的,因为实际上,k++ 其实是一个复合操作不是原子的,分为三步:

  1. 读取 k 的值。
  2. 对 k 加 1。
  3. 将 k 的值写回内存。

volatile是无法保证这三个操作是具有原子性的,有可能导致下面这种情况出现:线程 1 对 k 进行读取操作之后,还未对其进行修改。线程 2 又读取了 k 的值并对其进行修改(+1),再将 k 的值写回内存。线程 2 操作完毕后,线程 1 对 k 的值进行修改(+1),再将 k 的值写回内存。这也就导致两个线程分别对 k 进行了一次自增操作后,k 实际上只增加了 1。其实,如果想要保证上面的代码运行正确也非常简单,利用 synchronizedLock或者AtomicInteger都可以。

五、各种锁的概念以及实现

具体内容可以参考锁详解

六、常用并发工具类

具体内容可以参考并发工具类

七、ThreadLocal

7.1 ThreadLocal 的作用

ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意:

  • 因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。
  • 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。

ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。

总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景
在这里插入图片描述

7.2 ThreadLocal的使用方法

实现一个解析jwt令牌通过ThreadLocal传递数据到service层中的简单案例:

  1. TreadLocal封装成工具类方便使用
public class BaseContext {

    public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    public static void setCurrentId(Long id) {
        threadLocal.set(id);
    }

    public static Long getCurrentId() {
        return threadLocal.get();
    }

    public static void removeCurrentId() {
        threadLocal.remove();
    }

}

  1. 在统一拦截器校验令牌解析出员工id时将员工id加入到ThreadLocal
//1、从请求头中获取令牌
String token = request.getHeader(jwtProperties.getAdminTokenName());

//2、校验令牌
try {
    log.info("jwt校验:{}", token);
    Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
    Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
    BaseContext.setCurrentId(empId);
    log.info("当前员工id:", empId);
    //3、通过,放行
    return true;
} catch (Exception ex) {
    //4、不通过,响应401状态码
    response.setStatus(401);
    return false;
}
  1. 在service中获取员工id
// 获取当前记录的创建人和修改人
employee.setCreateUser(BaseContext.getCurrentId());
employee.setUpdateUser(BaseContext.getCurrentId());

7.3 ThreadLocal 原理

最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。 ThrealLocal 类中可以通过Thread.currentThread()获取到当前线程对象后,直接通过getMap(Thread t)可以访问到该线程的ThreadLocalMap对象。每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为 keyObject 对象为 value 的键值对。
比如我们在同一个线程中声明了两个 ThreadLocal 对象的话, Thread内部仅有一个ThreadLocalMap 存放数据,ThreadLocalMap的 key 就是 ThreadLocal对象,value 就是 ThreadLocal对象调用set方法设置的值。
ThreadLocal 数据结构如下图所示:
在这里插入图片描述

7.4 ThreadLocal 内存泄露问题

ThreadLocalMap 中使用的 keyThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时,key 会被清理掉,而 value 不会被清理掉。因此ThreadLocalMap 中就会出现 key 为 null 的 EntryThreadLocal 的**内存泄漏主要发生在线程池中,因为 每个线程里面ThreadLocalMap的生命周期和每个线程的生命周期是一样长的,当thread对象被线程池回收过后就意味着ThreadLocalMap不会被回收(GC) .ThreadLocalMap实现中已经考虑了这种情况,在调用 set() 、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后最好手动调用remove()方法

弱引用介绍: 如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。线程池

八、线程池

具体内容可参考这篇线程池详解

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值