JAVA篇:Java的线程

1、JVM线程模型

1.1 线程间变量共享

通过分析JVM的内存模型可知,JVM的内存模型包括虚拟机栈、本地方法栈、程序计数器、方法区和堆,其中虚拟机栈、本地方法栈和程序计数器是线程私有的,而方法区和堆是线程共有的。

多线程的根本问题在于:线程间变量共享

局部变量位于栈之中,并不涉及线程间变量共享问题,而类的静态变量保存在方法区,类所创建的实例及其包含的成员变量保存在堆之中,类变量和实例变量这两类变量则需考虑线程共享的安全问题。

在JVM内存模型中,程序(进程)拥有一块内存空间,可以被所有的线程共享,即MainMemory(主内存:堆),而每个线程又有一块独立的内存空间,即WorkingMemory(工作内存:栈)。通常情况下,当线程需要对某一共享变量进行修改时,通常会进行如下的过程:

  • 从主内存中拷贝变量的一份副本,并装载到工作内存中;

  • 在工作内存中执行代码,修改副本的值;

  • 用工作内存中的副本值更新主存中的相关变量值。

所谓的“线程安全”,即多个线程同时执行同一段代码时,不会出现不确定的或者与单线程条件下不一致的结果。通常,下列三种条件居其一的并发访问被JVM认为是线程安全的:

  1. 有final关键字修饰且已被赋值。

  2. 有volatile关键字修饰。

  3. 有锁保护(synchronized、ReentrantLock等)

1.2 JVM同步的实现

同步(synchronized)就是:在多个线程并发访问共享数据的时候,保证共享数据在同一时刻只被一个线程使用,有一种很简单的实现思想,那就是在共享数据里维护一个

  1. 在共享数据里保存一个锁。

  2. 在锁里保存这个线程的标识。

  3. 其他线程访问已加锁共享数据要等待锁释放。

JVM中的三种锁都是以上述思想为基础的,只是这三种锁实现的“重量级”不同。

  1. 偏向锁

  2. 轻量级锁

  3. 重量级锁

其中重量级锁是最初的锁机制,偏向锁和轻量级锁是在jdk1.6加入的,可以选择打开或者关闭。如果把偏向锁和轻量级锁都打开,那么在java代码中使用synchronized关键字的时候,jvm底层会先尝试先使用偏向锁,如果偏向锁不可用,则转换为轻量级锁,如果轻量级锁不可用,则转换为重量级锁。

三种锁的区别在哪?偏向锁和轻量锁可以看作是重量级锁在特定情况下的一种优化。

重量级锁每次访问共享变量,都会在存储的对象的对象头(MarkWord头)中保存指向互斥量的指针,在访问结束之后在释放锁,即使可能这个共享变量并未被其他的线程访问,而且使用互斥量会带来较大的性能消耗。

轻量级锁适用于线程串行获得锁的情况,无法处理并发的情况,一旦出现并发问题则会膨胀为重量级锁。轻量级锁依赖了一种叫做CAS(compare and swap)的操作,每次访问共享对象时都会使用CAS尝试获得锁,若是已有线程持有锁,线程则会不停重试CAS操作寄希望于锁的持有线程主动释放锁,在超过一定次数后还是没有成功获得锁,那么轻量级锁则会膨胀为重量级锁,除了持有锁的线程,其他尝试获得锁的线程进入等待状态。

偏向锁只能用于从始至终只有一个线程获得锁的情况,一旦出现另外一个线程尝试获得锁,则会进行revoke操作。revoke操作所作的就是查看原持有锁是否还活动并持有着锁(依旧访问着共享数据),若是不活动或者活动但不再持有锁,则偏向锁重置为无锁状态,新线程访问数据,反之,则新线程完成将偏向锁更改为轻量锁的操作。

1.3 JVM定义的Thread的类继承结构

1.3.1 JVM定义的Thread的类继承结构

JVM主要是C++实现的,JVM定义的Thread的类继承结构如下,其中的OSThread并不在继承关系,而是以组合的方式被Thread所使用。

 

 

 

