多线程
线程简介
一个程序执行后,就是一个进程,一个进程可以有多个线程,且至少有一个,否则这个进程就没有存在的意义了。我们自己写的Java程序启动后,通常至少有2个线程,一个是主线程,即main线程,也叫用户线程,我们写的线程都在该线程中;一个是垃圾回收线程,即gc线程,也叫守护线程。
线程的创建方式
线程的3种创建方式:
- Thread class --> 继承Thread类(重点)
- Runnable接口 --> 实现Runnable接口(重点)
- Callable接口 --> 实现Callable接口(了解)
一、Thread
使用方法,三步走:
1、自定义线程类,并继承Thread类;
2、重写run()方法,编写线程体执行的代码;
3、创建线程对象,调用start()方法,启动线程。
代码示例:
// 1、创建一个线程类,并继承Thread类 public class TestThread1 extends Thread { // 2、重写run()方法 @Override public void run() { for (int i = 0; i < 20; i++) { System.out.println("这是run线程的数据" + i); } } // 这里是用main函数测试的 public static void main(String[] args) { // 3、创建线程对象,并调用start()方法,启动线程 TestThread1 thread1 = new TestThread1(); thread1.start(); for (int i = 0; i < 20; i++) { System.out.println("这是main线程的数据" + i); } } }
从执行结果可以看出,2个循环是‘同时’执行的,输出的结果是穿插的。

Thread方法的练习:使用多线程,下载网络图片
准备工作:下载一个FileUtils文件工具类的依赖jar包
1、到maven仓库中,搜索Commons IO,然后下载对应的jar包(由于我是在Java项目测试的,所以下载的jar包,如果是Springboot项目中,可以下载对应的pom依赖);
2、添加到项目中:新建package叫lib,将jar包复制到该包中;
3、添加jar包到项目的Library中:右键lib,选择add as library,点击确定即可。
至此,准备工作完成。
多线程测试下载网络图片代码示例:
import org.apache.commons.io.FileUtils; import java.io.File; import java.io.IOException; import java.net.URL; // 1、自定义线程类,并继承Thread类 public class TestThread2 extends Thread{ private String url; private String name; // 通过有参构造的方式,在new一个线程时,传入相应的参数; public TestThread2(String url, String name){ this.url = url; this.name = name; } // 2、重写run()方法 @Override public void run() { // 方法的执行体,去调用图片下载的方法 WebDownload webDownload = new WebDownload(); webDownload.download(url,name); System.out.println("当前下载了" + name); } public static void main(String[] args) { // 3、创建线程对象,并调用start()方法; TestThread2 t1 = new TestThread2("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fpic1.win4000.com%2Fwallpaper%2F0%2F5850a64def538.jpg&refer=http%3A%2F%2Fpic1.win4000.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1613748235&t=6be89ef4fb6e5043d23461b1a7452a41","图片一.jpeg"); TestThread2 t2 = new TestThread2("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fpic1.win4000.com%2Fwallpaper%2F6%2F577e098f74c2b.jpg&refer=http%3A%2F%2Fpic1.win4000.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1613748235&t=152af68eccf40a7c6c9fe52f30002c37","图片二.jpeg"); TestThread2 t3 = new TestThread2("https://gimg2.baidu.com/image_search/src=http%3A%2F%2F5b0988e595225.cdn.sohucs.com%2Fimages%2F20171017%2Fc3da1ba09e2d4f0680e23e3f5099bb98.jpeg&refer=http%3A%2F%2F5b0988e595225.cdn.sohucs.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1613748235&t=983abc09a1bf16dafd59f1e25618f869","图片三.jpeg"); t1.start(); t2.start(); t3.start(); } } class WebDownload{ // 下载方法 public void download(String url, String name){ try { // 这个工具类,就是前面准备工作下载 FileUtils.copyURLToFile(new URL(url), new File(name)); } catch (IOException e) { e.printStackTrace(); System.out.println("文件下载的方法抛出异常" + e); } } }
### 这里是输出结果:顺序是3、1、2,且每次执行后,结果的顺序是不一样的。这就说明,线程是由CPU来调度的,并不是启动后,立马就执行的。
当前下载了图片三.jpeg
当前下载了图片一.jpeg
当前下载了图片二.jpeg
二、Runnable
使用方法三步走:
1、定义线程类,实现Runnable接口;
2、实现run()方法,编写线程执行体代码;
3、创建线程对象,调用start()方法,启动线程。
代码示例:
1 // 1、创建线程类,实现Runnable接口 2 public class TestThread3 implements Runnable { 3 // 2、实现run()方法,编写线程体执行程序 4 @Override 5 public void run() { 6 // 线程执行体 7 for (int i = 0; i < 2000; i++) { 8 System.out.println("我是线程体中的数据" + i); 9 } 10 } 11 12 public static void main(String[] args) { 13 // 3、创建实现了Runnable接口的类的对象 14 TestThread3 testThread = new TestThread3(); 15 Thread t1 = new Thread(testThread); 16 t1.start(); 17 // 上面2行可以合并为下面1行 18 // new Thread(testThread).start(); 19 20 for (int i = 0; i < 2000 ; i++) { 21 System.out.println("我是main体中的数据" + i); 22 } 23 }
运行结果部分截图:可以看出是穿插输出的。

