高并发-个人笔记

目录

一、认识并发编程

1.1 线程

1.2 多线程

1.3 线程安全

第二章 JUC并发介绍

1.1 并发包介绍

1.2 JMM(Java Memory Model)

 1.3 volatile关键字

1.3.1 可见性

1.3.2 原子性

1.3.3 有序性

第二章 CAS

2.1 CAS是什么?

2.2 CAS底层原理?

 2.3 CAS缺点

 2.4 CAS会导致“ABA问题”

2.4.1 AtomicReference原⼦引⽤

 2.4.2 ABA问题的解决(AtomicStampedReference 类似于时间戳)

第三章 阻塞队列

 1.1 阻塞队列概述

1.2.1 传统模式

1.2.2 ⽣产者消费者防⽌虚假唤醒(执⾏原理分析)

1.2.3 新版⽣产者消费者写法 ReentrantLock.Condition

 1.2.4 精准通知顺序访问

 1.2.5 Synchronized和Lock的区别

1.2.6 阻塞队列模式⽣产者消费者

第四章 线程池

 1.1 线程池基本概念

1.2 线程池三种常⽤创建⽅式

 1.2.1 newFixedThreadPool线程池

 1.2.2 newSingleThreadExecutor线程池

 1.2.3 newCachedThreadPool线程池

 1.2.4 线程池代码演示

1.3 线程池创建的七个参数

 1.4 线程池底层原理

 1.5 线程池的拒绝策略

1.5.1 AbortPolicy拒绝策略

 1.5.2 CallerRunsPolicy拒绝策略

 1.5.3 DiscardOldestPolicy拒绝策略

 1.5.4 DiscardPolicy拒绝策略

1.6 实际⽣产使⽤哪⼀个线程池?

 第五章 死锁编码和定位

 2.1 死锁编码

 2.2 死锁定位


一、认识并发编程

1.1 线程

1.1.1 进程和线程

进程

进程:进程指正在运行的程序,进程拥有一个完整的、私有的基本运行资源集合。通常,每个进程都有自己的内存空间。

进程往往被看做是程序或者应用的代名词,然而,用户看到的一个单独的应用程序实际上可能是一组相互协作的进程集合。

为了便于进程之间的通信,大多数操作系统都支持进程间通信(IPC),如pips和sockets。IPC不仅支持统一系统上的通信,也支持不同的系统。IPC通信方式包括管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Stream等方式,其中Socket和Streams支持不用主机上的两个进程IPC。

线程

线程有时也被成为轻量级的进程。进程和线程都提供了一个执行环境,但创建可一个新的线程比创建一个新的进程需要的资源要少。

线程是在进程中存在的 -- 每个进程最少有一个线程。线程共享进程的资源,包括内存和代开的文件。这样提高了效率,但潜在的问题就是线程间的通信。多线程的执行时Java平台的一个基本特征。每个应用都至少有一个线程或几个,如果算上“系统”线程的话,比如内存管理和信号处理等。但是从程序员的角度来看,启动的只有一个,叫主线程。

简而言之:一个程序运行后至少有一个进程。一个进程中可以包含多个线程。

1.1.2 线程实践

1.1.2.1 线程的创建

两种方式

public class HelloThread extends Thread {
    public void run() {
        System.out.println("Hello from a thread!");
    }

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

结果:

 方式二:

public class HelloRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Hello from a thread!");
    }

    public static void main(String[] args) {
        HelloRunnable helloRunnable = new HelloRunnable();
        Thread thread = new Thread(helloRunnable);
        thread.start();
    }
}

结果:

 启动线程

调用start方法

停止线程

线程自带的stop方法,一方面已经过时,另一方面,不会对停止的线程做状态保存,是的线程中设计的对象处于位置的状态,如果这些状态,其他线程也会使用,将会使得其他线程出现无法预料的异常,所以,停止程序的功能,需要自己实现。

public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        StopThread stopThread = new StopThread();
        stopThread.start();
        Thread.sleep(1000L);
        stopThread.stop();
        while(stopThread.isAlive()){}
        stopThread.print();
    }

    private static class StopThread extends Thread {
        private int x = 0;
        private int y = 0;

        public void run(){
            synchronized (this) {
                ++x;
                try{
                    Thread.sleep(3000L);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                ++y;
            }
        }
        public void print() {
            System.out.println("x=" + x + " y =" + y);
        }
    }
}

结果:上述代码中,run方法里是一个同步的原子操作,x和y必须要共同增加,然而这里如果调用thread.stop()方法强制中断线程,输入如下:

 没有异常,也破坏了我们的预期。如果这种问题出现在我们的程序中,会引发难以预期的异常。因此这种不安全的方式很早就被废弃了。

自定义stop线程

public class MyRunnable implements Runnable {
    private boolean doStop = false;

    public synchronized void doStop(){
        this.doStop = true;
    }

    private synchronized boolean keepRunning() {
        return this.doStop == false;
    }
    @Override
    public void run() {
        while(keepRunning()){
            System.out.println(Thread.currentThread().getName());
            System.out.println("Running");
        }

        try {
            Thread.sleep(3L * 1000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}
public class MyRunnableMain {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();

        Thread thread = new Thread(myRunnable);

        thread.start();

        System.out.println(Thread.currentThread().getName());

        try {
            Thread.sleep(10L * 1000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        myRunnable.doStop();
    }

}

结果:

 1.1.3 线程暂停和中断

暂停

Java中线程的暂停就是调用java.lang.Threa类中的sleep方法。该方法会使当前正在执行的线程暂停指定的一段时间,如果线程持有锁,sleep方法结束前并不会释放锁。

中断

java.lang.Thread类有一个interrupt方法,该方法直接对线程调用。当被interrupt的线程正在sleep或wait时,会抛出InterruptedException异常。

事实上,interrupt方法只是改变目标线程的中断状态(interrupt status),而那些会抛出InterruptedException异常的方法,如wait、sleep、join等,都是在方法内部不断地检查中端状态的值;

  • interrupted方法

Thread实例方法:必须由其他线程获取被调用线程的实例后,进行调用。实际上,只是改变了被调用线程的内部中断状态;

  •  Thread.interrupted方法

Thread类方法:必须在当前执行线程内调用,该方法返回当前线程的内部中断状态,然后清除中断状态(置为false);

  • isInterrupted方法

Thread实例方法:用来检查指定线程中的中断状态。当线程为中断状态是,会返回true;否则返回false.

public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        StopThread stopThread = new StopThread();
        stopThread.start();
        Thread.sleep(1000L);
        stopThread.interrupt();
        while(stopThread.isAlive()){}
        stopThread.print();
    }

    private static class StopThread extends Thread {
        private int x = 0;
        private int y = 0;

        public void run(){
            synchronized (this) {
                ++x;
                try{
                    Thread.sleep(3000L);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                ++y;
            }
        }
        public void print() {
            System.out.println("x=" + x + " y =" + y);
        }
    }
}

interrupt底层源码

    /**
     * 中断此线程。
     * <p>线程可以中断自身,这是允许的。在这种情况下,不用进行安全性验证({@link #checkAccess() checkAccess} 方法检测)
     * <p>若当前线程由于 wait() 方法阻塞,或者由于join()、sleep()方法,然后线程的中断状态将被清除,并且将收到 {@link InterruptedException}。
     * <p>如果线程由于 IO操作({@link java.nio.channels.InterruptibleChannel InterruptibleChannel})阻塞,那么通道 channel 将会关闭,
     * 并且线程的中断状态将被设置,线程将收到一个 {@link java.nio.channels.ClosedByInterruptException} 异常。
     * <p>如果线程由于在 {@link java.nio.channels.Selector} 中而阻塞,那么线程的中断状态将会被设置,它将立即从选择操作中返回。
     *该值可能是一个非零值,就像调用选择器的{@link java.nio.channels.Selector#wakeupakeup}方法一样。
     *
     * <p>如果上述条件均不成立,则将设置该线程的中断状态。</p>
     * <p>中断未运行的线程不必产生任何作用。
     * @throws  SecurityException 如果当前线程无法修改此线程
     */
    public void interrupt() {
        //如果调用中断的是线程自身,则不需要进行安全性判断
        if (this != Thread.currentThread())
            checkAccess();
		
        synchronized (blockerLock) {
            Interruptible b = blocker;
            if (b != null) {
                interrupt0();           // 只是设置中断标志
                b.interrupt(this);
                return;
            }
        }
        interrupt0();
    }
    // 静态方法,这个方法有点坑,调用该方法调用后会自动清除中断状态
    public static boolean interrupted(){
        return currentThread.isInterrupted(true);
    }

    // 这个方法不会清除中断状态
    public boolean isIntruppted(){
        return isInterrupted(false);
    }
    
    // 上面两个方法会调用这个本地方法,参数代表是否清除中断状态
    private native boolean isInterrupted(boolean CleanInterrupted);

interrupt():

  • interrupt中断操作时,非自身打断需要先检测是否有中断权限,这由jvm的安全机制配置;
  • 如果线程处于sleep,wait,join等状态,那么线程将立即开启退出被阻塞状态,并抛出一个InterruptedException异常;
  • 如果线程处于I/O阻塞状态,将会抛出ClosedByInterruptException(IOException的子类)异常;
  • 如果线程处于Selector上被阻塞,select方法将立即返回;
  • 如果非以上情况,将直接标记interrupt状态

注意:interrupt操作不会打断所有阻塞,只有上述阻塞情况下才在jvm的打断范围内,入出入锁阻塞的线程,不会搜interrupt中断;

阻塞情况下中断,抛出异常后线程恢复非中断状态,即interrupted = false

public class ThreadTest1 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new Task("mytask"));
        t.start();
        t.interrupt();
    }

