首页 > 技术文章 > 线程与进程、线程的六种状态之间的转换、线程安全的五个维度

optimus7 2020-08-10 14:50 原文

1. 线程与进程

  1.1 进程与线程的区别

    线程比进程更轻量级。

    有进程的时候,一个进程(的资源)可以被多个线程共享。

    没有进程的时候,线程也可以被CPU独立调度。

  1.2 多进程场景下的解决方案(信号量方案)

    在多道程序系统中存在许多进程,它们共享各种资源,然而有很多资源一次只能供一个进程使用,这便是临界资源。

    多进程中的临界资源大致上可以分为两类,

    一类是物理上的真实资源,如打印机;

    一类是硬盘或内存中的共享数据,如共享内存等。而进程内互斥访问临界资源的代码被称为临界区。

 

    针对临界资源的互斥访问,JVM层面的锁就已经失去效力了。

    在多进程的情况下,主要还是利用操作系统层面的进程间通信原理来解决临界资源的抢占问题。比较常见的一种方法便是使用信号量(Semaphores)。

 

    信号量在POSIX标准下有两种,分别为有名信号量和无名信号量。

    无名信号量通常保存在共享内存中,而有名信号量是与一个特定的文件名称相关联。

    信号量是一个整数变量(类比为停车场的空余车位),有计数信号量和二值信号量两种。对信号量的操作,主要是P操作(进入停车场)和V操作(出停车场)。

 

    P操作:先检查信号量的大小,若值大于零,则将信号量减1,同时进程获得共享资源的访问权限,继续执行;若小于或者等于零,则该进程被阻塞后,进入等待队列。

    V操作:该操作将信号量的值加1,如果有进程阻塞着等待该信号量,那么其中一个进程将被唤醒。

    举个例子,设信号量为1,当一个进程A在进入临界区之前,先进行P操作。

    发现值大于零,那么就将信号量减为0,进入临界区执行。

    此时,若另一个进程B也要进去临界区,进行P操作,发现信号量等于0,则会被阻塞。

    当进程A退出临界区时,会进行V操作,将信号量的值加1,并唤醒阻塞的进程B。此时B就可以进入临界区了。

 

    这种方式,其实和多线程环境下的加解锁非常类似。因此用信号量处理临界资源抢占,也可以简单地理解为对临界区进行加锁。

   1.3 程序使用线程操作CPU的三种实现 以及 JVM如何实现:

    1. 程序(exe)直接调用Windows内核线程的本地方法做事,直接作用于CPU。

      (虽然原理如此但是程序无法直接操作Windows内核线程,程序实际调用->Windows内核线程暴露的接口:轻量级进程,轻量级进程与Windows内核线程数量1:1)

 ​      缺点:运行程序时CPU处于用户态,而调用轻量级进程时CPU必须处于核心态,所以程序运行期间将会不断切换用户态<—>核心态。

    2. 程序(exe)只使用用户线程,而不使用Windows内核线程做事。

   ​    缺点:用户线程是程序内部创建的线程,对Windows内核不可见,所以无法使用Windows内核本地方法操作CPU。那程序怎么使用CPU呢:程序中重写操作系统内核的本地方法,这会非常麻烦。

    3. 程序——>用户线程——>调用轻量级进程——>调用Windows内核线程——>CPU,

   ​    优点:用户线程执行用户态的事情,调用轻量级进程执行内核态的事情,避免频繁切换。

    4. JVM的实现方式:

     java.exe和javac.exe等——>Thread.start启动用户线程——>Thread类中的native方法(不同OS不同JDK不同native方法)——>调用轻量级进程——>调用OS内核线程——>CPU。

  1.4 线程的调度

    1. 协同式调度:很古老,是等待一个线程干完活才把CPU给下一个,容易死机。

    2. 抢占式调度:被普遍使用。系统通过综合各种原因完成调度,我们也可以给出建议,即线程优先级,但是操作系统(Windows)在执行线程时是可以改变优先级的,所以优先级仅仅是个建议。

 

