1.1 线程、进程、多线程

1.1.1 普通方法调用和多线程

1.1.2 程序 进程 线程

一个进程可以有多个线程,比如视频中同时听到声音、看到图像和弹幕等等。

1.1.3 Process与Thread

  • 说起进程,就不得不说下程序。程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。

  • 进程则是执行程序的一次执行过程,它是一个动态的概念。是系统资源分配的单位。

  • 通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。

  • 线程是CPU调度和执行的单位。

    执行程序 -> 进程 -> 线程

1
注意:很多多线程是模拟出来的,真正的多线程是指有多个cpu,即多核,如服务器。如果是模拟出来的多线程,即在只有一个cpu的情况下,在同一个时间点,cpu只能执行一个代码,因为切换的很快,所以就有同时执行的“感觉”。

1.1.4 总结

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

1.2 线程创建

  • Thread 类->继承Thread类
  • Runnable 接口->实现Runnable接口(重点)
  • Callable 接口->实现Callable接口

1.2.1 Thread类

  • 自定义线程类继承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
25
26
27
28
29
30
31
32
package com.kibo24.demo1;

//创建线程方式1--继承Thread类,重写run()方法,调用start()方法

public class TestThread 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()线程

//创建一个线程对象
TestThread testThread1=new TestThread();
testThread1.start();

//调用start()方法
for (int i = 0; i < 2000; i++)
{
System.out.println("海子姐ybb--"+i);
}
}
}

线程交替执行,每次执行结果不同

总结

  • 线程开启不一定立即执行,由cpu调度执行;
1.2.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
53
54
55
56
57
58
59
60
61
62
package com.kibo24.demo1;

//需要导入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;
private String name;

public TestThread2(String url,String name)
{
this.url=url; //网络图片地址
this.name=name; //保存的文件名
}

@Override
public void run()
{
//重写run线程主体
WebDownloader wd=new WebDownloader();
wd.downloader(url,name);
System.out.println("下载的文件名为"+name);
}

public static void main(String[] args)
{
//构造函数

TestThread2 testThread1=new TestThread2("https://kibo24-1305312055.cos.ap-beijing.myqcloud.com/QQ%E5%9B%BE%E7%89%8720210912143516.gif","嘉然.gif");
TestThread2 testThread2=new TestThread2("https://kibo24-1305312055.cos.ap-beijing.myqcloud.com/QQ%E5%9B%BE%E7%89%8720210912143700.jpg","唐可可.gif");
TestThread2 testThread3=new TestThread2("https://kibo24-1305312055.cos.ap-beijing.myqcloud.com/QQ%E5%9B%BE%E7%89%8720210912143743.jpg","喜羊羊.gif");

testThread1.run();
testThread2.run();
testThread3.run();

}
}

//下载器
class WebDownloader
{
public void downloader(String url,String name)
{
try
{
//把url变成一个文件
//使用commons-io中的一个类
FileUtils.copyURLToFile(new URL(url),new File(name));
} catch (IOException e)
{
System.out.println("IO异常,downloader方法出现异常");
e.printStackTrace();
}
}
}

image-20210912144554812

运行结果

  • 运行结果不一定按顺序,由cpu调度安排
  • 由于此次测试数量比较少,cpu可以按顺序安排线程

1.2.2 Runnable接口(重要)

代码

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 com.kibo24.demo1;

//创建线程的第二个方法:Runnable接口,重写run()方法,
//执行线程需要丢入Runnable接口的实现类,调用start()方法
//注意这次没有继承Thread接口
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)
{
//创建Runnable接口的实现类对象
TestThread3 testThread3=new TestThread3();

//创建线程对象,通过线程对象来来开启线程
Thread thread=new Thread(testThread3);

//通过Thread开启线程
thread.start();

//以上代码同下
new Thread(testThread3).start();

//调用start()方法
for (int i = 0; i < 2000; i++)
{
System.out.println("海子姐ybb--"+i);
}
}
}

image-20210912151032864

注意

  • 创建“TestThread”类,implements Runnable;
  • 在TestThread类中重写(因为此时不继承Thread,所以严格来说不算是重写)run()方法(即编写需要执行的代码);
  • 创建TestThread类的对象和Thread的对象;
  • 创建Thread类对象时把TestThread的对象放进其构造函数中;
  • 通过Thread的对象启动线程;

或者

  • 直接使用 new Thread(TestThread的对象).start() 开启线程;

运行结果

image-20210912151109293

1.2.3 实现Callable接口(暂时不进行详解)

1.2.4 总结

继承Thread类

实现Runnable接口(重要)

  • 实现Runnable接口,具有多线程能力;
  • 启动线程:传入目标线程对象+Thread对象.start();
  • 推荐使用:避免单继承局限性(接口相对于继承的优点),灵活方便,便于同一个对象被多个线程使用
1.2.3.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
package com.kibo24.demo1;

