多线程详解

线程简介

  • 普通方法调用和多线程

    普通方法调用:主线程调用run()方法,主线程执行run()只有主线程一条执行路径

    多线程:主线程调用start()方法,子线程执行run();多条执行路径,主线程和子线程并行交替执行

  • 程序、进程Process线程Thread

    • 程序是指令和数据的有序集合,是一个静态的概念
  • 进程是程序的一次执行过程,是一个动态的概念。进程是资源分配的单位

    • 一个进程中可以包含若干个线程线程是CPU调度和执行的单位

      如视频中同时听声音,看图像,看弹幕。

    很多多线程是模拟出来的,真正的多线程是指有多个CPU,如服务器

  • 程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程gc线程

  • main()称之为主线程,为系统的入口,用于执行整个程序

  • 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为干预的

  • 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制

  • 线程会带来额外的开销,如CPU调度时间并发控制开销

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


线程创建

三种创建方式:

  1. Runnable interface:实现Runnable接口

  2. Thread class:继承Thread类

    Thread类就实现了Runnable接口

  3. Callable interface:实现Callable接口(初步阶段仅作了解)

线程创建后不一定立即执行,CPU负责安排调度


继承Thread类

  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
20
21
22
23
24
// 创建线程方式一:继承Thread类,重写run()方法,调用start()开启线程
public class TestThread1 extends Thread {
@Override
public void run() {
//run方法线程体
for (int i = 0; i < 100; i++) {
System.out.println("我在看代码---" + i);
}
}

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

// 创建一个线程对象
TestThread1 testThread1 = new TestThread1();

// 调用start方法开启线程
testThread1.start();

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

线程实践——网图下载

