java复习系列[2] - Java多线程

文章目录

JUC

thread

Erlang 之父 Joe Armstrong 用一张5岁小孩都能看懂的图解释了并发与并行的区别

img

并发是两个队列交替使用一台咖啡机,

并行是两个队列同时使用两台咖啡机

并发:多个线程访问同一个值

并行:同时做多个事情

线程状态

状态介绍
1NewJava中new了一个Thread对象
这时候它仅仅在堆中分配了内存
2Runnable:可运行状态(创建JVM栈和PC)
:Ready等待系统分配CPU资源
:Running正在运行,只有Ready状态可以转换到Running
Blocked:阻塞状态(超时等待,等待,阻塞)
3:Time Wait当前线程执行了sleep()方法,或者调用了其他线程的join()方法,或者发出了I/O请求时,就会进入这个状态。
4:Wait处于运行状态时,(主动)执行了某个对象的wait()方法
5:Blocked没有获取到锁,(被动)阻塞在队列中
6Dead线程退出run()方法时,就进入死亡状态,该线程结束生命周期
public enum State {
    
    /**
    * Thread state for a thread which has not yet started.
	*/ 
    NEW,   
	/**
     * Thread state for a runnable thread.  A thread in the runnable
     * state is executing in the Java virtual machine but it may
     * be waiting for other resources from the operating system
     * such as processor.
     */
    RUNNABLE,

    /**
     * Thread state for a thread blocked waiting for a monitor lock.
     * A thread in the blocked state is waiting for a monitor lock
     * to enter a synchronized block/method or
     * reenter a synchronized block/method after calling
     * {@link Object#wait() Object.wait}.
     */
    BLOCKED,

    /**
     * Thread state for a waiting thread.
     * A thread is in the waiting state due to calling one of the
     * following methods:
     * <ul>
     *   <li>{@link Object#wait() Object.wait} with no timeout</li>
     *   <li>{@link #join() Thread.join} with no timeout</li>
     *   <li>{@link LockSupport#park() LockSupport.park}</li>
     * </ul>
     *
     * <p>A thread in the waiting state is waiting for another thread to
     * perform a particular action.
     *
     * For example, a thread that has called <tt>Object.wait()</tt>
     * on an object is waiting for another thread to call
     * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
     * that object. A thread that has called <tt>Thread.join()</tt>
     * is waiting for a specified thread to terminate.
     */
    WAITING,

    /**
     * Thread state for a waiting thread with a specified waiting time. A thread is in the timed waiting state due to calling one of the following methods with a specified positive waiting time:
     * <ul>
     *   <li>{@link #sleep Thread.sleep}</li>
     *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
     *   <li>{@link #join(long) Thread.join} with timeout</li>
     *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
     *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
     * </ul>
     */
    TIMED_WAITING,

    /**
     * Thread state for a terminated thread. The thread has completed execution.
     */
    TERMINATED;
}
sleep() 与 wait() 的异同点

**共同点:**两者都会暂停线程的执行

sleep()wait()
方法Thread类的静态方法Object的实例方法
不会释放锁会释放锁
唤醒时间到了之后自动唤醒需要notify(), notifyAll()唤醒
作用暂停线程与notify(), notifyAll()结合,可用于线程的同步
wait() 与 阻塞 的异同点

**共同点:**两者都会暂停线程的执行

wait()阻塞
主动、被动主动进入被动进入
位置synchronized内部synchronized外部
进入了同步代码块,
调用wait的时候会释放锁
由于没有获取到锁,所以阻塞
执行范围synchronized同步块(方法)内部任意位置
Some Question !!!
有了sleep()为什么还要有wait(time)

wait(time)在等待时候会释放锁,从而可以让其他的线程可以获取锁并执行任务!

线程中断

interrupt() 它基于「一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。」

interrupt() 并不能真正的中断线程,这点要谨记。需要被调用的线程自己进行配合才行。也就是说,一个线程如果有被中断的需求,那么就需要这样做:

  1. 在正常运行任务时,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程。
  2. 在调用阻塞方法时正确处理InterruptedException异常。(例如:catch异常后就结束线程。)
   // 核心 interrupt 方法
   public void interrupt() {
        if (this != Thread.currentThread()) // 非本线程,需要检查权限
            checkAccess();

        synchronized (blockerLock) {
            Interruptible b = blocker;
            if (b != null) {
                interrupt0();           // 仅仅设置interrupt标志位
                b.interrupt(this);    // 调用如 I/O 操作定义的中断方法
                return;
            }
        }
        interrupt0();
    }
    // 静态方法,这个方法有点坑,调用该方法调用后会清除中断状态。
    public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }
    // 这个方法不会清除中断状态
    public boolean isInterrupted() {
        return isInterrupted(false);
    }
   // 上面两个方法会调用这个本地方法,参数代表是否清除中断状态
   private native boolean isInterrupted(boolean ClearInterrupted);
中断示例

中断线程

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new MyThread();
        t.start();
        Thread.sleep(1); // 暂停1毫秒
        t.interrupt(); // 中断t线程
        t.join(); // 等待t线程结束
        System.out.println("end");
    }
}

class MyThread extends Thread {
    public void run() {
        int n = 0;
        while (! isInterrupted()) {
            n ++;
            System.out.println(n + " hello!");
        }
    }
}
  1. 仔细看上述代码,main线程通过调用t.interrupt()方法中断t线程,但是要注意,interrupt()方法仅仅向t线程发出了“中断请求”,至于t线程是否能立刻响应,要看具体代码。而t线程的while循环会检测isInterrupted(),所以上述代码能正确响应interrupt()请求,使得自身立刻结束运行run()方法。

  2. 如果线程处于等待状态,例如,t.join()会让main线程进入等待状态,此时,如果对main线程调用interrupt()join()方法会立刻抛出InterruptedException,因此,目标线程只要捕获到join()方法抛出的InterruptedException,就说明有其他线程对其调用了interrupt()方法,通常情况下该线程应该立刻结束运行。

线程占用的内存

java线程占多大的内存,占哪里的内存

  1. 线程私有的 JVM 栈,-Xss可设置其内存占用大小
  2. Java里每新起一个线程,JVM会向操作系统请求新起一个本地线程,此时操作系统会用空闲的内存空间来分配这个线程。所以 Java 里线程并不会占用JVM的内存空间,而是会占用操作系统空闲的内存空间

Some Question !!!

线程上下文切换?
  1. 线程调度过程中,当前线程的时间片用完之后需要,切换下一个线程运行。
  2. 切换的时候,需要保存其他的信息(PC,栈信息)

JMM的三大特性

在这里插入图片描述

  • 原子性
  • 可见性
  • 有序性

volatile

轻量级线程同步的java关键字

怎么保证可见性?

使用MSEI 缓存一致性协议解决并发可见性

  1. 线程修改了 工作内存中的值之后,会使用store/write等原子操作修改主内存的值
  2. 并且触发 嗅探机制 让其他线程的变量副本失效
MESI协议
功能:

保证每个Cache中使用的共享变量的副本是一致的

MESI核心思想: 当CPU写数据时,如果发现操作的变量是共享变量,即在其他 CPU 中也存在该变量的副本,

会发出信号通知其他 CPU 将该变量的缓存行置为无效状态( 嗅探机制),

因此当其他 CPU 需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

监听和通知:基于总线嗅探机制来完成

四个状态:

