java多线程系列_Java多线程实战系列

69edf4630871eba1ce5a7d4db23268d1.png

多线程(英语:multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。 [1] 具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理(Chip-level multithreading)或同时多线程(Simultaneous multithreading)处理器。在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理(Multithreading)”。

主要内容

进程和线程线程的定义和创建线程生命周期线程控制线程同步

学习目标

fdd847db3948bfea901ce5531aed1b52.png

第一节 进程和线程

1.1 进程和线程

程序Program

程序是一段静态的代码,它是应用程序执行的蓝本

进程Process

进程是指一种正在运行的程序,有自己的地址空间

0e772c6eaae5abbd8fc58e1374629b40.png

进程的特点

动态性并发性独立性并发和并行的区别并行:多个CPU同时执行多个任务

并发:一个CPU(采用时间片)同时执行多个任务

7408076d5c049c0cbc0ddab5b86a41bf.png

线程Thread

进程内部的一个执行单元,它是程序中一个单一的顺序控制流程。线程又被称为轻量级进程(lightweight process)如果在一个进程中同时运行了多个线程,用来完成不同的工作,则称之为多线程线程特点

轻量级进程独立调度的基本单位共享进程资源可并发执行

c42daedcc83144392b8b55441c45443b.png

线程和进程的区别

a4e433079127031e61a6841032e5a1bb.png

生活案例:线程和进程的区别

班级:505,503,504小组:1,2,3,4,5……完成一件事情:大扫除总负责:校长步骤1:以班级为单位领取大扫除工具,本班级的所有小组都使用该班级领取的资源步骤2:以小组为单位开始大扫除步骤3:校长亲自监督,如果发现不合格,直接要求该小组重新打扫;如果某小组打扫完毕,校长可以直接给该小组安排其他任务对比

CPU:校长进程:班级 (一个班级可以有多个小组,班级是资源分配的单位)线程:小组 (校长直接指挥小组进行工作)。1.2线程定义和创建1:继承Thread类

Thread类是Java提供的线程顶级类,继承Thread类可快速定义线程。

【示例1】 使用多线程实现龟兔赛跑

public class TortoiseThread extends Thread {/*** 线程体,线程要完成的任务,要执行的代码*/@Overridepublic void run() {while (true) {System.out.println("乌龟领先了,加油.....," +"当前线程的名称:" + this.getName() +",当前线程的优先级别:" + this.getPriority());}}}public class Test {public static void main(String[] args) {//创建一个线程TortoiseThread thread = new TortoiseThread();//启动一个线程//thread.run();//这不是在启动线程,是在调用方法run()//启动线程,不见得立刻执行,而是进入就绪队列,等待获得CPUthread.start();//兔子也在跑,此处不在单独创建线程并启动while(true){System.out.println("兔子领先了,add oil....,当前线程名称:"+Thread.currentThread().getName()+",当前线程的优先级别:"+Thread.currentThread().getPriority());}}}

run() 线程体,线程要完成的任务start() 启动线程,线程进入就绪队列,等待获取CPU并执行之前讲解的程序都是单线程的本节作业

进程和线程的联系和区别通过继承Thread类定义和创建线程

第二节 线程的定义和创建

2.1 线程定义和创建2:实现Runnable接口

【示例2】使用多线程实现龟兔赛跑2

public classTortoiseRunnable implementsRunnable {//private int num = 100;/*** 线程体,线程要执行的任务*/@Overridepublic voidrun() {while(true){while(true){System.out.println("乌龟领先了,加油...."+Thread.currentThread().getName()+" "+Thread.currentThread().getPriority());}}}}public classTest {public static voidmain(String[] args) {//创建乌龟线程对象//Runnable runnable = new TortoiseRunnable();Runnable runnable = newRunnable(){@Overridepublic voidrun() {while(true){System.out.println("乌龟领先了............"+Thread.currentThread().getName());}}};Thread thread1 = newThread(runnable);//启动乌龟线程thread1.start();Thread thread2 = newThread(runnable);thread2.start();while(true){System.out.println("兔子领先了,add oil ...."+Thread.currentThread().getName()+" "+Thread.currentThread().getPriority());}}}

两种方式的优缺点

方式1:继承Thread类

缺点:Java单继承,无法继承其他类

优点:代码稍微简单

方式2:实现Runnable接口

优点 还可以去继承其他类 便于多个线程共享同一个资源

缺点:代码略有繁琐

实际开发中,方式2使用更多一些

可以使用匿名内部类来创建线程对象

已经学习的线程Thread的属性和方法

66bda70f68c5a5b76a9f6954ede1d88b.png

ac1e29665cf4941022b18cfabaefe498.png

2.2 线程定义和创建3:实现Callable接口

JDK1.5后推出了第三种定义线程的方式,实现Callable接口

【示例3】使用多线程获取随机数

public classRandomCallable implementsCallable {@OverridepublicInteger call() throwsException {Thread.sleep(5000);//throw new IOException();return newRandom().nextInt(10);}public static voidmain(String[] args)throwsInterruptedException, ExecutionException {//创建线程对象Callable callable = newRandomCallable();FutureTask task = newFutureTask(callable);Thread thread = newThread(task);//启动线程thread.start();//获取返回值System.out.println(task.isDone());//必须等线程执行完毕后,才能得到返回值,线程在此会阻塞Integer num = task.get();System.out.println(num);System.out.println(task.isDone());//线程是否执行完毕}}

第三种方式:实现Callable接口

与实行Runnable相比, Callable功能更强大些

方法名不同可以有返回值,支持泛型的返回值可以抛出检查异常需要借助FutureTask,比如获取返回结果Future接口

可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。FutrueTask是Futrue接口的唯一的实现类FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程行,又可以作为Future得到Callable的返回值2.2 线程的生命周期

0b63c56eaa47d023379787023dc651a3.png

新生状态:用new关键字建立一个线程对象后,该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间,通过调用start进入就绪状态就绪状态:处于就绪状态线程具备了运行条件,但还没分配到CPU,处于线程就绪队列,等待系统为其分配CPU当系统选定一个等待执行的线程后,它就会从就绪状态进入执行状态,该动作称之为“cpu调度”。运行状态:在运行状态的线程执行自己的run方法中代码,直到等待某资源而阻塞或完成任务而死亡。如果在给定时间片内没执行结束,就会被系统给换下来回到等待执行状态。阻塞状态:处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时停止自己的运行,进入阻塞状态。在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续运行。死亡状态:死亡状态是线程生命周期中最后一个阶段。线程死亡原因有三个。一个是正常运行的线程完成了它的全部工作;另一个是线程被强制性地终止,如通过执行stop方法来终止一个线程[不推荐使用】,三是线程抛出未捕获的异常本节作业

通过实现Runnable接口来定义线程实现龟兔赛跑通过实现Callable接口来定义线程实现获取随机数线程的生命周期(就绪状态的三个来源,运行状态的三个去处)第三节 线程控制

理解了线程生命周期的基础上,可以使用Java提供的线程控制命令对线程的生命周期进行干预。

join ()阻塞指定线程等到另一个线程完成以后再继续执行

sleep ()使线程停止运行一段时间,让出CPU,将处于阻塞状态

如果调用了sleep方法之后,没有其他等待执行的线程,这个时候当前线程不会马上恢复执行!

实际开发中经常使用Thread.sleep()来模拟线程切换,暴露线程安全问题。

yield ()让当前正在执行线程暂停,不是阻塞线程,而是将线程转入就绪状态

如果调用了yield方法之后,没有其他等待执行的线程,这个时候当前线程就会马上恢复执行!

setDaemon()可以将指定的线程设置成后台线程

创建后台线程的线程结束时,后台线程也随之消亡

只能在线程启动之前把它设为后台线程

interrupt()并没有直接中断线程,而是需要被中断线程自己处理

stop()结束线程,不推荐使用

【示例4】使用join()阻塞当前线程

public classTest {public static voidmain(String[] args) {inti = 0;while(i <= 200) {if(i == 20) {Thread thread1 = newTortoiseThread();thread1.setName("程咬金");thread1.start();try{// 线程A正在执行 线程B进来,线程B执行完,A才会执行。//A此间处于阻塞状态thread1.join();} catch(InterruptedException e) {e.printStackTrace();}}System.out.println("兔子领先了,add oil ...."+ i + " "+Thread.currentThread().getName() + " "+Thread.currentThread().getPriority());i++;}}}

【示例5】使用sleep()让线程休眠

public classTortoiseThread extendsThread {@Overridepublic voidrun() {while(true){try{Thread.sleep(1);} catch(InterruptedException e) {e.printStackTrace();}System.out.println("乌龟领先了,加油...."+this.getName()+" "+this.getPriority());}}}public classTest {public static voidmain(String[] args) {Thread thread1 = newTortoiseThread();thread1.start();while(true){try{Thread.sleep(1);} catch(InterruptedException e) {e.printStackTrace();}System.out.println("兔子领先了,add oil ...."+Thread.currentThread().getName()+" "+Thread.currentThread().getPriority());}}}

【示例6】使用setDaemon()设置守护线程

public classTest {public static voidmain(String[] args) {Thread thread1 = newTortoiseThread();thread1.setDaemon(true);//后台线程 守护线程 寄生线程thread1.start();inti =0;while(i<=200){System.out.println("兔子领先了,add oil ...."+i+" "+Thread.currentThread().getName()+" "+Thread.currentThread().getPriority());i++;}}}

【示例8】中断线程

public classTest {public static voidmain(String[] args) {Thread thread1 = newTortoiseThread();thread1.start();inti =0;while(i<=200){System.out.println("兔子领先了,add oil ...."+i+" "+Thread.currentThread().getName()+" "+Thread.currentThread().getPriority());i++;}//thread1.stop();//不推荐 相当于直接关机thread1.interrupt();//中断}}public classTortoiseThread extendsThread {private intnum= 100;public voidrun() {while(!isInterrupted()){//true falseSystem.out.println("乌龟领先了,加油.... "+this.getName()+" "+this.getPriority());}}}

本节作业

sleep()和yield()方法的异同之处使用interrupt()中断线程第四节 线程同步

4.1 问题的提出

应用场景:多个用户同时操作一个银行账户。每次取款400元,取款前先检查余额是否足够。如果不够,放弃取款

分析使用多线程解决开发一个取款线程类,每个用户对应一个线程对象因为多个线程共享同一个银行账户,使用Runnable方式解决思路创建银行账户类Account创建取款线程AccountRunnable创建测试类TestAccount,让两个用户同时取款【示例9】引入线程同步

/***银行账户*/public classAccount {private intbalance= 600;//取款public voidwithDraw(intmoney){this.balance= this.balance- money;}//查看余额public intgetBalance(){returnbalance;}}public classAccountRunnable implementsRunnable {privateAccount account= newAccount();@Overridepublic voidrun() {//判断余额是否足够,够,取之;不够,不取之;if(account.getBalance()>=400){try{Thread.sleep(1);} catch(InterruptedException e) {e.printStackTrace();}//取之account.withDraw(400);//输出信息System.out.println(Thread.currentThread().getName()+"取款成功,现在的余额是"+account.getBalance());}else{System.out.println("余额不足,"+Thread.currentThread().getName()+"取款失败,现在的余额是"+account.getBalance());}}}public classTest {public static voidmain(String[] args) {//创建两个线程Runnable runnable = newAccountRunnable();Thread zhangsanThread = newThread(runnable);Thread zhangsanWifeThread =newThread(runnable,"张三妻子");zhangsanThread.setName("张三");//启动两个线程zhangsanThread.start();zhangsanWifeThread.start();}}

分析:使用Thread.sleep()的目的在于模拟线程切换,在一个线程判断完余额后,不是立刻取款,而是让出CPU,这样另外一个线程获取CPU,并且进行余额的判断。线程安全问题就这么产生了。如果保证安全,必须判断余额和取款的语句必须被一个线程执行完才能让另外一个线程执行。

当多个线程访问同一个数据时,容易出现线程安全问题。需要让线程同步,保证数据安全

线程同步当两个或两个以上线程访问同一资源时,需要某种方式来确保资源在某一时刻只被一个线程使用

线程同步的实现方案同步代码块

synchronized (obj){ }

同步方法

private synchronized void makeWithdrawal(int amt) {}

Lock锁

4.1 同步代码块

【示例10】使用同步代码块实现线程同步

public classAccountRunnable implementsRunnable {privateAccount account= newAccount();@Overridepublic voidrun() {//此处省略300句synchronized(account){ //锁//判断余额是否足够,够,取之;不够,不取之;if(account.getBalance()>=400){try{Thread.sleep(1);} catch(InterruptedException e) {e.printStackTrace();}//取之account.withDraw(400);//输出信息System.out.println(Thread.currentThread().getName()+"取款成功,现在的余额是"+account.getBalance());}else{System.out.println("余额不足,"+Thread.currentThread().getName()+"取款失败,现在的余额是"+account.getBalance());}}//此处省略200句}}

总结1:认识同步监视器(锁子)

synchronized(同步监视器){ }必须是引用数据类型,不能是基本数据类型在同步代码块中可以改变同步监视器对象的值,不能改变其引用尽量不要String和包装类Integer做同步监视器.如果使用了,只要保证代码块中不对其进行任何操作也没有关系一般使用共享资源做同步监视器即可也可以创建一个专门的同步监视器,没有任何业务含义建议使用final修饰同步监视器总结2:同步代码块的执行过程

第一个线程来到同步代码块,发现同步监视器open状态,需要close,然后执行其中的代码第一个线程执行过程中,发生了线程切换(阻塞 就绪),第一个线程失去了cpu,但是没有开锁open第二个线程获取了cpu,来到了同步代码块,发现同步监视器close状态,无法执行其中的代码,第二个线程也进入阻塞状态第一个线程再次获取CPU,接着执行后续的代码;同步代码块执行完毕,释放锁open第二个线程也再次获取cpu,来到了同步代码块,发现同步监视器open状态,重复第一个线程的处理过程(加锁)强调:同步代码块中能发生线程切换吗?能!!! 但是后续的被执行的线程也无法执行同步代码块(锁仍旧close)

总结3:线程同步 优点和缺点

优点:安全缺点:效率低下 可能出现死锁总结4:其他

多个代码块使用了同一个同步监视器(锁),锁住一个代码块的同时,也锁住所有使用该锁的所有代码块,其他线程无法访问其中的任何一个代码块多个代码块使用了同一个同步监视器(锁),锁住一个代码块的同时,也锁住所有使用该锁的所有代码块, 但是没有锁住使用其他同步监视器的代码块,其他线程有机会访问其他同步监视器的代码块同步方法

【示例11】使用同步方法实现线程同步

public classAccountRunnable implementsRunnable {privateAccount account= newAccount();@Overridepublic voidrun() {//此处省略300句//判断余额是否足够,够,取之;不够,不取之;withDraw();//此处省略200句}publicsynchronizedvoidwithDraw(){ //同步监视器都是thisif(account.getBalance()>=400){try{Thread.sleep(1);} catch(InterruptedException e) {e.printStackTrace();}//取之account.withDraw(400);//输出信息System.out.println(Thread.currentThread().getName()+"取款成功,现在的余额是"+account.getBalance());}else{System.out.println("余额不足,"+Thread.currentThread().getName()+"取款失败,现在的余额是"+account.getBalance());}}public synchronized voidmethod2(){ //this}public synchronized voidmethod3(){ //this}}

总结:关于同步方法

不要讲run()定义为同步方法同步方法的同步监视器是this同步代码块的效率要高于同步方法同步方法的锁是this,一旦锁住一个方法,就锁住了所有的同步方法;同步代码块只是锁住使用该同步监视器的代码块,而没有锁住使用其他监视器的代码块同步方法是将线程锁在了方法的外部,而同步代码块锁将线程锁在了代码块的外部,但是却是方法的内部4.3 Lock锁

JDK1.5中推出了新一代的线程同步方式:Lock锁

示例12】使用Lock锁实现线程同步

public classAccountRunnable implementsRunnable {privateAccount account= newAccount();//买一把锁Lock lock= newReentrantLock(); //Re-entrant-Lock可重入锁@Overridepublic voidrun() {//此处省略300句try{//上锁lock.lock();//判断余额是否足够,够,取之;不够,不取之;if(account.getBalance()>=400){try{Thread.sleep(1);} catch(InterruptedException e) {e.printStackTrace();}method1();//取之account.withDraw(400);//输出信息System.out.println(Thread.currentThread().getName()+"取款成功,现在的余额是"+account.getBalance());}else{System.out.println("余额不足,"+Thread.currentThread().getName()+"取款失败,现在的余额是"+account.getBalance());}}finally{//解锁lock.unlock();}//此处省略100句}}

Lock锁JDK1.5后新增功能,与采用synchronized相比,lock可提供多种锁方案,更灵活java.util.concurrent.lock 中的 Lock 框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。这就为 Lock 的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义, 但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。注意:如果同步代码有异常,要将unlock()写入finally语句块Lock和synchronized的区别Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,遇到异常自动解锁Lock只有代码块锁,synchronized有代码块锁和方法锁Lock锁可以对读不加锁,对写加锁,synchronized不可以Lock锁可以有多种获取锁的方式,可以从sleep的线程中抢到锁,synchronized不可以使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)优先使用顺序:Lock----同步代码块(已经进入了方法体,分配了相应资源)----同步方法(在方法体之外)4.4 线程同步练习

实现三种线程同步方式实现多个窗口卖票,保证售票的安全性

【示例13】使用同步代码块实现多个窗口安全售票

public classTicketRunnable implementsRunnable{private intticketNum= 100;privateObject obj= newObject();public voidrun() {while(true){synchronized(obj){if(ticketNum<=0)break;try{Thread.sleep(10);} catch(InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"卖出第"+ticketNum+"张票");ticketNum--;}}}}

【示例14】使用同步方法实现多个窗口安全售票

public classTicketRunnable implementsRunnable{private intticketNum= 100;@Overridepublic voidrun() {while(true){sellOne();if(ticketNum==0){break;}}}public synchronized voidsellOne(){if(ticketNum==0){// break;return;}try{Thread.sleep(1);} catch(InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"卖出第"+ticketNum+"张票");ticketNum--;}}

【示例15】使用Lock锁实现多个窗口安全售票

public classTicketRunnable implementsRunnable{private intticketNum= 100;privateLock lock= newReentrantLock();public voidrun() {while(true){//上锁lock.lock();try{if(ticketNum==0){break;}try{Thread.sleep(1);} catch(InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"卖出第"+ticketNum+"张票");ticketNum--;}finally{//解锁lock.unlock();}}}}

本节作业

线程同步的三种方式是什么,各有什么优缺点。使用线程同步的三种方式完成多个用户对同一个账户取款使用线程同步的三种方式完成多个窗口一起售票

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值