通俗易懂带你了解Java多线程处理
何夜息随笔录-多线程的使用
进程和线程
首先需要分清进程和线程,进程是系统分配的单位,就是任务管理器可以看到的进程,这是由系统自动分配和管理的,每个进程都有进程PID
。
线程就是程序的一块逻辑,这是我们通过代码去创建的,我们可以操作多个进程,程序运行的时候就会吧线程自动放到进程中去执行。
一个进程可以包括多个线程!线程是CPU执行和调度的单位。
线程的创建
首先是使用继承Thread类,然后重写run方法,然后调用start方法执行线程。
然后看源码可以发现,这个被继承的Thread类,其实是实现了Runnable接口,然后接口里有一个run方法,所以第二种我们就可以直接实现
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8fIcw3d6-1631801467817)(https://www.heyexi.com/markdown/clipboard-1619094281173.png)]
public class ThreadTest extends Thread
{@Overridepublic void run(){System.out.println("这是线程里的方法");}public static void main(String[] args){System.out.println("这是主线程里的方法");ThreadTest threadTest = new ThreadTest();threadTest.start();//调用start方法,不是run方法}
}输出内容:
这是主线程里的方法
这是线程里的方法
注意,并不是先执行main方法里面的,再执行新建的线程,只是因为运行过快才会看到先执行了主线程里的输出,如果时间久的话这两个线程是会相互交叉的,因为线程是根据CPU的资源进行自由调度的。
当然,最通用的方法就是实现Runnable接口,重写里面的run方法,
然后呢,我们需要把这个实现了Runnable接口的类,作为Thread对象的构造参数,还是需要用Thread来启动线程!
public class ThreadTest2 implements Runnable
{@Overridepublic void run(){while (true)System.out.println("这里是线程内容");}public static void main(String[] args){new Thread(new ThreadTest2()).start();System.out.println("这是主线程里的方法");}
}
线程的状态:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lO63U3PE-1631801467823)(https://www.heyexi.com/markdown/clipboard-1619094322984.png)]
线程的停止
线程创建后,我们需要根据具体的条件今天停止线程,官方不建议使用stop等方法来强制停止,因为这个不安全。
常规做法是,自定义个停止线程的方法,用一个全局的布尔值的标识符来停止线程,当为假时就停止线程,这个条件由主方法进行控制。
package com.heyexi;public class StopThread implements Runnable
{private boolean flag = true;@Overridepublic void run(){while (flag)System.out.println("线程正在运行....");}//线程停止方法public void stop(){this.flag = false;System.out.println("线程已经停止");}public static void main(String[] args){StopThread stopThread = new StopThread();new Thread(stopThread).start();for (int i = 0; i < 1000; i++){System.out.println("i="+i);if(i==456)//停止线程条件stopThread.stop();}}
}输出:
i=452
i=453
i=454
i=455
i=456
线程已经停止
i=457
i=458
线程休眠
有时候为了防止线程资源在段时间内被某个线程抢完,我们需要禁止暂停,包括进行延迟等等,这时候我们需要调用sleep静态方法。
以下实现每秒输出一下当前时间。
package com.heyexi;import java.text.SimpleDateFormat;
import java.util.Date;public class SleepThread
{public static void main(String[] args) throws InterruptedException{SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");while (true){String time = dateFormat.format(new Date().getTime());System.out.println("当前时间:"+time);Thread.sleep(1000);}}
}当前时间:22:27:38
当前时间:22:27:39
当前时间:22:27:40
当前时间:22:27:41
当前时间:22:27:42
线程礼让
线程礼让(yield)是一个线程把CPU的资源拿出来,给另一个线程使用,但是线程的调度是CPU自由调度的,所以虽然某个线程礼让,但是不一定礼让成功,也就是CPU没有掉其他线程,还是调用了礼让的那个线程。
Thread.currentThread().getName()是获取当前线程的名字
public class test
{public static void main(String[] args){yeild y = new yeild();new Thread(y,"AAA").start();new Thread(y,"BBB").start();}
}class yeild implements Runnable{@Overridepublic void run(){for (int i = 0; i < 10; i++){System.out.println(Thread.currentThread().getName()+"开始执行线程!");}Thread.yield();//礼让System.out.println(Thread.currentThread().getName()+"线程结束!");}
}输出
BBB开始执行线程!
AAA开始执行线程!
AAA开始执行线程!
BBB线程结束!
AAA线程结束!
但是线程一旦启动都是CPU来调度的,这个例子是CPU资源足够,所以礼不礼让都应该B线程能执行,礼让应该是在资源不够使用才需要使用。
强制执行线程join
这是一个比较强制的措施,就是多线程一般都是交叉执行的,那有时候我们需要有一个事务的过程,就是有先后顺序,比如必须是取款机密码输入正确它才能执行取钱的进程。
所以我们可以考试使用join,也就是等输入正确密码这个线程执行完后才能执行取款线程。
请看如下代码:
package com.sfc;public class test
{public static void main(String[] args){Thread thread1 = new Thread(new InputPwdThread());Thread thread2 = new Thread(new GetMoney());thread1.start();thread2.start();}
}class InputPwdThread implements Runnable{@Overridepublic void run(){for (int i = 0; i < 5; i++){System.out.println(Thread.currentThread().getId()+"输入了"+(i+1)+"次密码");}System.out.println("密码输入成功!");}
}
class GetMoney implements Runnable{@Overridepublic void run(){System.out.println(Thread.currentThread().getId()+"取钱成功!");}
}输出
15取钱成功!
14输入了1次密码
14输入了2次密码
14输入了3次密码
14输入了4次密码
14输入了5次密码
密码输入成功!
可以看到这不是我们想要的结果
所以我们需要先让输入密码的线程执行完
package com.sfc;public class test
{public static void main(String[] args) throws InterruptedException{Thread thread1 = new Thread(new InputPwdThread());Thread thread2 = new Thread(new GetMoney());thread1.start();thread1.join();//需要写在线程2执行之前thread2.start();}
}class InputPwdThread implements Runnable{@Overridepublic void run(){for (int i = 0; i < 5; i++){System.out.println(Thread.currentThread().getId()+"输入了"+(i+1)+"次密码");}System.out.println("密码输入成功!");}
}
class GetMoney implements Runnable{@Overridepublic void run(){System.out.println(Thread.currentThread().getId()+"取钱成功!");}
}
输出
14输入了1次密码
14输入了2次密码
14输入了3次密码
14输入了4次密码
14输入了5次密码
密码输入成功!
15取钱成功!
线程的状态
线程有六种状态,可以通过Thread.getState();
获取得到状态
初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
阻塞(BLOCKED):表示线程阻塞于锁。
等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
终止(TERMINATED):表示该线程已经执行完毕。
线程的优先级
线程有优先级,范围是1-10,如果我们不知道优先级,默认就是5,我们如果想让某个线程先执行,可以设置它的优先级,通过setPriority()
,方法来实现。
注意:需要先设置优先级,再执行start方法。
public class test
{public static void main(String[] args) throws InterruptedException{Thread thread1 = new Thread(new InputPwdThread());Thread thread2 = new Thread(new GetMoney());thread2.setPriority(8);thread1.setPriority(2);thread1.start();thread2.start();}
}class InputPwdThread implements Runnable{@Overridepublic void run(){for (int i = 0; i < 5; i++){System.out.println(Thread.currentThread().getId()+"输入了"+(i+1)+"次密码");}System.out.println("密码输入成功!");}
}
class GetMoney implements Runnable{@Overridepublic void run(){System.out.println(Thread.currentThread().getId()+"取钱成功!");}
}输出
15取钱成功!
14输入了1次密码
14输入了2次密码
14输入了3次密码
14输入了4次密码
14输入了5次密码
密码输入成功!
但这优先级也不是绝对,CPU也可能先调用优先级低的,只是大多时是先调用优先级高的。
用户线程和守护线程
线程可以分为用户线程和守护线程,这两个有什么区别呢?
JVM
需要确保用户线程被执行完,比如我们自定义的线程和主线程,但是守护线程是不用等待执行完的,比如垃圾回收线程,只要所有用户线程执行完了,就算守护线程没有执行完,程序也会结束。
那么默认的线程是用户线程,我们可以通过setDaemon()
方法为true,就变成了守护线程。
package com.sfc;public class test
{public static void main(String[] args) throws InterruptedException{Thread thread1 = new Thread(new InputPwdThread());Thread thread2 = new Thread(new GetMoney());thread2.setDaemon(true);thread1.start();thread2.start();}
}class InputPwdThread implements Runnable{@Overridepublic void run(){for (int i = 0; i < 3; i++){System.out.println(Thread.currentThread().getId()+"我是用户线程");}}
}
class GetMoney implements Runnable{@Overridepublic void run(){while (true){System.out.println("我是守护线程");}}
}
输出
我是守护线程
我是守护线程
我是守护线程进程已结束,退出代码 0
可以看到,在用户线程结束后,守护线程还运行了一会,没有立刻停止,不过也停止了。
并发
什么是并呢?很简单,就是用一个对象或者资源,同时被多个线程操作。
比如学校的选修课,一个选修课同时被多个学生去请求选课,就会造成并发,每个学生的手机都创建了一个用户进程,然后都去请求修改同一个选修课的名额,就造成了并发。
那如何解决并发问题呢?
这时候我们需要使用线程同步,也就是队列+锁。
队列就是我们把学生请求的进程作为多个队列,按队列一个一个来操作。
然后就是锁,锁就是当某个线程获得了资源,那么就需要保证该线程独占资源,其他线程都不能访问,只有当这个进程访问完资源后其他线程才能访问该资源。、
我们首先来模拟一下没有处理过的并发:
import lombok.SneakyThrows;
public class test
{public static void main(String[] args){GetCourse getCourse = new GetCourse();new Thread(getCourse, "何夜息").start();//同学1抢票new Thread(getCourse, "张三").start();new Thread(getCourse, "李四").start();new Thread(getCourse, "王五").start();}
}//模拟学生抢票
class GetCourse implements Runnable{private int courseNum = 10;//课程名额private boolean flag = true;//外部停止方式@SneakyThrows@Overridepublic void run(){while (flag){getCourse();}}//抢课private void getCourse() throws InterruptedException{if(courseNum<=0)//没有名额了{this.flag = false;}Thread.sleep(200); //暂停一会,否则被用同一个进程抢完了,模拟延时//名额减一courseNum--;System.out.println(Thread.currentThread().getName()+"拿到了票,剩余:"+courseNum);}
}输出
李四拿到了票,剩余:9
张三拿到了票,剩余:9
何夜息拿到了票,剩余:9
王五拿到了票,剩余:8
李四拿到了票,剩余:7
何夜息拿到了票,剩余:6
张三拿到了票,剩余:7
王五拿到了票,剩余:5
李四拿到了票,剩余:3
张三拿到了票,剩余:3
何夜息拿到了票,剩余:3
王五拿到了票,剩余:2
张三拿到了票,剩余:-1
李四拿到了票,剩余:-1
何夜息拿到了票,剩余:-1
王五拿到了票,剩余:-2
张三拿到了票,剩余:-3进程已结束,退出代码 0
可以看到,前三个人都拿到了票,但是票还剩余9张,这就不合理了。还有就是没有票了,但是进程太快,还是拿到了,这也是不合理的,如果是前优惠券或者红包之类的,那这就很危险了。
如何解决呢?我们可以使用同步方法,就是让对象排队去修改,只需要在方法前加上synchronize关键字,就声明该方法为同步方法。
package com.sfc;import lombok.SneakyThrows;public class test
{public static void main(String[] args){GetCourse getCourse = new GetCourse();new Thread(getCourse, "何夜息").start();//同学1抢票new Thread(getCourse, "张三").start();new Thread(getCourse, "李四").start();new Thread(getCourse, "王五").start();}
}//模拟学生抢票
class GetCourse implements Runnable{private int courseNum = 10;//课程名额private boolean flag = true;//外部停止方式@SneakyThrows@Overridepublic void run(){while (flag){getCourse();}}//抢课private synchronized void getCourse() throws InterruptedException{if(courseNum<=0)//没有名额了{this.flag = false;return;}Thread.sleep(200); //暂停一会,否则被用同一个进程抢完了,模拟延时//名额减一courseNum--;System.out.println(Thread.currentThread().getName()+"拿到了票,剩余:"+courseNum);}
}输出
何夜息拿到了票,剩余:9
何夜息拿到了票,剩余:8
何夜息拿到了票,剩余:7
何夜息拿到了票,剩余:6
何夜息拿到了票,剩余:5
何夜息拿到了票,剩余:4
何夜息拿到了票,剩余:3
何夜息拿到了票,剩余:2
何夜息拿到了票,剩余:1
何夜息拿到了票,剩余:0
进程已结束,退出代码 0
可以看到,确实是依次拿了,没有在抢票。
这里就有重点了,这个synchronized同步方法,锁的是this它本身的类,也就是这个方法所属的类,这里就是GetCourse
这个类,那其他类来执行时就锁不住了。
如果是其他对象来操作这个方法,我们就可以使用synchronized(cls){}代码块。
也就是我们指定这个对象修改完了,其他对象才能访问。
注意,加了同步修饰,效率就会变低,因为我们只能等前面的对象用完了后面的对象才能使用,所以同步代码块我们一般只需要加在对修改的时候使用,对读取肯定不需要加。
package com.sfc;import lombok.SneakyThrows;public class test
{public static void main(String[] args){GetCourse getCourse = new GetCourse();new Thread(getCourse, "何夜息").start();//同学1抢票new Thread(getCourse, "张三").start();new Thread(getCourse, "李四").start();new Thread(getCourse, "王五").start();}
}//模拟学生抢票
class GetCourse implements Runnable{private int courseNum = 10;//课程名额private boolean flag = true;//外部停止方式@SneakyThrows@Overridepublic void run(){while (flag){getCourse();}}//抢课private void getCourse() throws InterruptedException{synchronized (this){if(courseNum<=0)//没有名额了{this.flag = false;return;}Thread.sleep(100); //暂停一会,否则被用同一个进程抢完了,模拟延时//名额减一courseNum--;System.out.println(Thread.currentThread().getName()+"拿到了票,剩余:"+courseNum);}}
}
输出
李四拿到了票,剩余:9
李四拿到了票,剩余:8
李四拿到了票,剩余:7
李四拿到了票,剩余:6
李四拿到了票,剩余:5
李四拿到了票,剩余:4
李四拿到了票,剩余:3
李四拿到了票,剩余:2
李四拿到了票,剩余:1
李四拿到了票,剩余:0进程已结束,退出代码 0
这里我们指定对象为this,因为这个方法是在该对象的run方法中调用的,所以同步代码块和同步方法都解决了并发的问题。
死锁
什么死锁?
所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。
死锁产生的4个必要条件?
产生死锁的必要条件:
- 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
- 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
- 环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。
Reentrant lock(可重入锁)
这个也是同步解决并发的方法,而且我感觉比synchronized更简单好用一些,synchronized 你需要考虑对那个对象加锁,这个只需要把不安全的修改代码加个锁,等使用完了,再把资源解锁了就行。
package com.sfc;import lombok.SneakyThrows;import java.util.concurrent.locks.ReentrantLock;public class test
{public static void main(String[] args){GetCourse getCourse = new GetCourse();new Thread(getCourse, "何夜息").start();//同学1抢票new Thread(getCourse, "张三").start();new Thread(getCourse, "李四").start();new Thread(getCourse, "王五").start();}
}//模拟学生抢票
class GetCourse implements Runnable{private int courseNum = 10;//课程名额private boolean flag = true;//外部停止方式private final ReentrantLock lock = new ReentrantLock();//声明一个锁@SneakyThrows@Overridepublic void run(){while (flag){getCourse();}}//抢课private void getCourse() throws InterruptedException{//把不安全的代码进行加锁try{lock.lock();//开始加锁不安全的代码if(courseNum<=0)//没有名额了{this.flag = false;return;}Thread.sleep(100); //暂停一会,否则被用同一个进程抢完了,模拟延时//名额减一courseNum--;System.out.println(Thread.currentThread().getName()+"拿到了票,剩余:"+courseNum);} finally{lock.unlock();//这里对资源进行解锁}}
}
线程池
为什么要使用线程池,当我们需要使用多线程时,如果都是自己管理去管理,可能会造成性能降低,抢占资源等各种现象,所以我们可以一个线程池容器,来为我们管理这些线程。
我通过ExecutorService
来创建线程池,启动线程,关闭线程池。
package com.sfc;import lombok.SneakyThrows;import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantLock;public class test
{public static void main(String[] args){GetCourse getCourse = new GetCourse();//创建线程池服务ExecutorService service = Executors.newFixedThreadPool(10);//参数为线程池大小//执行线程,不用自己掉start()了service.execute(new Thread(getCourse, "何夜息"));service.execute(new Thread(getCourse, "张三"));service.execute(new Thread(getCourse, "李四"));service.execute(new Thread(getCourse, "王五"));//关闭线程池service.shutdown();}
}//模拟学生抢票
class GetCourse implements Runnable{private int courseNum = 10;//课程名额private boolean flag = true;//外部停止方式private final ReentrantLock lock = new ReentrantLock();//声明一个锁@SneakyThrows@Overridepublic void run(){while (flag){getCourse();}}//抢课private void getCourse() throws InterruptedException{//把不安全的代码进行加锁try{lock.lock();if(courseNum<=0)//没有名额了{this.flag = false;return;}Thread.sleep(100); //暂停一会,否则被用同一个进程抢完了,模拟延时//名额减一courseNum--;System.out.println(Thread.currentThread().getName()+"拿到了票,剩余:"+courseNum);} finally{lock.unlock();}}
}
输出
pool-1-thread-1拿到了票,剩余:9
pool-1-thread-3拿到了票,剩余:8
pool-1-thread-3拿到了票,剩余:7
pool-1-thread-3拿到了票,剩余:6
pool-1-thread-3拿到了票,剩余:5
pool-1-thread-3拿到了票,剩余:4
pool-1-thread-3拿到了票,剩余:3
pool-1-thread-3拿到了票,剩余:2
pool-1-thread-3拿到了票,剩余:1
pool-1-thread-3拿到了票,剩余:0进程已结束,退出代码 0
然后发现给线程取名字好像不行哦,都是人家帮你弄好了。
通俗易懂带你了解Java多线程处理相关推荐
- 两道面试题,带你解析Java类加载机制
2019独角兽企业重金招聘Python工程师标准>>> 在许多Java面试中,我们经常会看到关于Java类加载机制的考察,例如下面这道题: class Grandpa {static ...
- public接口可以被任何一个类实现_一文带你深入Java核心技术:对象克隆+接口与回调,还有这种操作...
对象克隆 当拷贝一个变量时,原始变量与拷贝变量引用同一个对象,如图6-1所示.这就是说,改变一个变量所引用的对象将会对另一个变量产生影响. Employee original = new Employ ...
- java基本数据类型_老杜带你学Java【第六课】
上期链接:老杜带你学Java[第五课] 01 写在前面 欢迎来到杜老师的「零基础学Java」课堂~今后,我们就是Java软件工程师了.(此处应该有掌声
- 带你了解Java这么火爆的真实原因!
这几年,中国的互联网行业进入了高速发展的阶段,同时IT行业,也成为了热门,备受追捧和关注的行业.在全球云计算和移动互联网的产业环境下,Java工程师异常火爆,受到众多人的追捧.Java工程师为何会如此 ...
- 一文带你了解Java的命名规范!
在编程的世界里,每种语言都有自己的一些规范.下面,小千带你了解Java命名规范.对于程序员来说,如果想学好一门语言,如果想要自己写出来的代码能被他人轻易地读懂,深入的学习命名规范是非常必要的一件事情. ...
- 一文带你了解Java Agent
转载自 一文带你了解Java Agent Java Agent这个技术,对于大多数同学来说都比较陌生,像个黑盒子.但是多多少少又接触过,实际上,我们平时用的很多工具,都是基于Java Agent实现 ...
- 一文带你理解Java中Lock的实现原理
转载自 一文带你理解Java中Lock的实现原理 当多个线程需要访问某个公共资源的时候,我们知道需要通过加锁来保证资源的访问不会出问题.java提供了两种方式来加锁,一种是关键字:synchron ...
- 第九十三期:带你聊聊 Java 并发编程之线程基础
百丈高楼平地起,要想学好多线程,首先还是的了解一下线程的基础,这边文章将带着大家来了解一下线程的基础知识. 作者:小九 01.简介 百丈高楼平地起,要想学好多线程,首先还是的了解一下线程的基础,这边文 ...
- java 获取ip地址_老杜带你学Java【第二课】
上期链接:老杜带你学Java[第一课] 01 写在前面 欢迎来到杜老师的「零基础学Java」课堂~今后,我们就是Java软件工程师了.(此处应该有掌声???)本专题为<零基础学Java>专 ...
最新文章
- underscore.js _.initial[Array]
- kaptcha图形验证码组件
- Apache部署网页-Ubuntu16.04
- fatal error LNK1169: 找到一个或多个多重定义的符号 的解决方案
- rails 放在 apache一个目录下面的配置方法
- IO流递归拷贝一个文件夹里面的所有文件,到另一个文件夹。如果重复不拷贝,可续拷
- 【matlab】找出数组中符合条件的数并赋值
- java生日正则表达式_Java语言十五讲
- [Unity] Unity3D研究院编辑器之自定义默认资源的Inspector面板
- 【批处理DOS-CMD命令-汇总和小结】-添加注释命令(rem或::)
- IPV4怎么转换成IPV6?
- ppt太大怎么压缩变小?ppt压缩方法和步骤
- 计算机网络故障的排除,计算机网络故障诊断与排除
- DOOM3源码分析相关文章集合
- eclipse中转换项目编码时没有GBK的问题
- 深圳面试一周记录——.NET(B/S)开发
- 不只在办公室写代码, 程序员的一天还可以是这样的!
- 【免费赠送源码】Springboot科研项目管理系统3lk11计算机毕业设计-课程设计-期末作业-毕设程序代做
- CUDA out of memory解决办法
- 联合应用开发 JAD