Java多线程实战学习笔记


多线程学习

【狂神说Java】多线程详解_哔哩哔哩_bilibili

这个只是简单的学了一下怎么用,几个小demo,没有深入的讲。

1. 线程简介

线程,进程,多线程

  • 并发:同时发生,在一个时间段内执行,不一定是同一时间点
  • 并行:同时执行,在一个时间点上有多个线程执行

关于run()和start()

  • 一个进程可以有多个线程

Process 和 Thread

三个概念,程序、进程、线程:

  • 程序:静态代码
  • 进程:程序的一次执行过程
  • 线程:一个进程可以包含多个线程,线程是CPU调度和执行的单位

很多多线程是模拟出来的,真正的多线程是指有多个CPU。模拟的线程只是一个CPU切换的很快产生了多线程的错觉。其实就是并发和并行的区别。

一些要点

  • main() 函数就是主线程;
  • 默认的话有主线程、GC线程等;
  • 线程因为调度,会代来开销
  • 资源抢夺问题,需要并发控制

2. 线程实现(重点)

2.1 三种实现方式

  • 继承Thread类
  • 实现Runnable接口
  • 实现Callable接口

三种实现方式

2.1.1继承Thread类

Demo
  1. 自定义线程类,继承Thread
  2. 重写 run() 方法
  3. 调用 start() 方法启动线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class TestThread1 extends Thread {
// 重写run方法
@Override
public void run() {
for (int i = 0; i < 2000; i++) {
System.out.println("run方法线程"+i);
}
}

public static void main(String[] args) {
// start启动,run不是多线程
TestThread1 testThread1 = new TestThread1();
testThread1.start();
// main线程
for (int i = 0; i < 2000; i++) {
System.out.println("MMMMMMMMMMMMMM"+i);
}
}
}

PS: 线程开启不一定立即执行,由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
public class TestThread2 extends Thread {
private String url;
private String name;

public TestThread2(String url, String name) {
this.name = name;
this.url = url;
}

public static void main(String[] args) {
TestThread2 testThread1 = new TestThread2("https://commons.apache.org/proper/commons-io/images/commons-logo.png", "1.png");
TestThread2 testThread2 = new TestThread2("https://commons.apache.org/proper/commons-io/images/io-logo-white.png", "2.png");
TestThread2 testThread3 = new TestThread2("http://www.apache.org/events/current-event-125x125.png", "3.png");

testThread1.start();
testThread2.start();
testThread3.start();
}

@Override
public void run() {
// run方法负责调用下载方法
WebDownload webDownload = new WebDownload();
webDownload.downloader(url, name);
System.out.println("下载了文件,名为" + name);
}

class WebDownload {
public void downloader(String url, String name) {
String path = "F:/AWork/Project/2021/JavaDataStructure/src/com/songx64/baselearn/threadlearn/kuangThread/";
try {
FileUtils.copyURLToFile(new URL(url), new File(path+name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO异常,下载出错");
}
}
}
}

需要Apache的一个包 commons-ioCommons IO – Commons IO Overview (apache.org),用于文件下载

下载后Add to Library。

2.2 Runnable实现线程

  1. 创建类实现 Runnable 接口
  2. 创建 类的对象
  3. 将对象传入 Thread() 构造函数中
  4. 调用 Thread 实例的 start()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

public class TestThread3 implements Runnable {
public static void main(String[] args) {
// 下面这两行,需要将Runnable对象传入Thread类
TestThread3 testThread3 = new TestThread3();
new Thread(testThread3).start();

for (int i = 0; i < 2000; i++) {
System.out.println("主线程" + i);
}
}

@Override
public void run() {
for (int i = 0; i < 2000; i++) {
System.out.println("RRRRRRRRRRRRRRRRRRRR" + i);
}
}
}

Thread 与 Runnable 对比

  • 继承 Thread 类:
    • 启动线程:子类对象.start()
    • 不推荐使用:避免单继承局限性
  • 实现 Runnable 接口:
    • 启动线程:new Thread(对象) + thread.start()
    • 推荐使用:没有单继承局限性,方便同一个对象被多个线程使用

Runnable可以被重复使用

Thread:基于继承;Runnable:基于组合;创建Thread比一个Runnable成本要昂贵一点。/

实例:初识线程并发问题

卖票问题