    static class Task implements Runnable{
        String name;

        public Task(String name){
            this.name = name;
        }

        public void run() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println("thread has been interrupt");
            }
            System.out.println("interrupted:" + Thread.currentThread().isInterrupted());
            System.out.println("task " + name + " is over");
        }
    }
}

 调用Thread.interrupted()方法后居然恢复非中断状态

public class ThreadTest2 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new Task("mytask"));
        t.start();
        t.interrupt();
    }

    static class Task implements Runnable{
        String name;

        public Task(String name){
            this.name = name;
        }

        public void run() {
            System.out.println("first:" + Thread.interrupted());
            System.out.println("second:" + Thread.interrupted());
            System.out.println("task " + name + " is over");
        }
    }
}

 1.1.2.4 线程的状态

Java线程可能的状态

状态名称                        说明
NEW        初始状态,线程被构建,但是还没有调用start()方法
RUNABLE运行状态,Java线程将操作系统中的就绪和运行两种状态笼统地称作“运行中”
BLOCKED阻塞状态,表示线程阻塞于锁

WAITING

等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断)
TIME_WAITING超时等待状态,该状态不用于waiting,它是可以在指定的时间自行返回的
TERMINATED终止状态,表示当前线程已经执行完毕

1.2 多线程

线程是进程的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

1.2.1 并发和并行

  • 并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一事件间隔发生。
  • 并行值在不同实体上的多个事件,并发是在同一个实体上的多个事件。
  • 在一台处理器上“同时”处理多个任务,在多台处理器上同时处理多个任务。如hadoop分布式集群

1.2.2 多线程好处

提高CPU的利用率

单线程

5 seconds reading file A

2 seconds processing file B

5 seconds reading file B

2 seconds processing file B

----------------------------------------

14 seconds total

多线程

5 seconds reading file A

5 seconds reading file B + 2 seconds processing file A

2 seconds processing file B

--------------------------------------------

12 seconds total

一般来说,在等待磁盘IO,网络IO或者等待用户输入时,CPU可以同时去处理其他业务

更高效的响应

多线程技术使程序的响应速度更快,因为用户界面可以在进行其他工作的同时一直处于活动状态,不会造成无法响应的现象。

公平使用CPU资源

当前没有进行处理的任务,可以将处理器时间让给其他任务,也可以定期将处理时间让给其他任务;通过对CPU时间的划分,使得CPU时间片可以在多个线程之间切换,避免需要长时间处理的线程独占CPU,导致其他线程长时间等待。

1.2.3 多线程的代价

更复杂的设计

共享数据的读取,数据的安全性,线程之间的交互,线程的同步等

上下文环境切换

线程切换,cpu需要保存本地数据、程序指针等内容

更多的资源消耗

每个线程都需要内存维护自己的本地栈信息,操作系统也需要资源对线程进行管理维护;

1.3 线程安全

1.3.1 基本概念

何谓静态条件

当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。

导致竞态条件发生的代码区称作临界区。

在临界区中使用适当的同步就可以避免竞态条件,如使用synchronized或者加锁机制。

何谓线程安全

允许被多个线程同时执行的代码称作线程安全的代码。线程安全的代码不包含竞态条件。

1.3.2 对象的安全

局部基本类型变量

局部变量存储在线程自己的栈中。也就是说,局部变量永远也不会被多个线程共享。所以,基础类型的局部变量是线程安全的。下面是基础类型的局部变量的一个例子:

public class ThreadTest3 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new MyThread());
        for (int i = 0; i < 50; i++) {
            new Thread(t,"线程"+i).start();
        }
    }

    static class MyThread implements Runnable{
        public void run(){
            int a = 0;
            ++a;
            System.out.println(Thread.currentThread().getName() + ":" + a);
        }
    }
}

 无论多少个线程对run()方法中的基本类型a执行++a操作,只是更新当前线程栈的值,不会影响其他线程,也就是不共享数据。

局部的对象引用

对象的局部引用和基础类型的局部变量不太一样,尽管引用本身没有被共享,但引用所指的对象并没哟存储在线程的栈内。所有的对象都存在共享堆中。如果在某个方法中创建的对象不会逃逸出(即该对象不会被其方法获得,也不会被非局部变量引用到)该方法。那么它是线程安全的。实际上,哪怕将这个对象作为参数传递给其他方法,只要别的线程获取不到这个对象,那它仍是线程安全的。

public void method1(){
    LocalObject localObject = new LocalObject();
    localObject.callMethod();
    method2(localhost);
}

public void method2(LocalObject localObject){
    localObject.setValue("value");
}

对象成员(成员变量)

对象成员存储在堆上,如果两个线程同时更新同一个对象的同一个成员,那这个代码就不是线程安全的。

public class ThreadTest{
    public static void mian(String[] args){
        NotThreadSafe sharedInstance = new NotThreadSafe();
        new Thread(new MyRunnable(sharedInstance)).start();
        new Thread(new MyRunnable(sharedInstance)).start();
    }
}

class MyRunnable implements Runnable{
    NotThreadSafe instance = null;
    public MyRunnable(NotThreadSafe instance){
        this.instance = instance;
    }
}

public void run(){
    this.instance.add(" "+Thread.currentThread().geyName());
    System.out.println(this.instance.builder.toString());
}

class NotThreadSafe{
    StringBuffer builder = new StringBuffer();
    
    public void add(Sttring text){
        this.builder.append(text);
    }
}

如果两个线程同时调用同一个NotThreadSafe实例上的add方法,就会有竞态条件问题。

1.3.3 不可变性

通过创建不可变的共享对象来保证对象在线程间共享时不会被修改,从而实现线程安全。如下实例:

public class ImmutableValue{
    private int value =0;

    public ImmutableValue(int value){
        this.value = value;
    }

    public int getValue(){
        return this.value;
    }
}

请注意ImmutableValue类的成员变量value是通过构造函数赋值的,并且在类中没有set方法。这意味着一旦ImmutableValue实例被创建,value变量就不能再被修改,这就是不可变性。但你可以通过getValue()方法读取这个变量的值。

第二章 JUC并发介绍


1.1 并发包介绍

1.2 JMM(Java Memory Model)

JMM 是指 Java 内存模型 ,不是 JVM ,不是所谓的栈、堆、⽅法区。
每个 Java 线程都有⾃⼰的 ⼯作内存 。操作数据,⾸先从主内存中读,得到⼀份拷⻉,操作完毕后再写回到主内存
由于 JVM 运⾏程序的实体是线程,⽽每个线程创建时 JVM 都会为其创建⼀个 ⼯作内存 (有些地⽅成为栈空间),⼯作内存是每个线程的私有数据区域,⽽ Java 内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必 须在⼯作内存中进⾏,⾸先要将变量从主内存拷⻉到⾃⼰的⼯作内存空间,然后对变量进⾏操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的⼯作内存中存储着主内存中的变量副本拷⻉,因此不同的线程间⽆法访问
对⽅的⼯作内存,线程间的通信(传值)必须通过主内存来完成,期间要访问过程如下图:

  • JMM可能带来可⻅性原⼦性有序性问题。
  1. 所谓可⻅性,就是某个线程对主内存内容的更改,应该⽴刻通知到其它线程。
  2. 所谓原⼦性,是指⼀个操作是不可分割的,不能执⾏到⼀半,就不执⾏了。
  3. 所谓有序性,就是指令是有序的,不会被重排。

 1.3 volatile关键字

