了解相关概念

线程:线程是操作系统哪个进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位
进程:进程是程序的基本执行实体
并发:在同一时刻,有多个指令在单个CPU上交替执行
并行:在同一时刻,有多个指令在多个CPU上同时执行


实现方法

实现方法 优点 缺点
继承Thread类 比较简单,可以直接使用Thread类中的方法 可以扩展性差,不能再继承其他的类
实现Runnable接口 扩展性强,实现该接口的同时还可以继承其他的类 编程相对复杂,不能直接使用Thread类中的方法
实现Callable接口 扩展性强,实现该接口的同时还可以继承其他的类 编程相对复杂,不能直接使用Thread类中的方法

实现方法一:继承Thread类的方式实现

创建一个类,继承Thread类:extends Thread

1
2
3
4
5
6
7
8
9
10
public class MyThread extends Thread{
//重写run方法·
@Override
public void run() {
for (int i = 0; i < 10; i++) {
//获取当前线程名字,打印hello world
System.out.println(getName() + ":hello World");
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
public class demo {
public static void main(String[] args) {
//创建线程对象
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
//给线程命名
thread1.setName("线程1");
thread2.setName("线程2");
//启动线程
thread1.start();
thread2.start();
}
}

控制台输出:
1
2
3
4
5
6
7
线程2:hello World
线程1:hello World
线程2:hello World
线程1:hello World
线程2:hello World
线程1:hello World
线程1:hello World

实现方式二:实现Runnable接口的方式进行实现

创建一个类实现Runnable接口

1
2
3
4
5
6
7
8
9
10
11
12
public class MyRun implements Runnable{
// 重写run方法,run方法的方法体就是线程要执行的代码
@Override
public void run() {
for (int i = 0; i < 5; i++) {
// 获取当前线程的名字
/* Thread thread = Thread.currentThread();
System.out.println(thread.getName()+":HelloWorld");*/
System.out.println(Thread.currentThread().getName()+":HelloWorld");
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class test {
public static void main(String[] args) {
//创建对象
MyRun mr = new MyRun();
//创建线程对象
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
//设置线程的名字
t1.setName("线程1");
t2.setName("线程2");
//启动线程
t1.start();
t2.start();
}
}

控制台输出:
1
2
3
4
5
6
7
8
9
10
线程1:HelloWorld
线程2:HelloWorld
线程1:HelloWorld
线程2:HelloWorld
线程1:HelloWorld
线程2:HelloWorld
线程1:HelloWorld
线程2:HelloWorld
线程1:HelloWorld
线程2:HelloWorld

方式三:实现Callable接口

  1. 创建一个类MyCallable实现Callable接口,重写call方法,表示线程执行的任务
  2. 在启动类中创建MyCallable对象mc
  3. 创建FutureTask对象ft来管理多线程new FutureTask<>(mc)运行的结果,泛型为返回结果类型
  4. 创建Thread线程对象t来执行new Thread(ft);
  5. 开启线程t.start()获取线程结果ft.get();
1
2
3
4
5
6
7
8
9
10
11
//泛型表示返回值类型
public class MyCallable implements Callable <Integer>{
@Override//重写call方法:返回值表示多线程执行的结果
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i <= 100; i++) {
sum += i;
}
return sum;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建MyCallAble对象:表示多线程要执行的任务
MyCallable mc = new MyCallable();
//创建FutureTask对象:来管理多线程运行的结果
FutureTask<Integer> ft = new FutureTask<>(mc);
//创建线程来执行FutureTask对象
Thread t = new Thread(ft);
//启动线程
t.start();
//获取多线程运行的结果
Integer result = ft.get();
System.out.println(result);
}
}

控制台输出:5050


常见成员方法

方法名称 说明
String getName() 返回此线程的名字
void setName(String name) 设置线程的名字(构造方法也可以设置名字)
static Thread currentThread() 获取当前线程的对象
static void sleep(long time) 让线程休眠指定的时间,单位为毫秒
setPriority(int newPriority) 设置线程的优先级,最小1,最大10,默认5
final int getPriority() 获取线程的优先级
final void setDaemon(boolean on) 设置为守护线程
public static void yield() 出让线程/礼让线程
public static void join() 插入线程/插队线程

线程名称和休眠时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MyThread extends Thread{
public MyThread() {
}

public MyThread(String name) {
super(name);
}

@Override
public void run() {
for (int i = 0; i < 4; i++) {
System.out.println(getName()+" is running->"+ LocalTime.now());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

1
2
3
4
5
6
7
8
9
10
11
public class MethodsDemo {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName());//main
MyThread t1 = new MyThread("线程1");
MyThread t2 = new MyThread("线程2");
//t.setName("线程1");
t1.start();
t2.start();
}

}
控制台输出:
1
2
3
4
5
6
7
8
9
main
线程2 is running->16:12:49.590817200
线程1 is running->16:12:49.590817200
线程2 is running->16:12:50.610057300
线程1 is running->16:12:50.610057300
线程1 is running->16:12:51.625712200
线程2 is running->16:12:51.625712200
线程1 is running->16:12:52.636113800
线程2 is running->16:12:52.636113800

细节:
void setName(String name)

  • 如果没有给线程设置名字,线程默认名字为:Thread-X(X序号:从0开始)

static Thread currentThread()

  • 当JVM虚拟机启动之后,会自动启动多条线程,其中有一条线程就叫mian线程
  • 其作用就是调用mian方法,并执行里面的代码

static void sleep(long time)

  • 那条线程执行到这个方法,哪个线程就会在这里停留对应的时间
  • 方法的参数:就表示睡眠的时间,单位毫秒->1000ms = 1s
  • 当时间到了之后,线程会自动的醒来,继续执行下面的其他代码

线程优先级

1
2
3
4
5
6
7
8
public class MyRunable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 5; i++) {//这里可以把i换成100,打印结果更直观,更有代表性
System.out.println(Thread.currentThread().getName()+" 正在进行攻击");
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class MethodsDemo {
public static void main(String[] args) {
MyRunable mr = new MyRunable();
Thread t1 = new Thread(mr,"飞机");
Thread t2 = new Thread(mr,"坦克");
t1.setPriority(10);
t2.setPriority(1);
t1.start();
t2.start();

}

}
控制台输出:
1
2
3
4
5
6
7
8
9
10
飞机 正在进行攻击
坦克 正在进行攻击
飞机 正在进行攻击
坦克 正在进行攻击
飞机 正在进行攻击
坦克 正在进行攻击
飞机 正在进行攻击
坦克 正在进行攻击
飞机 正在进行攻击
坦克 正在进行攻击

细节:

  • 优先级越大只代表抢占到CPU的概率越大
  • 上述演示代码多次运行会发现飞机先运行完概率大一些,但也可能比坦克后运行结束

守护线程

当其他非守护线程执行完毕之后,守护线程会陆陆续续结束
通俗解释:如下面演示代码,当女神线程结束了,那么备胎也就没有存在的必要了
应用场景:但两用户在聊天(线程1)时,正在传输文件(线程2),当线程1结束了,线程2就没必要执行完毕了

1
2
3
4
5
6
7
8
public class Threadboy extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName()+"is running..."+i);
}
}
}

1
2
3
4
5
6
7
8
public class Threadgirl extends Thread{
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(getName()+"is running..."+i);
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
public class MethodsDemo {
public static void main(String[] args) {
Threadgirl t1= new Threadgirl();//女神线程
Threadboy t2= new Threadboy();//备胎线程
t1.setName("女神");//预设执行5次
t2.setName("备胎");//预设执行100次
t2.setDaemon(true);//设置备胎为守护线程
t1.start();
t2.start();
}
}

控制台输出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
备胎is running...0
女神is running...0
备胎is running...1
女神is running...1
备胎is running...2
女神is running...2
备胎is running...3
女神is running...3
备胎is running...4
女神is running...4
备胎is running...5
备胎is running...6
备胎is running...7
备胎is running...8
备胎is running...9
备胎is running...10
备胎is running...11

礼让线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyThread extends Thread{
public MyThread() {
}
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 6; i++) {
System.out.println(getName()+"is running..."+i);
// 表示出让当前CPU的执行权
Thread.yield();
}
}
}
1
2
3
4
5
6
7
8
public class test {
public static void main(String[] args) {
MyThread t1 = new MyThread("飞机");
MyThread t2 = new MyThread("坦克");
t1.start();
t2.start();
}
}
控制台输出:
1
2
3
4
5
6
7
8
9
10
11
12
飞机is running...0
飞机is running...1
坦克is running...0
坦克is running...1
飞机is running...2
坦克is running...2
飞机is running...3
坦克is running...3
飞机is running...4
坦克is running...4
飞机is running...5
坦克is running...5

