Java-线程-多线程详解


Java-线程-多线程详解

线程,进程,多线程

  • 线程与线程之间互不干扰,独立执行
  • 程序运行时,没有建立自己的线程,后台也会有多个线程,比如主线程,gc线程(垃圾回收线程)
  • main() 称之为主线程,为系统入,用于执行整个程序
  • 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为干预的。
  • 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制
  • 线程会带来额外的开销,如cpu调度时间,并发控制开销
  • 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致

法一创建线程 - 继承Thread类

三种创建方式

image-20221114092147928

  • 继承thread类
  • 重写run方法
  • 调用start()开启线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class TestThread1 extends Thread{

@Override
public void run(){
//run方法
for (int i=0;i<20;i++){
System.out.println("我在看代码---"+i);
}
}

public static void main(String[] args) {
//main线程、主线程

//创建一个线程对象
//调用start()方法
TestThread1 testThread1 = new TestThread1();
testThread1.start();

for (int i = 0; i < 20; i++) {
System.out.println("我在学习多线程---"+i);
}
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
我在看代码---0
我在看代码---1
我在学习多线程---0
我在看代码---2
我在学习多线程---1
我在看代码---3
我在学习多线程---2
我在看代码---4
我在学习多线程---3
我在看代码---5
我在学习多线程---4
我在学习多线程---5
我在看代码---6
我在学习多线程---6
我在学习多线程---7
我在学习多线程---8
我在学习多线程---9
我在学习多线程---10
我在学习多线程---11
我在看代码---7
我在看代码---8
我在学习多线程---12
我在学习多线程---13
我在看代码---9
我在学习多线程---14
我在看代码---10
我在学习多线程---15
我在学习多线程---16
我在学习多线程---17
我在学习多线程---18
我在学习多线程---19
我在看代码---11
我在看代码---12
我在看代码---13
我在看代码---14
我在看代码---15
我在看代码---16
我在看代码---17
我在看代码---18
我在看代码---19

注意

  • 线程开启不一定立即执行,由cpu调度执行

案例:下载图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package Demo01;


import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;

//练习Thread,实现多线程同步下载图片
public class TestThread2 extends Thread{

private String 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) {
TestThread2 t1 = new TestThread2("http://static.runoob.com/images/demo/demo1.jpg","1.jpg");
TestThread2 t2 = new TestThread2("http://static.runoob.com/images/demo/demo2.jpg","2.jpg");
TestThread2 t3 = new TestThread2("http://static.runoob.com/images/demo/demo3.jpg","3.jpg");


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异常,download方法出现问题");
}
}
}

法二创建线程 - 实现Runnable接口

  • 实现runnable接口,
  • 重写run方法,
  • 执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package Demo01;