volatile关键字是Java提供的一种轻量级同步机制。

  • 它能保证可见性有序性
  • 但不能保证原子性
  • 禁止指令重排

1.3.1 可见性

class MyData{
    int number = 0;
    public void setTo60(){
        this.number = 60;
    }
}
public class VolatileDemo {
    public static void main(String[] args) {
        System.out.println("可见性的测试");

        MyDatamyData = new MyData();
        // 启动一个线程,操作 number 变量
        new Thread(() ->{
            System.out.println(Thread.currentThread().getName() + "\t 执行");

            // 修改number的值
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            myData.setTo60();
            System.out.println(Thread.currentThread().getName()+ "\t 更新number的值===" + myData.number);

        },"线程A").start();

        // 在mian线程中,执行操作
        while (myData.number == 0){
           // 如果检测到的number值为0 main线程无线执行
        }
        System.out.println(Thread.currentThread().getName()+ "\t 获取到number的值===" + myData.number);
    }
}
MyData 类是资源类,⼀开始 number 变量没有⽤ volatile修饰,所以程序运⾏的结果是:
可见性的测试
线程A	 执行
线程A	 更新number的值===60
虽然⼀个线程把 number 修改成了 60 ,但是 main 线程持有的仍然是最开始的 0 ,所以⼀直循环,程序不会结束。
/**
 * Volatile 关键字
 */
class MyData{
    volatile int number = 0;
    public void setTo60(){
        this.number = 60;
    }
}
public class VolatileDemo {
    public static void main(String[] args) {
        System.out.println("可见性的测试");

        MyDatamyData = new MyData();
        // 启动一个线程,操作 number 变量
        new Thread(() ->{
            System.out.println(Thread.currentThread().getName() + "\t 执行");

            // 修改number的值
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            myData.setTo60();
            System.out.println(Thread.currentThread().getName()+ "\t 更新number的值===" + myData.number);

        },"线程A").start();

        // 在mian线程中,执行操作
        while (myData.number == 0){
           // 如果检测到的number值为0 main线程无线执行
        }
        System.out.println(Thread.currentThread().getName()+ "\t 获取到number的值===" + myData.number);
    }
}
如果对 number 添加了 volatile 修饰,运⾏结果是:
可见性的测试
线程A	 执行
线程A	 更新number的值===60
main	 获取到number的值===60
可⻅某个线程对 number 的修改,会⽴刻反映到主内存上。

1.3.2 原子性

原⼦性指的是什么意思?
不和分割,完整性,也即某个线程正则做某个具体业务时,中间不可以被加塞或者被分割。需要 整体完整,要么同时成功,要么同时失败。
class MyData1{
    // volatile并不能保证操作的原⼦性。这是因为,⽐如⼀条number++的操作,会形成3条指令。
    // int number = 0;
    volatile int number = 0;
    // 此时number前⾯已经加了volatile,但是不保证原⼦性
    public void addPlusPlus(){
        number++;
    }
}
public class ActomicDemo {
    public static void main(String[] args) {
        //volatileVisibilityDemo();
        atomicDemo();
    }
    private static void atomicDemo() {
        System.out.println("原⼦性测试");
        MyData1 myData = new MyData1();
        for (int i = 1; i <= 20; i++) {
            new Thread(()->{
                for (int j = 0; j < 1000 ; j++) {
                    myData.addPlusPlus();
                }
            },String.valueOf(i)).start();
        }
        while (Thread.activeCount() > 2){
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName()+"\t int类型最终number值: " + myData.number);
    }
}
volatile 不能保证操作的原⼦性 。这是因为,⽐如⼀条 number++ 的操作,会形成 3 条指令。
javap -c 包名.类名 

javap -c MyData
public void addPlusPlus();
 Code:
     0: aload_0
     1: dup
     2: getfield #2 // Field number:I //读
     5: iconst_1 //++常量1
     6: iadd //加操作
     7: putfield #2 // Field number:I //写操作
     10: return

假设有3个线程,分别执⾏number++,都先从主内存中拿到最开始的值,number=0,然后三个线程分别进⾏操作。假设线程0执⾏完毕,number=1,也⽴刻通知到了其它线程,但是此时线程1、2已经拿到了number=0,所以结果就是写覆盖,线程1、2将number变成1。
解决的⽅式就是:

  1.  对 addPlusPlus() ⽅法加锁。
  2. 使⽤ java.util.concurrent.AtomicInteger 类。

class MyData {
    volatile int number = 0;

    AtomicInteger atomicInteger = new AtomicInteger();

    public void setTo60(){
        this.number = 60;
    }

    public void addPlusPlus(){
        number++;
    }

    public void atomicPlusPlus(){
        atomicInteger.incrementAndGet();
    }
}
public class VolatileDemo {
    public static void main(String[] args) {
        // volatileTest();
        System.out.println("原子性测试");
        MyData myData = new MyData();
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    myData.addPlusPlus();
                    myData.atomicPlusPlus();
                }
            },String.valueOf(i) ).start();
        }

        // 当20个线程都执行完毕后,打印number的值
        while(Thread.activeCount() > 2){ // main线程, gc垃圾回收期线程
            Thread.yield();
        }
        System.out.println("main中打印最终的 number的值===" + myData.number);
        System.out.println("main中打印最终的 atomicInteger的值===" + myData.atomicInteger);
    }

    private static void volatileTest() {
        System.out.println("可见性的测试");

        MyData myData = new MyData();
        // 启动一个线程,操作 number 变量
        new Thread(() ->{
            System.out.println(Thread.currentThread().getName() + "\t 执行");

            // 修改number的值
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            myData.setTo60();
            System.out.println(Thread.currentThread().getName()+ "\t 更新number的值===" + myData.number);

        },"线程A").start();

        // 在mian线程中,执行操作
        while (myData.number == 0){
           // 如果检测到的number值为0 main线程无线执行
        }
        System.out.println(Thread.currentThread().getName()+ "\t 获取到number的值===" + myData.number);
    }
}
结果:可⻅,由于 volatile 不能保证原⼦性,出现了线程重复写的问题,最终结果⽐ 20000 ⼩。⽽ AtomicInteger 可以保证原⼦性。

1.3.3 有序性

计算机在执⾏程序时,为了提⾼性能,编译器和处理器常常会对指令做重排,⼀般分以下三种:

 单线程环境⾥⾯确保程序最终执⾏结果和代码顺序执⾏的结果⼀致;

处理器在进⾏重排序时必须要考虑指令之间的数据依赖性;
多线程环境中线程交替执⾏,由于编译器优化重排的存在,两个线程中使⽤的变量能否保证⼀致 性是⽆法确定的,结果⽆法预测。
volatile 可以保证 有序性 ,也就是防⽌ 指令重排序
所谓指令重排序 ,就是出于优化考虑, CPU 执⾏指令的顺序跟程序员⾃⼰编写的顺序不⼀致。就好⽐⼀份试卷,题号是⽼师规定的,是程序员规定的,但是考⽣( CPU )可以先做选择,也可以先做填空。

观看下⾯代码,在多线程场景下,说出最终值a的结果是多少? 5或者6我们采⽤ volatile 可实现禁⽌指令重排优化,从⽽避免多线程环境下程序出现乱序执⾏的现象

public class ResortSeqDemo {
     volatile int a = 0;
     volatile boolean flag = false;
     /*
     多线程下flag = true可能先执⾏,还没⾛到a=1就被挂起。
     其它线程进⼊method02的判断,修改a的值=5,⽽不是6。
     */
     public void method01(){
         a = 1;
         flag = true;
     } 

     public void method02(){
         if (flag){
             a += 5;
             System.out.println("*****最终值a: " + a);
         }
     }

     public static void main(String[] args) {
         ResortSeqDemo resortSeq = new ResortSeqDemo();
         new Thread(()->{resortSeq.method01();},"ThreadA").start();
         new Thread(()->{resortSeq.method02();},"ThreadB").start();
     }
}

 *****最终值a: 6