这些类构成了JVM的线程模型,其中最主要的是下面几个类:

  • java.lang.Thread:这个是java语言里的线程类,由这个java类创建的instance都会1:1映射到一个操作系统的osthread。

  • JavaThread:JVM中C++定义的类,一个JavaThread的instance代表了在JVM中的java.lang.Thread的instance,它维护了线程的状态,并且维护了一个指针指向java.lang.Thread创建的对象(oop)。它同时还维护了一个指针指向对应的OSThread,来获取底层操作系统创建的osthread的状态。

  • OSThread:JVM中C++定义的类,代表了JVM对底层操作系统的osthread的抽象,它维护着实际操作系统创建的线程句柄handle,可以获取底层osthread的状态。

  • VMThread:JVM中C++定义的类,这个类和用户创建的线程无关,是JVM本身用来进行虚拟机操作的线程,比如GC。

1.3.2 用户在JVM中创建线程

有两种方式可以让用户在JVM中创建线程:

  • new java.lang.Thread().start()

  • 使用JNI将一个native thread attach到JVM中

针对new java.lang.Thread.start()这种方式,只有调用start()方法的时候,才会真正的在JVM中去创建线程,主要的生命周期步骤有:

  1. 创建对应的JavaThread的实例

  2. 创建对应的OSThread的实例

  3. 创建实际的底层操作系统的native thread

  4. 准备相应的JVM资源,如ThreadLocal存储空间分配等

  5. 底层native thread开始运行,调用java.lang.Thread生成的Object的run()方法

  6. java.lang.Thread生成的Object的run()方法执行完毕返回后,或者抛出异常终止后,终止native thread

  7. 释放JVM相关的thread的资源,清除对应的JavaThread和OSThread

针对JNI将一个native thread attach到JVM中,主要步骤有:

  1. 通过JNI call AttachCurrentThread申请连接到执行的JVM实例

  2. JVM创建相应的JavaThread和OSThread对象

  3. 创建相应的java.lang.Thread的对象

  4. 一旦java.lang.Thread的对象闯创建之后,JNI就可以调用java代码了

  5. 当通过JNI call DetachCurrentThread之后,JNI就从JVM实例中断开连接

  6. JVM清除相应的JavaThread,OSThread,java.lang.Thread对象

1.3.3 JVM中线程状态

从JVM的角度来看待线程状态有以下几种:

 

 

 

其中的主要状态有以下5种:

  • _thread_new:新创建的线程

  • _thread_in_Java:在运行java代码

  • _thread_in_vm:在运行JVM本身的代码

  • _thread_in_native:在运行native代码

  • _thread_blocked:线程被阻塞了,包括等待一个锁,等待一个条件,sleep,执行一个阻塞的IO等

从OSThread的角度,JVM还定义了一些线程状态给外部使用,比如用jstack输出的线程堆栈信息种线程的状态:

 

 

 

比较常见的有:

  • Runnable:可以运行或者正在运行的

  • MONITOR_WAIT:等待锁

  • OBJECT_WAIT:执行了Object.wait()之后在条件队列种等待的

  • SLEEPING:执行了Thread.sleep()的

从JavaThread的角度,JVM定义了一些针对JavaThread对象的状态,基本类似。多了一个TIMED_WAITING的状态,用来表示定时阻塞的状态。

 

 

 

最后来看一下JVM内部的VMThread,主要有几类:

  • VMThread:执行JVM本身的操作

  • Periodic task thread:JVM内部执行定时任务的线程

  • GC threads:GC相关的线程,比如单线程/多线程的GC收集器使用的线程

  • Compiler threads:JIT用啦动态编译的线程

  • Signal dispatcher thread:Java解释器Interceptor用来辅助safepoint操作的线程。

2、java多线程编程

2.1 Runnable接口

Runnable接口仅仅定义了虚方法run(),其具体实现在java.lang.Thread之中。当我们使用实现Runnable接口重写run()方法的方式构建线程类,必须依赖Thread的start()方法来启动线程实例。

package java.lang;

@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

2.2 Thread类

Thread类的主要方法如下:

方法描述
1 public void start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
2 public void run() 如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。
3 public final void setName(String name) 改变线程名称,使之与参数 name 相同。
4 public final void setPriority(int priority) 更改线程的优先级。
5 public final void setDaemon(boolean on) 将该线程标记为守护线程或用户线程。
6 public final void join(long millisec) 等待该线程终止的时间最长为 millis 毫秒。
7 public void interrupt() 中断线程。
8 public final boolean isAlive() 测试线程是否处于活动状态。

测试线程是否处于活动状态。 上述方法是被Thread对象调用的。下面的方法是Thread类的静态方法。