  • Thread.currentThread.getName() :得到当前正在执行的线程方法
  • Thread.sleep(200) :此线程暂停200ms
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 TestThread4 implements Runnable {
private int ticketNums = 10;

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

new Thread(t, "小明").start();
new Thread(t, "Bob").start();
new Thread(t, "牛牛").start();

}

@Override
public void run() {
while (true) {
if (ticketNums <= 0) {
break;
}

// 存在并发问题,票会超卖
System.out.println(Thread.currentThread().getName() + "拿到了" + ticketNums-- + "张票");

// 加锁的话可以解决并发问题
/*
synchronized (this){
System.out.println(Thread.currentThread().getName()+"拿到了"+ticketNums--+"张票");
}*/

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

这里还没讲怎么解决,看之后的。

实例:龟兔赛跑

龟兔赛跑问题

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
public class Race implements Runnable{

private String winner;

@Override
public void run() {
for (int i = 0; i < 100; i++) {
// 兔子每隔10步,睡个觉
if(Thread.currentThread().getName().equals("兔子") && i%10==0 ){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 跑步
System.out.println(Thread.currentThread().getName()+"跑了-->"+i+"步");
if (gameOver(i)){
System.out.println("Winner is " + winner);
break;
}
}

}

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

new Thread(race,"兔子").start();
new Thread(race,"乌龟").start();

}

/**
* 判断,如果有人先跑到100步,那么游戏结束
* **/
public boolean gameOver(int steps){
if (winner == null){
if(steps >= 99){
winner = Thread.currentThread().getName();
return true;
}
return false;
}
return true;
}
}

2.3 Callable接口实现

Callable其实相当于一个增强的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
40
41
42
43
44
45
46
47
48
49
50
51
52
public class TestCallable implements Callable<Boolean> {
private String url;
private String name;

public TestCallable(String url, String name) {
this.name = name;
this.url = url;
}

public static void main(String[] args) throws ExecutionException, InterruptedException {
TestCallable tc1 = new TestCallable("https://commons.apache.org/proper/commons-io/images/commons-logo.png", "1.png");
TestCallable tc2 = new TestCallable("https://commons.apache.org/proper/commons-io/images/io-logo-white.png", "2.png");
TestCallable tc3 = new TestCallable("http://www.apache.org/events/current-event-125x125.png", "3.png");

// 主要就是这块,提交到线程池,然后使用Future获取返回结果并get
ExecutorService service = Executors.newFixedThreadPool(1);
Future<Boolean> result1 = service.submit(tc1);
Future<Boolean> result2 = service.submit(tc2);
Future<Boolean> result3 = service.submit(tc3);

System.out.println("result1 == " +result1.get());
System.out.println("result2 == " +result2.get());
System.out.println("result3 == " +result3.get());
// 最后需要关闭服务
service.shutdownNow();

}

@Override
public Boolean call() {
// run方法负责调用下载方法
WebDownload webDownload = new WebDownload();
webDownload.downloader(url, name);
System.out.println("下载了文件,名为" + name);
if(name == null){
return false;
}
return true;
}

class WebDownload {
public void downloader(String url, String name) {
String path = "F:/AWork/Project/2021/JavaDataStructure/src/com/songx64/baselearn/threadlearn/kuangThread/";
try {
FileUtils.copyURLToFile(new URL(url), new File(path+name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO异常,下载出错");
}
}
}
}

继承Callable的时候会有个泛型,指定返回值的类型。

然后实现的是 call() 方法,带有返回值。

Callable步骤

2.4 静态代理模式

多线程的 Thread 和 Runnable 就是静态代理模式。

代理模式 | 菜鸟教程 (runoob.com)

在代理模式(Proxy Pattern)中,一个类代表另一个类的功能。这种类型的设计模式属于结构型模式。

主要解决:在直接访问对象时带来的问题

何时使用:想在访问一个类时做一些控制。

如何解决:增加中间层。

关键代码:实现与被代理类组合。

这里举个例子:

image-20210623152348219

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
// 静态代理模式总结;
// 真实对象和代理对象都要实现同一个接口
// 代理对象要代理真实角色
// 好处:
// 代理对象可以做很多真实对象做不了的事情
// 真实对象专注做自已的事情

public class StaticProxy {
public static void main(String[] args) {
WeddingCompany weddingCompany = new WeddingCompany(new You());
weddingCompany.HappyMarry();


// 对比这个静态代理模式,和多线程的格式,可以看到多线程其实也是一样的
// Thread 继承了 Runnable接口
new WeddingCompany(new You()).HappyMarry();
new Thread(new TestRunnable()).start();

}
}

//结婚接口
interface Marry{
void HappyMarry();
}

//你来结婚
class You implements Marry{

@Override
public void HappyMarry() {
System.out.println("结婚,Happy");
}
}

// 婚庆公司,代理你做一些事情
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("结婚之后,收拾残局");
}
}

3. 线程状态

线程的五大状态

Java中的线程状态变化

3.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
//测试stop
// 1.建议线程正常停止--->利用次数,不建议死循环。
// 2.建议使用标志位--->设置一个标志位
// 3.不要使用stop或者destroy等过时或者JDK不建议使用的方法

public class TestStop implements Runnable {
// 1.设置标志位
private boolean flag = true;

@Override
public void run() {
int i = 0;
// 2. 根据标志位判断,是否执行
while (flag){
System.out.println("Thread is Running --" + i++);
}
}

// 3. 公共方法,修改标志位,停止线程
public void myStop(){
this.flag = false;
}

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

// 次数太少的话,子线程可能抢不到时间片
for (int i = 0; i < Integer.MAX_VALUE/2; i++) {
System.out.println("main--"+i);
if(i == Integer.MAX_VALUE/400000){
t.myStop();
System.out.println("Stop At " +i);
break;
}
}
}
}

3.2 线程休眠 sleep(int ms)

sleep()

  • sleep(1000) ,休眠1000毫秒,也就是1s
  • 会抛出 InterruptedException
  • 调用 sleep,线程进入阻塞状态;sleep 时间到达后,进入就绪状态
  • sleep 不会释放对象的锁,wait 会释放对象锁
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
public class TestSleep{
// 模拟倒计时
public void timeCount(int second) throws InterruptedException {
while (second > 0){
System.out.println(second--);
Thread.sleep(1000);
}
}

// 打印系统当前时间
public void timePrinter() throws InterruptedException {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
while (true){
System.out.println(dateFormat.format(new Date(System.currentTimeMillis())));
Thread.sleep(1000);
}
}
public static void main(String[] args) throws InterruptedException {

TestSleep testSleep = new TestSleep();
testSleep.timeCount(10);
testSleep.timePrinter();

}
}

3.3 线程礼让 yield()

  • 礼让线程,让当前正在执行的线程暂停,但不阻塞将线程
  • 从运行状态转为就绪状态
  • 让cpu重新调度,礼让不一定成功!看CPU心情
1
2
3
4
5
6
7
8
9
10
11
12
public class TestYield {
public static void main(String[] args) {
Runnable runnable = ()->{
System.out.println(Thread.currentThread().getName() + " 线程开始执行");
Thread.yield();
System.out.println(Thread.currentThread().getName() + " 线程停止");
};

new Thread(runnable,"线程A").start();
new Thread(runnable,"线程B").start();
}
}

PS:我礼让就没成功过。。电脑的原因吗

3.4 线程强制执行 join()

  • 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
27
28
29
public class TestJoin implements Runnable {

@Override
public void run() {
for (int i = 0; i < 5000; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Join线程-------------" + i);
}
}

public static void main(String[] args) throws InterruptedException {
TestJoin testJoin = new TestJoin();
Thread thread = new Thread(testJoin);
thread.start();

for (int i = 0; i < 5000; i++) {
// 当主线程 200 的时候,让thread插队
if(i == 1000){
thread.join();
}
System.out.println("Main线程 + " + i);
}

}
}

3.5 线程状态观测 getState()

JDK1.8文档:

public static enum Thread.State
extends Enum<Thread.State>
线程状态。线程可以处于以下状态之一:

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

  • RUNNABLE
    在Java虚拟机中执行的线程处于此状态。

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

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

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

  • TERMINATED
    已退出的线程处于此状态。

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

thread1.getState() :得到 thread1 的状态,上面的那几个值

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
public class TestState {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("///////");
}
});

Thread.State state = thread.getState();
System.out.println("start之前: " + state);

thread.start();
state = thread.getState();
System.out.println("start之后:" + state);

while (state != Thread.State.TERMINATED){
// 这里一定要更新
state = thread.getState();
System.out.println(state);
Thread.yield();
Thread.sleep(1000);
}

state = thread.getState();
System.out.println("线程死了!--" + state);

// 线程是一次性用品,死亡后不能再次启动
thread.start();
}
}