为什么 volatile 可实现禁⽌指令重排优化,从⽽避免多线程环境下程序出现乱序执⾏的现象?说说它的原理
我们先来了解⼀个概念, 内存屏障 Memory Barrier )⼜称内存栅栏,是⼀个 CPU 指令, volatile 底层就是⽤ CPU 内存屏障 Memory Barrier )指令来实现的,它有两个作⽤
  • ⼀个是保证特定操作的顺序性
  • ⼆是保证变量的可⻅性。

 

 由于编译器和处理器都能够执⾏指令重排优化。所以,如果在指令间插⼊⼀条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插⼊内存屏障可以禁⽌在内存屏障前后的指令进⾏重排序优化。内存屏障另外⼀个作⽤是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读到这些数据的最新版本。

哪些地⽅⽤到过 volatile
单例模式的安全问题
  • 传统
public class SingletonDemo {
    private static SingletonDemo instance = null;
    private SingletonDemo() {
        System.out.println(Thread.currentThread().getName() +"\t SingletonDemo构造⽅法执⾏了");
    }
    public static SingletonDemo getInstance(){
        if (instance == null) {
            instance = new SingletonDemo();
        }
        return instance;
    }

    // 改为多线程操作测试
    public static void main(String[] args) {
        //main线程操作
        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
    }
}

  •  改为多线程操作测试
public class SingletonDemo {
    private static SingletonDemo instance = null;
    private SingletonDemo() {
        System.out.println(Thread.currentThread().getName() +"\t SingletonDemo构造⽅法执⾏了");
    }
    public static SingletonDemo getInstance(){
        if (instance == null) {
            instance = new SingletonDemo();
        }
        return instance;
    }
    public static void main(String[] args) {
        //多线程操作
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                SingletonDemo.getInstance();
            },Thread.currentThread().getName()).start();
        }
    }
}
  • 调整后,采⽤常⻅的DCLDouble Check Lock)双端检查模式加了同步,但是在多线程下依然会有线程安全问题。
public class SingletonDemo {
    private static SingletonDemo instance = null;
    private SingletonDemo() {
        System.out.println(Thread.currentThread().getName() +"\t SingletonDemo构造⽅法执⾏了");
    }
    public static SingletonDemo getInstance(){
        if (instance == null) {
            synchronized (SingletonDemo.class){
                if (instance == null) {
                    instance = new SingletonDemo();
                }
            }
        }
        return instance;
    }
    public static void main(String[] args) {
        //多线程操作
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                SingletonDemo.getInstance();
            },Thread.currentThread().getName()).start();
        }
    }
}

 这个漏洞⽐较tricky,很难捕捉,但是是存在的。 instance=new SingletonDemo(); 可以⼤致分为三

instance = new SingletonDemo();
public static thread.SingletonDemo getInstance();
 Code:
 0: getstatic #11 // Field instance:Lthread/SingletonDemo;
 3: ifnonnull 37
 6: ldc #12 // class thread/SingletonDemo
 8: dup
 9: astore_0
 10: monitorenter
 11: getstatic #11 // Field instance:Lthread/SingletonDemo;
 14: ifnonnull 27
 17: new #12 // class thread/SingletonDemo 步骤1
 20: dup
 21: invokespecial #13 // Method "<init>":()V 步骤2
 24: putstatic #11 // Field instance:Lthread/SingletonDemo;步 骤3
底层Java Native Interface中的C语⾔代码内容,开辟空间的步骤
memory = allocate(); //步骤1.分配对象内存空间
instance(memory); //步骤2.初始化对象
instance = memory; //步骤3.设置instance指向刚分配的内存地址,此时instance!= null
剖析:
在多线程的环境下,由于有指令重排序的存在, DCL (双端检锁)机制不⼀定线程安全,我们可以加⼊volatile 可以禁⽌指令重排。
原因在与某⼀个线程执⾏到第⼀次检测,读取到的 instance 不为 null 时, instance 的引⽤对象可能没有完成初始化。
memory = allocate(); //步骤1. 分配对象内存空间
instance(memory); //步骤2.初始化对象
instance = memory; //步骤3.设置instance指向刚分配的内存地址,此时instance != null
步骤 2 和步骤 3 不存在数据依赖关系 ,⽽且⽆论重排前还是重排后,程序的执⾏结果在单线程中并没有改变,因此这种重排优化是允许的。
memory = allocate(); //步骤1. 分配对象内存空间
instance = memory; //步骤3.设置instance指向刚分配的内存地址,此时instance != null,但是对象还没有初始化完成!
instance(memory); //步骤2.初始化对象
但是指令重排只会保证串⾏语义的执⾏⼀致性(单线程),并不关⼼多线程的语义⼀致性。 所以,当⼀条线程访问 instance 不为 null 时,由于 instance 实例未必已初始化完成,也就造成了线程安全问题。
public static SingletonDemo getInstance(){
     if (instance == null) {
         synchronized (SingletonDemo.class){
             if (instance == null) {
                 instance = new SingletonDemo(); //多线程情况下,可能发⽣指令重排
             }
         }
     }
     return instance;
 }
如果发⽣指定重排,那么,
1. 此时内存已经分配,那么 instance=memory 不为 null
2. 碰巧,若遇到线程此时挂起,那么 instance(memory) 还未执⾏,对象还未初始化。
3. 导致了 instance!=null ,所以两次判断都跳过,最后返回的 instance 没有任何内容,还没初始化。
解决的⽅法就是对 singletondemo 对象添加上 volatile 关键字,禁⽌指令重排。
private static volatile SingletonDemo instance=null;

第二章 CAS


2.1 CAS是什么?

看下⾯代码进⾏思考,此时 number 前⾯是加了 volatile 关键字修饰的, volatile 不保证原⼦性,那么使 AtomicInteger 是如何保证原⼦性的?这⾥的原理是什么? CAS
class MyData {
 
     volatile int number = 0;

     AtomicInteger atomicInteger=new AtomicInteger();

     public void addPlusPlus(){
         number++;
     }

     public void addAtomic(){
         atomicInteger.getAndIncrement();
     }

     public void setTo60() {
         this.number = 60;
     }
}
CAS 的全称为 Compare-And-Swap ,⽐较并交换,是⼀种很重要的同步思想。它是⼀条 CPU 并发原语。
它的功能是判断主内存某个位置的值是否为跟期望值⼀样,相同就进⾏修改,否则⼀直重试,直到⼀致为⽌。这个过程是原⼦的。
看下⾯这段代码,思考运⾏结果是
public class CASDemo {

     public static void main(String[] args) {
         AtomicInteger atomicInteger = new AtomicInteger(5);
         System.out.println(atomicInteger.compareAndSet(5, 2020)+"\t 当前数据值 : "+ atomicInteger.get());
         //修改失败
         System.out.println(atomicInteger.compareAndSet(5, 1024)+"\t 当前数据值 : "+ atomicInteger.get());
     }
}
第⼀次修改,期望值为 5 ,主内存也为 5 ,修改成功,为 2020 。第⼆次修改,期望值为 5 ,主内存为2020 ,修改失败,需要重新获取主内存的值 。
查看 AtomicInteger.getAndIncrement() ⽅法,发现其没有加 synchronized 也实现了同步 。这是为什么?

CAS并发原语体现在JAVA语⾔中就是sum.misc.Unsafe类中的各个⽅法。看⽅法源码,调⽤UnSafe类中CAS⽅法,JVM会帮我们实现出CAS汇编指令。这是⼀种完全依赖于硬件的功能,通过它实现了原⼦操作。再次强调,由于CAS是⼀种系统原语,原语属于操作系统⽤语范畴,是由若⼲条指令组成的,⽤于完成某个功能的⼀个过程,并且原语的执⾏是连续的,在执⾏过程中不允许被中断,也就是说 CAS是⼀条CPU的原⼦指令,不会造成所谓的数据不⼀致问题。

2.2 CAS底层原理?

 AtomicInteger内部的重要参数

