第一章:并发编程线程基础

1.1 什么是线程?

线程是操作系统能够进行运算调度的最小单位,被包含在进程之中,是进程中的实际运作单位
思考1:线程与进程的区别?
进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,一个进程通常包含多个线程,进程之间的线程共享进程的资源。
思考2:并发与并行的区别?
并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是从宏观来看,二者都是一起执行的。
image.png
并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。

image.png

1.2 线程的创建与运行

Java种有两种线程创建的方法,分别是实现Runnable接口的run方法,继承Thread类并重写run方法。

image.png

1.2.1 线程创建方式一

继承Thread接口,要求如下:
1)编写程序,开启一个线程,改线程每隔1秒,在控制台输出,”hello world”
2)对上题进行改进:当输出50次”hello world”,结束改线程
3)使用JConsole监控线程执行情况,并画出程序示意图!

  1. **
  2. * @Author 周海权
  3. * @Date 2022/4/5 9:32
  4. * @Version 1.0
  5. */
  6. public class ThreadTest{
  7. //重写方法快捷键Ctrl+O
  8. static class MyThread extends Thread{
  9. @Override
  10. public void run() {
  11. while (true) {
  12. try {
  13. System.out.println("hello world");
  14. Thread.sleep(1000);
  15. } catch (InterruptedException e) {
  16. e.printStackTrace();
  17. }
  18. }
  19. }
  20. }
  21. public static void main(String[] args) {
  22. //创建线程
  23. MyThread thread = new MyThread();
  24. thread.start();
  25. }
  26. }
  1. static class MyThread extends Thread{
  2. int time = 0;
  3. @Override
  4. public void run() {
  5. while (time<=50) {
  6. try {
  7. System.out.println("hello world");
  8. Thread.sleep(1000);
  9. time++;
  10. } catch (InterruptedException e) {
  11. e.printStackTrace();
  12. }
  13. }
  14. }
  15. }

使用JConsole监控线程执行情况:
image.png
线程运行的示意图:

并发编程基础知识总结 - 图5
思考:为什么是调用start方法而不是直接调用run方法呢?
如果调用run方法,那就相当于没有开启线程去,而是主线程直接调用,那就相当于是串行执行了,而不是并发执行,做一个实现来证明这点。

  1. static class MyThread extends Thread{
  2. int time = 0;
  3. @Override
  4. public void run() {
  5. while (time<=10) {
  6. try {
  7. System.out.println("hello world:"+Thread.currentThread().getName()+"线程调用的");
  8. Thread.sleep(1000);
  9. time++;
  10. } catch (InterruptedException e) {
  11. e.printStackTrace();
  12. }
  13. }
  14. }
  15. }
  16. public static void main(String[] args) throws InterruptedException {
  17. //创建线程
  18. MyThread thread = new MyThread();
  19. thread.run();
  20. for (int i = 0 ; i < 10 ; i++){
  21. Thread.sleep(1000);
  22. System.out.println("主线程i="+i);
  23. }
  24. }

image.png
可以debug源码验证调用Start方法到底为我们做了什么
1.当调用Start的时候立即进入,核心的就是调用Start0()方法

  1. public synchronized void start() {
  2. /**
  3. * This method is not invoked for the main method thread or "system"
  4. * group threads created/set up by the VM. Any new functionality added
  5. * to this method in the future may have to also be added to the VM.
  6. *
  7. * A zero status value corresponds to state "NEW".
  8. */
  9. if (threadStatus != 0)
  10. throw new IllegalThreadStateException();
  11. /* Notify the group that this thread is about to be started
  12. * so that it can be added to the group's list of threads
  13. * and the group's unstarted count can be decremented. */
  14. group.add(this);
  15. boolean started = false;
  16. try {
  17. start0();
  18. started = true;
  19. } finally {
  20. try {
  21. if (!started) {
  22. group.threadStartFailed(this);
  23. }
  24. } catch (Throwable ignore) {
  25. /* do nothing. If start0 threw a Throwable then
  26. it will be passed up the call stack */
  27. }
  28. }
  29. }

2.Start0()是个本地方法,由我们的JVM去调用

  1. private native void start0();
  2. /**
  3. * If this thread was constructed using a separate
  4. * <code>Runnable</code> run object, then that
  5. * <code>Runnable</code> object's <code>run</code> method is called;
  6. * otherwise, this method does nothing and returns.
  7. * <p>
  8. * Subclasses of <code>Thread</code> should override this method.
  9. *
  10. * @see #start()
  11. * @see #stop()
  12. * @see #Thread(ThreadGroup, Runnable, String)
  13. */
  14. @Override
  15. public void run() {
  16. if (target != null) {
  17. target.run();
  18. }
  19. }

image.png
总结:其实调用start方法后线程并没有马上执行,而是处于就绪状态,这个就绪状态时指该线程处了CPU之外的资源全部都有了,等待获取CPU资源后才会真正处于运行状态。一旦run方法执行完毕,该线程就处于终止状态。

1.1.2 线程创建方式二:实现Runnable接口