3.6 线程优先级 getPriority()

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

线程执不执行还是得看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
24
25
26
27
28
29
30
31
32
public class TestPriority {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() + " --> " + Thread.currentThread().getPriority() );
MyPriority myPriority = new MyPriority();

Thread t1 = new Thread(myPriority,"t1");
Thread t2 = new Thread(myPriority,"t2");
Thread t3 = new Thread(myPriority,"t3");
Thread t4 = new Thread(myPriority,"t4");
Thread t5 = new Thread(myPriority,"t5");

t1.setPriority(1);
t2.setPriority(2);
t3.setPriority(3);
t4.setPriority(4);
t5.setPriority(Thread.MAX_PRIORITY);

t4.start();
t2.start();
t1.start();
t3.start();
t5.start();
}
}

class MyPriority implements Runnable{

@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " --> " + Thread.currentThread().getPriority() );
}
}

3.7 守护线程 setDaemon(boolean)

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

thread1.setDaemon(true) :将线程 thread1 设为守护线程

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
public class TestDaemon {
public static void main(String[] args) {
Bless bless = new Bless();
NormalT normalT = new NormalT();

Thread thread = new Thread(bless);
// 默认false表示为正常线程,true表示设为守护线程
thread.setDaemon(true);

Thread thread1 = new Thread(normalT);
thread1.start();
thread.start();

// 可以看到,主线程和Normal线程结束之后,程序结束。守护线程死循环也终止了
}

}