  1. 搜索commons-io
  2. 在apache官网下载 Apache Commons IO-bin的压缩包
  3. 解压后复制commons-io-2.8.0.jar
  4. 打开IDEA,在合适目录层级下new package – lib
  5. 在生成的lib下粘贴
  6. 右键package lib,选择Add as Library
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
import org.apache.commons.io.FileUtils;

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

//练习Thread,实现多线程同步下载图片
public class TestThread2 extends Thread {
private String url; //网络图片地址
private String name; //保存的文件名

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

//下载图片线程的执行体
@Override
public void run() {
WebDownloader webDownloader = new WebDownloader();
webDownloader.downloader(url, name);
System.out.println("下载了文件名为:" + name);
}

public static void main(String[] args) {
String str = "http://p3-q.mafengwo.net/s13/M00/5A/FE/wKgEaVyYzWqAZIvdABtU3xfCwTg13.jpeg";
TestThread2 t1 = new TestThread2(str, "douban1.jpg");
TestThread2 t2 = new TestThread2(str, "douban2.jpg");
TestThread2 t3 = new TestThread2(str, "douban3.jpg");

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

class WebDownloader {
//下载方法
public void downloader(String url, String name) {
try {
FileUtils.copyURLToFile(new URL(url), new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO异常,downloader方法出现问题");
}
}
}

实现Runnable接口(推荐)

  1. 自定义类实现Runnable接口
  2. 实现run()方法,编写线程执行体
  3. 创建线程对象,丢入Runnable接口实现类调用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
// 创建线程方式2:实现Runnable接口,实现run方法,执行线程需要丢入runnable接口实现类,调用start方法开启线程
public class TestThread3 implements Runnable {

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

public static void main(String[] args) {
// 创建Runnable接口的实现类对象
TestThread3 testThread3 = new TestThread3();

// 创建线程对象,通过线程对象来开启线程(代理)
new Thread(testThread3).start();

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


继承Thread类和实现Runnable接口的比较

继承Thread类

  • 子类继承Thread类具备多线程能力
  • 启动线程的方式:子类对象.strat()
  • 不建议使用:避免OOP(Object-Oriented Programming)单继承的局限性

实现Runnable接口

  • 实现Runnable接口的对象具备多线程能力

  • 启动线程的方式:Thread对象(传入目标对象).start()

  • 推荐使用

    避免单继承局限性,灵活方便,方便同一个对象被多个线程使用


线程实践 - 龟兔赛跑

  1. 创建赛道
  2. 判断比赛是否结束
  3. 输出胜利者
  4. 龟兔赛跑开始
  5. 模拟兔子睡觉
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

// 模拟龟兔赛跑
public class Race implements Runnable {

// 胜利者
private static String winner;

@Override
public void run() {
for (int i = 1; i <= 100; i++) {
// 模拟兔子休息
if (Thread.currentThread().getName().equals("兔子") && i % 10 == 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

// 判断比赛是否结束
boolean flag = gameOver(i);
// 如果比赛结束了,就停止程序
if (flag) {
break;
}
System.out.println(Thread.currentThread().getName() + "-->跑了" + i + "步");
}
}

//判断是否完成比赛
private boolean gameOver(int steps) {
//判断是否有胜利者
if (winner != null) {//已经存在胜利者
return true;
} else if (steps >= 100) {
winner = Thread.currentThread().getName();
System.out.println("winner is " + winner);
return true;
}
return false;
}

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

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

}
}

实现Callable接口(了解)

  1. 实现Callable接口,需要返回值类型
  2. 重写call方法,需要抛出异常
  3. 创建目标对象
  4. 创建执行服务:ExecutorService ser = Executors.newFIxedThreadPool(1);
  5. 提交执行:Future<Boolean> result1 = ser.submit(t1);
  6. 获取结果:boolean r1 = result1.get()
  7. 关闭服务:ser.shutdownNow();

线程实践——网图下载改写

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

public class TestCallable implements Callable<Boolean> { // <V> 定义返回值类型
private String url; //网络图片地址
private String name; //保存的文件名

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

//下载图片线程的执行体
@Override
public Boolean call() { // 与实现接口指定的返回值类型呼应
WebDownloader webDownloader = new WebDownloader();
webDownloader.downloader(url, name);
System.out.println("下载了文件名为:" + name);
return true;
}

public static void main(String[] args) throws ExecutionException, InterruptedException {
String str = "http://p3-q.mafengwo.net/s13/M00/5A/FE/wKgEaVyYzWqAZIvdABtU3xfCwTg13.jpeg";
TestCallable t1 = new TestCallable(str, "douban1.jpg");
TestCallable t2 = new TestCallable(str, "douban2.jpg");
TestCallable t3 = new TestCallable(str, "douban3.jpg");

// 创建执行服务
ExecutorService ser = Executors.newFixedThreadPool(3); // 能容纳3个线程的线程池

// 提交执行
Future<Boolean> r1 = ser.submit(t1);
Future<Boolean> r2 = ser.submit(t2);
Future<Boolean> r3 = ser.submit(t3);

// 获取结果
boolean rs1 = r1.get();
boolean rs2 = r2.get();
boolean rs3 = r3.get();

System.out.println(rs1);
System.out.println(rs2);
System.out.println(rs3);

//关闭服务
ser.shutdownNow();
}
}

class WebDownloader {
//下载方法
public void downloader(String url, String name) {
try {
FileUtils.copyURLToFile(new URL(url), new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO异常,downloader方法出现问题");
}
}
}

Lambda 表达式

  1. 实质是属于函数式编程的概念

    1. (params) -> expression [表达式]
    2. (params) -> statement [语句]
    3. (params) -> {statements}
    1
    new Thread(() -> System.out.println("多线程学习")).start();

理解Functional Interface(函数式接口)是学习Java 8 Lambda表达式的关键。

  • 任何接口,如果只包含一个抽象方法,它就是一个函数式接口

    Runnable就是一个函数式接口。

    1
    2
    3
    	public interface Runnable {
    public abstract void run(); // 接口中的所有定义的方法其实都是抽象的 public abstract,因此修饰符可省略
    }
  • 对于函数式接口,可以通过Lambda表达式来创建该接口的对象

为什么使用Lambda表达式

  • 避免匿名内部类定义过多
  • 使代码简洁
  • 去掉了一堆没有意义的代码,只留下核心逻辑

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
/*
推导Lambda表达式
*/
public class TestLambda1 {

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


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

like = new Like2(); // 静态内部类
like.lambda();

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

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

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

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

// 2. 实现类
class Like implements Ilike {
@Override
public void lambda() {
System.out.println("I like Lambda");
}
}

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
public class TestLambda2 {
public static void main(String[] args) {
// 带参数的Lambda表达式
Ilove love = (int a) -> {
System.out.println("I love you-->" + a);
};

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

// 简化2:去掉括号
love = a -> {
System.out.println("I love you-->" + a);
};

// 简化3:去掉花括号(只有一条语句时才能生效)
love = a -> System.out.println("I love you-->" + a);

love.love(1000);
}
}

interface Ilove {
void love(int a);
}


总结

  1. 使用Lambda表达式创建接口的对象,接口必须是函数式接口

  2. 可以去掉参数类型

    有多个参数时,显式/隐式声明参数类型需要统一

  3. 只有1个参数时,参数外的括号可以省略


静态代理模式 static proxy

  1. 真实对象代理对象都要实现同一个接口
  2. 代理对象要代理真实角色

好处:

  1. 代理对象可以完成很多真实对象无法处理的事情
  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

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

// 婚庆公司的案例是为了形象说明如下静态代理的实现
new Thread(() -> System.out.println("我爱你")).start();
}
}

interface Marry {
void happyMarry();
}

// 真实角色
class You implements Marry {
@Override
public void happyMarry() {
System.out.println("死生契阔,与子成说");
}
}

// 代理角色,帮你结婚
class WeddingCompany implements Marry {
// 代理真实目标角色
private Marry target;

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

@Override
public void happyMarry() {
before();
this.target.happyMarry(); // 真实对象
after();
}

private void before() {
System.out.println("布置现场");
}

private void after() {
System.out.println("收尾款");
}
}

线程状态

  1. NEW 创建

尚未启动(未执行start方法)的线程处于此状态

  1. RUNNABLE 运行

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

  2. BLOCKED 阻塞

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

  3. WAITING 等待

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

  4. TIMED_WAITING 超时等待(过期不候

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

  5. 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
//观察测试线程的状态
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(state); // NEW

// 观察启动后
thread.start();
state = thread.getState();
System.out.println(state); // RUNNABLE

while (state != Thread.State.TERMINATED) { // 只要线程不终止,就一直输出状态
Thread.sleep(100); // 加个延时,防止输出过快
state = thread.getState(); //更新线程状态
System.out.println(state);
}
}
}

线程方法

方法 说明
setPriority(int newPriority) 更改线程的优先级
static void sleep(long millis) 让正在执行的线程休眠指定的毫秒数
void join() 线程强制执行(直接获得最高优先级,并将完成执行)
static void yield() 暂停当前正在执行的线程对象,并执行其他线程
void interrupt() 中断线程,但不建议用这个方法
boolean isAlive() 检查线程是否处于活动状态

停止线程

  • 不推荐使用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
//测试停止线程
public class TestStop implements Runnable {

//1. 设置标识位
private boolean flag = true;

@Override
public void run() {
int i = 0;
while (flag) {
System.out.println("run...Thread" + i++);
}
}

//2. 设置一个公开的方法停止线程,转换标识位
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("线程该停止了");
}
}
}
}

线程休眠

