知识梳理_05_并发编程

基础概念

进程和线程区别

进程:
1)系统资源分配和调度的独立单位
2)至少包含一个线程
3)拥有自己的资源
线程:
1)进程中负责程序执行的执行单元
2)依靠程序执行的顺序控制流,只能使用程序的资源和环境,共享进程的全部资源
3)有自己的堆栈和局部变量,没有单独的地址空间
4)CPU调度和分派的基本单位,持有程序计数器,寄存器,堆栈

并发和并行区别

并发:一个处理器同时处理多个任务。例:一个人同时吃三个馒头
并行:多个处理器或者是多核的处理器同时处理多个不同的任务。 例:三个人同时吃三个馒头。

创建方式,和启动

  1. 继承thread,重写run方法,调用start()
public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        MyTh1 myTh1 = new MyTh1();
        myTh1.start();
        for (int i = 0; i < 10; i++) {
            Thread.sleep(300);
            System.out.println("main:" + i);
        }
    }
}

class MyTh1 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            try {
                MyTh1.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("MyTh1:" + i);
        }
    }
}
  1. 实现runnable接口,重写run方法,调用start()
public class RunnableDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new MyTh2());
        thread.start();
        for (int i = 0; i < 10; i++) {
            Thread.sleep(300);
            System.out.println("main:" + i);
        }
    }
}

class MyTh2 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            try {
                MyTh1.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("MyTh2:" + i);
        }
    }
}
  1. 线程池
public class PoolDemo {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        while (true){
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + " is running ..");
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}
  1. 实现Callable接口,重新call方法
public class CallableDemo {
    public static void main(String[] args) throws Exception {
        Callable c = new Myth3();
        //1.执行Callable方式,需要FutureTask实现类的支持,用于接收运算结果
        FutureTask<String> result = new FutureTask<String>(c);
        new Thread(result).start();
        //2.接收线程运算后的结果
        try {
            String r = result.get(); //FutureTask 可用于闭锁
            System.out.println(r);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

class Myth3 implements Callable{

    @Override
    public String call() throws Exception {
        System.out.println("Myth3");
        return Thread.currentThread().getName();
    }
}

线程状态

在这里插入图片描述

线程基础方法

  1. wait()
    暂定当前正在执行的线程,并释放资源锁,让其他线程可以有机会运行。
  2. sleep()
    使当前线程进入休眠,不会释放资源锁。
  3. yield()
    暂停当前正在执行的线程,并执行其他线程。(可能没有效果)
  4. join()
    等待其他线程终止,在当前线程中调用一个线程的 join() 方法,则当前线程转为阻塞
    状态,当另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待 cpu 的宠幸。
  5. notify()
    通知一个线程继续运行。
  6. setDaemon()
    设为守护线程

线程死锁

必备条件

  1. 互斥条件:该资源任意⼀个时刻只由⼀个线程占⽤。
  2. 请求与保持条件:⼀个进程因请求资源⽽阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:线程已获得的资源在末使⽤完之前不能被其他线程强⾏剥夺,只有⾃⼰使⽤完毕后
    才释放资源。
  4. 循环等待条件:若⼲进程之间形成⼀种头尾相接的循环等待资源关系。

Demo

public class DeadLockDemo {
    private static Object resource1 = new Object();
    private static Object resource2 = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            while (true) {
                synchronized (resource1) {
                    System.out.println(Thread.currentThread().getName() + ":get resource1");
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":waiting get resource2");
                    synchronized (resource2) {
                        System.out.println(Thread.currentThread().getName() + ":get resource2");
                    }
                }
            }
        }, "线程1").start();

        new Thread(() -> {
            while (true) {
                synchronized (resource2) {
                    System.out.println(Thread.currentThread().getName() + ":get resource2");
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":waiting get resource1");

                    synchronized (resource1) {
                        System.out.println(Thread.currentThread().getName() + ":get resource1");
                    }
                }
            }
        }, "线程2").start();
    }
}

运行结果:

线程1:get resource1
线程2:get resource2
线程1:waiting get resource2
线程2:waiting get resource1