序号方法描述
1 public static void yield() 暂停当前正在执行的线程对象,并执行其他线程。
2 public static void sleep(long millisec) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
3 public static boolean holdsLock(Object x) 当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。
4 public static Thread currentThread() 返回对当前正在执行的线程对象的引用。
5 public static void dumpStack() 将当前线程的堆栈跟踪打印至标准错误流。

Java种操作线程都是通过Thread的API来实现,其中核心逻辑都封装在本地方法中,其实现在jdk\src\share\native\java\lang\Thread.c中。但是我并没有找到源码,这个应该时属于hotspot源码的一部分……暂时不想深入native方法源码,具体可参考Hotspot Thread本地方法实现 源码解析

仅仅关注一下 start方法。

start方法用于创建关联的底层操作系统的本地线程并启动该线程执行run方法,在其源码中,其依赖native方法start0来进行。

    public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

    private native void start0();

本地方法start0是C++实现的,它会创建一个C++的JavaThread实例和OSThread实例,前者是线程在JVM中的表示,后者表示底层操作系统的原生线程,这三个都是一对一的关系,Thread实例和对应的JavaThread,JavaThread和对应的OSThread都保存了彼此的引用。创建并初始化完成后,会设置OSThread的优先级,栈帧大小等属性,然后启动OSThread执行JavaThread的run方法,该方法会执行Thread实例的run方法。如果重复调用start方法或者因为内存不足导致OSThread创建失败都会抛出异常。

OSThread在调用os::create_thread时会传入java_start函数,该函数会负责初始化OSThread,初始化完成将状态设置为INITIALIZED并唤醒startThread_lock上等待的负责创建OSThread的线程,然后继续等待直到OSThrea的状态不再是INITIALIEZED。这时创建OSThread的线程发现OSThread已经初始化完成就会退出OSThread的创建方法,然后进入启动OSThread执行的方法,该方法将状态设置为RUNNABLE,并唤醒startThread_lock等待的线程,这时创建的子线程就会开始正常执行,调用JavaThread的run方法。

2.3 synchronized 关键字

synchronized除了保障原子性外,其实也保障了可见性。因为synchronized无论是同步的方法还是同步的代码块,都会先把主内存的数据拷贝到工作内存中,同步代码块结束,会把工作内存中的数据更新到主内存中,这样主内存中的数据一定是最新的。更重要的是禁用了乱序重组以及保证了值对存储器的写入,这样就可以保证可见性。

synchronized同步的缺点

  1. synchronized关键字同步时,等待的线程将无法控制,只能死等。

  2. synchronized关键字同步时,不保证公平性,因此会有线程插队的现象。

synchronized有两种用法:同步方法和同步代码块,锁定的对象可能是:类、实例和临界资源。

    Object object = new Object();
    //同步方法:修饰静态方法,类锁
    public static synchronized void get1(){
        //do somethings
    }
    //同步方法:修饰非静态方法,对象锁
    public synchronized void get2(){
        //do somethings
    }
    public void get(){
        //同步代码块:锁定当前对象
        synchronized(this) {
            //do somethings
        }
        //同步代码块:锁定临界对象
        synchronized (object){
            //do somethings
        }
        //同步代码块:锁定类
        synchronized (this.getClass()){
            //do somethings
        }

    }

synchronized可以修饰静态方法或者非静态方法,区别就是给类加锁还是给实例加锁。而synchroniead修饰可以指定锁定的是类还是对象,或者是锁定需要保证同步的临界对象。实例锁和静态锁的区别是,实例锁只限定了同一实例的该方法(代码区)同一时间仅能被一个线程访问,而类锁则限定该类下所有实例访问限定的方法时同一时间仅有一个线程能进行访问。

20211008补充:

  1. synchronized锁是可重入锁,这个可重入特性也可由其子类继承。

  2. 线程发生异常,会自动释放synchronized锁定资源

2.4 volatie 关键字

synchronized关键字是使用互斥锁来防止多个线程执行同一段代码,那么就会很影响程序的执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是它无法替代synchronized,因为volatile关键字无法保证操作的原子性,需要配合synchronized使用。

volatile修饰的共享变量(类的成员变量、静态变量)具备以下两层语义:

  • 保证不同线程对这个变量进行操作时的可见性,即一个线程修改了该共享变量(实例变量、类变量),这新值对其他线程来说是立即可见的。

  • 禁止进行指令重排序,即1)当程序执行到volatile变量的读操作或者写操作的时候,其前面的操作肯定全部已经进行,且结果对后面的操作可见;其后面的操作肯定还没有进行。2)在进行指令优化时,不能将对volatile变量访问指令前面的语句放在其后面执行,也不能把volatile变量访问指令后面的语句放在其前面执行。

