道阻且长,行则将至,走慢一点没关系,不停下|

Ac_c0mpany丶

园龄:3年7个月粉丝:6关注:3

《实战Java高并发程序》——第2章 Java并行程序基础

进程和线程

简单的话来说,在Windows中看到的后缀为.exe的文件都是程序。不过程序是"死"的、静态的。当双击这个.exe文件的时候,这个.exe文件中的执行就会被加载,你就能得到一个有关这个程序的进程。进程是"活"的,或者说是正在被执行的
进程中可以容纳若干线程
那进程和线程之间究竟是一种什么关系呢?简单地说,进程是一个容器。比如一件漂亮的小别墅,别墅里有卧室、厨房、书房、洗手间等,当然还有一家三口住在里面。这时一家三口在家里爱去哪里就去哪里、爱干什么就干什么(进程中有三个活动线程),小别墅就像一个进程,家里的厨房、书房就像这个进程占有的资源。

  • 进程
    • 当一个程序被运行,从磁盘加载这个程序的代码到内存,这时就开启了一个进程
    • 进程可以视作程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图),也有的程序只能启动一个实例进程(例如网易云音乐)
  • 线程
    • 一个进程之内可以分为一到多个线程
    • 一个线程就是一个指令流,将指令流中的一条条指令按一定的顺序交给CPU执行
  • 二者对比
    • 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
    • 进程拥有共享的资源,如内存空间等,供内部的线程共享
    • 进程间的通信比较复杂
      • 同一台计算机的进程通信称为IPC(Inter-process communication)
      • 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,如HHTP
    • 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
    • 线程更轻量,线程上下文切换成本一般要比进行上下文切换低

线程是轻量级进程,是程序执行的最小单位。使用多线程而不是多进程去进行并发程序的设计,是因为线程的切换和调度的成本远远小于进程。

线程的6种状态


Java的线程有6种状态:初始(NEW)、运行(RUNNABLE)、阻塞(BLOCKED)、等待(WAITING)、超时等待(TIMED_WAITING)、终止(TERMINATED)
NEW状态表示刚刚创建的线程,这种线程还没开始执行。等到线程的start()方法执行时,才表示线程开始执行。当线程执行时,处于RUNNABLE状态,表示线程所需的一切资源都已经准备好了。如果线程在执行过程中遇到了synchronized同步块,就会进入BLOCKED阻塞状态,这时线程会暂停执行,直到获得请求的锁。WAITING和TIMED_WAITING都表示等待状态,它们的区别是WAITING会进入一个无时间限制的等待,TIMED_WAITING会进行一个有时间限制的等待。那么等待的线程究竟在等什么呢?一般来说,WAITING的线程是在等待一些特殊的事件。比如,通过wait()方法等待的线程在等待notify()方法,而通过join()方法等待的线程则会等待目标线程的终止。一旦等到了期望的事件,线程就会再次执行,进入RUNNABLE状态。当线程执行完毕后,则进入TERMINATED状态,表示结束。
注意:从NEW状态出发后,线程不能再回到NEW状态,同理,处于TERMINATED状态的线程也不能再回到RUNNABLE状态。

线程运行的原理

栈与栈帧

JVM中由堆、栈、方法区所组成。栈内存起始就是给线程用的,每个线程启动,虚拟机就会为其分配一块栈内存。

  • 每个栈由多个栈帧(Frame)组成,(每个栈帧)对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,(最顶上的那个栈帧)对应着当前正在执行的那个方法

线程上下文切换(Thread Context Switch)

因为一些原因导致CPU不再执行当前的线程,转而执行另外一个线程代码,例如:

  • 线程的CPU时间片用完
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己调用了sleep、yield、wait等方法

当Context Switch发送时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条JVM执行的执行地址,是线程私有的。

  • 状态包括程序计数器、虚拟机栈帧中每个栈帧的信息,如局部变量、操作数栈、返回地址等
  • Context Switch频繁切换会影响性能

初始线程:线程的基本操作

线程的创建


创建新执行线程有两种方法:

  • 将类声明为Thread类的子类(继承Thread类)
  • 类实现Runnable接口

方法一:继承Thread类

public class ThreadStyle extends Thread {
    public static void main(String[] args) {
        T1 t1 = new T1();
        t1.start();
    }
	class T1 extends Thread{
        @Override
        public void run() {
            System.out.println("通过继承Thread类的方式创建线程");
        }
    }
}
线程Thread中有一个run()方法,start()方法会新建一个线程并让这个线程去执行run()方法

t1.run();也是可以正常执行的,但是却不能新建一个线程,而是在当前线程中调用run()方法,此时的run()方法会被当做一个普通的方法被调用。