避免线程死锁

  1. 破坏互斥条件 :这个条件我们没有办法破坏,因为我们⽤锁本来就是想让他们互斥的(临界资源需要互斥访问)。
  2. 破坏请求与保持条件 :⼀次性申请所有的资源。
  3. 破坏不剥夺条件 :占⽤部分资源的线程进⼀步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  4. 破坏循环等待条件 :靠按序申请资源来预防。按某⼀顺序申请资源,释放资源则反序释放。破坏循环等待条件。

Volatile

Volatile的作用

  1. 保证变量可见性(当⼀个变量对共享变量进⾏了修改,那么另外的线程都是⽴即可以看到修改后的最新值)
  2. 禁止重排序
  3. 不保证原子性。

JMM(内存模型)

在这里插入图片描述

  • Java内存模型中规定了所有的变量都存储在主内存中;
  • 每条线程自己的工作内存
  • 线程的工作内存中保存了该线程使用的变量主内存副本拷贝
  • 线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量;
  • 不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要在主内存来完成。

重排序

一般来说处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的

为了遵守as-if-serial语义,编译器和处理器不会存在数据依赖关系的操作做重排序。

在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果(这也是as-if-serial语义允许对存在控制依赖的操作做重排序的原因);但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果

class ReorderExample {
	int a = 0;
	boolean flag = false;

	public void writer() {
	    a = 1;                   //1
	    flag = true;             //2
	}

	public void reader() {
	    if (flag) {                //3
	        int i =  a * a;        //4
	        ……
	    }
	}
}

现在有2个线程分别调用writer和reader方法,如果对1,2步骤做了重排序,结果就不一样。

ThreadLocal

参考:
https://www.cnblogs.com/bangiao/p/13204983.html

https://www.bilibili.com/video/BV117411g7ib?from=search&seid=17065525847319365701

作用

ThreadLocal对象可以提供线程局部变量,每个线程Thread拥有一份自己的副本变量,多个线程互不干扰。

原理

Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap

每个线程在往ThreadLocal里放值的时候,都会往自己的ThreadLocalMap里存,读也是以ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。
ThreadLocalMap有自己的独立实现,可以简单地将它的key为ThreadLocal的弱引用,value为代码中放入的值。

