Java的多线程
一、线程的基本概念
1.1 定义
引入线程: 打开计算中的任务管理器,有很多条目,每一条目对应一个应用程序,这个应用程序我们称之为 “进程” ,每一个进程都占用CPU资源和内存, 在这一个进程中 包含多个任务,他们可以“同时”运行, 这里的每一个任务称为”线程“
如果将Java的 应用程序比作一个进程,那么它包含的多个执行流程就是一个 线程。
生活中的多线程: 你现在正在玩游戏 ,你可以一边聊天(互喷),你也可以操控游戏,还可以问候队友。玩游戏就是一个进程,你的不同的操作对于游戏本身就是一个单线程,如果你可以同时操作,就是游戏可支持 多线程。
进程:进程是计算机中独立的应用程序,进程是动态,可运行的
线程:在进程中运行的单一任务,是进程的子程序
程序: 程序是数据描述和操作代码的集合,它是完成某个功能的代码,它是静态的
多线程: 一个进程中的多个子任务, 在多线程中会出现资源抢占问题, 在单核CPU下同一时间点某个进程下只能有一个线程运行。线程与线程之间会互抢资源
CPU资源分配
电脑可以运行多个应用程序(多进程),在同一时间片,CPU只能执行某一个任务,由于时间片切换非常快,你根本不能察觉会出现“等待”的情况,如果电脑出现 “卡死” 你可以任务资源没有获取并正在等待中。
单线程运行流程:程序只有一条运行线路,从开始到结束保持一致
多线程:可以有多条结束任务,对于那一条先结束无法预知
如何创建多线程的程序呢?
方式一: 继承Thread类
a、 定义一个类继承Thread类,重写run方法
b、创建该类的对象, 并调用start方法
public class MyThread extends Thread {
@Override
public void run() {
for(int i=0;i<100;i++){
//获取当前线程名
System.out.println(this.getName()+"----"+i);
}
}
}
public static void main(String[] args) {
// 创建线程对象
MyThread my = new MyThread();
//开启线程
my.start();
for(int i = 0 ;i < 100 ;i ++){
System.out.println("主线程的 i-----"+i);
}
// 结论: 对于多线程之间它们的执行过程会存在资源抢占,谁先获得cpu资源,谁就执行
}
方式二:实现Runnable接口
a、创建一个类实现一个接口
public class MyThread2 implements Runnable {
@Override
public void run() {
for(int i = 0;i<100;i++){
//获取当前 线程的线程名
System.out.println(Thread.currentThread().getName()+"----"+i);
}
}
}
b、借助Thread类开启线程
public static void main(String[] args) {
// 由于 MyThread2 与线程无关联,需要借助线程类完成启动
// 创建线程需要执行的任务类
MyThread2 my = new MyThread2();
Thread th = new Thread(my,"线程A");
th.start();
//再启动一个
Thread th2 = new Thread(my,"线程B");
th2.start();
问题:以上两种创建线程的区别?
1、继承方式适用于没有直接父类 ,相对简单 ,是单一继承, 而接口的方式目标类既可以继承类还可以实现其他接口
2、Runnable实现方式适用于 资源共享,线程同步情况。
3、Runnable实现方式并不是线程类,而是实现线程的目标类(Target)
补充: 创建线程并非只有以上两种方式,还可以通过匿名内部的方式创建线程和 线程池的方式。
2、线程的生命周期
生命周期定义
线程从创建到销毁的整个过程,称为线程生命周期, 好比人的生命周期就是从出生到去世的整个过程中间会经历的过程包括 出生,长大,变老,离开 都是一个人要经历的。
生命周期的阶段
1、新生状态 : 程序创建该线程(实例化对象)
2、就绪状态(可运行状态) : 当线程对象调用start()方法后 ,可以抢占cpu资源,但不会立马运行run方法
3、运行状态: 当抢占到资源后,立马运行run方法
4、阻塞状态: 在运行过程中,线程遇到阻塞事件(线程休眠,wait ,IO操作,join操作等),变为阻塞状态
5、死亡状态: 线程运行完毕,或异常中断 ,此时CPU资源被释放
二、多线程的常用方法及API
java.lang.Thread 类中提供了大量的相关的方法:
new Thread();
new Thread(name);
new Thread(Runnable,name);
new Thread(Runnable)
常用方法:
getId() :获取线程的唯一标识
getName() :获取线程名
getPriority():获取线程的优先级: 优先级从1-10 , min-priority:1 max-priority:10 norm- priority:5 注意说明优先级高的获取到cpu资源的概率越大,并不是一定会优先执行完成。
currentThread():获取当前线程的对象引用
getState():获取线程的状态 (这是返回线程状态的枚举, NEW:未启动,RUNABLE:可运行 BLOCK:阻塞状态, WAITING:等待状态TIMED-WAITIN: 等待另一个线程执行完)
interrupt():中断这个线程
isInterrupted(): 返回boolean 测试当前线程是否中断
isAlive():该线程是否处于活动状态
isDaemon():判断该线程是否是守护线程
setDaemon():设置该线程是否是守护线程
join() :合并线程,使它变为单线程
sleep(ms) :让当前线程休眠 ,休眠时间到了,自动唤醒
yield(): 让出cpu资源,使当前线程处理可运行状态(可运行状态也随时可以获取cpu资源)
案例1: 测试线程的基本属性
System.out.println("当前主线程:"+Thread.currentThread().getName());
System.out.println("主线程id:"+Thread.currentThread().getId());
//设置主线程的线程级别
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
System.out.println("主线程的线程级别:"+Thread.currentThread().getPriority());
// 创建线程对象
MyThread my = new MyThread();
//设置线程名
my.setName("线程A");
//设置优先级
my.setPriority(10);
my.start();
MyThread my1 = new MyThread();
my1.setName("线程B");
//设置优先级 线程A 获取到资源的概率 大于线程B (大概率线程A优先执行完)
my1.setPriority(1);
//新生态
System.out.println("线程"+my1.getName()+"状态-----"+my1.getState());
my1.start();
//可运行状态(就绪)
System.out.println("线程"+my1.getName()+"状态-----"+my1.getState());
for(int i = 0;i<100;i++){
System.out.println("主线程------"+i);
}
守护线程
案例2: 守护线程
线程类型分为两种 一种是用户线程一种是守护线程,用户线程是执行某一个任务的独立代码 ,守护线程是用于守护用户线程的线程, 它的特点是 当用户线程执行完毕后守护现在自动结束,当用户线程没有执行完, 守护线程也不会停止
操作系统中有守护进程 ,用于操作系统的运行,只有关机进程自动结束,这里守护线程和守护进程类似。
//创建线程对象
DaemonThread daemonThread = new DaemonThread();
//设置该线程为守护线程 守护的是与它并行的线程类 ,当主线程或其他线程执行完毕,守护线程自动结束
// daemonThread.setDaemon(true);
System.out.println("是否是守护线程:"+daemonThread.isDaemon());
daemonThread.start();
for(int i=0;i<100;i++){
System.out.println("主线程i------"+i);
}
活动的线程总数: Thread.activeCount()
线程中断
案例3: 关于终止线程
线程中止就是当线程运行时由于满足特定的条件需要停止运行,此时我们需要考虑如何安全的中止线程这里中止线程提供几个方法
方法1 : 打标记中断法
线程运行1000,当程序达到500时,中止程序
public class ThreadEnd extends Thread {
@Override
public void run() {
boolean isOver=false;
for(int i = 0 ;i<1000;i++){
if(i>=500){
isOver= true;
return ;
}
System.out.println("线程结果i-----------"+i);
}
System.out.println("正常结束");
}
public static void main(String[] args) {
ThreadEnd th = new ThreadEnd();
th.start();
}
}
方法2: 异常中断法
- interrupt() :给线程打一个中断标记,不会立马中断
- interrupted() : 检测线程是否中断,并清除中断标记,返回boolean ,如果线程打标记了,就返回true
- isInterrupted() : 检测线程是否中断,但不清除中断标记, 返回boolean
注意用法: interrupted() : 它所处于的位置,对应于它作用的位置 ,通过线程类名调用
interrupt() 和 isInterrupted() : 使用线程对象调用。
public class Thread1 extends Thread {
@Override
public void run() {
int i =0;
while(true){
System.out.println("线程--------------"+i);
//判断当前线程是否有中断标记 ,但是不清除中断标记
if(this.isInterrupted()){
// 通过抛出异常或 break
System.out.println("当前线程打中断标记,可以停止了");
break;
}
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread1 th = new Thread1();
th.start();
// 休眠一会儿
Thread.sleep(2000);
//给th打中断标记
System.out.println("打标记");
th.interrupt(); //给th打标记
}
3个方法的用法
// Thread.currentThread().interrupt();
// System.out.println("判断当前线程是否打标记 (清除标记):"+ Thread.interrupted());
System.out.println("判断线程是否打标记(不清除标记)"+ Thread.currentThread().isInterrupted());
System.out.println("判断当前线程是否打标记 (清除标记):"+ Thread.interrupted()); // 静态方法
join用法
案例四: join的用法: 合并当前线程 ,使其变为单线程 ,哪个线程调用join方法,就立即将该线程剩下的部分执行完成,再执行其他线程
public class ThreadJoin extends Thread {
@Override
public void run() {
ThreadJoin2 th = new ThreadJoin2();
th.setName("线程C");
th.start();
for(int i=0;i<100;i++){
try {
th.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-----"+i);
}
}
}
public static void main(String[] args) throws InterruptedException {
ThreadJoin threadJoin = new ThreadJoin();
threadJoin.setName("线程A");
threadJoin.start();
// ThreadJoin threadJoin2 = new ThreadJoin();
// threadJoin2.setName("线程B");
// threadJoin2.start();
for(int i=0;i<100 ;i++){
if(i==50){
// 合并线程 (threadJoin线程的所有代码合并到 主线程中,先执行threadJoin线程)
threadJoin.join();
}
// if(i==70){
// threadJoin2.join();
// }
System.out.println("main---"+i);
}
}
sleep用法
案例五: sleep的用法: 用于休闲当前线程 ,休眠时间结束后自动唤醒继续执行,如果同时有多个线程执行 ,如果线程没有同步的情况下,相互休眠不影响,资源被公用。
public static void main(String[] args) {
for(int i =0;i<10;i++){
try {
//让当前线程休眠200毫秒 200毫秒后自动唤醒线程 继续执行
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i);
}
}
class ThreadSleep implements Runnable{
@Override
public void run() {
for(int i =0;i<100;i++){
try {
Thread.sleep(1000); // 当前线程休眠时 不影响其他线程执行
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"----"+i);
}
}
}
ThreadSleep obj = new ThreadSleep();
Thread th1 = new Thread(obj , "线程A");
Thread th2 = new Thread(obj , "线程B");
th1.start();
th2.start();
yield用法
案例六:yield的用法 : 出让cpu, 让当先线程变为可运行状态 ,并也可以继续抢占cpu资源
public static void main(String[] args) {
ThreadYiled th = new ThreadYiled();
th.start();
// yield 让出cpu资源
for(int i = 0;i<100;i++){
if(i==50){
//主线程让cpu
System.out.println("让出cpu");
Thread.currentThread().yield();
}
System.out.println("主线程----"+i);
}
}
三、线程同步
线程并发场景:
在实际开发中,很多时候会出现多个线程同时访问同一个内存空间(变量)的场景,当他们同时对数据进行更新时,可能会出现数据不安全问题 ,例如经典的银行取钱案例
假设有一个账户(Account) ,余额是1100元,有小明和小明的爸爸同时取钱,小明拿着银行卡取ATM机取钱,小明的爸爸拿着存折取柜台取钱 ,他们都需要取1000块,小明在取钱时 系统会判断是否余额大于1000,如果大于,可以取钱,由于取钱需要一个过程,此时正好小明的爸爸也对该账户取1000,由于小明没有完成取钱的操作,卡里的钱还没有及时更新提交,所以小明的爸爸也可以通过系统判断的验证, 余额也大于1000,小明的爸爸也可以取钱,所以现在可能会出现他们两个人都取出1000元,导致账户数据不完整,这就是线程编发导致的问题
使用代码模拟场景
1、先有一个账户 (卡号,余额)
2、取钱的任务 ,由于需要使用同一个账户 ,这里的任务中有一个相同的账户 。
同步的解决办法:
1、将需要操作公共资源的代码增加 “同步锁” (也叫互斥锁)
语法:
synchronized(对象锁){
代码块
}
注意这里的对象锁必须满足 两个线程是同一个对象(同一把锁)
public void run() {
System.out.println("开始取钱了");
// 增加互斥锁,协同步伐 ,这个锁必须是公有的对象
synchronized(account) {
//先判断账户余额是否足够
if (account.getMoney() >= 1000) {
System.out.println(Thread.currentThread().getName() + "可以取钱");
System.out.println(Thread.currentThread().getName() + "正在取钱");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//更新账户余额
account.setMoney(account.getMoney() - 1000);
System.out.println(Thread.currentThread().getName() +
"取到了钱,卡里余额还剩:" + account.getMoney());
} else {
System.out.println("抱歉,卡里余额不足,不能取1000元");
}
}
// 以上代码需要将操作通过资源的代码块 增加同步关键字
}
2、 同步方法
在方法的返回值前面增加 “synchronize” , 此时的锁代表的是当前this对象
public synchronized void get(){
//先判断账户余额是否足够
if (account.getMoney() >= 1000) {
System.out.println(Thread.currentThread().getName() + "可以取钱");
System.out.println(Thread.currentThread().getName() + "正在取钱");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//更新账户余额
account.setMoney(account.getMoney() - 1000);
System.out.println(Thread.currentThread().getName() +
"取到了钱,卡里余额还剩:" + account.getMoney());
} else {
System.out.println("抱歉,卡里余额不足,不能取1000元");
}
}
同步代码块和同步方法的区别:
1、语法不同,同步代码块更灵活,可以自定义锁对象 ,而同步方法不可以指定锁对象