1.多线程概述
进程:App,一个类文件
线程:App的功能,类里的一个方法
2线程的核心概念
- 线程就是独立的执行路径
- 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gc线程;
- main()称之为主线程,为系统的入口用于执行整个程序
- 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排带调度,调度器是与操作系统紧密相关的,先后顺序是不能人为干预的
- 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制
- 线程会带来额外的开销,如cpu调度时间,并发控制开销
- 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
2.1线程的三种创建方式
-
继承Thread类(重点)
class Test1 extends Thread{ //重写Thread类的run方法 @ov public void run(){ //重写的方法体 } }
-
实现Runnable接口(重点)
Runnable接口是一个函数式接口,只有一个run方法
class Test2 implements Runnable{ //实现Runnable接口的run方法 public void run(){ //重写的方法体 } }
-
实现Callable接口(了解)
/* * 实现callable接口创建多线程 * */ public class MyFirstCallable implements Callable<Boolean> { @Override public Boolean call() throws Exception { for (int i = 0; i < 200; i++) { System.out.println("我要记单词"+i); } return true; } public static void main(String[] args) throws ExecutionException, InterruptedException { MyFirstCallable call = new MyFirstCallable(); //创建执行服务 ExecutorService ser = Executors.newFixedThreadPool(3); //提交请求 Future<Boolean> result1 = ser.submit(call); //获取结果 boolean b = result1.get(); //关闭服务 ser.shutdownNow(); } }
2.2初识多线程迸发
当多个线程同时操作同一个对象时,线程不安全,数据紊乱
public class ThreadBurst implements Runnable {
private int stack = 10;
@Override
public void run() {
while(stack>0){
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
//Thread.currentThread():当前Thread, getName:获取线程name
System.out.println(Thread.currentThread().getName()+"抢到了第"+(stack--)+"张票");
}
}
public static void main(String[] args) {
ThreadBurst tb1 = new ThreadBurst();
new Thread(tb1,"线程1").start();
new Thread(tb1,"线程2").start();
new Thread(tb1,"线程3").start();
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k8WqneYP-1606389313117)(C:\Users\asus\AppData\Roaming\Typora\typora-user-images\image-20201110142656696.png)]
可以看到,在while循环stack不小于0的情况下,线程1,2分别抢到了第0和第-1张票,数据发生了紊乱
3.Lamda表达式
3.1 函数式接口
**定义:**任何接口,如果只包含唯一一个抽象方法,那么它就是一个函数式接口。
**作用:**避免匿名内部类定义过多
**实质:**函数式编程
//函数式接口
public interface Runnable{
public abstract void run();
}
对于函数式接口来说,我们可以通过lamda表达式来创建该接口的对象
//lamda表达式
la = (int a) ->{
System.out.println("我是lamda表达式"+a);
};
la.show(2);
//简化lamda表达式
la = (a) ->{
System.out.println("我是简化之后的lamda表达式"+a);
};
la.show(3);
//简化括号 //这里a的数据类型取决于函数接口里的数据类型
la = a ->{
System.out.println("我是继续简化之后的lamda表达式"+a);
};
la.show(4);
//当只有一条语句的时候,大括号也能省略
la = a -> System.out.println("一条数据语句全部省略"+a);
la.show(5);
3.2 匿名内部类
外部类 ——> 静态内部类 ——> 匿名内部类——>Lamda表达式——>简化Lamda表达式
匿名内部类没有类名,无法创建对象,也无法继承,
package com.hsf.JavaSE.Test;
public class Test1 {
public static void main(String[] args) {
//匿名内部类:必须通过接口或父类来实现创建
La la = new La(){
@Override
public void show() {
System.out.println();
}
};
}
}
//函数式接口
interface La{
public abstract void show();
}
4.静态代理
**静态代理模式:**真实对象和代理对象都要实现同一个接口,代理对象要代理真实对象
package com.hsf.JavaSE.Thread;
//测试类
public class StaticProxy {
public static void main(String[] args) {
//XX预约婚庆公司(创建婚庆公司实例):new Wedding()婚庆公司 ;new You()客户
Wedding we = new Wedding(new You());
we.HappyMarry();
// new Thread(()-> System.out.println("我爱你")).start();
}
}
//接口:结婚功能
interface Marry{
public abstract void HappyMarry();
}
//角色:youself
class You implements Marry{
@Override
public void HappyMarry(){
System.out.println("神父:新郎,你是否愿意这个女人成为你的妻子与她缔结婚约?无论疾病还是健康,无论贫穷还是富有,或任何其他理由,都爱她,照顾她,尊重她,接纳她,永远对她忠贞不渝直至生命尽头?");
System.out.println("我:去你MD,劳资作为要征服全世界女人的后宫王,这辈子不可能结婚的");
}
}
//婚庆公司:布置场景,婚前过程,收钱
class Wedding implements Marry{
//使用Marry对象是因为:但凡实现了Marry接口的对象都可以使用该(婚庆公司)构造函数,通俗点就是一个接待统一接待所有客户
//如果使用用户的对象,那么针对不同的对象就得创建不同的接待人员,这样会导致人力资源的浪费
private Marry target;
public Wedding(Marry target){
this.target = target;
}
@Override
public void HappyMarry() {
before();
this.target.HappyMarry();
after();
}
public void before(){
System.out.println("结婚之前,布置场景");
}
public void after(){
System.out.println("结婚之后,收钱");
}
}
5.线程停止
5.1线程的四大状态
- **新生期:**创建Thread对象
- **就绪状态:**调用start()方法进入就绪状态,此时需等待cpu的调度 (注意,不能对已经启动的线程再次调用start方法,否则会抛出异常)
- **执行状态:**线程获得cpu的调度后,线程就进入Running(执行状态)并自动调用run()方法执行方法体,当运行中的线程调用yeild()方法,则该线程会让出cpu的控制权,进入Running状态。但该线程有可能再次降到cpu的控制权继续执行
- **死亡状态:**当线程的run()方法执行完之后,或者被强制终止(如调用方法:stop(),destory()),或发生Error、Exception等,那么这个线程就处于死亡状态这个线程对象也许是活的,但是它已经不是一个单独执行的线程。如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。
6.线程休眠sleep
sleep()方法:sleep是Thread的一个静态方法,主要作用是阻塞线程的运行,使线程进入一定的休眠时间,时间到达后,线程重新进入执行状态,
每一个对象都有一把锁,sleep不会释放锁
package com.hsf.JavaSE.Thread;
import java.text.SimpleDateFormat;
import java.util.Date;
/*
* 模拟倒计时
* */
public class Sleep {
public static void main(String[] args) throws InterruptedException {
String time ;
boolean flag = true;
while(flag){
time = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss").format(new Date(System.currentTimeMillis()));
System.out.println(time);
Thread.sleep(1000);
}
}
}
7.线程礼让yield
**yield()方法:**yield()方法是Thread的一个静态方法,主要作用是让正在运行的线程重新进入到执行状态Running,礼让不一定成功,因为有可能我让了你下一次还是我先执行,主要还是看cpu的调度,这可能就是关系户吧
package com.hsf.JavaSE.Thread;
public class TestYield {
public static void main(String[] args) {
MyYield yield = new MyYield();
new Thread(yield,"a").start();
new Thread(yield,"b").start();
//lamda表达式
new Thread(()-> System.out.println("三玖天下第一")).start();
}
}
class MyYield implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"开始执行");
Thread.yield();//礼让,但不一定成功,看cpu的调度
System.out.println(Thread.currentThread().getName()+"执行完毕");
}
}
8.线程强制执行
**join()方法:**join()方法是Thread的一个静态方法,主要作用是强制占有cpu资源,使其他线程进入阻塞状态,可能,这就是vip用户吧
package com.hsf.JavaSE.Thread;
public class Testjoin implements Runnable {
public static void main(String[] args) throws InterruptedException {
//代理
Thread th = new Thread(new Testjoin());
th.start();
for (int i = 0; i < 500; i++) {
if(i==200){
th.join();//插队
}
System.out.println("我是主线程main"+i);
}
}
@Override
public void run() {
for(int i=0;i<100;i++) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我是线程vip,都给我让开" +i);
}
}
}
9.线程的状态
- NEW:初始状态,线程刚被构建,但是还没有调用start()方法
- RUNNABLE:运行状态,Java系统系统中将操作系统中的就绪和运行两种状态笼统地称为“运行中”;
- BLOCKED:阻塞状态,表示线程阻塞于锁
- WAITTING:等待状态,表示线程进入等待状态,进入该状态表示当前线程做出一些特定动作(通知或者中断)
- TIME_WAITTING:超时等待状态,该状态不同于等待状态,它可以在指定的时间后自行返回
- TERMINATED:中止状态,表示当前线程已经执行完毕
new Thread().getState():获取线程的当前状态
Thread.State:枚举类型,线程的状态
10.线程的优先级
线程的优先级是为了在多线程环境中便于系统对线程的调度,优先级越高先执行机会越大,并不是一定先执行。
线程的优先级可以理解为线程抢占CPU时间片的概率,并不能保证优先级高的线程一定会先执行。
不同的系统有不同的线程优先级的取值范围,同一个优先级在不同的系统里的值可能是不同的。
一个线程的优先级设置遵从以下原则:
线程创建时,子继承父的优先级。
线程创建后,可通过调用setPriority()方法改变优先级。
线程的优先级是1-10之间的正整数,线程优先级最高为10,最低为1,默认为5。
1- MIN_PRIORITY
10-MAX_PRIORITY
5-NORM_PRIORITY
package com.hsf.JavaSE.Thread;
/*
* 线程的优先级
* */
public class PriorityTest {
public static void main(String[] args) {
aa a = new aa();
aa a1 = new aa();
aa a2 = new aa();
Thread th = new Thread(a,"我是支线线程");
th.setPriority(10);
th.start();
System.out.println(Thread.currentThread().getName()+"-->优先级:"+Thread.currentThread().getPriority());
Thread th1 = new Thread(a1,"支线线程2");
th1.setPriority(1);
th1.start();
th1 = new Thread(a2,"支线线程3");
th1.setPriority(5);
th1.start();
}
}
class aa implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"-->优先级:"+Thread.currentThread().getPriority());
}
}
11.守护线程与用户线程
在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程)
用个比较通俗的比如,任何一个守护线程都是整个JVM中所有非守护线程的保姆:
只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。
Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器),它就是一个很称职的守护者。
User和Daemon两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果 User Thread已经全部退出运行了,只剩下Daemon Thread存在了,虚拟机也就退出了。 因为没有了被守护者,Daemon也就没有工作可做了,也就没有继续运行程序的必要了。
这里有几点需要注意:
(1) thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。
(2) 在Daemon线程中产生的新线程也是Daemon的。
(3) 不要认为所有的应用都可以分配给Daemon来进行服务,比如读写操作或者计算逻辑。
自定义守护线程(daemon)
package com.hsf.JavaSE.Thread;
/*
* 守护线程,虚拟机不用等守护线程结束
* */
public class DaemonTest {
//用户线程
public static void main(String[] args) {
God god = new God();
Thread th = new Thread(god);
th.setDaemon(true);//默认false,表示用户线程,true表示守护线程
th.start();
new Thread(new Youu()).start();
}
}
//上帝
class God implements Runnable{
@Override
public void run() {
while(true){
System.out.println("我是上帝我不死");
}
}
}
//你
class Youu implements Runnable{
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
System.out.println("生命倒计时->"+(100-i));
if(i==100){
System.out.println("AWSL");
}
}
}
}
12.同步方法及同步块
13.死锁
13.1概念:
多个线程各自占着一些共享资源,并且互相等待其他线程占有的资源释放才能运行,而导致两个或多个线程都在等待对方释放资源,都停止执行的情形,某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生死锁问题
//死锁:多个线程拿着对方需要的资源,都在等对方释放资源,形成阻塞
public class DeadLock {
public static void main(String[] args) {
Makeup makeup1 = new Makeup(0,"灰姑凉");
Makeup makeup2 = new Makeup(1,"时崎狂三");
makeup1.start();
makeup2.start();
}
}
//口红
class Lipstick{
}
//镜子
class Mirror{
}
//化妆
class Makeup extends Thread{
//需要的资源只有一份,用static来保证
static Lipstick lipstick = new Lipstick();
static Mirror mirror = new Mirror();
int choice;//选择
String girlName;//化妆的女孩名字
public Makeup(int choice,String girlName){
this.choice = choice;
this.girlName = girlName;
}
@Override
public void run(){
try {
//开始化妆
makeup();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//化妆的方法
private void makeup() throws InterruptedException {
if(choice==0){
//同步块:锁住口红,不让其他线程使用,等我使用完了,才释放锁,让别人可以使用
synchronized(lipstick){//获得口红的锁
System.out.println(this.girlName+"获得口红的锁");
Thread.sleep(1000);
//1s之后想获得镜子
synchronized(mirror){
System.out.println(this.girlName+"获得镜子的锁");
}
}
}else{
synchronized(mirror){
System.out.println(this.girlName+"获得镜子的锁");
Thread.sleep(2000);
synchronized(lipstick){
System.out.println(this.girlName+"获得口红的锁");
}
}
}
}
}
13.1 互斥锁
互斥锁的类型
-
普通锁(PTHREAD_MUTEX_NORMAL):互斥锁默认类型。当一个线程对一个普通锁加锁以后,其余请求该锁的线程将形成一个等待队列,并在该锁解锁后按照优先级获得它,这种锁类型保证了资源分配的公平性。一个线程如果对一个已经加锁的普通锁再次加锁,将引发死锁;对一个已经被其他线程加锁的普通锁解锁,或者对一个已经解锁的普通锁再次解锁,将导致不可预期的后果。
-
检错锁(PTHREAD_MUTEX_ERRORCHECK): 一个线程如果对一个已经加锁的检错锁再次加锁,则加锁操作返回EDEADLK;对一个已经被其他线程加锁的检错锁解锁或者对一个已经解锁的检错锁再次解锁,则解锁操作返回EPERM;
-
**嵌套锁(PTHREAD_MUTEX_RECURSIVE):**该锁允许一个线程在释放锁之前多次对它加锁而不发生死锁;其他线程要获得这个锁,则当前锁的拥有者必须执行多次解锁操作;对一个已经被其他线程加锁的嵌套锁解锁,或者对一个已经解锁的嵌套锁再次解锁,则解锁操作返回EPERM
-
**默认锁(PTHREAD_MUTEX_ DEFAULT):**一个线程如果对一个已经加锁的默认锁再次加锁,或者虽一个已经被其他线程加锁的默认锁解锁,或者对一个解锁的默认锁解锁,将导致不可预期的后果;这种锁实现的时候可能被映射成上述三种锁之一;
13.2死锁避免方法
死锁产生的四个必要条件:
- 互斥条件:一个资源每次只能被一个线程使用
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
我们只需要想办法打破其中的任意一个或多个条件就可以避免死锁的发生
14.Lock锁(可重入锁)
14.1 Lock概述
从jdk5.0开始,java提供了更强大的线程同步机制—通过显示定义同步锁对象来实现同步。同步锁使用Lock对象充当
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源的进行访问的工具,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显示加锁、释放锁
14.2 synchronized与Lock的区别
- Lock是显示锁(手动开启和关闭锁,别忘记关锁);synchronized是隐式锁,出了作用域自动释放
- Lock只有代码块锁,synchronized有方法锁和代码块锁
- 使用Lock锁,jvm将花费较少的时间来调度线程,性能更好,并且具有更好的扩展性(提供更多的子类)
- 优先使用顺序:Lock > 同步代码块 >(已经进入了方法体,分配了相应的资源) > 同步方法(在方法体之外)
15(扩展)线程安全问题思考:
15.1 什么是进程
:电脑中时会有很多单独运行的程序,每个程序有一个独立的进程,而进程之间是相互独立存在的。比如下图中的QQ、酷狗播放器、电脑管家等等。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iKIxdGVh-1606389313119)(C:\Users\asus\AppData\Roaming\Typora\typora-user-images\image-20201113091510739.png)]
15.2 串行和并行
:说起多线程,这里就引申出了两个概念-串行和并行,
-
**串行:**所谓串行,其实是相对单条线程去执行多个任务来说的,就拿下载文件来说,当我们下载多个文件时,在串行中他是按照一定的顺序去下载的,如哔哩哔哩,爱奇艺等视频软件缓存视频,你必须等待前一个视频下载完了,第二个视频才能开始下载,它们在时间上是不可重叠的[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0y8ybHIc-1606389313121)(C:\Users\asus\AppData\Roaming\Typora\typora-user-images\image-20201113092341966.png)]
-
并行:借上面的例子,下载多个文件时,开启多条线程同时进行下载,这里是严格意义上的,在同一时刻发生,并且在时间上是重叠的,如哔哩哔哩,爱奇艺等视频软件,当你开通了会员之后,你就可以同时下载多个视频,由此可见,这些视频软件针对缓存这一块的线程做出了限制,只有开通了会员之后,多线程的功能才会开放
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w06rMkqw-1606389313122)(C:\Users\asus\AppData\Roaming\Typora\typora-user-images\image-20201113092646811.png)]
15.3 什么是多线程?
:了解了这串行和并行之后,我们再来说说什么是多线程。举个例子,我们打开腾讯管家,腾讯管家本身就是一个程序,也就是说它就是一个进程,它里面有很多的功能,我们可以看下图,能查杀病毒、清理垃圾、电脑加速等众多功能。按照单线程来说,无论你想要清理垃圾、还是要病毒查杀,那么你必须先做完其中的一件事,才能做下一件事,这里面是有一个执行顺序的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ybmL4OUj-1606389313124)(C:\Users\asus\AppData\Roaming\Typora\typora-user-images\image-20201113093425857.png)]
如果是多线程的话,我们其实在清理垃圾的时候,还可以进行查杀病毒、电脑加速等等其他的操作,这个是严格意义上的同一时刻发生的,没有执行上的先后顺序。了解完这些之后,我们就需要去了解一个使用多线程不都不知道的问题----->线程安全
15.4 什么是线程安全?
既然是线程安全问题,那么毫无疑问,所有的隐患都是在多个线程访问的情况下产生的,也就是我们要确保在多条线程访问的时候,我们的程序还能按照我们预期的行为去执行,我们看一下下面的代码。
中…(img-0y8ybHIc-1606389313121)]
-
并行:借上面的例子,下载多个文件时,开启多条线程同时进行下载,这里是严格意义上的,在同一时刻发生,并且在时间上是重叠的,如哔哩哔哩,爱奇艺等视频软件,当你开通了会员之后,你就可以同时下载多个视频,由此可见,这些视频软件针对缓存这一块的线程做出了限制,只有开通了会员之后,多线程的功能才会开放
[外链图片转存中…(img-w06rMkqw-1606389313122)]
15.3 什么是多线程?
:了解了这串行和并行之后,我们再来说说什么是多线程。举个例子,我们打开腾讯管家,腾讯管家本身就是一个程序,也就是说它就是一个进程,它里面有很多的功能,我们可以看下图,能查杀病毒、清理垃圾、电脑加速等众多功能。按照单线程来说,无论你想要清理垃圾、还是要病毒查杀,那么你必须先做完其中的一件事,才能做下一件事,这里面是有一个执行顺序的。
[外链图片转存中…(img-ybmL4OUj-1606389313124)]
如果是多线程的话,我们其实在清理垃圾的时候,还可以进行查杀病毒、电脑加速等等其他的操作,这个是严格意义上的同一时刻发生的,没有执行上的先后顺序。了解完这些之后,我们就需要去了解一个使用多线程不都不知道的问题----->线程安全
15.4 什么是线程安全?
既然是线程安全问题,那么毫无疑问,所有的隐患都是在多个线程访问的情况下产生的,也就是我们要确保在多条线程访问的时候,我们的程序还能按照我们预期的行为去执行,我们看一下下面的代码。