2. 线程的六种状态之间的关系

  

 

  2.0. 写在前面:

    Java的Thread类中记录着线程的六种状态:(Runnable状态包括了Runnable和Running)

    左:new

    中:Runnnable

    右:terminated

    上:blocked

    左下:waiting

    右下:timed_waiting

    Sleep和Yield不会释放锁。

    Wait会释放锁。

  2.1 左路:New—>Runnable(Runnable包括了Runnable和Running)

    start方法:Thread.start()

  2.2 中路:Runnable<—>Running

    Runnable—>Running:CPU分配时间片。

    Running—>Runnable:时间片用完 (或者叫线程上下文切换) 或者 本线程内调用Thread.yield (相当于时间片用完),

        则会重新竞争CPU。(不会释放锁,ps.锁指的是monitorexit)

  2.3 上路:Running—>Blocked—>Runnable

    Running—>Blocked:

      即线程执行synchronized块&方法时,执行monitorenter指令时,

      发现栈顶元素的monitor对象已经被别的线程占用(monitor的_count字段不为0),则本线程进入Blocked状态。

      等待monitor的_count字段为0时获取锁成功,继续执行synchronized块&方法。

    Blocked—>Runnable:等待monitor的_count字段为0时且获取锁成功时,从Blocked进入Runnable,

      等待CPU分配时间片后,进入Running状态,继续执行synchronized块&方法。

  2.4 左下路开始等待:Running—>Waiting

    途径1. Object.wait():本线程中执行Object.wait()

      (wait 使用前提是必须在synchronized中使用,且Object对象就是被锁对象,因为wait的实现必须要先获取被锁对象的monitor)

      (会释放锁,ps.锁指的是monitorexit)

    途径2. t2.join():本线程t1中调用了另一个线程t2.join()方法,本线程t1被等待

      (可能会释放锁,ps.锁指的是monitorexit:释放this的锁,前提是this已被锁)

    途径3. LockSupport.park():本线程中执行LockSupport.park()

      (不会释放锁,ps.锁指的是monitorexit。注意不要把park和monitorenter混淆)

  2.5 左下路解放等待:Waiting—>Runnable

    途径1. Object.notify / notifyAll():

      在另一个线程执行Object.notify / notifyAll(),使Object.wait()那个线程重新monitorenter开始竞争锁,当竞争成功后,恢复为Runnable。

      然后等CPU分配时间片,从wait之后开始执行。

      (notify 使用前提是必须在synchronized中使用,且Object对象就是被锁对象,因为 notify 的实现必须要先获取被锁对象的monitor)

    途径2. t2.join():

      等t2执行完,t1恢复到Runnable。

      途径3. LockSupport.unpark(线程):

      在另一个线程中执行LockSupport.unpark(被park的那个线程)

  2.6 右下路开始等待:Running—>Timed_Waiting

    途径1. Thread.sleep(时间):

      在本线程内执行Thread.sleep(时间)。(不会释放锁,ps.锁指的是monitorexit)

    途径2. t2.join(时间):另一个线程t2在本线程t1中,并执行t2.join(时间),所以本线程t1被等待(时间)

      (可能会释放锁,ps.锁指的是monitorexit:会释放this的锁,前提是this已被锁)

    途径3. Object.wait(时间):本线程中执行Object.wait(时间)

      (wait 使用前提是必须在synchronized中使用,且Object对象就是被锁对象,因为wait的实现必须要先获取被锁对象的monitor)

      (会释放锁,ps.锁指的是monitorexit)

    途径4. LockSupport.parkNanos(时间):本线程中执行LockSupport.parkNanos(时间)

      (不会释放锁,ps.锁指的是monitorexit。注意不要把park和monitorenter混淆)

    途径5. LockSupport.parkUtil(时间):本线程中执行LockSupport.parkUtil(时间)

      (不会释放锁,ps.锁指的是monitorexit。注意不要把park和monitorenter混淆)

  2.7 右下路解放等待:Timed_Waiting—>Runnable

    途径1. Thread.sleep(时间)+时间到期。

    途径2. t2.join(时间)+时间到期。

    途径3. Object.wait(时间)+时间到期,使Object.wait()那个线程重新monitorenter开始竞争锁,当竞争成功后,恢复为Runnable。

      然后等CPU分配时间片,从wait之后开始执行。

    途径4. LockSupport.parkNanos(时间)+时间到期。

    途径5. LockSupport.parkUtil(时间)+时间到期。

  2.8 右路:Running—>Terminated

    run方法执行完。

