2020-08-25
详解Java中的多线程(基础篇)
引言
多线程(multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。在同一时间执行多于一个的线程,同步完成多项任务,从而使我们能够更高效地利用CPU。但多线程本身并不能提高运行效率,只能够通过提高资源使用效率来提高系统的效率。接下来我们一起来学习多线程的概念和常见的用法。
一、基本概念
1.1线程与进程
进程:指的是内存中运行的一个应用程序,每个进程都有自己独立的内存空间
线程:指的是内存中的一条执行路径,共享一个空间,线程之间可以互相切换,并发指令。一个进程最少有一个线程。线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分成若干个线程
1.2线程的调度
线程的调度分为分时调度和抢占式调度。
分时调度:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
抢占式调度:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是 在同一时刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。
1.3同步和异步
同步:排队执行 , 效率低但是安全。同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。
异步:同时执行 , 效率高但是数据不安全。异步方法调用则更像一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作。而,异步方法通常会在另外一个线程中,“真实”地执行着。整个过程,不会阻碍调用者的工作。
1.4线程的六种状态
线程具有五种状态:他们分别是新建(New)、就绪(Runnable)、等待(Waiting)、定时等待(TimeWaiting)、阻塞(Blocked)和死亡(Dead)
新建(New):尚未启动的线程处于此状态
就绪(Runnable):称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。
阻塞(Blocked):线程被阻塞等待监视器锁定(排队时)
4.等待(Waiting):无限等待另一个线程执行特殊操作,醒了以后进入运行状态
5.定时等待(Timedwaiting):正在等待另一个线程执行最多指定等待时间的操作的线程处于此状态。
6.死亡(Terminated):已退出的线程处于此状态。
二、实现多线程的实例
2.1 继承Thread类
run方法可以看做线程要执行的方法,通过调用start方法来触发,可以参考如下示例:
public class ThreadTest { public static void main(String[] args) throws InterruptedException { MyThread t = new MyThread(); //创建线程对象 t.start();//调用start方法使线程执行 for(int i = 0;i<10;i++){ System.out.println(i); Thread.sleep(1000); } } } /** * 继承Thread类 */ public class MyThread extends Thread { @Override public void run() {//执行体 for(int i = 0;i<10;i++){ System.out.println(i); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
2.2 Runnable接口
实现Runable接口也是建立线程的一种方式,可以将实现Runable的接口称之为任务,相比于继承Thread类有以下优势:
(1)适用于多线程执行相同任务的情况
(2)避免了单继承带来的局限性
(3)线程与任务分离,提高了程序的健壮性
(4)后续学习线程池,接受Runable类型的任务而不接受Thread类型的线程
Runable接口的使用方法和继承的Thread类类似,创建对象的方法有所不同,如下所示:
MyRunnable r = new MyRunnable();//创建任务 Thread t = new Thread(r); //创建线程
我们也可以通过一个任务创建多个线程:
MyRunnable r = new MyRunnable();//创建任务 Thread t1 = new Thread(r); //创建线程 Thread t2 = new Thread(r); //创建线程 Thread t3 = new Thread(r); //创建线程
也可以通过更简便的写法创建线程:
Thread t = new Thread(new MyRunnable()); //创建线程对象 t.start();//调用start方法使线程执行
或者
new Thread(new MyRunnable()).start(); //创建线程对象
2.3 常用方法
2.3.1构造方法
Thread();// Thread(String name);//给线程定义一个名称 Thread(Runable target);//使用任务创建线程 Thread(Runable targer,String name);
2.3.2常用方法
getName();//返回线程的名称 getID();//返回线程的标示符(不可指定) getPriority();//返回此线程的优先级 setPriority();//设置/更改线程的优先级 start();//启动线程 sleep(long millis);//使线程休眠(会占用线程),需要设置休眠多少毫秒/纳秒 setDaemon(boolean on);//将线程标记为daemon线程
三、线程安全
线程不安全是指多个线程同时执行时,去争抢一个数据,使得某个数据判断时和使用时的值产生偏差,使得运行不符合预期的现象。
线程同步:synchronized(排队机制)是指当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态。
3.1同步代码块
格式:synchronized(锁对象){}
当线程获得锁以后,其他线程要等待代码块执行完毕再去重新获得锁
实例:
public class MyRunnable implements Runnable{ int count = 10; Object o = new Object();//三个线程共用一把锁,不能把锁写在执行体内 @Override public void run() {//执行体 while(true){ synchronized(o) { if (count > 0) { System.out.println("正在准备卖票"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } count--; System.out.println(Thread.currentThread().getName() + "抢票成功,还剩" + count + "张"); }else{ break; } } } } }
3.2 同步方法
以方法为单位进行加锁。
格式:public synchronized 返回值类型 方法名(){}
使得这个方法可以排队执行,它的锁为this或类名.class(静态时)(不用单独写出来)
当同步方法和同步代码块同时存在时,它们若使用同一个锁对象,如默认的this,当一个锁在执行时,其他锁无法执行。
3.3 显式锁Lock
同步代码块和同步方法都属于隐式锁。显式锁经常使用的是它的子类ReentrantLock
显式锁的使用方法很直观,需要自己上锁自己解锁,基本的流程如下所示:
Lock l = new ReentrantLock(); // 创建锁对象 l.lock();//上锁 l.lock();//解锁
3.4 公平锁和非公平锁
非公平锁(以上三种锁):大家一起抢
公平锁:先来先到,谁先等先占用这个锁
可以通过显式锁的构造方法来实现公平锁
ReentrantLock(boolean fair);//默认为非公平锁,当fair为true时,即为公平锁
3.5 多线程通信
实现多线程的交互,生产者与消费者机制
为解决多线程交互之间的不协调问题,经常通过Object类来实现交互,常用方法:
wait();//导致当前线程等待被唤醒,通常是通知或中断
notify();//随即唤醒一个线程
notiftAll();//唤醒全部线程
设两个线程分别为厨师和服务员,厨师做好菜后服务员将菜品端上餐桌,通过线程的交互,让两个线程能够互相协调,控制等待和执行的时间。使用实例如下:
package com.test.File.Thread; public class CookAndWaiter { public static void main(String[] args) { Food f = new Food(); new Cook(f).start(); new Waiter(f).start(); } /** * 厨师 */ static class Cook extends Thread{ private Food f; public Cook(Food f){ this.f = f; } @Override public void run() { for(int i=0;i < 100;i++){ if(i%2 == 0){ f.setNameAndTaste("麻辣香锅","麻辣味"); }else{ f.setNameAndTaste("香辣麻锅","香辣味"); } } } } /** * 服务员 */ static class Waiter extends Thread{ private Food f; public Waiter(Food f){ this.f = f; } public void run(){ for(int i=0;i<100;i++) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } f.get(); } } } static class Food{ private String name; private String taste; private boolean flag = true; public synchronized void setNameAndTaste(String name,String taste){ if (flag) { this.name = name; try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } this.taste = taste; flag = false; this.notifyAll(); try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } public synchronized void get(){ if(!flag) { System.out.println("服务员端走的饭菜是:" + name + ",口味是:" + taste); flag = true; this.notifyAll(); try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } }
四、带有返回值的线程Callable
4.1 使用格式
(1) 编写类实现Callable接口 , 实现call方法
class XXX implements Callable<T> { @Override public <T> call() throws Exception { return T; } }
(2)创建FutureTask对象 , 并传入第一步编写的Callable类对象
FutureTask<Integer> future = new FutureTask<>(callable);
(3)通过Thread,启动线程
new Thread(future).start();
4.2 与Runnable 的异同
相同点:
(1)都是接口
(2)都可以编写多线程程序
(3)都采用Thread.start()启动线程
不同点:
(1)Runnable没有返回值;Callable可以返回执行结果
(2)Callable接口的call()允许抛出异常;Runnable的run()不能抛出
- 宝塔7.4.2-pma未授权访问漏洞风险
- nodejs学习-http搭建简易服务器
- Google桌面搜索和百度硬盘搜索索引停滞的原因
- Spring Boot 自定義 HttpMessageConverter 解決 String 類型返回JSON對象問題
- Springboot中登录后关于cookie和session拦截案例
- leetcode_459. 重复的子字符串 python3
- 问题
- Python学习之路日志—《外星人入侵》游戏项目总结回顾(1)
- nodejs学习-路径模块path
- GK扫描仪全能王
- NET Core Kestrel部署HTTPS 一个服务器绑一个证书 一个服务器绑多个证书
- 资金流入流出预测–学习
- 【英语学习】431期之拖延症
- 求X值问题
- 狂啃500道Java灵魂考题:助你顺利拿下阿里P7腾讯Offer
- 【解Bug之路】——Nginx 502 Bad Gateway
- 含Java岗988道题分享 备战金九银十,你准备好了吗?,阿里,腾讯秋招面试题解析。
- java递归遍历文件夹寻找指定文件以及其代码优化
- Gephi快速入门(一):Windows下安装Gephi
- 完了,这个硬件成精了,它竟然绕过了 CPU