JAVA基础——多线程
Posted 我永远信仰
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JAVA基础——多线程相关的知识,希望对你有一定的参考价值。
文章目录
1、线程、进程、多任务
多任务
首先需要了解操作系统中的多任务:在同一时刻运行多个程序的能力。
例如,在编辑或下载邮件的同时可以打印文件。
多线程程序(进程)
多线程程序在较低的层次上扩展了多任务的概念:一个程序同时执行多个任务。
通常,每一个任务称为一个线程,可以同时运行一个以上线程的程序称为多线程程序,也可以称为进程。
线程是进程的一个执行片段。
多进程与多线程的区别
本质的区别在于每个进程拥有自己的一整套资源,而线程是共享资源。这听起来似乎有风险,的确也是这样,后面将解释这个问题。
然而,共享资源使线程之间的通信比进程之间的通信更有效、更容易。此外,在有些操作系统中,与进程相比,线程更“轻量级”,创建、销毁一个线程比启动一个新进程的开销要小得多。
多线程解释
使用单线程只有一条执行路径,在调用其他线程时需要等其执行完,主线程再继续执行,如果调用的线程需要执行一些耗时的操作,那么这种效率明显是极低的,如左图。
使用多线程可以解决上面遇到的问题。调用其它线程的时候,为它开辟一个新的线程,它是一个子线程,与主线程并行执行,效率明显提高。
在实际中,多线程非常有用。例如,一个浏览器可以同时下载几幅图片,可以在下载图片的同时也可以播放视频。浏览网页……
2、线程状态
线程可以有五大状态:
- new (新创建)
- Runnable(可运行)
- Blocked(被阻塞)
- Waiting(等待)
- Timed_Waiting(计时等待)
- Terminated(被终止)
线程的五大状态
1.新线程创建(new)
当用new操作符创建一个新线程时,如new Thread(),该线程还没开始运行。这意味着它的状态是new,程序还没开始运行线程中的代码。在线程运行之前还有一些基础工作要做。
可运行线程(Runnable)
一旦调用start方法,线程处于Runnable(可运行)状态。一个可运行状态的线程可能正在运行,也可能没有运行,这取决于操作系统给线程提供运行的时间。(一个正在运行中的线仍然处于可运行状态)
一旦一个线程开始运行,它不必始终保持运行,事实上,运行中的线程被中断,目的是为了让其他线程获得运行机会。线程调度细节依赖于操作系统提供的服务。抢占式调度系统给每一个可运行线程提供一个时间片来执行任务。当时间片用完,操作系统剥夺该线程的运行权,并给另一个线程运行机会。选择下一个线程时,操作系统考虑线程的优先级——见后续。
在具有多个处理器的机器上,每一个处理器运行一个线程,可以有多个线程并行运行。当然,如果线程的数目多于处理器的数目,调度器依然采用时间片机制。
在任何给定的时刻,一个可运行线程可能正在运行也可能没有运行(这就是为什么将这个状态称为可运行而不是运行)。
三种创建方式:(只关注前两种)
方法1
继承Thread类,重写run方法(线程的执行体),调用start开启线程。
public class Thread1 extends Thread{
@Override
public void run() {
for (int i = 0; i < 8; i++) {
System.out.println("我在学习线程"+i);
}
}
//主线程
public static void main(String[] args) {
//调用start方法开启多线程。这里可以看出交替输出,说明两个线程在交替执行。
new Thread1().start();
//执行普通的run方法。这里测试发现是顺序输出,只是执行了普通的方法
//new Thread1().run();
for (int i = 0; i < 8; i++) {
System.out.println("我在写代码"+i);
}
}
}
注意:线程开启不一定执行,由cpu调度。
一个多线程实例。(下载3张网络图片)
首先需要导入一个包 apache下的commons-io包,百度即可。
然后将这个包添加到库,就可以开始啦。
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
//多线程测试类
public class TestThread2 extends Thread{
private String url;//图片的url,网络地址。
private String name;//保存的文件名
public TestThread2(String url,String name){
this.url=url;
this.name=name;
}
//下载图片线程的执行体
@Override
public void run() {
WebDownloader webDownloader = new WebDownloader();
webDownloader.downloader(url,name);
System.out.println("下载了文件名为"+name);
}
//主线程
public static void main(String[] args) {
//创建三个下载器,分别下载三种图片,参数url和文件名。
TestThread2 t1 = new TestThread2("https://desk-fd.zol-img.com.cn/t_s960x600c5/g6/M00/0A/02/ChMkKWBZUWGIPXnSACv153Pjp5wAAL6tQAAAAAAK_X_094.jpg", "1.jpg");
TestThread2 t2 = new TestThread2("https://desk-fd.zol-img.com.cn/t_s960x600c5/g6/M00/0A/02/ChMkKmBZUVmIIIjiACJyjAuzCC4AAL6swNQumsAInKk211.jpg", "2.jpg");
TestThread2 t3 = new TestThread2("https://desk-fd.zol-img.com.cn/t_s960x600c5/g6/M00/0C/09/ChMkKWDMTWaIBEoYAA3JrKqtoqIAAQzRgPfjJsADcnE262.jpg", "3.jpg");
//开启线程
t1.start();
t2.start();
t3.start();
}
}
//下载器
class WebDownloader{
//下载方法
public void downloader(String url,String name){
try {
//用包下的工具类FileUtils,下载网络图片的方法copyURLToFile
FileUtils.copyURLToFile(new URL(url),new File(name));
} catch (IOException e) {
System.out.println("IO异常,downloader方法出现问题");
}
}
}
运行结果。
可见,输出顺序并不是按照代码的顺序来执行的。说明着三个线程同时执行,谁最先下好谁先输出。
方法2
实现Runnable接口,重写run方法(线程的执行体)。用Thread代理执行,将实现了Runnable类的对象丢给Thread代理调用start开启线程。这是与方法1的主要区别
public class TestThread3 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 8; i++) {
System.out.println("我在学习线程"+i);
}
}
//主线程
public static void main(String[] args) {
TestThread3 testThread3 = new TestThread3();
//代理,将实现了Runnable类的对象丢给Thread,然后调用start方法。
new Thread(testThread3).start();
for (int i = 0; i < 8; i++) {
System.out.println("我在写代码"+i);
}
}
}
下载三张图片的例子改成用Runnable实现。需要改动的地方
将extends Thread改成Implements Runnable
线程启动方式改为
//开启线程
t1.start();
t2.start();
t3.start();
//改为
new Thread(t1).start();
new Thread(t2).start();
new Thread(t3).start();
一个实例:模拟抢票(初识并发问题)
三个人抢票。票被抢光程序执行结束
public class TestThread4 implements Runnable{
private int ticket = 10;//票数
@Override
public void run() {
while(true){
if (ticket<=0){//当票被抢光
break;
}
System.out.println(Thread.currentThread().getName()+"抢到了第"+ticket--+"张票");
}
}
public static void main(String[] args) {
//创建一个对象。里面有十张票。
TestThread4 testThread4 = new TestThread4();
//开启三个线程抢着同一个对象的十张票。
new Thread(testThread4,"小明").start();
new Thread(testThread4,"小红").start();
new Thread(testThread4,"小芳").start();
}
}
运行结果:
这里发现,第10张票被抢了两次,同一张票只应该被抢一次,说明程序是有漏洞的。这涉及了并发的问题,后续会讲到。
总结
- 继承Thread类
- 子类继承Thread类具备多线程能力
- 启动线程:子类对象.start()
- 不建议使用,继承只可以单继承,有局限性。
- 实现Runnable类
- 实现接口Runnable类具备多线程能力
- 启动线程:Thread对象(传入目标对象).start
- 建议使用:接口可以多实现,灵活方便,同一个对象可以被多个线程使用。避免了单继承的局限性
方法三(了解即可)
执行步骤
- 创建执行服务
- 提交执行
- 获取结果
- 关闭服务
改造下载三张图片的案例
import com.thread.demo01.WebDownloader;
import java.util.concurrent.*;
public class TestCallable implements Callable<Boolean> {
private String url;
private String name;//保存的文件名
public TestCallable(String url, String name) {
this.url = url;
this.name = name;
}
//下载图片线程的知执行体
@Override
public Boolean call() {
WebDownloader webDownloader = new WebDownloader();
webDownloader.downloader(url, name);
System.out.println("下载了文件名为" + name);
return false;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
TestCallable t1 = new TestCallable("https://desk-fd.zol-img.com.cn/t_s960x600c5/g6/M00/0A/02/ChMkKWBZUWGIPXnSACv153Pjp5wAAL6tQAAAAAAK_X_094.jpg", "1.jpg");
TestCallable t2 = new TestCallable("https://desk-fd.zol-img.com.cn/t_s960x600c5/g6/M00/0A/02/ChMkKmBZUVmIIIjiACJyjAuzCC4AAL6swNQumsAInKk211.jpg", "2.jpg");
TestCallable t3 = new TestCallable("https://desk-fd.zol-img.com.cn/t_s960x600c5/g6/M00/0C/09/ChMkKWDMTWaIBEoYAA3JrKqtoqIAAQzRgPfjJsADcnE262.jpg", "3.jpg");
//创建执行服务
ExecutorService ser = Executors.newFixedThreadPool(3);
//提交执行
Future<Boolean> r1 = ser.submit(t1);
Future<Boolean> r2 = ser.submit(t2);
Future<Boolean> r3 = ser.submit(t3);
//获取结果
Boolean rs1 = r1.get();
Boolean rs2 = r2.get();
Boolean rs3 = r3.get();
System.out.println(rs1);
System.out.println(rs2);
System.out.println(rs3);
//关闭服务
ser.shutdownNow();
}
}
总结:
- 可以定义返回值
- 可以抛出异常
2.线程停止
线程只能启动一次,线程中断或者结束,一旦进入死亡状态,就不能再次启动。
线程因如下两个原因之一而被终止:
- 因为run方法正常退出而自然死亡
- 因为一个没有捕获的异常终止了run方法而意外死亡
注意:
- 不推荐使用JDK提供的stop()、destory()方法【已废弃】
- 推荐线程自己停下来
- 建议使用一个标志位来控制线程终止,当flag=false,终止线程运行。
实例:
在主线程中开启一个子线程,当循环达到900次时,设置标志位为false,终止子线程
public class ThreadStop implements Runnable {
//设置标志位,控制线程停止
private boolean flag = true;
@Override
public void run() {
int i = 1;
while (flag) {
System.out.println("线程run...在执行" + i++);
}
}
public void stop() {
this.flag = false;
}
public static void main(String[] args) {
ThreadStop threadStop = new ThreadStop();
new Thread(threadStop).start();
for (int i = 0; i < 1000; i++) {
System.out.println("main执行" + i + "次");
if (i == 900) {
threadStop.stop();
System.out.println("线程停止了");
}
}
}
}
3.线程休眠
当线程处于被阻塞或等待状态时,它暂时不活动。它不运行任何代码且消耗最少的资源。直到线程调度器重新激活它。
sleep(时间)
指定当前线程阻塞的毫秒数- sleep存在异常Interrupt Exception;
- sleep时间到达后线程进入就绪状态
- sleep进入阻塞
- sleep可以模拟网络延时,倒计时等
- 模拟网络延时:放大问题的发生性
- 每个对象都有一把锁,sleep不会释放锁
实例:
模拟倒计时:
public class ThreadSleep{
//模拟倒计时,10秒
public void tenDown() throws InterruptedException {
int num=10;
while (true) {
if (num<=0){
break;
}
System.out.println(num);
//休眠1秒
Thread.sleep(1000);
num--;
}
}
public static void main(String[] args) {
ThreadSleep threadSleep = new ThreadSleep();
try {
threadSleep.tenDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
4.线程让步
- 线程让步,让当前正在执行的线程暂停,但不阻塞
- 将线程从运行状态转为就绪状态
- 让CPU重新调度,让步不一定成功。看CPU的调度情况
- 比如说有a、b两个线程,a进入了cpu,然后a调用了yield()让步方法,a就从cpu里出来了,a和b重新竞争cpu。cpu重新调度,有可能调用a线程,也有可能调用b。
- 注意,yield是一个静态方法
实例:
两个线程的礼让。
package com.thread.demo01;
public class ThreadYield {
public static void main(String[] args) {
MyYield myYield = new MyYield();
new Thread(myYield,"a").start();
new Thread(myYield,"b").start();
}
}
class MyYield implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"线程开始执行");
Thread.yield();
System.out.println(Thread.currentThread().getName()+"线程结束执行");
}
}
/*
运行结果:成功案例
a线程开始执行
b线程开始执行
b线程结束执行
a线程结束执行
*/
5.线程强制执行(插队)
- 线程插队后,等到该线程执行完,才执行其他线程,其他线程处于阻塞状态
实例:
一开始两个线程共抢资源,当主线程执行到200的时候,vip线程执行join方法(插队),之后等到vip线程执行完后主线程才执行。
package com.thread.demo01;
/**
* @Author cyh
* @Date 2021/8/5 21:50
*/
public class ThreadJoin implements Runnable{
@Override
public void run() {
for (int i = 0; i < 500; i++) {
System.out.println("线程VIP"+i);
}
}
public static void main(String[] args) throws InterruptedException {
ThreadJoin threadJoin = new ThreadJoin();
Thread thread = new Thread(threadJoin);
thread.start();
for (int i = 0; i < 1000; i++) {
if (i == 200) {
thread.join();
}
System.out.println("main"+i);
}
}
}
6.观测线程状态
- 观测线程的状态可以让我们适时的对他做出一些改变。比如发现该线程一致处于等待状态,我们可以让他销毁或者插队。
例子:
观察线程的状态
package com.thread.demo01;
/**
* @Author cyh
* @Date 2021/8/5 22:20
*/
public class ThreadState {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}以上是关于JAVA基础——多线程的主要内容,如果未能解决你的问题,请参考以下文章