//多个线程同时操作同一个对象
//买火车票的例子
public class TestThread4 implements Runnable
{
//票数
private int ticketNum=10;

//重写run()方法
@Override
public void run()
{
while(true)
{
if(ticketNum<=0)
{
break;
}
//Thread.currentThread().getName()可以得到当前线程的名字
System.out.println(Thread.currentThread().getName()+"买到了"+ticketNum--+"号票");
}
}

public static void main(String[] args)
{
TestThread4 testThread4=new TestThread4();
new Thread(testThread4,"嘉然").start();
new Thread(testThread4,"七海").start();
new Thread(testThread4,"二十四").start();
}
}

结果

  • 每次执行的结果不同;
  • 多个线程操作同一个资源的情况下可能会出现“两个人拿到同一张票”的错误(当然本次没有发生这种数据并发的问题)
image-20210912153532205 image-20210912153750706
1.2.3.2 【龟兔赛跑-并发问题】

情景设定

  • 存在准确赛道长度;
  • 判断比赛是否结束;
  • 判断谁是胜者;
  • 龟兔赛跑开始执行;
  • 胜负结果以故事原文的乌龟获胜为准;
  • 兔子会睡觉。
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
71
72
73
74
75
76
77
package com.kibo24.demo1;

//模拟龟兔赛跑
public class Race implements Runnable
{
private static String winner;

@Override
public void run()
{
int m=0;
if(Thread.currentThread().getName().equals("兔子"))
{
m=2;
}
else if(Thread.currentThread().getName().equals("乌龟"))
{
m=1;
}
for (int i = 0; i <= 100; i+=m)
{
//限制当前线程是兔子才可以进入if结构
if(Thread.currentThread().getName().equals("兔子")&& (i==50))
{
try
{ //当前线程延迟1ms
Thread.sleep(1);
} catch (InterruptedException e)
{
e.printStackTrace();
}
}

//如果比赛结束就停止循环
boolean flag=gameOver(i);
if(flag)
{
break;
}
System.out.println(Thread.currentThread().getName()+"跑了"+i+"步");

}
}

//判断i是不是已经到达100
private boolean gameOver(int steps)
{
if(winner!=null)
{//已经存在胜者
return true;
}
else
{
if(steps>=100)
{

winner=Thread.currentThread().getName();
System.out.println(winner+"赢了!");
return true;
}
else
{
return false;
}
}
}

public static void main(String[] args)
{
Race race=new Race();

//理解为龟兔两条赛道,两条线程
new Thread(race,"兔子").start();
new Thread(race,"乌龟").start();

}
}

结果

  • 最后可能会出现赢了以后乌龟仍然向前跑的情况,这种情况较难避免,可能是因为虚拟机停止需要时间

1.3 静态代理模式

  • 用以下例子与多线程接口作比较
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
public class StaticPolicy
{
public static void main(String[] args)
{
You you=new You();

//二者都实现了Runnable接口
new Thread(()->System.out.println("嘉然你带我走吧")).start();
//代理对象(真实对象)
new WeddingCompany(new You()).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("结婚之后收尾款");
}

}

总结

  • 真实对象和代理对象都要实现同一个接口(Marry/Runnable);
  • 代理对象(WeddingCompany/Thread)要代理真实角色(You/TestThread);
  • 代理对象可以做到许多真实对象做不到的事情;
  • 真实对象专注于做自己的事情;

1.4 Lambda表达式

  • 理解Functional Interface(函数式接口)是学习Java8 lambda的关键所在;

  • 函数式接口的定义

    • 任何接口,如果只包含唯一一个抽象方法,那么他就是一个函数式接口;
    1
    2
    3
    4
    public interface Runnable
    {
    public abstract void fun();
    }
    • 对于函数式接口,可以通过lambda表达式来创建该接口的对象。

作用

  • 避免匿名内部类定义过多;
  • 可以让你的代码看起来很简洁;
  • 去掉无意义的代码,保留核心逻辑。

1.4.1 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
67
68
69
70
71
72
73
//Lambda表达式
public class TestLambda1
{

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


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("ybbbb");
}
}

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


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


//6.Lambda表达式
like=()-> System.out.println("ybbbbbbbbb");
like.Lambda();
}
}


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


//2.实现类
class Like implements ILike
{
@Override
public void Lambda()
{
System.out.println("ybb");
}
}

1.4.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
public class TestLambda22
{

public static void main(String[] args)
{

//接口名 对象名=(参数)->{代码};
ILove love=(int a)-> {System.out.println("ybb"+24);};

//简化1:去除参数类型
love=(a)-> {System.out.println("ybb"+24);};

//简化2:去除花括号
love=(a)-> System.out.println("ybb"+24);

//简化3:去除小括号
love=a-> System.out.println("ybb"+24);

//总结
//在只有一行代码的情况下才能去掉花括号
//在只有一个参数的情况下才能去掉小括号
//接口为函数式接口
//多个参数也可以去掉参数类型,如果去掉必须全部去掉
love.Love(24);
}
}


interface ILove
{
void Love(int a);
}

1.5 线程状态

五大状态:

方法 说明
setPriority(int newPriority) 更改线程的优先级
static void sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠
void join() 等待该线程终止
static void yield() 暂停当前正在执行的线程对象,并执行其他线程
void interrupt() 中断线程()
boolean isAlive() 测试线程是否处于活动状态

1.5.1 停止线程

  • 不推荐使用JDK提供的stop()、destroy()等方法;
  • 推荐线程自己终止;
  • 建立使用一个标志位进行终止变量,当flag==false,线程终止。
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
//测试stop
//1.建议线程正常停止-->利用有限次数,不建议使用死循环
//2.设置一个标志位
//3.不要使用JDK提供的过时的方法
public class TestStop implements Runnable
{
//设置标志位
private boolean flag=true;

@Override
public void run()
{
int i=0;
while(flag)
{
System.out.println("线程正在运行--"+i++);
}
}



//自定义方法
public void stop()
{
this.flag=false;
}

public static void main(String[] args)
{
TestStop testStop=new TestStop();
new Thread(testStop).start();

for (int i = 0; i < 1000; i++)
{
System.out.println("main线程--"+i);
if(i==900)
{
//调用stop方法,切换标志位,停止线程
testStop.stop();
System.out.println("线程停止了");
}
}

}
}

结果

一些问题

  • 使用标志位不保证线程一定立刻停止,只是暂时不打印日志了而已;
  • 当删掉代码“System.out.println(“main线程–”+i);”后,Thread线程也不输出数据,原因未知。

1.5.2 线程休眠

  • sleep()指定当前线程阻塞的毫秒数;
  • sleep存在异常InterruptedException;
  • sleep事件达到后线程进入就绪状态;
  • sleep可以模拟网络延时,倒计时等;
  • 每个对象都有一个锁,sleep不会释放锁
1.5.2.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
//模拟网络延时:放大问题的发生性
public class TestSleep implements Runnable
{
private int count=10;
@Override
public void run()
{
while(true)
{
if(count<=0)
{
break;
}
try
{
Thread.sleep(100);
} catch (InterruptedException e)
{
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"买到了"+count--+"号票");
}
}

public static void main(String[] args)
{
TestThread4 testThread4=new TestThread4();
new Thread(testThread4,"嘉然").start();
new Thread(testThread4,"七海").start();
new Thread(testThread4,"二十四").start();
}
}
1.5.2.2 倒计时/获取系统时间

代码

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
import java.text.SimpleDateFormat;
import java.util.Date;

//模拟倒计时:sleep()+循环即可
public class TestCountDown
{
public static void countDown()
{
int num=10;
while(true)
{
try
{
Thread.sleep(1000);
} catch (InterruptedException e)
{
e.printStackTrace();
}

System.out.println(num--);

if(num<=0)break;
}
}

public static void main(String[] args)
{
//输出倒计时
countDown();

//打印当前系统时间
//使用System.currentTimeMillis()获取当前系统时间
Date stime=new Date(System.currentTimeMillis());
while(true)
{
try
{
//打印当前时间
//使用 new SimpleDateFormat(格式).format(stime)
System.out.println(new SimpleDateFormat("HH:mm:ss").format(stime));
//暂停1秒
Thread.sleep(1000);
//重新获取当前时间
stime=new Date(System.currentTimeMillis());

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

1.5.3 线程礼让

  • 礼让线程,让当前正在执行的线程暂停,但是不阻塞;
  • 让线程从运行状态转为就绪状态;
  • 让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
//测试礼让线程:不一定成功
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()+"线程停止执行");
}
}

结果

yield是让线程从运行状态转换到就绪状态,让其他线程执行(相当于让其他线程插队,自己则不阻塞队伍),cpu调度执行依然是随机的;

sleep是让线程从运行状态转为阻塞状态,阻塞结束后转为就绪状态

也就是说,在这个例子中,不管写不写yield(),都会出现4种情况

1.5.4 线程合并

  • Join合并线程,等待此线程执行完后,再执行其他线程,其他线程阻塞;
  • 相当于强行插队,让自己先走完(并且会阻塞队伍,使其他线程无法执行)。

代码

  • 思路:先执行main线程,等到i达到200时执行vip线程并让其join(阻塞式插队),等待vip线程执行完毕后,main线程继续执行
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
//测试Join方法:插队并阻塞
public class TestJoin implements Runnable
{
@Override
public void run()
{
for (int i = 0; i < 10; i++)
{
System.out.println("第"+i+"号vip来力");

}
}

public static void main(String[] args)
{
//启动线程
TestJoin testJoin=new TestJoin();
Thread thread=new Thread(testJoin);



//主线程
for (int i = 0; i < 500; i++)
{
System.out.println("你main爷爷来了"+i);
if(i==200)
{
thread.start();
try
{
thread.join();
} catch (InterruptedException e)
{
e.printStackTrace();
}
}


}
}
}

结果

1.5.5 线程状态观测

Thread.State

线程状态,可以处于以下状态之一:

  • New

    尚未启动的线程处于此状态

  • RUNNABLE

    在Java虚拟机中的线程处于此状态

  • BLOCKED

    被阻塞等待监视器锁定的线程处于此状态

