劣实基础–Java 并发编程基础知识

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/lwj_zeal/article/details/89070171

Java 并发编程基础知识

CPU 核心数和线程数的关系

目前的 CPU 有双核,四核,八核,一般情况下,它和线程数是1:1的对应关系,也就是四核 CPU 一般就能并行执行 4 个线程。但 Intel 引入超线程技术后,使核心数与线程数形成1:2的关系,也就是我们常说的 4核8线程

线程调度与线程调度模型

任意时刻,只有一个线程占用 CPU,处于运行状态。而多线程并发执行就是轮流获取 CPU 执行权。

  • 分时调用模型

轮流获取 CPU 执行权,均分 CPU 执行时间。

  • 抢占式调度模型

优先级高的线程优先获取 CPU 执行权,这也是 JVM 采用的线程调度模型。

进程与线程

进程是程序运行资源分配的最小单位。

这些资源就包括 CPU,内存空间,磁盘 IO 等。同一进程中的多个线程共享该进程的所有资源,而不同进程是彼此独立的。举个栗子,在手机开启一个 APP 实际上就是开启了一个进程了,而每一个 APP 程序会有很多线程在跑,例如刷新 UI 的线程等,所以说进程是包含线程的。

线程是 CPU 调度的最小单位,必须依赖于进程而存在。

线程是比进程更小的,能独立运行的基本单位,每一个线程都有一个程序计数器,虚拟机栈等,它可以与同一进程下的其它线程共享该进程的资源。

并行与并发

  • 并行

指应用能够同时执行不同的任务。例如多辆汽车可以同时在同一条公路上的不同车道上并行通行。

  • 并发

指应用能够交替执行不同的任务,因为一般的计算机只有一个 CPU 也就是只有一颗心,如果一个 CPU 要运行多个进程,那就需要使用到并发技术了,例如时间片轮转进程调度算。比如单 CPU 核心下执行多线程任务时并非同时执行多个任务,而是以一个非常短的时间不断地切换执行不同的任务,这个时间是我们无法察觉的出来的。

两者的区别:并行是同时执行,并发是交替执行。

高并发编程的意义

  • 充分利用 CPU 资源

线程是 CPU 调度的最小的单位,我们的程序是跑在 CPU 的一个核中的某一个线程中的,如果在程序中只有一个线程,那么对于双核心4线程的CPU来说就要浪费了 3/4 的 CPU 性能了,所以在创建线程的时候需要合理的利用 CPU 资源,具体可以看看 AsyncTask 内部的线程池是如何设计的

  • 加快响应用户的时间

如果多个任务时串行执行的话,那么效果肯定不好,在移动端开发中,并发执行多个任务是很常见的操作,最常见的就是多线程下载了。

  • 可以使代码模块化,异步化,简单化

在 Android 应用程序开发中的,一般 UI 线程负责去更新界面相关的工作,而一些 IO,网络等操作一般会放在异步的工作线程去执行,这样使得不同的线程各司其职,异步化。

线程之间的安全性问题

问题1:同一进程间的多个线程是可以共享该进程的资源的,当多个线程访问共享变量时,就会线程安全问题。

问题2:为了解决线程之间的同步问题,一般会引入锁机制,对于线程之间抢夺锁时也是有可能造成死锁问题。

问题3:在 JVM 内存模型中,每一个线程都会分配一个虚拟机栈,这个虚拟机栈是需要占用内存空间的,如果无限制的创建线程的话,会耗尽系统的内存。

线程的开启与关闭

线程的启动

  • 派生 Thread 类
//开启一个线程
Thread thread = new Thread() {
    @Override
    public void run() {
        super.run();
        System.out.println("thread started");
    }
};
thread.start();
  • 实现 Runnable 接口,将其交给 Thread 类去执行
Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("runnable run invoked");
    }
});
thread.start();
  • 实现 Callable 接口

因为 Thread 构造中只接收 Runnable 类型的接口,需要实现将 Callable 的实现类包装为 FutureTask 之后交给 Thread 类去执行。

Callable<String> callable = new Callable<String>() {
    @Override
    public String call() throws Exception {
        Thread.sleep(1500);
        return "work done!";
    }
};

FutureTask<String> futureTask = new FutureTask(callable);
Thread thread = new Thread(futureTask);
thread.start();
try {
    //get() 是一个阻塞式的操作,一直等待 call 方法执行完毕。
    String resule = futureTask.get();
} catch (ExecutionException e) {
    e.printStackTrace();
}

再来看 FutureTask 的应用:我们观察到 AsyncTask 内部就使用到了 FutureTask ,因为在 doInBackground() 需要有一个返回值,而恰好 Callable 就可以实现子线程执行完有返回值。使用 FutureTask 来封装 WorkRunnable 对象,然后再交给对应的线程池去执行。具体的代码如下:

FutureTask

总结:对于第1,2两种方式是在线程执行完毕后,无法得到执行的结果,而第三种方式是可以获取执行结果的。

线程的终止

  • 线程自然终止