  • sleep指定当前线程阻塞的毫秒数
  • sleep存在异常InterruptedException
  • sleep时间达到后,线程进入就绪状态
  • sleep可以模拟网络延时模拟倒计时
    • 模拟网络延时可以放大发生问题的概率
  • 每一个对象都有一个锁,sleep不会释放锁

实际使用过程中,不会直接调用Thread的sleep方法,而是会通过java.util.Concurrent.TimeUnit调用sleep方法(线程安全)。

模拟网络延时

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
package com.hunter.thread.demo04;

import com.hunter.thread.demo01.TestThread4;

// 模拟网络延时:放大发生问题的概率
public class TestSleep implements Runnable {

// 票数
private int ticketNums = 10;

@Override
public void run() {
while (true) {
if (ticketNums <= 0) {
break;
}
// 模拟延时
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-->拿到了第" + ticketNums-- + "张票");
}
}

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

new Thread(tickets, "小明").start();
new Thread(tickets,"张三").start();
new Thread(tickets, "黄牛").start();
}
}

模拟倒计时/走秒

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

public class TestSleep2 {

public static void tenDown() throws InterruptedException {
int num = 10;

while (true) {
Thread.sleep(1000);
System.out.println(num--);
if (num <= 0) {
break;
}
}
}

public static void main(String[] args) {
try {
tenDown();
} catch (InterruptedException e) {
e.printStackTrace();
}

// 打印当前系统时间
Date startTime = new Date(System.currentTimeMillis()); //获取系统当前时间

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

线程礼让

  • 让当前正在执行的线程暂停,但不阻塞
  • 将线程从运行状态转为就绪状态
  • 让CPU重新调度(礼让不一定成功)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 测试礼让线程
public class TestYield {

public static void main(String[] args) {
MyYield myYield = new MyYield();

new Thread(myYield, "a").start();
new Thread(myYield, "b").start();
}
}


class MyYield implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "线程开始执行");
Thread.yield(); // 礼让
System.out.println(Thread.currentThread().getName() + "线程停止执行");
}
}