3. 线程安全的五个维度

  3.1 不可变:

    final变量(基本类型Final是常量,值和引用都不可变。引用类型Final的值可变+引用不可变)

  3.2 绝对安全:

    不存在。

    因为即使Vector单独使用(remove方法、get方法、size方法等方法都是synchronized)时可以保证使用方法期间是不可以另一个线程操作该vector对象的,

    但是组合使用无法保证原子性(场景:Vector.size()遍历集合+然后删除),意思是size方法之后+remove方法之前可能会被别的线程插进来。

     Thread removedThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for(int i=0;i<vector.size();i++){
                        vector.remove(i);
                    }
                }
            }); 

   3.3 相对安全:

     普遍存在。Vector+Synchronized修饰。   

      Thread removedThread=new Thread(new Runnable() {
                @Override
                public void run() {
                    /**
                     * 使用synchronized锁机制将操作对象进行锁定,
                     * 当执行size方法之后+remove方法之前,其他线程不可以访问这行vector
                     */
                    synchronized (vector) { 
                        for (int i = 0; i < vector.size(); i++) {
                            vector.remove(i);
                        }
                    }
                }
            });

 

  3.4 线程兼容:

    普遍存在。ArrayList+Synchronized修饰。

  3.5 线程对立:

    两个线程的操作互斥,放在多线程并发环境会出现死锁。列举死锁的例子:比如Thread.suspend()和Thread.resume()。

4. FAQ

  4.1 parkNanos

    parkNanos的逻辑是,等待nano时间就会被释放,释放的概念也就是unpark(线程),也就是继续执行线程。

    parkNanos在ReentrantLock(lock).Trylock(long timeout, TimeUnit unit)里面使用:

       timeout是指定的最大等待响应时间,包括了争取时间和park时间,deadline时间如果还没获取到资源就返回false退出队列啥也不干了。

    parkNanos的用处:

       最多parkNanos剩的时间那么久,时间到了就unpark这个线程,即这个节点继续死循环,再做最后一次争取锁,如果再失败就会发现剩的时间小于0,然后就执行退出队列操作。

  4.2 Interrupt()

    Thread有三个public的Interrupt方法:

      public void interrupt() : 非静态方法,被某个Thread实例所调用,作用是把该线程interrupt,但是只是在当前线程中打了一个中止标志,并不是真的停止线程。

      public boolean isInterrupted() : 非静态方法,被某个Thread实例所调用,作用是返回该线程是否有中止标志。

      public static boolean interrupted(): 静态方法,被Thread类或者某个Thread类或者某个Thread实例调用(谁调用都没关系),

                      作用是返回当前线程(即正在执行这个方法的线程)是否有中止标志,并清除中止标志。

    Interrupt的常见用法:

      如果线程正在执行Object.wait、t2.join、Thread.sleep、Lock.lockInterruptibly时会检查该线程是否已经中止标志,

      比如,线程先执行interrupt,再Thread.sleep,这时Thread.sleep会产生InterruptedException异常,会被catch到。抛出InterruptedException异常后,线程即恢复到未interrupt状态。

  4.3 面试题:如何停止一个正在运行的线程? 

    1. 利用try&catch&sleep方法+Interrupt中止标志抛异常

      方法1:本线程在Sleep期间,别人的线程把我t.interrupt()了,则我会抛出异常,所以Sleep所在的try块被中止执行了,如果所有逻辑写在tryt块中,可实现终止线程的作用。    

      方法2:本线程在Sleep之前,本线程就已经t.interrupt()了,则我只要Sleep就会抛出异常,所以Sleep所在的try块被中止执行了,如果所有逻辑写在tryt块中,可实现终止线程的作用。

      ps. 虽然Sleep所在的try块被中止执行了,但是try catch整体之后的逻辑还是照常执行。如果try catch整体之后还有逻辑,则不会实现终止线程的作用。

      ps. 意外发现小知识,主线程中执行t.interrupt()之后,t线程的确生成了中止标志,但是如果这是主线程sleep一会儿,t线程的中止标志就会被清除,而且再次t.interrupt()也不会再生成中止标志。

    2. 利用Interrupt中止标志+检查标志位+return

      方法1:本线程的逻辑途中写几处检查点Thread.interrupted(),如果外面把我t.interrupt()了则此处检查点将返回true。如果为true的话则执行return,或者,如果为false的话则执行return。

    3. 暴力中止

      方法1:本线程内执行currentThread().stop();

      方法2:别的线程内执行t.stop();

      ps.该方法本废弃不建议使用。

 

 

 

推荐阅读