//创建线程方式二,实现runnable接口,重写run方法,执行
public class TestThread3 implements Runnable{
@Override
public void run(){
//run方法
for (int i=0;i<20;i++){
System.out.println("我在看代码---"+i);
}
}

public static void main(String[] args) {
//main线程、主线程

//创建runnable接口的实现类对象
TestThread3 testThread3 = new TestThread3();
//创建线程对象,通过线程对象来开启我们的线程,代理
Thread thread = new Thread(testThread3);

thread.start();

//简写
// new Thread(testThread3).start();

for (int i = 0; i < 20; i++) {
System.out.println("我在学习多线程---"+i);
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
我在看代码---0
我在看代码---1
我在学习多线程---0
我在看代码---2
我在看代码---3
我在学习多线程---1
我在看代码---4
我在学习多线程---2
我在看代码---5
我在学习多线程---3
我在看代码---6
我在看代码---7
我在看代码---8
我在看代码---9
我在看代码---10
我在看代码---11
我在看代码---12
我在看代码---13
我在看代码---14
我在看代码---15
我在看代码---16
我在看代码---17
我在看代码---18
我在学习多线程---4
我在看代码---19
我在学习多线程---5
我在学习多线程---6
我在学习多线程---7
我在学习多线程---8
我在学习多线程---9
我在学习多线程---10
我在学习多线程---11
我在学习多线程---12
我在学习多线程---13
我在学习多线程---14
我在学习多线程---15
我在学习多线程---16
我在学习多线程---17
我在学习多线程---18
我在学习多线程---19

小结

  • 继承Thread类
    • 子类继承Thread类具备多线程能力
    • 启动线程:子类对象.start()
    • 不建议使用:避免OOP单继承局限性
  • 实现Runnable接口
    • 实现接口Runnable具有多线程能力
    • 启动线程:传入目标对象+Thread()对象.start()
    • 推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用

初识并发问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package Demo01;

//多个线程同时操作同一个对象
//买火车票的例子

//发现问题,多个线程操作同一个资源,线程不安全,数据紊乱
public class TestThread4 implements Runnable{

//票数
int ticketNums = 10;

@Override
public void run(){
while (true){
if (ticketNums<=0){
break;
}
//模拟延时
try{
Thread.sleep(200);
} catch (InterruptedException e){
e.printStackTrace();
}

System.out.println(Thread.currentThread().getName()+"拿到了第"+ticketNums--+"票");
}
}

public static void main(String[] args) {
TestThread4 ticket = new TestThread4();

new Thread(ticket,"小明").start();
new Thread(ticket,"老师").start();
new Thread(ticket,"黄牛").start();
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
黄牛拿到了第9
老师拿到了第10
小明拿到了第8
老师拿到了第7
黄牛拿到了第7
小明拿到了第6
老师拿到了第5
小明拿到了第3
黄牛拿到了第4
小明拿到了第2
黄牛拿到了第0
老师拿到了第1
小明拿到了第-1

龟兔赛跑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package Demo01;

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%10==0){
try {
Thread.sleep(200);
} 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;
}{
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,"乌龟").start();
}
}

法三创建线程 - 实现Callable接口

  1. 实现Callable接口,需要返回值类型
  2. 重写call方法,需要抛出异常
  3. 创建目标对象
  4. 创建执行服务:
  5. 提交执行:
  6. 获取结果:
  7. 关闭服务:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package Demo02;

import Demo01.TestThread2;
import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.*;

//线程创建方式三:实现Callable接口
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 true;
}

public static void main(String[] args) throws ExecutionException, InterruptedException {
TestCallable t1 = new TestCallable("http://static.runoob.com/images/demo/demo1.jpg","1.jpg");
TestCallable t2 = new TestCallable("http://static.runoob.com/images/demo/demo2.jpg","2.jpg");
TestCallable t3 = new TestCallable("http://static.runoob.com/images/demo/demo3.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();

}
}


//下载器
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异常,download方法出现问题");
}
}
}
1
2
3
4
5
6
下载了文件名为:1.jpg
下载了文件名为:3.jpg
下载了文件名为:2.jpg
true
true
true

静态代理

  • 真实对象与代理对象实现同一个接口
  • 代理对象要代理真实角色
  • 好处:
    • 代理对象可以做真实对象做不了的事情
    • 真实对象专注做自己的事情
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package proxystatic;

public class StaticProxy {

public static void main(String[] args) {
// new Thread(()-> System.out.println("I Love You")).start();
// new WeddingCompany(new You()).HappyMarry();
You you = new You();

WeddingCompany weddingCompany = new WeddingCompany(you);

weddingCompany.HappyMarry();
}

}

interface Marry{
void HappyMarry();
}

class You implements Marry{
@Override
public void HappyMarry(){
System.out.println("秦老师要结婚了,超开心");
}
}

class WeddingCompany implements Marry{
private Marry target;

public WeddingCompany(Marry target){
this.target = target;
}

@Override
public void HappyMarry(){
before();
this.target.HappyMarry();
after();
}

private void before(){
System.out.println("结婚之前,布置现场");
}

private void after(){
System.out.println("结婚之后,收尾款");
}
}
1
2
3
结婚之前,布置现场
秦老师要结婚了,超开心
结婚之后,收尾款
  • Thread类是一个代理,实现了Runnable接口
  • 里面真实的类也实现了Runnable接口
  • 所以Thread类可以做真实类的代理

Lambda表达式

目的是简化程序

image-20221116151439125

image-20221116151521313

image-20221116151645947

各种类与lambda表达式