注意:不要用run()方法来开启新线程,它只会在当前线程(上述例子是main线程)中串行执行run()方法
在默认情况下,线程Thread的run()方法什么都没有做,因此,这个线程一启动就马上结束了。
如果想让线程做点什么就必须重写run()方法,把"任务"填充进去
// 匿名内部类的写法
Thread t1 = new Thread() {
    @Override
    public void run() {
        System.out.println("Hello, I am t1");
    }
};
t1.start();

方法二:实现Runnable接口

public class RunnableStyle {
    public static void main(String[] args) {
        // 声明一个Runnable接口实例
        T2 t2 = new T2();
        Thread thread = new Thread(t2);
        thread.start();
    }
}

class T2 implements Runnable {
    @Override
    public void run() {
        System.out.println("通过实现Runnable接口的方式创建线程");
    }
}
Runnable接口是一个单方法接口,它只有一个run()方法:
public interface Runnable {
  	public abstract void run();
}

Thread类有一个非常重要的构造方法:public Thread(Runnable targe);
它传入一个Runnable接口的实例,在调用start()方法时,新的线程就会执行Runnable.run()方法。
实际上,默认的Thread.run()方法就是这么做的:
public void run() {
  if (target != null) {
      target.run();
  }
}

注意:默认的Thread.run()方法就是直接调用内部的Runnable接口。因此,使用Runnable接口告诉线程该做什么更为合理。(也就是如果是通过实现Runnable接口的方式,可以使用run()方法或start()方法启动线程)

总结

  1. 通过实现Runnable接口来创建线程更好

解释:

  • 从代码角度:具体任务(run方法)应该和"创建和运行线程的机制(Thread类)"解耦,用runnable对象可以实现解耦
  • 继承Thread类以后,由于Java是单继承的,这样就无法再继承其他的类,限制了类的可扩展性
  1. 两种方法的本质对比

这两种方法的本质,最终都是调用了start()方法来新建线程。这两个方法的最主要区别在于run()方法的内容来源

  • 通过继承Thread类:整个run()都被重写
  • 实现Runnable接口:最终调用的是target.run()

终止线程(stop)

一般来说,线程执行完毕就会关闭,无须手动关闭。但是,一些服务端的后台可能会常驻系统,它们通常是一个大大的无穷循环,它们通常不会正常关闭。
JDK线程Thread提供了一个stop()方法,如果使用stop()方法,就可以立即将一个线程终止,非常方便。
但是stop()方法过于暴力,强制把执行到一半的线程终止,可能会引起一些数据不一致的问题,所以被标注为废弃方法,不推荐使用。

stop()方法在结束线程时,会直接终止线程,并立即释放这个线程所持有的锁,而这些锁恰恰是用来维持对象一致性的。如果此时,写线程正写到一半,强行终止线程对象就会被写坏。同时,由于锁已经被释放,另外一个等待该锁的读线程就顺理成章地读到了这个被写坏的对象,悲剧也就此发生。
java 终止线程的4种方式_java中如何停止一个线程-CSDN博客

线程中断(interrupt)(重要)

在Java中,线程中断是一种重要的线程协作机制。
严格来讲,线程中断并不会使线程立即关闭,而是给线程发送一个通知,告知目标线程,有人希望你关闭啦!至于目标线程接到通知后如何处理,则完全由目标线程自行决定。

public void Thread.interrupt()  //中断线程
public boolean Thread.isInterrupted()  // 判断线程是否被中断
public static boolean Thread.interrupted() // 判断线程是否被中断,并清除当前中断状态

真正的停止线程,其实是如何正确的interrupt通知需要停止的线程,以及被停止的线程如何配合的问题。

  • Thread.interrupt()方法是一个实例方法。它通知目标线程中断,也就是设置中断标志位。中断标志位表示当前线程已经被中断了。
  • Thread.isInterrupted()方法也是实例方法,它判断当前线程是否被中断(通过检查中断标志位)。
  • 静态方法Thread.interrupted()也可用来判断当前线程的中断状态,但同时会清除当前线程的中断状态。

等待(wait)和通知(notify)(重要)

等待方法wait()和通知方法notify()并不在Thread类中,而在Object类中,这意味着任何对象都可以调用这两个方法。

当在一个对象实例上调用wait()方法后,当前线程就会在这个对象上等待。这是什么意思呢?
比如,在线程A中,调用了wait()方法,那么线程A就会停止执行,转为等待状态。
等待到何时结束呢?线程A会一直等待到其他线程调用了notify()方法为止。这时,object对象俨然称为多个线程之间的有效沟通手段。

