1.认识线程

线程的概念:

一个线程就是一个”执行流”,每一个线程之间都可以按照顺序执行自己的代码,多个线程之间”同时”执行多份代码

进程包含线程

一个进程可以包含一个线程或多个线程(多个线程可能是多个CPU核心上同时运行, 也可能在一个CPU核心上,通过快速调度,进行运行)

每个线程是独立的执行流,多个线程之间也是并发执行的

线程是操作系统调度运行的基本单位

进程是操作系统资源分配的基本单位

一个进程的多个线程之间,共用一份系统资源(内存空间 , 文件描述符)

2.Thread及常用方法

五种创建线程的方法

a.使用继承Thread,重写run方法

class MyThread extends Thread {
     @Override
     //run方法可以称为线程的入口方法
     public void run() {
         while(true) {
             System.out.println("hello thread");
             try {
                 //sleep 是 Thread 的一个静态方法
                 Thread.sleep(1000);
                      //休眠过程中,中途唤醒
             } catch (InterruptedException e) {
                 throw new RuntimeException(e);
             }
         }
     }
 }
public class ThreadDemo1 {
    public static void main(String[] args) {
        Thread t = new MyThread();
        //调用操作系统的api,创建新线程,新线程里调用了t.run
        t.start();
        while (true) {
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

}

多个线程在CPU的调度执行顺序上时随机的(打印顺序不确定)

多线程总结​1_多线程

b.使用实现runnable接口,重写run方法

class MyRunnable implements Runnable {
    @Override
    public void run() {
        while (true) {
            System.out.println("hello thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
public class ThreadDemo2 {
    public static void main(String[] args) {
        MyRunnable myRunnable  = new MyRunnable();
        Thread t = new Thread(myRunnable);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

c.继承Thread 使用匿名内部类

public class ThreadDemo3 {
    public static void main(String[] args) {
        Thread thread = new Thread() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };
        thread.start();
        while (true) {
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

d.实现Runnable,使用匿名内部类

public class ThreadDemo4 {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        });
        t.start();
        while (true) {
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

        }
    }
}

e.使用Lambda 表达式(推荐)

 Thread t = new Thread(() -> {}

()里面放参数  只有一个参数可以省略

{} 里面写java 代码, 只要一行也可以省略

public class ThreadDemo5 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (true) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();
        while (true) {
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

}

Thread类里的其他一些方法

多线程总结​1_多线程_02

start() : 真正从系统创建一个线程,新线程执行run方法;

run() : 表示线程的入口方法,线程启动执行什么逻辑(交给系统去自动调用)

中断一个线程

public class ThreadDemo9 {
    public static boolean isQuit;
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (!isQuit) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("线程终止");
        });
        t.start();
        isQuit = true;

    }
}

上述使用自己创建的变量来控制循环

而java中 Thread类内置了一个标志位来控制循环

public class ThreadDemo6 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            //currentThread 获取当前线程实例,此处得到的对象就是t;
            //isInterrupted 就是t对象自带的一个标志位
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException ex) {
                        throw new RuntimeException(ex);
                    }
                }
                break;
            }
        });     //或者    }, "Thread_0"); 可以自定义线程名;
        //开始线程
        t.start();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        //把t内部标志位设置成true;也可以唤醒sleep
        t.interrupt();

    }
}

interrupt方法的作用:

1.设置标志位为true

2.如果该线程正在阻塞(比如执行sleep)此时就会把阻塞状态唤醒,通过抛出异常让sleep立即结束

(sleep 被唤醒后会自动把标志位清空(true 变为 false)使得下次循环可以继续执行)

sleep被唤醒清空标志位的目的:让线程自身对线程何时结束有一个明确的控制

并不是立即中断(用代码灵活控制)


join() -等待一个线程

线程之间并发执行,操作系统对线程的调度是无序的

无法判断谁先谁后

public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("1");
            }
        });
        t.start();
        t.join();
        System.out.println("2");

    }

在main线程调用t.join

表示让main 线程等待 t 线程先结束再往下执行(保证t先结束)

join() 还可以添加参数表示最大等待时间

3.线程的状态

如何查看线程

在你安装的java jdk bin 目录中

使用管理员模式打开jconsole.exe;

多线程总结​1_多线程_03

先运行你所编译的线程

多线程总结​1_多线程_04

多线程总结​1_多线程_05

多线程总结​1_多线程_06

直接点击不安全的连接

多线程总结​1_多线程_07

多线程总结​1_多线程_08

线程的六个状态

多线程总结​1_多线程_09

多线程总结​1_多线程_10

4.多线程带来的线程安全问题*

本质上是因为多线程的调度顺序是不确定的(抢占式执行)

线程不安全的原因

a.抢占式执行

b.多个线程修改同一个变量

c.修改操作不是原子的

举例说明

