何夜息随笔录-多线程的使用

进程和线程

首先需要分清进程和线程,进程是系统分配的单位,就是任务管理器可以看到的进程,这是由系统自动分配和管理的,每个进程都有进程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个必要条件?

产生死锁的必要条件:

  1. 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
  2. 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
  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多线程处理相关推荐

  1. 两道面试题,带你解析Java类加载机制

    2019独角兽企业重金招聘Python工程师标准>>> 在许多Java面试中,我们经常会看到关于Java类加载机制的考察,例如下面这道题: class Grandpa {static ...

  2. public接口可以被任何一个类实现_一文带你深入Java核心技术:对象克隆+接口与回调,还有这种操作...

    对象克隆 当拷贝一个变量时,原始变量与拷贝变量引用同一个对象,如图6-1所示.这就是说,改变一个变量所引用的对象将会对另一个变量产生影响. Employee original = new Employ ...

  3. java基本数据类型_老杜带你学Java【第六课】

    上期链接:老杜带你学Java[第五课] 01 写在前面 欢迎来到杜老师的「零基础学Java」课堂~今后,我们就是Java软件工程师了.(此处应该有掌声

  4. 带你了解Java这么火爆的真实原因!

    这几年,中国的互联网行业进入了高速发展的阶段,同时IT行业,也成为了热门,备受追捧和关注的行业.在全球云计算和移动互联网的产业环境下,Java工程师异常火爆,受到众多人的追捧.Java工程师为何会如此 ...

  5. 一文带你了解Java的命名规范!

    在编程的世界里,每种语言都有自己的一些规范.下面,小千带你了解Java命名规范.对于程序员来说,如果想学好一门语言,如果想要自己写出来的代码能被他人轻易地读懂,深入的学习命名规范是非常必要的一件事情. ...

  6. 一文带你了解Java Agent

    转载自  一文带你了解Java Agent Java Agent这个技术,对于大多数同学来说都比较陌生,像个黑盒子.但是多多少少又接触过,实际上,我们平时用的很多工具,都是基于Java Agent实现 ...

  7. 一文带你理解Java中Lock的实现原理

    转载自   一文带你理解Java中Lock的实现原理 当多个线程需要访问某个公共资源的时候,我们知道需要通过加锁来保证资源的访问不会出问题.java提供了两种方式来加锁,一种是关键字:synchron ...

  8. 第九十三期:带你聊聊 Java 并发编程之线程基础

    百丈高楼平地起,要想学好多线程,首先还是的了解一下线程的基础,这边文章将带着大家来了解一下线程的基础知识. 作者:小九 01.简介 百丈高楼平地起,要想学好多线程,首先还是的了解一下线程的基础,这边文 ...

  9. java 获取ip地址_老杜带你学Java【第二课】

    上期链接:老杜带你学Java[第一课] 01 写在前面 欢迎来到杜老师的「零基础学Java」课堂~今后,我们就是Java软件工程师了.(此处应该有掌声???)本专题为<零基础学Java>专 ...

最新文章

  1. underscore.js _.initial[Array]
  2. kaptcha图形验证码组件
  3. Apache部署网页-Ubuntu16.04
  4. fatal error LNK1169: 找到一个或多个多重定义的符号 的解决方案
  5. rails 放在 apache一个目录下面的配置方法
  6. IO流递归拷贝一个文件夹里面的所有文件,到另一个文件夹。如果重复不拷贝,可续拷
  7. 【matlab】找出数组中符合条件的数并赋值
  8. java生日正则表达式_Java语言十五讲
  9. [Unity] Unity3D研究院编辑器之自定义默认资源的Inspector面板
  10. 【批处理DOS-CMD命令-汇总和小结】-添加注释命令(rem或::)
  11. IPV4怎么转换成IPV6?
  12. ppt太大怎么压缩变小?ppt压缩方法和步骤
  13. 计算机网络故障的排除,计算机网络故障诊断与排除
  14. DOOM3源码分析相关文章集合
  15. eclipse中转换项目编码时没有GBK的问题
  16. 深圳面试一周记录——.NET(B/S)开发
  17. 不只在办公室写代码, 程序员的一天还可以是这样的!
  18. 【免费赠送源码】Springboot科研项目管理系统3lk11计算机毕业设计-课程设计-期末作业-毕设程序代做
  19. CUDA out of memory解决办法
  20. 联合应用开发 JAD

热门文章

  1. 店铺评分,提升DSR小技巧,宝贝描述,客服问题,发货与快递
  2. 基于ssm的招标投标系统
  3. 用proxyee-down快速下载百度网盘大文件
  4. 移动计算机类岗位综合知识测评,【浙江移动产品运营面试】一开始是笔试,普通的行测题+移动知识+性格测试。-看准网...
  5. Git怎么把SVN拍在沙滩上的?
  6. 为什么 Gi t把 SVN 拍在了沙滩上!
  7. 计算机网络——组建小型局域网
  8. C语言实现不带头结点的单链表逆置的三种方法
  9. 分析扁平线圈特斯拉模块电路
  10. SSL协议解析及SSL虚拟专用网