可以看到,当一个线程礼让后,有可能会再次抢到下次cpu的执行权,如上方前4行的打印输出

插入线程

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyThread extends Thread{
public MyThread() {
}
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+" is running..."+i);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class test {
public static void main(String[] args) throws InterruptedException {
MyThread t = new MyThread("阿杰");
t.start();
//表示把t这个线程插入到当前线程之前
//t:阿杰
//当前线程:main线程
t.join();

for (int i = 0; i < 10; i++) {//当前线程为main线程
System.out.println(Thread.currentThread().getName()+" is running..."+i);
}
}
}
控制台输出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
注释掉t.join时:
main is running...0
阿杰 is running...0
main is running...1
阿杰 is running...1
main is running...2
阿杰 is running...2
main is running...3
阿杰 is running...3
main is running...4
阿杰 is running...4
main is running...5
阿杰 is running...5
main is running...6
阿杰 is running...6
main is running...7
阿杰 is running...7
main is running...8
阿杰 is running...8
main is running...9
阿杰 is running...9

加入t.join时:
阿杰 is running...0
阿杰 is running...1
阿杰 is running...2
阿杰 is running...3
阿杰 is running...4
阿杰 is running...5
阿杰 is running...6
阿杰 is running...7
阿杰 is running...8
阿杰 is running...9
main is running...0
main is running...1
main is running...2
main is running...3
main is running...4
main is running...5
main is running...6
main is running...7
main is running...8
main is running...9

