day10【线程安全、volatile关键字、原子性、并发包、死锁、线程池】

day10【线程安全、volatile关键字、原子性、并发包、死锁、线程池】

今日内容

  • 线程安全
    • 同步锁\Lock锁—–掌握
    • 可见性问题
    • 有序性问题
    • 原子性问题
  • volatile关键字—–掌握
  • 原子类——掌握
  • 并发包
  • 线程池—–掌握
  • 死锁

教学目标

  • 能够解释安全问题的出现的原因

  • 能够使用同步代码块解决线程安全问题

  • 能够使用同步方法解决线程安全问题

  • 能够说出volatile关键字的作用

  • 能够说明volatile关键字和synchronized关键字的区别

  • 能够理解原子类的工作机制

  • 能够掌握原子类AtomicInteger的使用

  • 能够描述ConcurrentHashMap类的作用

  • 能够描述CountDownLatch类的作用

  • 能够描述CyclicBarrier类的作用

  • 能够表述Semaphore类的作用

  • 能够描述Exchanger类的作用

  • 能够描述Java中线程池运行原理

第一章 线程安全

知识点–1.1 线程安全问题

目标

  • 能够解释安全问题的出现的原因

路径

  • 问题演示
  • 问题原因分析

讲解

  • 我们通过一个案例,演示线程的安全问题:

    电影院要卖票,我们模拟电影院的卖票过程。假设要播放的电影是 “葫芦娃大战奥特曼”,本次电影的座位共100个(本场电影只能卖100张票)。

    售票窗口: 使用线程来模拟

    共同卖100张票

    窗口卖票的任务是一样的(线程的任务代码是一样的)

我们来模拟电影院的售票窗口,实现多个窗口同时卖 “葫芦娃大战奥特曼”这场电影票(多个窗口一起卖这100张票)需要窗口,采用线程对象来模拟;需要票,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
package com.itheima.demo1_线程安全问题;

/**
* @Author:pengzhilin
* @Date: 2020/9/17 8:47
*/
public class MyRunnable implements Runnable {
// 共享变量
int tickets = 100;

@Override
public void run() {
// 线程的任务代码---卖票
while (true) {
if (tickets < 1) {
break;
}
// 暂停100ms模拟收钱的操作
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":正在出售第" + tickets + "张票");
tickets--;
}
}
}

测试类:

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
package com.itheima.demo1_线程安全问题;

/**
* @Author:pengzhilin
* @Date: 2020/9/17 8:46
*/
public class Test {
public static void main(String[] args) {
/*
需求: 模拟电影院4个窗口卖100张电影票
分析:
售票窗口: 使用线程来模拟
4个窗口共同卖100张票
4个窗口卖票的任务是一样的(线程的任务代码是一样的)
问题:
1.重复票
2.遗漏票
3.负数票(最多到-2)
*/
// 创建任务对象
MyRunnable mr = new MyRunnable();

// 创建4个窗口---创建4条线程
Thread t1 = new Thread(mr, "窗口1");
Thread t2 = new Thread(mr, "窗口2");
Thread t3 = new Thread(mr, "窗口3");
Thread t4 = new Thread(mr, "窗口4");

// 启动线程,执行任务
t1.start();
t2.start();
t3.start();
t4.start();
}
}

程序执行后,结果会出现的问题

1588211860221

发现程序出现了两个问题:

  1. 相同的票数,比如100这张票被卖了四回。
  2. 不存在的票,比如0票与-1票,-2票,是不存在的。
  3. 遗漏票,例如:99,98,97没有出现

这种问题,几个窗口(线程)票数不同步了,这种问题称为线程不安全。

卖票案例问题分析:

1588211760503

小结

略

知识点-1.2 synchronized

目标

  • synchronized关键字概述

路径

  • synchronized关键字概述

讲解

  • synchronized关键字:表示“同步”的。它可以对“多行代码”进行“同步”——将多行代码当成是一个完整的整体,一个线程如果进入到这个代码块中,会全部执行完毕,执行结束后,其它线程才会执行。这样可以保证这多行的代码作为完整的整体,被一个线程完整的执行完毕。

  • synchronized被称为“重量级的锁”方式,也是“悲观锁”——效率比较低。

  • synchronized有几种使用方式:
    a).同步代码块【常用】

    b).同步方法【常用】

当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。

要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了同步机制(synchronized)来解决。

根据案例简述:

1
窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作结束,窗口1和窗口2和窗口3才有机会进入代码去执行。也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。

小结

略

知识点–1.3 同步代码块

目标

  • 掌握同步代码块的使用

路径

  • 同步代码块的介绍
  • 同步代码块的使用

讲解

  • 同步代码块:synchronized关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。

格式:

1
2
3
synchronized(同步锁){
需要同步操作的代码
}

同步锁:

对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.

  1. 锁对象 可以是任意类型。
  2. 多个线程对象 要使用同一把锁。

注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。

使用同步代码块解决代码:

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 com.itheima.demo2_同步代码块;

/**
* @Author:pengzhilin
* @Date: 2020/9/17 8:47
*/
public class MyRunnable implements Runnable {
// 共享变量
int tickets = 100;

@Override
public void run() {
// 线程的任务代码---卖票
while (true) {
// 加锁
synchronized (this) {
if (tickets < 1) {
break;
}
// 暂停100ms模拟收钱的操作
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +
":正在出售第" + tickets + "张票");

tickets--;
}
// 释放锁
}
}
}

package com.itheima.demo2_同步代码块;

/**
* @Author:pengzhilin
* @Date: 2020/9/17 8:46
*/
public class Test {
public static void main(String[] args) {
/*
同步代码块:
概述:synchronized关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问
格式:
synchronized(锁对象){

}
锁对象:
1.语法上,锁对象可以是任意类的对象
2.多条线程想要实现同步,必须锁对象一致
*/
// 创建任务对象
MyRunnable mr = new MyRunnable();

// 创建4个窗口---创建4条线程
Thread t1 = new Thread(mr, "窗口1");
Thread t2 = new Thread(mr, "窗口2");
Thread t3 = new Thread(mr, "窗口3");
Thread t4 = new Thread(mr, "窗口4");

// 启动线程,执行任务
t1.start();
t2.start();
t3.start();
t4.start();
}
}

当使用了同步代码块后,上述的线程的安全问题,解决了。

小结

1
2
3
4
5
6
7
格式:
synchronized(锁对象){

}
锁对象:
1.语法上,锁对象可以是任意类的对象
2.多条线程想要实现同步,必须锁对象一致

知识点–1.4 同步方法

目标

  • 掌握同步方法的使用

路径

  • 同步方法的介绍
  • 同步方法的使用

讲解

  • 同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。

格式:

1
2
3
public synchronized void method(){
可能会产生线程安全问题的代码
}

同步锁是谁?

​ 对于非static方法,同步锁就是this。

​ 对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。

使用同步方法代码如下:

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
package com.itheima.demo3_同步方法;

/**
* @Author:pengzhilin
* @Date: 2020/9/17 8:47
*/
public class MyRunnable implements Runnable {
// 共享变量
int tickets = 100;

@Override
public void run() {
// 线程的任务代码---卖票
while (true) {
if (sellTickets()) break;
}
}

private synchronized boolean sellTickets() {
if (tickets < 1) {
return true;
}
// 暂停100ms模拟收钱的操作
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +
":正在出售第" + tickets + "张票");

tickets--;
return false;
}
}