也就是 run 执行完毕,或者是内部出现一个异常导致线程提前结束。

  • 暴力终止

    • suspend() 使线程挂起,并且不会释放线程占有的资源(例如锁),resume() 使挂起的线程恢复。

    • stop() 暴力停止,立刻释放锁,导致共享资源可能不同步。

以上几个方法已经被 JDK 标记为废弃状态。

  • interrupt() 安全终止

第一种情况:如果线程处于正常运行状态,那么线程的中断状态会被置为 true ,并且线程还是会正常执行,仅此而已。

第二种情况:如果当前线程如果是处于阻塞状态,例如调用了 wait,join,sleep 等方法,那么则会抛出InterruptedException异常。

总结: interrupt() 并不能中断线程,需要线程配合才能实现中断操作。

示例01

public class EndThread extends Thread {

    @Override
    public void run() {
        super.run();

        System.out.println("isInterrupted:"+isInterrupted());
        while (true) {//尽管在其他线程中调用了 interrupt() 方法,但是线程并不会终止
            //while (!isInterrupted()){
            System.out.println("I am Thread body");
        }
//        System.out.println("isInterrupted:"+isInterrupted());
    }

    public static void main(String[] args) throws InterruptedException {

        final EndThread endThread = new EndThread();

        endThread.start();

        Thread.sleep(10);
        //在其他线程中去调用线程的interrupt方法给线程打一个终止的标记
        endThread.interrupt();
    }
}

上面的代码时在线程体中执行一个 while(true)的死循环,然后在其他线程中调用 endThread.interrupt() 观察当前线程是否会执行完毕。

经测试:在调用线程的 interrupt()方法之后,while(true)是不会结束循环的,也就是线程还是一直在运行着。所以说 interrupt() 并不会应用 run 方法的执行

示例02

下面再来看看另一个关于 interrupt 方法的使用

在线程体内部 sleep(2000) 并且 try catch 对应的 InterruptedException 异常,如果在其他线程调用了 endThread.interrupt() 那么此处就会抛出 InterruptedException 异常,并且会isInterrupted() 会返回 false 。

public class EndThread2 extends Thread {
    @Override
    public void run() {
        super.run();

        System.out.println("isInterrupted:" + isInterrupted());
        try {
            //在其他线程调用 endThread.interrupt() 之后,会抛出 InterruptedException 异常并且线程的
            // isInterrupted 会被标记为 false。因此最后输出的结果还是 false
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
            System.out.println("catch InterruptedException");
        }
        System.out.println("isInterrupted:" + isInterrupted());
    }

    public static void main(String[] args) throws InterruptedException {

        final EndThread2 endThread = new EndThread2();

        endThread.start();

        Thread.sleep(300);
        //在其他线程中去调用线程的interrupt方法给线程打一个终止的标记
        endThread.interrupt();

    }
}

执行结果:

isInterrupted:false
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at endthread.EndThread.run(EndThread.java:28)
catch InterruptedException
isInterrupted:false

示例03

示例01中的 while(true) 修改为 while(!isInterrupted()){},在外界调用了 endThread.interrupt()之后,线程的 isInterrupted() 就会返回 true,标记着你可以结束线程了。

public class EndThread extends Thread {

    @Override
    public void run() {
        super.run();

        System.out.println("isInterrupted:"+isInterrupted());
       while (!isInterrupted()){
            System.out.println("I am Thread body");
        }
       System.out.println("isInterrupted:"+isInterrupted());
    }

    public static void main(String[] args) throws InterruptedException {

        final EndThread endThread = new EndThread();

        endThread.start();

        Thread.sleep(10);
        //在其他线程中去调用线程的interrupt方法给线程打一个终止的标记
        endThread.interrupt();
     )
    }
}

示例04

还有一种方式是设置一个 boolean 类型的变量 mIsExit 标记,当线程体内部判断到 mIsExit 为 false 那么就跳出循环。具体示例代码如下:

public class EndThread extends Thread {
    //这个变量需要在其他线程中判断,因此需要设置为线程可见的
    private volatile boolean mIsExit = true;
    @Override
    public void run() {
        super.run();
        while(mIsExit){
            System.out.println("I am Thread body");
        }
    }

    public static void main(String[] args) throws InterruptedException {

        final EndThread endThread = new EndThread();

        endThread.start();

        Thread.sleep(3);
        //设置标记为退出状态
        endThread.mIsExit = false;
    }
}

线程其他 API

Thread#start() 与 Thread#run()

start() 方法调用之后会让一个线程进入就绪等待队列,当获取到 CPU 执行权之后会执行线程体 run()方法。

run() 方法只是 Thread 类中一个普通方法,如果手动去调用,跟调用普通方法没有什么区别。

Thread#run() 与 Runnable#run()

在 Java 中只有 Thread 才能表示一个线程,而 Runnable 只能表示一个任务,任务是需要交给线程去执行的,当出现如下代码时,你看到可能会懵逼,执行结果到底是什么?

//接受一个 runnable 接口
Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("runnable run invoked");
    }
}) {
    @Override
    public void run() {
        super.run();
        System.out.println("thread started");
    }
};
thread.start();