线程强制执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 测试join方法
public class TestJoin implements Runnable {

@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("VIP线程来了" + i);
}
}

public static void main(String[] args) throws InterruptedException {

//启动线程
TestJoin testJoin = new TestJoin();
Thread thread = new Thread(testJoin);
thread.start();

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

线程的优先级

Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。

线程的优先级用数字表示,范围从1~10:

  • Thread.MIN_PRIORITY == 1;
  • Thread.MAX_PRIORITY == 10;
  • Thread.NORM_PRIORITY == 5;

使用以下方式改变或获取优先级:

getPriority().setPriority(int xxx)

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) {
// 主线程的默认优先级
System.out.println(Thread.currentThread().getName() + "-->" + Thread.currentThread().getPriority());

MyPriority myPriority = new MyPriority();

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

// 设置优先级再启动
t1.start();

t2.setPriority(1);
t2.start();

t3.setPriority(4);
t3.start();

t4.setPriority(Thread.MAX_PRIORITY);
t4.start();


}

}

class MyPriority implements Runnable {

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

线程的优先级低只是获得调度的概率低,并不是优先级低就不会被调用,最终还是看CPU的调度。如果优先级低的线程反而总能优先执行,称为性能倒置,但一般不会发生。


守护线程 deamon

  • 线程分为用户线程守护线程
  • 虚拟机必须确保用户线程执行完毕
  • 虚拟机不用等待守护线程执行完毕
  • 如,后台记录操作日志,监控内存,垃圾回收等待…
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 TestDaemon {

public static void main(String[] args) {
God god = new God();
You you = new You();

Thread thread = new Thread(god);
thread.setDaemon(true); // 默认为false,表示是用户线程,正常的线程都是用户线程

thread.start();

new Thread(you).start(); // 用户线程启动
}
}

// 上帝
class God implements Runnable {

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

// 你
class You implements Runnable {

@Override
public void run() {
for (int i = 0; i < 30000; i++) {
System.out.println("你一生都开心地活着");
}
System.out.println("=========================goodbye world!========================");
}
}

线程同步机制

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

由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题。为了保证数据在方法中被访问时的正确性,在访问时加入锁机制来同步。当一个线程获得对象的排它锁,独占资源,其他线程必须等待使用后释放锁即可。存在以下问题:

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

由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种用法:synchronized方法和synchronized块。


同步方法

同步方法: public synchronized void method(int args) {}

synchronized方法控制对对象的访问,每个对象对应一把锁每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。

缺陷:若将一个大的方法申明为synchronized,将会影响效率


同步块

同步块: synchronized(Obj) {}

Obj称之为同步监视器

  • Obj可以是任何对象,但是推荐使用共享资源作为同步监视器
  • 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是class[反射中讲解]

同步监视器的执行过程:

  1. 第一个线程访问,锁定同步监视器,执行其中代码
  2. 第二个线程访问,发现同步监视器被锁定,无法访可
  3. 第一个线程访问完毕,解锁同步监视器
  4. 第二个线程访问,发现同步监视器没有锁,然后锁定并访问
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class TestSyn03 {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
synchronized (list) {
list.add(Thread.currentThread().getName());
}
}).start();

}
//添加sleep是为了主线程 能在其余线程都启动执行之后 再执行
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size()); // 不同步的话会出现不足10000的情况(存在多个线程操作了同一个list的下标)
}
}