package com.itheima.demo3_同步方法;

/**
* @Author:pengzhilin
* @Date: 2020/9/17 8:46
*/
public class Test {
public static void main(String[] args) {
/*
同步方法:
概述:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
格式:
修饰符 synchronized 返回值类型 方法名(形参列表){

}
锁对象:
非静态同步方法: 锁对象是this
静态同步方法: 锁对象是该方法所在类的字节码对象(类名.class)
*/
// 创建任务对象
MyRunnable mr = new MyRunnable();

// 创建4个窗口---创建4条线程
Thread t1 = new Thread(mr, "窗口1");
Thread t2 = new Thread(mr, "窗口2");
Thread t3 = new Thread(mr, "窗口3");
Thread t4 = new Thread(mr, "窗口4");

// 启动线程,执行任务
t1.start();
t2.start();
t3.start();
t4.start();
}
}

小结

1
2
3
4
5
6
7
 格式:
修饰符 synchronized 返回值类型 方法名(形参列表){

}
锁对象:
非静态同步方法: 锁对象是this
静态同步方法: 锁对象是该方法所在类的字节码对象(类名.class)

扩展–同步方法的锁对象

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
开发中,一条线程使用的是同步代码块,一条线程使用的是同步方法,但这2条线程需要实现同步--->实现这个需求,同步代码块和同步方法的锁对象必须一致,而同步方法的锁对象是默认的,所以必须清楚同步方法的锁对象

package com.itheima.demo4_同步方法的锁对象;

/**
* @Author:pengzhilin
* @Date: 2020/9/17 10:09
*/
public class Demo {


// 锁对象: this--d这个对象
public synchronized void method1() {
System.out.println(Thread.currentThread().getName() + ":打开厕所门...");
System.out.println(Thread.currentThread().getName() + ":关闭厕所门...");
System.out.println(Thread.currentThread().getName() + ":脱裤子...");
System.out.println(Thread.currentThread().getName() + ":蹲下...");
System.out.println(Thread.currentThread().getName() + ":用力...");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":擦屁股...");
System.out.println(Thread.currentThread().getName() + ":穿裤子...");
System.out.println(Thread.currentThread().getName() + ":冲厕所...");
System.out.println(Thread.currentThread().getName() + ":打开厕所门,洗手,走人...");
}

// 锁对象: Demo.class
public static synchronized void method2() {
System.out.println(Thread.currentThread().getName() + ":打开厕所门...");
System.out.println(Thread.currentThread().getName() + ":关闭厕所门...");
System.out.println(Thread.currentThread().getName() + ":脱裤子...");
System.out.println(Thread.currentThread().getName() + ":蹲下...");
System.out.println(Thread.currentThread().getName() + ":用力...");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":擦屁股...");
System.out.println(Thread.currentThread().getName() + ":穿裤子...");
System.out.println(Thread.currentThread().getName() + ":冲厕所...");
System.out.println(Thread.currentThread().getName() + ":打开厕所门,洗手,走人...");
}
}

package com.itheima.demo4_同步方法的锁对象;

/**
* @Author:pengzhilin
* @Date: 2020/9/17 10:06
*/
public class Test {
public static void main(String[] args) {
// 上厕所不被打扰
Demo d = new Demo();

new Thread(new Runnable() {
@Override
public void run() {
// 张三上厕所
synchronized (d) {
System.out.println(Thread.currentThread().getName() + ":打开厕所门...");
System.out.println(Thread.currentThread().getName() + ":关闭厕所门...");
System.out.println(Thread.currentThread().getName() + ":脱裤子...");
System.out.println(Thread.currentThread().getName() + ":蹲下...");
System.out.println(Thread.currentThread().getName() + ":用力...");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":擦屁股...");
System.out.println(Thread.currentThread().getName() + ":穿裤子...");
System.out.println(Thread.currentThread().getName() + ":冲厕所...");
System.out.println(Thread.currentThread().getName() + ":打开厕所门,洗手,走人...");
}
}
}, "张三").start();


// 李四上厕所---调用method1方法
new Thread(new Runnable() {
@Override
public void run() {
// 李四上厕所
d.method1();
}
}, "李四").start();

}
}

知识点–1.5 Lock锁

目标

  • 掌握Lock锁的使用

路径

  • Lock锁的介绍
  • Lock锁的使用

讲解

java.util.concurrent.locks.Lock机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更加面向对象

Lock锁也称同步锁,加锁与释放锁方法化了,如下:

  • public void lock() :加同步锁。
  • public void unlock():释放同步锁。

使用如下:

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
package com.itheima.demo5_Lock锁;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
* @Author:pengzhilin
* @Date: 2020/9/17 8:47
*/
public class MyRunnable implements Runnable {
// 共享变量
int tickets = 100;
Lock lock = new ReentrantLock();

@Override
public void run() {
// 线程的任务代码---卖票
while (true) {
// 加锁
lock.lock();
if (tickets < 1) {
lock.unlock();
break;
}
// 暂停100ms模拟收钱的操作
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +
":正在出售第" + tickets + "张票");

tickets--;
// 释放锁
lock.unlock();
}
}
}

package com.itheima.demo5_Lock锁;

/**
* @Author:pengzhilin
* @Date: 2020/9/17 8:46
*/
public class Test {

public static void main(String[] args) {
/*
Lock的实现类是ReentrantLock
Lock锁也称同步锁,加锁与释放锁方法化了,如下:
- public void lock():加同步锁。
- public void unlock():释放同步锁。

*/
// 创建任务对象
MyRunnable mr = new MyRunnable();

// 创建4个窗口---创建4条线程
Thread t1 = new Thread(mr, "窗口1");
Thread t2 = new Thread(mr, "窗口2");
Thread t3 = new Thread(mr, "窗口3");
Thread t4 = new Thread(mr, "窗口4");

// 启动线程,执行任务
t1.start();
t2.start();
t3.start();
t4.start();
}
}

小结

略

第二章 高并发及线程安全

知识点– 高并发及线程安全

目标

  • 理解高并发及线程安全的概述

路径

  • 高并发的概述
  • 线程安全的概述

讲解

  • 高并发:是指在某个时间点上,有大量的用户(线程)同时访问同一资源。例如:天猫的双11购物节、12306的在线购票在某个时间点上,都会面临大量用户同时抢购同一件商品/车票的情况。
  • 线程安全:在某个时间点上,当大量用户(线程)访问同一资源时,由于多线程运行机制的原因,可能会导致被访问的资源出现”数据污染”的问题。

小结

略

知识点– 多线程的运行机制

目标

  • 理解多线程的运行机制

路径

  • 多线程的运行机制

