Java线程系列详解

一,基本概念

  • 进程: 程序是计算机指令的集合,它以文件形式存储在磁盘上,而进程就是一个执行中的程序,而每一个进程都有其独立的内存空间和系统资源。
  • 线程: 线程运行在进程中,不能独立存在。线程是CPU调度的最小单元。

线程安全: 线程安全包含原子性可见性和有序性。

1.1 线程模型

  • 内核线程模型: 使用内核线程实现的方式,通常也被成为1 : 1实现模型。内核线程(Kernel Level
    Thread,KLT)是直接由操作系统内核来支持的线程,这种线程由内核来控制切换,内核通过调度器(Scheduler)来对线程进行调度,并负责将线程任务映射到各个处理器,在多核操作系统中具有能力并行处理多个任务,这种支持多线程的内核被称为多线程内核(Mutil-Threads
    Kernel)。
  • 用户线程模型 用户线程实现的方式被称为1 : N模型,非内核线程都可以被看作是用户线程(User Thread,UT)的一种。在用户空间的线程,线程的控制无需内核参与,内核也无法感知其实现模式,这种线程也不需要进行用户态和内核态的切换,因此用户线程对资源的使用率较小,支持大规模的线程数量。
  • 混合模型: 整合两种模型特性的混合实现模型,也被成为M : N模型。

1.2 线程调度策略

  • 协同式调度: 线程调度由其本身来控制,线程在自身工作执行完成后,主动通知系统切换到另一个线程执行。
  • 抢占式调度: 线程的调度由系统分配执行时间,线程的切换由系统决定。JAVA使用的就是抢占式。

1.3 线程的生命周期

在这里插入图片描述
初始状态: 指的是线程已经被创建,但是还不允许分配CPU执行。这个状态属于编程语言特有的,不过这里所谓的被创建,仅仅是在编程语言层面被创建,而在操作系统层面,真正的线程还没有被创建。

可运行状态: 指的是线程可以分配CPU执行。在这种状态下,真正的操作系统线程已经被创建了,所以可以分配CPU,执行。

运行状态: 当有空闲的CPU 时,操作系统会将其分配给一个处于可运行状态的线程,被分配到CPU 的线程状态就转化为运行状态。

休眠状态: 运行状态的线程如果调用了一个阻塞的API(例如以阻塞方式读文件)或者等待某个事件(例如条件变量)那么线程的状态就转到休眠状态,同时释放CPU的使用权,休眠状态的线程永远没有机会获得CPU的使用权,当等待的事件出现了,线程就会从休眠状态转换到可运行状态。

终止状态: 线程运行完或者出现异常就会进入终止状态,终止状态下的线程不会切换到其他任何状态,进入终止状态就意味着线程的声明周期结束了。

这五种状态在不同的编程语言会有简化合并。例如,C 语言的 POSIX Threads 规范,就把初始状态和可运行状态合并了; Java 语言里则把可运行状态和运行状态合并了,这两个状态在操作系统调度层面有用,而 JVM 层面不关心这两个状态,因为 JVM 把线程调度交给操作系统处理了。

二,Java中的线程

2.1 Java中线程的生命周期

NEW 初始状态

RUNNABLE 可运行状态/运行状态

BLOCKED 阻塞状态

WAITING 无时限等待

TIMED_WAITING 有时限等待

TERMINATED 终止状态

Java 线程中的 BLOCKED、WAITING、TIMED_WAITING 是一种状态,即前面我们提到的休眠状态。 也就是说,只要Java 线程处于这三种状态之一,那么这个线程就永远没有CPU 的使用权。

Java 线程的生命周期可以简化为:
在这里插入图片描述
BLOCKED、WAITING、TIMED_WAITING 可以理解为线程导致休眠状态的三种原因。 那具体是哪些情形会导致线程从RUNNABLE 转化到这三种状态呢?

RUNNABLE 与 BLOCKED 的状态转换