volatile关键字的原理:“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”(《深入理解Java虚拟机》)

lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

  1. 它确保指令重排时不会把其后面的指令排到内存屏障前面的位置,也不会把前面的指令排到内存屏障的后面。

  2. 它会强制将对缓存的修改操作立即写入主存

  3. 写操作时也导致其他cpu中对象的缓存行无效,必须立即重新从内存读取数据

public volatile boolean inited = false;

//线程1:
context= loadContext();
inited = true;

//线程2:

while(!inited ){
  sleep()
}
doSomethingwithconfig(context);

2.5 应用

package lwx.coding;

public class MyThread {
    static int all_ticket = 10;
    class MyThread1 implements Runnable{
        int ticket = 10;
        @Override
        public void run(){
            while (ticket>0){
                System.out.println(Thread.currentThread().getName()+"尝试卖票::"+(this.ticket));
                synchronized (this){
                    if(this.ticket>0){
                        try{
                            Thread.sleep(300);
                            System.out.println(Thread.currentThread().getName()+"卖票---->"+(this.ticket--));
                        }catch (InterruptedException e){
                            e.printStackTrace();
                        }
                    }
                }
            }

        }
    }

    class Mythread2 extends Thread{
        int ticket = 10;
        @Override
        public void run(){
            while (ticket>0){
                System.out.println(Thread.currentThread().getName()+"尝试卖票::"+(this.ticket));
                synchronized (this){
                    if(this.ticket>0){
                        try{
                            Thread.sleep(300);
                            System.out.println(Thread.currentThread().getName()+"卖票---->"+(this.ticket--));
                        }catch (InterruptedException e){
                            e.printStackTrace();
                        }
                    }
                }
            }

        }

    }

    class Mythread3 extends Thread{
        //int ticket = 10;
        @Override
        public void run(){
            while (all_ticket>0){
                System.out.println(Thread.currentThread().getName()+"尝试卖票::"+(all_ticket));
                synchronized (this){
                    if(all_ticket>0){
                        try{
                            Thread.sleep(300);
                            System.out.println(Thread.currentThread().getName()+"卖票---->"+(all_ticket--));
                        }catch (InterruptedException e){
                            e.printStackTrace();
                        }
                    }
                }
            }

        }

    }

    public void test1(){
        MyThread1 myThread1 = new MyThread1();
        new Thread(myThread1,"Runable线程1").start();
        new Thread(myThread1,"Runable线程2").start();
    }

    public void test2(){
        Mythread2 myThread2 = new Mythread2();
        new Thread(myThread2,"Thread线程1").start();
        new Thread(myThread2,"Thread线程2").start();
    }
    public void test3(){
        /*这里还是会出现冲突,譬如两个线程同卖票4,是因为是两个实例在工作,同步代码块锁定的是当前对象*/
        Mythread3 MT3_1 = new Mythread3();
        Mythread3 MT3_2 = new Mythread3();
        new Thread(MT3_1,"MT3_1").start();
        new Thread(MT3_2,"MT3_2").start();
    }


    public static void main(String[] arg2){
        MyThread myThread = new MyThread();
        //myThread.test1();
        //myThread.test2();
        myThread.test3();


    }
}

参考

聊聊JVM(五)从JVM角度理解线程

从jvm的角度来看java的多线程

JVM线程模型详解

Hotspot Thread本地方法实现 源码解析

HotSpot Runtime Overview

Java 多线程编程

Java并发编程:volatile关键字解析

Java 并发编程之 Synchronized 关键字最全讲解

Java锁Synchronized对象锁和类锁区别

Java并发编程:volatile关键字解析

 

0、JAVA多线程编程

Java多线程编程所涉及的知识点包含线程创建、线程同步、线程间通信、线程死锁、线程控制(挂起、停止和恢复)。之前 JAVA篇:Java的线程仅仅了解了部分线程创建和同步相关的小部分知识点,但是其实在编程过程中遇到的事情并不仅仅限于此,所以进行整理,列表如下:

posted @ 2021-05-23 15:52  l.w.x  阅读(572)  评论(0编辑  收藏  举报