线程的生命周期

生命周期包含:新建,就绪,运行,阻塞,死亡。还有等待和计时等待
新建:创建线程对象
就绪(不停的抢CPU):有执行资格,没有执行权
运行(运行代码):有执行资格,有执行权
阻塞:没有执行资格,没有执行权
死亡:线程死亡
等待:没有执行资格,没有执行权
计时等待:没有执行资格,没有执行权

新建—start()—>就绪
就绪—抢到CPU的执行权—>运行—其他线程抢走CPU的执行权—>就绪
运行—sleep()或者其他阻塞式方法—>阻塞—sleep()方法时间到或其他阻塞方式结束—>就绪
运行—run()执行完毕—>死亡
运行—wait()—>等待—notify()—>就绪
运行—sleep(10)—>计时等待—时间到—>就绪

但是线程的状态只有六个
新建(NEW) ——> 创建线程对象
就绪状态(RUNNABLE) ——> start方法
阻塞状态(BLOCKED) ——> 无法获得锁对象
等待状态(WAITING) ——> wait方法
计时等待(TIMED_WAITING) ——> sleep方法
结束状态(TERMINATED) ——> 全部代码运行完毕

sleep会让线程睡眠,睡眠时间到了后,为何不会立马执行下面的代码?(需要抢到CPU的执行权)


线程安全问题

