Java-多线程基础知识点

1.进程和线程的基本概念

进程是正在进行的程序,每个进程都有自己独立的一块内存空间,一个进程中可以有多个线程,多个线程可共享数据。

线程就是进程中的一个独立控制单元,一个单一的控制流,线程在控制着进程的执行,一个进程中至少有一个线程。

线程和进程的区别

  • 内存角度:进程有独立的内存空间,进程中的数据存放空间(堆和栈)是独立的,进程中至少有一个线程;线程实际上没有其独立的内存空间,堆空间是共享的,栈空间是独立的。
  • 资源角度:进程是资源分配的基本单位,线程是资源调度的基本单位,实际上不拥有资源,消耗的资源也比进程小,线程相互之间可以影响,又称为轻型进程或进程元。

上下文切换:指 CPU 从一个进程(或线程)切换到另一个进程(或线程)。上下文是指某一时间点 CPU 寄存器和程序计数器的内容。通常是计算密集型的,意味着此操作会消耗⼤量的 CPU 时间,故线程也不是越多越好。(指 CPU 从一个进程(或线程)切换到另一个进程(或线程)。上下文是指某一时间点 CPU 寄存器和程序计数器的内容。程序计数器是一个专用的寄存器,⽤于表明指令序列中 CPU 正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置,具体实现依赖于特定的系统。)

线程组:一个线程必定属于一个线程组,如果没有指定,默认是当前执行的线程。例如main函数中创建的线程所属的线程组。线程优先级不可超过线程组优先级

 

2.为什么要用多线程?

(1)为了更好的利用cpu的资源,如果只有一个线程,则第二个任务必须等到第一个任务结束后才能进行,如果使用多线程则在主线程执行任务的同时可以执行其他任务,而不需要等待;

(2)进程之间不能共享数据,线程可以;

(3)系统创建进程需要为该进程重新分配系统资源,创建线程代价比较小;

(4)Java语言内置了多线程功能支持,简化了java多线程编程。

 

3.线程的种类

(1)守护(Daemon)线程和用户线程

概念:守护线程守护用户线程,一旦用户线程结束了,守护线程还守护谁?没事做了jvm就退出了。守护线程是后台提供通用服务的线程,例如垃圾回收线程之类的。

  • 可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程。需在启动之前设置,否则会跑出一个IllegalThreadStateException异常,所以不能把正在运行的常规线程设置为守护线程。 
  • Daemon线程中产生的新线程也是Daemon的。
  • 守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断(因为用户线程执行完了,守护线程跟着没了)。

(2)父线程和子线程

概念:子线程是由父线程创建并启动的,在Java中两者没有本质区别。

JVM在启动时,首先创建main线程,去执行main方法。在main方法中创建其他的线程后,如果main线程执行完毕,其他线程也会继续执行。

需要注意的是,子线程会在默认情况下继承父线程的类别,如果父线程是守护线程,子线程也是守护线程。当然可以通过setDaemon方法改变属性。

  • 默认情况,在新开启一个子线程的时候,他是前台线程,只有,将线程的IsBackground属性设为true,他才是后台线程;
  • 当子线程是前台线程,则主线程结束并不影响其他线程的执行,只有所有前台线程都结束,程序结束;
  • 当子线程是后台线程,则主线程的结束,会导致子线程的强迫结束;
  • 后台线程一般做的都是需要花费大量时间的工作,如果不这样设计,主线程已经结束,而后台工作线程还在继续,第一有可能使程序陷入死循环,第二主线程已经结束,后台线程即时执行完成也已经没有什么实际的意义;

JVM的退出:会自动的检测注册的hook线程,并调用其run方法;释放资源。

 

 

4.线程的状态(线程的生命周期)