public class ThreadDemo10 {
    static class Countor{
        public int count = 0;
        public void add () {
            count++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Countor countor = new Countor();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 6000; i++) {
                countor.add();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 6000; i++) {
                countor.add();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(countor.count);
    }
}

多线程总结​1_多线程_11

结果并不是预期的 12000

出现这种情况的原因要和线程的随机调度相关

countor.add() 这个方法本质是三个CPU指令

Load: 加载 把内存中的数据加载到CPU寄存器上;

Add: 在寄存器上进行count++;

Save:保存 把寄存器的值保存回到内存当中;

多线程总结​1_多线程_12

多线程总结​1_多线程_13

每次加法操作的三个指令都是随机的

有可能数据还没有Save, 另一个线程就读取到了未修改的值(类似于事务脏读问题)

如何解决这一问题

我们可以把countor.add() 变为原子的

引入了加锁 后续说明

d.内存可见性引起的

(多线程环境下,由于编译器对代码的优化,而产生了bug)

举例说明

import java.util.Scanner;

public class ThreadDemo11 {
    public static int a = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (a == 0) {

            }
            System.out.println("线程1循环结束");
        });
        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个数");
            a = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

目的是输入一个非零,打印"线程1循环结束";

结果显示

多线程总结​1_多线程_14

显然结果不符

这就是由内存可见性导致的

while (a == 0) { }

此时CPU存在

Load加载指令:从内存读取数据到CPU寄存器,   

Compare比较指令:比较寄存器的值是否是零

而读取寄存器是比读取内存快了几个数量级的 (每秒执行上亿次)

此时编译器就认为Load开销大,而每次Load读的结果(0)都一样,编译器就会把Load优化了,只保留第一次读取的Load(0),后面改的就没有用了(a = scanner.nextInt())

t1线程频繁读取主内存效率较低,就被优化成直接读取直接的工作内存(Compare寄存器),

t2线程修改了主内存的结果,但是t1线程没有读取主内存,导致修改不能被识别

为了解决上述问题引入了volatile(不稳定)关键字 后续说明

e.指令重排序引起的

指令重排序也是有关编译器的优化

保证整体逻辑不变的情况下,调整代码的执行顺序,使得程序变得更加的高效;

(多线程环境下也不好说了)

5.加锁

适用场景多个线程写

java中利用关键字:synchronizedj 进行加锁操作

public class ThreadDemo10 {
    static class Countor{
        public int count = 0;
        public void add () {
            synchronized (this) {
                count++;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Countor countor = new Countor();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 6000; i++) {
                countor.add();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 6000; i++) {
                countor.add();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(countor.count);
    }
}

多线程总结​1_多线程_15

a.代码块进行加锁

多线程总结​1_多线程_16

b.类进行加锁

(如果修饰的是static方法,就是给类对象进行加锁)

多线程总结​1_多线程_17

此时当t1线程进行countor.add()的时候

t2线程就会进入阻塞等待

保证了++操作变为了原子的,解决了这个问题

加锁本质就是把并发变成串行

当然多个线程对同一个锁对象加锁就会产生锁竞争问题

6.volatile关键字

适用场景 一个线程读 一个线程写

被volatile 修饰的变量,编译器就会知道它是不稳定的就会禁止优化,每次都是从内存上读取数据

import java.util.Scanner;

public class ThreadDemo11 {
    volatilepublic static int a = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (a == 0) {

            }
            System.out.println("线程1循环结束");
        });
        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
             a = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

多线程总结​1_多线程_18

a被volatile 修饰后t1线程就可以正常退出来了

7.wait and notify

线程的调度顺序是无序的(某些需求下希望线程有序)

wait:让线程等一等;

notify: 通知线程起来了,继续执行

wait 和 notify 都是object类的方法只要是个类对象(基本数据类型除外)都可以使用

wait 必须写到synchronized 代码块里面

线程中wait做的事情:

a.是当前执行的线程进行等待

b.解开锁

c.等待被唤醒,尝试重新获取锁

public class ThreadDemo12 {
    public static void main(String[] args) {
          Object block = new Object();
          Thread t1 = new Thread(() -> {
              synchronized (block) {
                  try {
                      System.out.println("开始等待");
                      block.wait();
                      System.out.println("等待结束");
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              }
          });
          t1.start();
          Thread t2 = new Thread(() -> {
              synchronized (block) {
                  System.out.println("开始通知");
                  block.notify();
                  System.out.println("结束通知");
              }
          });
          t2.start();
    }
}

多线程总结​1_多线程_19


8.面试题

进程和线程区别*

1.进程包含线程

2.进程有自己独立的内存空间和文件描述符,同一个进程的多个线程之间,共享同一份地址空间和文件描述符

3.进程是操作系统资源分配的基本单位,线程是操作系统的调度执行的基本单位

4.进程之间具有独立性,一个进程挂了不影响另一个进程;而同一个进程的多个线程之间,一个线程挂了,可能把整个进程带走,影响其他的线程

方法重载和重写的区别

多线程总结​1_多线程_20

相容:父类返回值的派生类(子类)

run 和 start 的区别

start() : 真正从系统创建一个线程,新线程执行run方法;

run() : 表示线程的入口方法,线程启动执行什么逻辑(交给系统去自动调用)

java如何编译的

执行java程序的前提要把类加载出来

.java源代码文件 通过javac 编译成.class 字节码文件使用java命令来运行

JVM就可以执行.class文件(把文件内容读取到内存中  类加载) 解析并且在内存中构造出对应的类对象

wait 和 sleep 的区别

wait需要搭配synchronized使用,而sleep不需要

wait 是Object的方法,而sleep是Thread的静态方法