越来越方便书写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package lambdaShow;
/*
* 推导lambda表达式
* */
public class TestLambda1 {

//3.静态内部类
static class Like2 implements ILike{
@Override
public void lambda(){
System.out.println("i like lambda2");
}
}

public static void main(String[] args) {
ILike like = new Like();
like.lambda();

like = new Like2();
like.lambda();

//4.局部内部类
class Like3 implements ILike{
@Override
public void lambda(){
System.out.println("i like lambda3");
}
}

like = new Like3();
like.lambda();


//5.匿名内部类,没有类的名称,必须借助接口或者父类
like = new ILike() {
@Override
public void lambda() {
System.out.println("i like lambda4");
}
};
like.lambda();


//6.用lambda简化
like = () -> {
System.out.println("i like lambda5");
};
like.lambda();


}

}

//1.定义一个函数式接口
interface ILike{
void lambda();
}

//2.实现类
class Like implements ILike{
@Override
public void lambda(){
System.out.println("i like lambda");
}
}
1
2
3
4
5
i like lambda
i like lambda2
i like lambda3
i like lambda4
i like lambda5

带参数的lambda表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package lambdaShow;

public class TestLambda2 {


public static void main(String[] args) {
ILove love = (int a)->{
System.out.println("i love you-->"+a);
};
love.love(2);
}



}

interface ILove{
void love(int a);
}
1
i love you-->2

微小简化lambda表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package lambdaShow;

public class TestLambda2 {


public static void main(String[] args) {

ILove love = null;
// ILove love = (int a)->{
// System.out.println("i love you-->"+a);
// };
// love.love(2);

//简化1.去掉参数类型
// love = (a) ->{
// System.out.println("i love you-->"+a);
// };
// love.love(2);
//简化2.去掉括号
// love = a->{
// System.out.println("i love you-->"+a);
// };
// love.love(2);
//简化3.去掉花括号
love = a-> System.out.println("i love you-->"+a);
love.love(2);
//总结:代码如果有多行,不可以简化花括号
//多个参数传递,要带回来括号,但是可以都省略参数类型
}



}

interface ILove{
void love(int a);
}
1
i love you-->2

回顾

  • Runnable接口中,就只有一个方法,所以我们可以使用lambda表达式来实现这个接口

线程停止

  • 线程状态

image-20221116180958635

  • 线程方法

image-20221116181130439

  • 停止线程
    • 不推荐使用JDK提供的stop(), destroy()方法【已废弃】
    • 推荐线程自己停下来
    • 建议使用一个标志位进行终止变量,当flag=false,则终止线程运行

image-20221116181451681

1
2
3
4
5
6
7
8
9
10
11
12
run...Thread816
run...Thread817
main899
main900
run...Thread818
run...Thread819
run...Thread820
run...Thread821
线程停止了
main901
main902
main903

线程休眠_sleep

  • sleep指定当前线程阻塞的毫秒数
  • sleep存在异常InterruptedException
  • sleep时间达到后,线程进入就绪状态
  • sleep可以模拟网络延时,倒计时等
  • 每一个对象都有一个锁,sleep不会释放锁
1
2
// cpu太快了,没有延迟,可能看不到代码背后的问题,比如线程不安全问题
// 网络延时可以放大问题的发生性

比如之前的买票代码,没有延迟,就可能一个人买完了票

模拟倒计时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package lambdaShow;
// 模拟倒计时
public class TestSleep2 {


public static void main(String[] args) {

try {
tenDown();
} catch (InterruptedException e) {
e.printStackTrace();
}

}


public static void tenDown() throws InterruptedException {
int num = 10;
while(true){
Thread.sleep(1000);
System.out.println(num--);
if(num<=0){
break;
}
}

}

}
1
2
3
4
5
6
7
8
9
10
11
10
9
8
7
6
5
4
3
2
1
0

模拟时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package state;

import java.text.SimpleDateFormat;
import java.util.Date;

// 模拟倒计时
public class TestSleep2 {


public static void main(String[] args) {

Date startTime = new Date(System.currentTimeMillis());

while (true){
try{
Thread.sleep(1000);
System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));
startTime = new Date(System.currentTimeMillis());
}catch (InterruptedException e){
e.printStackTrace();
}
}

}