如果一个线程调用了object.wait()方法,那么它就会进入object对象的等待队列,会让线程进入WAITING状态。这个等待队列中可能会有多个线程,因为系统运行了多个线程,同时等待某一个对象。
当object.notify()方法被调用时,它就会从这个等待队列中随机选择一个线程,并将其唤醒。
需要注意的是,这个选择是不公平的,并不是先等待的线程就会优先被选择,这个选择完全是随机的。
除notify()方法外,Object对象还有一个类似的notifyAll()方法,它会唤醒在这个等待队列中等待的所有线程。

wait做的事情:

  1. 释放当前的锁
  2. 使当前执行代码的线程进入阻塞等待(把线程放到等待队列中)
  3. 满足一定条件时(收到通知时)被唤醒,同时重新尝试获取这个锁

wait结束等待的条件:

  1. 其他线程调用该对象的notify方法
  2. wait等待时间超时(wait方法提供一个带有timeout参数的版本,来指定等待时间)
  3. 其他线程调用该等待线程的 interrupted 方法,导致 wait 抛出 InterruptedException 异常

注意:wait() 必须搭配 synchronized 来使用,wait()必须写到synchronized代码块里面(notify 方法也必须在synchronized代码块中使用)。脱离 synchronized 使用 wait() 会直接抛出异常。
如果还没有获取到锁就尝试解锁,运行后就会抛出非法的锁状态异常。这里的代码抛出该异常正是这个原因:wait()方法内部有一步重要的操作:先解锁,再阻塞等待
因此,在使用 wait() 前,必须先加锁,把wait()写到synchronized代码块内部。同时,Java也规定调用 notify() 也必须在synchronized代码块中。
并且,加锁的锁对象必须要与调用wait()的锁对象是同一个。如果加锁对象与调用wait()的对象不是同一个,也会抛出 IllegalMonitorStateException 异常。

注意wait()方法和sleep()方法都可以让线程等待若干时间。除wait()方法可以被唤醒外,另外一个区别就是wait()方法会释放目标对象锁,而sleep()方法不会释放任何资源。
Java多线程基础-7:wait() 和 notify() 用法解析_java notify_碳基肥宅的博客-CSDN博客

  1. wait() 和 notify() 方法都必须搭配 synchronized 和同一个锁对象,如果wait()和notify()作用于不同的锁对象,是没有任何作用的。
  2. 如果一个线程调用对象的notify()方法,但该线程并不处于wait的状态中,notify()不会产生作用(也没有副作用)。
  3. 如果有多个线程在wait(),notify()是只随机唤醒一个,而notifyAll()则是唤醒所有。

挂起(suspend)和继续执行(resume)

略。这两个方法已经被弃用。

不推荐使用suspend()方法挂起线程是因为suspend()方法在暂停线程的同时,并不会释放任何资源。
如果resume()方法意外地在suspend()方法前就执行了,那么被挂起的线程可能很难有机会被继续执行。而且,对于被挂起的线程,从它的线程状态上看,居然还是RUNNABLE,这也会严重影响我们对系统当前状态的判断。

等待线程结束(join)和谦让(yield)(重要)

一个线程的输入可能非常依赖于另一个或者多个线程的输出,此时,这个线程就需要等待依赖线程执行完毕,才能继续执行。

public final void join() throws InterruptedException
public final synchronized void join(long millis) throws InterruptedException

第一个join()方法表示无限等待,它会一直阻塞当前线程,直到目标线程执行完毕。
第二个join()方法给出了一个最大等待时间,如果超过给定时间,目标线程还在执行,则当前线程也会因为"等不及了"而继续往下执行。

join通常是加入的意思,这个意思用在这里也非常贴切。因为一个线程要加入另外一个线程,最好的方法就是等着它一起走。

另一个方法就是yield()方法:

public static native void yield();

是一个静态方法,一旦执行,它会使当前线程让出CPU
但要注意,让出CPU并不表示当前线程不执行了,当前线程在让出CPU后,还会进行CPU资源的争夺,但是,是否能够再次分配到就不一定了。
因此,对yield()方法的调用就好像在说:“我已经完成了一些最重要的工作了,我可以休息一下了,可以给其他线程一些工作机会啦!”
如果你觉得一个线程不那么重要,或者优先级非常低,而且又害怕它会占用太多的CPU资源,那么可以在适当的时候调用yield()方法,给予其他重要线程更多的工作机会。

yield()让当前线程暂停,但是不阻塞;yield()应该做的是让当前线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。

volatile与Java内存模型(JMM)