设计一个案例:一个电影院卖一部电影票,一共100张票,三个售票窗口,设计程序模拟卖票。

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 TicketOffice extends Thread {
//static 表示这个类的所有对象,都共享ticket数据
static int ticket = 0;//0~99

public TicketOffice() {
}

public TicketOffice(String name) {
super(name);
}

@Override
public void run() {
while (true) {
if (ticket < 100) {
try {
Thread.sleep(100);//减慢售出速度,观察打印结果
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
ticket++;
System.out.println(getName() + "出售第" + ticket + "张票");
} else {
break;
}
}
}
}

1
2
3
4
5
6
7
8
9
10
11
public class Main {
public static void main(String[] args) {
TicketOffice t1 = new TicketOffice("售票处1");
TicketOffice t2 = new TicketOffice("售票处2");
TicketOffice t3 = new TicketOffice("售票处3");
t1.start();
t2.start();
t3.start();
}
}

查看控制台输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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
售票处1出售第0张票
售票处3出售第0张票
售票处2出售第0张票
售票处1出售第3张票
售票处3出售第4张票
售票处2出售第5张票
售票处1出售第6张票
售票处3出售第7张票
售票处2出售第8张票
售票处3出售第9张票
售票处1出售第9张票
售票处2出售第11张票
售票处3出售第12张票
售票处1出售第13张票
售票处2出售第14张票
售票处3出售第15张票
售票处1出售第16张票
售票处2出售第17张票
售票处3出售第18张票
售票处1出售第19张票
售票处2出售第20张票
售票处3出售第21张票
售票处1出售第22张票
售票处2出售第23张票
售票处3出售第24张票
售票处1出售第25张票
售票处2出售第26张票
售票处3出售第27张票
售票处1出售第28张票
售票处2出售第29张票
售票处3出售第30张票
售票处1出售第31张票
售票处2出售第32张票
售票处3出售第33张票
售票处1出售第34张票
售票处2出售第35张票
售票处3出售第36张票
售票处1出售第37张票
售票处2出售第38张票
售票处3出售第39张票
售票处1出售第40张票
售票处2出售第41张票
售票处3出售第42张票
售票处1出售第43张票
售票处2出售第44张票
售票处3出售第45张票
售票处1出售第46张票
售票处2出售第47张票
售票处3出售第48张票
售票处1出售第49张票
售票处2出售第50张票
售票处3出售第51张票
售票处1出售第52张票
售票处2出售第53张票
售票处3出售第54张票
售票处1出售第55张票
售票处2出售第56张票
售票处3出售第57张票
售票处1出售第58张票
售票处2出售第59张票
售票处3出售第60张票
售票处1出售第61张票
售票处2出售第62张票
售票处3出售第63张票
售票处1出售第64张票
售票处2出售第65张票
售票处3出售第66张票
售票处1出售第67张票
售票处2出售第68张票
售票处3出售第69张票
售票处1出售第70张票
售票处2出售第71张票
售票处3出售第72张票
售票处1出售第73张票
售票处2出售第74张票
售票处3出售第75张票
售票处1出售第76张票
售票处2出售第77张票
售票处3出售第78张票
售票处1出售第79张票
售票处2出售第80张票
售票处3出售第81张票
售票处1出售第82张票
售票处2出售第83张票
售票处3出售第84张票
售票处1出售第85张票
售票处2出售第86张票
售票处3出售第87张票
售票处1出售第88张票
售票处2出售第89张票
售票处3出售第90张票
售票处1出售第91张票
售票处2出售第92张票
售票处3出售第93张票
售票处1出售第94张票
售票处2出售第95张票
售票处3出售第96张票
售票处1出售第97张票
售票处2出售第98张票
售票处3出售第99张票
售票处1出售第100张票
售票处2出售第101张票

可以发现当多个线程操作同一个数据时会存在如下问题(线程执行时,有随机性)
问题一:相同一张票出现多次
分析:当t1进入睡眠,t2抢夺cpu,之后进入睡眠,t3抢到cpu,之后进入睡眠,现在t1,t2,t3会陆陆续续醒来并抢夺cpu,
如果t1在执行ticket++之后未打印票据就立马被t2线程抢走cpu,之后t2同上被t3抢走,之后三个线程打印的票据就同一个ticket值3

问题二:出现超出了范围的票
分析:当tick=99的时候,t1进来休眠,t2进来休眠,t3进来休眠,之后陆续醒来执行ticket++然后打印票数


同步代码块

针对上述案例的安全问题,需要引用同步代码块,把操作共享数据的代码锁起来
格式:

1
2
3
synchronized (锁){
操作共享数据的代码
}

特点1:锁默认打开,有一个线程进去了,锁自动关闭
特点2: 里面的代码全部执行完毕,线程出来,锁自动打开

上述案例优化

有两处细节:

  1. synchronized(lock)需要写在循环里面
  2. 锁对象必须是唯一的,一般会写当前类的字节码文件对象如:synchronized(TicketOffice.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
    public class TicketOffice extends Thread {
    //static 表示这个类的所有对象,都共享ticket数据
    static int ticket = 0;//0~99

    public TicketOffice() {
    }

    public TicketOffice(String name) {
    super(name);
    }

    //锁对象,一定要是唯一的,使用static 修饰
    static Object lock = new Object();

    @Override
    public void run() {
    while (true) {
    synchronized (lock){
    if (ticket < 2000) {//为了让打印结果更明显,设置为2000张票
    try {
    Thread.sleep(5);//睡眠时间为0.005s
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    }
    System.out.println(getName() + "出售第" + ticket + "张票");
    ticket++;
    } else {
    break;
    }
    }
    }
    }
    }

同步方法

就是把synchronized关键字加到方法上
格式:修饰符 synchronized 返回值类型 方法名(方法参数){...}

特点1:同步方法是锁住方法里面所有代码
特点2:锁对象不能自己指定

  • 非静态:this
  • 静态:当前类的字节码文件对象

书写技巧,按同步代码块来写,选中synchronized里面的方法,在idea里面按Ctrl+Alt+m抽取成一个方法

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 MyRunnable implements Runnable {

int ticket = 0;

@Override
public void run() {
//循环
while (true) {
if (method()) break;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}

//非静态,锁对象为this
private synchronized boolean method() {
if (ticket == 100) {
return true;
}else {
//判断共享数据是否到了末尾,如果没有到末尾
ticket++;
System.out.println(Thread.currentThread().getName() + "出售第" + ticket + "张票");
}
return false;
}
}

1
2
3
4
5
6
7
8
9
10
11
public class Main {
public static void main(String[] args) {
MyRunnable mr = new MyRunnable();
Thread t1 = new Thread(mr,"窗口1");
Thread t2 = new Thread(mr,"窗口2");
Thread t3 = new Thread(mr,"窗口3");
t1.start();
t2.start();
t3.start();
}
}

控制台输出结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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
窗口1出售第1张票
窗口3出售第2张票
窗口2出售第3张票
窗口3出售第4张票
窗口1出售第5张票
窗口2出售第6张票
窗口3出售第7张票
窗口1出售第8张票
窗口2出售第9张票
窗口3出售第10张票
窗口1出售第11张票
窗口2出售第12张票
窗口3出售第13张票
窗口1出售第14张票
窗口2出售第15张票
窗口3出售第16张票
窗口1出售第17张票
窗口2出售第18张票
窗口3出售第19张票
窗口1出售第20张票
窗口2出售第21张票
窗口3出售第22张票
窗口1出售第23张票
窗口2出售第24张票
窗口3出售第25张票
窗口1出售第26张票
窗口2出售第27张票
窗口3出售第28张票
窗口1出售第29张票
窗口2出售第30张票
窗口3出售第31张票
窗口1出售第32张票
窗口2出售第33张票
窗口3出售第34张票
窗口1出售第35张票
窗口2出售第36张票
窗口3出售第37张票
窗口1出售第38张票
窗口2出售第39张票
窗口3出售第40张票
窗口1出售第41张票
窗口2出售第42张票
窗口3出售第43张票
窗口1出售第44张票
窗口2出售第45张票
窗口3出售第46张票
窗口1出售第47张票
窗口2出售第48张票
窗口3出售第49张票
窗口1出售第50张票
窗口2出售第51张票
窗口3出售第52张票
窗口1出售第53张票
窗口2出售第54张票
窗口3出售第55张票
窗口1出售第56张票
窗口2出售第57张票
窗口3出售第58张票
窗口1出售第59张票
窗口2出售第60张票
窗口3出售第61张票
窗口1出售第62张票
窗口2出售第63张票
窗口3出售第64张票
窗口1出售第65张票
窗口2出售第66张票
窗口3出售第67张票
窗口1出售第68张票
窗口2出售第69张票
窗口3出售第70张票
窗口1出售第71张票
窗口2出售第72张票
窗口3出售第73张票
窗口1出售第74张票
窗口2出售第75张票
窗口3出售第76张票
窗口1出售第77张票
窗口2出售第78张票
窗口3出售第79张票
窗口1出售第80张票
窗口2出售第81张票
窗口3出售第82张票
窗口1出售第83张票
窗口2出售第84张票
窗口3出售第85张票
窗口1出售第86张票
窗口2出售第87张票
窗口3出售第88张票
窗口1出售第89张票
窗口2出售第90张票
窗口3出售第91张票
窗口1出售第92张票
窗口2出售第93张票
窗口3出售第94张票
窗口1出售第95张票
窗口2出售第96张票
窗口3出售第97张票
窗口1出售第98张票
窗口2出售第99张票
窗口3出售第100张票


Lock锁

Lock锁可以手动上锁和手动释放锁
Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作

  • void lock():获得锁
  • void unlock():释放锁

Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化
ReentrantLock的构造方法=》ReentrantLock():创建一个ReentrantLock的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class MyThread extends Thread{

static int ticket = 0;

static Lock lock = new ReentrantLock();

@Override
public void run() {
while (true) {
try {
Thread.sleep(100);
System.out.println("线程" + getName() + "进入");
lock.lock();//加锁
if (ticket < 20) {
ticket++;
System.out.println(getName() + "正在售出第" + ticket + "张票");
} else {
break;
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
System.out.println(getName()+"释放锁");
lock.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
线程窗口1进入
线程窗口2进入
线程窗口3进入
窗口1正在售出第1张票
窗口1释放锁
窗口2正在售出第2张票
窗口2释放锁
窗口3正在售出第3张票
窗口3释放锁
线程窗口3进入
窗口3正在售出第4张票
窗口3释放锁
线程窗口2进入
窗口2正在售出第5张票
窗口2释放锁
线程窗口1进入
窗口1正在售出第6张票
窗口1释放锁
线程窗口3进入
窗口3正在售出第7张票
窗口3释放锁
线程窗口2进入
窗口2正在售出第8张票
窗口2释放锁
线程窗口1进入
窗口1正在售出第9张票
窗口1释放锁
线程窗口3进入
窗口3正在售出第10张票
窗口3释放锁
线程窗口1进入
线程窗口2进入
窗口1正在售出第11张票
窗口1释放锁
窗口2正在售出第12张票
窗口2释放锁
线程窗口3进入
窗口3正在售出第13张票
窗口3释放锁
线程窗口1进入
线程窗口2进入
窗口1正在售出第14张票
窗口1释放锁
窗口2正在售出第15张票
窗口2释放锁
线程窗口3进入
窗口3正在售出第16张票
窗口3释放锁
线程窗口2进入
线程窗口1进入
窗口2正在售出第17张票
窗口2释放锁
窗口1正在售出第18张票
窗口1释放锁
线程窗口3进入
窗口3正在售出第19张票
窗口3释放锁
线程窗口2进入
线程窗口1进入
窗口2正在售出第20张票
窗口2释放锁
窗口1释放锁
线程窗口3进入
窗口3释放锁
线程窗口2进入
窗口2释放锁

lock.lock()之后,以防循环跳出时没有释放锁.(在idea中选中代码,按住Ctrl+Alt+t即可快捷加tryCatch了)
可以把加锁后,下面的业务代码,用tryCatch包裹,之后finally{lock.unlock();//解锁}

注意:睡眠应该放在锁外面,才不会是一个进程把票全卖完


死锁

避免锁嵌套的情况


生产者和消费者(等待唤醒机制)

常见方法

方法名称 说明
void wait() 当前线程等待,直到被其他线程唤醒
void notify() 随机唤醒单个线程
void notifyAll() 唤醒所有线程

机制说明

用厨师和顾客的例子来解释
消费者:

  • 1.判断桌子上是否有食物
  • 2.如果没有则等待
  • 3.如果有则开吃
  • 4.吃完之后,唤醒厨师

生产者:

  • 1.判断桌上有没有食物
  • 2.有:等待唤醒
  • 3.没有:制作食物
  • 4.把食物放在桌上
  • 5.唤醒消费者

案例演示:
期望结果:厨师做一份食物,放桌子上,顾客就吃一份。桌子上有,厨师等待顾客,没有则顾客等待厨师,顾客最多吃10份

1
2
3
4
5
6
7
8
9
10
11
12
//中间变量:桌面
public class Desk {

// 桌子上是否有食物 0: 没有 1: 有
public static int foodFlag =0;

//总个数
public static int count = 10;

//锁类
public static Object lock = new Object();
}

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 Cook extends Thread{
@Override
public void run() {
/**
* 1.循环
* 2.同步代码块
* 3.判断共享数据是否到了末尾(到了末尾)
* 4.判断共享数据是否到了末尾(没有到末尾,执行核心逻辑)
*/

while(true){//1.循环
synchronized (Desk.lock){
if(Desk.count==0){//消费结束
break;
}else{
if(Desk.foodFlag == 1){//桌面有食物,等待
try {
Desk.lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}else{//桌面没有食物
System.out.println("厨师做了一份食物");
Desk.foodFlag = 1;//修改桌面状态
Desk.lock.notifyAll();//唤醒所有线程
}
}
}
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//消费者:顾客
//消费者:顾客
public class Customer extends Thread {
@Override
public void run() {
/**
* 1.循环
* 2.同步代码块
* 3.判断共享数据是否到了末尾(到了末尾)
* 4.判断共享数据是否到了末尾(没有到末尾,执行核心逻辑)
*/

while (true) {//1.循环
synchronized (Desk.lock) {
if (Desk.count == 0) {
break;
}else {
if (Desk.foodFlag == 0) {//桌面没有食物
try {
Desk.lock.wait();//等待
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} else {//桌面有食物
Desk.count--;//执行核心逻辑,把总数减一
System.out.println("顾客消费了一份,还能吃" + Desk.count + "份");
Desk.lock.notifyAll();//唤醒所有线程
Desk.foodFlag = 0;//修改桌面状态
}
}
}
}
}
}

1
2
3
4
5
6
7
8
public class Main {
public static void main(String[] args) {
Customer cs = new Customer();
Cook ck = new Cook();
cs.start();
ck.start();
}
}

查看控制台输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
厨师做了一份食物
顾客消费了一份,还能吃9
厨师做了一份食物
顾客消费了一份,还能吃8
厨师做了一份食物
顾客消费了一份,还能吃7
厨师做了一份食物
顾客消费了一份,还能吃6
厨师做了一份食物
顾客消费了一份,还能吃5
厨师做了一份食物
顾客消费了一份,还能吃4
厨师做了一份食物
顾客消费了一份,还能吃3
厨师做了一份食物
顾客消费了一份,还能吃2
厨师做了一份食物
顾客消费了一份,还能吃1
厨师做了一份食物
顾客消费了一份,还能吃0

等待唤醒机制(阻塞队列方式实现)

把桌子比作一个流水线,厨师做好了菜便放到流水线上:put,流水线上的存放数量可自定义。
顾客按顺序从流水线上取餐:take
put数据时:放不进去时,会等着,也叫阻塞
take数据时:取出第一个数据,取不到会等着,也叫做阻塞

接口:iterable,Collection,Quenue,BlockingQueue
实现类: ArrayBlockingQueue(底层是数组,有界)。LinkedBlockingQueue(底层是链表,无界,最大为int的最大值)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Cook extends Thread{
private ArrayBlockingQueue<String> arrayBlockingQueue;

public Cook(ArrayBlockingQueue<String> arrayBlockingQueue) {
this.arrayBlockingQueue = arrayBlockingQueue;
}

@Override
public void run() {
while (true) {
try {
arrayBlockingQueue.put("面条");
System.out.println("厨师做了面条");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Customer extends Thread{
private ArrayBlockingQueue<String> arrayBlockingQueue;

public Customer(ArrayBlockingQueue<String> arrayBlockingQueue) {
this.arrayBlockingQueue = arrayBlockingQueue;
}

@Override
public void run() {
while (true) {
try {
String take = arrayBlockingQueue.take();
System.out.println("顾客拿"+take);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
1
2
3
4
5
6
7
8
9
10
public class Main {
public static void main(String[] args) {
//线程工厂为1
ArrayBlockingQueue<String> arrayBlockingQueue = new ArrayBlockingQueue<>(1);
Cook cook = new Cook(arrayBlockingQueue);
Customer consumer = new Customer(arrayBlockingQueue);
cook.start();
consumer.start();
}
}

上述用阻塞唤醒机制实现中,锁仅仅在take和put方法里面,所以打印结果是错的,但逻辑是对的


Demo案例题

题一:抢100块红包,分成了3个包,5个人去抢,红包为共享数据,5个人是5条线程
打印结果:XXX抢到了XXX元 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
public class RedPacket extends Thread {
public RedPacket() {
}
public RedPacket(String name) {
super(name);
}
//总金额
static double money = 100;
//3份红包
static int count = 3;
//最小金额
static final double MIN = 0.01;

@Override
public void run() {
synchronized (RedPacket.class) {
if (count == 0) {//没有红包了
System.out.println(getName() + "没有抢到红包");
} else {
double prize = 0;
if (count == 1) {//最后一份红包
prize = money;
} else {//前两份为随机
Random r = new Random();
//100块,第一个人最多抢99.89块
//bounds为随机的范围
double bounds = money - MIN * (count - 1);
prize = r.nextDouble(bounds);
if (prize < MIN) {//如果随机数小于最小金额,则设置为最小金额
prize = MIN;
}
}
money = money - prize;
count--;//剩余的红包数量
System.out.println(getName() + "抢到了" + prize + "元");
}
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Main {
public static void main(String[] args) {
RedPacket t1 = new RedPacket("张三");
RedPacket t2 = new RedPacket("李四");
RedPacket t3 = new RedPacket("王五");
RedPacket t4 = new RedPacket("赵六");
RedPacket t5 = new RedPacket("刘七");

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

}
}

控制台输出

1
2
3
4
5
张三抢到了43.009498320737066
刘七抢到了42.53333463574706
赵六抢到了14.457167043515874
王五没有抢到红包
李四没有抢到红包

优化思路,将最小金额设置为0.01,每个人抢的红包是0.01的整数倍,范围1~9999

题二:抽奖箱抽奖,抽奖池中的奖项为{10,5,20,50,100,200,500,800,2,80,300,700}
创建两个抽奖箱(线程)设置线程名称分别为“抽奖箱1”,“抽奖箱2”
随机从抽奖池中获取奖项元素并打印在控制台上,格式如下:
每次抽出一个奖项就打印一个(随机)
抽奖箱1又产生了一个10元大奖
抽奖箱1又产生了一个100元大奖
抽奖箱1又产生了一个200元大奖
抽奖箱1又产生了一个800元大奖
抽奖箱2又产生了一个700元大奖

查看思路
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
public class MyThread extends Thread{

ArrayList<Integer> arrayList;

public MyThread(ArrayList<Integer> arrayList) {
this.arrayList = arrayList;
}

@Override
public void run() {
while (true){
synchronized (MyThread.class) {
if (arrayList.size()!= 0) {
//打乱集合
Collections.shuffle(arrayList);
//取出第一个元素作为奖项
int pize = arrayList.remove(0);
System.out.println(getName() + "取出了" + pize+"元大奖");
}else {
break;
}
}
try {//这里只是为了演示效果,不让一个线程全打印完,让线程睡眠10毫秒
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Main {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
Collections.addAll(list,10,5,20,50,100,200,500,800,2,80,300,700);

MyThread t1 = new MyThread(list);
MyThread t2 = new MyThread(list);
t1.setName("抽奖箱1");
t2.setName("抽奖箱2");
t1.start();
t2.start();
}
}

控制台输出

1
2
3
4
5
6
7
8
9
10
11
12
抽奖箱1取出了700元大奖
抽奖箱2取出了20元大奖
抽奖箱1取出了300元大奖
抽奖箱2取出了50元大奖
抽奖箱1取出了80元大奖
抽奖箱2取出了800元大奖
抽奖箱1取出了10元大奖
抽奖箱2取出了500元大奖
抽奖箱1取出了200元大奖
抽奖箱2取出了5元大奖
抽奖箱2取出了100元大奖
抽奖箱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
public class MyThread extends Thread{

ArrayList<Integer> arrayList;

public MyThread(ArrayList<Integer> arrayList) {
this.arrayList = arrayList;
}

@Override
public void run() {
ArrayList<Integer> list = new ArrayList<>();
while (true){
synchronized (MyThread.class) {
if (arrayList.size()!= 0) {
//打乱集合
Collections.shuffle(arrayList);
//取出第一个元素作为奖项
int pize = arrayList.remove(0);
// System.out.println(getName() + "取出了" + pize+"元大奖");
list.add(pize);
}else {
System.out.println("抽奖结束:"+getName()+"抽中:"+list);
break;
}
}
try {//这里只是为了演示效果,不让一个线程全打印完,让线程睡眠10毫秒
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}

控制台输出

1
2
抽奖结束:抽奖箱1抽中:[300, 100, 2, 20, 50, 500]
抽奖结束:抽奖箱2抽中:[10, 700, 800, 80, 5, 200]

为什么会有这样的结果呢?java中,堆内存是唯一的,栈内存不是唯一的,栈内存跟线程有关系
上述中,当main线程中t1和t2执行start()时,会开启两个线程和栈内存,分别执行run方法,所以上述的两个线程都有属于自己的list集合

题四:在题三的基础上,要求打印抽奖盒中最大的奖项

查看思路
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 MyCallable implements Callable<Integer> {
ArrayList<Integer> arrayList;

public MyCallable(ArrayList<Integer> arrayList) {
this.arrayList = arrayList;
}

@Override
public Integer call() throws Exception {
ArrayList<Integer> list = new ArrayList<>();
while (true){
synchronized (MyCallable.class) {
if (arrayList.size()!= 0) {
//打乱集合
Collections.shuffle(arrayList);
//取出第一个元素作为奖项
int pize = arrayList.remove(0);
//奖项添加到中奖列表中
list.add(pize);
}else {
System.out.println("抽奖结束:"+Thread.currentThread().getName()+"抽中:"+list);
break;
}
}
Thread.sleep(10);
}
if(list.isEmpty()){
return 0;
}else {
return Collections.max(list);
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建奖池
ArrayList<Integer> list = new ArrayList<>();
Collections.addAll(list,10,5,20,50,100,200,500,800,2,80,300,700);
//创建多线程要运行的参数对象
MyCallable mc = new MyCallable(list);
//存储两个线程的结果
FutureTask<Integer> ft1 = new FutureTask<>(mc);
FutureTask<Integer> ft2 = new FutureTask<>(mc);
//创建线程对象
Thread t1 = new Thread(ft1,"抽奖盒1");
Thread t2 = new Thread(ft2,"抽奖盒2");
//开启线程
t1.start();
t2.start();
//获取线程结果
Integer i = ft1.get();
Integer j = ft2.get();
System.out.println("第一个抽奖箱中最高奖项为:"+i);
System.out.println("第二个抽奖箱中最高奖项为:"+j);
}
}

控制台输出结果

1
2
3
4
抽奖结束:抽奖盒1抽中:[100, 300, 2, 500, 20, 50]
抽奖结束:抽奖盒2抽中:[5, 80, 700, 200, 10, 800]
第一个抽奖箱中最高奖项为:500
第二个抽奖箱中最高奖项为:800


线程池

核心原理:

  1. 创建一个池子,池子里面是空的
  2. 提交任务时。池子会创建新的线程对象,任务执行完毕后,线程归还给池子。下回再次提交任务时,不需要创建新的线程,直接复用已有的线程即可
  3. 但是如果提交任务时,池子中没有空闲线程,也无法创建新的线程,任务就会排队等待

线程池代码实现
Executors:线程池的工具类通过调用方法返回不同类型的线程池对象

方法名称 说明
public static ExecutorService newCachedThreadPoll() 创建一个没有上限的线程池
public static ExecutorService newFixedThreadPool() 创建有上限的线程池
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Main {
public static void main(String[] args) {
//获取线程池对象
//ExecutorService pool = Executors.newCachedThreadPool();
ExecutorService pool = Executors.newFixedThreadPool(2);
//提交任务
pool.submit(new MyRunable());
pool.submit(new MyRunable());
pool.submit(new MyRunable());

//关闭线程池,一般都不会关闭,项目中一般是24小时运行
//pool.shutdown();
}
}
1
2
3
4
5
6
7
8
public class MyRunable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"------");
}
}
}

自定义线程池

以餐馆为例
核心元素一:正式员工数量 ——>核心线程数量(不能小于0)
核心元素二:餐厅最大员工数——> 线程池中最大线程的数量(最大数量>=核心线程数量)
核心元素三:临时员工空闲多长时间被辞退(值)——> 空闲时间(值)(不能小于0)
核心元素四:临时员工空闲多长时间被辞退 (单位)——> 空闲时间(单位)(用TimeUnit指定)
核心元素五:排队的客户 ——> 阻塞队列(不能为null)
核心元素六:从哪里招人 ——>创建线程的方式(不能为null)
核心元素七:当排队人数过多,超出顾客请下次再来(拒绝服务)——> 要执行的任务过多时的解决方案(不能为null)

流程总结

  1. 创建一个空的池子
  2. 有任务提交时,线程池会创建线程去执行任务,执行完毕归还线程

不断的提交任务,会有以下三个临界点:

  • 当核心线程满时,再提交任务就会排队
  • 当核心线程满,队伍满时,会创建临时线程
  • 当核心线程满,队伍满,临时线程满时,会触发任务拒绝策略

自定义线程池(任务拒绝策略)

任务拒绝策略 说明
ThreadPoolExecutor.AbortPolicy 默认策略:丢弃任务并抛出RejectedExecutionException异常
ThreadPoolExecutor.DiscardPolicy 丢弃任务,但是不抛出异常 这是不推荐的做法
ThreadPoolExecutor.DiscardoldestPolicy 抛弃队列中等待最久的任务 然后把当前任务加入队列中
ThreadPoolExecutor.CallerRunsPolicy 调用任务的run()方法绕过线程池直接执行

Demo演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Main {
public static void main(String[] args) {
ThreadPoolExecutor pool = new ThreadPoolExecutor(
3, //核心线程数量,能小于0
6,//最大线程数,不能小于0,最大数鼠 >= 核心线程数駄
60, //空闲线程最大存活时间
TimeUnit.SECONDS, //时间单位
new ArrayBlockingQueue<>( 3), //任务队列
Executors.defaultThreadFactory(),//创建线程工厂
new ThreadPoolExecutor.AbortPolicy()//任务的拒绝策略
);

pool.submit(new MyRunable());
}
}