个人学习笔记,部分图片资源来自学习文件。
文章首部
一、基本概念
1. 程序
程序(program)是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
2. 进程
进程(process)是程序的一次执行过程,或是正在运行的一个程序。是一个动态
的过程:有它自身的产生、存在和消亡的过程。——生命周期
- 如:运行中的Q Q,运行中的MP 3播放器。
- 程序是静态的,进程是动态的。
- 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。
3. 线程
线程(thread),进程可进一步细化为线程,是一个程序内部的一条执行路径。
- 若一个进程同一时间并行执行多个线程,就是支持多线程的。
- 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(P C),线程切换的开
销小。 - 一个进程中的多个线程共享相同的内存单元/内存地址空间 → 它们从同一堆中分配对象,可以
访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。
4.单核CPU和多核CPU
- 单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程
的任务。 - 多核CPU,才能更好的发挥多线程的效率。(现在的服务器都是多核的)
- 一个Java应用程序 java.exe,至少有三个线程:main()主线程,gc()
垃圾回收线程,异常处理线程。(当然如果发生异常,会影响主线程。)
5.并行与并发
- 并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
- 并发:一个CPU(采用时间片)同时(快速切换)执行多个任务。比如:一个人做同一件事。
6.多线程的优点
多线程的引入背景:以单核CPU为例,只使用单个线程先后完成多个任务(调用多个方
法),肯定比用多个线程来完成用的时间更短,为何仍需多线程呢?
- 多线程的优点:
- 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
- 提高计算机系统CPU的利用率
- 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和
修改。
7.多线程的使用情景
- 程序需要同时执行两个或多个任务。
- 程序需要实现一些需要等待的任务时,如用户输入、文件读写
操作、网络操作、搜索等。 - 需要一些后台运行的程序时。
二、 线程的创建与使用
1、多线程的创建
方式一:继承于Thread类
- 步骤:
- 1.创建一个继承于Thread类的子类
- 2.重写Thread类中的run() --> 将此线程执行的操作声明在run方法中
- 3.创建Thread子类的对象
- 4.通过此对象调用start()
- start()方法的作用:
- 启动当前线程
- 调用当前线程的run()
- 注意点:
- 不能以在主线程[ main() ]中直接调用run()的方式启动线程
- 同一个线程对象,不能让已经start()再去执行,会报错–IllegalThreadStateException
方式二:实现Runnable接口
- 步骤:
- 1.创建一个实现了Runnable接口(implements)的类
- 2.实现类去实现Runnable中的抽象方法–run()
- 3.创建实现类的对象
- 4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
- 5.通过Thread类的对象调用start()
两种创建方式的比较
实现Runnable接口的方式比较实用,开发中优先选择:
- 其一:实现方式没有Java单继承的局限性
- 其二:更适合来处理多线程的问题,自然实现了数据共享功能
二者的联系:public class Thread implements Runnable
共同点:都重写了run(),将线程将要执行的操作放在run()中
2、Thread类的常用方法
- void start(): 启动线程,并执行对象的run()方法
- run(): 线程在被调度时执行的操作
- run()方法进行异常处理时只能用try-catch,因为run()方法是重写Thread中的方法,原方法无 “ throws + 异常类型 “的结构
- String getName(): 返回线程的名称
- void setName(String name):设置该线程名称
- 给当前线程命名–线程对象.setName("XXX ");
- 通过构造器给线程命名–public 当前线程类名(String name){
super(name);} - 给主线程命名–Thread.currentThread().setName(“主线程”);
- static Thread currentThread(): 返回当前线程。在Thread子类中就
是this,通常用于主线程和Runnable实现类 - this.yield():释放当前CPU的执行权(可能换另一个线程执行)
- 方法(2)
返回目录
三、线程的生命周期
Thread.State类定义的线程的几种状态
要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
- 新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建
状态。 - 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已
具备了运行的条件,只是没分配到CPU资源。 - 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线
程的操作和功能。 - 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中
止自己的执行,进入阻塞状态。 - 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束。
- 图示:
线程状态的转换
四、线程的同步
当多个线程 “ 同时 ”操作共享数据时,可能出现线程A未执行完,线程B、C…就进入操作共享数据(多个线程共同操作的数据),可能导致出现线程安全问题。在 Java 中需要通过线程同步的方式去解决此类问题,但运行效率会变慢(类似单线程)。方式分为同步代码块、同步方法、Lock锁。(建议使用顺序 : Lock锁 > 同步代码块 > 同步方法)
方式一:同步代码块
synchronized(同步监视器){
//需要同步的代码
}
//说明:1、需要同步的代码--:操作共享数据的代码
// 2、同步监视器--:锁(任何一个类的对象,多个线程必须共用一把锁)
同步监视器(锁)的使用
//1、任意创建一个类的对象,再当做锁
Object obj = new Object();// 一定要唯一
synchronized(obj){
//需要同步的代码
}
//2、使用--“ 类.class ” 当做锁
synchronized(类.class){ // 说明:类也是对象
//需要同步的代码
}
//使用“ this ” 当做锁--当实现类的对象唯一时
synchronized(this){ // this--实现类的对象
//需要同步的代码
}
方式二:同步方法
当操作共享数据的代码被完整的包括在同一个方法中,将这个方法声明为同步方法。
//示例:
@Override
public void run() {
while (true){
show();
}
}
//使用同步方法解决实现Runnable接口的线程安全问题
public synchronized void show(){
//方法中的同步监视器(this)没有显式地表达出来
//操作共享数据的代码
}
//使用同步方法解决继承Thread类的线程安全问题
public static synchronized void show(){
//方法中的同步监视器(当前类.class)没有显式地表达出来
//操作共享数据的代码
}
方式三、Lock锁(JDK5.0才有)
//实例化Lock锁
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true){
try{
//调用lock();
lock.lock();
// 需要同步的代码
}finally {
//调用解锁的方法
lock.unlock();
}
}
}
synchronized 与 Lock的不同点
- synchronized机制在执行完需要同步的代码后,自动释放同步监视器
- Lock需要手动的启动同步监视器[.lock()], 也需要手动去结束 [unlock()]
死锁问题
- 死锁
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃
自己需要的同步资源,就形成了线程的死锁 - 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于
阻塞状态,无法继续
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃
- 如何解决:
- 专门的算法、原则
- 尽量减少同步资源的定义
- 尽量避免嵌套同步
五、线程的通信
涉及使用的方法
-
wait():使用调用wait()方法的进程进入阻塞状态,并释放同步监视器; 本身报错,要用try-catch
-
notify():执行此方法,唤醒被阻塞的线程;如果有多个线程被阻塞,唤醒优先级高的线程
-
notifyAll():执行此方法,唤醒所有被阻塞的线程
-
使用说明:
- 三个方法只能在 同步代码块 或 同步方法 内调用
- 三个方法的调用者必须是同步代码块或同步方法中的同步监视器(this 或 某个类的对象)
- 三个方法定义在java.lang.Object类中
示例代码
private int number = 1;
@Override
public void run() {
while (true){
synchronized (this) {
//唤醒阻塞的线程
notify();
//当达到3个或3个以上时,用notifyAll()
if (number <= 100){
System.out.println(Thread.currentThread().getName()
+ " : " + number);
number++;
}else {
break;
}
try {
//使用调用wait()方法的进程进入阻塞状态,释放锁
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
经典例题
生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处
取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图
生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通
知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如
果店中有产品了再通知消费者来取走产品。
例题源代码
六、JDK5.0新增线程创建方式
实现 Callable 接口(功能较Runnable更强大)
步骤
- 1、创建一个实现Callable接口的实现类
- 2、实现call()方法,将此线程需要执行的操作声明在call()方法中,call()可以有返回值
- 3、创建Callable接口实现类的对象
- 4、将此Callable接口实现类的对象作为参数传递到FutureTask的构造器中,创建FutureTask的对象
- 5、将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread类的对象,并start()
- 6、获取Callable中的call()的返回值(可不作表述)
优点
- call()可以有返回值,run()不能有返回值。
- call()可以抛出异常,被外部的操作捕获,获取异常信息,run()不能。
- Callable是支持泛型的。
需要借助Future接口
Future接口:
- 可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
- FutrueTask是Futrue接口的唯一的实现类
- FutureTask 同时实现了Runnable, Future接口。它既可以作为
Runnable被线程执行,又可以作为Future得到Callable的返回值
线程池
优点:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中的线程,不用每次都创建)
- 便于线程管理:
- corePoolSize: 核心池大小
- maximumPoolSize: 最大线程数
- keepAliveTime: 线程没有任务时最多保持多久终止
- ·····
线程池相关API:
ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
- void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行
Runnable - Future submit(Callable task):执行任务,有返回值,一般又来执行
Callable - void shutdown() :关闭连接池
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
- Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
- Executors.newFixedThreadPool(n); 创建一个可重用固定线程数的线程池
- Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
- Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运
行命令或者定期地执行。
步骤:
- 1、提供指定线程数量的线程池
//10个线程的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
- 2、执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
service.execute(new NumberThread());
- 3、关闭线程池
service.shutdown() ;
设置线程池属性
ExecutorService是接口,属性较少,要在实现类ThreadPoolExecutor中进行设置
ExecutorService service = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
//设置线程池属性--体现了便于线程管理
service1.setCorePoolSize(15);
service1.setMaximumPoolSize(10);
te(new NumberThread());
- 3、关闭线程池
service.shutdown() ;
设置线程池属性
ExecutorService是接口,属性较少,要在实现类ThreadPoolExecutor中进行设置
ExecutorService service = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
//设置线程池属性--体现了便于线程管理
service1.setCorePoolSize(15);
service1.setMaximumPoolSize(10);