死锁

多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行的阻塞现象。某一个同步块同时拥有两个以上对象的锁时,就可能会发生死锁的问题。

产生死锁的必要条件

  1. 互斥条件:一个资源每次只能被一个进程使用;
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放;
  3. 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

只要想办法破解其中任意一个或多个条件,就可以避免死锁


Lock (锁)

  • JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步,同步锁使用Lock对象充当。

  • java.utl.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具

    锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象

  • ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock(Reentrant:可重入),可以显式加锁、释放锁。

常用方法:

  1. lock()
  2. unlock()
  3. trylock() 尝试获取锁

使用方法:

1
2
3
4
5
6
7
8
9
10
class A {
private final ReentrantLock lock = new ReentrantLock();
public void m() {
lock.lock();
try {
// 保证线程安全的代码
} finally {
lock.unlock(); //如果同步代码有异常,要将unlock()写入finally语句块
}
}

例子:

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 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 {
int ticketNums = 10;

//定义lock锁
private final ReentrantLock lock = new ReentrantLock();

@Override
public void run() {
while (true) {
try {
lock.lock(); //加锁
if (ticketNums > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ticketNums--);
} else {
break;
}
} finally {
// 显式地解锁
lock.unlock(); // 有异常需要处理时,放到finally中
}
}
}
}

CopyOnWriteArrayList

CopyOnWriteArrayList是java.util.concurrent下的安全类型的集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TestJUC {
public static void main(String[] args) {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>(); //该类本来就是并发安全(并发包下的类)
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
list.add(Thread.currentThread().getName());
}).start();
}

try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
}
}

synchronized 与 Lock 的对比

  • synchronized是内置的java关键字,Lock是一个java类

  • Lock是显式锁(手动开启和关闭锁,别忘记关闭锁) ;synchronized是隐式锁,出了作用域自动释放。

  • Lock只有代码块锁, synchronized有代码块锁和方法锁

  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好,并且具有更好的扩展性(提供更多的子类)

  • synchronized是可重入锁,而且是非公平锁;Lock锁也是可重入锁默认是非公平锁

  • synchronized适合锁少量的同步代码,lock锁的灵活度非常高,适合锁大量的同步代码

  • 优先使用顺序

    Lock > 同步代码块(已经进入了方法体,分配了相应资源) > 同步方法(在方法体之外)


线程通信

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

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

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

生产者消费者问题

  • 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费。
  • 如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止
  • 如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止

这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件

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

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
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
//生产者消费者问题,利用缓冲区解决:管程法
public class TestProductCustomer {

public static void main(String[] args) {
SynContainer container = new SynContainer();

new Producer(container).start();
new Consumer(container).start();

}

}

// 生产者
class Producer extends Thread {
SynContainer container;

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

//生产
@Override
public void run() {
for (int i = 0; i < 100; i++) {
container.push(new Chicken(i));
System.out.println("生产了id为" + i + "的鸡");
}
}
}

// 消费者
class Consumer extends Thread {
SynContainer container;

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

//消费
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("消费了id为" + container.pop().id + "的鸡");
}
}
}