class Bless implements Runnable{

@Override
public void run() {
while (true){
System.out.println("God Bless You");
}
}
}

class NormalT implements Runnable{

@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("此线程存活");
}
System.out.println("此线程死亡");
}
}

4. 线程同步(重点)

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

处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。这时候我们就需要线程同步。

线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用。

  • 队列 + 锁,解决线程同步的安全性。

synchronized,排他锁独占资源。

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

死锁

产生死锁的四个必要条件:
1.互斥条件:一个资源每次只能被一个进程使用。
2.请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。3.不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
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
public class DeadLock {
public static void main(String[] args) {
Integer i1 = 10;
Integer i2 = 20;

Thread thread = new Thread(new MyThread(i1, i2, 0), "线程1");
Thread thread2 = new Thread(new MyThread(i1, i2, 1), "线程2");

thread.start();
thread2.start();
}
}

class MyThread implements Runnable {

private Integer i1;
private Integer i2;
private Integer choice;

public MyThread(Integer i1, Integer i2, Integer choice) {
this.i1 = i1;
this.i2 = i2;
this.choice = choice;
}

@Override
public void run() {
// 用一个choice,分别先拿不同资源
if (choice == 0) {
// 先拿1,再拿2
synchronized (i1) {
System.out.println(Thread.currentThread().getName() + "得到了i1: " + i1);
// 将两个这个拿到外层,可以解决死锁
synchronized (i2) {
System.out.println(Thread.currentThread().getName() + "得到了i2: " + i2);
}
}
} else {
// 先拿2,再拿1
synchronized (i2) {
System.out.println(Thread.currentThread().getName() + "得到了i2: " + i2);
synchronized (i1) {
System.out.println(Thread.currentThread().getName() + "得到了i1: " + i1);
}
}
}

}
}

将最后一段的代码改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override
public void run() {
// 用一个choice,分别先拿不同资源
if (choice == 0) {
// 先拿1,再拿2
synchronized (i1) {
System.out.println(Thread.currentThread().getName() + "得到了i1: " + i1);
}
synchronized (i2) {
System.out.println(Thread.currentThread().getName() + "得到了i2: " + i2);
}
} else {
// 先拿2,再拿1
synchronized (i2) {
System.out.println(Thread.currentThread().getName() + "得到了i2: " + i2);
}
synchronized (i1) {
System.out.println(Thread.currentThread().getName() + "得到了i1: " + i1);
}
}

}

这样就能避免死锁了。为啥呢?我自己理解的,是锁升级到重量级锁了,阻塞了其中的一个线程。

Lock锁

