1、基本概念:程序、进程、线程
程序
- 完成特定任务、用某种语言编写的一组指令的集合。一段静态的代码,静态对象。
进程
- 程序的一次执行过程,或是正在运行的一个程序,动态的过程:产生、存在、和消亡
- 程序是静态的,进程是动态的
- 进程是资源分配的单位
线程
线程是程序内部的一条执行路径
一个进程,同一时间并行多个线程,多线程
线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器
存在安全问题
一个java应用程序,至少有三个线程:main()主线程,
使用多线程的优点
- 提高应用程序的响应,增强用户体验
- 提高计算机系统CPU的利用率
- 改善程序结构,将复杂的进程分为多个线程,独立运行
何时需要多线程
- 程序需要同时执行两个或多个任务
- 程序实现一些需要等待的任务时,如:用户输入,文件读写操作、网络操作
- 需要后台运行的程序
单核CPU和多核CPU
单核CPU
- 假的多线程,在一个时间单元内,只能执行一个线程的任务
- 实际上是单线程,运行中将其他进程挂起,执行,执行速度很快,所以在使用上感觉是多线程。
多核CPU
- 真正意义上的多线程
并行与并发
- 并行:多个CPU同时执行多个任务
- 并发:一个CPU “同时” 执行多个任务,如:秒杀
2、线程的创建和使用
线程的创建
方式一
继承于Thread类
步骤:
- 创建一个继承于Thread的子类
- 重写Thread类的run()方法–》此线程执行的操作
- 创建Thread类的子类对象(可使用匿名对象)
- 通过此对象调用start()
- 若要再创建新的线程,需创建新的实例来调用start()方法
//例:遍历100以内的所有偶数 //1.创建一个继承于Thread类的子类 public class MyThread extends Thread{ //2.重写Thread类的run() @Override public void run() { for (int i = 0; i <=1000 ; i++) { if(i%2==0){ System.out.println(Thread.currentThread().getName()+":"+i); } } } } class ThreadTest { public static void main(String[] args) { //3.创建Thread类的子类的对象 MyThread myThread= new MyThread(); //4.通过此对象调用start()①启动当前线程②调用当前线程的run() myThread.start(); // 如下操作任然是在main()线程执行的 for (int i = 0; i <=1000 ; i++) { if(i%2!=0){ // Thread.currentThread().getName()获取线程名 System.out.println(Thread.currentThread().getName()+":"+i); } } } }
方式二
实现Runnable接口
步骤:
创建一个实现了Runnable接口的类
实现类中实现Runnable中的抽象方法:run()
创建实现类的对象
将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
通过Thread类的对象调用start()
//1. 创建一个实现了Runnable接口的类 class MIThread implements Runnable{ // 2. 实现类中实现Runnable中的抽象方法:run() @Override public void run() { for (int i = 0; i < 100; i++) { if(i%2==0){ System.out.println(Thread.currentThread().getName()+":"+i); } } } } class ThreadTes{ public static void main(String[] args) { // 3. 创建实现类的对象 MIThread mi= new MIThread() ; // 4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象 Thread t1=new Thread(mi); // 5. 通过Thread类的对象调用start(),启动当前线程,调用当前线程的run方法,底层是Thread含有Runnable类型参数的构造器 t1.start(); // 再启动一个线程 Thread t2=new Thread(mi); t2.start(); } }
比较两种创建方式
开发中优先选择,实现Runnable接口的方式
原因:
- 实现的方式没有类的单继承的局限
- 实现方式更适合处理多线程有共享数据的情况
联系: public class Thread implements Runnable
相同点:两种方式都需要重写run() 将线程 要执行的逻辑声明在run()中;
线程的调度
- 调度策略
- 时间片
- 抢占式,高优先级的线程抢占CPU
- java的调度方法
- 先来先服务,先进先出队列
- 高优先级,优先调度的抢占式策略
线程优先级
优先级
- MAX_PRIORITY : 10
- MIN_PRIORITY: 1
NORM_PRIORITY : 5
获取和设置当前线程的优先级
- getPriority()
- setPriority( int p)
高优先级的线程要抢占低优先级线程的CPU执行权,只是从概率上讲,高优先级有很高概率会执行,但并不意味着高优先级的线程执行完,低优先级的才执行;
3、线程的生命周期
线程生命周期的五种状态
新建:Thread类或子类对象被声明创建时
就绪:新建的线程被start()后,获取了其他所有的资源,只是没分配到CPU资源
运行:获取CPU资源,执行
阻塞:某种特殊情况下,让出CPU并临时终止自己的执行
死亡:完成它的全部工作或线程提前被强制性的终止,异常导致结束;
状态图
4、线程同步
多个线程操作共享数据时,出现的线程安全问题
解决方式:
方式一:同步代码块—不能包含多了,也不能包含少了
synchronized(同步监视器){ //需要被同步的代码 }
说明:操作共享数据的代码==需要被同步的代码
共享数据:多个线程共同操作的变量同步监视器:锁,任何一个类的对象都可充当为锁
要求:多个线程必须要共用同一把锁
对于继承Thread类创建多线程方法
- 同步监视器可以考虑使用定义静态类对象
- 可以考虑使用当前类对象synchronized (类名.class)
对于实现Runnable接口创建多线程方法
- 同步监视器可以考虑使用 this
方式二:同步方法: 如果操作共享数据的代码完整的声明在一个方法中,将此方法声明为同步的
private synchronized void show(){ //同步监视器 this } private static synchronized void show(){ //同步监视器:当前类本身 }
同步方法仍然涉及到同步监视器,只是不需要显式的声明
非静态的同步方法,同步监视器是:this
静态的同步方法,同步监视器是:当前类本身
方式三:Lock(锁) jdk5.0新增
实例化ReentrantLock
调用lock()方法
调用解锁方法unlock()
package com.han.java.ere; import java.util.concurrent.locks.ReentrantLock; /** * 解决线程安全的方式三:lock--- jdk5.0新增 * @author hlrstart * @create 2021-04-04-9:35 */ public class LockTest { public static void main(String[] args) { Windw win=new Windw(); Thread t1=new Thread(win); Thread t2=new Thread(win); Thread t3=new Thread(win); t1.setName("窗口一"); t2.setName("窗口二"); t3.setName("窗口三"); t1.start(); t2.start(); t3.start(); } } class Windw implements Runnable{ private int sticket=100; // 1.实例化ReentrantLock private ReentrantLock lock=new ReentrantLock() ; @Override public void run() { while(true){ try { // 2.调用lock()方法 lock.lock(); if (sticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ": 票号为" + sticket); sticket--; } else { break; } }finally { // 3.调用解锁方法 lock.unlock(); } } } }
同步的方式优缺点
- 优点:解决了线程安全问题
- 局限性:操作同步代码时,是单线程的过程,效率低
线程死锁问题
不同的线程分别占用对方需要的资源不放弃,都在等待对方释放自己需要的资源。
出现死锁,不会出现异常,不会出现提示,所有线程都处于阻塞状态,无法继续。
5、线程通信
涉及到的三个方法
wait():一旦执行此方法,当前线程进入阻塞状态,并释放同步监视器
notify():一旦执行此方法,就会唤醒被wait()的一个线程,若有多个线程被wait(),就唤醒优先级高的那一个
notifyAll() :一旦执行此方法,就会唤醒所有被wait()的线程
说明
- wait()、notify()、notifyAll() 三个方法必须使用在同步代码块或同步方法中。
- wait()、notify()、notifyAll() 三个方法的调用者必须是同步代码块或同步方法中的同步监视器,否则会出现异常。
- wait()、notify()、notifyAll() 三个方法是定义在java.lang.Object类中。
6、JDK5.0新增线程创建方式
新增方式一:实现Callable接口
与使用Runnable相比,Callable功能更强大些
- call()方法可以有返回值
- call()方法可以抛出异常
- 支持泛型的返回值
- 需要借助FutureTask类,获取返回结果
实现步骤
- 创建一个实现Callable的实现类
- 实现call方法,将此线程需要执行的操作声明在call()中
- 创建Callable接口实现类的对象
- 将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask 的对象
- 将FutureTask 的对象作为参数传递到Thread类的构造器中,创建Thread对象,调用start()
- 获取Callable中call方法的返回值
package com.han.createThread; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; /** * 创建线程的方式三:实现Callable接口 ---jdk5.0 新增 * * @author hlrstart * @create 2021-04-04-11:24 */ //1.创建一个实现Callable的实现类 class NumThread implements Callable{ // 2. 实现call方法,将此线程需要执行的操作声明在call()中 @Override public Object call() throws Exception { int sum=0; for (int i = 1; i <=100 ; i++) { if(i%2==0){ System.out.println(i); sum+=i; } } return sum; } } public class ThreadNEw { public static void main(String[] args) { // 3.创建Callable接口实现类的对象 NumThread num=new NumThread(); //4.将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask 的对象 FutureTask f=new FutureTask(num); //5.将FutureTask 的对象作为参数传递到Thread类的构造器中,创建Thread对象,调用start() new Thread(f).start(); try { // 6. get()方法的返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值 Object sum= f.get(); System.out.println("总和为:"+sum); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } }
新增方式二:使用线程池(开发中使用)
思想:提前创建好多个线程,放入线程池,使用时直接获取,使用完放回池中。
好处:
- 提高响应速度(减少创建新线程的时间)
- 降低资源消耗(重复利用)
- 便于线程管理
实现步骤
提供指定线程数量的线程
执行指定的线程的操作,需要提供实现Runnable接口或Callable接口实现类的对象
class Num implements Runnable{ @Override public void run() { for (int i = 0; i <=100; i++) { if(i%2==0){ System.out.println(Thread.currentThread().getName()+":"+i); } } } } public class ThreadPool { public static void main(String[] args) { //1.提供指定线程数量的线程 ExecutorService service = Executors.newFixedThreadPool(10); //2.执行指定的线程的操作,需要提供实现Runnable接口或Callable接口实现类的对象 service.execute(new Num()); //适用于Runnable // service.submit(Callable callable); //适用于callable } }
7、涉及到的面试题
sleep()和wait()的异同?、
相同点:
- 执行该方法,都可以使当前线程进入阻塞状态
不同点:
- 两个方法的定义位置不同:Thread类中定义sleep(),Object类中定义wait()
- 调用的要求不同:sleep()可以在任何需要的场景下调用,wait()必须使用在同步代码块或同步方法中
- 是否会释放同步监视器:若两个方法都使用在同步代码块或同步方法中,sleep()不会释放(例如睡眠中,厕所门依然没有打开),wait()会释放
面试题:synchronized 与Lock的异同
相同:
- 都可以解决线程安全问题
不同点:
synchronized机制在执行完同步代码之后,自动释放同步监视器
Lock需要手动启动同步(lock()),同时结束同步也需要手动的实现(unlock())