MESI - Modified, Exclusive, Shared, Invalid

  1. Modify(修改):当缓存行中的数据被修改时,该缓存行置为M状态
  2. Exclusive(独占):当只有一个缓存行使用某个数据时,置为E状态
  3. Shared(共享):当其他CPU中也读取某数据到缓存行时,所有持有该数据的缓存行置为S状态
  4. Invalid(无效):当某个缓存行数据修改时,其他持有该数据的缓存行置为I状态
流程示例:
  1. CPU1读取主存数据 A = 1,CPU1的缓存中保存数据A的副本,该缓存状态为 E
  2. CPU2 读取主存数据 A=1,CPU2的缓存中保存数据A的副本,此时嗅探机制发现CPU1 的缓存中也有该数据,则CPU1与CPU2两个缓存都置为 S
  3. CPU1 修改CPU1缓存A=2,并修改主存 A=2,同时CPU1的缓存状态设置为 S,总线发出通知,CPU2的缓存变状态变为 I
  4. CPU2 再次读取 A,虽然CPU2在缓存中命中数据 A = 1,但是发现状态为 I,因此直接丢弃该数据,去主存中获取A的最新数据

如果CPU2 未在 CPU1修改缓存之后再次读取A(也就是没有第4步),而是直接在第二步之后直接对A进行了操作,然后修改 A的值,那么这样的话,可能会和 第三步 的操作冲突,导致 只有一次修改有效!!!

这也是不能保证原子性的原因!!!

吃透Java并发:volatile是怎么保证可见性的


怎么保证有序性?

先回答一个问题,为什么需要进行指令重排?

为了提高效率!

Java会对指令进行优化,优化过程中可能会进行指令重排。

volatile如何禁止指令重排呢?

对volatile修饰的变量增加内存屏障

内存屏障工作原理:

指令之间插入一条内存屏障并禁止cpu对volatile修饰的变量进行重排序,也就是通过插入内存屏障禁止在内存屏障前后执行重排序优化

具体实现:

在volatile的变量中,会加一个 lock 前缀的汇编指令。

在这里插入图片描述

为什么不保证原子性?

volatile int state = 100;
state++;

volatile int state = 100;修改state变量分为 4 步骤:

  1. 读取state到 工作内存
  2. 修改变量值
  3. 工作变量中的 state 写回 主内存
  4. 插入内存屏障(lock指令),让其他线程可见

在 1 2 3 步骤中,不能保证没有其他的线程进行修改。也就是说:在第三步的时候,没有判断state在主存中是否改变;

  1. 线程读取 state

  2. temp = state + 1 3、state = temp 当 i=5 的时候A,B两个线程同时读入了 state 的值, 然后A线程执行了 temp =state + 1的操作,

  3. 要注意,此时的 i 的值还没有变化,然后B线程也执行了 temp = state + 1的操作,注意,此时A,B两个线程保存的 state 的值都是5,temp 的值都是6,

  4. 然后A线程执行了 state = temp (6)的操作,此时 state 的值会立即刷新到主存并通知其他线程保存的 state 值失效,

  5. 此时 B 线程需要重新读取 i 的值那么此时B线程保存的 i 就是6,同时B线程保存的 temp 还仍然是6, 然后B线程执行 state = temp (6),所以导致了计算结果比预期少了1

    volatile为什么不能保证原子性 (看评论)

CAS

Unsafe类的native方法是调用系统原语,所以具有原子性。

在这里插入图片描述

CAS缺陷

  1. 如果CAS失败,会一直尝试,CPU的开销大(并发量很大的时候,效率低)
  2. 只是保证一个共享变量的原子操作
  3. ABA问题

ABA问题

增加版本号控制

ABA -> A1B2A3

synchronized 与 CAS

Synchronized是只允许一个线程运行,虽然一致性保证了,但是降低并发性,而cas底层是unsafe,并且不加锁,保证一致性,允许多个线程同时操作,并发量得到保障,但是循环比较

synchronized

线程A获取到锁之后,其他线程进入到阻塞状态,线程A结束之后;线程会进行上下文切换

​ 上下文切换比较的浪费资源

在这里插入图片描述

synchronized关键字

synchronized关键字

Synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行

JVM 是通过进入、退出对象监视器(Monitor) 来实现对方法、同步块的同步的,而对象监视器的本质依赖于底层操作系统的互斥锁(Mutex Lock) 实现。

具体实现:

是在编译之后的字节码中,在同步方法调用前加入一个monitorenter指令,在退出方法和异常处插入monitorexit的指令。

synchronized的使用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uWVr8tq7-1638363515166)(Java多线程.assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM2NzM5MDQw,size_16,color_FFFFFF,t_70.png)]

synchronized的锁升级

用户态和内核态:

  • 用户态:用户程序运行的状态
  • 内核态:控制计算机的核心资源,硬件、CPU调度、内存分配等
  • 用户态的程序要使用到内核态的功能就必须使用系统调用的接口

简单来说在JVM中synchronized重量级锁的底层原理:

monitorentermonitorexit字节码依赖于底层的操作系统的 Mutex Lock来实现的,但是由于使用Mutex Lock需要将当前线程挂起并从用户态切换到内核态来执行,这种切换的代价是非常昂贵的。

JDK直到1.5 与 之前都是重量级锁。1.6的时候进行了优化:无锁 - 偏向锁 - 自旋锁(轻量级锁) - 重量级锁

优化的原因:

重量级锁的获取与释放都需要经过操作系统,是一个重量级的操作:

  1. 对于重量级锁,一旦获取锁失败就会陷入阻塞状态(OS级别的阻塞),这涉及到了用户态到核心态的切换(这个操作开销极大)
  2. 研究表面,线程持有锁的时间是比较短暂的(就算当前获取不到锁,那么要不了多久也能获取锁,这种情况阻塞线程会造成资源浪费)

在这里插入图片描述

在这里插入图片描述

无锁状态
偏向锁

研究表明:大部分场景中都不会发生锁资源的竞争。这种情况下,同一个线程获取这个锁,若需要获取这个锁都需要进行一系列操作(CAS自旋),那这样操作是资源。

那么锁就会进入偏向模式,当同一个线程再次请求该锁的时候,无需做任何同步,直接进行同步区域执行。这样就省去了大量有关锁申请的操作。

在没有锁竞争的情况下,偏向锁具有很大的优化效果

轻量级锁
重量级锁
题目: synchronized和Lock有什么区别? 用新的Lock有什么好处?你举例说说
synchronizedReentrantLock
实现上JVM级别的实现JUC中的类
使用上在方法上和方法块上使用使用lock、unlock方法
非公平锁非公平锁(默认) 和 公平锁
等待是否可中断不可中断,除非出现异常和正常结束可中断,
1. 设置超时方法 tryLock(long, unit)
2. lockInterruptibly() 放入代码块中,调用interrupt方法可中断
精确唤醒notify()随机唤醒
notifyAll()随机唤醒
Condition的signal 可精确唤醒
  1. 原始构成:
    1. synchronized是JVM级别,属于关键字;(其中重量级锁)底层通过monitor对象完成,其实wait和notify方法也依赖于monitor对象(所以在同步方法块或方法中才可以调用 wait 和 notify方法)
    2. Lock是JUC中的接口,是API层面的锁,其基于CAS实现
  2. 使用方法
    1. 不需要用户手动释放
    2. 需要手动释放锁,否则可能造成死锁
  3. 等待是否可中断
    1. synchronized不可中断,除非出现异常和正常接数
    2. ReentrantLock可中断,1. 设置超时方法 tryLock(long, unit) 2. lockInterruptibly() 放入代码块中,调用interrupt方法可中断
  4. 加锁是否公平
    1. synchronized非公平锁
    2. ReentrantLock默认非公平锁,可设置 公平 或 非公平
  5. 锁绑定多个条件 Condition
    1. synchronized 没有;notify是随机唤醒某个线程,nodifyAll()是唤醒所有线程
    2. RenentrantLock 用来实现分组唤醒需要被唤醒的线程,可以精确唤醒某个线程