JUC,就是 import java.util.concurrent

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 TestLock {
public static void main(String[] args) {
TestLock2 testLock2 = new TestLock2();
new Thread(testLock2,"小明").start();
new Thread(testLock2,"小二").start();
new Thread(testLock2,"小王").start();
}
}

class TestLock2 implements Runnable{
private final ReentrantLock reentrantLock = new ReentrantLock();

int tirckNums = 10;
@Override
public void run() {
while (true){
// 加锁
reentrantLock.lock();
try {
if (tirckNums > 0){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-->"+tirckNums--);
}else {
break;
}
}finally {
// 解锁
reentrantLock.unlock();
}
}
}
}

手动锁,自己调用 lockunlock ,锁代码块。

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

5. 线程通信问题

5.1 生产者消费者问题

可以观看:一篇文章,让你彻底弄懂生产者–消费者问题 - 简书 (jianshu.com) ,使用了3种不同的方法。

生产者消费者问题

问题分析

Java方法

  1. 并发协作模型“生产者Ⅰ消费者模式”—>管程法

    • 生产者:负责生产数据的模块(可能是方法﹐对象﹐线程﹐进程);
    • 消费者:负责处理数据的模块(可能是方法﹐对象﹐线程,进程);
    • 缓冲区∶消费者不能直接使用生产者的数据﹐他们之间有个“缓冲区

    生产者将生产好的数据放入缓冲区 ,消费者从缓冲区拿出数据

  2. 并发协作模型“生产者/消费者模式”—>信号灯法

    就是一个标志位

5.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
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 TestPC {
public static void main(String[] args) {
SynContainer synContainer = new SynContainer();

Producer producer = new Producer(synContainer);
Consumer consumer =new Consumer(synContainer);

producer.start();
consumer.start();
}
}

/**
* 生产者
**/
class Producer extends Thread {

SynContainer synContainer;

public Producer(SynContainer synContainer) {
this.synContainer = synContainer;
}

@Override
public void run() {
// 生产
for (int i = 0; i < 100; i++) {
synContainer.push(new Production(i));
System.out.println("生产者生产了--> "+i);
}
}
}

/**
* 消费者
**/
class Consumer extends Thread {
SynContainer synContainer;

public Consumer(SynContainer synContainer) {
this.synContainer = synContainer;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
int id = synContainer.pop().id;
System.out.println("消费者消费了--> " + id);
}

}
}

/**
* 产品
**/
class Production {
// 产品编号
int id;

public Production(int id) {
this.id = id;
}
}

/**
* 缓冲区
**/
class SynContainer {
// 缓冲区容器,总容量
Production[] productions = new Production[10];
// 当前存在的容量
int count = 0;

//生产者放入产品
public synchronized void push(Production production) {
// 缓冲区已满
if (count == productions.length) {
// 缓冲区已满,生产者停止生产;
// 通知消费者,进行消费
try{
this.wait();
}catch (Exception e){
e.printStackTrace();
}
}
// 放入
productions[count] = production;
count++;
// 生产出来了,通知消费者,可以立即消费
this.notifyAll();
}

// 消费者取走产品
public synchronized Production pop() {
if (count == 0){
// 通知生产者生产
try{
this.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}

// 取出
count--;
Production production = productions[count];

// 取出之后,缓冲区有空格了
this.notifyAll();

return production;

}
}

Java中的管程 - 被罚站的树 - 博客园 (cnblogs.com)

Java 采用的是管程技术,synchronized 关键字及 wait()、notify()、notifyAll() 这三个方法都是管程的组成部分。而管程和信号量是等价的,所谓等价指的是用管程能够实现信号量,也能用信号量实现管程。但是管程利用OOP的封装特性解决了信号量在工程实践上的复杂性问题,因此java采用管理机制。

所谓管程,指的是管理共享变量以及对其操作过程,让它们支持并发访问。翻译为 Java 领域的语言,就是管理类的成员变量和成员方法,让这个类是线程安全的。

有一点需要再次提醒,对于 MESA 管程来说,有一个编程范式,就是==需要在一个 while 循环里面调用 wait()==。这个是 MESA 管程特有的

5.3 信号灯法

JAVA并发框架之Semaphore实现生产者与消费者模型 - 陈峰 - 博客园 (cnblogs.com)

锁和 信号量(Semaphore) 是实现多线程同步的两种常用的手段。

信号量需要初始化一个许可值,许可值可以大于0,也可以小于0,也可以等于0.