只有一种场景会触发这种转换,就是线程等待synchronized 的隐式锁。synchronized 修饰的代码块、方法同一时刻只能有一个线程执行,其他线程只能等待,这种情况下,等待的线程就会从RUNNABLE 转换到 BLOCKED 状态。 而当等待的线程获取到了synchronized 的隐式锁时,就又会从 BLOCKED 转换到 RUNNABLE 状态。

RUNNABLE 与 WAITING 的状态转换

第一场景,获得synchronized 隐式锁的线程,调用无参数的Object.wait()方法。

第二场景,调用无参数的 Thread.join() 方法。其中的 join() 是一种线程同步方法,例如有一个线程对象 thread A,当调用 A.join() 的时候,执行这条语句的线程会等待 thread A 执行完,而等待中的这个线程,其状态会从 RUNNABLE 转换到 WAITING。当线程 thread A 执行完,原来等待它的线程又会从 WAITING 状态转换到 RUNNABLE。

第三场景,调用 LockSupport.park() 方法。 其中的 LockSupport 对象,也许你有点陌生,其实 Java 并发包中的锁,都是基于它实现的。调用 LockSupport.park() 方法,当前线程会阻塞,线程的状态会从 RUNNABLE 转换到 WAITING。调用 LockSupport.unpark(Thread thread) 可唤醒目标线程,目标线程的状态又会从 WAITING 状态转换到 RUNNABLE。

RUNNABLE 与 TIMED_WAITING 的状态转换

有五种场景触发这种转换:

1、调用带有超时参数的Thread.sleep(long millis) 方法;

2、获得synchronized 隐式锁的线程,调用带超时参数的 Object.wait(long timeout) 方法;

3、调用带超时参数的 Thread.join(long millis) 方法;

4、调用带超时参数的 LockSupport.parkNanos(Object blocker, long deadline) 方法;

5、调用带超时参数的 LockSupport.parkUntil(long deadline) 方法。

这里你会发现,TIMED_WAITING 和 WAITING 状态的区别,仅仅是触发条件多了超时参数。

从 NEW 到 RUNNABLE 状态

Java 刚创建出来的Thread 就是NEW 状态的,而创建Thread 对象主要有两种方法。

一种是继承 Thread 对象,重写 run() 方法。

另一种是实现 Runnable 接口,重写 run() 方法,并将该实现类作为创建 Thread 对象的参数。

NEW 状态的线程,不会被操作系统调度,因此不会被执行。Java 线程要执行,就必须转换到RUNNABLE 状态,从 NEW 状态转换到 RUNNABLE 状态很简单,只要调用线程对象的 start() 方法就可以了 。

从 RUNNABLE 到 TERMINATED 状态

线程执行完 run() 方法后,会自动转换到 TERMINATED 状态,当然如果执行 run() 方法的时候异常抛出,也会导致线程终止。 有时候我们需要强制中断 run() 方法的执行,例如 run() 方法访问一个很慢的网络,我们等不下去了,想终止怎么办呢?Java 的 Thread 类里面倒是有个 stop() 方法,不过已经标记为 @Deprecated,所以不建议使用了。正确的姿势其实是调用 interrupt() 方法。

2.2 Java中如何创建线程

Thread: Thread类位于java.lang包,JDK1.0引入。在HotSpot虚拟机中,线程使用的是基于操作系统的1 : 1的内核实现模型来创建线程,线程的创建、调度、执行、销毁等由内核进行控制,调度过程通过抢占式策略进行调度。

创建线程的方式,继承Thread和实现Runable接口,这两种方式无法获取线程运行结果和异常,但可以使用Callable(JDK1.5)接口实现。把Thread和Callable关联可以使用FutureTask。

继承Thread类

public class Test extends Thread{
    @Override
    public void run() {
        System.out.println("Thread is Created");
    }
}

实现Runnable接口

public class Test implements Runnable{
    @Override
    public void run() {
        System.out.println("Thread is Created");
    }
}

实现Callable接口

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

Thread中无论哪一种构造方法都没有Callable类型的target,只能传入Runnable类型的target,那如何把Thread和Callable联系起来,这里就要引入Future接口和FutureTask实现类。