1. Unsafe
CAS 的核⼼类,由于 Java ⽅法⽆法直接访问底层系统,需要通过本地( native )⽅法来访问, Unsafe相当于⼀个后⻔,基于该类可以直接操作特定内存的数据。 Unsafe 类存在 sum.misc
包中,其内部⽅ 法操作可以像 C 的指针⼀样直接操作内存,因为 Java CAS 操作的执⾏依赖于 Unsafe 类的⽅法。 注意 Unsafe 类中的所有⽅法都是 native 修饰的,也就是说 Unsafe 类中的⽅
法都直接调⽤操作系统底层 资源执⾏相应任务
2. 变量 valueOffset ,表示该变量值在内存中的偏移地址,因为 Unsafe 就是根据内存偏移地址获取数 据的。
3. 变量 value volatile 修饰,保证了多线程之间的内存可⻅性。
AtomicInteger.getAndIncrement() 调⽤了 Unsafe.getAndAddInt() ⽅法。 Unsafe 类的⼤部分⽅法都是 native 的,⽤来像 C 语⾔⼀样从底层操作内存。

这个⽅法的 var1 var2 ,就是根据 对象 偏移量 得到在 主内存的快照值 var5 。然后 compareAndSwapInt ⽅法通过 var1 var2 得到当前 主内存的实际值 。如果这个 实际值 快照值 等,那么就更新主内存的值为var5+var4。如果不等,那么就⼀直循环,⼀直获取快照,⼀直对⽐,直 到实际值和快照值相等为⽌。
参数介绍
var1 AtomicInteger 对象本身
var2 该对象值的引⽤地址
var4 需要变动的数量
var5 是通过 var1 var2 ,根据 对象 偏移量 得到在 主内存的快照值 var5
⽐如有 A B 两个线程,⼀开始都从主内存中拷⻉了原值为 3 A 线程执⾏到 var5=this.getIntVolatile ,即 var5=3 。此时 A 线程挂起, B 修改原值为 4 B 线程执⾏完毕,由 于加了 volatile ,所以
这个修改是⽴即可⻅的。 A 线程被唤醒,执⾏ this.compareAndSwapInt() 法,发现这个时候主内存的值不等于快照值 3 ,所以继续循环, 重新 从主内存获取。

 2.3 CAS缺点

 

CAS 实际上是⼀种⾃旋锁,
1. ⼀直循环,开销⽐较⼤。我们可以看到 getAndAddInt ⽅法执⾏时,有个 do while ,如果 CAS 失败,会⼀直进⾏尝试。如果 CAS ⻓时间⼀直不成功,可能会给 CPU 带来很⼤的开销。
2. ⼀个共享变量 执⾏操作时,我们 可以使⽤循环 CAS 的⽅式来保证原⼦操作,但是,对多个共享变量操作时,循环 CAS 就⽆法保证操作的原⼦性,这个时候就可以⽤锁来保证原⼦性。
3. 引出了 ABA 问题

 2.4 CAS会导致“ABA问题”

⾼频⾯试题
1. 原⼦类AtomicIntegerABA问题谈谈?原⼦更新引⽤你知道吗?
2. 我们知道ArrayList是线程不安全,请编码写⼀个不安全的案例并给出解决⽅案
3. 公平锁/⾮公平锁/可重⼊锁/递归锁/⾃旋锁谈谈你的理解?请⼿写⼀个⾃旋锁
4. CountDownLath/CyclicBarrier/Semaphore使⽤过吗?
5. 阻塞队列知道吗?
6. 线程池⽤过吗?ThreadPoolExecutor谈谈你的理解?⽣产上你如何设置合理参数
7. 死锁编码及定位分析
所谓 ABA 问题,就是 CAS 算法实现需要取出内存中某时刻的数据并在当下时刻⽐较并替换,这⾥存在⼀ 时间差 ,那么这个时间差可能带来意想不到的问题。
⽐如,⼀个线程 B 从内存位置 Value 中取出 2 ,这时候另⼀个线程 A 也从内存位置 Value 中取出 2 ,并且线程 A 进⾏了⼀些操作将值变成了 5 ,然后线程 A ⼜再次将值变成了 2 ,这时候线程 B
进⾏ CAS 操作发现 内存中仍然是 2 ,然后线程 B 操作成功
尽管线程B CAS操作成功,但是不代表这个过程就是没有问题的。

有这样的需求,⽐如 CAS 只注重头和尾 ,只要⾸尾⼀致就接受。
但是有的需求,还看重过程,中间不能发⽣任何修改,这就引出了 AtomicReference 原⼦引⽤。

2.4.1 AtomicReference原⼦引⽤

AtomicInteger 对整数进⾏原⼦操作,如果是⼀个 POJO 呢?可以⽤ AtomicReference 来包装这个POJO ,使其操作原⼦化。
public class AtomicReferenceDemo {
     public static void main(String[] args) {
         User user1 = new User("Jack",25);
         User user2 = new User("Tom",21);
         AtomicReference<User> atomicReference = new AtomicReference<>();
         atomicReference.set(user1);
 
         System.out.println(atomicReference.compareAndSet(user1,user2)+"\t"+atomicReference.get()); // true
 
        System.out.println(atomicReference.compareAndSet(user1,user2)+"\t"+atomicReference.get()        ); //false
         }
}

 2.4.2 ABA问题的解决(AtomicStampedReference 类似于时间)

ThreadA 100 1 2020 2
ThreadB 100 1 111 2 100 3
使⽤ AtomicStampedReference 类可以解决 ABA 问题。这个类维护了⼀个 版本号 ”Stamp ,在进⾏ CAS操作的时候,不仅要⽐较当前值,还要⽐较 版本号 。只有两者都相等,才执⾏更新操作。
解决 ABA 问题的关键⽅法:

 参数说明:

V expectedReference , 预期值引⽤
V newReference , 新值引⽤
int expectedStamp 预期值时间戳
int newStamp , 新值时间戳
public class ABADemo {
    static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
    static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);

    public static void main(String[] args) {
        System.out.println("======ABA问题的产⽣======");
        new Thread(() -> {
            atomicReference.compareAndSet(100, 101);
            atomicReference.compareAndSet(101, 100);
        }, "t1").start();
        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(atomicReference.compareAndSet(100, 2020) + "\t" + atomicReference.get().toString());
        }, "t2").start();
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("======ABA问题的解决======");
        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t第⼀次版本号: " + stamp);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            atomicStampedReference.compareAndSet(100, 101,

                    atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "\t第⼆次版本号: " + atomicStampedReference.getStamp());
            atomicStampedReference.compareAndSet(101, 100,

                    atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "\t第三次版本号: " + atomicStampedReference.getStamp());
        }, "t3").start();
        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t第⼀次版本号: " + stamp);
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException
                    e) {
                e.printStackTrace();
            }
            boolean result = atomicStampedReference.compareAndSet(100, 2020,
                    stamp, stamp + 1);
            System.out.println(Thread.currentThread().getName() + "\t修改成功与否:"+result+" 当前最新版本号"+atomicStampedReference.getStamp());
            System.out.println(Thread.currentThread().getName() + "\t当前实际值:"+atomicStampedReference.getReference());
        }, "t4").start();
    }
}

第三章 阻塞队列


 1.1 阻塞队列概述

概念:
  • 在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),⼀旦条件满⾜,被挂起的线程⼜会⾃动被唤醒。
  • 阻塞队列 是⼀个队列,在数据结构中起的作⽤如下图:
当队列是空的,从队列中获取( Take )元素的操作将会被阻塞
当队列是满的,从队列中添加( Put )元素的操作将会被阻塞
试图中空的队列中获取元素的线程将会被阻塞,直到其他线程往空的队列插⼊新的元素
试图向已满的队列中添加新元素的线程将会被阻塞,直到其他线程从队列中移除⼀个或多个元素
或者完全清空,使队列变得空闲起来后并后续新增
好处 :阻塞队列不⽤⼿动控制什么时候该被阻塞,什么时候该被唤醒,简化了操作。
体系 Collection Queue BlockingQueue 七个阻塞队列实现类。