  • WAITTING

    正在等待另一个线程执行特定动作的线程处于此状态

  • TIMED_WAITTING

    正在等待另一个线程执行动作达到指定等待时间的线程处于此状态

  • TERMINATED

    已退出的线程处于此状态

一个线程可以在给定时间处于一个状态,这些状态是不反映任何操作系统线程状态的虚拟机状态

代码

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
import com.kibo24.demo1.TestThread;

//观察测试线程状态
public class TestState
{
public static void main(String[] args)
{
Thread thread=new Thread(()->{
for (int i = 1; i <= 5; i++)
{
System.out.println("ybb");
try
{
//每秒输出一次ybb
Thread.sleep(1000);
} catch (InterruptedException e)
{
e.printStackTrace();
}

}
});

//观察状态
Thread.State state=thread.getState();
System.out.println(state);


//观察启动后状态
thread.start(); //启动线程
state=thread.getState();
System.out.println(state);

//如果线程不终止就循环监测
while(state!= Thread.State.TERMINATED)
{
try
{
Thread.sleep(300);
} catch (InterruptedException e)
{
e.printStackTrace();
}
//更新线程状态
state=thread.getState();
System.out.println(state);
}

//终止的线程不能复活
//再次start()会报错
//thread.start();
}
}

结果

1.5.6 线程的优先级

  • Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。
  • 线程优先级用数字来表示,范围[1,10]。
    • Thread.MIN_PRIORITY=1
    • Thread.MAX_PRIORITY=10
    • Thread.NORM_PRIORITY=5
  • 使用以下方式改变优先级
    • getPriority(). setPriority (int priority)

代码

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
///进程优先级:超出范围会报错
public class TestPriority
{
public static void main(String[] args)
{
//主线程默认优先级5
System.out.println(Thread.currentThread().getName()+"优先级:"+Thread.currentThread().getPriority());


//创建线程
Priority priority=new Priority();
Thread t1=new Thread(priority,"1");
Thread t2=new Thread(priority,"2");
Thread t3=new Thread(priority,"3");
Thread t4=new Thread(priority,"4");
Thread t5=new Thread(priority,"5");
Thread t6=new Thread(priority,"6");

//先设置优先级,再使用
t1.start();
t2.setPriority(2);
t2.start();
t3.setPriority(4);
t3.start();
t4.setPriority(7);
t4.start();
t5.setPriority(1);
t5.start();
t6.setPriority(10);
t6.start();
}
}

class Priority implements Runnable
{
@Override
public void run()
{
System.out.println(Thread.currentThread().getName()+"优先级:"+Thread.currentThread().getPriority());
}
}

结果

  • 优先级高的线程不一定就会跑在优先级低的线程前面;
  • 自己设置的优先级相当于“一个建议”,最终依然要服从cpu调度;
  • 当有网络延迟的时候优先级的作用会更好的体现。

1.5.7 守护线程

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

代码

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
//测试守护线程
//嘉然(守护线程)守护你(用户线程)
public class TestDaemon
{
public static void main(String[] args)
{
Diane diane=new Diane();
You you=new You();

Thread thread=new Thread(diane);
//默认false,表示守护线程
thread.setDaemon(true);

//嘉然守护线程启动
thread.start();

//你的线程启动
new Thread(you).start();
}
}

//嘉然
class Diane implements Runnable
{
@Override
public void run()
{
while(true)
{
System.out.println("嘉然守护着你");
}
}
}

//你
class You implements Runnable
{
@Override
public void run()
{
for (int i = 0; i < 3650; i++)
{
System.out.println("第"+i+"天,你活的好好的");
}
System.out.println("你终于死了,准备重开吧");
}
}

结果

  • 在用户线程达到3649,即执行完毕后,虚拟机停止;
  • 守护线程(嘉然)在用户线程结束后依然跑了一会儿,是因为虚拟机停止需要时间。

1.6 线程同步机制

  • 多个线程操作同一个资源

1.6.1 并发

  • 同一个对象被多个线程同时操作
    • 如抢课,抢票,抢六级名额等

1.6.2 线程同步

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

1.6.3 队列和锁

  • 由于同一进程的多个线程共享同一块存储空间,存在访问冲突的问题,为了保证数据在方法中被访问的正确性,在访问时加入锁机制 synchronized当一个线程获得对象的排他锁,就独占资源,其他线程必须等待,使用后释放锁即可继续执行,锁机制存在的问题:
    • 一个线程持有锁会导致其他所有需要此锁的线程挂起;
    • 在多线程竞争下,加锁或释放锁会导致比较多的上下文切换和调度延时,引发性能问题;
    • 如果一个高优先级线程等待一个低优先级线程释放锁,会导致优先级倒置,引发性能问题。
1.6.3.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

//不安全的买票
public class UnsafeBuyTickets
{

public static void main(String[] args)
{
BuyTickets buyTickets=new BuyTickets();
new Thread(buyTickets,"嘉然").start();
new Thread(buyTickets,"七海").start();
new Thread(buyTickets,"二十四").start();
}
}

class BuyTickets implements Runnable
{
private int count=0;
boolean flag=true;//外部停止方式

@Override
public void run()
{
//买票
while(flag)
{
buy();
}
}

private void buy()
{
//判断是否有票
if(count>=10)flag=false;
System.out.println(Thread.currentThread().getName()+"买到了第"+(count++)+"号票");
}
}

结果