模拟多线程抢票
直接上代码:
public class TestThread4 implements Runnable { private int tickets = 10; @Override public void run() { while(true){ if(tickets <= 0){ break; } System.out.println("第" + tickets-- + "张票被用户" + Thread.currentThread().getName() + "买走了"); } } public static void main(String[] args) { TestThread4 ticket = new TestThread4(); new Thread(ticket, "张三").start(); new Thread(ticket, "李四").start(); new Thread(ticket, "王五").start(); new Thread(ticket, "赵六").start(); } }
运行结果如图:

由上图发现问题:第9张票,同时被2个人买走,这在现实中是不合理的,这就涉及到了并发的问题,需要用线程同步来解决。(后续讲解)
线程状态
线程一共有5种状态,它们之间的关系,如下图所示:
一、线程停止
线程停止的方式有多种,但是JDK提供的一些方法如stop()、destroy()等方法都是不建议使用的,且已废弃,所以推荐让线程自己停下来,即设置一个标志位,并在线程中使用这个标志位,当达到某种条件时,通过转换标志位的值,来进行线程的终止。
示例代码:
public class TestLamda implements Runnable { // 1、设置一个标志位 private boolean flag = true; @Override public void run() { int i = 0; // 2、线程中的方法使用标志位 while(flag){ System.out.println("这是线程中的数据 " + i++); } } // 3、外部定义一个改变标志位的方法 public void stopThread(){ this.flag = false; } public static void main(String[] args) { TestLamda tl = new TestLamda(); new Thread(tl).start(); for (int i = 0; i < 1000 ; i++) { System.out.println("这是main中的数据 " + i); if(i == 988){ // 4、当达到某种条件时,调用3中的方法,转换标志位,来终止线程。 tl.stopThread(); System.out.println("线程要停止了。。。"); } } } }
运行结果:

由图可知,当main方法中的i值为988时,停止了线程中的方法,此时,线程中的设置累加到了785,每启动一次,该值是不同的。
二、线程休眠
线程休眠,JDK提供的方法为Thread.sleep();
- sleep(时间) 指定当前线程阻塞(休眠)的毫秒数。
- sleep方法存在InterruptedException异常,需要try...catch捕捉或抛出;
- sleep时间结束后,线程重新进入 就绪状态。
- sleep可以模拟网络延时、倒计时等等。
- 每一个对象都有一个锁,sleep不会释放锁。
使用sleep(),每隔2秒,输出一下当前时间;
public static void main(String[] args) { while(true){ System.out.println(new Date()); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } }
输出结果:
Mon Jan 25 22:29:56 CST 2021
Mon Jan 25 22:29:58 CST 2021
Mon Jan 25 22:30:00 CST 2021
Mon Jan 25 22:30:02 CST 2021
Mon Jan 25 22:30:04 CST 2021
Mon Jan 25 22:30:06 CST 2021
三、线程礼让
线程礼让,JDK提供的方法为Thread.yield();
- 让当前正在执行的线程暂停,但不阻塞。
- 将线程从 运行状态 转为 就绪状态 。
- 让CPU重新调度,礼让 不一定成功。
代码示例:
public class TestYield implements Runnable { public static void main(String[] args) { TestYield testLamda = new TestYield(); new Thread(testLamda,"线程A").start(); new Thread(testLamda,"线程B").start(); } @Override public void run() { System.out.println(Thread.currentThread().getName() + "开始执行"); Thread.yield(); // 线程礼让 System.out.println(Thread.currentThread().getName() + "执行结束"); } }
多次运行结果如下图:

四、线程强制执行
线程的强制执行,用的是join方法,需要new一个Thread对象,然后调用该方法;
该方法的作用是:优先执行join进来的线程中的方法。就好比排队时插队。
代码示例:
public class TestJoin implements Runnable { public static void main(String[] args) { TestJoin test = new TestJoin (); // 需要创建对象 Thread th = new Thread(test,"线程A"); th.start(); for (int i = 0; i < 100; i++) { if(i == 20){ // 当i == 20时,VIP线程插入,强制执行; try { th.join(); } catch (InterruptedException e) { e.printStackTrace(); } }
// 这是main线程执行的语句 System.out.println("我是普通用户" + i); } } @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println("我是VIP,我先来" + i); } } }
执行结果截图:可以看出,VIP进来后,所有的数据跑完,才跑main中的数据。

线程的优先级
Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。(注:优先级高的不一定100%最先执行)
线程的优先级使用数字来表示,范围从1-10,其中1、5、10还可以用以下方式表示:
- Thread.MIN_PRIORITY => 1;
- Thread.MAX_PRIORITY => 10;
- Thread.NORM_PRIORITY => 5;
线程优先级的设置/获取方式分别是:
- getPriority() 获取优先级;
- setPriority(int num) 设置优先级;
如果要设置线程的优先级,一定要先设置,再调用start()方法。
代码示例:
public class Test implements Runnable { public static void main(String[] args) { // main线程的优先级 System.out.println(Thread.currentThread().getName() + "的优先级为 -->" + Thread.currentThread().getPriority()); Test test = new Test(); Thread th1 = new Thread(test); Thread th2 = new Thread(test); Thread th3 = new Thread(test); Thread th4 = new Thread(test); Thread th5 = new Thread(test); th1.setPriority(Thread.MIN_PRIORITY); th2.setPriority(Thread.MAX_PRIORITY); th3.setPriority(6); th4.setPriority(3); th1.start(); th2.start(); th3.start(); th4.start(); th5.start(); // 没有设置,则是默认的优先级,即5 } @Override public void run() { System.out.println(Thread.currentThread().getName() + "的优先级为 -->" + Thread.currentThread().getPriority()); } }
多次运行,结果如图:优先级高的,并不一定最先执行。


线程同步(重点)
线程同步是用来解决并发(多个线程操作同一资源的现象,即为并发)问题的。
线程同步本质就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再进入。
当然,仅仅有队列还是不行的,这样是不安全的,为了安全,需要用到锁机制,队列加锁才能真正意义上实现 线程同步。
当一个线程获得对象的锁,其他线程则必须挂起、等待,这样就会引起性能问题,正所谓:鱼与熊掌不可兼得,性能和安全亦是如此。
线程不安全示例:
示例一:买火车票:
public class Test implements Runnable {
// 定义10张火车票 private int ticket = 10;
// 定义状态位用来终止线程 private boolean flag = true; public static void main(String[] args) { Test te = new Test(); new Thread(te, "张三").start(); new Thread(te, "李四").start(); new Thread(te, "王五").start(); } @Override public void run() { while(flag){ if(ticket <= 0){ flag = false; } try { // 延迟 可以放大问题 Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "买了第" + ticket-- + "张票"); } } }
不安全现象:多人买同一张票、票数出现负数。

原因:每一个线程都是一个独立执行的路径,每个线程都在自己的工作内存中交互,当看到最后一张票时,各自把最后一张票拿到自己的内存中进行处理,从而导致数据不安全。
对于线程不安全的处理,可以使用synchronized关键字来处理,它有2种使用方法:
- synchronized方法:在方法上加synchronized关键字;
- synchronized块:将对象加锁,具体语法:synchronized(Object){执行体}
因此买火车票的安全处理方式,是将买票的方法加锁,即可实现安全买票。
方法加锁有2种方式:
一是:synchronized 的方式,它可以隐式的加锁,解锁;
二是:Lock锁的方式,它是显示的加锁(lock()),解锁(unlock()),详见 ReentrantLock类的源码。

此外,synchronized还可以锁对象,Lock锁是无法锁对象的。
线程通讯
线程间通讯需要使用到线程的wait()方法和notify()notifyAll()方法。
线程池
线程池,其实就是一个能容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多的资源;
创建线程池的方式:通常,线程池都是通过线程池工厂创建,再调用线程池中的方法获取线程,再通过线程去执行任务方法,这里以Runnable的方式为例:
- 创建线程池对象;
- 创建 Runnable 接口子类的实例对象;
- 提交 Runnable 接口子类对象;
- 关闭线程池;
// 创建线程池的方式 10是线程池中线程的个数。
ExecutorService threadService = Executors.newFixedThreadPool(10);
// 创建Runnable的实例对象
MyRunnable myRunnable = new MyRunnable();
// 从线程池中获取线程对象
threadService.submit(myRunnable);
// 关闭连接池
threadService.shutdown();
注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。将使用完的线程又归还到了线程池中。
浙公网安备 33010602011771号