讲解

  • 当一个线程启动后,JVM会为其分配一个独立的”线程栈区”,这个线程会在这个独立的栈区中运行。

  • 看一下简单的线程的代码:

    1. 一个线程类:
    1
    2
    3
    4
    5
    6
    7
    8
    public class MyThread extends Thread {
    @Override
    public void run() {
    for (int i = 0; i < 20; i++) {
    System.out.println("小强: " + i);
    }
    }
    }
    1. 测试类:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class Demo {
    public static void main(String[] args) {
    //1.创建线程对象
    MyThread mt = new MyThread();

    //2.启动线程
    mt.start();
    for (int i = 0; i < 20; i++) {
    System.out.println("旺财: " + i);
    }
    }
    }
  • 启动后,内存的运行机制:

  • 多个线程在各自栈区中独立、无序的运行,当访问一些代码,或者同一个变量时,就可能会产生一些问题

小结

略

知识点– 多线程的安全性问题-可见性

目标

  • 多线程的安全性问题-可见性

路径

  • 多线程的安全性问题-可见性

讲解

  • 概述: 一个线程没有看见另一个线程对共享变量的修改

  • 例如下面的程序,先启动一个线程,在线程中将一个变量的值更改,而主线程却一直无法获得此变量的新值。

    1. 线程类:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class MyThread extends Thread {

    boolean flag = false;// 主和子线程共享变量

    @Override
    public void run() {

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

    // 把flag的值改为true
    flag = true;
    System.out.println("修改后flag的值为:"+flag);

    }
    }
    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
    public class Test {
    public static void main(String[] args) {
    /*
    多线程的安全性问题-可见性:
    一个线程没有看见另一个线程对共享变量的修改
    */
    // 创建子线程并启动
    MyThread mt = new MyThread();
    mt.start();

    // 主线程
    while (true){
    if (MyThread.flag == true){
    System.out.println("死循环结束");
    break;
    }
    }
    /*
    按照分析结果应该是: 子线程把共享变量flag改为true,然后主线程的死循环就可以结束
    实际结果是: 子线程把共享变量flag改为true,但主线程依然是死循环
    为什么?
    其实原因就是子线程对共享变量flag修改后的值,对于主线程是不可见的
    */

    }
    }

  • 原因:

  • Java内存模型(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。

  • 简而言之: 就是所有共享变量都是存在主内存中的,线程在执行的时候,有单独的工作栈内存,会把共享变量拷贝一份到线程的单独工作内存中,并且对变量所有的操作,都是在单独的工作内存中完成的,不会直接读写主内存中的变量值

小结

  • 一个线程没有看见另一个线程对共享变量的修改

知识点– 多线程的安全性问题-有序性

目标

  • 多线程的安全性问题-有序性

路径

  • 多线程的安全性问题-有序性

讲解

  • 有些时候“编译器”在编译代码时,会对代码进行“重排”,例如:

    ​ int a = 10; //1

    ​ int b = 20; //2

    ​ int c = a + b; //3

    第一行和第二行可能会被“重排”:可能先编译第二行,再编译第一行,总之在执行第三行之前,会将1,2编译完毕。1和2先编译谁,不影响第三行的结果。

  • 但在“多线程”情况下,代码重排,可能会对另一个线程访问的结果产生影响:

    多线程环境下,我们通常不希望对一些代码进行重排的!!

小结

略

知识点–多线程的安全性问题-原子性

目标

  • 多线程的安全性问题-原子性

路径

  • 多线程的安全性问题-原子性

讲解

  • 概述:所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行,多个操作是一个不可以分割的整体。

  • 请看以下示例:

    • 一条子线程和一条主线程都对共享变量a进行++操作,每条线程对a++操作100000次

    1.制作线程类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    package com.itheima.demo8_多线程的安全性问题_原子性;

    /**
    * @Author:pengzhilin
    * @Date: 2020/9/17 11:50
    */
    public class MyThread extends Thread {
    // 共享变量
    static int a = 0;

    @Override
    public void run() {
    // 任务:对共享变量a自增10万次
    for (int i = 0; i < 100000; i++) {
    a++;
    }
    System.out.println("子线程执行完毕!");
    }
    }

    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
    package com.itheima.demo8_多线程的安全性问题_原子性;

    /**
    * @Author:pengzhilin
    * @Date: 2020/9/17 11:48
    */
    public class Test {

    public static void main(String[] args) {
    /*
    多线程的安全性问题-原子性:
    概述:所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,
    要么所有的操作都不执行,多个操作是一个不可以分割的整体。
    演示:一条子线程和一条主线程都对共享变量a进行++操作,每条线程对a++操作100000次
    */
    // 创建线程对象
    MyThread mt = new MyThread();
    // 启动线程
    mt.start();

    // 主线程对共享变量a自增10万次
    for (int i = 0; i < 100000; i++) {
    MyThread.a++;
    }

    // 暂停,保证主线程和子线程都对共享变量a自增完了10万次,再统计a的结果
    try {
    Thread.sleep(3000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }

    // 打印最终共享变量a的值
    System.out.println("最终:"+MyThread.a);// 最终:162243
    /*
    期望:最终a的值为200000
    */
    }
    }

    原因:两个线程对共享变量的操作产生覆盖的效果

小结

略

第三章 volatile关键字

知识点–什么是volatile关键字

目标

  • volatile关键字概述

路径

  • volatile关键字概述

讲解

  • volatile是一个”变量修饰符”,它只能修饰”成员变量”,它能强制线程每次从主内存获取值,并能保证此变量不会被编译器优化。
  • volatile能解决变量的可见性、有序性;
  • volatile不能解决变量的原子性

小结

略

知识点– volatile解决可见性

目标

  • volatile解决可见性

路径

  • volatile解决可见性

讲解

  • 将1.3的线程类MyThread做如下修改:

    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
    package com.itheima.demo9_volatile解决可见性;

    /**
    * @Author:pengzhilin
    * @Date: 2020/9/17 11:01
    */
    public class MyThread extends Thread {
    // 共享变量(主和子线程共享)
    volatile static boolean flag = false;

    @Override
    public void run() {
    // 暂停5秒
    try {
    Thread.sleep(5000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    // 修改flag的值
    flag = true;
    System.out.println("子线程把flag的值修改为true了");
    }
    }

    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
    package com.itheima.demo9_volatile解决可见性;

    /**
    * @Author:pengzhilin
    * @Date: 2020/9/17 11:00
    */
    public class Test {
    public static void main(String[] args) {
    /*
    volatile解决可见性:使用volatile关键字修饰共享变量
    */
    // 创建并启动线程
    MyThread mt = new MyThread();
    mt.start();

    // 主线程
    while (true){
    if (MyThread.flag == true){
    System.out.println("结束死循环");
    break;
    }
    }

    }
    }

    当变量被修饰为volatile时,会迫使线程每次使用此变量,都会去主内存获取,保证其可见性

小结

略

知识点– volatile解决有序性

目标

  • volatile解决有序性

路径

  • volatile解决有序性

讲解

  • 当变量被修饰为volatile时,会禁止代码重排

小结

略

知识点– volatile不能解决原子性

目标

  • volatile不能解决原子性

路径

  • volatile不能解决原子性

讲解

  • 对于示例1.5,加入volatile关键字并不能解决原子性:

    1. 线程类:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    package com.itheima.demo10_volatile不能解决原子性;

    /**
    * @Author:pengzhilin
    * @Date: 2020/9/17 11:50
    */
    public class MyThread extends Thread {
    // 共享变量
    volatile static int a = 0;

    @Override
    public void run() {
    // 任务:对共享变量a自增80万次
    for (int i = 0; i < 800000; i++) {
    a++;
    }
    System.out.println("子线程执行完毕!");
    }
    }

    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
    package com.itheima.demo10_volatile不能解决原子性;

    /**
    * @Author:pengzhilin
    * @Date: 2020/9/17 11:48
    */
    public class Test {

    public static void main(String[] args) {

    // 创建线程对象
    MyThread mt = new MyThread();
    // 启动线程
    mt.start();

    // 主线程对共享变量a自增80万次
    for (int i = 0; i < 800000; i++) {
    MyThread.a++;
    }

    // 暂停,保证主线程和子线程都对共享变量a自增完了80万次,再统计a的结果
    try {
    Thread.sleep(3000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }

    // 打印最终共享变量a的值
    System.out.println("最终:"+ MyThread.a);// 最终:少于160万

    }
    }

    所以,volatile关键字只能解决”变量”的可见性、有序性问题,并不能解决原子性问题

小结

略

第四章 原子类

知识点– 原子类概述

目标

  • 原子类概述

路径

  • 原子类概述

讲解

  • 在java.util.concurrent.atomic包下定义了一些对“变量”操作的“原子类”:

    ​ 1).java.util.concurrent.atomic.AtomicInteger:对int变量操作的“原子类”;

    ​ 2).java.util.concurrent.atomic.AtomicLong:对long变量操作的“原子类”;

    ​ 3).java.util.concurrent.atomic.AtomicBoolean:对boolean变量操作的“原子类”;

    它们可以保证对“变量”操作的:原子性、有序性、可见性。

小结

略

知识点– AtomicInteger类示例

目标

  • AtomicInteger类示例

路径

  • AtomicInteger类示例

讲解

  • 我们可以通过AtomicInteger类,来看看它们是怎样工作的

    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
    package com.itheima.demo11_AtomicInteger类;

    import java.util.concurrent.atomic.AtomicInteger;

    /**
    * @Author:pengzhilin
    * @Date: 2020/9/17 11:50
    */
    public class MyThread extends Thread {
    // 共享变量
    //static int a = 0;
    //static AtomicInteger a = new AtomicInteger();// a表示整数0
    static AtomicInteger a = new AtomicInteger(0);// a表示整数0

    @Override
    public void run() {
    // 任务:对共享变量a自增10万次
    for (int i = 0; i < 100000; i++) {
    //a++;
    a.getAndIncrement();// 相当于a++
    }
    System.out.println("子线程执行完毕!");
    }
    }

    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
    package com.itheima.demo11_AtomicInteger类;

    /**
    * @Author:pengzhilin
    * @Date: 2020/9/17 11:48
    */
    public class Test {

    public static void main(String[] args) {
    /*
    AtomicInteger类:保证原子操作
    */
    // 创建线程对象
    MyThread mt = new MyThread();
    // 启动线程
    mt.start();

    // 主线程对共享变量a自增10万次
    for (int i = 0; i < 100000; i++) {
    //MyThread.a++;
    MyThread.a.getAndIncrement();
    }

    // 暂停,保证主线程和子线程都对共享变量a自增完了10万次,再统计a的结果
    try {
    Thread.sleep(3000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }

    // 打印最终共享变量a的值
    System.out.println("最终:"+ MyThread.a);

    }
    }

    我们能看到,无论程序运行多少次,其结果总是正确的!

小结

略

知识点– AtomicInteger类的工作原理-CAS机制

目标

  • AtomicInteger类的工作原理-CAS机制

路径

  • AtomicInteger类的工作原理-CAS机制

讲解

1588057318197

小结

略

知识点– AtomicIntegerArray类示例

目标

  • AtomicIntegerArray类示例

路径

  • AtomicIntegerArray类示例

讲解

  • 常用的数组操作的原子类:
    1).java.util.concurrent.atomic.AtomicIntegetArray:对int数组操作的原子类。 int[]

    ​ 2).java.util.concurrent.atomic.AtomicLongArray:对long数组操作的原子类。long[]

    ​ 3).java.util.concurrent.atomic.AtomicReferenceArray:对引用类型数组操作的原子类。Object[]

  • 数组的多线程并发访问的安全性问题:

    1. 线程类:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class MyThread extends Thread {
    public static int[] arr = new int[1000];

    @Override
    public void run() {
    for (int i = 0; i < arr.length(); i++) {
    arr[i]++;
    }
    System.out.println("结束");
    }
    }

    1. 测试类:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class Demo {
    public static void main(String[] args) throws InterruptedException {
    for (int i = 0; i < 1000; i++) {
    new MyThread().start();//创建1000个线程,每个线程为数组的每个元素+1
    }

    Thread.sleep(1000 * 5);//让所有线程执行完毕

    System.out.println("主线程休息5秒醒来");
    for (int i = 0; i < MyThread.arr.length; i++) {
    System.out.println(MyThread.arr[i]);
    }
    }
    }

    正常情况,数组的每个元素最终结果应为:1000,而实际打印:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    1000
    1000
    1000
    1000
    999
    999
    999
    999
    999
    999
    999
    999
    1000
    1000
    1000
    1000

    可以发现,有些元素并不是1000.

  • 为保证数组的多线程安全,改用AtomicIntegerArray类,演示:

    1. 线程类:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class MyThread extends Thread {

    //改用原子类,使用数组构造
    public static AtomicIntegerArray arr = new AtomicIntegerArray(1000);
    @Override
    public void run() {
    for (int i = 0; i < arr.length(); i++) {
    arr.addAndGet(i, 1);//将i位置上的元素 + 1
    }
    System.out.println("结束");
    }
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
      
    > 1. 测试类:

    ```java
    public class Demo {
    public static void main(String[] args) throws InterruptedException {
    for (int i = 0; i < 1000; i++) {
    new MyThread().start();
    }
    Thread.sleep(1000 * 5);//让所有线程执行完毕

    System.out.println("主线程休息5秒醒来");
    for (int i = 0; i < MyThread.arr.length(); i++) {
    System.out.println(MyThread.arr.get(i));
    }
    }
    }

    先在能看到,每次运行的结果都是正确的。

小结

略

第五章 并发包

在JDK的并发包里提供了几个非常有用的并发容器和并发工具类。供我们在多线程开发中进行使用。

知识点–CopyOnWriteArrayList

目标

  • 掌握CopyOnWriteArrayList使用

路径

  • 演示ArrayList线程不安全
  • 演示CopyOnWriteArrayList线程安全

讲解

  • ArrayList的线程不安全:

    1. 定义线程类:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    package com.itheima.demo13_CopyOnWriteArrayList;

    import java.util.ArrayList;

    /**
    * @Author:pengzhilin
    * @Date: 2020/9/17 16:07
    */
    public class MyThread1 extends Thread{
    // 共享变量
    static ArrayList<Integer> list = new ArrayList<>();

    @Override
    public void run() {
    // 往集合中添加100000个元素
    for (int i = 0; i < 100000; i++) {
    list.add(i);
    }
    }
    }

    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.itheima.demo13_CopyOnWriteArrayList;

    /**
    * @Author:pengzhilin
    * @Date: 2020/9/17 16:07
    */
    public class Test1 {
    public static void main(String[] args) {
    // 演示ArrayList线程不安全:2条线程都往ArrayList集合中添加100000个元素
    // 创建并启动线程
    new MyThread1().start();

    // 主线程往集合中添加100000个元素
    for (int i = 0; i < 100000; i++) {
    MyThread1.list.add(i);
    }

    // 为了保证主线程和子线程对list集合都操作完毕
    try {
    Thread.sleep(5000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }

    System.out.println("list集合元素个数:"+ MyThread1.list.size());
    /*
    期望: 200000个元素
    实际: 少于200000个元素
    */
    }
    }

    最终结果可能会抛异常,或者最终集合大小是不正确的。

  • CopyOnWriteArrayList是线程安全的:

    1. 定义线程类:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    package com.itheima.demo13_CopyOnWriteArrayList;

    import java.util.ArrayList;
    import java.util.concurrent.CopyOnWriteArrayList;

    /**
    * @Author:pengzhilin
    * @Date: 2020/9/17 16:07
    */
    public class MyThread2 extends Thread{
    // 共享变量
    //static ArrayList<Integer> list = new ArrayList<>();
    static CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();

    @Override
    public void run() {
    // 往集合中添加1000个元素
    for (int i = 0; i < 1000; i++) {
    list.add(i);
    }
    }
    }

    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
    package com.itheima.demo13_CopyOnWriteArrayList;

    /**
    * @Author:pengzhilin
    * @Date: 2020/9/17 16:07
    */
    public class Test2 {
    public static void main(String[] args) {
    // 演示CopyOnWriteArrayList线程安全:2条线程都往CopyOnWriteArrayList集合中添加1000个元素
    // 创建并启动线程
    new MyThread2().start();

    // 主线程往集合中添加1000个元素
    for (int i = 0; i < 1000; i++) {
    MyThread2.list.add(i);
    }

    // 为了保证主线程和子线程对list集合都操作完毕
    try {
    Thread.sleep(5000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }

    System.out.println("list集合元素个数:"+ MyThread2.list.size());
    /*
    实际: 2000个元素
    */
    }
    }

    结果始终是正确的。

小结

略

知识点–CopyOnWriteArraySet

目标

  • 掌握-CopyOnWriteArraySet使用

路径

  • 演示HashSet线程不安全
  • 演示CopyOnWriteArraySet线程安全

讲解

  • HashSet仍然是线程不安全的:

    1. 线程类:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class MyThread extends Thread {
    public static Set<Integer> set = new HashSet<>();//线程不安全的
    @Override
    public void run() {
    for (int i = 0; i < 10000; i++) {
    set.add(i);
    }
    System.out.println("添加完毕!");
    }
    }

    1. 测试类:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class Demo {
    public static void main(String[] args) throws InterruptedException {
    MyThread t1 = new MyThread();
    t1.start();

    //主线程也添加10000个
    for (int i = 10000; i < 20000; i++) {
    MyThread.set.add(i);

    }
    Thread.sleep(1000 * 3);
    System.out.println("最终集合的长度:" + MyThread.set.size());
    }
    }

    最终结果可能会抛异常,也可能最终的长度是错误的!!

  • CopyOnWriteArraySet是线程安全的:

    1. 线程类:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class MyThread extends Thread {
    // public static Set<Integer> set = new HashSet<>();//线程不安全的
    //改用:线程安全的Set集合:
    public static CopyOnWriteArraySet<Integer> set = new CopyOnWriteArraySet<>();

    @Override
    public void run() {
    for (int i = 0; i < 10000; i++) {
    set.add(i);
    }
    System.out.println("添加完毕!");
    }
    }

    1. 测试类:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class Demo {
    public static void main(String[] args) throws InterruptedException {
    MyThread t1 = new MyThread();
    t1.start();

    //主线程也添加10000个
    for (int i = 10000; i < 20000; i++) {
    MyThread.set.add(i);

    }
    Thread.sleep(1000 * 3);
    System.out.println("最终集合的长度:" + MyThread.set.size());
    }
    }

    可以看到结果总是正确的!!

小结

略

知识点– ConcurrentHashMap

目标

  • 掌握ConcurrentHashMap使用

路径

  • 演示HashMap线程不安全
  • 演示Hashtable线程安全
  • 演示ConcurrentHashMap线程安全

讲解

  • HashMap是线程不安全的。

    1. 线程类:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    package com.itheima.demo15_ConcurrentHashMap;

    import java.util.HashMap;

    /**
    * @Author:pengzhilin
    * @Date: 2020/9/17 16:29
    */
    public class MyThread extends Thread {
    // 共享变量
    static HashMap<Integer, Integer> map = new HashMap<>();

    @Override
    public void run() {
    // 往集合中添加键值对
    for (int i = 0; i < 300000; i++) {
    map.put(i,i);
    }
    }
    }

    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
    package com.itheima.demo15_ConcurrentHashMap;

    /**
    * @Author:pengzhilin
    * @Date: 2020/9/17 16:28
    */
    public class Test1 {
    public static void main(String[] args) throws InterruptedException {
    // 演示HashMap线程不安全:2条线程往集合中添加键值对
    new MyThread().start();

    // 往集合中添加键值对
    for (int i = 0; i < 300000; i++) {
    MyThread.map.put(i,i);
    }

    Thread.sleep(5000);

    System.out.println("集合键值对个数:"+MyThread.map.size());
    /*
    期望: 3万个
    实际: 大于3万个
    */

    }
    }

    运行结果可能会出现异常、或者结果不准确!!

  • Hashtable是线程安全的,但效率低:

    我们改用JDK提供的一个早期的线程安全的Hashtable类来改写此例,注意:我们加入了”计时”。

    1. 线程类:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    package com.itheima.demo15_ConcurrentHashMap;

    import java.util.HashMap;
    import java.util.Hashtable;

    /**
    * @Author:pengzhilin
    * @Date: 2020/9/17 16:29
    */
    public class MyThread2 extends Thread {
    // 共享变量
    //static HashMap<Integer, Integer> map = new HashMap<>();
    static Hashtable<Integer, Integer> map = new Hashtable<>();

    @Override
    public void run() {
    // 往集合中添加键值对
    for (int i = 0; i < 400000; i++) {
    map.put(i,i);
    }
    }
    }

    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
    package com.itheima.demo15_ConcurrentHashMap;

    /**
    * @Author:pengzhilin
    * @Date: 2020/9/17 16:28
    */
    public class Test2 {
    public static void main(String[] args) throws InterruptedException {
    // 演示Hashtable线程不安全:2条线程往集合中添加键值对
    new MyThread2().start();

    // 往集合中添加键值对
    for (int i = 0; i < 400000; i++) {
    MyThread2.map.put(i,i);
    }

    Thread.sleep(5000);

    System.out.println("集合键值对个数:"+ MyThread2.map.size());
    /*
    实际: 30万个
    */

    }
    }

    能看到结果是正确的,但耗时较长。

  • 改用ConcurrentHashMap

    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
    package com.itheima.demo15_ConcurrentHashMap;

    import java.util.Hashtable;
    import java.util.concurrent.ConcurrentHashMap;

    /**
    * @Author:pengzhilin
    * @Date: 2020/9/17 16:29
    */
    public class MyThread3 extends Thread {
    // 共享变量
    //static HashMap<Integer, Integer> map = new HashMap<>();
    //static Hashtable<Integer, Integer> map = new Hashtable<>();
    static ConcurrentHashMap<Integer, Integer> map = new ConcurrentHashMap<>();

    @Override
    public void run() {
    // 往集合中添加键值对
    for (int i = 0; i < 400000; i++) {
    map.put(i,i);
    }
    }
    }

    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
package com.itheima.demo15_ConcurrentHashMap;

/**
* @Author:pengzhilin
* @Date: 2020/9/17 16:28
*/
public class Test3 {
public static void main(String[] args) throws InterruptedException {
// 演示ConcurrentHashMap线程不安全:2条线程往集合中添加键值对
new MyThread3().start();

// 往集合中添加键值对
for (int i = 0; i < 400000; i++) {
MyThread3.map.put(i,i);
}

Thread.sleep(5000);

System.out.println("集合键值对个数:"+ MyThread3.map.size());
/*
实际: 40万个
*/

}
}

可以看到效率提高了很多!!!

  • HashTable效率低下原因:
1
2
public synchronized V put(K key, V value) 
public synchronized V get(Object key)

HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同步方法时,会进入阻塞状态。如线程1使用put进行元素添加,线程2不但不能使用put方法添加元素,也不能使用get方法来获取元素,所以竞争越激烈效率越低。

ConcurrentHashMap高效的原因:CAS + 局部(synchronized)锁定

小结

略

知识点– CountDownLatch

目标

  • 掌握CountDownLatch使用

路径

  • CountDownLatch的介绍
  • CountDownLatch的使用

讲解

CountDownLatch允许一个或多个线程等待其他线程完成操作。

例如:线程1要执行打印:A和C,线程2要执行打印:B,但线程1在打印A后,要线程2打印B之后才能打印C,所以:线程1在打印A后,必须等待线程2打印完B之后才能继续执行。

CountDownLatch构造方法:

1
public CountDownLatch(int count)// 初始化一个指定计数器的CountDownLatch对象

CountDownLatch重要方法:

1
2
public void await() throws InterruptedException// 让当前线程等待
public void countDown() // 计数器进行减1
  • 示例
    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
package com.itheima.demo16_CountDownLatch使用;

import java.util.concurrent.CountDownLatch;

/**
* @Author:pengzhilin
* @Date: 2020/9/17 17:02
*/
public class MyThread1 extends Thread {

CountDownLatch cdl;

public MyThread1(CountDownLatch cdl) {
this.cdl = cdl;
}

@Override
public void run() {
//打印A
System.out.println("打印A...");

//调用await()方法进入等待(线程2打印B)
try {
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}

//打印C
System.out.println("打印C...");
}
}

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
package com.itheima.demo16_CountDownLatch使用;

import java.util.concurrent.CountDownLatch;

/**
* @Author:pengzhilin
* @Date: 2020/9/17 17:04
*/
public class MyThread2 extends Thread {

CountDownLatch cdl;

public MyThread2(CountDownLatch cdl) {
this.cdl = cdl;
}

@Override
public void run() {
// 打印B
System.out.println("打印B...");

// 调用countDown()方法让计数器-1
cdl.countDown();
}
}

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
package com.itheima.demo16_CountDownLatch使用;

import java.util.concurrent.CountDownLatch;

/**
* @Author:pengzhilin
* @Date: 2020/9/17 16:53
*/
public class Test {
public static void main(String[] args) throws InterruptedException {
/*
CountDownLatch允许一个或多个线程等待其他线程完成操作。
CountDownLatch的api:
构造方法:public CountDownLatch(int count) 初始化一个指定计数器的CountDownLatch对象
成员方法:
public void await() 让当前线程等待(当计数器的值为0,那么等待结束)
public void countDown() 计数器进行减1

例如:线程1要执行打印:A和C,线程2要执行打印:B,但要求线程1打印C之前,一定要打印B
分析:
线程1:
任务:
打印A
调用await()方法进入等待(线程2打印B)
打印C
线程2:
任务:
打印B
调用countDown()方法让计数器-1

注意:
1.创建的CountDownLatch对象的计数器初始值为1
2.线程1和线程2使用的CountDownLatch对象要一致
*/
// 创建CountDownLatch对象,指定计数器的值为1
CountDownLatch cdl = new CountDownLatch(1);
// 创建并启动线程
new MyThread1(cdl).start();
Thread.sleep(5000);
new MyThread2(cdl).start();
}
}

4). 执行结果:
会保证按:A B C的顺序打印。

说明:

CountDownLatch中count down是倒数的意思,latch则是门闩的含义。整体含义可以理解为倒数的门栓,似乎有一点“三二一,芝麻开门”的感觉。

CountDownLatch是通过一个计数器来实现的,每当一个线程完成了自己的任务后,可以调用countDown()方法让计数器-1,当计数器到达0时,调用CountDownLatch。

await()方法的线程阻塞状态解除,继续执行。

小结

1
2
3
4
5
6
CountDownLatch允许一个或多个线程等待其他线程完成操作。
CountDownLatch的api:
构造方法:public CountDownLatch(int count) 初始化一个指定计数器的CountDownLatch对象
成员方法:
public void await() 让当前线程等待(当计数器的值为0,那么等待结束)
public void countDown() 计数器进行减1

知识点– CyclicBarrier

目标

  • 掌握CyclicBarrier使用

路径

  • CyclicBarrier的介绍
  • CyclicBarrier的使用

讲解

CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。

例如:公司召集5名员工开会,等5名员工都到了,会议开始。

我们创建5个员工线程,1个开会线程,几乎同时启动,使用CyclicBarrier保证5名员工线程全部执行后,再执行开会线程。

CyclicBarrier构造方法:

1
2
3
public CyclicBarrier(int parties, Runnable barrierAction
//parties: 代表要达到屏障的线程数量
//barrierAction:表示达到屏障后要执行的线程

CyclicBarrier重要方法:

1
public int await()// 每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞
  • 示例代码:
    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
package com.itheima.demo17_CyclicBarrier使用;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

/**
* @Author:pengzhilin
* @Date: 2020/9/17 17:17
*/
public class MyRunnable implements Runnable {

CyclicBarrier cb;

public MyRunnable(CyclicBarrier cb) {
this.cb = cb;
}

@Override
public void run() {
// 到达会议室
System.out.println(Thread.currentThread().getName()+":到达了会议室");

//调用await()方法告诉CyclicBarrier,当前线程到了屏障,然后当前线程阻塞
try {
cb.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}

//离开会议室
System.out.println(Thread.currentThread().getName()+":离开会议室");

}
}

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
package com.itheima.demo17_CyclicBarrier使用;

import com.itheima.demo12_AtomicIntegerArray类.MyThread;

import java.util.concurrent.CyclicBarrier;

/**
* @Author:pengzhilin
* @Date: 2020/9/17 17:11
*/
public class Test {
public static void main(String[] args) {
/*
概述:CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。
它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,
屏障才会开门,所有被屏障拦截的线程才会继续运行。
api:
构造方法:
public CyclicBarrier(int parties, Runnable barrierAction)
参数1parties: 代表要达到屏障的线程数量
参数2barrierAction:表示达到屏障后要执行的线程任务

成员方法:
public int await() 每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞

例如:公司召集5名员工开会,等5名员工都到了,会议开始。
分析:
1.5名员工就可以使用5条线程来表示
2.5条线程的任务都一样:
到达会议室
调用await()方法告诉CyclicBarrier,当前线程到了屏障,然后当前线程阻塞
离开会议室
*/
// 创建CyclicBarrier
CyclicBarrier cb = new CyclicBarrier(5, new Runnable() {
@Override
public void run() {
System.out.println("好了,人到齐了,咱们开始开会...");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("好了,咱们今天的会议就到这结束,晚上聚餐!");
}
});

// 创建任务
MyRunnable mr = new MyRunnable(cb);
// 创建5条线程
new Thread(mr,"员工1").start();
new Thread(mr,"员工2").start();
new Thread(mr,"员工3").start();
new Thread(mr,"员工4").start();
new Thread(mr,"员工5").start();

}
}

4). 执行结果:

1588230015254

使用场景

使用场景:CyclicBarrier可以用于多线程计算数据,最后合并计算结果的场景。

需求:使用两个线程读取2个文件中的数据,当两个文件中的数据都读取完毕以后,进行数据的汇总操作。

小结

略

知识点– Semaphore

目标

  • 掌握Semaphore使用

路径

  • Semaphore的介绍
  • Semaphore的使用

讲解

Semaphore的主要作用是控制线程的并发数量。

synchronized可以起到”锁”的作用,但某个时间段内,只能有一个线程允许执行。

Semaphore可以设置同时允许几个线程执行。

Semaphore字面意思是信号量的意思,它的作用是控制访问特定资源的线程数目。

Semaphore构造方法:

1
public Semaphore(int permits)						permits 表示许可线程的数量

Semaphore重要方法:

1
2
public void acquire() throws InterruptedException	表示获取许可
public void release() release() 表示释放许可
  • 示例一:同时允许1个线程执行

1). 制作一个ClassRoom类:

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.itheima.demo18_Semaphore使用;

import java.util.concurrent.Semaphore;

/**
* @Author:pengzhilin
* @Date: 2020/9/17 17:31
*/
public class ClassRoom {

Semaphore sp = new Semaphore(3);

public void comeIn(){
// 获得许可
try {
sp.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 在教室
System.out.println(Thread.currentThread().getName()+":获得许可,进入教室...");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":离开教室,释放许可...");
// 释放许可
sp.release();
}
}

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
package com.itheima.demo18_Semaphore使用;

/**
* @Author:pengzhilin
* @Date: 2020/9/17 17:28
*/
public class Test {
public static void main(String[] args) {
/*
Semaphore使用:
概述:Semaphore的主要作用是控制线程的并发数量。
api:
public Semaphore(int permits) permits 表示许可线程的数量
public void acquire() 表示获取许可
public void release() 表示释放许可
演示:5名同学要进教室,但要设置每次只能2个同学进入教室
*/
// 创建ClassRoom对象
ClassRoom cr = new ClassRoom();

// 创建并启动线程
new Thread(new Runnable() {
@Override
public void run() {
cr.comeIn();
}
}, "张三1").start();

new Thread(new Runnable() {
@Override
public void run() {
cr.comeIn();
}
}, "张三2").start();

new Thread(new Runnable() {
@Override
public void run() {
cr.comeIn();
}
}, "张三3").start();

new Thread(new Runnable() {
@Override
public void run() {
cr.comeIn();
}
}, "张三4").start();

new Thread(new Runnable() {
@Override
public void run() {
cr.comeIn();
}
}, "张三5").start();

}
}

小结

略

知识点– Exchanger

目标

  • 掌握Exchanger使用

路径

  • Exchanger的介绍
  • Exchanger的使用

讲解

Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。

这两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange()方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。

A线程 exchange方法 把数据传递B线程

B线程 exchange方法 把数据传递A线程

Exchanger构造方法:

1
public Exchanger()

Exchanger重要方法:

1
public V exchange(V x)
  • 示例一
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
package com.itheima.demo19_Exchanger使用;

import java.util.concurrent.Exchanger;

/**
* @Author:pengzhilin
* @Date: 2020/9/17 17:46
*/
public class MyThread1 extends Thread {

Exchanger<String> ex;

public MyThread1(Exchanger<String> ex) {
this.ex = ex;
}

@Override
public void run() {
System.out.println("线程1:准备把数据传递给线程2...");
String msg = null;
try {
msg = ex.exchange("数据1");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1: 接收到线程2的数据是"+msg);
}
}


package com.itheima.demo19_Exchanger使用;

import java.util.concurrent.Exchanger;

/**
* @Author:pengzhilin
* @Date: 2020/9/17 17:49
*/
public class MyThread2 extends Thread {

Exchanger<String> ex;

public MyThread2(Exchanger<String> ex) {
this.ex = ex;
}

@Override
public void run() {
System.out.println("线程2:准备把数据传递给线程1...");
String msg = null;
try {
msg = ex.exchange("数据2");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程2: 接收到线程1的数据是"+msg);
}
}

2). 制作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
package com.itheima.demo19_Exchanger使用;

import java.util.concurrent.Exchanger;

/**
* @Author:pengzhilin
* @Date: 2020/9/17 17:37
*/
public class Test {
public static void main(String[] args) {
/*
Exchanger<V>使用:
概述:Exchanger用于进行线程间的数据交换。
api:
public Exchanger();
public V exchange(V x): 传递数据,参数就是你要传递的数据,返回值就是其他线程传递给你的数据
演示:
线程1: 把"数据1"传递给线程2
线程2: 把"数据2"传递给线程1
*/
// 创建Exchanger对象
Exchanger<String> ex = new Exchanger<>();

// 创建并启动线程
new MyThread1(ex).start();
new MyThread2(ex).start();

}
}

使用场景:可以做数据校对工作

需求:比如我们需要将纸制银行流水通过人工的方式录入成电子银行流水。为了避免错误,采用AB岗两人进行录入,录入到两个文件中,系统需要加载这两个文件,

并对两个文件数据进行校对,看看是否录入一致,

小结

略

第六章 线程池方式

知识点– 线程池的概念

目标

  • 能够理解线程池的概念

路径

  • 线程池的思想
  • 线程池的概念
  • 线程池的好处

讲解

线程池的思想

我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。

那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?

在Java中可以通过线程池来达到这样的效果。

线程池概念

  • 线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

由于线程池中有很多操作都是与优化资源相关的,我们在这里就不多赘述。我们通过一张图来了解线程池的工作原理:

线程池的好处

  1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

小结

  • 线程池的原理: 创建线程池的时候初始化指定数量的线程,当有任务需要线程执行的时候,就在线程池中随机分配空闲线程来执行当前的任务;如果线程池中没有空闲的线程,那么该任务就进入任务队列中进行等待,等待其他线程空闲下来,再执行任务.(线程重复利用)

知识点–线程池的使用

目标

  • 能够使用线程池执行任务

路径

  • 线程池相关类的介绍
  • 线程池的使用

讲解

Java里面线程池的顶级接口是java.util.concurrent.Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService。

要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在java.util.concurrent.Executors线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工厂类来创建线程池对象。

Executors类中有个创建线程池的方法如下:

  • public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)

获取到了一个线程池ExecutorService 对象,那么怎么使用呢,在这里定义了一个使用线程池对象的方法如下:

  • public Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行任务

  • public <T> Future<T> submit(Callable<T> task):获取线程池中的某一个线程对象,并执行任务

    Future接口:用来记录线程任务执行完毕后产生的结果。

使用线程池中线程对象的步骤:

  1. 创建线程池对象。
  2. 创建Runnable接口子类对象。(task)
  3. 提交Runnable接口子类对象。(take task)
  4. 关闭线程池(一般不做)。

Runnable实现类代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyRunnable implements Runnable {
@Override
public void run() {
//任务
System.out.println(Thread.currentThread().getName()+":开始执行实现Runnable方式的任务....");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":执行完毕....");
}
}

线程池测试类:

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
public class Test1_Runnable {
public static void main(String[] args) {
/*
线程池使用一:任务是通过实现Runnable的方式创建
1.使用Executors工厂类中的静态方法来创建线程池:
public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象,通过参数指定线程池中的线程数量
2..提交并执行任务:
- public Future<?> submit(Runnable task):通过参数传入任务,获取线程池中的某一个线程对象,并执行任务
*/
// 1.创建线程池,初始化线程
ExecutorService es = Executors.newFixedThreadPool(3);// 创建一个线程池对象,该线程池中有3条线程

// 2.创建任务
MyRunnable mr = new MyRunnable();

// 3.提交并执行任务
es.submit(mr);
es.submit(mr);
es.submit(mr);
es.submit(mr);

// 4.销毁线程池(一般不操作)
//es.shutdown();
}
}

Callable测试代码:

  • <T> Future<T> submit(Callable<T> task) : 获取线程池中的某一个线程对象,并执行.

    Future : 表示计算的结果.

  • V get() : 获取计算完成的结果。

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
package com.itheima.demo20_线程池的使用;

import java.util.concurrent.Callable;

/**
* @Author:pengzhilin
* @Date: 2020/9/17 18:26
*/
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("任务开始...");
Thread.sleep(5000);
System.out.println("任务结束...");
return "itheima";
}
}

package com.itheima.demo20_线程池的使用;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

/**
* @Author:pengzhilin
* @Date: 2020/9/17 18:26
*/
public class Test2 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 1.创建线程池,初始化2条线程
ExecutorService pools = Executors.newFixedThreadPool(2);

// 2.创建任务
MyCallable mc = new MyCallable();

// 3.提交任务,执行任务
pools.submit(mc);
pools.submit(mc);
pools.submit(mc);
pools.submit(mc);
pools.submit(mc);
pools.submit(mc);
pools.submit(mc);
Future<String> f = pools.submit(mc);
System.out.println(f.get());// itheima

// 4.销毁线程池(开发中,一般不会)
pools.shutdown();
}
}

小结

略

实操–线程池的练习

需求

  • 使用线程池方式执行任务,返回1-n的和

分析

因为需要返回求和结果,所以使用Callable方式的任务

实现

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 com.itheima.demo21_线程池的练习;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

/**
* @Author:pengzhilin
* @Date: 2020/9/17 18:30
*/
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 线程池的练习: 使用线程池方式执行任务,返回1-n的和
// 创建线程池
ExecutorService pools = Executors.newFixedThreadPool(2);

// 创建任务
MyCallable mc = new MyCallable(100);

// 提交任务
Future<Integer> f = pools.submit(mc);
System.out.println("n的累加和:"+f.get());// n的累加和:5050

// 销毁线程池(一般不操作)
pools.shutdown();
}
}

SumCallable.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
package com.itheima.demo21_线程池的练习;

import java.util.concurrent.Callable;

/**
* @Author:pengzhilin
* @Date: 2020/9/17 18:31
*/
public class MyCallable implements Callable<Integer> {
int n;

public MyCallable(int n) {
this.n = n;
}

@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= n; i++) {
sum += i;
}
return sum;
}
}

小结

略

总结

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
必须练习:
1.同步代码块和同步锁案例
2.volatile关键字的使用
3.原子类的使用
4.线程池的使用
5.理解高并发线程安全问题出现的原因\解决办法

并发包:---->理解做好笔记--(休息的时候复习)

- 能够解释安全问题的出现的原因
可见性问题:
原因:一条线程对共享变量的修改,对其他线程不可见
解决办法: volatile关键字修饰共享变量

有序性问题:
原因: 编译器可能会对代码进行重排,造成数据混乱
解决办法:volatile关键字修饰共享变量

原子性问题:
共享变量原子性问题:
原因:多条线程对共享变量的多次操作,产生了覆盖的效果
解决办法: 原子类,同步锁,Lock锁

代码块原子性问题:
原因:一段代码,被一条线程执行,可能会被其他线程打断,从而造成数据混乱
解决办法: 同步锁,Lock锁

- 能够使用同步代码块解决线程安全问题
同步代码块:
格式:
synchronized(锁对象){

}
锁对象:
1.语法上,可以是任意类的对象
2.如果多条线程想要实现同步,那么这多条线程的锁对象必须一致

- 能够使用同步方法解决线程安全问题
同步方法:
格式:
修饰符 synchronized 返回值类型 方法名(形参列表){}
锁对象;
非静态同步方法: 锁对象this
静态同步方法:该方法所在类的字节码对象(类名.class)

- 能够说出volatile关键字的作用
解决可见性,有序性问题,不能解决原子性问题

- 能够说明volatile关键字和synchronized关键字的区别
1.volatile只能修饰成员变量,synchronized可以修饰代码块或者方法
2.volatile是强制要求子线程每次使用共享变量都是重新从主内存中获取
synchronized实现的是互斥访问
3.volatile只能解决可见性,有序性问题,不能解决原子性问题,但synchronize都可以解决

- 能够理解原子类的工作机制
cas机制: 比较并交换

- 能够掌握原子类AtomicInteger的使用
构造方法\成员方法

- 能够描述ConcurrentHashMap类的作用
当成Map集合使用,只是线程安全

- 能够描述CountDownLatch类的作用
h允许一个或多个线程等待其他线程完成操作

- 能够描述CyclicBarrier类的作用
。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。

- 能够表述Semaphore类的作用
控制线程并发的数量

- 能够描述Exchanger类的作用
2条线程之间交换数据

- 能够描述Java中线程池运行原理
线程池的原理: 创建线程池的时候初始化指定数量的线程,当有任务需要线程执行的时候,就在线程池中随机分配空闲线程来执行当前的任务;如果线程池中没有空闲的线程,那么该任务就进入任务队列中进行等待,等待其他线程空闲下来,再执行任务.(线程重复利用)


day10【线程安全、volatile关键字、原子性、并发包、死锁、线程池】
http://www.kju147.com/2022/06/28/day10【线程安全、volatile关键字、原子性、并发包、死锁、线程池】/
作者
个人学习
发布于
2022年6月28日
许可协议