Thread中的native层方法:

yield: yield的字面意思是退让。调用该方法会向调度程序提示当前线程愿意放弃其当前对处理器的使用,调度程序可以随意忽略此提示。yield是一种启发式尝试,使用它可以改善线程之间的相对进展,否则会过度使用 CPU。

join: join方法让一个线程加入到另一个线程之前执行,在此线程执行期间,其他线程进入阻塞状态,当然也可以指定join入参(指定执行等待的超时时间),最多等待几毫秒让该线程终止,超时0意味着永远等待。此实现使用以this.isAlive为条件的this.wait调用循环,当线程终止时,将调用this.notifyAll方法。建议应用程序不要在Thread实例上使用wait、notify或notifyAll。如果任何线程中断了当前线程,会抛出InterruptedException异常时清除当前线程的中断状态。

sleep: 当调用线程的sleep方法,使当前执行的线程休眠(暂时停止执行)指定的毫秒数,取决于系统计时器和调度程序的精度和准确性。如果任何线程中断了当前线程,会抛出InterruptedExceptio异常时清除当前线程的中断状态。

interrupt: 使用interrupt方法会中断这个线程,除非当前线程正在中断自己,否则会调用该线程的checkAccess方法,这可能会导致抛出SecurityException。主要有以下几种场景:

  • 如果一个线程被Object类的wait、或者Thread的join、sleep方法调用处于阻塞状态时,那么它的中断状态会被清除并且会收到一个InterruptedException。
  • 如果该线程在InterruptibleChannel上的IO操作中被阻塞,则通道将关闭,线程的中断状态将被设置,线程将抛出java.nio.channels.ClosedByInterruptException。
  • 如果该线程在java.nio.channels.Selector中被阻塞,则该线程的中断状态将被设置,并且它将立即从选择操作返回,可能带有非零值,就像调用了选择器的唤醒方法一样。如果前面的条件都不成立,则将设置该线程的中断状态。

2.3 ThreadLocal

ThreadLocal直译为线程局部变量,其主要作用就是实现线程本地存储功能,通过线程本地资源隔离,解决多线程并发场景下线程安全问题。在一个线程中是共享的,在不同线程之间是隔离的。向ThreadLocal存入一个值,实际上是向当前线程对象中的ThreadLocalMap存入值,而key就是当前ThreadLocal实例。

ThreadLocal内部代码:

    static class ThreadLocalMap {

        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

        // 初始化默认容量为 16
        private static final int INITIAL_CAPACITY = 16;

        // 数据存储结构底层实现为Entry数组,其长度必须为2的倍数
        private Entry[] table;
        // table中Entry的实际数量,初始值为0
        private int size = 0;
        // resize扩容阈值加载因子为2/3
        private void setThreshold(int len) {
        threshold = len * 2 / 3;
        }
}

三,Java中的线程同步

在多线程操作时,存在的问题就是线程同步。当使用多个线程来访问同一个数据时,将会导致数据不准确,相互之间产生冲突,非常容易出现线程安全问题。

线程同步的真实意思,其实是“排队”:几个线程之间要排队,一个一个对共享资源进行操作,而不是同时进行操作。

所以我们用同步机制来解决这些问题,加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。

3.1 加锁的方式

synchronized: 重量级锁,这种方式比较灵活,修饰一个代码块,被修饰的代码块称为同步语句块。
Lock: Lock中的实现类ReentrantLock和ReentrantReadWriteLock。
volatile: volatile关键字只能修饰变量,不能保证原子性,不会发生阻塞。

Java中的线程模型如下图所示:

我们知道
(1)每个线程都有自己的本地内存空间(java栈中的帧)。线程执行时,先把变量从内存读到线程自己的本地内存空间,然后对变量进行操作。
(2)对该变量操作完成后,在某个时间再把变量刷新回主内存。

那么我们再了解下锁提供的两种特性:互斥(mutual exclusion) 和可见性(visibility):

(1)互斥(mutual exclusion):互斥即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据;