public static void tenDown() throws InterruptedException {
int num = 10;
while(true){
Thread.sleep(1000);
System.out.println(num--);
if(num<=0){
break;
}
}

}

}
1
2
3
4
5
14:54:58
14:54:59
14:55:00
14:55:01
14:55:02

线程礼让

  • 礼让线程,让当前正在执行的线程暂停,但不阻塞
  • 将线程从运行态,转为就绪状态
  • 让cpu重新调度,礼让不一定成功!看cpu心情
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package state;

public class TestYield {

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()+"线程停止执行");
}
}

礼让成功

1
2
3
4
b线程开始执行
a线程开始执行
b线程停止执行
a线程停止执行

礼让失败

1
2
3
4
a
a
b
b

例子不好没试出来

线程强制执行_join

相当于插队

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package state;

public class TestJoin implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("线程vip来了"+i);
}
}

public static void main(String[] args) throws InterruptedException {
//启动线程
TestJoin testJoin = new TestJoin();
Thread thread = new Thread(testJoin);
thread.start();
thread.sleep(1000);

//主线程
for (int i = 0; i < 1000; i++) {
if(i==200){
thread.join();
}
System.out.println("main"+i);
}
}
}

img

线程状态观测

  • Thread.State
    • NEW //尚未启动的线程处于此状态
    • RUNNABLE //在Java虚拟机中执行的线程处于此状态
    • BLOCKED //被阻塞等待监视器锁定的线程处于此状态
    • WAITING //正在等待另一个线程执行特定动作的线程处于此状态
    • TIMED_WAITING //正在等待另一个线程执行动作达到指定等待时间的线程处于此状态
    • TERMINATED //已退出的线程处于此状态

线程优先级

  • Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按学号优先级决定应该调度哪个线程来执行。
  • 线程优先级用数字表示,范围从1~10,在Thread中定义了几个常量来表示优先级
    • Thread.MIN_PRIORITY = 1;
    • Thread.MAX_PRIORITY = 10;
    • Thread.NORM_PRIORITY = 5; //默认优先级,不设置的话就是默认优先级
  • 线程优先级高不一定先执行,但是给它的资源就会多一些
  • 使用以下方式来改变或获取优先级
    • getPriority() //获取优先级
    • setPriority(int xxx) //改变优先级

守护线程

  • 线程分为用户线程和守护线程
  • 虚拟机必须确保用户线程执行完毕
  • 虚拟机不用等待守护线程执行完毕
  • 守护线程:后台记录操作日志,监控内存,垃圾回收等待

通过 setDaemon(true) 方法来设置守护线程,参数为true是守护线程,默认是false用户线程

线程同步

  • 并发:同一个对象被多个线程同时操作

    • 上万人同时抢100张票
    • 两个银行同时取钱
  • 在现实生活中,我们遇到“同一个对象被多个线程同时操作”这种问题,比如食堂打饭,解决办法就是排队

  • 处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象,这时候我们就需要线程同步。线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程在使用。

  • 队列和锁:在线程访问对象时,加入锁机制 synchronized, 当一个线程获得对象的排他锁,独占资源,其他线程必须等待,使用后释放锁即可,存在以下问题

    • 一个线程持有锁会导致其他所有需要此锁的线程挂起
    • 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换,和调度延时,引起性能问题
    • 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级导致,引起性能问题

三大不安全案例

买票案例

线程不安全,票数出现负数

每个线程都在自己的工作内存交互,内存控制不当会造成数据不一致

买票时,每个线程都把票数拿到自己的内存,当票数只剩一张时,所以它们看到的都是1,然后都去拿这一张票,当第一个线程拿到这张票时,票数修改为0,第二个线程再去拿时,票数就变成了-1.

银行取钱案例

线程不安全的集合

以ArrayList 为例

我们加入了10000个元素,实际加入集合的只有7761个,原因是当这些线程在同一瞬间把集合元素添加到集合的同一位置,就覆盖掉了前面的元素,所以集合中的元素数量才会少

如何解决不安全的问题?


Author: Liang Junyi
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint policy. If reproduced, please indicate source Liang Junyi !
  TOC