  • 因为没有线程同步(排队),所以有可能出现多个线程争抢同一份资源引发的错误。
1.6.3.2 【不安全的取款】
  • 两个人同时去银行,从同一个账户取钱。

代码

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
//不安全的取款
public class UnsafeBank
{
public static void main(String[] args)
{
Account account=new Account();
account.money=114514;
account.name="七海的礼物";

Bank bank=new Bank(account,50000,"嘉然");
Bank bank2=new Bank(account,70000,"七海");

//这里依然是两个线程操作同一个资源啊
bank.start();
bank2.start();
}
}

class Account
{
int money;//余额
String name;//卡名
}

//银行
class Bank extends Thread
{
//账户
Account account;
//取钱数目
int drawingMoney;
//手里的钱
int nowMoney=0;

public Bank(Account account, int drawingMoney, String name)
{
super(name);//调用父类的有参构造,传入name
this.account = account;
this.drawingMoney = drawingMoney;
}

@Override
public void run()
{
//判断钱够不够
if(account.money<drawingMoney)
{
System.out.println(Thread.currentThread().getName()+"想取"+drawingMoney+",但是没钱了");
return;
}

try
{
Thread.sleep(100);
} catch (InterruptedException e)
{
e.printStackTrace();
}

account.money-=drawingMoney;
nowMoney+=drawingMoney;
System.out.println(Thread.currentThread().getName()+"取走了"+drawingMoney+",还剩"+account.money+",手里现在有"+nowMoney);

}
}

结果

  • 两个线程都先判断余额足够,然后等待一下开始取钱(所以代码中的if判断余额的机制拦不住两个线程)
  • 使用队列和锁让他们排队可以解决问题。
1.6.3.3 【不安全的List】

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.ArrayList;
import java.util.List;

//线程不安全的集合
public class UnsafeList
{
public static void main(String[] args)
{
List<String> list=new ArrayList<String>();
for (int i = 1; i <= 10000; i++)
{
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}

System.out.println("list的大小是(理想是10000):"+list.size());
}
}

结果

  • add命令可能会使多个元素去抢同一块内存,导致list的最终大小减小;

  • 当循环数量更大的时候,丢失的数量也会更大。

1.7 同步方法

  • 由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提出一套机制,这套机制就是synchronized关键字,它有两种用法:synchronized方法synchronized方法块
1
同步方法:public synchronized void method(int args){}
  • synchronized方法控制对“对象”的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线层会阻塞,方法一旦执行,就独占该锁,知道该方法返回才释放锁,后面被阻塞的线程才会获得这个锁,继续执行;
1
缺点:将一个大的方法申明为synchronized方法会影响效率

1.7.1 同步方法的弊端

  • 方法里面需要修改的内容才需要锁,锁的太多会浪费资源;

1.7.2 同步块

  • 同步块:

    1
    synchronized(Obj){}
  • Obj称为同步监视器

    • Obj可以是任何对象,但是推荐使用共享资源作为同步监视器;
    • 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,即这个对象本身,或者是class
  • 同步监视器的执行过程:

    • 第一个线程访问,锁定同步监视器,执行其中的代码;
    • 第二个线程访问,发现同步监视器被锁定,无法访问;
    • 第一个线程访问完毕,解锁同步监视器;
    • 第二个线程访问,发现同步监视器没有锁,进行锁定并开始执行代码。
1.7.2.1 【经过修改的取钱代码】

代码

  • 在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
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
//不安全的取款
public class UnsafeBank
{
public static void main(String[] args)
{
Account account=new Account();
account.money=114514;
account.name="七海的礼物";

Bank bank=new Bank(account,50000,"嘉然");
Bank bank2=new Bank(account,70000,"七海");

//这里依然是两个线程操作同一个资源啊
bank.start();
bank2.start();
}
}

class Account
{
int money;//余额
String name;//卡名
}

//银行
class Bank extends Thread
{
//账户
Account account;
//取钱数目
int drawingMoney;
//手里的钱
int nowMoney=0;

public Bank(Account account, int drawingMoney, String name)
{
super(name);//调用父类的有参构造,传入name
this.account = account;
this.drawingMoney = drawingMoney;
}

@Override
public void run()
{
synchronized (account)
{
//判断钱够不够
if(account.money<drawingMoney)
{
System.out.println(Thread.currentThread().getName()+"想取"+drawingMoney+",但是没钱了");
return;
}

try
{
Thread.sleep(100);
} catch (InterruptedException e)
{
e.printStackTrace();
}

account.money-=drawingMoney;
nowMoney+=drawingMoney;
System.out.println(Thread.currentThread().getName()+"取走了"+drawingMoney+",还剩"+account.money+",手里现在有"+nowMoney);
}

}
}
  • 将run方法用synchronized锁住;
  • 但是因为run中要修改的是account而不是默认的额this(Bank的对象),所以要使用代码块synchronized(account),而不能直接在方法前加上synchronized;
  • 锁的对象就是**变化(增删改)**的对象。

结果