Java内存模型都是围绕着原子性有序性可见性展开的。
Java使用了一些特殊的操作或者关键字来声明、告诉虚拟机。在这个地方,要尤其注意,不能随意变动优化目标指令。关键字volatile就是其中之一。
当你用关键字volatile声明一个变量时,就等于告诉了虚拟机,这个变量极有可能会被某些程序或者线程修改。为了确保这个变量被修改后,应用程序范围内的所有线程都能够“看到”这个改动,虚拟机就必须采用一些特殊的手段,保证这个变量的可见性等特点。

注意:关键字volatile不能代替锁,它也无法保证一些符合操作的原子性。

分门别类的管理:线程组

在一个线程中,如果线程数量很多,而且功能比较明确,就可以将相同功能的线程放在一个线程组里。

public class ThreadGroupTest implements Runnable {
 
    @Override
    public void run() {
        try {
            while (!Thread.currentThread().isInterrupted()) {
                Thread currentThread = Thread.currentThread();
                System.out.println("currentThread:" + currentThread.getName()
                        + "; threadGroup:" + currentThread.getThreadGroup().getName()
                        + "; parentThreadGroup:" + currentThread.getThreadGroup().getParent().getName());
                Thread.sleep(2000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
 
    public static void main(String[] args) {
        ThreadGroup rootGroup = new ThreadGroup("线程组root");
        Thread thread0 = new Thread(rootGroup, new ThreadGroupTest(), "thread-0");
        thread0.start();
 
        ThreadGroup group1 = new ThreadGroup(rootGroup, "线程组1");
        ThreadGroup group2 = new ThreadGroup(rootGroup, "线程组2");
 
        Thread thread1 = new Thread(group1, new ThreadGroupTest(), "thread-1");
        Thread thread2 = new Thread(group1, new ThreadGroupTest(), "thread-2");
        thread1.start();
        thread2.start();
 
        Thread thread3 = new Thread(group2, new ThreadGroupTest(), "thread-3");
        Thread thread4 = new Thread(group2, new ThreadGroupTest(), "thread-4");
        thread3.start();
        thread4.start();
    }
}

驻守后台:守护线程(Daemon)

守护线程是一种特殊的线程,就和它的名字一样,它是系统的守护者,在后台默默地完成一些系统性的服务,比如垃圾回收线程JIT线程就可以理解为守护线程。与之相对应的是用户线程,我们可以认为用户线程是工作线程,它会完成程序应该要完成的业务操作。如果用户线程全部结束,则意味着这个程序实际上无事可做了。守护线程要守护的对象已经不存在了,那么整个程序就应该结束。因此,当一个Java程序内只有守护线程时,Java虚拟机就会自然退出。
设置守护线程:setDaemon(true)
注意守护线程必须在线程start()之前设置,否则会报异常,告诉你守护线程设置失败。但是该线程依然可以继续执行,只是被当做用户线程而已。

先做重要的事:线程优先级

Java中的线程可以有自己的优先级。优先级高的线程在竞争资源时会更有优势,更可能抢到资源,当然,这只是一个概率问题。
在Java中,使用1到10表示线程优先级。
内置的三个静态标量:

1为最低优先级,5为默认值,10为最高优先级。

在Java中,可以通过setPriority(int newPriority)方法来设置线程的优先级。
数字越大则优先级越高,但有效范围在1到10之间。高优先级的线程在大部分情况下都会首先完成任务,但不能保证所有情况都这样。

本文作者:Ac_c0mpany丶

本文链接:https://www.cnblogs.com/keyongkang/p/17828739.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   Ac_c0mpany丶  阅读(22)  评论(0编辑  收藏  举报
历史上的今天:
2022-11-13 【Vue2-04】scoped样式
2022-11-13 【Vue2-03】props属性
2022-11-13 【Vue2-02】ref属性
2022-11-13 【Vue2-01】Vue脚手架
2022-11-13 【ES6】模块化
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起
  1. 1 You Are My Sunshine REOL
You Are My Sunshine - REOL
00:00 / 00:00
An audio error has occurred.

作曲 : Traditional

You are my sunshine

My only sunshine.

You make me happy

When skies are gray.

You'll never know, dear,

How much I love you.

Please don't take my sunshine away

The other night, dear,

When I lay sleeping

I dreamed I held you in my arms.

When I awoke, dear,

I was mistaken

So I hung my head and cried.

You are my sunshine,

My only sunshine.

You make me happy

When skies are gray.

You'll never know, dear,

How much I love you.

Please don't take my sunshine away.

You are my sunshine,

My only sunshine

You make me happy

When skies are gray.

You'll never know, dear

How much I love you

Please don't take my sunshine away

Please don't take my sunshine away.

Please don't take my sunshine away.