(1)初始(new):新创建了一个线程对象,但还没有调用start()方法。

  • 继承Thread实现:获取当前线程直接用this;
  • 实现Runnable:多实现,资源共享,Thread类中每个线程独立不共享;
  • 实现Callable:可以返回参数;

 

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class TestCreateThread {

    public static void main(String[] args) throws InterruptedException {
        MyThread myThread=new MyThread();
        myThread.start();
        Thread thread=new Thread(new MyRunnable());
        thread.start();
        FutureTask<String> futureTask=new FutureTask<>(new MyCaller());
        new Thread(futureTask).start();
        try{
            //用FutureTask的get方法获取线程返回的参数
            String res=futureTask.get();
            System.out.println(res);
        }catch (Exception e){
            e.printStackTrace();
        }

    }

}

class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("继承Thread类的方式创建线程,需要重写run()方法");
        System.out.println("获取当前线程只需要用this调用,this="+this);
    }
}

class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("实现Runnable接口创建线程");
        System.out.println("优点:接口可以多实现,资源共享,Thread类中每个线程独立不共享");
    }
}

class MyCaller implements Callable<String>{
    @Override
    public String call() throws Exception {
        System.out.println("通过实现Callable接口创建线程,可以返回参数");
        return "String参数";
    }
}
View Code

 

(2)运行(runnable):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。连续调用start()方法会抛出IllegalThreadStateException异常。

该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。(其他就绪状态:当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。锁池里的线程拿到对象锁后,进入就绪状态。)

(3)阻塞(blocked):表示线程阻塞于锁,等待锁的释放以进⼊同步区。

(4)等待(waiting):处于等待状态的线程变成runnable状态需要其他线程唤醒。

调用如下3个方法会使线程进入等待状态

  • Object.wait():使当前线程处于等待状态直到另一个线程唤醒它。调⽤wait方法前线程必须持有对象的锁。例如线程a调⽤wait()方法时,会释放当前的锁,直到有其他线程调⽤notify()/notifyAll()方法唤醒等待锁的线程。但是被唤醒后,如果有其他线程bcd也在等锁,不一定给a,看系统调度。
  • Thread.join():等待线程执行完毕,底层调用的是Object实例的wait方法。调⽤join()方法不会释放锁,会一直等待当前线程执行完毕(转换为TERMINATED状态)。假设当前执行的线程是a,有线程b调用了join方法,a交出cpu让b执行完先,此时a进入waiting状态,而b进入runable状态。如果join加了参数时间例如100,则表示线程a让线程b执行100毫秒先,然后就是二者并行。join(0)=join()。
  • LockSupport.park():除非获得调⽤许可,否则禁用当前线程进行线程调度。

(5)超时等待(timed_waiting):该状态不同于waiting,线程等待一个具体的时间,时间到后会被自动唤醒。

调⽤如下方法会使线程进⼊超时等待状态

  • Thread.sleep(long millis):使当前线程睡眠指定时间,不会释放锁,到时间后变回runable状态,
  • Object.wait(long timeout):线程休眠指定时间,会释放锁,等待期间可以通过notify()/notifyAll()唤醒;
  • Thread.join(long millis):使当前线程执行指定时间,并且使线程进⼊TIMED_WAITING状态。
  • LockSupport.parkNanos(long nanos):除⾮获得调⽤许可,否则禁⽤当前线程进行线程调度指定时间。如果在join的时间内线程还没结束,依旧是timed_waiting状态,时间一过才是terminated状态。
  • LockSupport.parkUntil(long deadline):同上,也是禁⽌线程进行调度指定时间;

(6)终止(terminated):表示该线程已经执行完毕。

5.线程的中断

interrupt()方法:作用是中断线程。

本线程中断自身是被允许的,且"中断标记"设置为true

其它线程调用本线程的interrupt()方法时,会通过checkAccess()检查权限。这有可能抛出SecurityException异常。

  • 若线程在阻塞状态时,调用了它的interrupt()方法,那么它的“中断状态”会被清除并且会收到一个InterruptedException异常。例如,线程通过wait()进入阻塞状态,此时通过interrupt()中断该线程;调用interrupt()会立即将线程的中断标记设为“true”,但是由于线程处于阻塞状态,所以该“中断标记”会立即被清除为“false”,同时,会产生一个InterruptedException的异常。
  • 如果线程被阻塞在一个Selector选择器中,那么通过interrupt()中断它时;线程的中断标记会被设置为true,并且它会立即从选择操作中返回。
  • 如果不属于前面所说的情况,那么通过interrupt()中断线程时,它的中断标记会被设置为“true”。

