相关链接
相关理解
- 进程:分配应用程序的内存空间
- 线程:负责进程中内容执行的控制单元,也叫执行路径或执行情景
- 多线程:一个进程可以有多个执行路径。一个进程至少有一个线程
- 任务:每个线程要运行的内容
- CPU切换线程执行可以时间片来切换执行不同进程。随机切换。
好处
可以让多部分同时运行
弊端
不同的进程开启了很多的线程,CPU分给每个线程的执行时间变少,因此线程运行变慢,效率变低(可以加CPU)
JVM的多线程
- JVM启动时启动了多个线程,至少可以分析出两个线程
- 主线程(main函数执行),任务代码都定义在main函数
- 垃圾回收线程(GC),任务代码定义在垃圾回收器内部
- 其他
- 主线程结束,其他线程不一定结束。所有线程结束,jvm结束。
finalize():子类重写该方法可以执行其他清除,不写的话,直接默认被垃圾回收器清除。Object类的方法
System.gc():运行垃圾回收器,告诉垃圾回收器收垃圾,什么时候回收垃圾是随机的。
多线程创建
- windows当中,任务管理器创建需要的进程和线程
- 主线程名是main
继承Thread类来创建线程
- 定义一个类继承Thread类
- 覆盖Thread类中的run()方法,run()调用需要执行的代码
- 建一个该类的对象,创建线程
- 调用start()方法启动线程,run()方法自动运行
- 一个类有父类,就不要继承Thread了(不然多继承了)
- 快速创建线程可以使用匿名内部类
- 也可以实现
Runnable接口
Thread常用方法
YAML
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| getName(): 获取线程的名称,线程对象创建后,名称就定义了。运行run()方法也可以获取其名称 currentThread(): 返回当前正在执行线程的引用 start(): 开启一个线程 stop(): 停止当前所有线程的运行(危险,慎用) sleep(): 使当前线程睡眠 wait(): 使当前线程处于冻结状态 notify(): 从线程池中唤醒一个线程 interrupt(): 中断当前的wait或sleep等的状态 setDaemon(true): 设置线程为守护线程,需要在start()前设置。 - 前台线程全部结束,该线程会自动结束,无论是否是冻结状态。 - 守护线程也叫后台线程,true标记为守护线程 join(): 执行该方法就是冻结当前运行线程,执行该线程。 - 该线程运行完,再继续运行刚刚中止的线程。可能抛出异常InterruptedException toString(): 返回字符串形式的线程名称,包括优先级和线程组 - 优先级: 获取CPU执行权的几率,越大获取几率越高(1-10) - 线程组: 线程的组的划分,可以创建线程的时候指定 setPriority(Thread.MAX_PRIORITY): 设置进程执行优先级, - MAX_PRIORITY 10 , - MIN_PRIORITY 1 - NORM_PRIORITY 5(默认为5) yield(): 释放当前线程执行权。
|
继承Thread的多线程
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| class Test{ public static void main(String[] args){
DemoThread d1 = new DemoThread("旺财"); DemoThread d2 = new DemoThread("qiang"); d1.start(); for(int x = 0; x < 20; x++){ System.out.println(x+">>>"+Thread.currentThread().getName()); } d2.start(); } } class DemoThread extends Thread{ private String name;
DemoThread(String name){ super(name); }
@Override public void run(){ show(); }
public void show(){ for(int x = 0; x < 10; x++){ for(int y = -999999999; y < 999999999; y++){} System.out.println(name + "....x=" + x + "....name=" + Thread.currentThread().getName()); } } }
|
上述代码运行在栈中的体现
- 每一个线程一个栈
- 主函数先运行main(),开启一个栈
- 到了d1.start(),运行run(),开启一个栈
- 到了d2.start(),运行run(),开启一个栈
- 每个线程中的代码在各自的栈中分别执行
- 如果主线程或其他某个线程出现异常,该线程执行中断,不影响其他线程。
![image]()
实现Runnable的多线程
用于有父类,但需要创建多线程,这个更常用
- 定义类实现
Runnable接口,覆盖run()方法,将线程任务代码封装到run()中
- 创建Thread对象,传入实现了Runnable接口的子类对象,即可调用线程该对象创建的任务
- 向Thread对象传入Runnable接口子类对象是因为它封装了任务代码,传进去,线程开启前才能运行指定任务。
Runnable多线程示例
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 26 27 28 29
| class Test{ public static void main(String[] args){ DemoThread d = new DemoThread(); Thread t1 = new Thread(d); Thread t2 = new Thread(d); t1.start(); t2.start(); for(int i = 0; i < 10; i++){ for(int j = -999999999; j < 999999999; j++){} System.out.println(i+"..."+Thread.currentThread().getName()); } } } class DemoThread implements Runnable{ public void show(){ for(int i = 0; i < 10; i++){ for(int j = -999999999; j < 999999999; j++){} System.out.println(i+"..."+Thread.currentThread().getName()); } } @Override public void run(){ show(); } }
|
传入Runnable对象的解释
Thread接收实现了Runnable的对象,并对其执行
JAVA
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| class Thread { private Runnable r;
Thread(){ }
Thread(Runnable r){ this.r = r; }
public void run(){ if(r != null) r.run(); }
public void start(){ run(); } }
|
Runnable接口的好处
- 将线程任务从线程子类分离出来,将线程的任务进行对象封装,不必继承Thread所有方法。
- 避免了Java单继承局限性
- 与Thread相比有了思想上的变化,继承Thread是让任务成为他的一部分,实现Runnable是将任务封装成一个对象。
- Thread也实现了Runnable是因为和其他对象可以向上抽取任务的run()方法。
线程的状态
YAML
1 2 3 4 5 6 7 8
| start(): 线程从创建到运行 run(): 线程从运行到消亡(运行完了线程就消亡了) stop(): 线程从运行到消亡(主动关闭线程,不安全) sleep(time): 线程从运行到冻结,time(毫秒)时间后运行或临时阻塞 wait(): 线程从运行到冻结(一直冻结) notify(): 线程从冻结到运行或临时阻塞 CPU执行资格: 可以被CPU处理,在处理队列中排队 CPU执行权: 正在被CPU处理
|
![image]()
线程使用
同一段代码,好多人都需要同时用,可以封装成线程。
Runtime异常的情况
- 功能没问题,传值出现问题
- 功能状态发生问题
Thread t = new Thread(); t1.start();
- 线程已开启,处于一种状态,再开启会出现问题
线程安全
可以参看实际问题中的买票代码
范例解释
JAVA
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
public void sale(){ while(true){ if(num > 0){
System.out.println(Thread.currentThread().getName()+">>>>"+num--); } else break; } }
|
线程安全产生的原因
线程安全问题的解决
使用同步
- 将操作共享数据的多条代码封装成整体(使用同步代码块),有一个线程在执行这个代码,其他线程不能执行,该线程执行完,其他线程继续执行。
- 表现形式
- 同步代码块
- 同步函数(synchronized可以修饰函数)
- 好处
- 弊端
- 相对降低了效率(执行同步代码的线程不会一直占有CPU,执行权转到外部线程时,外部线程会一直判断同步锁,却无法执行代码)
- 前提
- 必须有多个线程使用同一个锁(锁写到方法里,每个线程一个锁,相当于没加,所以写到成员变量里)
同步代码块格式
JAVA
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
synchronized(对象){ if(num > 0){ try{ Thread.sleep(10); } catch(InterruptedException e){ } System.out.println(Thread.currentThread().getName()+">>>>"+num--); } else { break; } }
|
同步函数格式
- 将函数同步,一个线程占有该函数之后,除非将里边的所有代码全部执行完,否则不会放开该函数占有权。就算放开执行权,其他线程无法占有该函数,也无法执行,只能最开始的线程执行。
- 所以只将需要同步的代码封装到函数里即可,然后调用该函数
- 同步函数用的锁是this对象(
synchronized(this))。
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 26 27 28 29 30 31 32 33 34
| public synchronized void run(){ while(true){ if(num > 0){ try{ Thread.sleep(10); } catch(InterruptedException e) { } System.out.println(Thread.currentThread().getName()+">>>"+num--); } else { break; } } }
public void run(){ while(true){ show(); } } public synchronized void show(){ if(num > 0){ try{ Thread.sleep(10); } catch(InterruptedException e){ } System.out.println(Thread.currentThread().getName()+">>>"+num--); } }
|
同步函数与同步代码块区别
- 同步函数的锁只能是this对象
synchronized(this),同步代码块可以用不同的对象。
- 同步代码块用了this,可以简写为同步函数。
- 建议使用同步代码块
测试同步函数和同步代码块是否在用同一个锁
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 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
|
class Ticket implements Runnable { private int num = 100; boolean flag = true;
@Override public void run() { if (flag) { while(true) { synchronized(this) { if(num > 0) { try { Thread.sleep(10); } catch(InterruptedException e) { } System.out.println(Thread.currentThread().getName()+">>>code>>>"+num--); } } } } else { while(true){ show(); } } }
public synchronized void show(){ if(num > 0){ try{ Thread.sleep(10); } catch(InterruptedException e){ } System.out.println(Thread.currentThread().getName()+">>>function>>>"+num--); } } } class Test { public static void main(String[] args) { Ticket t = new Ticket(); Thread t1 = new Thread(t); Thread t2 = new Thread(t); t1.start(); try { Thread.sleep(10); } catch(InterruptedException e) { } t.flag = false; t2.start(); } }
|
静态同步函数
使用的锁是当前字节码文件所属的对象
this.getClass()可以从对象获取当前静态函数所在字节码文件所属的对象
类名.class可以直接获取当前静态函数所在字节码文件所属的对象(用静态属性获取)
JAVA
1 2 3 4 5 6 7 8 9 10 11
| public static synchronized void show(){ if(num > 0){ try{ Thread.sleep(10); } catch(InterruptedException e){ } System.out.println(Thread.currentThread().getName()+">>>function>>>"+num--); } }
|
单例模式的懒汉式和饿汉式的线程安全问题
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| class Single{ private static final Single s = new Single(); private Single(){ } public static Single getInstance(){ return s; } }
class Single{ private static Single s = null; private Single(){ } public static synchronized Single getInstance(){ if(s == null) Single = new Single(); return s; } }
class Single { private static Single s = null; private Single() { } public static Single getInstance() { if(s == null){ synchronized(Single.class){ if(s == null) Single = new Single(); } } return s; } }
|
同步中的死锁
常见情景
同步嵌套:一共两个锁,两个线程,每个线程必须同时拿到两个锁才能进行执行功能,但是每个线程只拿到一个,想用对方的,但对方都没放开,产生死锁。
下边的代码在调用的过程中会在执行到某次的时候,每个线程各自拿到一个锁(this或obj),想得到对方那个,却得不到
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 26 27 28 29 30 31 32 33 34 35 36 37 38
| class Lock implements Runnable{ private boolean flag; Lock(boolean flag){ this.flag = flag; } public void run(){ if(flag){ synchronized(MyLock.locka){ System.out.println(Thread.currentThread().getName()+"if>>>locka"); synchronized(MyLock.lockb){ System.out.println(Thread.currentThread().getName()+"if>>>lockb"); } } } else{ synchronized(MyLock.lockb){ System.out.println(Thread.currentThread().getName()+"else>>>lockb"); synchronized(MyLock.locka){ System.out.println(Thread.currentThread().getName()+"else>>>locka"); } } } } } class MyLock{ public static final Object locka = new Object(); public static final Object lockb = new Object(); } class Test{ public static void main(String[] args){ Lock la = new Lock(true); Lock lb = new Lock(false); Thread t1 = new Thread(la); Thread t2 = new Thread(lb); t1.start(); t2.start(); } }
|
线程通信
背景
多个线程处理同一资源,但是任务不同(四个车同时往进拉煤,三个车同时往外运煤)
- 赋值:input
- 取值:output
- 资源:resource
线程间等待唤醒机制
下边三个方法(监视器方法)都必须定义在同步中,用于操作线程状态,必须要明确操作的是哪个锁的线程。
- wait():让线程处于冻结状态,会抛出中断异常(InterruptedException)
- 释放CPU执行权和执行资格,并将该线程存到该锁的线程池
- notify():唤醒该锁线程池中的一个任意线程(使线程具备执行资格)
- notifyAll():唤醒该锁线程池中所有线程(使线程具备执行资格)
sleep与wait的异同
- sleep必须指定时间,wait可以不指定时间
- sleep和wait都释放了CPU执行权
- sleep没有释放线程锁,wait释放了线程锁
锁
- 操作的是哪个锁,就用哪个锁的方法来处理该线程
- 锁也叫监视器。
- 线程操作方法放到了Object类中,是因为这些方法都是监视器(锁)的方法,任意对象都可以作为监视器(锁),故而放到里边
等待唤醒示例
示例是线程间等待唤醒机制,适用于单生产消费模式
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 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 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171
|
class Resource{ String name; String sex; boolean flag = false; } class Input implements Runnable{ Resource r; Input(Resource r){ this.r = r; } public void run(){ int x = 0; while(true){ synchronized(r){ if(r.flag) { try{ r.wait(); } catch(InterruptedException e){ } } if(x == 0){ r.name = "Easul"; r.sex = "male"; } else { r.name = "王文博"; r.sex = "女"; } r.flag = true; r.notify(); } x =(++x) % 2; } } } class Output implements Runnable{ Resource r; Output(Resource r){ this.r = r; } public void run(){ while(true){ synchronized(r){ if(!r.flag) { try{ r.wait(); } catch(InterruptedException e){ } } System.out.println(r.name+">>>"+r.sex); r.flag = false; r.notify(); } } } } class Test{ public static void main(String[] args){
Resource r = new Resource(); Input in = new Input(r); Output out = new Output(r); Thread t1 = new Thread(in); Thread t2 = new Thread(out); t1.start(); t2.start(); } }
class Resource{ private String name; private String sex; private boolean flag = false; public synchronized void set(String name, String sex) { if(flag) { try{ this.wait(); } catch(InterruptedException e){ } } this.name = name; this.sex = sex; flag = true; this.notify(); } public synchronized void out(){ if(!flag) { try{ this.wait(); } catch(InterruptedException e){ } } System.out.println(this.name+"<<<"+this.sex); flag = false; this.notify(); } } class Input implements Runnable{ Resource r; Input(Resource r){ this.r = r; } public void run(){ int x = 0; while(true){ if(x == 0){ r.set("Easul", "male"); } else{ r.set("王文博", "女"); } x =(++x) % 2; } } } class Output implements Runnable{ Resource r; Output(Resource r){ this.r = r; } public void run(){ while(true){ r.out(); } } } class Test{ public static void main(String[] args){
Resource r = new Resource(); Input in = new Input(r); Output out = new Output(r); Thread t1 = new Thread(in); Thread t2 = new Thread(out); t1.start(); t2.start(); } }
|
多生产者和多消费者
多次生产和死锁原因
- if判断标记,只判断一次,可能导致不该运行的线程运行,数据出现错误。
- notify只能唤醒一个线程,有很大几率无法唤醒对方线程。(加while会导致死锁)
解决多次生产和死锁
- while判断标记,解决了线程获取执行权后是否执行的问题。
- notifyAll解决了本方线程一定能够唤醒对方线程的问题。(唤醒了所有线程,降低了效率,需要判断本方线程)
示例代码
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 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
|
class Resource{ private String name; private int count = 1; private boolean flag = false; public synchronized void set(String name){ while(flag) { try{ wait(); } catch(InterruptedException e){ } } this.name = name + count; count++; System.out.println(Thread.currentThread().getName()+"生产了"+this.name); this.flag = true; notifyAll(); } public synchronized void out(){ while(!flag) { try{ wait(); } catch(InterruptedException e){ } } System.out.println(Thread.currentThread().getName()+"消费了"+this.name); this.flag = false; notifyAll(); } } class Producer implements Runnable{ private Resource r; Producer(Resource r){ this.r = r; } public void run(){ while(true) r.set("烤鸭"); } } class Consumer implements Runnable{ private Resource r; Consumer(Resource r){ this.r = r; } public void run(){ while(true) r.out(); } } class Test{ public static void main(String[] args){ Resource r = new Resource(); Producer p = new Producer(r); Consumer c = new Consumer(r); Thread t0 = new Thread(p); Thread t1 = new Thread(p); Thread t2 = new Thread(c); Thread t3 = new Thread(c); t0.start(); t1.start(); t2.start(); t3.start(); } }
|
锁的升级
使用java.util.concurrent.locks包中的Lock接口和Condition接口
Lock接口替代了synchronized,将同步和锁封装成了对象
Condition接口替代了Object的监视器方法(wait notify notifyAll),将其方法封装成了对象。
- 一个Lock可以挂多个Condition。每个Condition都有一组监视器
Lock与synchronized的区别
- synchronized对于锁的操作是隐式的,看不到获取与释放锁的过程
- Lock对于锁的操作是可控的
Lock相关方法
YAML
1 2 3
| lock(): 开启锁 unlock(): 释放锁 newCondition(): 获取一组监视器对象,类型为Condition,同一个锁可以获取多组监视器
|
Condition相关方法
YAML
1 2 3
| await(): 将线程从运行到冻结 signal(): 将线程从冻结到阻塞 signalAll(): 将线程池中所有冻结线程恢复到阻塞
|
多生产者多消费者升级版
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 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
| import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.Condition; class Resource{ private String name; private int count = 1; private boolean flag = false; private Lock lock = new ReentrantLock(); private Condition producer_con = lock.newCondition(); private Condition consumer_con = lock.newCondition(); public void set(String name){ lock.lock(); try{ while(flag) { try{ producer_con.await(); } catch(InterruptedException e){ } } this.name = name + count; count++; System.out.println(Thread.currentThread().getName()+"生产了"+this.name); this.flag = true; consumer_con.signal(); } finally { lock.unlock(); } } public void out(){ lock.lock(); try{ while(!flag) { try{ consumer_con.await(); } catch(InterruptedException e){ } } System.out.println(Thread.currentThread().getName()+"消费了"+this.name); this.flag = false; producer_con.signal(); } finally { lock.unlock(); } } } class Producer implements Runnable{ private Resource r; Producer(Resource r){ this.r = r; } public void run(){ while(true) r.set("烤鸭"); } } class Consumer implements Runnable{ private Resource r; Consumer(Resource r){ this.r = r; } public void run(){ while(true) r.out(); } } class Test{ public static void main(String[] args){ Resource r = new Resource(); Producer p = new Producer(r); Consumer c = new Consumer(r); Thread t0 = new Thread(p); Thread t1 = new Thread(p); Thread t2 = new Thread(c); Thread t3 = new Thread(c); t0.start(); t1.start(); t2.start(); t3.start(); } }
|
注意:同步里只能有一个线程执行,但是活着的可以不止一个
JAVA
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
class Demo{ void show() { wait(); } void method() { wait(); code notifyAll(); } }
|
停止线程几种方式
- 中断:一种冻结状态,中止正在运行的状态
- 线程的stop()方法
- 线程的run()方法结束
- 任务中都有循环结构,控制循环就可以结束任务
- 控制循环结束用标记来控制,常用。while(flag){}
- 如果线程处于wait(冻结状态)或sleep,就无法结束线程(无法读取到标记)
- 使用interrupt(),中断当前wait()或sleep()的状态使其具备CPU执行资格
- 因为是强制执行,wait或sleep可能会抛出InterruptedException
买票
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
|
class Ticket implements Runnable{ private int num; Ticket(int num){ this.num = num; } public void sale(){ while(true){ if(num > 0) System.out.println(Thread.currentThread().getName()+">>>>"+num--); else break; } } public void run(){ sale(); } } class Test{ public static void main(String[] args){ Ticket t = new Ticket(100); Thread t1 = new Thread(t); Thread t2 = new Thread(t); Thread t3 = new Thread(t); Thread t4 = new Thread(t); t1.start(); t2.start(); t3.start(); t4.start(); } }
|
代码中问题如下
- 对于这个代码来说,创建了任务对象,放到堆中,创建了四个线程,每个线程所使用的数据都是这个任务对象的数据,从而实现了数据共享,不需要静态。
- 运行后出现的输出不规律
- 多核CPU交替运行可能导致运算的顺序不同
- 输出和运算顺序不一样(参考异常输出和其他线程共同运行的情况)
- 会有线程安全,可能会买同一张票,或者有负数的票
输出1还是2
JAVA
1 2 3 4 5 6 7 8 9 10 11
|
new Thread(new Runnable(){ public void run(){ System.out.println(1); } }){ public void run(){ System.out.println(2); } }.start();
|