多线程
java是通过java.lang.Thread类的对象来代表线程的
创建
多线程创建方式1:继承Thread类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// 1、定义一个类继承Thread类,成为线程类
class MyThread extends Thread{
// 2、重写run方法,声明线程要做的事情
@Override
public void run(){
for(int i = 0;i<4;i++){
System.out.println("子线程输出:"+i);
}
}
}
// main方法本身就是由一条主线程负责执行的
public static void main(String[] args){
// 3、创建线程对象,代表具体的线程
Thread t = new MyThread();
// 4、启动线程:会自动执行线程的run方法
// t.run(); // 如何直接调用run方法,而不使用start,cpu不会注册新线程执行,此时还是单线程在执行
t.start();
for(int i = 0;i<4;i++){
System.out.println("主线程输出:"+i);
}
}
|
方式1优点:编码简单
缺点:线程类已经继承Thread,无法继承其他类,不利于功能的扩展
多线程创建方式2:实现Runnable接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
// 1、定义一个类实现Runnable接口
class MyRunnable implements Runnable{
// 2、重写run方法,声明线程要做的事情
@Override
public void run(){
for(int i = 0;i<4;i++){
System.out.println("子线程输出:"+i);
}
}
}
// main方法本身就是由一条主线程负责执行的
public static void main(String[] args){
// 3、创建任务类的一个对象
Runnable target = new MyRunnable();
// 4、把任务对象交给线程对象
// public Thread(Runnable task) 封装Runnable对象成为线程对象
Thread t = new Thread(target);
// 5、启动线程
t.start();
for(int i = 0;i<4;i++){
System.out.println("主线程输出:"+i);
}
}
|
优点:
任务类只是实现了接口,可以继续继承其他类、实现其他接口,扩展性强
缺点
需要多一个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
|
// 方式2匿名内部类实现
Runnable target = new Runnable(){
@Override
public void run(){
for(int i = 0;i<4;i++){
System.out.println("子线程1输出:"+i);
}
}
}
Thread t = new Thread(target);
t.start();
// 进一步优化
new Thread(new Runnable(){
@Override
public void run(){
for(int i = 0;i<4;i++){
System.out.println("子线程2输出:"+i);
}
}
}).start();
// 进一步简写
new Thread(() -> {
for(int i = 0;i<4;i++){
System.out.println("子线程3输出:"+i);
}
}).start();
|
多线程创建方式3:实现Callable接口
前两种实现方式,无法解决run方法返回结果的问题(即:线程执行完毕返回数据)
JDK5提供了Callable接口和FutureTask类可以解决这个问题
步骤
- 创建任务对象
- 定义一个类实现Callable接口,重写 call方法,封装要做的事情和要返回的数据
- 把Callable类型的对象封装成FutureTask(线程任务对象)
- 把线程任务对象交给Thread对象
- 调用Thread对象的start方法启动线程
- 线程执行完毕后,通过FutureTask对象的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
|
// 1、定义一个类实现Callable接口
class MyCallable implements Callable<String>{
private int n;
public MyCallable(int n){
this.n = n;
}
// 2、重写call方法,声明任务和返回的结果
@Override
public String call() throws Exception{
int sum = 0;
for(int i=0;i<=n;i++){
sum+=1;
}
return "子线程求和(1-"+n+")的结果是:"+sum;
}
}
// 3、创建Callable对象
Callable<String> call = new MyCallable(100);
// 4、把Callable对象,封装成FutureTask对象
// 未来任务对象有两个作用:
// 1> 它是一个Runnable对象
// 2> 它可以获取线程执行后的结果
FutureTask<String> task = new FutureTask<>(call);
// 5、把未来任务对象交给线程对象
Thread t = new Thread(task);
// 6、启动线程
t.start();
Callable<String> call2 = new MyCallable(200);
FutureTask<String> task2 = new FutureTask<>(call2);
Thread t2 = new Thread(task2);
t2.start();
// 获取第一线程的结果
try{
// 如果第一个线程没有执行完毕,会在这里等待第一个线程执行完毕后,再获取结果
String rs1 = task.get();
System.out.println(rs1);
}catch(Excetpion e){
e.printStackTrace();
}
// 获取第二线程的结果
try{
String rs2 = task2.get();
System.out.println(rs2);
}catch(Excetpion e){
e.printStackTrace();
}
|
优点:
线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;
可以在线程执行完毕后区获取线程执行的结果
缺点
编码复杂一点
线程常用方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
// 1、定义一个类继承Thread类,成为线程类
class MyThread extends Thread{
public MyThread(String name){
super(name);
}
// 2、重写run方法,声明线程要做的事情
@Override
public void run(){
for(int i = 0;i<4;i++){
System.out.println(Thread.currentThread().getName()+"子线程输出:"+i);
}
}
}
// main方法本身就是由一条主线程负责执行的
public static void main(String[] args){
// 3、创建线程对象,代表具体的线程
Thread t1 = new MyThread("1号线程"); // 通过有参构造器为线程命名
// Thread t1 = new MyThread();
// public void setName(String name) 为线程起名字
// t1.setName("1号线程");
// 4、启动线程:会自动执行线程的run方法
// t.run(); // 如何直接调用run方法,而不使用start,cpu不会注册新线程执行,此时还是单线程在执行
t1.start();
System.out.println(t1.getName());// 默认Thread-0
Thread t2 = new MyThread();
t2.setName("2号线程");
t2.start();
System.out.println(t2.getName());// 默认Thread-1
// 这个代码是哪个线程在执行,就会得到哪个线程对象
Thread m = Thread.currentThread();
System.out.println(m.getName()); // main
for(int i = 0;i<4;i++){
System.out.println(m.getName()+"线程输出:"+i);
}
}
|
线程休眠
1
2
3
4
5
|
for (int i=0;i<=5;i++){
System.out.println("输出:"+i);
// 作用:让当前线程执行的慢一点
Thread.sleep(1000); // 休眠1s后再执行
}
|
join线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
// 1、定义一个类继承Thread类,成为线程类
class MyThread extends Thread{
// 2、重写run方法,声明线程要做的事情
@Override
public void run(){
for(int i = 0;i<4;i++){
System.out.println("子线程输出:"+i);
}
}
}
// main方法本身就是由一条主线程负责执行的
public static void main(String[] args){
// 3、创建线程对象,代表具体的线程
Thread t = new MyThread();
// 4、启动线程:会自动执行线程的run方法
// t.run(); // 如何直接调用run方法,而不使用start,cpu不会注册新线程执行,此时还是单线程在执行
t.start();
for(int i = 0;i<4;i++){
System.out.println("主线程输出:"+i);
if(i == 2){
t.join(); // 让t插队先执行完毕!在继续执行主线程后面的程序
}
}
}
|
线程安全
是什么
多个线程,同时操作同一个共享资源时,可能会出现业务安全问题
出现原因
- 存在多个线程在同时执行
- 同时访问一个共享资源
- 存在修改该共享资源
模拟线程安全问题
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
|
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Account {
private String cardId;
private double money;
// 取钱
public void drawMoney(double money) {
String name = Thread.currentThread().getName();
if(this.money >= money) {
System.out.println(name + "取钱成功,吐出" + money);
// 更新余额
this.money -= money;
System.out.println(name+"取钱成功后,账户余额:" + this.money);
}else{
System.out.println(name + "来取钱:余额不足");
}
}
}
public class DrawThread extends Thread {
private Account acc;
public DrawThread(String name,Account acc) {
super(name);
this.acc = acc;
}
@Override
public void run() {
acc.drawMoney(10000);
}
}
// 1、创建一个账户对象,两个人共享
Account acc = new Account("ICBC-100",10000);
// 2、创建两个线程代表小明小红,同时去同一个账户取钱
new DrawThread("小明",acc).start();
new DrawThread("小红",acc).start();
|
线程同步
解决线程安全的方案
思想
让多个线程实现先后依次访问共享资源,这样就解决了安全问题
方案
原理:每次只允许一个线程加锁,加锁后才能进行访问,访问完毕后自动解锁,其他线程才能再加锁进来
同步代码块
作用:把访问共享资源的核心代码给上锁,以此保证线程安全
1
2
3
|
synchronized(同步锁){
访问共享资源的核心代码
}
|
原理
每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行
注意事项
- 对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug
取钱案例修复
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Account {
private String cardId;
private double money;
// 取钱
public void drawMoney(double money) {
String name = Thread.currentThread().getName();
synchronized (this) { // 使用共享资源作为锁对象,对于实例方法建议使用this作为锁对象
if(this.money >= money) {
System.out.println(name + "取钱成功,吐出" + money);
// 更新余额
this.money -= money;
System.out.println(name+"取钱成功后,账户余额:" + this.money);
}else{
System.out.println(name + "来取钱:余额不足");
}
}
}
}
|
锁对象使用规范
- 建议使用共享资源作为锁对象,对于实例方法建议使用
this
作为锁对象
- 对于静态方法建议使用
字节码(类名.class)
对象作为锁对象
同步方法
作用:
把访问共享资源的核心方法给上锁,以此保证线程安全
格式
1
2
3
|
修饰符 synchronized 返回值类型 方法名称(形参列表){
操作共享资源代码
}
|
原理
每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行
底层原理
隐式锁对象,只是锁的范围是整个方法代码
取钱案例修复
1
2
3
4
5
6
7
8
9
10
11
|
public synchronized void drawMoney(double money) {
String name = Thread.currentThread().getName();
if(this.money >= money) {
System.out.println(name + "取钱成功,吐出" + money);
// 更新余额
this.money -= money;
System.out.println(name+"取钱成功后,账户余额:" + this.money);
}else{
System.out.println(name + "来取钱:余额不足");
}
}
|
只需要给drawMoney
加锁synchronized
就可以了,推荐使用同步代码块
,因为同步方法锁的范围更广
锁方法使用规范
- 如果方法是实例方法:同步方法默认使用
this
作为锁对象
- 如果方法是静态方法:同步方法默认使用
类名.class
作为锁对象
Lock锁
Lock锁是JKD5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大
lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock
来构建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
|
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Account {
private String cardId;
private double money;
private Lock lock = new ReentrantLock();
public Account(String cardId, double money) {
this.cardId = cardId;
this.money = money;
}
// 取钱
public void drawMoney(double money) {
String name = Thread.currentThread().getName();
try {
lock.lock(); // 加锁
if (this.money >= money) {
System.out.println(name + "取钱成功,吐出" + money);
// 更新余额
this.money -= money;
System.out.println(name + "取钱成功后,账户余额:" + this.money);
} else {
System.out.println(name + "来取钱:余额不足");
}
} finally {
lock.unlock(); // 解锁
}
}
}
|
线程通信
当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以相互协调,并避免无效的资源争夺
常见模型
生产者与消费者模型
生产者线程负责生产数据
消费者线程负责消费生产者生产的数据
注意:
生产者生产完数据应该等待自己,通知消费者消费
消费者消费完数据也应该等待自己,再通知生产者生产
案例
notify
,notifyAll
,wait
属于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
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
|
// 桌子
@Data
public class Desk {
private String data; // 包子数据
// 消费包子
public synchronized void get() throws Exception {
String name = Thread.currentThread().getName();
if(data==null) {
// 没有包子,暂停自己,唤醒别人
// void notify() // 唤醒正在等待的单个线程
// void notifyAll() // 唤醒正在等待的所有线程
this.notifyAll();
// void wait() // 让当前线程等待并释放所占锁,直到另一个线程调用notify()方法或notifyAll()方法
this.wait();
}else{
// 有包子,开始消费
System.out.println(name + "吃了" + data);
data = null;
this.notifyAll();
this.wait();
}
}
public synchronized void put() throws Exception {
String name = Thread.currentThread().getName();
if(data==null) {
// 没有包子,做包子
data = name + "做的包子";
System.out.println(name + "生产了一个包子");
this.notifyAll();
this.wait();
}else{
// 有包子
this.notifyAll();
this.wait();
}
}
}
// 生产者
public class MakeThread extends Thread{
private Desk desk;
public MakeThread(Desk desk, String name) {
super(name);
this.desk = desk;
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000);
desk.put();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
// 消费者
public class ConsumerThread extends Thread {
private Desk desk;
public ConsumerThread(Desk desk, String name) {
super(name);
this.desk = desk;
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000);
desk.get();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
//测试
public static void main(String[] args) {
// 1、竞争一个桌子
Desk desk = new Desk();
// 2、创建3个生产者线程
new MakeThread(desk,"生产1").start();
new MakeThread(desk,"生产2").start();
new MakeThread(desk,"生产3").start();
// 3、创建2个消费者线程
new ConsumerThread(desk,"消费1").start();
new ConsumerThread(desk,"消费2").start();
}
|
线程池
就是一个可以复用线程的技术
不使用线程池
用户每发起一个请求,后台就需要创建一个新线程来处理,下次新任务来了肯定又要创建新线程处理的,而创建新线程的开销是很大的,并且请求过多时,肯定会产生大量的线程出来
,这样会严重影响系统的性能
线程池工作原理
线程池包含了一个任务队列(任务队列WorkQueue)和线程容器(工作线程WorkThread),线程容器中存储了固定线程,当队列中来了一个任务,就会从线程容器里获取一个线程来处理任务,当线程容器里的线程都被使用,这时队列里的任务就会等待,直到线程容器中有空闲的线程,才会继续处理任务队列里的任务
创建线程池
JDK5.0开始提供了代表线程池的接口:ExcecutorService
获取线程池对象方式
- 使用
ExcecutorService
的实现类ThreadPoolExcecutor
自创建一个线程池对象
- 使用
Excutors
(线程池工具类)调用方法返回不同特点的线程池对象
ThreadPoolExcecutor构造器讲解
1
|
public ThreadPoolExcecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExcecutionHandler handler)
|
- 参数一(corePoolSize):指定线程池的核心线程数量 正式工:3人
- 参数二(maximumPoolSize):指定线程池的最大线程数量 最大员工数:5人(临时工:2人)
- 参数三(keepAliveTime):指定临时线程的存活时间 临时工空闲多久被开除
- 参数四(unit):指定临时线程存活的时间单位(秒、分、时、天)
- 参数五(workQueue):指定线程池的任务队列 客人排队的地方
- 参数六(threadFactory):指定线程池的线程工厂 负责招聘员工的(hr)
- 参数七(handler):指定线程池的任务拒绝策略(线程都在忙,任务队列也满了的时候,新任务来了该怎么处理) 忙不过来咋办?
- ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExccutionException异常(默认策略)
- ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常(不推荐)
- ThreadPoolExecutor.DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中
- ThreadPoolExecutor.CallerRunsPolicy:由主线程负责调用任务的run()方法从而绕过线程池直接执行
临时线程什么时候创建
新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程
什么时候会开始拒绝新任务
核心线程和临时线程都在忙,任务队列也满了,新的任务过来时才会开始拒绝任务
创建方式1
使用ExcecutorService
的实现类ThreadPoolExcecutor
自创建一个线程池对象
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 MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 3; i++) {
System.out.println(Thread.currentThread().getName()+"输出:"+i);
System.out.println(Thread.currentThread().getName()+"线程进入休眠!~");
try {
Thread.sleep(Integer.MAX_VALUE);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
// 1、创建线程池
ExecutorService pool = new ThreadPoolExecutor(3,5,1, TimeUnit.MINUTES,new ArrayBlockingQueue<>(3),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
// 2、 处理任务
Runnable target = new MyRunnable();
// void execute(Runnable command)
pool.execute(target); // 自动创建线程,并处理此任务
pool.execute(target); // 自动创建线程,并处理此任务
pool.execute(target); // 自动创建线程,并处理此任务
pool.execute(target); // 复用线程,线程设置的是3个
pool.execute(target); // 复用线程,线程设置的是3个
pool.execute(target); // 复用线程,线程设置的是3个
pool.execute(target); // 到了临时线程的时机了
pool.execute(target); // 到了临时线程的时机了
pool.execute(target); // 到了任务拒绝策略了!
// 3、线程池没有死亡
// pool.shutdownNow(); // 立即关闭线程,不管任务是否完成!返回没有执行完的任务
// pool.shutdown();// 等待全部任务执行完毕后,再关闭
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
public class MyCallable implements Callable<String> {
private int n;
MyCallable(int n) {
this.n = n;
}
@Override
public String call() throws Exception {
int sum = 0;
for (int i = 1; i <= n; i++) {
sum += i;
}
return Thread.currentThread().getName() + "求和1~"+n+"的结果是:" + sum;
}
}
public static void main(String[] args) {
// 1、创建线程池
ExecutorService pool = new ThreadPoolExecutor(3,5,1, TimeUnit.MINUTES,new ArrayBlockingQueue<>(3),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
// 2、 处理任务
Future<String> f1 = pool.submit(new MyCallable(100));
Future<String> f2 = pool.submit(new MyCallable(200));
Future<String> f3 = pool.submit(new MyCallable(300));
try {
String s = f1.get();
System.out.println(s);
} catch (Exception e) {
e.printStackTrace();
}
try {
String s = f2.get();
System.out.println(s);
} catch (Exception e) {
e.printStackTrace();
}
try {
String s = f3.get();
System.out.println(s);
} catch (Exception e) {
e.printStackTrace();
}
}
|
创建方式2
使用Excutors
(线程池工具类)调用方法返回不同特点的线程池对象
Excutors线程池工具类,提供了很多静态方法用于返回不同特点的线程池对象
方法名称 |
说明 |
public static ExceutorService newFixedThreadPool(int nThreads) |
创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它 |
public static ExceutorService newSingleThreadExecutor() |
创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程 |
public static ExceutorService newCachedThreadPool() |
线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了60s,则会被回收掉 |
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) |
创建一个线程池,可以实现在给定的延迟后运行任务,或定期执行任务 |
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 static void main(String[] args) {
// 1、创建线程池
ExecutorService pool = Executors.newFixedThreadPool(3); // 就更改了这一行代码
// 2、 处理任务
Future<String> f1 = pool.submit(new MyCallable(100));
Future<String> f2 = pool.submit(new MyCallable(200));
Future<String> f3 = pool.submit(new MyCallable(300));
try {
String s = f1.get();
System.out.println(s);
} catch (Exception e) {
e.printStackTrace();
}
try {
String s = f2.get();
System.out.println(s);
} catch (Exception e) {
e.printStackTrace();
}
try {
String s = f3.get();
System.out.println(s);
} catch (Exception e) {
e.printStackTrace();
}
}
|
Executors使用可能存在的陷阱
大并发系统环境中使用Exceutors,如果不注意可能会出现系统风险
并发&并行
进程
正在运行的程序(软件)就是一个独立的进程
线程
线程属于进程的,一个进程可以同时运行多个线程
进程中的多个线程其实是并发
与并行
执行的
并发
进程中的线程是由cpu负责调度执行的,但cpu能同时处理线程的数量是有限的,为了保证全部线程都能往前执行,CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发
并行
在同一时刻,同时有个多个线程在被cpu(电脑核数)调度执行
线程生命周期
线程主要有6种状态
- new(新建)
- Runnable(可运行)
- Teminated(被终止)
- Blocked(锁阻塞)
- Waiting(无限等待)
- Timed Waiting(计时等待)