(2)可见性(visibility):简单来说就是一个线程修改了变量,其他线程可以立即知道。保证可见性的方法:volatile,synchronized,final(一旦初始化完成其他线程就可见)。

volatile
简单概括volatile,它能够使变量在值发生改变时能尽快地让其他线程知道。

编译器为了加快程序运行的速度,对一些变量的写操作会先在寄存器或者是CPU缓存上进行,最后才写入内存。而在这个过程中,变量的新值对其他线程是不可见的。

如下场景:

public class RunThread extends Thread {

    private boolean isRunning = true;

    public boolean isRunning() {
        return isRunning;
    }

    public void setRunning(boolean isRunning) {
        this.isRunning = isRunning;
    }

    @Override
    public void run() {
        System.out.println("进入到run方法中了");
        while (isRunning == true) {
        }
        System.out.println("线程执行完成了");
    }
}

public class Run {
    public static void main(String[] args) {
        try {
            RunThread thread = new RunThread();
            thread.start();
            Thread.sleep(1000);
            thread.setRunning(false);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在main线程中,thread.setRunning(false),将启动的线程RunThread中的共享变量设置为false,从而想让RunThread.java的while循环结束。如果使用JVM -server参数执行该程序时,RunThread线程并不会终止,从而出现了死循环。

原因: 现在有两个线程,一个是main线程,另一个是RunThread。它们都试图修改isRunning变量。按照JVM内存模型,main线程将isRunning读取到本地线程内存空间,修改后,再刷新回主内存。

而在JVM设置成 -server模式运行程序时,线程会一直在私有堆栈中读取isRunning变量。因此,RunThread线程无法读到main线程改变的isRunning变量。从而出现了死循环,导致RunThread无法终止。

修改:

volatile private boolean isRunning = true;

原理:

当对volatile标记的变量进行修改时,会将其他缓存中存储的修改前的变量清除,然后重新读取。一般来说应该是先在进行修改的缓存A中修改为新值,然后通知其他缓存清除掉此变量,当其他缓存B中的线程读取此变量时,会向总线发送消息,这时存储新值的缓存A获取到消息,将新值给B。最后将新值写入内存。当变量需要更新时都是此步骤,volatile的作用是被其修饰的变量,每次更新时,都会刷新上述步骤。

3.2 synchronized

原理:https://segmentfault.com/a/1190000041268785

Java语言的关键字,可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。当两个并发线程访问同一个对象object中的这个加锁同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。然而,当一个线程访问object的一个加锁代码块时,另一个线程仍然可以访问该object中的非加锁代码块。

object中的方法:

  • wait():释放占有的对象锁,线程进入等待池,释放cpu,而其他正在等待的线程即可抢占此锁,获得锁的线程即可运行程序。而sleep()不同的是,线程调用此方法后,会休眠一段时间,休眠期间,会暂时释放cpu,但并不释放对象锁。也就是说,在休眠期间,其他线程依然无法进入此代码内部。休眠结束,线程重新获得cpu,执行代码。wait()和sleep()最大的不同在于wait()会释放对象锁,而sleep()不会!
  • notify():该方法会唤醒因为调用对象的wait()而等待的线程,其实就是对对象锁的唤醒,从而使得wait()的线程可以有机会获取对象锁。调用notify()后,并不会立即释放锁,而是继续执行当前代码,直到synchronized中的代码全部执行完毕,才会释放对象锁。JVM则会在等待的线程中调度一个线程去获得对象锁,执行代码。需要注意的是,wait()和notify()必须在synchronized代码块中调用。
  • notifyAll()则是唤醒所有等待的线程。

3.3 lock

采用synchronized关键字来实现同步的话,就会导致一个问题:如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。

另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。

1)Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;

2)Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

3.4 总结:三种锁的区别

volatile和synchronized区别

1)volatile本质是在告诉jvm当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。

2)volatile仅能使用在变量级别,synchronized则可以使用在变量,方法。

3)volatile仅能实现变量的修改可见性,而synchronized则可以保证变量的修改可见性和原子性。
  
