线程
多线程
- 线程就是独立的执行路径:
- 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gC线程;
- main()称之为主线程,为系统的入口,用于执行整个程序;
- 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为的干预的。
- 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制:
- 线程会带来额外的开销,如cpu调度时间,并发控制开销。
- 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
多线程是什么?为什么要用多线程?
介绍多线程之前要介绍线程,介绍线程则离不开进程。
进程 :是一个正在执行中的程序,每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元;
线程:就是进程中的一个独立控制单元,线程在控制着进程的执行。一个进程中至少有一个线程。
多线程:一个进程中不只有一个线程。
垃圾回收线程:gc 是垃圾回收线程
为什么要用多线程:
-
为了更好的利用cpu的资源,如果只有一个线程,则第二个任务必须等到第一个任务结束后才能进行,如果使用多线程则在主线程执行任务的同时可以执行其他任务,而不需要等待;
-
进程之间不能共享数据,线程可以;
-
系统创建进程需要为该进程重新分配系统资源,创建线程代价比较小;
-
Java语言内置了多线程功能支持,简化了java多线程编程。
线程的生命周期:
- 新建 :从新建一个线程对象到程序start() 这个线程之间的状态,都是新建状态;
- 就绪 :线程对象调用start()方法后,就处于就绪状态,等到JVM里的线程调度器的调度;
- 运行 :就绪状态下的线程在获取CPU资源后就可以执行run(),此时的线程便处于运行状态,运行状态的线程可变为就绪、阻塞及死亡三种状态。
- 等待/阻塞/睡眠 :在一个线程执行了sleep(睡眠)、suspend(挂起)等方法后会失去所占有的资源,从而进入阻塞状态,在睡眠结束后可重新进入就绪状态。
- 终止 :run()方法完成后或发生其他终止条件时就会切换到终止状态。
创建线程的方法
-
继承Thread类
步骤: ①、自定义类继承Thread类;
②、重写Thread类中的run方法,编写线程执行体;
作用:将自定义代码存储在run方法,让线程运行
③、创建对象,调用线程的start方法启动线程:
该方法有两步:启动线程,调用run方法
//创建线程方式一:继承Thread类,重写run()方法,调用start开启线程
public class TestThread extends Thread {
@Override
public void run() {
//run方法线程体
for (int i = 0; i < 100; i++) {
System.out.println("线程1\t" + i);
}
}
public static void main(String[] args) {
//main线程,主线程
//创建一个线程对象
TestThread thread = new TestThread();
//调用start()方法开启线程
thread.start();
for (int j = 0; j < 100; j++) {
System.out.println("main主线程" + j);
}
}
}
结论:两个线程可以同时执行,体现了多线程同步执行
注意:线程开启不一定执行,由CPU调度执行
案例二
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
public class TestThread01 extends Thread{
private String url; //网络图片地址
private String name;//保存的文件名
public TestThread01(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) {
TestThread01 t1 = new TestThread01("https://gitee.com/cjtqqcom/image/raw/master/img/屏幕截图 2022-10-11 105536.png","图片01");
TestThread01 t2 = new TestThread01("https://gitee.com/cjtqqcom/image/raw/master/img/屏幕截图 2022-10-03 213303.png","图片02");
TestThread01 t3 = new TestThread01("https://gitee.com/cjtqqcom/image/raw/master/img/屏幕截图 2022-10-03 213206.png","图片03");
t1.start();
t2.start();
t3.start();
}
}
//下载器
class WebDownLoader{
//下载方法
public void downLoader(String url,String name){
try {
FileUtils.copyURLToFile(new URL(url),new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO异常,downLoader方法出现问题");
}
}
}
输出结果为:下载了文件名:图片03
下载了文件名:图片02
下载了文件名:图片01
- 实现runnable接口
步骤:1.自定义类实现Runnable接口
2.实现Runnable接口中run方法的重写,编写线程执行体
3.创建线程对象,调用start()方法启动线程
该方法有三步:创建实现类对象 , 创建代理对象 ,启动线程
//创建线程方法2:实现runnable接口,重写run方法,执行线程需要丢入runnable接口实现类,调用start方法
public class TestThread02 implements Runnable{
@Override
public void run() {
//run方法线程体
for (int i = 0; i < 20; i++) {
System.out.println("线程1\t "+i);
}
}
public static void main(String[] args) {
//创建runnable接口的实现类对象
TestThread02 thread02 = new TestThread02();
//创建线程对象,通过线程对象来开启我们的线程,运用了线程代理
new Thread(thread02).start();
for (int i = 0; i < 20; i++) {
System.out.println("主线程\t"+i);
}
}
}
结论:两个线程可以同时执行,体现了多线程同步执行
推荐使用Runnable对象,因为Java单继承的局限性
小结
-
继承Thread类
- 子类继承Thread类具备多线程能力
- 启动线程:子类对象.start()
- 不建议使用:避免OOP单继承局限性
-
实现Runnable:接口
- 实现接口Runnable.具有多线程能力
- 启动线程:传入目标对象+Thread对象.start()
- 推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用
//同一个对象被多个线程使用
//一份资源
startThread4 station = new startiead4();
//多个代理
new Thread(station,"小明").scart();
new Thread(station,"老师").start();
new Thread(station,"小红").start();
- 多线程同时操作同一个对象
- 出现问题:多线程操作同一个资源,会出现线程并发问题,导致线程不安全,数据紊乱
//案例:抢车票为例子
public class TestThread03 implements Runnable{
//票数
private int ticketNums = 10;
@Override
public void run() {
while (true){
if(ticketNums <= 0){
break;
}
//用于CPU处理太快,这里加了一个模拟延时
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--+"票");
}
}
public static void main(String[] args) {
TestThread03 testThread03 = new TestThread03();
new Thread(testThread03,"张三").start();
new Thread(testThread03,"李四").start();
new Thread(testThread03,"王五").start();
}
}
输出结果为:
李四-->9票
王五-->10票
张三-->10票
李四-->8票
王五-->8票
张三-->7票
王五-->6票
李四-->4票
张三-->5票
李四-->3票
张三-->1票
王五-->2票
通过Callable和Future创建线程:
实现步骤:①、创建Callable接口的实现类,并实现call()方法,改方法将作为线程执行体,且具有返回值。
②、创建Callable实现类的实例,使用FutrueTask类进行包装Callable对象,FutureTask对象封装了Callable对象的 call()方法的返回值
③、使用FutureTask对象作为Thread对象启动新线程。
④、调用FutureTask对象的get()方法获取子线程执行结束后的返回值。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class CallableThreadTest implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int i = 0;
for ( i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
return i;
}
public static void main(String[] args) {
CallableThreadTest callable = new CallableThreadTest();
FutureTask<Integer> ft = new FutureTask<>(callable);
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i