Java多线程基础
Process与Thread
程序:程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念
而进程则是执行程序的一次执行过程,它是一个动态的概念。是系统资源分配的单位
通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程是CPU调度和执行的单位。
注意:很多多线程是模拟出来的,真正的多线程是指有多个cpu,即多核,如服务器。如果是模拟出来的多线程,即在一个cpu的情况下,在同一个时间点,cpu只能执行一个代码,因为切换的很快,所以就有了同时执行的错觉。
核心概念
- 线程就是独立的执行路径
- 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gc线程
- main()称之为主线程,为系统的入口,用于执行整个程序
- 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为干预的
- 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制
- 线程会带来额外的开销,如cpu调度事键,并发控制开销
- 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
三种进程创建方式
1.集成Thread类(重点)
创建线程方式一:继承Thread类,重写run方法,调用start开启线程
步骤:
- 自定义线程类继承Thread类
- 重写run()方法,编写线程执行体
- 创建线程对象,调用start()方法启动线程
public class TestThread extends Thread {
@Override
public void run() {
//run方法线程体
for(int i = 0;i < 20;i++){
System.out.println("I'm looking code " + i);
}
}
public static void main(String[] args) {
//main主线程
//创建一个线程对象
TestThread testThread = new TestThread();
//调用start方法开启线程
testThread.start();
for(int i = 0;i < 20;i++){
System.out.println("I'm studying thread " + i);
}
}
}
两条线程交替执行
运行结果:
I’m looking code 0
I’m studying thread 0
I’m looking code 1
I’m studying thread 1
I’m looking code 2
I’m looking code 3
I’m looking code 4
I’m looking code 5
I’m looking code 6
I’m looking code 7
I’m looking code 8
I’m looking code 9
I’m looking code 10
I’m looking code 11
I’m looking code 12
I’m looking code 13
I’m looking code 14
I’m looking code 15
I’m studying thread 2
I’m looking code 16
I’m looking code 17
I’m studying thread 3
I’m studying thread 4
I’m studying thread 5
I’m studying thread 6
I’m studying thread 7
I’m studying thread 8
I’m studying thread 9
I’m studying thread 10
I’m studying thread 11
I’m studying thread 12
I’m studying thread 13
I’m studying thread 14
I’m studying thread 15
I’m studying thread 16
I’m studying thread 17
I’m studying thread 18
I’m studying thread 19
I’m looking code 18
I’m looking code 19
注意:线程开启不一定立即执行,由CPU安排调度
thread练习:实现多线程同步下载
需要引入commons-io依赖:
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
编写线程类:
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("download " + name);
}
public static void main(String[] args) {
TestThread2 t1 = new TestThread2("https://img-home.csdnimg.cn/images/20211210050448.jpg","1.jpg");
TestThread2 t2 = new TestThread2("https://img-bss.csdn.net/1635839557569.jpg","2.jpg");
TestThread2 t3 = new TestThread2("https://img-bss.csdn.net/1639039638150.png","3.png");
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 Exception,downloader error");
}
}
}
我们预测的执行顺序,可能是先t1,再t2,最后t3。其实是这三个进程同时执行,没有先后顺序,运行结果也不固定。
运行结果(多种情况):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xSJiC9Ya-1639450526884)(C:\Users\17863\AppData\Roaming\Typora\typora-user-images\image-20211211191650324.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wxtG7qL0-1639450526886)(C:\Users\17863\AppData\Roaming\Typora\typora-user-images\image-20211211191708075.png)]
2.实现Runnable接口(重点)
步骤:
- 定义MyRunnable类实现Runnable接口
- 实现run()方法,编写线程执行体
- 创建线程对象,调用start()方法启动线程
public class TestThread3 implements Runnable {
@Override
public void run() {
for(int i = 0;i < 20;i++){
System.out.println("I'm looking code " + i);
}
}
public static void main(String[] args) {
//创建runnable接口的实现类对象
TestThread3 testThread3 = new TestThread3();
new Thread(testThread3).start();
for(int i = 0;i < 20;i++){
System.out.println("I'm studying thread " + i);
}
}
}
运行结果:
I’m studying thread 0
I’m looking code 0
I’m studying thread 1
I’m looking code 1
I’m looking code 2
I’m studying thread 2
I’m looking code 3
I’m studying thread 3
I’m looking code 4
I’m studying thread 4
I’m looking code 5
I’m studying thread 5
I’m looking code 6
I’m studying thread 6
I’m looking code 7
I’m studying thread 7
I’m looking code 8
I’m studying thread 8
I’m looking code 9
I’m studying thread 9
I’m studying thread 10
I’m studying thread 11
I’m studying thread 12
I’m studying thread 13
I’m studying thread 14
I’m looking code 10
I’m looking code 11
I’m looking code 12
I’m looking code 13
I’m looking code 14
I’m looking code 15
I’m looking code 16
I’m looking code 17
I’m studying thread 15
I’m looking code 18
I’m studying thread 16
I’m studying thread 17
I’m looking code 19
I’m studying thread 18
I’m studying thread 19
仍是同时交替执行
thread练习:实现多线程同步下载
引入commons-io:
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
编写线程类:
public class TestThread2 implements Runnable {
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("download " + name);
}
public static void main(String[] args) {
TestThread2 t1 = new TestThread2("https://img-home.csdnimg.cn/images/20211210050448.jpg","1.jpg");
TestThread2 t2 = new TestThread2("https://img-bss.csdn.net/1635839557569.jpg","2.jpg");
TestThread2 t3 = new TestThread2("https://img-bss.csdn.net/1639039638150.png","3.png");
new Thread(t1).start();
new Thread(t2).start();
new Thread(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 Exception,downloader error");
}
}
}
运行结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-APVXuapx-1639450526886)(C:\Users\17863\AppData\Roaming\Typora\typora-user-images\image-20211211192925142.png)]
对比继承Thread类和实现Runnable接口两种方法
继承Thread类:
- 子类继承Thread类具备多线程能力
- 启动线程:子类对象.start()
- 不建议使用:避免OOP单继承局限性
实现Runnable接口:
- 实现接口Runnable具有多线程能力
- 启动线程:传入目标对象+Thread对象.start()
- 推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用
初识并发问题
举一个多线程操作同一对象的例子
模拟买火车票的场景
public class TestThread4 implements Runnable {
//票数
private int ticketNums = 10;
@Override
public void run() {
while(true){
if(ticketNums <= 0){
break;
}
System.out.println(Thread.currentThread().getName() + "拿到了第 " + ticketNums-- + " 张票");
}
}
public static void main(String[] args) {
TestThread4 ticket = new TestThread4();
new Thread(ticket,"user1").start();
new Thread(ticket,"student").start();
new Thread(ticket,"teacher").start();
}
}
运行结果:
user1拿到了第 9 张票
teacher拿到了第 9 张票
student拿到了第 10 张票
teacher拿到了第 7 张票
user1拿到了第 8 张票
teacher拿到了第 5 张票
student拿到了第 6 张票
teacher拿到了第 3 张票
teacher拿到了第 1 张票
user1拿到了第 4 张票
student拿到了第 2 张票
可以发现一个问题:多个线程操作同一个资源的情况下,线程不安全,数据紊乱
龟兔赛跑
- 首先来个赛道距离,然后要离终点越来越近
- 判断比赛是否结束
- 打印出胜利者
- 龟兔赛跑开始
- 故事中是乌龟赢的,兔子需要睡觉,所以我们来模拟兔子睡觉
- 终于,乌龟赢得比赛
public class Race implements Runnable{
private static String winner;
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if(Thread.currentThread().getName().equals("兔子") && i % 50 == 0){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//判断比赛是否结束
boolean flag = gameover(i);
if(flag){
//比赛结束
break;
}
System.out.println(Thread.currentThread().getName() + "跑了 " + i + "步");
}
}
//判断是否完成比赛
private boolean gameover(int steps){
//判断是否有胜利者
if(winner != null){
return true;
} else {
if(steps >= 100){
winner = Thread.currentThread().getName();
System.out.println("winner is " + winner);
return true;
}
}
return false;
}
public static void main(String[] args) {
Race race = new Race();
new Thread(race,"兔子").start();
new Thread(race,"乌龟"