// 产品
class Chicken {
int id; // 产品编号

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

// 缓冲区
class SynContainer {
// 需要一个容器大小
Chicken[] chickens = new Chicken[10];

//容器计数器
int count = 0;

// 生产者放入产品
public synchronized void push(Chicken chicken) {
// 如果容器满了,则等待消费者消费
while (count == chickens.length) { // 使用循环,避免虚假唤醒
//生产者等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

// 如果没有满,就需要丢入产品
chickens[count] = chicken;
count++;

// 可以通知消费者消费了
this.notifyAll();
}

// 消费者取出产品
public synchronized Chicken pop() {
// 判断能否消费
while (count == 0) { // 使用循环,避免虚假唤醒
// 等待生产者生产
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

// 如果有产品,就取出
count--;
Chicken chicken = chickens[count];

// 通知生产者生产
this.notifyAll();

return chicken;
}
}

解决方式二:信号灯法

利用标识位

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
//生产者消费者问题,利用标识位解决:信号灯法
public class TestProductCustomer2 {
public static void main(String[] args) {
TV tv = new TV();
new Player(tv).start();
new Audience(tv).start();
}
}

//生产者:演员
class Player extends Thread {
TV tv;
public Player(TV tv) {
this.tv = tv;
}

@Override
public void run() {
for (int i = 0; i < 20; i++) {
if (i % 2 == 0) {
this.tv.play("两天一夜第一季播放中");
} else {
this.tv.play("进广告");
}
}
}
}


//消费者:观众
class Audience extends Thread {
TV tv;
public Audience(TV tv) {
this.tv = tv;
}

@Override
public void run() {
for (int i = 0; i < 20; i++) {
tv.watch();
}
}
}

//产品:节目
class TV {
// 演员表演,观众等待
// 观众观看,演员等待
String show;
boolean flag = true;

// 表演
public synchronized void play(String show) {

if (!flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("演员表演了:" + show);
// 通知观众观看
this.notifyAll();
this.show = show;
this.flag = !this.flag;
}

// 观看
public synchronized void watch() {
if (flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("观看了:" + show);

//通知演员表演
this.notifyAll();
this.flag = !this.flag;
}
}

Lock版

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
//生产者消费者问题,利用缓冲区解决:管程法
public class TestProductCustomer {

public static void main(String[] args) {
SynContainer container = new SynContainer();

new Producer(container).start();
new Consumer(container).start();

}

}

// 生产者
class Producer extends Thread {
SynContainer container;

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

//生产
@Override
public void run() {
for (int i = 0; i < 100; i++) {
container.push(new Chicken(i));
System.out.println("生产了id为" + i + "的鸡");
}
}
}

// 消费者
class Consumer extends Thread {
SynContainer container;

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

//消费
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("消费了id为" + container.pop().id + "的鸡");
}
}
}

// 产品
class Chicken {
int id; // 产品编号

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

// 缓冲区
class SynContainer {
// 需要一个容器大小
Chicken[] chickens = new Chicken[10];

//容器计数器
int count = 0;

Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();

// 生产者放入产品
public void push(Chicken chicken) {
// 如果容器满了,则等待消费者消费
lock.lock();
try {
while (count == chickens.length) { // 使用循环,避免虚假唤醒
//生产者等待
condition.await();
}
// 如果没有满,就需要丢入产品
chickens[count] = chicken;
count++;

// 可以通知消费者消费了
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}

// 消费者取出产品
public Chicken pop() {
lock.lock();
// 判断能否消费
try {
while (count == 0) { // 使用循环,避免虚假唤醒
// 等待生产者生产
condition.await();
}
// 如果有产品,就取出
count--;
Chicken chicken = chickens[count];

// 通知生产者生产
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
return chicken;
}
}

线程池

  • 背景:经常创建和销毀、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
  • 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。
    可以避兔频繁创建销毀、实现重复利用。
  • 好处:
    1. 提高响应速度(减少了创建新线程的时间)
    2. 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
    3. 便于线程管理

使用线程池

JDK 5.0起提供了线程池相关API:ExecutorServiceExecutors

  • ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor

    • 构造函数的参数
      • corePoolSize核心池的大小
      • maximumPoolSize:最大线程数
      • keepAliveTime:线程没有任务时最多保持多长时间后会终止
      • workQueue: 一个阻塞队列,用来存储等待执行的任务。
        • ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列
        • LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列
        • SynchronousQueue: 一个不存储元素的阻塞队列
      • threadFactory:定义如何启动一个线程,可以设置线程的名称,并且可以确定是否后台线程等。
    • void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
    • <T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般用来执行Callable
    • void shutdown()关闭连接池
  • Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池

    Executors.defaultThreadFactory()


为什么要自定义线程池

  • Executors.newFixedThreadPool()使用的LinkedBlockingQueue相当于是一个无界队列,因为队列长度限制为Integer.MAX_VALUE,如果瞬间请求非常大,会有OOM的风险。
  • Executors.newCachedThreadPool()最大线程数设置为最大的Integer.MAX_VALUE,如果最大线程数maximumPoolSize达到最大,那么会导致OOM异常。

使用自定义线程池ThreadPoolExecutor,可以控制最大线程数、阻塞队列长度,避免OOM。

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 ThreadPoolUtil {
/**
* 自定义线程池,初始化
*/
public static ExecutorService initExecutor() {
//参数可以写到配置里面,更加灵活
//核心线程数
int corePoolSize = 20;
//最大线程数
int maximumPoolSize = 100;
//存活时间及单位
long keepAliveTime = 2;
TimeUnit timeUnit = TimeUnit.SECONDS;
//阻塞队列长度
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(200);

//创建线程或线程池时设置线程名称,方便出错时回溯。
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setNameFormat("demo-pool-%d").build();

return new ThreadPoolExecutor(corePoolSize, maximumPoolSize,
keepAliveTime, timeUnit, workQueue, threadFactory);

}
}

自定义线程池 - 乐之者v - 博客园 (cnblogs.com)

Java线程池中三种方式创建 ThreadFactory 设置线程名称 - 甜菜波波 - 博客园 (cnblogs.com)


阻塞队列 BlockingQueue

  • 当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起),直到有数据放入队列。
  • 当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞(挂起),直到队列中有空的位置,线程被自动唤醒。

常见的BlockingQueue

  1. ArrayBlockingQueue

    基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置

BlockingQueue - 不会就问咯 - 博客园 (cnblogs.com)


ThreadLocal

ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题。

Java中的ThreadLocal详解 - 夏末秋涼 - 博客园 (cnblogs.com)


JUC并发编程

wait 和 sleep 方法的区别

wait() sleep()
所属对象 Object类 Thread类
使用范围 同步方法、同步代码块 没有限制
是否会释放锁 不会
使用场景 线程间的交互、通信 暂停执行

实际开发中的多线程操作方式

JDK中的Executors虽然提供了如newFixedThreadPool()newSingleThreadExecutor()newCachedThreadPool()等创建线程池的方法,但都有其局限性,不够灵活。

使用ThreadPoolExecutor有助于大家明确线程池的运行规则,创建符合自己的业务场景需要的线程池,避免资源耗尽的风险。

java线程池ThreadPoolExecutor类使用详解 - DaFanJoy - 博客园 (cnblogs.com)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* corePoolSize: 线程池中的线程数量,它的数量决定了添加的任务是开辟新的线程去执行,还是放到workQueue任务队列中去;
* maximumPoolSize: 指定了线程池中的最大线程数量,这个参数会根据你使用的workQueue任务队列的类型,决定线程池会开辟的最大线程数量;
* keepAliveTime: 当线程池中空闲线程数量超过corePoolSize时,多余的线程会在多长时间内被销毁;
* unit: keepAliveTime的单位
* workQueue: 任务队列,被添加到线程池中,但尚未被执行的任务;它一般分为直接提交队列、有界任务队列、无界任务队列、优先任务队列几种
* threadFactory:线程工厂,用于创建线程,一般用默认即可;
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory)

CountDownLatch

countDownLatch是一个同步工具类,用来协调多个线程之间的同步,或者说起到线程之间的通信(而不是用作互斥的作用)。

CountDownLatch能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。使用一个计数器进行实现。计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减一。当计数器的值为0时,表示所有的线程都已经完成一些任务,然后在CountDownLatch上等待的线程就可以恢复执行接下来的任务。


用法一

一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。

  1. 某一线程在开始运行前等待n个线程执行完毕。将CountDownLatch的计数器初始化为new CountDownLatch(n),每当一个任务线程执行完毕,就将计数器减1:countdownLatch.countDown()
  2. 当计数器的值变为0时,在CountDownLatch上await()的线程就会被唤醒。

用法二

实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。

初始化一个共享的CountDownLatch(1),将其计算器初始化为1,多个线程在开始执行任务前首先countdownlatch.await(),当主线程调用countDown()时,计数器变为0,多个线程同时被唤醒。

CountDownLatch的理解和使用 - Shane_Li - 博客园 (cnblogs.com)