线程基础
很不严谨的说,线程是什么?线程就是为了让很多个东西并发执行,大大的提高程序执行的效率啊
三个非常重要的概念:
- 程序:一组写好了的静态代码块(就我们写的那些代码玩意)
- 进程:正在进行着的程序,即静态的代码执行起来了
- 线程:是进程的小单元,或者说多个线程构成一个进程(线程的出现是因为进程满足不了人们的需求,于是进程被细化了)
线程的转换(五个较为简单的转换)可以通过这个图了解一下:
在Java中,
线程有三类:
- 主线程:系统线程,如Java中的Java虚拟机(主线程是最先执行的)
- 用户线程:Java中main函数执行的那些玩意
- 守护线程(精灵):比如Java中的GC(垃圾回收器,他是为主线程服务的,当系统启动后,GC就随之产生了;Java虚拟机断掉了,GC也就不干活了)
线程是操作系统层次的(又或者说是cpu层次的,进程怎么执行的,顺序是啥。。。这些都是靠cpu分配的)
不过线程怎么去用,Java的JDK已经给我们写好了
实现线程的过程(有两个方法):
- 自己描述一个类
- 实现线程的两个前提条件(继承
Thread
是最方便的,但是Java是单继承的,很可能会和其他的继承冲突;所以还可以通过实现Runnable
接口,不过它要额外的通过写一个Thread
类来执行start()
方法):- 方法一:继承一个父类
Thread
- 方法二:实现一个接口
Runnable
- 方法一:继承一个父类
- 我们都必须重写
run()
方法,
因为这个方法来源于cpu,是操作系统给JDK提供的接口,JDK将它包装成run()
,其实我们执行的线程就是写在run()
方法中的代码执行起来了 - new一个线程对象(我们是无法调用
run()
的,它只有cpu才可以调用,cpu分配时间碎片才能开始执行)
我们只能调用start()
方法(这个方法继承自Thread
),它会使这个线程除了cpu资源的其它资源都得到满足,让它进入就绪状态,在就绪队列中等待着cpu执行它
(补充:为什么要这么麻烦呢?为什么不直接使用Thread
类或者Runnable
接口,直接在里面写线程的代码,但是run()
方法是不允许有参数的,我们一般要执行的线程都要传参数,所以只能通过继承和实现来解决参数传递的问题)
【一个例子】实现线程的两种方式
- 继承一个父类 Thread,可以看到调用线程时写法较为简单
public class Running extends Thread {
String name;
public Running(String name){
this.name = name;
}
@Override
public void run() {
for(int i = 1; i <= 10; i++){
System.out.println(name + "跑到了" + i + "米");
}
}
public static void main(String[] args) {
Running Lux = new Running("Lux");
Running Ahri = new Running("Ahri");
Running Annie = new Running("Annie");
//从Thread类中继承过来的方法
Lux.start();
Ahri.start();
Annie.start();
}
}
- 实现一个接口 Runnable,可以看到调用线程时写法复杂一些
public class Running implements Runnable {
String name;
public Running(String name){
this.name = name;
}
@Override
public void run() {
for(int i = 1; i <= 10; i++){
System.out.println(name + "跑到了" + i + "米");
}
}
public static void main(String[] args) {
Running Lux = new Running("Lux");
Running Ahri = new Running("Ahri");
Running Annie = new Running("Annie");
//Thread类中才有start()方法
Thread thread1 = new Thread(Lux);
thread1.start();
Thread thread2 = new Thread(Ahri);
thread2.start();
Thread thread3 = new Thread(Annie);
thread3.start();
}
}
多次运行线程代码,发现线程无论执行顺序,还是开始执行时间都是随机的(实际上是操作系统通过算法调度和分配资源,JDK是不能影响它的)
我们只需要类中继承或实现线程,重写run()方法,最后调用start()方法就可以了
我们不要直接调用run()方法,这样就不是多线程了,只是按顺序执行的单线程!!!
模拟一个火车站售票小例子
- 使用控制台输入输出,仅仅是为了简单玩一下线程,例子很简陋
- 三个类
- 车票
- 售票系统12306
- 售票窗口
- 代码:github
- 效果展示:
从广州北站售出:[ 北京11 --> 深圳11 : 150.0 ]
从广州西站售出:[ 北京10 --> 深圳10 : 125.0 ]
从广州南站售出:[ 北京12 --> 深圳12 : 175.0 ]
从广州西站售出:[ 北京14 --> 深圳14 : 225.0 ]
从广州南站售出:[ 北京15 --> 深圳15 : 125.0 ]
...
...
对不起广州南站窗口车票已售完
对不起广州西站窗口车票已售完
对不起广州北站窗口车票已售完
多线程同时读取一个文件
- 题目:有一个26字节的文件 test.txt,里面按顺序存储着26个小写英文字母,使用3个线程同时读取
- 使用控制台输入输出,仅仅是为了简单玩一下线程与IO,例子很简陋
- 分析:
- 使用字节型输入流,使用skip()方法
- 1号线程:读取1-10字节
- 2号线程:读取11-20字节
- 3号线程:读取21-26字节
- 代码:github
- 效果展示:
Thread-0读取:a
Thread-2读取:u
Thread-1读取:k
Thread-2读取:v
Thread-0读取:b
Thread-2读取:w
...
...
生产消费者模型
生产消费者模型
我创建一个生产者向仓库添加物品
和两个消费者向仓库中拿走物品
在多线程并发进行中会出现这么一种问题:
如果仓库只剩一个物品时,出现了这么一个顺序
- 消费者1进行判断(还剩一个)
- 消费者2进行判断(还剩一个)
- 消费者1执行get方法
- get方法作用是:获取物品
- 消费者2执行get方法(这时候就出现问题了,虽然判断时仓库还有一个可以拿;但是到执行get方法时,仓库是空的,那么问题出现了:多线程并发抢夺资源)
- 生产者消费者模型:
- 模拟上述描述的问题,并且代码中不涉及其余线程操作,只会出现生产者消费者模型的问题
- 三个类:Producer、Consumer、WareHouse
- 分别代表:生产者、消费者、仓库
- 代码:github
- 不出所料,在执行的过程中出现问题了
两个消费者,成功演示出了线程安全问题;两个消费者并发访问,可能产生抢夺资源的问题;所以多个线程并发执行的时候,有安全隐患
synchronized
- 生产者消费者模型 解决办法:
- 让仓库被线程访问的时候,仓库对象被锁定(即仓库对象只能被一个线程访问,其它的线程处于等待状态)
- 使用一个特征修饰符即可
synchronized
,又称为线程安全锁,表示同步的意思,作用:一个时间点只有一个线程访问 - 给仓库提供的获取物品的方法(get)加上线程安全锁,可以避免多线程并发抢夺资源
synchronized 有两种写法:
- 将
synchronized
关键字,放在方法的结构上(特征修饰符),其实它锁定的是synchronized修饰方法所在的对象,由调用该方法的线程锁定
public synchronized void test(){代码}
- 将
synchronized
关键字,放在方法(/构造方法/块)内部
public void test(){
很多代码
synchronized(对象){ //只有方法执行到这里的时候才被锁定
//锁定哪个对象呢?可以由synchronized()的参数决定
某些代码
}
很多代码
}
第二种写法性能更加棒;而且更为灵活,给内部的synchronized(){}
的参数传对象,不仅可以由调用方法的线程锁定,还可由其他线程对象来锁定
线程对象改变引起的异常
-
如果线程调用的方法中有
wait()
和notify()
/notifyAll()
方法,但是没有加synchronized
线程安全锁时会出现一种异常:java.lang.IllegalMonitorStateExceptionr -
注意:wait和notify需要成对出现,或者wait和notifyAll成对出现,否则可能出现假死锁
- wait() 线程等待
- notify() 唤醒某个线程
- notifyAll() 唤醒所有线程
- wait() notify() notifyAll()都是Object的方法,但和
synchronized
关键字一样,也是哪个线程调用则对哪个线程起作用
在生产者消费者模型代码基础上进行修改,仓库的获取物品与存放物品的方法不加线程安全锁,并且使用线程等待与线程唤醒方法
这就是张冠李戴的字面解释了,当访问仓库的生产者线程等待,告知生产者的这一刹那,对象变成了另一个线程:该等待的没有等待,不该等待的等待了
join
- join()方法也是Thread类中的方法,挺有用的
- 可以让两个并行的线程变成单线程
线程创建好之后,我们不能控制线程执行的先后顺序,我们只能让线程进入就绪状态
【例子】首先来回顾一下线程并发执行的情况,以线程ThreadOne与ThreadTwo为例:
ThreadOne one = new ThreadOne();
ThreadTwo two = new ThreadTwo();
one.start();
two.start();
- one执行,one结束,two执行,two结束
- one执行,two执行,one结束,two结束
- one执行,two执行,two结束,one结束
- …
【例子】如果有两个线程ThreadOne和ThreadTwo,我想让ThreadTwo加入到ThreadOne中,可以这么写:
ThreadOne
public class ThreadOne extends Thread {
public void run() {
System.out.println("thread-one start");
ThreadTwo two = new ThreadTwo();
two.start();
try {
two.join();//线程2加入到线程1中
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread-one end");
}
}
ThreadTwo
public class ThreadTwo extends Thread{
public void run() {
System.out.println("thread-two start");
try {
Thread.sleep(5000);//睡眠5秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread-two end");
}
}
在main中,调用ThreadOne线程的start()方法:
-
这里用的是
join()
的无参方法,thread-one启动后,必须要等着thread-two执行完,才能往后面执行 -
join(millis)
还有一个有参重载方法
将ThreadOne类中的 two.join()
改为 two.join(4000)
thread-one启动后,不是必须等待着thread-two执行完,而是等待4000毫秒如果thread-two还没执行完,thread-one就继续往后执行
join源码结合例子分析
【join源码】
- 无参的
join()
方法:
public final void join() throws InterruptedException {
join(0);//还是会调用有参的join方法
}
- 有参的
join()
方法:
public final synchronized void join(long millis) throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) { //这就是调用无参join时的执行代码
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
- ThreadOne类中调用的无参
two.join()
时- 只要 ThreaTwo 活着,那么 ThreadOne 就一直是等待状态
while (two.isAlive()) {
two.wait(0);
}
- ThreadOne类中调用的有参且参数大于0
two.join(4000)
时- ThreaTwo 活着的期间,ThreadOne 等待millis毫秒后打破循环,即不再等待了
while (two.isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
two.wait(delay);
now = System.currentTimeMillis() - base;
}
join遇上线程安全锁
- 感受一下 synchronized 的强大吧
- 三个线程类:ThreadOne、ThreadTwo、ThreadThree
- ThreadOne 中调用 ThreadTwo,并
two.join(2000);
,不过 ThreadTwo 需要执行5秒 - ThreadTwo 中调用 ThreadThree,并将自身通过参数传给 ThreadThree
- ThreadThree 通过 synchronized 将 ThreadTwo 锁起来10秒
- 代码:github
- 执行效果:
- one启动
- two启动
- three启动
- two就join进one中了(two要执行5000毫秒,但是one只给了two2000毫秒)
- 2000毫秒之后,one想要把two从自己的线程中剔除掉;但是发现,two已经不在自己的手中,two已经被three锁定(要被锁定10000毫秒)
- one只能等待three将two释放后才能剔除掉two
所以,锁是非常强大滴
死锁
- synchronized非常厉害,一旦线程被锁定,不释放的情况,其它的线程都需要等待
- 如果锁没用好,有可能产生死锁的问题(互相都想要对方的资源,却都得不到满足)
这么说吧,
Ahri手里有a资源,然后需要b资源
Lux手里有b资源,然后需要a资源
而且,a资源被Ahri独占了,其它进程拿不到;b资源被Lux独占了,其它进程都拿不到
那么:
Ahri得不到Lux的b资源
Lux得不到Ahri的a资源
就会这样:
Ahri和Lux一直等着对方释放资源
这就是死锁
哲学家进餐问题
哲学家进餐问题 是一个著名的死锁问题:
四个人在餐桌上吃饭,但是只有四只筷子,
四个人同时拿筷子,
而且要求,每一个人先拿左手边的筷子,然后拿右手边的筷子
所以嘛,有可能他们四个人拿筷子的速度恰好一样,都拿了左手边的筷子,于是僵持住了
- 代码:github
避免死锁的方法:
- 礼让----产生时间差
- 不要产生共用的问题
有趣的Timer类
- 计时器/定时器是JDK已经写好了的线程类
java.util
包,Timer
类
【小例子】用Timer
类模拟一个短信轰炸的效果(每隔一段时间发送信息)
Timer有这么一个方法schedule()
,有四个重载方法
对上面出现的参数的解释
TimerTask task
表示一个任务,以字符串形式表示
TimerTask
是一个抽象类,new不了的,不过可以使用匿名内部类对象Date time
、Date firstTime
表示起始时间(到了起始时间,任务开始执行)long delay
表示延迟时间(及延迟给的时间后,任务开始执行)long period
周期,表示多长时间后再干一次,是一个循环
短信轰炸
//导包
//...
public class TestTimer {
private int count = 1;//记录轰炸次数
ArrayList<String> userBox = new ArrayList<>();//存储众多个人信息
{
userBox.add("a"); userBox.add("b"); userBox.add("c"); userBox.add("d");
}
启动一个小线程,记录时间,可以每隔一段时间去做一件事情
public void test() throws ParseException {
System.out.println("开始啦");
Timer timer = new Timer();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date firstTime = sdf.parse("2020-07-27 16:30:00");
timer.schedule(new TimerTask() {
public void run() {
System.out.println("第" + count++ + "次执行");
for(int i = 0; i < userBox.size(); i++){
System.out.println("给" + userBox.get(i) + "发送了一条消息:陌生人祝你幸福");
}
System.out.println("做了点坏事儿 真开心~~~");
}
},firstTime,3000);
}
public static void main(String[] args) {
TestTimer demo = new TestTimer();
try{
demo.test();
}catch (ParseException e){
e.getErrorOffset();
}
}
}