4)volatile不会造成线程的阻塞,而synchronized可能会造成线程的阻塞。

5)当一个域的值依赖于它之前的值时,volatile就无法工作了,如n=n+1,n++等。如果某个域的值受到其他域的值的限制,那么volatile也无法工作,如Range类的lower和upper边界,必须遵循lower<=upper的限制。

6)使用volatile而不是synchronized的唯一安全的情况是类中只有一个可变的域。

synchronized和lock区别

1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现。

2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生。而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁

3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断。

4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。

5)Lock可以提高多个线程进行读操作的效率。

四.线程池

4.1 线程池概念

一种使用线程的模式,存放了很多可以复用的线程,对线程统一管理。我们可以使用new的方式去创建线程,但若是并发线程太高,每个线程执行时间不长,这样频繁的创建销毁线程是比较耗费资源的,线程池就是用来解决此问题的。

使用线程池的优点:

  • 降低资源的消耗:线程可以重复使用,不需要在创建线程和消耗线程上浪费资源;
  • 提高响应速度:任务到达时,线程可以复用已有的线程,及时响应;
  • 可管理性:无限制的创建线程会降低系统效率,线程池可以对线程进行管理、监控、调优。

4.2 创建线程池的几种方式

  • newCacheThreadPool:创建一个可以缓存的线程池,如果线程池长度超过处理需要,可以灵活回收空闲线程,没回收的话就新建线程。
  • newFixedThread:创建一个定长的线程池,可控制最大并发数,超出的线程进行队列等待。
  • newScheduleThreadPool:可以创建定长的、支持定时任务,周期任务执行。
  • newSingleExecutor:创建一个单线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

ThreadPoolExecutor: 线程池最核心的一个类,我们来看它参数最完整的构造类,代码如下:

  public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

在这里插入图片描述

4.3 RejectedExecutionHandler

触发任务拒接的条件:当前同时运行的线程数量达到最大线程数maximumPoolSize,并且队列也放满了任务,即触发饱和拒绝策略。ThreadPoolExecutor中定义了四个拒绝策略内部类。

DiscardPolicy

当任务添加到线程池中被拒绝时,直接丢弃任务,不抛出异常

public static class DiscardPolicy implements RejectedExecutionHandler {
        public DiscardPolicy() { }
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        }
}

AbortPolicy

当任务添加到线程池中被拒绝时,直接丢弃任务,并抛出RejectedExecutionException异常

  public static class AbortPolicy implements RejectedExecutionHandler {

        public AbortPolicy() { }

        //不处理,直接抛出异常
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            throw new RejectedExecutionException("Task " + r.toString() +
                                                 " rejected from " +
                                                 e.toString());
        }
    }

DiscardOldestPolicy

当任务添加到线程池中被拒绝时,判断线程池是否还在运行,然后获取队列,让队首(最久)的元素出队,直接抛弃,把当前任务添加执行,不出意外还是添加到队列中,除非当前这会好几个线程执行完,线程数小于了corePoolSize。

public static class DiscardOldestPolicy implements RejectedExecutionHandler {

        public DiscardOldestPolicy() { }

        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            //线程池还没有销毁停止
            if (!e.isShutdown()) {
                //获取队列,并让队列头(最久)的任务出队,丢弃队头
                e.getQueue().poll();
                //执行新任务,新任务再添加到队列中
                e.execute(r);
            }
        }
    }

CallerRunsPolicy

当任务添加到线程池中被拒绝时,判断线程池是否还在运行,直接在主线程中运行此任务,即在调用execute或者submit的方法中执行,不再使用线程池来处理此任务。

public static class CallerRunsPolicy implements RejectedExecutionHandler {

        public CallerRunsPolicy() { }

        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            //线程池还在运行
            if (!e.isShutdown()) {
                //让主进程来运行此任务
                r.run();
            }
        }
    }

4.4 线程池工作队列

SynchronousQueue