interrupted()方法:判断的是当前线程是否处于中断状态。是类的静态方法,同时会清除线程的中断状态。

isInterrupted()方法:判断调用线程是否处于中断状态,不会清除状态。

6.线程中的常用方法

(1)getName();//获取线程名

(2)currentThread();//取得当前线程对象

(3)isAlive();//判断线程是否启动

(4)join();//线程的强行运行,需要先启动,t.join(),让线程t执行完再回来执行当前线程,会释放锁

public class Join {
    static class ThreadA implements Runnable {
        @Override
        public void run() {
            try {
                System.out.println("我是子线程,我先睡1秒");
                Thread.sleep(1000);
                System.out.println("我是子线程,我睡完了1秒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new ThreadA());
        thread.start();
        thread.join();//当前线程指的是main线程,停止执行当前线程,先执行thread线程
        System.out.println("如果不加join方法,我会先被打出来,加了就不一样了");
    }
}
Join.class

 

(5)static void sleep(long millis)

使当前正在执行的线程暂停(睡眠)millis毫秒,睡眠的线程会变成阻塞状态,放弃争夺CPU资源,暂停结束后会变成就绪状态,等待运行。

调用方式:多线程直接调用sleep(millis)方法,其他情况都需要通过Thread.sleep(millis)方式调用,适用于网页爬虫等场景,假装是真人慢慢浏览,免得封号。需要捕获异常,不释放锁,易死锁。

(6)yield();//线程的礼让,让出CPU交给线程池中拥有相同优先级的线程,由运行状态变成就绪状态,并不是阻塞,不释放锁

(7)setPriority(int newPriority);//设置线程的优先级

优先级的范围是1-10。MIN_PRIORITY,NORM_PRIORITY,MAX_PRIORITY分别表示1,5,10优先级。不设置默认是5。优先级高的线程1更有可能抢到CPU,但不是一定的。如果某个线程优先级大于线程所在线程组的最大优先级,那么该线程的优先级将会失效,取而代之的是线程组的最大优先级。

(9)suspend()resume()stop()是被弃用的。因为在调用方法之后,线程不会保证占用的资源被正常释放

(10)notify()

唤醒在此对象监视器上等待的单个线程。

(11)notifyAll()

唤醒在此对象监视器上等待的所有线程。

(12)wait()

wait一般和notify/notifyAll一起使用,这三个方法都是Object的方法并且只能在synchrnoized中使用,因为wait和notify方法使用的前提是必须先获取一个锁。Wait的作用是使当前线程进入阻塞状态,放弃争夺CPU资源,释放锁,线程会进入该对象的等待池中,但不会主动去竞争该对象的锁;notify是随机唤醒一个等待当前对象的锁的线程,notifyAll是唤醒所有等待当前对象的锁的线程。wait方法必须放在同步块或同步方法中,

 

7.线程间的通信

(1)锁与同步

在Java中,锁的概念都是基于对象的,所以我们⼜经常称它为对象锁。一个锁同一时间只能被一个线程持有。线程A持有锁L,其他线程例如线程B要得到这个锁需要等A释放锁L。线程需要不断尝试获得锁,失败了就继续尝试,可能会耗费服务器资源。

关键字synchronized锁住的是对象,而不是代码。

public class TestThread {
    public static void main(String[] args) {
        Object lock=new Object();
        Athread a1=new Athread(lock);
        Athread a2=new Athread(lock);
        Athread a3=new Athread(lock);
        Athread a4=new Athread(lock);
        //所有线程的对象锁都是同一个lock
        a1.start();
        a2.start();
        a3.start();
        a4.start();
    }
}


class Athread extends Thread{
    private Object lock;
    public Athread(Object lock) {
        this.lock=lock;
    }
    @Override
    public void run() {
        System.out.println("A启动了");
        synchronized(lock){
            for(int i=0;i<=10;i++) {
                //System.out.println("A的i="+i);
                System.out.println("当前线程是:"+Thread.currentThread()+" i="+i);
            }
        }
    }
}
synchronized锁对象

修饰方法的情况以后遇到再补

(2)等待/通知机制

基于Object 类的 wait() 方法和 notify() , notifyAll() 方法来实现的。notify()方法会随机叫醒一个正在等待的线程,而notifyAll()会叫醒所有正在等待的线程。例如有锁Object lock = new Object();是锁lock调用方法唤醒其他有lock锁的线程,lock在哪个线程类调用wait()就是让哪个线程等待。

(3)信号量

volatile来保证变量的同步,但是这种变量需要进行原子操作,i++这种不是原子操作,读取i,+1,赋值三个原子操作构成,这类操作要用synchronized上锁。volatile效果有点像static,static是在类里的,在实例对象中操作;volatile是搞主存的,保证多线程对于同一个变量的同步。

 

8.Java内存模型

JMM与Java内存区域划分的区别与联系

区别:两者是不同的概念层次。JMM是抽象的,他是⽤来描述一组规则,通过这个规则来控制各个变量的访问方式,围绕原⼦性、有序性、可⻅性等展开的。而Java运行时内存的划分是具体的,是JVM运行Java程序时,必要的内存划分。

联系:都存在私有数据区域和共享数据区域。一般来说,JMM中的主内存属于共享数据区域,他是包含了堆和方法区;同样,JMM中的本地内存属于私有数据区域,包含了程序计数器、本地方法栈、虚拟机栈。

(1)原子性:多线程一起执行时,一个线程操作开始后不会被其他线程干扰,操作不可被中断。i = i+1;其中就包括,读取i的值,计算i,写入i。这行代码在Java中是不具备原子性的,则多线程运行肯定会出问题,所以也需要我们使用同步和lock这些东西来确保这个特性了。原子性其实就是保证数据一致、线程安全。

(2)可见性:一个线程修改共享变量时,其他线程能够立即知道这个修改。单线程不用管这个,多线程通过volatile, synchronized, final关键字实现可见性。

(3)有序性:Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序的过程不会影响单线程的执行结果,却会影响到多线程并发执行的正确性。

 

9.重排序和happens-before

(1)重排序的概念

计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排。

  • 编译器优化重排:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  • 指令并行重排:如果不存在数据依赖性,处理器可以改变语句对应的机器指令的执行顺序。
  • 内存系统重排:内存系统重排由于处理器使⽤缓存和读写缓存冲区,这使得加载(load)和存储(store)操作看上去可能是在乱序执行,因为三级缓存的存在,导致内存与缓存的数据同步存在时间差。

指令重排可以保证串行语义一致,但是没有义务保证多线程间的语义也一致。所以在多线程下,指令重排序可能会导致一些问题。

(2)as-if-serial语义

保证单线程内重排序后的执行结果和程序代码本身应有的结果是一致的

(3)happens-before

概念:如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可⻅,而且第一个操作的执行顺序排在第二个操作之前。两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么JMM也允许这样的重排序。

意义:保证正确同步的多线程程序的执行结果不被重排序改变。  

(4)天然的happens-before

程序顺序规则:一个线程中的每一个操作,happens-before于该线程中的任意后续操作。

监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。

volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。

传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。

start规则:如果线程A执行操作ThreadB.start()启动线程B,那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。

join规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于 线程A从ThreadB.join()操作到A成功返回。

 

 


 

参考&引用

https://www.jianshu.com/p/d50cfcaf8102

https://www.cnblogs.com/huangyichun/p/7126851.html

《深入浅出Java多线程.pdf》

Callable
posted @ 2020-04-28 17:13  守林鸟  阅读(324)  评论(0编辑  收藏  举报