  • 给我排队取钱(锁住之后的排队顺序取决于线程启动(start)的顺序,即先来后到
1.7.2.2 【经过修改的List】

代码

  • 在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
import java.util.ArrayList;
import java.util.List;

//线程不安全的集合
public class UnsafeList
{
public static void main(String[] args)
{
List<String> list=new ArrayList<String>();
for (int i = 1; i <= 10000; i++)
{
new Thread(()->{
synchronized (list)
{
list.add(Thread.currentThread().getName());
}
}).start();
}

try
{
Thread.sleep(300);
} catch (InterruptedException e)
{
e.printStackTrace();
}

System.out.println("list的大小是(理想是10000):"+list.size());
}
}

结果

  • 上面的代码中加了Thread.sleep(),如果不加的话,可能会出现大小仍然小于10000的情况;
  • 原因分析:for循环执行很快,add执行也很快,但是等for循环结束之后,还有一些线程没有跑完,这时候延迟显示结果就是在等待所有线程结束;
  • 不同的是,不加synchronized之前减少的原因是多个线程争抢同一块资源+线程没跑完之前就显示list大小,而加上synchronized之后的原因就只有线程没跑完之前就显示list大小这一点了;
  • 所以如果sleep时间过短,理论上显示的数值也有可能小于10000。
  • 这两种结果的区别:
    • 多个线程争抢同一块资源+线程没跑完之前就显示list大小
      • 最终结果,list的大小也不一定会到达10000,因为存在多抢一的操作;
    • 线程没跑完之前就显示list大小
      • 最终结果,虽然不加sleep也会显示不到10000,但是最终list的大小实际上是达到10000了的,多sleep一会儿必定能显示list大小为10000。

1.7.3 JUC安全集合

代码

  • java.util.concurrent包,是一个安全的包,相当于加上了synchronized
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
import java.util.concurrent.CopyOnWriteArrayList;

//测试JUC安全类型的集合
public class TestJUC
{
public static void main(String[] args)
{
CopyOnWriteArrayList<String> copyOnWriteArrayList=new CopyOnWriteArrayList();
for (int i = 0; i < 10000; i++)
{
new Thread(()->{
copyOnWriteArrayList.add(Thread.currentThread().getName());
}).start();
}

System.out.println("list的大小是:"+copyOnWriteArrayList.size());

try
{
Thread.sleep(300);
} catch (InterruptedException e)
{
e.printStackTrace();
}

System.out.println("list的大小是:"+copyOnWriteArrayList.size());
}
}

结果

具体原因上面分析过了

1.7.4 死锁

  • 多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或以上的线程都在等待对方释放资源而停止执行的情况,某一个同步块同时拥有两个或以上个对象的锁时,就可能会发生死锁的问题。

代码

  • 双层嵌套锁
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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
//死锁:多个线程抱着对方需要的资源形成僵持的状态
public class DeadLock
{
public static void main(String[] args)
{
Kibo24 k1=new Kibo24(0,"二十四");
Kibo24 k2=new Kibo24(1,"王富贵");

new Thread(k1).start();
new Thread(k2).start();
}

}

class Jiaran{}

class Nanami{}

class Kibo24 implements Runnable
{
//需要的资源只有一份,用static声明
static Jiaran dIane=new Jiaran();
static Nanami nanami=new Nanami();

int choice;
String name;

//构造函数
public Kibo24(int choice, String name)
{
this.choice = choice;
this.name = name;
}

@Override
public void run()
{
//抢人!
rob();
}

public void rob()
{
//相当于互相持有对方的锁
if(choice==0)
{
synchronized (dIane)
{
//获得嘉然的锁
System.out.println(this.name+"要把嘉然抢走了!");

try
{
//等待一秒钟
Thread.sleep(1000);

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

System.out.println(this.name+"还想要抢走七海!");

synchronized (nanami)
{//获得七海的锁
System.out.println(this.name+"要把七海抢走了!");
}
}
}
else
{
synchronized (nanami)
{
//获得七海的锁
System.out.println(this.name+"要把七海抢走了!");

try
{
//等待一秒钟
Thread.sleep(1000);

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

System.out.println(this.name+"还想要抢走嘉然!");

synchronized (dIane)
{//获得嘉然的锁
System.out.println(this.name+"要把嘉然抢走了!");
}
}
}
}
}

结果

  • 双方都想争夺对方手里的资源,然后就会卡住不动
1.7.4.1 总结
  • 产生死锁的必要条件:
    • 互斥:一个资源每次只能被一个进程使用;
    • 请求与保持条件:一个进程因请求资源而阻塞时,对已经获得的资源保持不放;
    • 不剥夺条件:进程已经获得的资源,在未使用完之前,不能强行剥夺;
    • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源的关系。

只要避免一个或多个条件就可以避免死锁的发生

1.7.5 Lock锁

  • 从JDK5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步,同步锁使用Lock对象充当;
  • Lock接口是控制多个线程对资源共享进行访问的工具,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象;
  • ReentrantLock(可重入锁)类实现了Lock,他拥有和synchrd相同的并发性和内存语义,在实现线程安全的控制中,比较常用的私ReentrantLock,可以显式加锁,释放锁。

代码

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
import java.util.concurrent.locks.ReentrantLock;

//测试Lock锁
public class TestLock
{
public static void main(String[] args)
{
//典中典之多个线程操作一个资源
TestLocker t1=new TestLocker();
new Thread(t1,"嘉然").start();
new Thread(t1,"七海").start();
new Thread(t1,"二十四").start();
}
}

class TestLocker implements Runnable
{
private int count=0;

//定义可重入锁Lock
private ReentrantLock lock=new ReentrantLock();

@Override
public void run()
{
while(true)
{
try
{
Thread.sleep(300);
} catch (InterruptedException e)
{
e.printStackTrace();
}

try
{
//加锁
lock.lock();//建议使用try-finally
if(count<=10)
{

System.out.println(Thread.currentThread().getName()+"抢到了第"+count+++"号票");
}
else
{
break;
}
}
finally
{
//解锁
lock.unlock();
}

}
}
}
  • 注意sleep的位置,如果在lock()内,就会只连续地执行一个或几个线程;如果在lock()外,多个线程才会基本全部执行;
  • 原因分析:这是一个先等再锁还是先锁再等的问题。
    • 先等再锁:在线程上锁之前的等待过程中,任何一个线程率先执行都是有可能的
    • 先锁再等:由于调度很迅速,于是第一个启动的线程马上被上锁,在其解锁后依旧马上被上锁,使其他线程阻塞,其他线程很难执行(并不是不可能执行)
  • 综上所述,建议先等再锁

结果

  • 先等再锁
  • 先锁再等
1.7.5.1 总结
  • Lock是显式锁,手动开启和关闭,synchronized是隐式锁,执行出了作用域自动释放;
  • Lock只有代码块锁,synchrond有代码块锁和方法锁;
  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好,并且具有良好的扩展性(有更多的子类);
  • 优先使用顺序:
    • Lock > 同步代码块(已经进入了方法体,分配了相应资源) > 同步资源(在方法体之外)

1.8 线程协作

  • 这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件。
    • 对于生产者,没有生产产品之前,要通知消费者等待;在生产了产品之后,又需要马上通知消费者;
    • 对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费;
    • 在生产消费者问题中,仅有synchronized是不够的
      • synchronized可阻止并发更新用一个资源共享,实现了同步;
      • synchronized不能用来实现不同线程之间的消息传递(通信)。

1.8.1 线程通信

  • Java提供了几个方法解决线程之间的通信问题:

    方法名 作用
    wait() 表示线程一直等待,直到其他线程通知,与sleep不同,会释放锁
    wait(long time) 指定等待的时间
    notify() 唤醒一个处于等待状态的线程
    notifyAll() 唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先调度

均是Object类的方法,只能在同步方法或者同步代码块中使用,否则会抛出异常IIIegalMonitorStateException

1.8.1.1 管程法

并发协作模型“生产者/消费者”—>管程法

  • 生产者:负责产生数据的模块(可能是方法,对象,线程,进程)
  • 消费者:负责处理数据的模块(可能是方法,对象,线程,进程)
  • 缓冲区:消费者不能直接使用生产者数据,生产者将数据放入缓冲区,消费者从缓冲区拿出数据
  • notify和notifyAll
    • 使用notify()时,ThreadScheduler将从等待该监视器上的线程的池中选择一个随机线程,这取决于线程调度器,但是会保证只有一个线程会被通知:(随机性)
    • notifyAll方法将唤醒所有线程

代码

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136

//测试生产者消费者模型:利用缓冲区解决:管程法
public class TestPC
{
public static void main(String[] args)
{
Container container=new Container();
Productor productor=new Productor(container);
Consumer consumer=new Consumer(container);

//开启线程
new Thread(productor).start();
new Thread(consumer).start();

}

}

class Productor implements Runnable
{
private Container container;

public Productor(Container container)
{
this.container=container;
}

@Override
public void run()
{
for (int i = 1; i <= 100; i++)
{
container.push(new Product(i));
}

}
}


class Consumer implements Runnable
{
private Container container;

public Consumer(Container container)
{
this.container=container;
}

@Override
public void run()
{
for (int i = 1; i <= 100; i++)
{
container.pop(new Product(i));
}
}
}


class Product
{
private int id;
public Product(int id)
{
this.id=id;
}

public int getId()
{
return this.id;
}
}


class Container
{
Product[] products=new Product[10];
private int count=0;

//需要生产者放入产品
public synchronized void push(Product product)
{
//判断容器是否已满
//如果满了
while(count==products.length)
{
//进入睡眠状态,等待唤醒
//生产者等待消费者拿走产品
try
{//等待的同时解锁,这时之前被阻塞的消费者pop方法会拿到锁
this.wait();

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

//如果没有满,生产者放入产品
//并通知消费者来拿走产品
products[count++]=product;
System.out.println("生产者生产了第"+product.getId()+"个产品");

//生产之后唤醒消费者的线程,但是只是唤醒,还是被阻塞,因为没有拿到锁
this.notifyAll();

}

//消费者拿走产品
public synchronized Product pop(Product product)
{
Product finalProduct=null;
while(count==0)
{
//如果没有产品了,就等待生产者放入
//进入睡眠状态,等待唤醒
try
{//等待的同时解锁,此时之前被阻塞的生产者push方法拿到锁
this.wait();
} catch (InterruptedException e)
{
e.printStackTrace();
}
}

//如果有,就拿走产品
finalProduct=products[--count];
System.out.println("消费了第"+product.getId()+"个产品");

//拿走之后唤醒生产者的线程,但是只是唤醒,还是被阻塞,因为没有拿到锁
this.notifyAll();


return finalProduct;
}
}

注意使用while循环判断条件,使用if可能会出现假唤醒问题

结果

1.8.1.2 信号灯法
  • 标志位flag

代码

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
//测试生产者消费者问题:信号灯法:标志位
public class TestPCTwo
{
public static void main(String[] args)
{
PlayStation playStation=new PlayStation("Apex");
Player player=new Player(playStation);
Fixer fixer=new Fixer(playStation);

new Thread(player).start();
new Thread(fixer).start();

}
}


//生产者-->七海
class Player implements Runnable
{
private PlayStation playStatio;
//构造函数
public Player(PlayStation playStatio)
{
this.playStatio = playStatio;
}

@Override
public void run()
{
for (int i = 1; i <= 20; i++)
{
this.playStatio.Play();
}
}
}

//消费者-->修理工
class Fixer implements Runnable
{
private PlayStation playStatio;
//构造函数
public Fixer(PlayStation playStatio)
{
this.playStatio = playStatio;
}

@Override
public void run()
{
for (int i = 1; i <= 20; i++)
{
this.playStatio.fix();
}
}
}

class PlayStation
{
private String game;
//游戏
//玩家游戏,修理工等待
//当为真时玩家游戏,为假时修理工修理
private boolean flag=true;

public PlayStation(String game)
{
this.game = game;
}

public synchronized void Play()
{
//如果flag是false,说明修理工正在修理bug,要等待
while (flag == false)
{
try
{
this.wait();
} catch (InterruptedException e)
{
e.printStackTrace();
}

}

//七海正在玩游戏
System.out.println("七海正在玩"+this.game+",并出了bug");
this.flag=!this.flag;
//唤醒修理工
this.notifyAll();
}

//修理
//修理工修理游戏bug,玩家等待
public synchronized void fix()
{
//如果flag为true,说明七海还没玩游戏,要等待
while(flag==true)
{
try
{//睡眠状态,但是释放锁
this.wait();
} catch (InterruptedException e)
{
e.printStackTrace();
}
}

//修理bug完成
System.out.println("修理工修理了"+"的bug");
this.flag=!this.flag;
//唤醒七海继续玩游戏
this.notifyAll();
}
}
  • 如果两个for循环的次数不一样的话会卡住,因为该代码中两个线程交替执行,执行过程中会进入等待状态,等待另一个线程的唤醒,如果循环次数不一样的话,说明其中一个线程会先结束,此时另一个进入等待状态的线程无法再被唤醒,就会导致进程被卡住

结果

1.8.2 线程池

  • 问题:经常创建和销毁,使用量特别大的资源,比如并发情况下的线程,对性能影响很大;

  • 思路:提前创造好多个线程,放入线程池中,使用时直接获取,使用完放回线程池中。可以避免频繁的创建销毁,可以实现线程的重复利用;

  • 好处:

    • 提高响应速度(减少创建新线程的事件)
    • 降低资源消耗(重复利用线程池中的线程,不需要每次都创建新线程)
    • 便于线程管理:
      • corePoolSize:核心池的大小
      • maximumPoolSize:最大线程数
      • keepAliveTime:线程没有任务时最多保持多久后会终止
  • JDK5.0开始提供线程池相关API:ExecutorServiceExecutors

  • ExecutorService:线程池接口,常见子类ThreadPoolExecutor

    • void executor(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable;
    • Futuresubmit(Callable task):执行任务,有返回值,一般用来执行Callable;
    • void shutdown():关闭连接池
  • Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池

代码

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
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

//测试线程池
public class TestPool
{
public static void main(String[] args)
{
//1.创建服务,创建线程池
ExecutorService service= Executors.newFixedThreadPool(10);

service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());

//2.关闭连接
service.shutdown();
}

}

class MyThread implements Runnable
{
@Override
public void run()
{
System.out.println(Thread.currentThread().getName());
}
}
  • 进入线程池的线程名称:pool-编号-thread-编号

结果