Day22-多线程
1. 多线程
1.1 基本概念:程序、进程、线程
程序(program)是为完成特定任务、用某种语言编写的一组指令的集合。即指一 段静态的代码,静态对象。
进程(process)是程序的一次执行过程,或是正在运行的一个程序。是一个动态
的过程:有它自身的产生、存在和消亡的过程。——生命周期
如:运行中的QQ,运行中的MP3播放器
程序是静态的,进程是动态的
进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域
线程(thread),进程可进一步细化为线程,是一个程序内部的一条执行路径。
若一个进程同一时间并行执行多个线程,就是支持多线程的
线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开
销小
一个进程中的多个线程共享相同的内存单元/内存地址空间à它们从同一堆中分配对象,可以 访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资 源可能就会带来安全的隐患。
示意图:
1.2 单核CPU和多核CPU的理解
a) 单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程 的任务。例如:虽然有多车道,但是收费站只有一个工作人员在收费,只有收了费 才能通过,那么CPU就好比收费人员。如果有某个人不想交钱,那么收费人员可以 把他“挂起”(晾着他,等他想通了,准备好了钱,再去收费)。但是因为CPU时 间单元特别短,因此感觉不出来。
b) 如果是多核的话,才能更好的发挥多线程的效率。(现在的服务器都是多核的)
c) 一个Java应用程序java.exe,其实至少有三个线程:main()主线程,gc() 垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。
1.3 并行与并发
并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事1
比如可以同时处理所有请求,就是并行,并且并行必须在多处理器中(大于等于2)
不能同时处理多个请求,需要间隔切换执行,就是并发(单处理器和多处理器都可以存在)
注意 : 单线程其实是比多线程效率高的,因为多线程需要线程切换,比较耗费资源,只不过多线程给用户的响应时间要快
1.4 使用多线程优点
背景:以单核CPU为例,只使用单个线程先后完成多个任务(调用多个方 法),肯定比用多个线程来完成用的时间更短,为何仍需多线程呢?
多线程程序的优点:
- 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
- 提高计算机系统CPU的利用率
- 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和
修改
1.5 何事需要多线程
程序需要同时执行两个或多个任务。
程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜 索等。
需要一些后台运行的程序时。
1.6 创建方式
1.6.1 thread
创建线程有两种方式,但是启动线程只有一种方式,就是调用线程对象中的start()方法
第一种 : 继承Thread类 并覆写 run()方法
Java语言的JVM允许程序运行多个线程,它通过java.lang.Thread
类来体现。
Thread类的特性
每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常 把run()方法的主体称为线程体
通过该Thread对象的start()方法来启动这个线程,而非直接调用run()
构造器
Thread():创建新的Thread对象
Thread(String threadname):创建线程并指定线程实例名
Thread(Runnable target):指定创建线程的目标对象,它实现了Runnable接
口中的run方法
Thread(Runnable target, String name):创建新的Thread对象
创建过程
继承Thread类
1)定义子类继承Thread类。
2)子类中重写Thread类中的run方法。
3)创建Thread子类对象,即创建了线程对象。
4)调用线程对象start方法:启动线程,调用run方法。
package com.demo._Thread;
/**
* 创建线程有两种方式,但是启动线程只有一种方式,就是调用线程对象中的start()方法
*
* 第一种 : 继承Thread类 并覆写 run()方法
*/
public class Thread_01_Create {
public static void main(String[] args) {
// 创建线程对象
Thread t = new Processor();
// 启动,会自动执行run()方法,但是注意 我们不能手动调用run()方法
t.start();
for (int i = 0; i < 10; i++) {
System.out.println("main线程 : " + i);
}
/**
* main线程 : 0
测试线程 : 0
测试线程 : 1
测试线程 : 2
测试线程 : 3
测试线程 : 4
测试线程 : 5
测试线程 : 6
测试线程 : 7
测试线程 : 8
main线程 : 1
main线程 : 2
main线程 : 3
main线程 : 4
测试线程 : 9
main线程 : 5
main线程 : 6
main线程 : 7
main线程 : 8
main线程 : 9*/
}
}
class Processor extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("测试线程 : " + i);
}
}
}
1.6.2 Runnable
第二种创建线程的方式 : 实现Runable接口并覆写run()方法
启动 还是使用线程对象中的start()方法
package com.demo._Thread;
/**
* 第二种创建线程的方式 : 实现Runable接口并覆写run()方法
*
* 启动 还是使用线程对象中的start()方法
*
*/
public class Thread_02_Create {
public static void main(String[] args) {
// 创建线程对象
Thread thread = new Thread(new Processor_01());
// 启动
thread.start();
for (int i = 0; i < 10; i++) {
System.out.println("main线程 : " + i);
}
/**
* main线程 : 0
main线程 : 1
main线程 : 2
main线程 : 3
main线程 : 4
main线程 : 5
测试线程 : 0
测试线程 : 1
测试线程 : 2
测试线程 : 3
测试线程 : 4
测试线程 : 5
main线程 : 6
测试线程 : 6
测试线程 : 7
测试线程 : 8
测试线程 : 9
main线程 : 7
main线程 : 8
main线程 : 9
* */
}
}
class Processor_01 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("测试线程 : " + i);
}
}
}
1.6.3 继承和实现的区别
继承方式和实现方式的联系与区别
public class Thread extends Object implements Runnable
区别
继承Thread:线程代码存放Thread子类run方法中。
实现Runnable:线程代码存在接口的子类的run方法。
实现方式的好处
避免了单继承的局限性
多个线程可以共享同一个接口实现类的对象,非常适合多个相同线 程来处理同一份资源。
1.7常用方法
start() : 启动线程唯一方式
setName( ) : 设置线程的名字,默认是 Thread-0 , Thread-1 … 以此类推
getName( ) : 获取线程的名字
setPriority() : 设置线程优先级
getPriority() : 获取线程优先级
static currentThread() : 获取当前线程的内存地址(获取当前线程对象)
static sleep() : 让当前线程进入睡眠,参数是毫秒数
1.8 线程优先级
1.8.1 时间片分配
Java的调度方法
同优先级线程组成先进先出队列(先到先服务),使用时间片策略
对高优先级,使用优先调度的抢占式策略
1.8.2 优先级使用
优先级 : java中分为10个等级,分别是1-10
另外在Thread类中还提供了三个常量,分别保存1,5,10 优先级
最高 : 10 Thread.MAX_PRIORITY
正常 : 5 Thread.NORM_PRIORITY
最低 : 1 Thread.MIN_PRIORITY
getPriority() : 获取该线程优先级
setPriority() : 设置该线程优先级
线程创建时继承父线程的优先级Thread的优先级是5
低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用
package com.demo._Thread;
/**
* 优先级 : java中分为10个等级,分别是1-10
*
* 另外在Thread类中还提供了三个常量,分别保存1,5,10 优先级
*
* 最高 : 10 Thread.MAX_PRIORITY
* 正常 : 5 Thread.NORM_PRIORITY
* 最低 : 1 Thread.MIN_PRIORITY
*
* getPriority() : 获取该线程优先级
*
* setPriority() : 设置该线程优先级
*
线程创建时继承父线程的优先级 Thread的优先级是5
低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用,
概率性的问题
*/
public class Thread_03_Priority {
public static void main(String[] args) {
Thread thread = new Processer_02();
// 设置 thread线程对象的 优先级为10
thread.setPriority(Thread.MAX_PRIORITY);
thread.start();
// currentThread : 静态方法,获取当前线程对象
// 设置main方法优先级为1
Thread.currentThread().setPriority(1);
for (int i = 0; i < 10; i++) {
System.out.println("main线程 : "+i);
}
/**
* main线程 : 0
测试线程 : 0
main线程 : 1
测试线程 : 1
测试线程 : 2
测试线程 : 3
测试线程 : 4
main线程 : 2
测试线程 : 5
main线程 : 3
测试线程 : 6
main线程 : 4
测试线程 : 7
测试线程 : 8
测试线程 : 9
main线程 : 5
main线程 : 6
main线程 : 7
main线程 : 8
main线程 : 9
*/
}
}
class Processer_02 extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("测试线程 : "+i);
}
}
}
1.9 生命周期
1.9.1 概述
创建 --> 就绪 --> 运行 --> 阻塞 --> 复活 --> 阻塞… --> 死亡
JDK中用Thread.State类定义了线程的几种状态
要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类 及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五 种状态:
新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建 状态
就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线 程的操作和功能
阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
1.9.2 代码实例
package com.demo._Thread;
/**
* 线程 生命周期
*
* 创建 , 就绪 , 运行 , 阻塞 (就绪,运行,阻塞.....), 死亡
*
* static currentThread() : 静态方法,用于获取当前线程的对象,写在哪个线程中,就获取哪个线程的对象
*
* static sleep(long m) : 静态方法,让当前线程睡眠,需要传入对应的毫秒数,写在哪个线程就睡眠哪个线程,起到阻塞
*/
public class Thread_04_Lifecycle {
public static void main(String[] args) {
// 创建
Thread t1 = new Thread(new Processer());
Thread t2 = new Thread(new Processer());
// t1和t2在创建对象时,就已经生成名字,Thread-0和Thread-1
// 所以 就算此时把t1线程名字改为t1 , 那么 t2线程的名字 还是Thread-1
t1.setName("t1");
t2.setName("t2");
// 启动线程,进入就绪
t1.start();
t2.start();
for (int i = 0; i < 10; i++) {
// 静态方法,获取当前线程对象
System.out.println(Thread.currentThread().getName() + " : " + i);
try {
// 睡眠1秒,进入阻塞
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Processer implements Runnable {
@Override
public void run() {
// 运行
for (int i = 0; i < 10; i++) {
// 获取当前线程对象,并调用getName
// 由于没有继承Thread类 所以不能使用this直接调用Thread相对应的方法,因为没有继承了
// 所以需要获取当前线程对象,再调用
System.out.println(Thread.currentThread().getName() + " : " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
1.10 线程控制
Interrupt() : 强制唤醒某一个线程
1.10.1 Interrupt
sleep() 让当前线程睡眠
唤醒睡眠的线程有两种方式
1 正常唤醒 : 就是睡眠时间到了
2 异常唤醒 : 强制打断睡眠,会报异常
interrupt : 强制唤醒某个线程
package com.demo._Thread;
/**
* sleep() 让当前线程睡眠
*
* 唤醒睡眠的线程有两种方式
* 1 正常唤醒 : 就是睡眠时间到了
* 2 异常唤醒 : 强制打断睡眠,会报异常
*
* interrupt : 强制唤醒某个线程
*/
public class Thread_05_Interrupt {
public static void main(String[] args) {
Thread t = new Processer_03();
t.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 1秒后 唤醒t线程
t.interrupt();
/**
* 强制唤醒线程,可能导致系统分配的资源不对
* java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
叮叮叮~闹钟响了
开始上班
at com.demo._Thread.Processer_03.run(Thread_05_Interrupt.java:30)*/
}
}
class Processer_03 extends Thread{
@Override
public void run() {
try {
Thread.sleep(300000);
System.out.println("哎呀,睡醒了");
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("叮叮叮~闹钟响了");
}
System.out.println("开始上班");
}
}
1.10.3 yield
Thread.yeild : 暂停当前正在执行的线程,并执行其他线程
1 静态方法,意味着 写在哪个线程 , 哪个线程就让位
2 给相同的优先级线程让位,不同优先级不让位
3 该当前线程执行的执行的时候(拿到了时间片),把该执行机会(时间片) 让给其他线程
package com.demo._Thread;
/**
* Thread.yeild : 暂停当前正在执行的线程,并执行其他线程
*
* 1 静态方法,意味着 写在哪个线程 , 哪个线程就让位
*
* 2 给相同的优先级线程让位,不同优先级不让位
*
* 3 该当前线程执行的执行的时候(拿到了时间片),把该执行机会(时间片) 让给其他线程
*/
public class Thread_07_Yield {
public static void main(String[] args) {
Thread t1 = new Processor_05();
t1.setName("t1");
t1.start();
for (int i = 0; i < 10; i++) {
System.out.println("main = "+i);
}
}
}
class Processor_05 extends Thread {
@Override
public void run() {
for (int i = 0; i < 10 ; i++) {
// 让位 暂停当前正在执行的线程,并执行其他线程
Thread.yield();
// 由于继承的Thread类 所以可以直接调用getName方法
System.out.println(getName()+" = "+i);
}
}
}
1.10.4 stop
stop : 终止一个线程,容易出现问题,不太安全,不推荐使用
可以使用标识符的形式来结束
package com.demo._Thread;
/**
* stop : 终止一个线程,容易出现问题,不太安全,不推荐使用
*
* 可以使用标识符的形式来结束
*/
public class Thread_08_Stop {
public static void main(String[] args) {
Processor_06 t1 = new Processor_06();
t1.setName("t1");
t1.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 3秒后 终止 t1 线程
// 使用stop终止,容易出现问题
// t1.stop();
// 推荐使用标识符解决
t1.isStop = true;
}
}
class Processor_06 extends Thread {
// 如果是true 就终止线程
boolean isStop = false;
@Override
public void run() {
for (int i = 0; true; i++) {
// 判断是否终止
if (isStop) {
return;
}
System.out.println(Thread.currentThread().getName() + " = " + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
1.11 线程同步机制
1.11.1 概述
问题的提出
多个线程执行的不确定性引起执行结果的不稳定
多个线程对账本的共享,会造成操作的不完整性,会破坏数据
线程同步 :
当多个线程操作同一个数据的时候,为了保证数据的一致
线程同步本质就是数据同步,是一种安全机制
异步编程 :
线程之间是完全独立的,谁的运行也不会受到别的线程的影响
同步编程 :
线程之间不是独立的,相互之间是有影响的,某个功能必须必只能让一个线程同时执行,主要为了数据安全
同步的原因 :
1 数据同步,为了数据安全,某种情况来讲,同步可以理解为暂时把多线程转换为单线程
2 什么时候需要同步
1 必须多线程(单线程没有啥并发和冲突的情况)
2 多个线程有可能同时操作同一个数据的可能性
3 主要是数据的更改操作
只要对方法加上 synchronized的成员方法,就代表该方法不能被多个线程同时访问
锁是每个对象都有的,synchronized只是把锁锁住的一个持续动作,而多个线程必须保存同一个对象,才能使用同一把锁,才能相互排斥,才能保证数据安全
如果多个线程之间保存的不是同一个对象,尽管是同一个类的不同对象,也是没有办法相互排斥的
1.11.2 示例,暴露问题
package com.demo._Thread;
/**
* 线程同步 : 当多个线程操作同一个数据的时候,为了保证数据的一致 线程同步本质就是数据同步,是一种安全机制
*
* 异步编程 : 线程之间是完全独立的,谁的运行也不会受到别的线程的影响
*
* 同步编程 : 线程之间不是独立的,相互之间是有影响的,某个功能必须必只能让一个线程同时执行,主要为了数据安全
*
* 同步的原因 : 1 数据同步,为了数据安全,某种情况来讲,同步可以理解为暂时把多线程转换为单线程 2 什么时候需要同步 1
* 必须多线程(单线程没有啥并发和冲突的情况) 2 多个线程有可能同时操作同一个数据的可能性 3 主要是数据的更改操作
*
* 只要对方法加上 synchronized的成员方法,就代表该方法不能被多个线程同时访问
*
* 锁是每个对象都有的,synchronized只是把锁锁住的一个持续动作,而多个线程必须保存同一个对象,才能使用同一把锁,才能相互排斥,才能保证数据安全
*
* 如果多个线程之间保存的不是同一个对象,尽管是同一个类的不同对象,也是没有办法相互排斥的
*
*/
public class Thread_09_Synchronization {
public static void main(String[] args) {
// 创建账户对象,余额为5000
Account account = new Account(5000);
// 两个线程对象保存同一个账户对象
Thread t1 = new Thread(new Processor_07(account));
Thread t2 = new Thread(new Processor_07(account));
t1.setName("t1");
t2.setName("t2");
t1.start();
t2.start();
}
}
// 线程类
class Processor_07 implements Runnable {
// 保存账户 对象
private Account account;
public Processor_07(Account account) {
this.account = account;
}
@Override
public void run() {
account.withDraw(1000);
System.out.println(Thread.currentThread().getName()
+ " 取款成功,取款1000元,余额是 : " + account.getBalance());
}
}
// 实体类
class Account {
private double balance;
public Account(double balance) {
this.balance = balance;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
/**
* 取钱方法
*
* @param money
*/
public synchronized void withDraw(double money) {
double after = balance - money;
// System.out.println(Thread.currentThread().getName()+" 我来取钱啦~");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance = after;
}
}
不加synchronized (同步)
1.11.3 解决方案
Java对于多线程的安全问题提供了专业的解决方式:同步机制
1.同步代码块:
synchronized (对象){
// 需要被同步的代码;
}
2.synchronized还可以放在方法声明中,表示整个方法为同步方法。
例如:
public synchronized void show (String name){
….
}
1.12 Lock
1.12.1 概述
从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同
步锁对象来实现同步。同步锁使用Lock对象充当。
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的 工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象 加锁,线程开始访问共享资源之前应先获得Lock对象。
ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和 内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以 显式加锁、释放锁。
1.12.2 使用
package com.demo._Thread;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Lock : 可以锁代码块,并且性能较好,不用频繁的去调度线程
* 并且开启加锁和关闭锁 都是需要手动的 又称为 显示锁
* 而 synchronized是隐式锁,超出作用域 自动解锁
*
1.Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是 隐式锁,出了作用域自动释放
2.Lock只有代码块锁,synchronized有代码块锁和方法锁
3.使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
优先使用顺序:
Lock 同步代码块(已经进入了方法体,分配了相应资源)
*
*/
public class Thread_10_Lock {
public static void main(String[] args) {
ATM atm = new ATM(5000);
Thread t1 = new Thread(new Processor_08(atm));
Thread t2 = new Thread(new Processor_08(atm));
t1.setName("t1");
t2.setName("t2");
t1.start();
t2.start();
/**
* t2 进来了
t1 进来了
t2取钱成功,取钱1000.0,剩余4000.0元
t1取钱成功,取钱1000.0,剩余3000.0元*/
}
}
//线程类
class Processor_08 implements Runnable {
// 保存账户 对象
private ATM atm;
public Processor_08(ATM atm) {
this.atm = atm;
}
@Override
public void run() {
atm.withDraw(1000);
}
}
class ATM {
private double balance;
// 锁
private Lock lock = new ReentrantLock();
public ATM(double balance) {
super();
this.balance = balance;
}
public void withDraw(double money) {
System.out.println(Thread.currentThread().getName() + " 进来了");
// ---- 非访问数据区域,不需要同步
// 开启锁 , 数据同步
lock.lock();
try {
double after = balance - money;
balance = after;
System.out.println(Thread.currentThread().getName() + "取钱成功,取钱"
+ money + ",剩余" + balance + "元");
}finally{
// 解锁
lock.unlock();
}
// ---- 非访问数据区域,不需要同步
}
}
1.12.3 synchronized 与 Lock的对比
1.Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是 隐式锁,出了作用域自动释放
2.Lock只有代码块锁,synchronized有代码块锁和方法锁
3.使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有
更好的扩展性(提供更多的子类)
优先使用顺序:
Lock 同步代码块(已经进入了方法体,分配了相应资源) à 同步方法
(在方法体之外)