类名作⽤
ArrayBlockingQueue 数组结构构成的有界阻塞队列
LinkedBlockingQueue
链表结构 构成的 有界(但默认值为 Integer.MAX_VALUE 阻塞队列
PriorityBlockingQueue ⽀持优先级排序的⽆界阻塞队列
DelayQueue 使⽤优先级队列实现的延迟⽆界阻塞队列
SynchronousQueue 不存储元素的阻塞队列,也即单个元素的队列
LinkedTransferQueue 由链表构成的⽆界阻塞队列
LinkedBlockingDeque由链表构成的双向阻塞队列
粗体标记的三个⽤得⽐较多,许多消息中间件底层就是⽤它们实现的。需要注意的是 LinkedBlockingQueue 虽然是有界的,但有个巨坑,其默认⼤⼩ Integer.MAX_VALUE ,⾼达 21 亿,
⼀般情况下内存早爆了(在线程池的 ThreadPoolExecutor 有体现)。
API
抛出异常是指当队列满时,再次插⼊会抛出异常;
返回布尔是指当队列满时,再次插⼊会返回 false ;阻塞是指当队列满时,再次插⼊会被阻塞,直到队列取出⼀个元素,才能插⼊。超时是指当⼀个时限过后,才会插⼊或者取出。 API 使⽤⻅ BlockingQueueDemo

public class BlockingQueueDemo {
 public static void main(String[] args) throws InterruptedException {
     BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<String>(3);
     addAndRemove(blockingQueue);
     offerAndPoll(blockingQueue);
     putAndTake(blockingQueue);
     outOfTime(blockingQueue);
 }
 private static void outOfTime(BlockingQueue<String> blockingQueue) throws InterruptedException {
     System.out.println(blockingQueue.offer("a",2L, TimeUnit.SECONDS));
     System.out.println(blockingQueue.offer("a",2L, TimeUnit.SECONDS));
     System.out.println(blockingQueue.offer("a",2L, TimeUnit.SECONDS));
     System.out.println(blockingQueue.offer("a",2L, TimeUnit.SECONDS));
 }
 private static void putAndTake(BlockingQueue<String> blockingQueue)throws InterruptedException {
     blockingQueue.put("a");
     blockingQueue.put("b");
     blockingQueue.put("c");
     blockingQueue.put("d");
     System.out.println(blockingQueue.take());
     System.out.println(blockingQueue.take());
     System.out.println(blockingQueue.take());
     System.out.println(blockingQueue.take());
 }
 private static void offerAndPoll(BlockingQueue<String> blockingQueue) {
     System.out.println(blockingQueue.offer("a"));
     System.out.println(blockingQueue.offer("b"));
     System.out.println(blockingQueue.offer("c"));
     System.out.println(blockingQueue.offer("e"));
     System.out.println(blockingQueue.peek());
     System.out.println(blockingQueue.poll());
     System.out.println(blockingQueue.poll());
     System.out.println(blockingQueue.poll());
     System.out.println(blockingQueue.poll());
 }
 private static void addAndRemove(BlockingQueue<String> blockingQueue) {
     System.out.println(blockingQueue.add("a"));
     System.out.println(blockingQueue.add("b"));
     System.out.println(blockingQueue.add("c"));
     System.out.println(blockingQueue.add("e"));
     System.out.println(blockingQueue.element());
     System.out.println(blockingQueue.remove());
     System.out.println(blockingQueue.remove());
     System.out.println(blockingQueue.remove());
     System.out.println(blockingQueue.remove());
 }
}

1.2.1 传统模式

传统模式使⽤ Synchronized 来进⾏操作
class Aircondition{
 private int number = 0;
 //⽼版写法
 public synchronized void increment() throws Exception{
     //1.判断
     if (number != 0){
     this.wait();
 }
 //2.⼲活
 number++;
 System.out.println(Thread.currentThread().getName()+"\t"+number);
 //3通知
 this.notifyAll();
 }
 public synchronized void decrement() throws Exception{
     //1.判断
     if (number == 0){
     this.wait();
 }
 //2.⼲活
 number--;
 System.out.println(Thread.currentThread().getName()+"\t"+number);
 //3通知
 this.notifyAll();
 }
}
/**
* 题⽬:现在两个线程,可以操作初始值为零的⼀个变量,
* 实现⼀个线程对该变量加1,⼀个线程对该变量-1,
* 实现交替,来10轮,变量初始值为0.
* 1.⾼内聚低耦合前提下,线程操作资源类
* 2.判断/⼲活/通知
* 3.防⽌虚假唤醒(判断只能⽤while,不能⽤if)
* 知识⼩总结:多线程编程套路+while判断+新版写法
*/
public class ProdConsumerDemo {
 public static void main(String[] args) {
 Aircondition aircondition = new Aircondition();
 new Thread(()->{
     for (int i = 1; i <= 10; i++) {
     try {
         aircondition.increment();
     } catch (Exception e) {
         e.printStackTrace();
     }
 }
 },"A").start();
 new Thread(()->{
     for (int i = 1; i <= 10; i++) {
         try {
             aircondition.decrement();
         } catch (Exception e) {
             e.printStackTrace();
         }
     }
 },"B").start();
 }
}
运⾏结果

  • ⽣产者消费者防⽌虚假唤醒

1.2.2 ⽣产者消费者防⽌虚假唤醒(执⾏原理分析)

4 个线程并发执⾏的示意图, 基于代码截图讲解

1.2.3 新版⽣产者消费者写法 ReentrantLock.Condition

 新模式使⽤ Lock 来进⾏操作,需要⼿动加锁、解锁。详⻅ProdConsTradiDemo

class Aircondition{
 private int number = 0;
 private Lock lock = new ReentrantLock();
 private Condition condition = lock.newCondition();
 //新版写法
 public void increment() throws Exception{
     lock.lock();
     try{
         //1.判断
         while (number != 0){
             condition.await();
         }
         //2.⼲活
         number++;
 
        System.out.println(Thread.currentThread().getName()+"\t"+number);
         //3通知
         condition.signalAll();
     } catch (Exception e) {
         e.printStackTrace();
     } finally {
         lock.unlock();
     }
 }

 public void decrement() throws Exception{
     lock.lock();
     try{
         //1.判断
         while (number == 0){
             condition.await();
         }
         //2.⼲活
         number--;
 
         System.out.println(Thread.currentThread().getName()+"\t"+number);
         //3通知
         condition.signalAll();
     } catch (Exception e) {
         e.printStackTrace();
     } finally {
         lock.unlock();
     }
 }
}
/**
* 题⽬:现在两个线程,可以操作初始值为零的⼀个变量,
* 实现⼀个线程对该变量加1,⼀个线程对该变量-1,
* 实现交替,来10轮,变量初始值为0.
* 1.⾼内聚低耦合前提下,线程操作资源类
* 2.判断/⼲活/通知
* 3.防⽌虚假唤醒(判断只能⽤while,不能⽤if)
* 知识⼩总结:多线程编程套路+while判断+新版写法
*/
public class ProdConsumerDemo {
 public static void main(String[] args) {
 Aircondition aircondition = new Aircondition();
 new Thread(()->{
     for (int i = 1; i <= 10; i++) {
         try {
             aircondition.increment();
         } catch (Exception e) {
             e.printStackTrace();
         }
     }
 },"A").start();
 new Thread(()->{
     for (int i = 1; i <= 10; i++) {
         try {
 
             aircondition.decrement();
         } catch (Exception e) {
             e.printStackTrace();
         }
     }
 },"B").start();
 new Thread(()->{
     for (int i = 1; i <= 10; i++) {
         try {
             aircondition.increment();
         } catch (Exception e) {
             e.printStackTrace();
         }
     }
 },"C").start();
 new Thread(()->{
     for (int i = 1; i <= 10; i++) {
         try {
             aircondition.decrement();
         } catch (Exception e) {
             e.printStackTrace();
         }
     }
 },"D").start();
 }
}
运⾏结果

 1.2.4 精准通知顺序访问

class ShareData{
 private int number = 1;//A:1,B:2,C:3
 private Lock lock = new ReentrantLock();
 private Condition c1 = lock.newCondition();
 private Condition c2 = lock.newCondition();
 private Condition c3 = lock.newCondition();
 public void printc1(){
     lock.lock();
     try {
         //1.判断
         while (number != 1){
             c1.await();
         }
         //2.⼲活
         for (int i = 1; i <= 5; i++) {
 
            System.out.println(Thread.currentThread().getName()+"\t"+i);
         }
         //3.通知
         number = 2;
         //通知第2个
         c2.signal();
     } catch (Exception e) {
         e.printStackTrace();
     } finally {
         lock.unlock();
     }
 }
 public void printc2(){
     lock.lock();
     try {
         //1.判断
         while (number != 2){
             c2.await();
         }
         //2.⼲活
         for (int i = 1; i <= 10; i++) {
 
            System.out.println(Thread.currentThread().getName()+"\t"+i);
         }
         //3.通知
         number = 3;
         //如何通知第3个
         c3.signal();
     } catch (Exception e) {
         e.printStackTrace();
     } finally {
         lock.unlock();
     }
 }
 public void printc3(){
     lock.lock();
     try {
         //1.判断
         while (number != 3){
         c3.await();
     }
     //2.⼲活
     for (int i = 1; i <= 15; i++) {
 
        System.out.println(Thread.currentThread().getName()+"\t"+i);
     }
     //3.通知
     number = 1;
     //如何通知第1个
     c1.signal();
     } catch (Exception e) {
         e.printStackTrace();
     } finally {
         lock.unlock();
     }
 }
}
/**
* 备注:多线程之间按顺序调⽤,实现A->B->C
* 三个线程启动,要求如下:
* A打印5次,B打印10次,C打印15次
* 接着
* A打印5次,B打印10次,C打印15次
* 来10轮
* 1.⾼内聚低耦合前提下,线程操作资源类
* 2.判断/⼲活/通知
* 3.多线程交互中,防⽌虚假唤醒(判断只能⽤while,不能⽤if)
* 4.标志位
*/
public class ConditionDemo {
 public static void main(String[] args) {
     ShareData shareData = new ShareData();
     new Thread(()->{
         for (int i = 1; i <= 10; i++) {
             shareData.printc1();
         }
     },"A").start();
     new Thread(()->{
         for (int i = 1; i <= 10; i++) {
             shareData.printc2();
         }
     },"B").start();
     new Thread(()->{
         for (int i = 1; i <= 10; i++) {
             shareData.printc3();
         }
     },"C").start();
 }
}
运⾏结果

 1.2.5 SynchronizedLock的区别

synchronized 关键字和 java.util.concurrent.locks.Lock 都能加锁,两者有什么区别呢?
1. 原始构成 sync JVM 层⾯的,底层通过 monitorenter monitorexit 来实现的。 Lock 是JDK API 层⾯的。( sync ⼀个 enter 会有两个 exit ,⼀个是正常退出,⼀个是异常退出)
2. 使⽤⽅法 sync 不需要⼿动释放锁,⽽ Lock 需要⼿动释放。
3. 是否可中断 sync 不可中断,除⾮抛出异常或者正常运⾏完成。 Lock 是可中断的,通过调⽤ interrupt() ⽅法。
4. 是否为公平锁 sync 只能是⾮公平锁,⽽ Lock 既能是公平锁,⼜能是⾮公平锁。
5. 绑定多个条件 sync 不能,只能随机唤醒。⽽ Lock 可以通过 Condition 来绑定多个条件,精确唤醒。

1.2.6 阻塞队列模式⽣产者消费者

为什么需要 BlockingQueue
好处是我们不需要关⼼什么时候需要阻塞线程,什么时候需要唤醒线程,因为这⼀切 BlockingQueue 都给你⼀⼿包办好了,使⽤阻塞队列 后就不需要⼿动加锁了。
Concurrent 包发布以前,在多线程环境下,我们每个程序员都必须去⾃⼰控制这些细节,尤其还要兼顾效率和线程安全,⽽这会给我们的程序带来不⼩的复杂度

public class ProdConsBlockQueueDemo {
 public static void main(String[] args) {
     MyResource myResource = new MyResource(new ArrayBlockingQueue<>(5));
     new Thread(() -> {
         System.out.println(Thread.currentThread().getName() + "\t⽣产线程启动");
         try {
             myResource.myProd();
         } catch (Exception e) {
             e.printStackTrace();
         }
     }, "prod").start();
     new Thread(() -> {
         System.out.println(Thread.currentThread().getName() + "\t⽣产线程启动");
         try {
             myResource.myProd();
         } catch (Exception e) {
             e.printStackTrace();
         }
     }, "prod-2").start();
     new Thread(() -> {
         System.out.println(Thread.currentThread().getName() + "\t消费线程启动");
     try {
         myResource.myCons();
     } catch (Exception e) {
         e.printStackTrace();
     }
     }, "cons").start();
     new Thread(() -> {
         System.out.println(Thread.currentThread().getName() + "\t消费线程启动");
         try {
             myResource.myCons();
         } catch (Exception e) {
             e.printStackTrace();
         }
    }, "cons-2").start();
     try {
         TimeUnit.SECONDS.sleep(5);
     } catch (Exception e) {
         e.printStackTrace();
     }
     System.out.println("5秒钟后,叫停");
     myResource.stop();
  }
}
class MyResource {
 private volatile boolean FLAG = true; //默认开启,进⾏⽣产+消费
 private AtomicInteger atomicInteger = new AtomicInteger();
 private BlockingQueue<String> blockingQueue = null;
 public MyResource(BlockingQueue<String> blockingQueue) {
     this.blockingQueue = blockingQueue;
     System.out.println(blockingQueue.getClass().getName());
 }
 public void myProd() throws Exception {
     String data = null;
     boolean retValue;
     while (FLAG) {
         data = atomicInteger.incrementAndGet() + "";//++i
         retValue = blockingQueue.offer(data, 2L, TimeUnit.SECONDS);
         if (retValue) {
             System.out.println(Thread.currentThread().getName() + "\t"+ "插⼊队列" + data + "成功");
         } else {
             System.out.println(Thread.currentThread().getName() + "\t"+ "插⼊队列" + data + "失败");
         }
         TimeUnit.SECONDS.sleep(1);
     }
     System.out.println(Thread.currentThread().getName() + "\t⽼板叫停了,FLAG已更新为false,停⽌⽣产");
 }
 public void myCons() throws Exception {
     String res;
     while (FLAG) {
     res = blockingQueue.poll(2L, TimeUnit.SECONDS);
     if (null == res || "".equals(res)) {
     // FLAG = false;
     System.out.println(Thread.currentThread().getName() + "\t超过2秒钟没有消费,退出消费");
     return;
 }
     System.out.println(Thread.currentThread().getName() + "\t\t消费队列" + res + "成功");
     }
 }
 public void stop() {
     this.FLAG = false;
 }
}

 运⾏结果

第四章 线程池


 1.1 线程池基本概念

概念 :线程池主要是控制运⾏线程的数量,将待处理任务放到等待队列,然后创建线程执⾏这些任务。如果超过了最⼤线程数,则等待。
为什么⽤线程池?
10 年前单核 CPU 电脑,假的多线程,像⻢戏团⼩丑玩多个球, CPU 需要来回切换。
现在是多核电脑,多个线程各⾃跑在独⽴的 CPU 上,不⽤切换效率⾼。
线程池的优点
线程池做的⼯作只要是控制运⾏的线程数量,处理过程中将任务放⼊队列,然后在线程创建后启动这些任务,如果线程数量超过了最⼤数量,超出数量的线程排队等候,等其他线程执⾏完
毕,再从队列中取 出任务来执⾏
线程池的主要特点 为:线程复⽤;控制最⼤并发数;管理线程。
1. 线程复⽤:不⽤⼀直 new 新线程,重复利⽤已经创建的线程来降低线程的创建和销毁开销,节省系 统资源。
2. 提⾼响应速度:当任务达到时,不⽤创建新的线程,直接利⽤线程池的线程。
3. 管理线程:可以控制最⼤并发数,控制线程的创建等。
体系 Executor ExecutorService AbstractExecutorService ThreadPoolExecutor Thr eadPoolExecutor 是线程池创建的核⼼类。类似 Arrays Collections ⼯具类, Executor 也有⾃ ⼰的⼯具类 Executors

1.2 线程池三种常⽤创建⽅式

Java 中的线程池是通过 Executor 框架实现的,该框架中⽤到了 Executor Executors ,ExecutorService ThreadPoolExecutor 这⼏个类

 1.2.1 newFixedThreadPool线程池

使⽤ LinkedBlockingQueue 实现,定⻓线程池。
特点:执⾏⻓期任务性能好,创建⼀个线程池,⼀池有 N 个固定的线程,有固定线程数的线程

 1.2.2 newSingleThreadExecutor线程池

使⽤ LinkedBlockingQueue 实现,⼀池只有⼀个线程。
特点:⼀个任务⼀个任务的执⾏,⼀池⼀线程

 1.2.3 newCachedThreadPool线程池

使⽤ SynchronousQueue 实现,变⻓线程池。
特点:执⾏很多短期异步任务,线程池根据需要创建新线程,但在先前构建的线程可⽤时将重⽤他们。可扩容,遇强则强

 1.2.4 线程池代码演示

/**
* 线程池代码演示
*/
public class ThreadPoolDemo {
 public static void main(String[] args) {
     //System.out.println("=======Fixed Thread Pool========");
     //⼀个池⼦有5个⼯作线程,类似银⾏有5个受理窗⼝
     //threadPoolTask( Executors.newFixedThreadPool(5) );
     // System.out.println("======Single Thread Pool=========");
     // //⼀个池⼦有1个⼯作线程,类似银⾏有1个受理窗⼝
     // threadPoolTask( Executors.newSingleThreadExecutor() );
     // System.out.println("=====Cached Thread Pool=======");
     // //不定量线程,⼀个池⼦有N个⼯作线程,类似银⾏有N个受理窗⼝
     // threadPoolTask( Executors.newCachedThreadPool() );
     System.out.println("=====Custom Thread Pool=======");
     threadPoolTask( new ThreadPoolExecutor(
         2,
         5,
         1L,
         TimeUnit.SECONDS,
         new LinkedBlockingQueue<>(3),
         Executors.defaultThreadFactory(),
         new ThreadPoolExecutor.CallerRunsPolicy()
     ));
 }
 private static void threadPoolTask(ExecutorService threadPool) {
     //模拟有10个顾客来办理业务
     try {
         for (int i = 1; i <= 10; i++) {
             threadPool.execute(() -> {
 
                System.out.println(Thread.currentThread().getName()+"\t办理业务");
             });
         }
     } catch (Exception e) {
         e.printStackTrace();
     } finally {
         threadPool.shutdown();
     }
 }
}

1.3 线程池创建的七个参数

 

 1.4 线程池底层原理

理解 :线程池的创建参数,就像⼀个 银⾏
corePoolSize 就像银⾏的 当值窗⼝ ,⽐如今天有 2 位柜员 在受理 客户请求 (任务)。
如果超过 2 个客户,那么新的客户就会在 等候区 (等待队列 workQueue )等待。
等候区 也满了,这个时候就要开启 加班窗⼝ ,让其它 3 位柜员来加班,此时达到 最⼤窗⼝ maximumPoolSize ,为 5 个。
如果开启了所有窗⼝,等候区依然满员,此时就应该启动 拒绝策略 handler ,告诉不断涌⼊的客户,叫他们不要进⼊,已经爆满了。
由于不再涌⼊新客户,办完事的客户增多,窗⼝开始空闲,这个时候就通过 keepAlivetTime 将多余的3 加班窗⼝ 取消,恢复到 2 当值窗⼝
案例图

 原理图:上⾯银⾏的例⼦,实际上就是线程池的⼯作原理。

流程图

流程:
1. 在创建了线程池后,开始等待请求。
2. 当调⽤ execute() ⽅法添加⼀个请求任务时,线程池会做出如下判断:
2.1 如果正在运⾏的线程数量⼩于 corePoolSize ,那么⻢上创建核⼼线程运⾏执⾏这个任务;
2.2 如果正在运⾏的线程数量⼤于或等于 corePoolSize ,那么将这个任务放⼊队列;
2.3 如果这个时候等待队列已满,且正在运⾏的线程数量⼩于 maximumPoolSize ,那么还是要创建⾮核⼼线程⽴刻运⾏这个任务;
2.4 如果这个时候等待队列已满,且正在运⾏的线程数量⼤于或等于 maximumPoolSize ,那么线程池会启动饱和拒绝策略来执⾏。 3. 当⼀个线程完成任务时,它会从等待队列中取出下⼀个任务来执⾏。
4. 当⼀个线程⽆事可做超过⼀定的时间( keepAliveTime )后,线程会判断:如果当前运⾏的线程数⼤于 corePoolSize ,那么这个⾮核⼼线程就被停掉。当线程池的所有任 务完成后,它最终会收缩到 corePoolSize

Java 开发⼿册》是阿⾥巴巴集团技术团队:

 1.5 线程池的拒绝策略

 

当等待队列满时,且达到最⼤线程数,再有新任务到来,就需要启动拒绝策略。 JDK 提供了四种拒绝策略,分别是。
1. AbortPolicy :默认的策略,直接抛出 RejectedExecutionException 异常,阻⽌系统正常运⾏。
2. CallerRunsPolicy :既不会抛出异常,也不会终⽌任务,⽽是将任务返回给调⽤者,从⽽降低新任务的流量。
3. DiscardOldestPolicy :抛弃队列中等待最久的任务,然后把当前任务加⼊队列中尝试再次提交任 务。
4. DiscardPolicy :该策略默默地丢弃⽆法处理的任务,不予任何处理也不抛出异常。如果允许任务 丢失,这是最好的⼀种策略。

1.5.1 AbortPolicy拒绝策略

 

 1.5.2 CallerRunsPolicy拒绝策略

 

 1.5.3 DiscardOldestPolicy拒绝策略

 

 1.5.4 DiscardPolicy拒绝策略

 

1.6 实际⽣产使⽤哪⼀个线程池?

单⼀、可变、定⻓都不⽤ !原因就是 FixedThreadPool SingleThreadExecutor 底层都是⽤ LinkedBlockingQueue 实现的,这个队列最⼤⻓度为 Integer.MAX_VALUE ,显然会导致
OOM 。所 以实际⽣产⼀般⾃⼰通过 ThreadPoolExecutor 7 个参数,⾃定义线程池。
ExecutorService threadPool = new ThreadPoolExecutor(
 2,
 80*2,
 1L,
 TimeUnit.SECONDS,
 new LinkedBlockingQueue<>(3),
 Executors.defaultThreadFactory(),
 new ThreadPoolExecutor.AbortPolicy()
);

 ⾃定义线程池参数选择

对于 CPU 密集型任务,最⼤线程数是 CPU 线程数 +1
对于 IO 密集型任务,尽量多配点,可以是 CPU 线程数 *2 ,或者 CPU 线程数 /(1- 阻塞系数 )

IO 密集型,即该任务需要⼤量的 IO ,即⼤量的阻塞。
在单线程上运⾏ IO 密集型的任务会导致浪费⼤量的 CPU 运算能⼒浪费在等待。
所以在 IO 密集型任务中使⽤多线程可以⼤⼤的加速程序运⾏,及时在单核 CPU 上,这种加速主要就是利⽤了被浪费掉的阻塞时间。
IO 密集型时,⼤部分线程都阻塞,故需要多配置线程数:
参考公式: CPU 核数 / 1 - 阻塞系数 ) 阻塞系数在 0.8~0.9 之间
⽐如 8 CPU 8/(1 - 0.9) = 80 个线程数

 第五章 死锁编码和定位

 2.1 死锁编码