ThreadLocalMap有点类似HashMap的结构,只是HashMap是由数组+链表实现的,而ThreadLocalMap中并没有链表结构。
我们还要注意Entry, 它的key是``ThreadLocal<?> k ,继承自`WeakReference, 也就是我们常说的弱引用类型。

在这里插入图片描述
图片来源:马士兵

强引用,软引用,弱引用,虚引用

  • 强引用: 引用变量指向null,回收,比如:Object obj = new Object(); --> Object obj = null;
  • 软引用: 内存不够用时,回收 ,SoftReference类实现软引用, 可以用于缓存
  • 弱引用: 发生GC,就会被回收, 比如: ThreadLocalMap的Entry对象
  • 虚引用: 可以理解为跟强引用对象没了引用变量一样, 随时可以被回收, 当对象被回收时通过Queue可以检测到,PhantomReference来实现虚引用,通常用于处理堆外内存 如NIO 直接内存

内存泄漏

ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收**,这个时候就可能会产生内存泄露
ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。

使用完 ThreadLocal方法后 最好手动调用remove()方法。

JUC

CAS

CAS(Compare And Swap/Set)比较并交换,
CAS 算法的过程是这样:它包含 3 个参数CAS(V,E,N)。

  • V 表示要更新的变量(主内存),
  • E 表示预期值(本地内存),
  • N 表示新值。

当且仅当 V 值等于 E 值时,才会将 V 的值设为 N,如果 V 值和 E 值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后, CAS 返回 V 的真实值。
一般情况下是一个自旋操作,即不断的重试

ABA 问题
如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。

JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力,其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

AQS

参考:
https://blog.csdn.net/TZ845195485/article/details/109210263

基础概念

简单点说AQS就是基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,同时等待被唤醒。

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

内部Node

在这里插入图片描述

详细步骤

在这里插入图片描述

共享锁/独占锁

AQS定义了两种资源共享方式

  • Exclusive:独占,只有一个线程能执行。
    以 ReentrantLock 为例,state 初始化为 0,表示未锁定状态。A 线程 lock()时,会调用 tryAcquire()独占该锁并将 state+1。此后,其他线程再 tryAcquire()时就会失败,直到 A 线程 unlock()到 state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证 state 是能回到零态的。

  • Share:共享,多个线程可以同时执行,如Semaphore、CountDownLatch、ReadWriteLock、CycleBarrier。
    以 CountDownLatch 以例,任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后 countDown()一次,state 会 CAS(Compare and Swap)减 1。等到所有子线程都执行完后(即 state=0),会 unpark()主调用线程,然后主调用线程就会从 await()函数返回,继续后余动作。

公平锁/非公平锁

  • 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
    在这里插入图片描述

  • 非公平锁:当线程要获取锁时,先通过两次 CAS 操作去抢锁,如果没抢到,当前线程再加入到队列中等待唤醒。ReentrantLock默认。
    在这里插入图片描述

LockSupport

在这里插入图片描述

什么是LockSupport
  • 通过park()和unpark(thread)方法来实现阻塞和唤醒线程的操作。
  • LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。归根结底,LockSupport调用的Unsafe中的native代码。
  • 官网解释:
    LockSupport是用来创建锁和其他同步类的基本线程阻塞原语;
    LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),permit只有两个值1和零,默认是零;
    可以把许可看成是一种(0,1)信号量(Semaphore),但与Semaphore不同的是,许可的累加上限是1。
阻塞方法
  • permit默认是0,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时, park方法会被唤醒,然后会将permit再次设置为0并返回。
  • static void park( ):底层是unsafe类native方法
  • static void park(Object blocker)
唤醒方法(注意这个permit最多只能为1)
  • 调用unpark(thread)方法后,就会将thread线程的许可permit设置成1(注意多次调用unpark方法,不会累加,permit值还是1)会自动唤醒thread线程,即之前阻塞中的LockSupport.park()方法会立即返回
  • static void unpark( )
对比(LockSupport的优点)
  • 传统的synchronized和Lock实现等待唤醒通知的约束
  1. 线程先要获得并持有锁,必须在锁块(synchronized或lock)中
  2. 必须要先等待后唤醒,线程才能够被唤醒
  • LockSupport
  1. 无锁块要求
  2. 之前错误的先唤醒后等待,LockSupport照样支持
Demo
/*
(1).阻塞
 (permit默认是O,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,
 park方法会被唤醒,然后会将permit再次设置为O并返回)
 static void park()
 static void park(Object blocker)
(2).唤醒
static void unpark(Thread thread)
 (调用unpark(thread)方法后,就会将thread线程的许可permit设置成1(注意多次调用unpark方法,不会累加,
 permit值还是1)会自动唤醒thread线程,即之前阻塞中的LockSupport.park()方法会立即返回)
 static void unpark(Thread thread)
* */
public class LockSupportDemo {
    public static void main(String[] args) {

        Thread t1=new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"\t"+"coming....");
            LockSupport.park();
            /*
            如果这里有两个LockSupport.park(),因为permit的值为1,上一行已经使用了permit
            所以下一行被注释的打开会导致程序处于一直等待的状态
            * */
            //LockSupport.park();
            System.out.println(Thread.currentThread().getName()+"\t"+"被B唤醒了");
            },"A");
        t1.start();

        //下面代码注释是为了A线程先执行
        //try { TimeUnit.SECONDS.sleep(3);  } catch (InterruptedException e) {e.printStackTrace();}

        Thread t2=new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"\t"+"唤醒A线程");
            //有两个LockSupport.unpark(t1),由于permit的值最大为1,所以只能给park一个通行证
            LockSupport.unpark(t1);
            //LockSupport.unpark(t1);
        },"B");
        t2.start();
    }
}
面试题目:
  1. 为什么可以先唤醒线程后阻塞线程?
    (因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞)
  2. 为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?
    (因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证;而调用两次park却需要消费两个凭证,证不够,不能放行)

原子类

原子类介绍

在这里插入图片描述

常用方法

在这里插入图片描述

底层原理

在这里插入图片描述

Semaphore(信号量)

Semaphore 可以控制同时访问的线程个数, 通过acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。
Semaphore可以用来构建一些对象池,资源池之类的,比如数据库连接池。

场景:5台机器,20个工人,每个机器只能由1个工人使用,使用完成后可以交给其他人使用。

public class SemaphoreExample {
    // 请求的数量
    private static final int threadCount = 20;

    public static void main(String[] args) throws InterruptedException {
        ExecutorService threadPool = Executors.newFixedThreadPool(300);
        // 一次只能允许执行的线程数量。
        final Semaphore semaphore = new Semaphore(5);
        for (int i = 0; i < threadCount; i++) {
            final int threadnum = i;
            threadPool.execute(() -> {// Lambda 表达式的运用
                try {
                    semaphore.acquire();
                    System.out.println("工人" + threadnum + "占用一个机器在生产...");
                    Thread.sleep(3000);
                    System.out.println("工人" + threadnum + "释放出机器");
                    semaphore.release();// 释放一个许可
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        threadPool.shutdown();
    }
}

运行结果:

工人0占用一个机器在生产...
工人4占用一个机器在生产...
工人1占用一个机器在生产...
工人3占用一个机器在生产...
工人2占用一个机器在生产...
工人2释放出机器
工人4释放出机器
工人0释放出机器
工人1释放出机器
工人3释放出机器
工人8占用一个机器在生产...
工人7占用一个机器在生产...
工人5占用一个机器在生产...
工人6占用一个机器在生产...
工人9占用一个机器在生产...
工人7释放出机器
工人5释放出机器
工人8释放出机器
工人9释放出机器
工人6释放出机器

CountDownLatch(线程计数器)

允许一个或多个线程,等待其他一组线程完成操作,再继续执行。

CountDownLatch 是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当 CountDownLatch 使用完毕后,它不能再次被使用。

两种典型用法

  1. 某一线程在开始运行前等待 n 个线程执行完毕。将 CountDownLatch 的计数器初始化为 n :new CountDownLatch(n),每当一个任务线程执行完毕,就将计数器减 1 countdownlatch.countDown(),当计数器的值变为 0 时,在CountDownLatch上 await() 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。
public class CountDownLatchExample {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("当前线程开始等待其他2个线程执行完毕...");
        System.out.println("------------------");
        CountDownLatch countDownLatch = new CountDownLatch(2);
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("其他线程A," + Thread.currentThread().getName() + "开始执行...");
                countDownLatch.countDown();// 每次减去1
                System.out.println("其他线程A," + Thread.currentThread().getName() + "结束执行...");
                System.out.println("需等待完成的线程数 -1");
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("其他线程B," + Thread.currentThread().getName() + "开始执行...");
                countDownLatch.countDown();
                System.out.println("其他线程B," + Thread.currentThread().getName() + "结束执行...");
                System.out.println("需等待完成的线程数 -1");
            }
        }).start();
        countDownLatch.await();// 调用当前方法主线程阻塞  countDown结果为0, 阻塞变为运行状态
        System.out.println("------------------");
        System.out.println("需等待完成的线程 全部执行完毕...");

        System.out.println("继续当前线程执行...");
    }
}

运行结果:

当前线程开始等待其他2个线程执行完毕...
------------------
其他线程A,Thread-0开始执行...
其他线程A,Thread-0结束执行...
需等待完成的线程数 -1
其他线程B,Thread-1开始执行...
其他线程B,Thread-1结束执行...
需等待完成的线程数 -1
------------------
需等待完成的线程 全部执行完毕....
继续当前线程执行..
  1. 实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的 CountDownLatch 对象,将其计数器初始化为 1 :new CountDownLatch(1)多个线程在开始执行任务前首先 coundownlatch.await(),当主线程调用 countDown() 时,计数器变为 0,多个线程同时被唤醒
public class CountDownLatchExample2 {
    public static void main(String[] args) throws Exception {
        System.out.println("当前线程说 请准备!!!!");
        CountDownLatch countDownLatch = new CountDownLatch(1);
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程" + Thread.currentThread().getName() + "已准备...");
                try {
                    countDownLatch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程" + Thread.currentThread().getName() + "开始执行任务");
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程" + Thread.currentThread().getName() + "已准备...");
                try {
                    countDownLatch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程" + Thread.currentThread().getName() + "开始执行任务");
            }
        }).start();
        Thread.sleep(6000);
        System.out.println("当前线程说 开始!!!!");
        countDownLatch.countDown();
    }
}

运行结果:

当前线程说 请准备!!!!
线程Thread-0已准备...
线程Thread-1已准备...
当前线程说 开始!!!!
线程Thread-1开始执行任务
线程Thread-0开始执行任务

CyclicBarrier(循环栅栏)

允许一组线程相互之间等待,达到一个共同点,再继续执行。

class Writer extends Thread {
    private CyclicBarrier cyclicBarrier;

    public Writer(CyclicBarrier cyclicBarrier) {
        this.cyclicBarrier = cyclicBarrier;
    }

    @Override
    public void run() {
        System.out.println("线程" + Thread.currentThread().getName() + ",开始处理数据");
        try {
            Thread.sleep(1000);
            System.out.println("线程" + Thread.currentThread().getName() + ",处理完成,进入等待状态");
            cyclicBarrier.await();
        } catch (Exception e) {
        }
        System.out.println("其他线程都处理完了...线程" + Thread.currentThread().getName()+",开始进行下一步操作.......");
    }
}

public class CyclicBarrierExample {
    public static void main(String[] args) throws InterruptedException {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
        for (int i = 0; i < 3; i++) {
            Writer writer = new Writer(cyclicBarrier);
            writer.start();
        }
    }
}

运行结果:

线程Thread-1,开始处理数据
线程Thread-2,开始处理数据
线程Thread-0,开始处理数据
线程Thread-1,处理完成,进入等待状态
线程Thread-2,处理完成,进入等待状态
线程Thread-0,处理完成,进入等待状态
其他线程都处理完了...线程Thread-0,开始进行下一步操作.......
其他线程都处理完了...线程Thread-2,开始进行下一步操作.......
其他线程都处理完了...线程Thread-1,开始进行下一步操作.......

Synchronized

参考:
https://mp.weixin.qq.com/s/2ka1cDTRyjsAGk_-ii4ngw

用户态和内核态

Linux系统的体系结构大家大学应该都接触过了,分为用户空间(应用程序的活动空间)和内核。

我们所有的程序都在用户空间运行,进入用户运行状态也就是(用户态),但是很多操作可能涉及内核运行,比我I/O,我们就会进入内核运行状态(内核态)。
在这里插入图片描述
这个过程是很复杂的,也涉及很多值的传递,我简单概括下流程:

  1. 用户态把一些数据放到寄存器,或者创建对应的堆栈,表明需要操作系统提供的服务。
  2. 用户态执行系统调用(系统调用是操作系统的最小功能单位)。
  3. CPU切换到内核态,跳到对应的内存指定的位置执行指令。
  4. 系统调用处理器去读取我们先前放到内存的数据参数,执行程序的请求。
  5. 调用完成,操作系统重置CPU为用户态返回结果,并执行下个指令。
    所以大家一直说,1.6之前是重量级锁,没错,但是他重量的本质,是ObjectMonitor调用的过程,以及Linux内核的复杂运行机制决定的,大量的系统资源消耗,所以效率才低。

Java对象的构成

在 JVM 中,对象在内存中分为三块区域:

  • 对象头
    Mark Word(标记字段):默认存储对象的HashCode(如果有调用)GC信息锁信息。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。
    在这里插入图片描述
    图片来源:马士兵教育

    Klass Point(类型指针):对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例

  • 实例数据
    这部分主要是存放类的数据信息,父类的信息。

  • 对其填充
    由于虚拟机要求对象起始地址必须是8字节的整数倍,填充数据不是必须存在的,仅仅是为了字节对齐。

    Tip:不知道大家有没有被问过一个空对象占多少个字节?就是8个字节,是因为对齐填充的关系哈,不到8个字节对其填充会帮我们自动补齐。

Monitor对象

每个对象都有一个Monitor对象相关联,Monitor对象中记录了持有锁的线程信息等待队列等。Monitor对象包含以下三个字段:

  • owner 记录当前持有锁的线程
  • EntryList 是一个队列,记录所有阻塞等待锁的线程
  • WaitSet 也是一个队列,记录调用 wait() 方法并还未被通知的线程

当线程持有锁的时候,线程id等信息会拷贝进owner字段,其余线程会进入阻塞队列entrylist,当持有锁的线程执行wait方法,会立即释放锁进入waitset,当线程释放锁的时候,owner会被置空,公平锁条件下,entrylist中的线程会竞争锁,竞争成功的线程id会写入owner,其余线程继续在entrylist中等待。

Synchronized介绍

在 Java 早期版本中, synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的, Java 的线程是映射到操作系统的原⽣线程之上的。如果要挂起或者唤醒⼀个线程,都需要操作系统帮忙完成,⽽操作系统实现线程之间的切换时需要从⽤户态转换到内核态,这个状态之间的转换需要相对⽐较⻓的时间,时间成本相对较⾼,这也是为什么早期的synchronized 效率低的原因。

庆幸的是在 Java 6 之后 Java 官⽅对从 JVM 层⾯对synchronized较⼤优化,所以现在的 synchronized 锁效率也优化得很不错了。 JDK1.6对锁的实现引⼊了⼤量的优化,如⾃旋锁、适应性⾃旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

synchronized 关键字底层原理属于 JVM 层⾯

底层原理

  1. synchronized 同步语句块的情况
    在这里插入图片描述
    synchronized 同步语句块的实现使⽤的是 monitorenter 和 monitorexit 指令,其中 monitorenter指令指向同步代码块的开始位置, monitorexit 指令则指明同步代码块的结束位置。 当执⾏monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中, synchronized 锁便是通过这种⽅式获取锁的,也是为什么Java中任意对象可以作为锁的原因)的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执⾏monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外⼀个线程释放为⽌。
    为什么有两次monitorexit?
    第一次是正常情况下退出,第二次是异常情况下退出。

  2. synchronized 修饰⽅法的的情况
    在这里插入图片描述
    synchronized 修饰的⽅法并没有 monitorenter 指令和 monitorexit 指令,取得代之的是
    ACC_SYNCHRONIZED 标识,该标识指明了该⽅法是⼀个同步⽅法, JVM 通过ACC_SYNCHRONIZED 访问标志来辨别⼀个⽅法是否声明为同步⽅法,从⽽执⾏相应的同步调⽤。

三种使用情况

  1. 修饰代码块:
    同步代码块
synchronized(对象)//这个对象可以为任意对象 
{ 
    需要被同步的代码 
} 
  1. 修饰实例方法:
    同步方法
    在方法上修饰synchronized 称为同步方法
public synchronized void sale() {
	if (trainCount > 0) {
		System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - trainCount + 1) + "张票");
		trainCount--;
	}
}

同步方法使用的是什么锁?
同步方法使用的是同步代码块this锁

• 修饰静态方法:
方法上加上static关键字,使用synchronized 关键字修饰 或者使用类.class文件。
静态的同步方法使用的锁是 当前类的字节码文件对象

public static void sale() {
		synchronized (ThreadTrain3.class) {
			if (trainCount > 0) {
				System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - trainCount + 1) + "张票");
				trainCount--;
			}
		}
}

锁升级

参考:
https://www.cnblogs.com/snow-man/p/10874464.html

锁的4中状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态(级别从低到高)

升级方向无锁状态->偏向锁状态->轻量级锁状态->重量级锁状态,这个升级过程是不可逆的。

  1. 偏向锁状态
  • 偏向锁的基本原理
    前面说过,大部分情况下,锁不仅仅不存在多线程竞争, 而是总是由同一个线程多次获得,为了让线程获取锁的代 价更低就引入了偏向锁的概念。怎么理解偏向锁呢? 当一个线程访问加了同步锁的代码块时,会在对象头中存储当前线程的 ID,后续这个线程进入和退出这段加了同步 锁的代码块时,不需要再次加锁和释放锁。而是直接比较 对象头里面是否存储了指向当前线程的偏向锁。如果相等 表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了

  • 偏向锁的获取和撤销逻辑
    (1)首先获取锁 对象的 Markword,判断是否处于可偏向状态。(biased_lock=1、且 ThreadId 为空)
    (2)如果是可偏向状态,则通过 CAS 操作,把当前线程的 ID 写入到 MarkWord) 如果 cas 成功,那么 markword 就会变成这样。 表示已经获得了锁对象的偏向锁,接着执行同步代码块, 如果 cas 失败,说明有其他线程已经获得了偏向锁, 这种情况说明当前锁存在竞争,需要撤销已获得偏向锁的线程,并且把它持有的锁升级为轻量级锁(这个操作需要等到全局安全点,也就是没有线程在执行字节码)才能执行
    (3)如果是已偏向状态,需要检查 markword 中存储的 ThreadID 是否等于当前线程的 ThreadID ,如果相等,不需要再次获得锁,可直接执行同步代码块,如果不相等,说明当前锁偏向于其他线程,需要撤销偏向锁并升级到轻量级锁

  • 偏向锁的撤销
    偏向锁的撤销并不是把对象恢复到无锁可偏向状态(因为 偏向锁并不存在锁释放的概念),而是在获取偏向锁的过程 中,发现 cas 失败也就是存在线程竞争时,直接把被偏向的锁对象升级到被加了轻量级锁的状态。

  • 对原持有偏向锁的线程进行撤销时,原获得偏向锁的线程 有两种情况:
    (1) 原获得偏向锁的线程如果已经退出了临界区,也就是同步代码块执行完了,那么这个时候会把对象头设置成无锁状态并且争抢锁的线程可以基于 CAS 重新偏向当前线程
    (2) 如果原获得偏向锁的线程的同步代码块还没执行完,处于临界区之内,这个时候会把原获得偏向锁的线程升级为轻量级锁后继续执行同步代码块

  1. 轻量级锁状态
    锁升级为轻量级锁之后,对象的 Markword也会进行相应的的变化。升级为轻量级锁的过程:
    (1) 线程在自己的栈桢中创建锁记录 LockRecord。
    (2) 将锁对象的对象头中的MarkWord复制到线程的刚刚创建的锁记录LockRecord中。
    (3) 将锁记录中的 Owner 指针指向锁对象。
    (4) 将锁对象的对象头的 MarkWord替换为指向锁记录的指针。

    自旋锁
    轻量级锁在加锁过程中,用到了自旋锁。
    所谓自旋,就是指当有另外一个线程来竞争锁时,这个线 程会在原地循环等待,而不是把该线程给阻塞,直到那个获得锁的线程释放锁之后,这个线程就可以马上获得锁的。
    注意,锁在原地循环的时候,是会消耗 cpu 的,就相当于 在执行一个啥也没有的 for 循环。
    所以,轻量级锁适用于那些同步代码块执行的很快的场景,这样,线程原地等待很短的时间就能够获得锁了。 自旋锁的使用,其实也是有一定的概率背景,在大部分同 步代码块执行的时间都是很短的。所以通过看似无异议的 循环反而能提升锁的性能。 但是自旋必须要有一定的条件控制,否则如果一个线程执行同步代码块的时间很长,那么这个线程不断的循环反而 会消耗 CPU 资源。默认情况下自旋的次数是 10 次, 可以通过 preBlockSpin 来修改
    在 JDK1.6 之后,引入了自适应自旋锁,自适应意味着自旋 的次数不是固定不变的,而是根据前一次在同一个锁上自 旋的时间以及锁的拥有者的状态来决定。 如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并 且持有锁的线程正在运行中,那么虚拟机就会认为这次自 旋也是很有可能再次成功,进而它将允许自旋等待持续相 对更长的时间。
    如果对于某个锁,自旋很少成功获得过, 那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。

  2. 重量级锁状态
    当轻量级锁自旋超过阈值,膨胀到重量级锁之后,意味着线程只能被挂起阻塞来等待被唤醒了。

Synchronized与Lock对比

  • synchronized是关键字,是JVM层面的底层啥都帮我们做了,而Lock是一个接口,是JDK层面的有丰富的API。
  • synchronized会自动释放锁,而Lock必须手动释放锁。
  • synchronized是不可中断的,Lock可以中断也可以不中断。
  • 通过Lock可以知道线程有没有拿到锁,而synchronized不能。
  • synchronized能锁住方法和代码块,而Lock只能锁住代码块。
  • Lock可以使用读锁提高多线程读效率。
  • synchronized是非公平锁,ReentrantLock可以控制是否是公平锁

线程池

参考:
Java线程池实现原理及其在美团业务中的实践

新手也能看懂的线程池学习总结

概念

线程池是指在初始化一个多线程应用程序过程中创建一个线程集合,然后在需要执行新的任务时重用这些线程而不是新建一个线程。

作用

第一:降低资源消耗。
第二:提高响应速度。
第三:提高线程的可管理性。

ThreadPoolExecutor

  /**
     * 用给定的初始参数创建一个新的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;
    }
  • 核心参数
  1. corePoolSize : 核心线程数线程数定义了最小可以同时运行的线程数量。
  2. maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
  3. workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,信任就会被存放在队列中

推荐使用 ThreadPoolExecutor 构造函数创建线程池原因

这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

通过 Executor 框架的工具类 Executors 来实现存在以下问题:

  • FixedThreadPool 和 SingleThreadExecutor :允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。
  • CachedThreadPool 和 ScheduledThreadPool :允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。
//newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>(),
                                      threadFactory);
    }


//newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>(),
                                    threadFactory));
    }

//newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>(),
                                      threadFactory);
    }
    
//newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }

流程

在这里插入图片描述

计算线程池最多能接受多少任务

在这里插入图片描述
上面的线程池中最多接受 10(队列长度) + 4(最大线程数) = 14个任务。

几个常见的对比

Runnable vs Callable

Runnable 接口不会返回结果或抛出检查异常,但是Callable 接口可以。所以,如果任务不需要返回结果或抛出异常推荐使用 Runnable 接口,这样代码看起来会更加简洁。

@FunctionalInterface
public interface Runnable {
   /**
    * 被线程执行,没有返回值也无法抛出异常
    */
    public abstract void run();
}
@FunctionalInterface
public interface Callable<V> {
    /**
     * 计算结果,或在无法这样做时抛出异常。
     * @return 计算得出的结果
     * @throws 如果无法计算结果,则抛出异常
     */
    V call() throws Exception;
}

execute() vs submit()

  1. execute() 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
  2. submit() 方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Future 的 get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

shutdown() VS shutdownNow()

  1. shutdown() :关闭线程池,线程池的状态变为 SHUTDOWN。线程池不再接受新任务了,但是队列里的任务得执行完毕。
  2. shutdownNow() :关闭线程池,线程的状态变为 STOP。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List。

isTerminated() VS isShutdown()

  1. isShutDown() 当调用 shutdown() 方法后返回为 true。
  2. isTerminated() 当调用 shutdown() 方法后,并且所有提交的任务完成后返回为 true

参数设置

有一个简单并且适用面比较广的公式:

  • CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
  • I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N
  • 可以考虑把核心线程数、最大线程数设置为一样,allowCoreThreadTimeOut 参数设置为 true,用来提高活动线程数,当 allowCoreThreadTimeOut 参数设置为 true 的时候,核心线程在空闲了 keepAliveTime 的时间后也会被回收的,相当于线程池自动给你动态修改了活动线程数。

拒绝策略

在这里插入图片描述

WorkQueue

  1. ArrayBlockingQueue是一个有边界的阻塞队列,它的内部实现是一个数组。有边界的意思是它的容量是有限的,我们必须在其初始化的时候指定它的容量大小,容量大小一旦指定就不可改变。
  2. LinkedBlockingQueue阻塞队列大小的配置是可选的,如果我们初始化时指定一个大小,它就是有边界的,如果不指定,它就是无边界的。说是无边界,其实是采用了默认大小为Integer.MAX_VALUE的容量 。它的内部实现是一个链表。
  3. PriorityBlockingQueue是一个没有边界的队列,它的排序规则和 java.util.PriorityQueue一样。需要注意,PriorityBlockingQueue中允许插入null对象。
  4. SynchronousQueue队列内部仅允许容纳一个元素。当一个线程插入一个元素后会被阻塞,除非这个元素被另一个线程消费。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值