Join

Java中join()方法原理及使用教程

ublic static void main(String[] args) {
    Runnable runnable = new Runnable() {
			@Override
			public void run() {
				System.out.println("子线程执行");
			}
		};
    Thread thread1 = new Thread(runnable);
    Thread thread2 = new Thread(runnable);
    thread1.start();
    thread2.start();
    try {
        //主线程开始等待子线程thread1,thread2
        thread1.join();
        thread2.join();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    //等待两个线程都执行完(不活动)了,才执行下行打印
    System.out.println("执行完毕")`;;
}

join源码中调用了 wait()
public final synchronized void join(long millis)
throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }
    if (millis == 0) {
        // 关键实现在此行,通过wait方法永久等待。
        while (isAlive()) { // 判断 当前线程是否存活
            wait(0);		// 让 调用join方法的线程 等待 (以上例子中表示:主线程等待)
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}
        // 关键实现在此行,通过wait方法永久等待。
        while (isAlive()) { // 判断 当前线程 是否存活
            wait(0);		// 让 调用join方法的线程 等待
        }
// 让 调用join方法的线程 等待 (以上例子中表示:主线程等待)
// 以上例子中调用 join() 方法是主线程

一些问题

用户态到核心态的切换?

集合类不安全问题

ArrayList的并发问题

在这里插入图片描述

  1. 故障现象

    java.util.ConcurrentModificationException

  2. 导致的原因

    并发争抢修改,一个线程正在写入,另外一个线程也要来写。ArrayList是线程不安全的

  3. 解决方案

    1. 加锁
    2. 线程安全的集合类
      1. Vector
      2. Collections.synchronizedList(new ArrayList())
      3. CopyOnWriteArrayList,写时复制
  4. 优化建议(同样的错误不出现第二次)

HashSet的并发问题

类似于ArrayList,内部使用CopyOnWriteArrayList。

HashSet底层是HashMap,但是为什么放的只是一个值?

HashSet中放的是 Key,Value中放的是 私有静态的对象;也就是说,所有的HashSet中的Value都是存放的都是 PRESENT。

private static final Object PRESENT = new Object();

public boolean add(E e) {
      return map.put(e, PRESENT)==null;  
}

HashMap

HashTable

ConcurrentHashMap

线程安全的集合

CopyOnWriteXXX

    private E get(Object[] a, int index) {
        return (E) a[index];
    }
    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

读的时候,不加锁,读取的是副本

写的时候,加锁,新建一个副本进行写,然后覆盖原始版本

Question:当你修改后,CopyOnWriteXXX怎么保证覆盖原始版本时候,其他线程不会出现数据获取错误?

不要进入误区!其他线程读取数据只会在 覆盖 原始版本的前后,之前的话访问的是之前的数据地址,之后的话,访问的是新的数据地址。

A线程获取数据,会拿到当前数据数组中的引用,并进行访问

在这里插入图片描述

公平锁、非公平、重入(递归)、自旋

公平锁和非公平锁

公平:先来后到,申请的顺序排队

非公平:抢夺(ReentrantLock和 synchronized 默认)

关于两者区别
公平锁:

公平锁,就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第1个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己

非公平锁:

非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。

非公平锁在进入队列之前会有三次获取锁的机会:

  1. 调用lock(),马上会尝试获取锁
  2. 在判断是否需要进行排队的时候,会尝试获取锁
  3. 在进入队列之前,又会进行一次锁的获取

图解获取公平锁和非公平锁过程

可重入锁

指的是同一线程外层函数获得锁之后﹐内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁;也即是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块。

在这里插入图片描述

自旋(Unsafe + CAS)

是指尝试获取锁的线程不会立即阻寨,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU

在synchronized中叫 轻量级锁

也叫 乐观锁

自旋的本质是:while循环 + CAS

在这里插入图片描述

独占(写)和共享(读)

之前是无论读写,都只是一个线程可以操作。

现在将读写分离,读时候共享,写的时候独占

ReentrantReadWriteLock: 写的时候独占,读的时候共享

CountDownLatch

让一些线程阻塞直到另一些线程完成一系列操作后才被唤醒。

CountDownLatch主要有两个方法:

  1. 当一个或多个线程调用await方法时,调用线程会被阻塞。
  2. 其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞),当计数器的值变为零时,因调用await方法被阻塞的线程会被唤醒,继续执行。

在这里插入图片描述

人走完,才关门

为了模拟高并发,让一组线程在指定时刻 (秒杀时间) 执行抢购,这些线程在准备就绪后,进行等待( CountDownLatch.await() ),直到秒杀时刻的到来,然后一拥而上;这也是 本地测试接口并发 的一个简易实现。

CyclicBarrier

CyclicBarrier的字面意思是可循环(Cyclic)使用的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过cyclicBarrier的await()方法。

集齐7颗龙珠就能召唤神龙

在这里插入图片描述

人齐了,才开会

Semaphore

信号量主要用于两个目的:

  1. 一个是用于多个共享资源的互斥使用,

  2. 另一个用于并发线程数的控制。

例子:6个车抢3个停车位

在这里插入图片描述

7个 阻塞队列

在多线程领域中所谓的阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒

在这里插入图片描述

名称描述
ArrayBlockingQueue有界,数组实现,FIFO,支持公平和非公平锁
LinkedBlockingQueue有界(默认Integer.MAX_VALUE),链表实现,FIFO
SynchronousQueue有界(一个元素),不存储元素的阻塞队列,每个出队操作必须等待入队操作
PriorityBlockingQueue无界,可指定优先级(默认自然序)
DelayBlockingQueue无界,可指定时间Time,在Time之后才能够获取
LinkedTransferQueue无界,链表,多了transfer和tryTransfer方法
LinkedBlockingDeque双向队列,队头队尾都可以添加和移除元素

ArrayBLockingQueue:

是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序。

LinkedBLockingQueue(虽然是有界,但是界限是Integer.MAX_VALUE):

一个基于链表结构的阻塞队列,此队列拟FTFO(先进先出)排序元素,吞吐量通常要高于ArrayBLockingQueue。

	// 可以设定阻塞队列的大小。
	public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }  
	public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        last = head = new Node<E>(null);
    }

SynchronousQueue:

一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高

BlockingQueue 使用:

方法异常特殊值阻塞超时
入队add(e)offer(e)put(e)offer(e, time)
出队remove()poll()take()poll(e, time)
检查element()peek()--
情况解释:队列满 、空时候,入队、出队操作后的处理方式
异常抛出异常
特殊值返回boolean值,表示是否成功
阻塞阻塞到可用
超时阻塞一段时间,超时后取消本次操作(出队或者入队)

在这里插入图片描述

Condition

提供对应于 Object类的 wait 和 notify 功能的类;

对应方法:await 和 signal方法

线程池

Callable

  1. Thread没有以Callable接口的方法,怎么使用
  2. Callable的 返回值怎么获取

传统的Thread构造方法没有Callable的构造方法,所以使用一个中间件(RunnableFuture)来处理这个问题。

Runable Future

RunableFuture

FutureTask(实现类)

在这里插入图片描述

在这里插入图片描述

四大基本线程池

Executors类创建线程池:

  • FixedThreadPool
  • SingleThreadPool
  • CachedThreadPool
  • ScheduledThreadPool
    public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>(), // 链表、有界(Integer.MAX_VALUE)
                                      threadFactory);
    }

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>())); // 链表、有界(Integer.MAX_VALUE)
    }

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());// 不存在元素的队列,这种方式会一直创建线程
    }

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {// 创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Time类。
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }

    public ScheduledThreadPoolExecutor(int corePoolSize,
                                       ThreadFactory threadFactory) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue(), threadFactory);
    }

FixedThreadPool 和 SingleThreadPool: 堆积大量请求,造成OOM

固定了线程数目(前一个用户确定,后一个线程数为1),但是其允许阻塞队列最大数目为 Integer.MAX_VALUE。容易堆积大量的请求,从而导致OOM

CachedThreadPool 和 ScheduledThreadPool: 创建太多线程,造成OOM

允许创建的线程数目为 Integer.MAX_VALUE,可能导致创建大量的线程,从而导致OOM

所以Alibaba不推荐使用 Executors 来创建线程池,而希望使用 ThreadPoolExecutor 来创建线程池。

7 - 基本参数

  1. 核心线程数目 int corePoolSize
  2. 最大线程数目 int maximumPoolSize
  3. 非核心线程存活时间 long keepAliveTime
  4. 存活时间单位 TimeUnit unit
  5. 阻塞队列 BlockingQueue workQueue
  6. 线程工厂 ThreadFactory threadFactory
  7. 饱和策略 RejectedExecutionHandler handler

核心线程

一般情况下:线程池所谓的核心线程,并不是特指哪几个线程是核心的,理论上讲,线程池中创建出来的线程都有可能被销毁,而所谓的核心线程是指特定条件下(线程数小于等于corePoolSize),剩下的线程都算“核心”线程,不会被销毁!

当allowCoreThreadTimeOut手动设置为 true,核心线程会像最大线程一样存活一定时间(keepAliveTime)后被销毁

allowCoreThreadTimeOut 默认设置为: false

可设置允许核心线程超时销毁:executor.allowCoreThreadTimeOut(true);

4 - 饱和策略

  1. 丢弃任务,并且抛出异常(默认) new ThreadPoolExecutor.AbortPolicy()
  2. 直接丢弃 new ThreadPoolExecutor.DiscardPolicy()
  3. 丢弃等待最久的任务 new ThreadPoolExecutor.DiscardOldestPolicy()
  4. 由调用线程处理该任务 new ThreadPoolExecutor.CallerRunsPolicy()

调用线程:将任务交给线程池的线程

Tips:

可根据应用场景实现 RejectedExecutionHandler 接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。

前三者都是直接丢弃,第四个是谁调用谁处理

自定义饱和策略

package com.company.Thread.Pool;

import java.util.concurrent.*;

public class TPExecutor {
    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                3,
                5,
                1000,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque(10),
                Executors.defaultThreadFactory(),
                new RejectPool());
        for(int i=0; i<100; i++){
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("-------------------------------");
                }
            });
        }
    }

    static class RejectPool implements RejectedExecutionHandler {
        public RejectPool(){}
        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            System.out.println("自定义拒绝策略");
        }
    }
}

线程池生命周期

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

**ctl:**线程池运行状态 3 位 runState 和 线程数目 29 位 workerCount

线程池的 5 种状态:

	private static final int RUNNING    = -1 << COUNT_BITS;
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    private static final int STOP       =  1 << COUNT_BITS;
    private static final int TIDYING    =  2 << COUNT_BITS;
    private static final int TERMINATED =  3 << COUNT_BITS;
运行状态状态解释
RUNNING线程池正常工作,可提交新任务,也可处理阻塞队列的任务
SHUTDOWN线程池关闭,不接受新任务,但会处理阻塞队列的任务
STOP线程池关闭,不接受新任务,会中断正在处理任务的线程
TIDYING所有的任务都已经终止,有效线程数目(所有线程都已结束) 为 0
TERMINATEDterminated方法执行完后进入该状态

在这里插入图片描述

Worker线程

private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable{
        /** Thread this worker is running in.  Null if factory fails. */
        final Thread thread;
        /** Initial task to run.  Possibly null. */
        Runnable firstTask;
    }

如何创建Worker?

会创建两种线程:核心线程 + 最大线程

如何执行阻塞队列中的任务?

  1. 构建Worker对象
        Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }
  1. 运行Worker的run() 方法
        public void run() {
            runWorker(this);
        }
  1. run方法内部调用 runWorker方法;

    • 其中 task = getTask()) != null 是执行阻塞队列任务的核心

    • 加锁:Worker继承了AQS,加锁:表示当前线程正在执行任务

      1. 加锁:表示当前线程正在执行任务

      2. 如果正在执行任务,则不应该中断线程。

      3. 如果该线程现在不是独占锁的状态,也就是空闲的状态,说明它没有在处理任务,这时可以对该线程进行中断

      4. 线程池在执行shutdown方法或tryTerminate方法时会调用interruptIdleWorkers方法来中断空闲的线程,interruptIdleWorkers方法会使用tryLock方法来判断线程池中的线程是否是空闲状态;如果线程是空闲状态则可以安全回收。

        在这里插入图片描述

    • 判断线程池是否 STOP

    • 执行任务

    • 解锁

  2. Worker线程回收

    • 交给垃圾回收机制去做,线程池只是维护好线程的引用,当决定哪些线程需要回收,清除其引用即可。
final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
            // getTask() 内部使用的是 put 和 take 阻塞式获取
            while (task != null || (task = getTask()) != null) {
                w.lock();
                // If pool is stopping, ensure thread is interrupted;
                // if not, ensure thread is not interrupted.  This
                // requires a recheck in second case to deal with
                // shutdownNow race while clearing interrupt
                // 1. 判断线程池是否 STOP 
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                // 2. 执行任务
                try {
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                        task.run();
                    } catch (RuntimeException x) {
                        thrown = x; throw x;
                    } catch (Error x) {
                        thrown = x; throw x;
                    } catch (Throwable x) {
                        thrown = x; throw new Error(x);
                    } finally {
                        afterExecute(task, thrown);
                    }
                } finally {
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            // 获取不到任务时,主动回收自己
            processWorkerExit(w, completedAbruptly);
        }
    }

在这里插入图片描述

Worker为什么要实现AQS:

保证在空闲时可以响应中断,在执行任务时不可被中断。

Worker为什么不使用 ReentrantLock????

ReentrantLock是允许重入的。

线程池进行shutdown或者修改线程池大小的时候,需要先拿到锁。也就是要达到在空闲时可以响应中断,在执行任务时不可被中断的目的。

We implement a simple non-reentrant mutual exclusion lock rather than use ReentrantLock because we do not want worker tasks to be able to reacquire the lock when they invoke pool control methods like setCorePoolSize. Additionally, to suppress interrupts until the thread actually starts running tasks, we initialize lock state to a negative value, and clear it upon start (in runWorker).

因为我们 不希望 Worker在调用像setCorePoolSize这样的线程池控制方法时能够重新获取锁。

此外,为了在线程实际开始运行任务之前抑制中断,我们将锁状态初始化为负值,并在启动时清除它

线程池中 Worker 只需要两个状态:

  • 一个是独占锁,表明正在执行任务;
  • 一个是不加锁,表明是空闲状态。

Worker 在专注与运行 task 的时候怎么会去调用**setCorePoolSize()**之类的方法呢?

以下两个方法在ThreadPoolExecutor里, 可以重写, 它在task.run 之前被调用,故还是有这种机会的。

protected void beforeExecute(Thread t, Runnable r) { }
protected void afterExecute(Runnable r, Throwable t) { }

ThreadPoolExecutor的实例

package com.company.Thread;

import java.util.concurrent.*;

public class MyMain {

    public static void main(String[] args){

        ExecutorService executorService = new ThreadPoolExecutor(
                2,
                5,
                1L,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.DiscardOldestPolicy()
                /*  new ThreadPoolExecutor.CallerRunsPolicy()
                * 	任务超出之后,多的任务就让调用者完成
                *		:在这个例子中就是:main线程
                * */

                /*  new ThreadPoolExecutor.AbortPolicy()
                *
                * java.util.concurrent.RejectedExecutionException:
                *       任务超出 最大线程数目 + 阻塞队列数目 之后
                *           很大可能报这个错误
                *   如:5 + 3 < 9
                 * */
        );
        try{
            for(int i=1; i<=10; i++){
                int finalI = i;
                executorService.execute(() ->{
                    System.out.println(
                            Thread.currentThread().getName()
                                    + "\t 办理业务" + finalI);
                });
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            executorService.shutdown();
        }
    }
}

合理配置线程数目

分为两种情况:

  1. CPU密集型
  2. IO密集型

CPU密集型:

是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),

而在单核CPU,无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就那些

CPU密集型任务配置尽可能少的线程数量:
一般公式:CPU核数 + 1个线程的线程池

IO密集型

  1. 由于IO密集型任务线程并不是一直在执行任务(CPU未一直占用),则应配置尽可能多的线程,如:CPU核数*2

  2. IO密集型,即该任务需要大量的IO,即大量的阻塞。
    在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费。所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。

    IO密集型时,大部分线程都阻塞,故需要多配置线程数:
    参考公式:CPU核数/ (1- 阻塞系数)

    阻塞系数在0.8~0.9之间

    比如8核CPU:8/(1-0.9)= 80个线程数

线程池的关闭

自动关闭:线程池自动关闭的两个条件:
  1. 线程池的引用不可达;

  2. 线程池中没有线程;

死锁编码与其定位

死锁: 是指两个或两个以上的进程(线程)之间互相持有对方所需要的资源,从而造成互相等待的现象。

若无外力干涉它们都将无法推进下去。

死锁的四个条件:

  1. 资源互斥
  2. 占有不释放
  3. 不剥夺
  4. 循环等待

产生死锁的主要原因:

  • 系统资源不足
  • 资源分配不当

死锁的定位分析

package com.company.Thread;

class ThreadDL implements Runnable{
    String lockA;
    String lockB;

    public ThreadDL(String lockA, String lockB) {
        this.lockA = lockA;
        this.lockB = lockB;
    }

    public void run(){
        synchronized (lockA){
            System.out.println(Thread.currentThread().getName() + "获取到A锁");
            try {
                Thread.sleep(1000);
                synchronized (lockB){
                    System.out.println(Thread.currentThread().getName() + "获取到B锁");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class DeadLock {

    public static void main(String[] args) {
        String A = "lockA";
        String B = "lockB";
        new Thread(new ThreadDL(A, B),"XXX").start();
        new Thread(new ThreadDL(B, A), "YYY").start();
    }
}

D:\software\java\jdk\bin\java.exe
XXX获取到A锁
YYY获取到A锁

jps:查看java的进程

jstack:jstack是java虚拟机自带的一种堆栈跟踪工具。jstack用于打印出给定的java进程ID或core file或远程调试服务的Java堆栈信息

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

在这里插入图片描述

线程的一些应用

单例模式

饿汉式(立即加载)

/*
饿汉式单例:类加载时候就创建对象
* */
class FastSingle {
    private static volatile FastSingle instance = new FastSingle();

    private FastSingle(){}

    public FastSingle getInstance(){
        return instance;
    }
}

懒汉式(延迟加载)

class Single {

    private static volatile Single instance = null;// 指令重排问题

    private Single(){
        if(instance != null){
            throws new Exception();
        }
    }

    public static Single getInstance(){
        if(instance == null){
            synchronized (Single.class){
                if(instance == null){
                    instance = new Single();
                }
            }
        }
        return instance;
    }
}

静态内部类(延迟加载)

/*
静态内部类实现单例
* */

class StaticSingle{
    /*
    为什么外部类加载时静态内部类未加载,
    《effective java》里面说静态内部类只是刚好写在了另一个类里面,实际上和外部类没什么附属关系。

    静态内部类:使用时候才加载
    * */
    private static class StaticSingleInstance{
        private static final StaticSingle instance = new StaticSingle();
    }
    private StaticSingle(){}

    public static StaticSingle getInstance(){
        return StaticSingleInstance.instance;
    }
}

枚举类型(立即加载)

/*
枚举类实现单例:
	zybuluo.com/natsumi/note/692892
枚举的特点:
	JVM会保证enum不能被反射并且构造器方法只执行一次。
* */
enum EnumSingle{
    INSTANCE; // 可以理解为 public static final EnumSingle INSTANCE;
    public EnumSingle getInstance(){
        return INSTANCE;
    }
}

死锁实现

package com.company.Thread.DeadLock;

public class DeadLock implements Runnable{
    String LockA;
    String LockB;

    public DeadLock(String lockA, String lockB){
        this.LockA = lockA;
        this.LockB = lockB;
    }

    @Override
    public void run() {
        synchronized (this.LockA){
            try {
                System.out.println(Thread.currentThread().getName() + "获取到锁" + this.LockA);
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            synchronized (this.LockB){
                System.out.println(Thread.currentThread().getName() + "咿呀! 没锁柱");
            }
        }
    }

    public static void main(String[] args) {
        String lockA = "Lock A";
        String lockB = "Lock B";
        Thread thread1 = new Thread(new DeadLock(lockA, lockB));
        Thread thread2 = new Thread(new DeadLock(lockB, lockA));
        thread1.start();
        thread2.start();
    }
}

Semaphore 三个线程轮流打印X次 ABC

package com.company.Thread;

import java.util.concurrent.Semaphore;

public class SemaphoreABC {
    Semaphore semaphoreA = new Semaphore(1);
    Semaphore semaphoreB = new Semaphore(0);
    Semaphore semaphoreC = new Semaphore(0);
    int count;
    int total;
    public ABC(int all){
        this.total = all;
        count = 0;
    }
    public void printABC(){

        Thread A = new Thread(new Runnable() {
            @Override
            public void run() {
                while(true) {
                    try {
                        semaphoreA.acquire();
                        if(count == total){
                            break;
                        }
                        System.out.println("A");
                        semaphoreB.release();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });

        Thread B = new Thread(new Runnable() {
            @Override
            public void run() {
                while(true) {
                    try {
                        semaphoreB.acquire();
                        if(count == total){
                            break;
                        }
                        System.out.println("B");
                        semaphoreC.release();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });

        Thread C = new Thread(new Runnable() {
            @Override
            public void run() {
                while(true){
                    try {
                        semaphoreC.acquire();
                        System.out.println("C");
                        count++;
                        if(count == total){ // 终止条件
                            semaphoreA.release();
                            semaphoreB.release();
                            break;
                        }
                        semaphoreA.release();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });

        A.start();
        B.start();
        C.start();
    }

    public static void main(String[] args) {
        new SemaphoreABC(3).printABC();
    }
}

ReentrantLock 三个线程轮流打印 ABC

package com.company.Thread.ABC;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionABC {
    Lock lock = new ReentrantLock();
    Condition conditionA = lock.newCondition();
    Condition conditionB = lock.newCondition();
    Condition conditionC = lock.newCondition();
    int threadFlag = 1;

    public void printABC(){
        Thread A = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        lock.lock();
                        while (threadFlag != 1) {
                            conditionA.await();
                        }
                        System.out.println("A");
                        threadFlag = 2;
                        conditionB.signal();
                        lock.unlock();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        Thread B = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        lock.lock();
                        while (threadFlag != 2) {
                            conditionB.await();
                        }
                        System.out.println("B");
                        threadFlag = 3;
                        conditionC.signal();
                        lock.unlock();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });

        Thread C = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        lock.lock();
                        while (threadFlag != 3) {
                            conditionC.await();
                        }
                        System.out.println("C");
                        threadFlag = 1;
                        conditionA.signal();
                        lock.unlock();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });

        A.start();
        B.start();
        C.start();
    }

    public static void main(String[] args) {
        new ConditionABC().printABC();
    }
}

Condition : 生产者消费者问题

package com.company.Thread.PAndC;

import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class PAndC {
    Queue<String> source;
    int count;

    Lock lock;
    Condition condition;
    public PAndC(Queue<String> source, int count) {
        this.source = source;
        this.count = count;
        this.lock = new ReentrantLock();
        condition = this.lock.newCondition();

    }

    public void producrMethod(){
        while (true) {
            try {
                lock.lock();
                while (source.size() == count) {
                    condition.await();
                }
                source.offer("资源");
                System.out.println(Thread.currentThread().getName() + " 生产者: " + source.size() );
                condition.signal();
                lock.unlock();

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void consumerMethod(){
        while (true) {
            try {
                lock.lock();
                while (source.size() == 0) {
                    condition.await();
                }
                source.poll();
                System.out.println(Thread.currentThread().getName() + " 消费者: " + source.size() );
                condition.signal();
                lock.unlock();

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void GenAndCon(){
        Thread producer = new Thread(new Runnable() {
            @Override
            public void run() {
                producrMethod();
            }
        });

        Thread producer2  = new Thread(new Runnable() {
            @Override
            public void run() {
                producrMethod();
            }
        });
        Thread consumer = new Thread(new Runnable() {
            @Override
            public void run() {
                consumerMethod();
            }
        });

        producer.start();
        producer2.start();
        consumer.start();
    }

    public static void main(String[] args) {
        new PAndC(new LinkedList<>(), 5).GenAndCon();
    }
}

Semaphore : 生产者消费者问题

package com.company.Thread.PAndC;

import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.Semaphore;

public class PCSemaphore {
    Queue<String> source;
    int count;

    Semaphore semaphoreP; // 控制生产者
    Semaphore semaphoreC; // 控制消费者
    Semaphore mutex; // 互斥量,控制 source 的互斥访问
    public PCSemaphore(Queue<String> source, int count) {
        this.source = source;
        this.count = count;
        // semaphoreP 可以生产的数量(表示缓冲区可用的数量)。 通过生产者调用acquire,减少permit数目
        semaphoreP = new Semaphore(5);
        // semaphoreC 可以消费的数量。通过生产者调用release,增加permit数目
        semaphoreC = new Semaphore(0);
        mutex = new Semaphore(1);
    }

    public void producrMethod(){
        while (true) {
            try {
                semaphoreP.acquire();
                mutex.acquire();
                source.offer("资源");
                System.out.println(Thread.currentThread().getName() + " 生产者: " + source.size() );
                mutex.release();
                semaphoreC.release();

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void consumerMethod(){
        while (true) {
            try {
                semaphoreC.acquire();
                mutex.acquire();
                source.poll();
                mutex.release();
                System.out.println(Thread.currentThread().getName() + " 消费者: " + source.size() );
                // 生产者调用release,增加可以消费的数量
                semaphoreP.release();

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void GenAndCon(){
        Thread producer = new Thread(new Runnable() {
            @Override
            public void run() {
                producrMethod();
            }
        });

        Thread producer2  = new Thread(new Runnable() {
            @Override
            public void run() {
                producrMethod();
            }
        });
        Thread consumer = new Thread(new Runnable() {
            @Override
            public void run() {
                consumerMethod();
            }
        });

        producer.start();
        producer2.start();
        consumer.start();
    }

    public static void main(String[] args) {
        new PCSemaphore(new LinkedList<>(), 5).GenAndCon();
    }
}

wait() 和 notifyAll() :生产者消费之

消费者

package com.company.Thread.ProducerAndConsumer;

import java.util.List;
import java.util.concurrent.TimeUnit;

public class Consumer implements Runnable{

    /**
     * 产品容器
     */
    private final List<Integer> container;

    public Consumer(List<Integer> container) {
        this.container = container;
    }

    /**
     * 消费者消费产品
     */
    private void consume() throws InterruptedException {
        // System.out.println("  consume  not in ");
        synchronized (container){
            // System.out.println("  consume in  ");
            while (container.isEmpty()){
                System.out.println("...容器是空的,暂停消费...");
                container.wait();
            }
            Integer p = container.remove(0);
            //模拟1秒消费一个产品
            TimeUnit.MILLISECONDS.sleep(1000);
            System.out.println("消费产品:" + p);
            container.notifyAll();
        }
    }
    @Override
    public void run() {
        while (true){
            try {
                consume();
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println("consume error");
            }
        }
    }
}

生产者

package com.company.Thread.ProducerAndConsumer;

import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;

public class Producer implements Runnable {
    /**
     * 产品容器
     */
    private final List<Integer> container;

    public Producer(List<Integer> container) {
        this.container = container;
    }

    /**
     * 生产者生产方法
     *
     * @throws InterruptedException
     */
    private void produce() throws InterruptedException {
        //产品容器容量
        int capacity = 5;
        // System.out.println("   produce  not in ");
        synchronized (container) {
            // System.out.println("   produce in ");
            //当容器已满,暂停生产
            while (container.size() == capacity) {
                System.out.println("...容器已经满了,暂停生产...");
                container.wait();
            }
            Random random = new Random();
            int p = random.nextInt(50);
            //模拟1秒生产一个产品
            TimeUnit.MILLISECONDS.sleep(1000);
            System.out.println("生产产品:" + p);
            container.add(p);
            container.notifyAll();
        }
    }

    @Override
    public void run() {
        while (true) {
            try {
                produce();
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println("produce error");
            }
        }
    }
}

调用

package com.company.Thread.ProducerAndConsumer;

import java.util.ArrayList;
import java.util.List;

public class Test {
    public static void main(String[] args) {
        List<Integer> container = new ArrayList<>();
        Thread producer = new Thread(new Producer(container));
        Thread producer2 = new Thread(new Producer(container));
        Thread producer3 = new Thread(new Producer(container));
        Thread consumer = new Thread(new Consumer(container));
        Thread consumer2 = new Thread(new Consumer(container));
        producer.start();
        producer2.start();
        producer3.start();
        consumer.start();
        consumer2.start();

    }
}

JVM

JVM内存

JVM的垃圾回收

在这里插入图片描述

Java的类加载机制?

  1. 加载:双亲委派机制进行类的加载
  2. 链接
    1. 验证:语法错误查验
    2. 准备:内存分配,初始化 0
    3. 解析:字符引用 -> 直接引用
  3. 初始化:clinit方法(静态方法块之类)

Java中new一个对象之后发生了什么?

  1. 类加载
  2. 内存分配
  3. 初始化 0
  4. 设置对象头
  5. 初始构造函数之类

GC的作用区域:

堆和方法区

怎么判别垃圾是否可回收?

  • 引用计数

  • 可达性分析

哪些对象可以作为GC roots?

  1. 虚拟机栈中对象
  2. 本地方法栈的对象
  3. 方法区中静态属性的对象,常量池中的引用
  4. JVM内部调用( Class对象,常驻异常Exception,ClassLoader )
  5. 反映JVM的内部情况的JMXBean,JVMTI的回调
  6. Synchronized所持有的对象

如何进行JVM调优和参数配置:

JVM参数类型

  • 标配参数

  • x参数

    • -Xint 解释执行
    • -Xcomp 第一次使用就编译成本地代码
    • -Xmixed 混合模式

    编译 和 解释 的区别

  • XX参数

    • Boolean类型 (+ 代表开启对应参数功能, - 表示关闭对应参数功能:-XX: +PrintGCDetails
    • K - V 类型 (设置某些参数值,如元空间大小: -XX:MetaspaceSize = 128m
    • 其他:
      • -Xms和-Xmx
      • -Xms 等价于 -XX:InitialHeapSize (初始堆内存,1/64)
      • -Xmx 等价于 -XX:MaxheapSize(最大堆内存,1/4)

    用于设定具体的参数,分为两种类型参数:+/-布尔类型,KV键值类型

jinfo: 查看正在运行的Java进程对应的某些JVM参数是否开启

jinfo -flag 具体参数 java进程编号

jinfo -flags java进程编号

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-924Mf2e7-1638363515178)(Java多线程.assets/image-20210715172319162.png)]

java -XX:+PrintFlagsInitial //获取JVM初始参数

 [

java -XX:+PrintFlagsFinal //获取JVM被修改后的参数

在这里插入图片描述

-XX:+PrintCommandLineFlags:打印命令行参数

JVM常用的基本配置

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2zvAVjDj-1638363515181)(Java多线程.assets/image-20210715192135109.png)]

-Xms 等同于 -XX:InitialHeapSize 设置初始堆内存大小,默认物理内存的1/64

-Xmx 等同于 -XX:MaxHeapSie 设置最大堆内存大小,默认物理内存的1/4

-Xss 等同于 -XX:ThreadStackSize 设置线程栈的大小,默认512k - 1024k

-Xmn 设置年轻代大小

-XX:MetaspaceSize 设置元空间的大小(默认情况下,元空间只受到本地内存限制)

具有默认大小

-XX:+UseSerialGC 设置串行垃圾回收

-XX:+UseParallelGC 设置并行垃圾回收

-XX:PrintGCDetails 打印GC细节

-XX:SurvivorRatio 设置eden、S0、S1空间的比例,默认8:1:1;若设置-XX:SurvivorRatio=4 则为4:1:1,这儿设置的是eden的比例

-XX:NewRatio 设置年轻代与老年代的比例,若-XX:NewRatio=2,则新生代:老年代 = 1:2

若-XX:NewRatio=4,则新生代:老年代 = 1:4(默认 1:2)

-XX:MaxTenuringThreshold 设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概率。

java -XX:PrintGCDetails

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KQ3EbJEC-1638363515182)(Java多线程.assets/image-20210715193738653.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-md74xiGJ-1638363515182)(Java多线程.assets/image-20210715193942551.png)]

在这里插入图片描述

https://docs.oracle.com/javase/8/docs/index.html

引用问题 (强、软、弱,虚,引用队列)

引用层级关系

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6IZ7Tp9v-1638363515183)(Java多线程.assets/image-20210715205953295.png)]

四大引用与引用队列

强引用

**GC角度:**就算出现OOM也不会对该对象进行回收

**场景:**是我们最常见的引用,将对象赋给一个引用变量,这个引用变量就是强引用。当一个对象是强引用的时候,它处于可达状态,不会被GC回收掉。

强引用也是造成内存泄漏的原因之一

软引用

**GC角度:**内存够用则不进行回收,不够用则进行回收

**场景:**内存敏感程序中,比如高速缓存中就会用到

弱引用

**GC角度:**引用的对象存在强引用则不会被回收,反之,引用的对象不存在强引用则会被回收

场景: ThreadLocal的键就是使用的弱引用

虚引用

顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。
**GC角度:**如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列(ReferenceQueue)联合使用
**场景:**虚引用的主要作用是跟踪对象被垃圾回收的状态。仅仅是提供了一种确保对象被finalize以后,做某些事情的机制。
PhantomReference的get方法总是返回null,因此无法访问对应的引用对象。其意义在于说明一个对象已经进入finalization阶段,可以被gc回收,用来实现比finalization机制更灵活的回收操作。

换句话说,设置成引用关联的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理

引用队列

Java中引用在GC的时候,在这期间(GC之前)都会被放在引用队列ReferenceQueue中保存一下。

**作用:**我们希望当一个对象被gc掉的时候通知用户线程,进行额外的处理时,就需要使用引用队列了。ReferenceQueue即这样的一个对象,当一个obj被gc掉之后,其相应的包装类,即ref对象会被放入queue中。我们可以从queue中获取到相应的对象信息,同时进行 额外的处理 。比如反向操作,数据清理等。

所以虚引用和引用队列常常一起使用

相关场景介绍

软引用场景

假如有一个应用需要读取大量的本地图片:

  • 如果每次读取图片都从硬盘读取则会严重影响性能,

  • 如果一次性全部加载到内存中又可能造成内存溢出。

此时使用 软引用 可以解决这个问题:
设计思路是:用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题。

Map<String, SoftReference<Bitmap>>imageCache = new HashMap<String, SoftReference<Bitmap>>();

知道WeakHashMap吗?

WeakHashMap的键是弱引用

在这里插入图片描述

引用总结

在这里插入图片描述

OOM 常见错误

  • StackOverflowError 栈空间溢出
  • OutOfMemoryError:Java heap space 堆空间不足了
  • OutOfMemoryError:Metaspace
  • OutOfMemoryError:Direct buffer memory
  • OutOfMemoryError:GC overhead limit exceeded
  • OutOfMemoryError:unable to create new native thread

StackOverflowError

Java heap space

GC overhead limit exceeded

OutOfMemoryError:GC overhead limit exceeded

GC回收时间过长时会抛出OutOfMemroyError。

过长的定义是,超过98%的时间用来做GC并且回收了不到2%的堆内存,连续多次GC 都只回收了不到2%的极端情况下才会抛出。

假如不抛出GC overhead limit 错误会发生什么情况呢?那就是GC清理的这么点内存很快会再次填满,迫使GC再次执行.这样就形成恶性循环,CPU使用率一直是100%,而GC却没有任阿成果

在这里插入图片描述

Direct buffer memory

OutOfMemoryError:Direct buffer memory

元空间大小受到本地内存限制;

导致原因:
NIO程序经常使用ByteBuffer来读取或者写入数据,这是一种基于通道(Channel)与缓冲区(Buffer)的I/0方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为邈免了在Java堆和Native堆中来回复制数据。

***ByteBuffer.allocate(capability)***第一种方式是分此JVM堆内存,属于GC管辖范围,由于需要拷贝所以速度相对较馒

***ByteBuffer.allocateDirect(capability)***第2种方式是分派OS本地内存,不属于GC管辖范围,由于不需要内存拷贝所以速度相对较快

但如果不断分配本地内存,堆内存很少使用,那么JVM就不需要执行GC,DirectByteBuffer对象们就不会被回收,这时候堆内存充足,但本地内存可能已经使用光了,再次尝试分配本地内存就会出现OutOfMemoryError。那程序献直接崩溃了。

unable to create new native thread

OutOfMemoryError:unable to create new native thread

导救原因:

  1. 你的应用创建了太多线程了,一个应用进创逮多个线程,超过系统承载极限
  2. 你的服务器并不允许你的应用程序创建这么多线程, Linux系统默认允许单个进程可以创建的线程数是1024个,你的应用创建超过这个数量,就会报java.lang.OutOfMemoryError: unable to create new native thread

解决办法:

  1. 想办法降低你应用程序创建线程的数量,分析应用是否真的需要创建这么多线程,如果不是,改代码将线程数降到最低
  2. 对于有的应用,确实需要创建很多钱程,远超过Linux系统的默认1024个线程的限制,可以迪过修改Linux服务器配置,扩大inux默认限制

简单的说,就是创建的线程太多了

Metaspace

OutOfMemoryError:Metaspace

Java 8及之后的版本使月的etaspace来替代永久代。
Metaspace是方法区在Hotspot中的实现,它与持久代最大的区别在于:Metaspace并不在虚拟机内存中而是使用本地内存

也即在java8中, classe metadata(the virtual machines internal presentation of Java class),被存储在叫做Metaspace firinative memory

java8后,永久代被元空间Metaspace取代了,其中存放了以下信息:

  • 虚拟机
  • 加载的类信息
  • 运行时常量池
  • 静态变量
  • 即时编译后的代码

模拟Metaspace空间溢出,我们不断生成类往元空间灌,类占据的空间总是会超过Metaspace指定的空间大小的

在这里插入图片描述

垃圾回收

对象已死

  1. 引用计数法

  2. 可达性分析

垃圾回收算法

  1. 标记清除
  2. 复制
  3. 标记整理
  4. 分代回收

垃圾回收器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z6FBnCIJ-1638363515185)(Java多线程.assets/image-20210716102205386.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GzAJPLlb-1638363515186)(Java多线程.assets/image-20210716103246921.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-32bkIfba-1638363515186)(Java多线程.assets/image-20210716103617700.png)]

查看默认的垃圾回收器

java -XX:+PrintCommandLineFlags -version

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hcrs7CjV-1638363515187)(Java多线程.assets/image-20210716102125394.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T4iKOsTg-1638363515187)(Java多线程.assets/image-20210716102454526.png)]

Client / Server

适用范围:

  • 只需要掌握Server模式即可,Client模式基本不会用

操作系统:

  • 32位Window操作系统,不论硬件如何都默认使用Client的JVM模式
  • 32位其它操作系统,2G内存同时有2个cpu以上用Server模式,低于该配置还是Client模式
  • 64位only server模式

SerialGC(新生代)

新生代串行,老年代串行

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Me4EvNI5-1638363515188)(Java多线程.assets/image-20210716103552015.png)]

ParNew(新生代)

新生代并行,老年代串行

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dYZTtdMn-1638363515188)(Java多线程.assets/image-20210716104258112.png)]

Parallel Scavenge

新生代并行,老年代也是并行

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fuOe0fxB-1638363515189)(Java多线程.assets/image-20210716104310501.png)]

吞吐量

类似于ParNew,但是更注重于吞吐量的垃圾回收器,吞吐量=用户线程运行时间/(用户线程运行时间+GC运行时间),这意味着高效利用CPU的时间,它多用于在后台运算而不需要太多交互的任务

自适应调节策略

也是ParallelScavenge收集器与ParNew收集器的一个重要区别。(自适应调节策略:虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间(-XX:MaxGCPauseMillis)或最大的吞吐量。

常用JVM参数:

-XX:+UseParallelGC 或 -XX:+UseParallelOldGC(可互相激活) 使用Parallel Scanvenge收集器

开启该参数后:新生代使用复制算法,老年代使用标记-整理算法

多说一句:

-XX:ParallelGCThreads=数字N表示启动多少个GC线程

cpu>8 N=5/8
cpu<8 N=实际个数

Concurrent Mark Sweep

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dQXmaVCp-1638363515189)(Java多线程.assets/image-20210716105953278.png)]

  1. 由于并发进行,CMS在收集与应用线程会同时会增加对堆内存的占用,

也就是说,CMS必须要在老年代堆内存用尽之前完成垃圾回收,否则CMS回收失败时,将触发担保机制,串行老年代收集器将会以STW的方式进行一次GC,从而造成较大停顿时间

  1. 标记清除方法,产生内存碎片

Garbage First

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T9aWcQFi-1638363515189)(Java多线程.assets/image-20210716111623646.png)]

  • 像CMS收集器一样,能与应用程序线程并发执行。
  • 整理空闲空间更快。
  • 需要更多的时间来预测GC停顿时间。
  • 不希望牺牲大量的吞吐性能。
  • 不需要更大的Java Heap。

在这里插入图片描述

G1收集器的设计目标是取代CMS收集器,它同CMS相比,在以下方面表现的更出色:

  1. G1是一个有整理内存过程的垃圾收集器,不会产生很多内存碎片。
  2. G1的Stop The World(STW)更可控, ;G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间。

CMS垃圾收集器虽然减少了暂停应用程序的运行时间,但是它还是存在着内存碎片问题。于是,为了去除内存碎片问题,同时又保留CMS垃圾收集器低暂停时间的优点,JAVA7发布了一个新的垃圾收集器-G1垃圾收集器。

G1是在2012年才在jdk1.7u4中可用。oracle官方计划在JDK9中将G1变成默认的垃圾收集器以替代CMS。

它是一款面向服务端应用的收集器,主要应用在多CPU和大内存服务器环境下,极大的减少垃圾收集的停顿时间,全面提升服务器的性能,逐步替换CMS

  1. 内存划分为子区域
  2. 整体标记整理算法,局部复制算法,不会产生内存碎片
  3. 物理上不区分年轻代和老年代,但是在逻辑上存在分代概念(也就是说不同分代的子区域并不是物理上连续的)
  4. 可以精确控制停顿时间
  5. 顺序:初始标记 - 并发标记 - 最终标记 - 筛选回收

如何 JVM + GC + SpringBoot 进行调优

  1. IDEA开发完微服务工程

  2. maven进行clean package

  3. 要求微服务启动的时候,同时配置我们的JVM/GC的调优参数

    1. 内部启动:

      在这里插入图片描述

    2. 外部启动:

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VQqJTcmu-1638363515191)(Java多线程.assets/image-20210716113402898.png)]

总结

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v9Z4jsUF-1638363515191)(Java多线程.assets/image-20210716110439141.png)]

Java的工具

jps

jstack

jinfo

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值