  • 如果大于0,表示,还有许可证可以发放,线程不会被阻塞;
  • 如果小于或者等于0,表示,没有许可证可以发放了,线程被阻塞住了。

它有两个常用的操作:

  • acquire() 申请许可证,如果有,就可以获得,如果没有就等待了。相当于减法。
  • release() 归还许可证,保证循环使用。相当于加法。

看一个例子,就会明白了,还是实现上次的那个生产者和消费者的例子。

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
package com.songx64.baselearn.threadlearn.kuangThread.gaoji;

import java.util.LinkedList;
import java.util.concurrent.Semaphore;

/**
* Created on 2021/6/26,上午 11:03
* 信号量机制 Semaphore
*
* @author SongX64
*/
public class TestPC2 {

public static void main(String[] args) {
SynContainer2 synContainer2 = new SynContainer2();
Thread p1 = new Producer2(synContainer2);
Thread c1 = new Consumer2(synContainer2);
Thread c2 = new Consumer2(synContainer2);

p1.start();
c1.start();
c2.start();
}
}


/**
* 产品
**/
class Production2 {
public int id;

public Production2(int id) {
this.id = id;
}
}

/**
* 生产者
**/
class Producer2 extends Thread {
SynContainer2 synContainer2;

public Producer2(SynContainer2 synContainer2) {
this.synContainer2 = synContainer2;
}

@Override
public void run() {
for (int i = 0; i < 20; i++) {
synContainer2.push(new Production2(i));
System.out.println("生产了-->" + i);
}
}
}

class Consumer2 extends Thread {
SynContainer2 synContainer2;

public Consumer2(SynContainer2 synContainer2) {
this.synContainer2 = synContainer2;
}

@Override
public void run() {
for (int i = 0; i < 20; i++) {
int id = synContainer2.pop().id;
System.out.println(Thread.currentThread().getName()+"消费了<--" + id);
}
}
}

/**
* 缓冲区
**/
class SynContainer2 {
// 容器
LinkedList<Production2> production2s = new LinkedList<>();
// 互斥信号量,保证安全性
Semaphore mutex = new Semaphore(1);
// 为满信号量,初始是最大容量
Semaphore isFull = new Semaphore(10);
// 为空信号量
Semaphore isEmpty = new Semaphore(0);


// 生产者生产,为满阻塞
public void push(Production2 production2) {
try {
//大于0,就放行
//acquire,就是减操作,如果 <=0,就阻塞
//release,就是加操作,如果 >0,就不会被阻塞
isFull.acquire();
// 添加操作是互斥的,因为占用了缓冲区
mutex.acquire();
int i = isFull.availablePermits();
production2s.add(production2);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放互斥锁
mutex.release();
// 生产完成后,让空的增加,可以进行消费
isEmpty.release();
}
}

// 消费者消费,为空阻塞
public Production2 pop() {
Production2 temp = null;
// 为空减少一个信号量,如果是空的0再减就阻塞了;

try {
isEmpty.acquire();
mutex.acquire();
temp = production2s.removeLast();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
mutex.release();
isFull.release();
}
return temp;
}
}

写的有点乱。。。但是知道Semaphore的用法就行了

6.高级主题

6.1线程池

  • 背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
  • 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
  • 好处:
    • 提高响应速度(减少了创建新线程的时间)
    • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
    • 便于线程管理(….)
      • corePoolSize:核心池的大小
      • maximumPoolSize:最大线程数
      • keepAliveTime:线程没有任务时最多保持多长时间后会终止

Java线程池类

简单使用:

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
public class TestPool {
public static void main(String[] args) {

// 1.1 手动创建线程池(建议),指定参数
// 参数:核心线程数,最大线程数,核心线程外的工作线程存活时间,时间单位,阻塞队列
ThreadPoolExecutor executorService = new ThreadPoolExecutor(
10,
20,
100,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>());

// 1.2 自动创建线程池(不建议),10个核心线程/最大线程
//ExecutorService executorService = Executors.newFixedThreadPool(10);

executorService.execute(new MyThread());
executorService.execute(new MyThread());
executorService.execute(new MyThread());
executorService.execute(new MyThread());
}
}
class MyThread implements Runnable{

@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}

文章作者: SongX64
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 SongX64 !
  目录