java 多线程
[toc]
多线程
进程
在计算机中,我们把一个任务称为一个进程,浏览器就是一个进程,视频播放器是另一个进程,类似的,音乐播放器和Word都是进程。
进程和线程的关系:一个进程可以包含一个或多个线程,但至少会有一个线程。
创建一个线程
方法一:继承Thread.类,重写run()方法,调用start开启线程
public class Demo04 extends Thread{//继承Thread类 @Override public void run() {//重写run方法 for (int i = 0; i < 5; i++) { System.out.println("线程里的方法:"+i); } } public static void main(String[] args) { Demo04 demo04 = new Demo04(); demo04.start();//用start方法开启线程 //demo04.run();//这个就不会另外起一个线程 System.out.println("主线程中的方法"); }//start方法会产生主方法和另一个线程的方法同时执行,而直接调用run方法就会按原来顺序从上往下执行 }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- 方法二(推荐):实现Runnable接口,创建线程对象,通过线程对象来开启我们的线程,代理
- ```java
public class Demo05 implements Runnable{//实现Runnable接口
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("线程里的方法"+i);
}
}
public static void main(String[] args) {
Demo05 demo05 = new Demo05();//创建线程对象
new Thread(demo05).start();//通过线程对象来开启我们的线程
//重载方法:new Thread(实现Runnable接口的对象,线程的命名);
System.out.println("主线程中的方法");
}
}
线程并发
并发:是指同一个时间段内多个任务同时都在执行,并且都没有执行结束。强调通过很小的时间片的切换对将程序交替执行,假象的并行效果。
并行:两个以上事件(或线程)在同一时刻发生,没有时间片的交换,跑在不同的CPU资源上,一般是多核
抢票实例:
public class Demo06 implements Runnable{ private int TicketNum =10;//共同的抢票 @Override public void run() { while (TicketNum>0){ System.out.println(Thread.currentThread().getName()+"抢到了第"+(TicketNum--)+"张票");//Thread.currentThread().getName()可以获取线程的名称; } } public static void main(String[] args) { Demo06 Ticket = new Demo06(); new Thread(Ticket,"1号线程").start(); new Thread(Ticket,"2号线程").start(); new Thread(Ticket,"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
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
#### 静态代理
- 优点:可以在不修改目标对象的前提下**扩展**目标对象的**功能**。
- 操作步骤
1. 先定义一个接口(有相印的方法)
2. 定义真实类和代理类,都要实现这个接口
3. 代理类中创建一个接口的属性,创建这个属性的构造函数
4. 主函数中代理类的构造函数传入一个实现接口的类/实例就可以实现代理的拓展功能
- ```java
public interface Marry {//定义接口,
void HappyMarry();//接口中的方法
}
class You implements Marry {//真实的类实现接口
@Override
public void HappyMarry() {
System.out.println("终于结婚了");
}
}
class WeddingHouse implements Marry {//代理类,实现接口
private Marry target;//定义一个接口的属性
public WeddingHouse(Marry target) {
this.target = target;
}
@Override
public void HappyMarry() {
before();//对原来接口的扩充
target.HappyMarry();//原来接口的方法
after();//对原来接口的扩充
}
private void after() {
System.out.println("处理后事");
}
private void before() {
System.out.println("邀请嘉宾");
}
}
//主函数中实现
public static void main(String[] args) {
//第一种,直接创建一个代理类的对象,并且在构造函数中传入实现接口的真实对象
WeddingHouse weddingHouse = new WeddingHouse(new You());
weddingHouse.HappyMarry();
//第二种,直接调用隐士对象,就相当是new Thread(实现Runnable接口的对象).start();
new WeddingHouse(new You()).HappyMarry();
//第三种,直接采用匿名内部类,直接进行输出代理拓展的内容
new WeddingHouse(new Marry() {
@Override
public void HappyMarry() {
System.out.println("我也要结婚啦");//这里没有用真实的类,根据自己需求实现想要的功能
}
}).HappyMarry();
}
lambda表达式
函数式接口的定义:
任何接口,如果只包含唯一一个抽象方法,那么它就是一个函数式接口。1
2
3public interface Runnable{
public abstract void run();
}对于函数式接口,我们可以通过lambda表达式来创建该接口的对象。
实现:创建一个类,实现接口,通过接口创建属性就可以新建一个实现接口类的对象,调用实现的方法,或者式lambda表达式
interface Love { void iLove(); } class You implements Love {//实现了接口 @Override public void iLove() { System.out.println("i love you ");//重写方法 } } public static void main(String[] args) { Love love = new You();//新建一个接口的属性,创建一个实现类的对象 love.iLove();//通过实例来调用方法 Love love1 = new Love() { @Override public void iLove() { System.out.println("i love you 1"); } };//匿名内部类,不用实现类就可以直接通过接口进行方法的内部实现 love1.iLove(); Love love2 = () -> System.out.println("i love you 2");//lambda表达式对上面的简化, 实质是一样的。不需要写new ,也不需要写方法名(因为只有一个);只有实现方法体 love2.iLove(); } //如果有参数: Love love2 = (参数) -> System.out.println("i love you 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
#### 线程的五大状态
- <img src="C:\Users\Leilei\AppData\Roaming\Typora\typora-user-images\image-20220623150006422.png" alt="image-20220623150006422" style="zoom:25%;" />
- 含义:在Java程序中,一个线程对象只能调用一次`start()`方法启动新线程,并在新线程中执行`run()`方法。一旦`run()`方法执行完毕,线程就结束了(死亡的线程无法再次`start`)。因此,Java线程的状态有以下几种:
- New:新创建的线程,尚未执行;
- Runnable:运行中的线程,正在执行`run()`方法的Java代码;
- Blocked:运行中的线程,因为某些操作被阻塞而挂起;
- Waiting:运行中的线程,因为某些操作在等待中;
- Timed Waiting:运行中的线程,因为执行`sleep()`方法正在计时等待;
- Terminated:线程已终止,因为`run()`方法执行完毕。
用一个状态转移图表示如下:
```ascii
┌─────────────┐
│ New │
└─────────────┘
│
▼
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
┌─────────────┐ ┌─────────────┐
││ Runnable │ │ Blocked ││
└─────────────┘ └─────────────┘
│┌─────────────┐ ┌─────────────┐│
│ Waiting │ │Timed Waiting│
│└─────────────┘ └─────────────┘│
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
│
▼
┌─────────────┐
│ Terminated │
└─────────────┘
当线程启动后,它可以在
Runnable、Blocked、Waiting和Timed Waiting这几个状态之间切换,直到最后变成Terminated状态,线程终止。线程终止的原因有:
- 线程正常终止:
run()方法执行到return语句返回; - 线程意外终止:
run()方法因为未捕获的异常导致线程终止; - 对某个线程的
Thread实例调用stop()方法强制终止(强烈不推荐使用)。
一个线程还可以等待另一个线程直到其运行结束。例如,
main线程在启动t线程后,可以通过t.join()等待t线程结束后再继续运行:线程方法:
方法 说明 setPriority(int newPriority)更改线程的优先级 static void sleep(long millis)在指定的毫秒数内让当前正在执行的线程休
眠void join()等待该线程终止 static void yield()暂停当前正在执行的线程对象,并执行其他
线程void interrupt()中断线程,别用这个方式 boolean isAlive测试线程是否处于活动状态 线程停止的方法:
建议线程正常停止—>利用次数,不建议死循环。
建议使用标志位—>设置一个标志位
不要使用stop或者destroy等过时的方法
class ThreadStop implements Runnable {//线程类 private boolean flag = true;//提供线程体中的标识 public void run() { while (flag) {//线程使用标识 System.out.println("Thread is running"); } } public void stop() {//提供外部方法停止 flag = false; } public static void main(String[] args) { ThreadStop threadStop = new ThreadStop(); new Thread(threadStop).start(); for (int i = 0; i < 999; i++) { if (i == 900) threadStop.stop();//调用线程类提供的公有方法 System.out.println("main is running"); } } }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
##### 线程休眠:
- 作用:
- 模拟网络延时,放大问题的发生性->(排查线程不安全)
- 每个对象都有一个锁,`sleep`不会释放锁
##### 线程礼让:
- 作用:
- 礼让线程,让当前正在执行的线程暂停,但不阻塞->(礼让一下,再竞争一次)
- 将线程从运行状态转为就绪状态
- 让CPU重新调度,**礼让不一定成功**!看CPU心情
- `yield`也不会释放锁
- 线程强制执行`jion`
- 可以将`jion`想象成插队,插主线程,很霸道,但是对于两个并行的线程无法直接插队
- 线程的优先级
- 如果有些比较重要的代码,需要先跑,可以设置优先级,虽然设置优先级并不一定会一定在前面先执行,但是至少他所占有的机会会变大一点
- 默认为5,最高的优先级为10,最低为1,超范围报错
- 线程优先级的两个方法
- ```java
setPriority(级别);//设置线程优先级
getPriority();//获取线程的优先级
守护线程
守护线程就像餐厅的服务员,而普通的用户线程就像是顾客,如果顾客都走光了,那么守护线程存在的意义也没有了,自然也会结束
作用
- 常见的做法,就是将守护线程用于后台支持任务,比如垃圾回收、释放未使用对象的内存、从缓存中删除不需要的条目。
- 咦,按照这个解释,那么大多数
JVM线程都是守护线程。
守护线程的方法:
setDaemon(true)//设置守护线程,注意,一定要在start方法之前设置守护线程; isDaemon()//判断是否是守护线程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
#### 线程同步
- 形成条件:队列+锁
##### 重点:synchronized到底锁的是谁
- 下面用一个实例来证明
- ```java
class Date {
public void func1() {//普通方法
try {
Thread.sleep(3000);//休眠3秒
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("1...");
}
public void func2() {//普通方法
System.out.println("2...");
}
}
public static void main(String[] args) {
Date date = new Date();
new Thread(date::func1, "A").start();//lambda表达式启动线程
try {
Thread.sleep(1000);//休眠1秒
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(date::func2,"B").start();//lambda表达式启动线程
}
//结果:1s后输出2...再过2秒后输出1...
//原因:两个都是新起的线程,没有相关的同步性,按程序顺序执行
//2...
//1...
锁方法
普通方法:谁调用就锁谁
public synchronized void func1() {//把上面的修改为同步方法 public synchronized void func2() {//把上面的修改为同步方法 //结果:过了三秒同时输出,结果是先1...后2... //原因,锁了这个方法,谁来调用就锁哪个对象,这里只有一个对象,date,所以先启动A线程,然后过了sleep3秒(不放锁),主程序的sleep1秒对这里没有影响,直接就已经包含在sleep的3秒内了,因线程同步,三秒钟后,等A的锁释放,B立刻拿锁,输出 //1... //2...1
2
3
4
5
6
7
8
- <img src="C:\Users\Leilei\AppData\Roaming\Typora\typora-user-images\image-20220623195402761.png" alt="image-20220623195402761" style="zoom:33%;" />
- ```java
public synchronized void func1() {//把上面的修改为同步方法
public void func2() {//普通方法
//结果:1秒后输出2...再过2秒输出1...
//原因:B线程不是同步的方法,直接就是两个线程在跑,public static void main(String[] args) {//新建两个Date()对象 Date date1 = new Date(); Date date2 = new Date(); new Thread(date1::func1, "A").start(); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } new Thread(date2::func2,"B").start(); } public synchronized void func1() {//把上面的修改为同步方法 public synchronized void func2() {//把上面的修改为同步方法 //结果:1秒后输出2...再过2秒输出1... //原因:,直接就是两个线程在跑,没有同步争夺资源1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- 静态方法:锁定的是类
- ```java
public static void main(String[] args) {
Date date1 = new Date();
Date date2 = new Date();
new Thread(() -> {
date1.func1();
}, "A").start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> {
date2.func2();
}, "B").start();
}
public static synchronized void func1() {//变成了静态方法
public static synchronized void func2() {//变成了静态方法
//结果:过了三秒同时输出,结果是先1...后2...
//原因:虽然date1与date2是两个不同的对象,但是因为静态同步方法锁的是这个类,两个对象都是来自同一个类,所以存在线程同步的关系。
修饰代码块:
可以锁各种东西,放什么锁什么
class Date2 { public void func() { System.out.println("start..."); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("end..."); } } public static void main(String[] args) { Date2 date2 = new Date2(); for (int i = 0; i < 5; i++) { new Thread(() -> { date2.func(); }).start();//创建5个线程 } } //结果:直接是5个start,1秒后输出5end //原因:没有任何锁,直接是5个线程1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- ```java
//只改变成同步代码块
public void func() {
synchronized (this) {//改成代码同步
System.out.println("start...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("end...");
}
}
//结果:在排队,交替输出start和end
//原因:这里this是这个Date2的对象,只有一个date2对象,变成了线程同步public static void main(String[] args) { for (int i = 0; i < 5; i++) { new Thread(() -> { Date2 date2 = new Date2();//只改变每次创建的对象 date2.func(); }).start(); } } //结果:在排队,交替输出start和end //原因:虽然同步代码块修饰了这个类创建的对象,但这时候对象创建放在了循环里,相当每次创建不同的对象,那么每次调用func方法就是直接开启新线程,没有线程同同步1
2
3
4
5
- ```java
synchronized (Date2.class) {//改成代码同步锁这个类
//结果:在排队,交替输出start和end
//原因:锁的是这个类,虽然创建的是5个不同的对象,但是都是同一个类,产生了线程同步
死锁:
多个线程互相抱着对方需要的资源,然后形成僵持即一个线程可以获取一个锁后,再继续获取另一个锁。
public void add(int m) { synchronized(lockA) { // 获得lockA的锁 this.value += m; synchronized(lockB) { // 获得lockB的锁 this.another += m; } // 释放lockB的锁 } // 释放lockA的锁 } public void dec(int m) { synchronized(lockB) { // 获得lockB的锁 this.another -= m; synchronized(lockA) { // 获得lockA的锁 this.value -= m; } // 释放lockA的锁 } // 释放lockB的锁 }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
- 解决方案:一个线程可以获取一个锁后,再继续获取另一个锁。
- 那么我们应该如何避免死锁呢?答案是:线程获取锁的顺序要一致,一次只获得一把锁,避免一个线程同时获得两把锁
- ##### `lock`锁
- `lock`是显示锁,只能锁代码块
- 好处:用`lock`,`jvm`将花费较少的时间调度线程,性能更好,可拓展性高
- ```java
class TestLock implements Runnable {
private int ticket = 100;
private final ReentrantLock lock = new ReentrantLock();//可重入的锁,一定要记住这个方法,通过他的lOck和unlock方法实现同步
@Override
public void run() {
while (true) {
try {
lock.lock();//在需要变化的地方设置锁
if (ticket <= 0) {
return;
} else {
System.out.println(ticket--);
}
} catch (RuntimeException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();//一般放在finally块里释放锁
}
}
}
}
线程协作
生产者消费者模型具体来讲,就是在一个系统中,存在生产者和消费者两种角色,他们通过内存缓冲区进行通信,生产者生产消费者需要的资料,消费者把资料做成产品。(是一种问题,不是设计模式)
再具体一点:
- 生产者生产数据到缓冲区中,消费者从缓冲区中取数据。
- 如果缓冲区已经满了,则生产者线程阻塞。
- 如果缓冲区为空,那么消费者线程阻塞。
通过wait()和notify()方法,采用 阻塞队列 方式实现生产者消费者模式
管程法:
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("生产力"+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++) { Chicken pop = Container.pop(); System.out.println("消费类"+pop.id+"只鸡"); } } } class Chicken {//生产的对象 int id;//对象id public Chicken(int id) { this.id = id; } } class SynContainer { //同步容器,锁就是锁他,通过他来实现中间的桥梁,使两个线程跑在这个上,变成并发 Chicken[] chickens = new Chicken[10];//定义容器大小 int count = 0;//记录生成的数 public synchronized void push(Chicken chicken) { if (count == chickens.length) { try { wait();//容器满了就会调用wait方法释放锁,让消费者先行 } catch (InterruptedException e) { throw new RuntimeException(e); } } chickens[count++] = chicken; this.notifyAll();//释放锁 } public synchronized Chicken pop() { if (count == 0) { try { wait();//没有消费对象的时候,就会wait释放锁,让生产者先行 } catch (InterruptedException e) { throw new RuntimeException(e); } } count--; Chicken chicken = chickens[count]; this.notifyAll();//释放锁 return chicken; } } public static void main(String[] args) { SynContainer synContainer = new SynContainer(); Producer producer = new Producer(synContainer); Consumer consumer = new Consumer(synContainer); producer.start(); consumer.start(); } //结果:一个线程里,生产者与消费者正常是交错执行,如果满足了wait的条件,就释放了锁,给另外一方执行1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#### 线程池
- 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。
可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
- 好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理(.…)
- 用法:
```java
public static void main(String[] args) {
//1.创建服务,创建线程池
//newFixedThreadPool参数为:线程池大小
ExecutorService service = Executors.newFixedThreadPool(10);
//执行
service.execute(new 实现了Runnable接口的类);
service.shutdown();
}