死锁:是指两个或两个以上的进程在执⾏过程中,因争夺资源⽽造成的⼀种 相互等待的现象 ,若⽆外⼒⼲涉,那它们将都⽆法推进下去,如果系统资源充⾜,进程的资源请求都能够得到
满⾜,死锁出现的 可能性就很低,否则就会因争夺有限的资源⽽陷⼊死锁。

 

 代码演示

class HoldLockThread implements Runnable {
 private String lockA;
 private String lockB;
 public HoldLockThread(String lockA, String lockB) {
     this.lockA = lockA; 123456
     this.lockB = lockB;
 }

 @Override
 public void run() {
     synchronized (lockA) {
         System.out.println(Thread.currentThread().getName() + "\t⾃⼰持有:" + lockA + "\t尝试获取:" + lockB);
         try {
             TimeUnit.SECONDS.sleep(2);
         } catch (Exception e) {
             e.printStackTrace();
         }
         synchronized (lockB) {
             System.out.println(Thread.currentThread().getName() + "\t⾃⼰持有:" + lockB + "\t尝试获取:" + lockA);
         }
     }
 }
}
public class DeadLockDemo {
 public static void main(String[] args) {
     String lockA = "lockA";
     String lockB = "lockB";
     new Thread(new HoldLockThread(lockA, lockB), "ThreadA").start();
     new Thread(new HoldLockThread(lockB, lockA), "ThreadB").start();
 }
}
运⾏结果:

 2.2 死锁定位

 jps指令: jps -l 可以查看运⾏的Java进程

 

 jstack指令: jstack pid 可以查看某个Java进程的堆栈信息,同时分析出死锁。

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值