没有容量,直接提交队列,是无缓存等待队列,当任务提交进来,它总是马上将任务提交给线程去执行,如果线程已经达到最大,则执行拒绝策略;所以使用SynchronousQueue阻塞队列一般要求maximumPoolSize为无界(无限大),避免线程拒绝执行操作。从源码中可以看到容量为0:

   //是否为空,直接返回的true
   public boolean isEmpty() {
        return true;
    }

    //队列大小为0
    public int size() {
        return 0;
    }

LinkedBlockingQueue

默认情况下,LinkedBlockingQueue是个无界的任务队列,默认值是Integer.MAX_VALUE,当然我们也可以指定队列的大小。从构造LinkedBlockingQueue源码中可以看出它的大小指定方式:

   //默认构造函数,大小为Integer最大
    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);
    }

为了避免队列过大造成机器负载,或者内存泄漏,我们在使用的时候建议手动传一个队列的大小。内部分别使用了takeLock和putLock对并发进行控制,添加和删除操作不是互斥操作,可以同时进行,这样大大提供了吞吐量。源码中有定义这两个锁:

   //获取元素使用的锁
   private final ReentrantLock takeLock = new ReentrantLock();

   //加入元素使用的锁
   private final ReentrantLock putLock = new ReentrantLock();

  //获取元素时使用到takeLock锁
  public E peek() {
        if (count.get() == 0)
            return null;
        final ReentrantLock takeLock = this.takeLock;
       //加锁操作
        takeLock.lock();
        try {
            //获取元素
            Node<E> first = head.next;
            if (first == null)
                return null;
            else
                return first.item;
        } finally {
            //解锁
            takeLock.unlock();
        }
    }
    
    //添加元素到队列中使用putLock锁
    public boolean offer(E e) {
        if (e == null) throw new NullPointerException();
        final AtomicInteger count = this.count;
        if (count.get() == capacity)
            return false;
        int c = -1;
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        //加锁操作
        putLock.lock();
        try {
            //队列中存放的数据小于队列设置的值
            if (count.get() < capacity) {
                //添加元素
                enqueue(node);
                c = count.getAndIncrement();
                if (c + 1 < capacity)
                    notFull.signal();
            }
        } finally {
            //解锁
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
        return c >= 0;
    }


ArrayBlockingQueue

可以理解为有界的队列,创建的时候必须要指定队列的大小,从源码可以看出构造的时候要传递值:

    public ArrayBlockingQueue(int capacity) {
        this(capacity, false);
    }

DelayQueue

是一个延迟队列,无界、队列中每个元素都有过期时间,当从队列获取元素时,只有过期的元素才会出队,而队列头部是最早过期的元素,若是没有过期,则进行等待。利用这个特性,我们可以用来处理定时任务调用的场景,例如订单过期未支付自动取消,设置一个在队列中过期的时间,过期了后,再去查询订单的状态,若是没支付,则调用取消订单的方法。

//获取元素
public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            for (;;) {
                //获取元素
                E first = q.peek();
                if (first == null)
                    //进入等待
                    available.await();
                else {
                    //获取过期时间
                    long delay = first.getDelay(NANOSECONDS);
                    if (delay <= 0)
                        //小于等于0则过期,返回此元素
                        return q.poll();
                    first = null; 
                    if (leader != null)
                        available.await();
                    else {
                        Thread thisThread = Thread.currentThread();
                        leader = thisThread;
                        try {
                            //设置还需要等待的时间
                            available.awaitNanos(delay);
                        } finally {
                            if (leader == thisThread)
                                leader = null;
                        }
                    }
                }
            }
        } finally {
            if (leader == null && q.peek() != null)
                available.signal();
            lock.unlock();
        }
    }


4.5 创建线程池方式

newFixedThreadPool: 创建一个定长的线程池,可控制最大并发数,超出的线程进行排队等待。源码如下:

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

从源码可以看出此线程池的核心线程数、最大线程数都是nThreads,线程空闲回收时间配置也没有意义了,所以闲置时间给0,队列使用LinkedBlockingQueue无界的方式,当线程数达到nThreads后,新任务放到队列中。