以上方式的输出结果是:
如果线程 run 方法内部调用了 super.run() 那么输出结果如下:

runnable run invoked
thread started

如果线程 run 方法内部不调用 super.run() 那么输出结果如下:

thread started

我们可以通过源码来解答这个问题:在创建线程时,如果往构造函数中传入一个 Runnable 对象,那么它会给线程 target 属性赋值,并且在线程体执行时先判断 target 是否为空,不为空,则先执行 Runnablerun 方法,再执行当前线程体的子类中的 run 方法。

//Thread.java
private Runnable target;

public Thread(ThreadGroup group, Runnable target, String name,
              long stackSize) {
    init(group, target, name, stackSize);
}

@Override
public void run() {
    if (target != null) {
        //如果传入的 Runnable 实例,那么会调用调用 runnable 实例的 run 方法
        target.run();
    }
}

yield()

Java线程中的 Thread.yield() 方法,译为线程让步。顾名思义,就是说当一个线程使用了这个方法之后,它会放弃自己CPU执行权,让自己或者其它的线程运行,注意是让自己或者其他线程运行,并不是单纯的让给其他线程。yield()的作用是让步。它能让当前线程由“运行状态”进入到“就绪状态”,从而让其它具有相同优先级的等待线程获取执行权;但是,并不能保证在当前线程调用yield()`之后,其它具有相同优先级的线程就一定能获得执行权;也有可能是当前线程又进入到“运行状态”继续运行!

public class YieldDemo {

    public static void main(String[] args) {
        Yieldtask yieldtask = new Yieldtask();

        Thread t1 = new Thread(yieldtask, "A");
        Thread t2 = new Thread(yieldtask, "B");
        Thread t3 = new Thread(yieldtask, "C");
        Thread t4 = new Thread(yieldtask, "D");

        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }

    public static class Yieldtask implements Runnable {


        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + "-" + i);

                if (i == 5) {
                    Thread.yield();
                }
            }
        }
    }
}

从运行结果可以看到,调用 yield() 方法的线程之后,CPU 执行权不一定会给其他线程抢到,有可能还是当前线程抢到 CPU 执行权。

...
A-0
D-4
D-5
D-6//在这里,还是执行 D 这个线程
D-7
D-8
D-9
B-2
B-3
B-4
B-5
C-6
B-6
A-1
A-2
A-3
A-4
...

join()

join() 把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。

这里举一个栗子:午饭时间到了,老王(线程A)调用了微波炉热饭的方法,预约了4分钟,当微波炉跑了2分钟,这时老王看到一个美女(线程B)过来,这时主动调用了线程B.join()方法,此时把微波炉让给了美女,这时老王就等待美女热饭,直到热好美女的饭之后,才轮到老王去继续热饭,这就是一个 join() 方法的作用。

下面画了一个草图:

join

public class JoinDemo implements Runnable {

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        JoinDemo joinDemo = new JoinDemo();
        Thread thread = new Thread(joinDemo);
        thread.start();
        //join() 会阻塞当前线程,等待子线程执行完毕
        //在这里主线程会等待子线程执行完毕之后才能往下执行。
        thread.join();
        System.out.println(Thread.currentThread().getName() + "  " + " done!");
    }

    @Override
    public void run() {
        try {
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName()+" done!");        
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

执行结果:
Thread-0 done!
main   done!

线程状态

在 Java Thread 类中定义了一个 State 枚举,内部定义以下6个线程状态。

Thread 状态

在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(TIME_WAITING,WAITING)和死亡(TERMINATED)五种状态。

我这里绘制一个草图,描述了各个状态之前的切换:

线程状态切换图解.png

  • 新建状态(NEW)

新建一个线程对象,此时并没有执行 start() 方法,这时的线程状态就是处于新建状态。

Thread thread = new Thread(){
    public void run() {...}
};
  • 就绪状态(RUNNABLE)

start() 方法调用之后会让一个线程进入就绪等待队列,JVM 会为其创建对应的虚拟机栈,本地方法栈和程序计数器。处于这个状态的线程还没开始运行,需要等待 CPU 的执行权。

  • 运行状态(RUNNING)

处于就绪状态的线程在抢到 CPU 执行权之后,就处于运行状态,执行该线程的 run 方法。

  • 阻塞状态(BLOCKED)

TIME_WAIT/WAIT:

运行的线程执行 wait() /wait(long),join()/join(long)方法,那么这些线程放弃 CPU 执行和线程持有的锁,并且 JVM 会将这些线程放入到等待池中。

BLOCKED:

运行时的线程在获取同步锁时,发现锁被其他线程持有,这时 JVM 会将当前线程放入锁池中。

  • 结束(TERMINATED)

线程 run 方法执行完毕,或者线程被异常终止。

总结

以上是关于 Java 多线程的一些基本知识点的总结,有任何不对的地方,请多多指正。

记录于2019年4月7日

猜你喜欢

转载自blog.csdn.net/lwj_zeal/article/details/89070171