一、概念
程序、进程、线程的基本概念
- 程序:为了完成特定任务,用某种语言编写的一组指令集。一段静态的代码块,静态对象
- 进程:正在运行的一个程序,相当于是一个程序执行的生命周期过程。
- 线程:进程内的一条执行路线,拥有独立的运行栈和程序计数器(pc)
单线程与多线程的区别
- 单线程:
- 相当于程序运行时只有一条主线流程
- 流式程序速度上运行快
- 多线程
- 相当于程序运行时有多条主线流程运行
- 提高应用程序的响应
- 提高CPU的利用率
- 改善程序结构,将复杂的进程拆分为多个线程运行
二、使用
Java的一门支持多线程的编程语言,通过java.lang.Thread类来体现使用
线程的创建
JDK1.5之前创建新执行线程有两种方法
- 继承Thread类的方式
- 定义子类继承Thread类
- 子类中重写Thread类中的run方法
- 创建Thread子类对象,即创建了线程对象
- 调用线程对象start方法:启动线程,调用run方法
- 实现Runnable接口的方式
- 定义子类,实现Runnable接口
- 子类中重写Runnable接口中的run方法
- 通过Thread类含参构造器创建线程对象
- 将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中
- 调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法
注意:一个线程对象只能调用一次start()方法启动
继承方式和实现方式
- 继承方式
- Java中只能单继承,之后的代码重构会有很大弊端
- 继承之后可以直接使用Thread的API方法
- 线程使用各自对象数据的共享性差,要通过static来实现耗费内存空间
- 实现方式
- Java中接口的多继承,代码重构方便
- 对于Thread中API的调用要通过 Thread.currentThread方式
- 线程使用同一个对象数据间共享性方便。
Java中的线程分为两类:一种是 守护线程,一种是 用户线程。
- 它们在几乎每个方面都是相同的,唯一的区别是判断JVM何时离开
- 守护线程是用来服务用户线程的,通过在start()方法前调用
- thread.setDaemon(true)可以把一个用户线程变成一个守护线程
- Java垃圾回收就是一个典型的守护线程
- 若JVM中都是守护线程,当前JVM将退出
Java中线程的生命周期
线程的同步(synchronized)
同步代码块和同步方法块
对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以
参与执行
同步锁的范围
- 如何 找问题 , 即代码是否存在线程安全 ? ( 非常重要 )
- 明确哪些代码是多线程运行的代码
- 明确多个线程是否有共享数据
- 明确多线程运行代码中是否有多条语句操作共享数据
- 如何解决呢 ? ( 非常重要 )
- 对多条操作共享数据的语句,只能让一个线程都执行完
- 在执行过程中,其他线程不可以参与执行
- 所有操作共享数据的这些语句都要放在同步范围中
- 切记 :
范围太小:没锁住所有有安全问题的代码
范围太大:没发挥多线程的功能。
释放锁的操作
- 当前线程的同步方法、同步代码块执行结束
- 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行
- 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束
- 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁
不会释放锁的操作
- 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行
- 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)。
- 应尽量避免使用suspend()和resume()来控制线程
线程死锁
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
Lock(锁)
- 从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具
- ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的ReentrantLock,可以显式加锁、释放锁
synchronized与Lock
- Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
- 优先使用顺序:
Lock 同步代码块(已经进入了方法体,分配了相应资源) 同步方法(在方法体之外)
wait()、notify()、notifyAll()
-
在Object类中声明
-
wait():令当前线程挂起并放弃CPU、同步资源并等待,使别的线程可访问并修改共享资源,而当前线程排队等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行
-
notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待
-
notifyAll ():唤醒正在排队等待资源的所有线程结束等待.
JDK5.0 新增线程创建方式
方式一:实现Callable接口
Callable与Runnable相比功能更强大些
- 相比run()方法,可以有返回值
- 方法可以抛出异常
- 支持泛型的返回值
- 需要借助FutureTask类,比如获取返回结果
- 可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
- FutrueTask是Futrue接口的唯一的实现类
- FutureTask 同时实现了Runnable, Future接口。它既可以作为
- Runnable被线程执行,又可以作为Future得到Callable的返回值
方式二:线程池
概念:
jdk1.5之后,jdk内置了线程池。一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
优势:
- 降低资源消耗
- 提高响应速度
- 提高线程的可管理性
使用:
java.util.concurrent.Executor
接口是线程池的顶级接口,但是严格意义上讲Executor
并不是一个线程池,而只是一个执行线程的工具。
java.util.concurrent.ExecutorService
才是真正的线程池接口,它继承了Executor
接口。
java.util.concurrent.Executors
线程工厂类,里面提供了一些静态工厂,用于生成一些常用的线程池。官方建议使用Executors工程类来创建线程池对象。
四种线程池:
- newFixedThreadPool:
- 创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。
- newCachedThreadPool:
- 创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。
- newScheduledThreadPool :
- 可安排在给定延迟后运行命令或者定期地执行的线程池
- newSingleThreadExecutor:
- 单例线程池(这个线程池只有一个线程),这个线程池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去