newSingleThreadExecutor: 创建一个单线程池,它只会用唯一的工作线程来执行任务,超出的线程进行排队等待。源码如下:

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

从源码可以看出此线程池的核心线程数、最大线程数都是1,线程空闲回收时间配置也没有意义了,所以闲置时间给0,队列使用LinkedBlockingQueue无界的方式,当线程数达到1后,新任务放到队列中。

newCachedThreadPool: 创建一个可缓存的线程池,如果线程池长度大于处理需要,则根据线程空闲时间大于60s的会进行销毁;新任务添加进来,若是没有空闲的线程复用,则会立马创建一个线程来处理,因为使用的是无缓存队列。源码如下:

    public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>(),
                                      threadFactory);
    }

从源码可以看出此线程池的核心线程数为0、最大线程数为无界Integer.MAX_VALUE,线程空闲回收时间60S,队列使用SynchronousQueue无缓存的方式,当有任务添加,能复用之前线程则复用,没有空闲线程则创建新线程。

newScheduledThreadPool:

创建支持定时、周期任务的线程池。源码如下:

   //Executors类中
   public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }

   //ScheduledThreadPoolExecutor类中
    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }


从源码可以看出此线程池的核心线程数为corePoolSize、最大线程数为无界Integer.MAX_VALUE,线程空闲回收时间0S,当线程数大于corePoolSize时,有线程处理完任务后,接下来就进行销毁。队列使用DelayedWorkQueue延迟队列,可以设置延时时间,当元素达到延时时间,才从队列出队。

五,原子性可见性有序性

JVM中的原子指令:

  • lock(锁定):作用于主内存的变量,把一个变量标记为一条线程独占状态
  • unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
  • read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
  • use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量
  • store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作
  • write(写入):作用于工作内存的变量,它把store操作从工作内存中的一个变量的值传送到主内存的变量中

Java多线程的场景会出现三个问题:可见性、原子性、有序性。

5.1 原子性

即一个操作或者多个操作,要么全部执行,并且执行的过程不会被任何因素打断,要么就都不执行。

举例说明:

x = 10; 	//语句1
y = x; 		//语句2
x++; 		//语句3
x = x + 1; 	//语句4

注意: 其实只有语句1是原子性操作,其他三个语句都不是原子性操作。

语句1是直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中。
语句2实际上包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及将x的值写入工作内存,这2个操作都是原子性操作,但是合起来就不是原子性操作了。
同样的,x++和 x = x+1包括3个操作:读取x的值,进行加1操作,写入新的值

也就是说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。

从上面可以看出,Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。

由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。

5.2 可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

//线程1执行的代码
int i = 0;
i = 10;
//线程2执行的代码
j = i;

当线程1执行 i =10这句时,会先把i的初始值加载到工作内存中,然后赋值为10,那么在线程1的工作内存当中i的值变为10了,却没有立即写入到主存当中。
此时线程2执行 j = i,它会先去主存读取i的值并加载到线程2的工作内存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10。这就是可见性问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值。

对于可见性,Java提供了volatile关键字来保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

5.3 有序性

有序性就是程序执行的顺序按照代码的先后顺序执行。

int i = 0;
boolean flag = false;
i = 1; //语句1
flag = true; //语句2

上面代码定义了一个int型变量,定义了一个boolean类型变量,然后分别对两个变量进行赋值操作。

从代码顺序上看,语句1是在语句2前面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗?不一定,为什么呢?这里可能会发生指令重排序(Instruction Reorder)。

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

比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。

但是要注意,虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,那么它靠什么保证的呢?再看下面一个例子:

int a = 10; //语句1
int r = 2; //语句2
a = a + 3; //语句3
r = a*a; //语句4

上边的代码可能执行的顺序是:2->1->3->4 。不可能的顺序:2->1->4->3,处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行。

六,线程优化

Android平台上的Java线程,就是Android虚拟机线程,而虚拟机线程由是通过系统调用而创建的Linux线程。在我们创建线程并执行start方法时,最终执行nativeCreate()这个方法。

