本文章目录:
- 进程与线程的概念
- 两者的优缺点和对比
- 多线程的使用
–两种创建的方式:Thread 和 Runnable
–两种启动的方式:start 和 run
–多线程的关闭 - 多线程信息共享
- 多线程的管理
- 锁浅谈
推荐小白入门视频:
B站的2019Java多线程精讲【千锋大数据】点击直达https://www.bilibili.com/video/BV1Z4411G7vn/?p=1
该视频讲漏的知识点(对比我知道的):
1.volatile:解决共享变量突破上下限的
简单来说下多进程的实质概念:
OS(操作系统)将CPU的使用时间分成N多极其短暂的时间片段,例如几纳秒几微秒,然后分配给多个进程。
(这里指单核的情况下)实质上是串行依次进行多个任务。因为CPU交替间隔太短,造成了同时进行的错觉。
多核自然就能实现真正意义的并行多进程任务,多个嘛
简单概括就是:OS把CPU的心掰成很多片,给了多个人(进程),轮流使用
多进程的优点:
- 同时运行多个任务
- 但某个程序被IO堵塞时,可以释放CPU给其他进程使用
多进程的缺点:
太笨重,不好管理,也不好切换
多线程的概念:
————多线程是对多进程的优化方案
线程简单讲就是降低了量级的小进程,它是将单个进程 进行 进一步分化,更加合理的利用CPU资源
多线程是把创造它的进程当作小OS,这样就可以实现单进程下的多线程串行、并行任务
注意:(单核下)多线程的并行任务本质上也和多进程一样,都是CPU的串行执行,不是同时执行!!!
多线程存在的好处:
- 资源利用: 不会造成当程序某个模块IO堵塞时,就立马释放整个进程的CPU,直接切换进程,造成极大浪费资源(进程笨重),而是会继续停留在该进程执行其他线程。
- 执行效率: 在执行同样数量的任务时,单进程多线程和单进程单线程相比,显然多线程更快,并行比串行快
多进程和多线程的对比:
- 量级方面: 线程更轻量级,更容易管理和切换,CPU切换的代价小
- 数据共享:(同个进程下)多线程可以共享数据(同个运行空间),通讯更高效。而多进程之间的数据一般不能共享和通讯。
概念终于理清了
多线程的使用
一、创建新线程:
- 继承java.lang.Thread类:
可以直接new ,即 new 继承类的类名.start(); - 实现java.lang.Runnable接口:
不能直接new启动,必须依赖Tread类,即new Thread(new 实现接口类的类名.start() );
两种方式都要实现run方法
用Thread 和 Runnable 创建的对比:
- Runnable的唯一缺点: 需要Thread类的支持,即无法直接new 启动
- Runnable不需要占用继承的名额(单继承,多接口)
- Runnable实现数据共享更容易:Runnable普通变量就可以共享,而Thread方式必须static变量才能共享
建议使用实现Runnable接口方式来完成多线程
题外话——java的四大接口:
- Comparable:用于对象比较
- Runnable:用于对象线程化
- Serializable:用于对象序列化
- Clonable:用于对象克隆
共享资源 又称为 临界资源
二、多线程启动
多线程启动的两种方式:
————————————易错处
- 利用start方法启动:会自动以新线程启动,是并行 运行
- 直接执行run方法:将变成串行执行!(这样和单线程差不多)
简单讲就是run启动的直接就是直线执行代码!被run启动的线程不结束,就不会执行该线程之后的代码(即串行)
注意点:
- 一个线程只能有0或者1个start方法
- 多个线程的启动顺序是随机的(真正意义上的,只能确定main线程是最先启动)
三、多线程的关闭:
- run方法结束后,系统自动关闭
- 程序(进程) 结束: 当所有线程(包括main线程)都结束,程序才结束
- 手动关闭:
stop( )
注意点:main线程结束不等于程序结束,其他线程也可以继续单独进行
四、多线程信息共享(重点)
这里有个概念:
- 粗粒度:子线程与子线程之间、子线程与main线程缺乏交流,没有同步协调操作,如信息不共享
- 细粒度:线程之间有交流,有同步协调操作,最简单的体现就是信息共享
换句话说就是:线程之间没有信息共享,就是粗粒度,有就是细粒度
信息共享方法:
- 通过共享变量实现信息共享
MPI并行库实现线程之间直接互发消息(MPI是C/C++的并行库,java原生库并不支持这种形式。题外话:MPI是高性能计算的主要模型)
共享变量:
- static变量: 大家都懂,静态嘛,略。(继承Tread方式的只能使用这种static变量达到共享!)
- 同一Runnable里的成员变量,即实现了Runnable接口的类同一个实例的成员变量都可以共享
注意点:如果是同一Runnable类的不同实例,那么他们的成员变量是不共享的!!!必须同一个Runnable类的同一个实例
这里暂时忽略数据同步的问题
public class ThreadDemo {
public static void main(String[] a){
ThreadRun r1 = new ThreadRun();
new Thread(r1).start();
new Thread(r1).start();
new Thread(r1).start();
new Thread(r1).start();
}
}
class ThreadRun implements Runnable{
private int tickets = 200;
@Override
public void run() {
while(tickets > 0){
try{
Thread.sleep(50);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("卖出门票一张,剩余:"+ tickets--);
}
}
}
结果:
不同实例r1、r2,代码和结果如下,显然不能数据共享
public class ThreadDemo {
public static void main(String[] a){
ThreadRun r1 = new ThreadRun();
ThreadRun r2 = new ThreadRun();
new Thread(r1).start();
new Thread(r2).start();
new Thread(r1).start();
new Thread(r2).start();
}
}
class ThreadRun implements Runnable{
private int tickets = 200;
@Override
public void run() {
while(tickets > 0){
try{
Thread.sleep(50);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("卖出门票一张,剩余:"+ tickets--);
}
}
}
现在来解决一下数据不同步的问题
两方面下手(两种选一种就行):
- 数据同步:共享变量加个volatile修饰符
- 关键步骤加锁:给代码块或者函数加synchronized修饰符
题外话——关键步骤加锁:
- 互斥:同一时间只能有一个线程执行该代码块或函数
- 同步:多个线程的运行必须按照某种规定的先后顺序来执行这个代码段
互斥是同步的一种特例,同步限制更严
synchronized会加大性能负担,但使用方便,是java里互斥的一种简单实现
数据加volatile:
该共享数据所有线程都能看到它的实时变化,就不会出现数据不同步的问题
代码块或函数加synchronized:
该代码块或者函数在同一瞬间只能让一个线程进入,不能同时被多个线程执行,数据就同步了
上面两种都要做的:在最后的临界资源语句(即加锁的地方)那里加判断条件,就不会出现数据上限或下限被突破的问题(第一张图片里的-1,-2)
synchronize使用注意点:
- 依赖单例对象锁: synchronize需要一个对象作为锁,而且这个对象是单例的,即不可重复synchronize(new 对象)
- 使用synchronize加锁方法或函数时,直接当修饰符用,不需要写对象锁:如果是静态的方法或函数,则锁是当前类,即默认synchronize(当前类名);若是非静态的,那么对象锁就是this,即默认synchronize(this)
修改后的代码如下:
package com.company;
public class ThreadDemo {
public static void main(String[] a){
ThreadRun r1 = new ThreadRun();//必须同个实例才能共享
new Thread(r1).start();
new Thread(r1).start();
new Thread(r1).start();
new Thread(r1).start();
}
}
class ThreadRun implements Runnable{
// private volatile int tickets = 200; //volatile和synchronize两个有一个存在就行
private volatile int tickets = 200;
String socket = ""; //锁
@Override
public void run() {
while(tickets > 0){
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(socket) { //这里是24行,synchronized依赖一个对象(任意),用该对象作为锁
if (tickets>0){
System.out.println("卖出门票一张,剩余:" + --tickets);
}
}
//24到28也可以换成注释的写法,下面的写法就不需要再重新new个对象当锁(例如上面的socket)
//printStr();
}
}
// public synchronized void printStr(){
// if (tickets>0){
// System.out.println("卖出门票一张,剩余:" + --tickets);
// }
// }
}
结果:
五、多线程管理
5.1、先了解下线程的状态:
1. NEW 刚创建: new
2. RUNNABLE就绪态:start
3. RUNNING运行中:run
4. BLOCK阻塞:sleep
5. TERMINATED结束
线程状态流程图:(借用别人网课的图,嘿嘿偷下懒,看不清的,去上面的视频链接自己看图)
若有侵权请通知我,立马删
5.2、管理的内容实际上就两种:
- 信息共享: 上面说了,略
- 改变线程状态:改变自己线程或其他线程的状态,以达到想要的程序效果
改变线程状态:
- 新生态 (NEW) ------> 就绪态(RUNNABLE) : 利用start( )
- 就绪态 < ------> 运行态(互转):OS 自己控制(这两个状态就暂时合称为活动期吧)
- 活动期 ------> 阻塞态(BLOCK):
suspend( ) - 阻塞(BLOCK) ------>活动期 :
resume( ) - 活动期------>TERMINATED(死亡态):
stop( ) - 阻塞(BLOCK) ------>TERMINATED(死亡态):
stop( )
被废弃的API:(不安全、旧的)
- suspend( ):暂停
- resume( ):恢复
- stop( ):停止
- distory( ):销毁
API解释:
- join( ):等待另一个线程结束才运行
- interrupt( ):向另一个线程发送中断信号,使他进入阻塞状态
管理分被动和主动两种方式
- 方式一:被动停止或恢复自身:在一个线程里用上述API管理另一个线程
- 方式二:主动停止或恢复自身:线程自身用上述API管理自身(实际上就是其他线程利用共享变量来告知另一个线程,间接让另一个线程停止或恢复活动,不直接干预)
被动管理是粗暴的且不受它自身控制的:有可能在被结束时,来不及释放自身持有的资源,或者来不及执行其他指定的结束操作!
5.3、线程的优先级
简简单单的就一句:线程名.setPriority(数值);
注意点:
- 优先级的设置范围是1到10,数值越大,越优先
- 优先级默认值是5
- 优先级必须在线程启动前设置
- 优先级只是减小或增大该线程抢夺到CPU的概率,并不是百分百决定线程先后的执行顺序
5.4、线程的礼让
概念:强迫该线程放弃抢到的CPU资源,重新回到就绪态,继续抢CPU资源
简单说就是强迫该家伙把到手的肥肉扔出去,重新去抢肥肉,美其名曰:孔融让梨。
就一句:线程名.yield( );
例子一(被动停止或恢复自身的线程):
百度网盘——生产者与消费者例子的链接,点击直达
https://pan.baidu.com/s/1ClceQNd_vvDN5nza6MPxHg 提取码:rio1
例子二(主动停止或恢复自身的线程):
线程主动管理的例子,点击直达
https://pan.baidu.com/s/1oX5pQMvqXlgFNVMhIpbA8Q 提取码:k77f
六、锁浅谈(极浅极浅的)
1.互斥锁:上面的synchronize
- 必须:需要一个对象锁(类也是对象)
- 其他:略
2.显式锁:ReentTranLock
- lock( ):给后面代码加锁
- unlock( ) :线程归还锁,即解锁
import java.util.concurrent.locks.ReentrantLock;
public class SimpleThreadDemo {
public static void main (String[] a){
//显式锁ReenTranLock
ThreadTickets tickets = new ThreadTickets();
Thread threadTickets1 = new Thread(tickets);
Thread threadTickets2 = new Thread(tickets);
Thread threadTickets3 = new Thread(tickets);
threadTickets1.start();
threadTickets2.start();
threadTickets3.start();
}
}
//显式锁例子
class ThreadTickets implements Runnable{
ReentrantLock RTlock = new ReentrantLock();//实例化一个显式锁
private int tickets = 200;
@Override
public void run() {
while(tickets > 0){
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
RTlock.lock(); //给后面的代码加锁
if(tickets > 0){
System.out.println(Thread.currentThread().getName()
+ "卖出门票一张,剩余:" + --tickets);
}
RTlock.unlock(); //线程归还锁,即解锁
}
}
}
3.死锁:
死锁就是在多线程里,自己需要的锁被被别的线程持有,而自己持有的锁是别的线程需要的,互相不放开已有的锁,互相卡住,导致彼此都是wait
简单说就是:A、B是邻居,A在B家里,B在A家里,两家的门都是锁的,但A、B赌气就是都不通过窗户扔手里的钥匙给对方,两人就都被锁住了。
import java.util.concurrent.locks.ReentrantLock;
public class SimpleThreadDemo {
public static void main (String[] a){
//死锁例子
Thread threadDead1 = new Thread(new ThreadDeadA());
Thread threadDead2 = new Thread(new ThreadDeadB());
Thread threadDead3 = new Thread(new ThreadDeadC());
threadDead3.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
threadDead1.start();
threadDead2.start();
}
}
//死锁例子
class ThreadDeadA implements Runnable{
@Override
public void run() {
try {
synchronized ("钥匙1"){
System.out.println("我是A,我有1号房钥匙,但我现在在2号房间,我不想给B钥匙");
Thread.sleep(3500);
System.out.println("我堂堂高贵的A,就算饿死在2号房间,我也不会先扔钥匙给B!");
//解除下面四行的注释符,则死锁解开
// Thread.sleep(4000);
// System.out.println("我是A,这2号房也太多蚊子了,叮得老子浑身痒,受不了了,我就先扔钥匙给B吧");
// Thread.sleep(2500);
// "钥匙1".wait();
synchronized ("钥匙2"){
System.out.println("我是A,我拿到2号房钥匙了,我出来了,终于不用被蚊子叮了,真香");
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class ThreadDeadB implements Runnable{
@Override
public void run() {
try{
synchronized ("钥匙2"){
System.out.println("我是B,我有2号房钥匙,但我现在在1号房间, 我不想给A钥匙");
Thread.sleep(1000);
synchronized ("钥匙1"){
System.out.println("我是B,没想到半夜A睡觉前偷偷给我钥匙了。" +
"我拿到1号房钥匙了,我出来了");
Thread.sleep(3000);
System.out.println("两把钥匙对我也没用了,我就偷偷扔钥匙进去," +
"弄点声音叫醒他吧,别让他继续困在里面了");
// "钥匙1".notify(); //随机叫醒一个拿过1号钥匙的人,不吵到别的邻居
"钥匙1".notifyAll(); //叫醒所有拿过1号钥匙的邻居们,不吵到别的邻居
// notifyAll(); //管他们睡不睡,老子就是想唱大河向东流
}
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
class ThreadDeadC implements Runnable{
public String c = "钥匙1";
@Override
public void run() {
synchronized ("钥匙1"){
try {
System.out.println("我是路人C,我脚崴了,痛死我啦");
"钥匙1".wait();
Thread.sleep(2000);
System.out.println("我是C,有神秘人救了我,是不是上帝?");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}