注:因为java是单继承的,如果我们的某个类已经继承了某个父类,如果在继承一个类,就不太可能了,所以我们可以实现Runnable接口

  1. public static void main(String[] args) {
  2. RunnableTask runnableTask = new RunnableTask();
  3. new Thread(runnableTask).start();
  4. }
  5. static class RunnableTask implements Runnable{
  6. @Override
  7. public void run() {
  8. System.out.println("hello world: "+Thread.currentThread().getName()+"调用的");
  9. }
  10. }

image.png
问题:为什么可以new Thread(runnableTask)呢?
因为其底层使用了静态代理模式:
用代码模拟Runnable接口如何使用代理模式的

1.3 线程通知与等待

1.3.1 wait()函数

当一个线程调用了wait()方法时,该调用线程会被阻塞挂起,直到发生下面几件事情之一才会返回,
1)其他线程调用了该共享对象的notify()或者notifyAll()方法
2)其他线程调用了该线程的interrupt方法,该线程抛出InterruptedException异常返回
注:如果调用wait()方法的线程必须实现获取该对象的监视器锁。
思考:一个线程如何才能获取一个共享变量的监视器锁呢?
1.执行Synchronized同步代码块,使用该共享变量作为参数
Synchronized(共享变量){
dosomething
}
2.调用该共享变量的方法,并且该方法使用了Sybchronized修饰。
Synchronized void add(int a, int b){
//dosomething
}

1.3.2 notify()函数

一个线程调用共享对象的notify()方法后,会唤醒一个在该共享变量上调用wait系统方法被挂起的线程。一个共享变量上可能会有多个线程在等待具体唤醒哪个线程是随机的。

1.3.3 notifyAll()函数

不同于在共享变量上调用notify()函数会唤醒被阻塞到该共享变量上的一个线程,notifyAll()方法则会唤醒所有在该共享变量上由于调用wait系统方法而被挂起的线程。

1.3.4 通知与等待的代码演示

  1. final static Object ob = new Object();
  2. static class T1 extends Thread {
  3. @Override
  4. public void run() {
  5. synchronized (ob) {
  6. System.out.println("t1-----我正在等待执行中");
  7. try {
  8. Thread.sleep(2000);
  9. ob.wait();
  10. System.out.println("t1------我要开始了");
  11. Thread.sleep(3000);
  12. System.out.println("t1-------我执行结束了,唤醒t2");
  13. ob.notify();
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. }
  18. }
  19. }
  20. static class T2 extends Thread {
  21. @Override
  22. public void run() {
  23. synchronized (ob) {
  24. System.out.println("t2-----我现在执行了");
  25. try {
  26. Thread.sleep(1000);
  27. System.out.println("t2-----------我释放了执行权");
  28. ob.notify();
  29. System.out.println("t2-----------我再次拿到了执行权,开始执行");
  30. Thread.sleep(5000);
  31. System.out.println("t2-----------我等着t1执行之后在执行");
  32. ob.wait();
  33. System.out.println("t2-----------我彻底结束了");
  34. } catch (InterruptedException e) {
  35. e.printStackTrace();
  36. }
  37. }
  38. }
  39. }
  40. public static void main(String[] args) {
  41. Thread t1 = new T1();
  42. Thread t2 = new T2();
  43. t1.start();
  44. t2.start();
  45. }
  46. }


t1先执行,然后阻塞此线程,等待唤醒
t2获取ob 开始执行,唤醒等待的线程中的一个,其他线程必须等待它执行结束之后才可以执行
t1再次拿到执行权,执行

1.4 等待线程执行终止的join方法

在项目实践中经常会遇到一个场景,就是需要等待某几件事完成后才能继续往下执行,比如多个线程加载资源,需要等待多个线程全部加载完毕再汇总处理。
简单的例子如下:

  1. public static void main(String[] args) throws InterruptedException {
  2. Thread threadone = new Thread(new Runnable() {
  3. @Override
  4. public void run() {
  5. try {
  6. Thread.sleep(1000);
  7. } catch (InterruptedException e) {
  8. e.printStackTrace();
  9. }
  10. System.out.println("child ThreadOne over!");
  11. }
  12. });
  13. Thread threadtwo = new Thread(new Runnable() {
  14. @Override
  15. public void run() {
  16. try {
  17. Thread.sleep(1000);
  18. } catch (InterruptedException e) {
  19. e.printStackTrace();
  20. }
  21. System.out.println("child threadTwo over!");
  22. }
  23. });
  24. //启动子线程
  25. threadone.start();
  26. threadtwo.start();
  27. System.out.println("wait all child thread over");
  28. //等待子进程执行完毕,返回
  29. threadone.join();
  30. threadtwo.join();
  31. System.out.println("all child thread over");
  32. }

image.png

1.5 让线程睡眠的sleep方法

Thread类中有一个静态的sleep方法。当一个执行中的线程调用Thread的sleep方法后,调用线程会暂停指定实践的执行权,也就是在这期间不参与CPU的调度,但是该线程所拥有的监视器资源,比如锁还是持不放开、指定的睡眠时间到了后,线程就进入就绪状态,然后参与CPU的调度,