过多的线程使用会带来内存上的消耗同时也降低了cpu调度的效率。通常我们创建线程的方式可以使用线程池,从而控制线程的使用量。但是如果项目中存在第三方的引用,那么我们就没法控制了。因此对于线程的控制尤为重要。

6.1 线程监控

对于Java层可以通过asm插桩的方式;对于native层可以通过PIL Hook,目前行业上比较出名的就是xhook,和bhook了。

ASM插桩的方式

对线程的监控,首先我们要统计当前的信息对不对,可以直接通过

Thread.getAllStackTraces()

这个方法返回的是个Map集合包含了线程信息,如下数据:

{Thread[Jit thread pool worker thread 0,5,main]=[Ljava.lang.StackTraceElement;@de6305c, Thread[RenderThread,7,main]=[Ljava.lang.StackTraceElement;@e6cf65, Thread[Thread-6,5,]=[Ljava.lang.StackTraceElement;@295443a, Thread[FinalizerWatchdogDaemon,5,system]=[Ljava.lang.StackTraceElement;@333e8eb, Thread[main,5,main]=[Ljava.lang.StackTraceElement;@ed3f648, Thread[Binder:6528_2,5,main]=[Ljava.lang.StackTraceElement;@8f9fce1, Thread[queued-work-looper-schedule-handler,5,main]=[Ljava.lang.StackTraceElement;@4836a06, Thread[Thread-5,5,]=[Ljava.lang.StackTraceElement;@295443a, Thread[ReferenceQueueDaemon,5,system]=[Ljava.lang.StackTraceElement;@ce628c7, Thread[FinalizerDaemon,5,system]=[Ljava.lang.StackTraceElement;@f562ef4, Thread[flutter-worker-0,5,main]=[Ljava.lang.StackTraceElement;@eb8861d, Thread[Binder:6528_3,5,main]=[Ljava.lang.StackTraceElement;@4dc2092, Thread[Binder:6528_4,5,main]=[Ljava.lang.StackTraceElement;@6928a63, Thread[Signal Catcher,5,system]=[Ljava.lang.StackTraceElement;@e214660, Thread[Profile Saver,5,system]=[Ljava.lang.StackTraceElement;@5dc6719, Thread[AsyncTask #1,5,main]=[Ljava.lang.StackTraceElement;@c97b3de, Thread[flutter-worker-1,5,main]=[Ljava.lang.StackTraceElement;@11969bf, Thread[Binder:6528_1,5,main]=[Ljava.lang.StackTraceElement;@ec4688c, Thread[queued-work-looper,5,main]=[Ljava.lang.StackTraceElement;@9c35bd5, Thread[HeapTaskDaemon,5,system]=[Ljava.lang.StackTraceElement;@295443a}

可以拿到了当前所有的线程信息,如果不给它指定的名字的话,默认就会出现类似这种情,比如Thread-1。这种名称对于我们来说没有意义,不知道在哪里用到。

因此可以用asm对调用thread进行插桩,通过改变指令调用函数,把普通的空参数Thread()方法变成带有name的构造方法Thread(String)进行hook处理,把调用者名称的信息放到前置的ldc指令,从而到达一个转化的效果。
在这里插入图片描述ASM其实就是一个可以编译字节码的工具,分为core api和 tree api。

具体流程可以看这两篇文章:core apitree api

native层可以通过PIL Hook,具体可以看这篇文章PIL Hook

6.2 线程数过多的问题

在ART虚拟机中,每创建一个线程都需要为其分配独立的Java栈空间。当Java层未显示设置栈空间大小时,native层会在FixStackSize函数会分配默认的栈空间大小。

每个线程至少会占用1M的虚拟内存大小(实际大小根据厂商定义不同),而在32位系统上,由于每个进程可分配的用户用户空间虚拟内存大小只有3G,如果一个应用的线程数过多,而当进程虚拟内存空间不足时,创建线程的动作就可能导致OOM问题。

6.3 优化方式

  • 在应用中使用统一的线程池。

  • 将应用中的野线程及野线程池进行收敛。

总结

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值