Java多线程总结


深夜更博:)
本篇文章将对Java的多线程的相关知识点进行总结,可能会有稍许的遗漏,后续会进行补充。主要对如下知识点进行总结:

  • 线程的实现方式
  • 变量在线程中的使用
  • 线程的同步

线程的实现方式

继承Thread类

一个类只要继承了Thread类,并重写run()方法,则可以实现多线程的操作:
在这里插入图片描述
在这里插入图片描述
这种实现多线程的方式不多,因为Java中不支持多继承,这种实现多线程的方式在工作当中不常用

实现Runnable接口

一个类只要实现了Runnable接口,并重写run()方法,则就可以实现多线程操作(在Java中是可以实现多个接口的)
在这里插入图片描述
Runnable是个接口,而我们自己定义的MyThread1类是Runnable的实现,所以这是最典型的多态特性,父类引用指向子类的对象,父类的引用是Runnable,指向MyThread1

开发多线程的时候,只需要把业务逻辑扔到run方法里
在调用的时候注意调用的是start方法,不是run方法

两种启动方式的比较

继承Thread类和实现Runnable接口的比较:

  • 将我们希望线程执行的代码放到run方法中,然后通过start方法来启动线程,start方法首先为线程的执行准备好系统资源,然后再去调用run方法
  • 两种方法均需执行线程的start方法为线程分配必须的系统资源,调度线程运行并执行线程的run方法
  • 在使用的时候使用实现接口优先(避免单继承)
  • 实现Runnable接口的方式能够实现资源的共享

Thread的JDK源码分析

源码中Thread.java代码中的相关描述

这个thread在代码里面是一个线程的执行,Java虚拟机允许一个应用程序由多个线程来执行,支持并发
关于并发的解读(结合大数据):

  • 在Spark性能调优的时候,很关键的一个点就是提高并行度;其实并行度就是提高线程执行的数量:
    (指的都是在一个executor内)
    如果设置为1个core,也就意味着同时只有1个task进行跑
    如果资源够,设置为10个core,就会有10个线程进行跑
    即可以通过core的调整来提升并行度,从而对Spark进行调优
    每个线程都是有优先级的,优先级高的线程肯定比优先级低的线程要先被调度到
源码中关于start方法的相关描述

调用start的时候,线程就开始执行,JVM就开始调用thread的run方法
这样的结果就会有2个线程在同时运行:

  • 当前的线程(调用start方法)
  • 另外一个线程(执行run方法)
Thread构造方法
  • 当生成一个线程对象时,如果没有为其设定名字,那么线程对象的名字将使用如下形式:Thread-number,该number将是自动增加的(从0开始增加),并被所有的Thread对象所共享(因为它是static类型的成员变量)
  • 当使用继承Thread类来生成线程对象时,我们需要重写run()方法,因为Thread类的run()方法此时什么事情都没做(此时target==null,通过代码我们可以发现不会去做任何事情)
  • 当使用实现Runnable接口来生成线程对象时,我们需要实现Runnable接口的run()方法,然后使用new Thread(new MyThread())(我们这里假使MyThread已经实现了Runnable接口)来生成线程对象,这时的线程对象的run()方法就会调用MyThread类的run方法,这样我们自己编写的run()方法就执行了

变量在线程中的使用

成员变量

如果一个变量是成员变量,那么多个线程对同一个对象的成员变量进行操作时,他们对成员变量是彼此影响的(一个线程对成员变量的改变会影响到另外一个线程的操作)
代码如下:
在这里插入图片描述
运行结果(只截取一部分):
在这里插入图片描述

局部变量

如果一个变量是局部变量,那么每个线程都会有一个局部变量的拷贝(这个考察的是java中对象的分配,哪些在堆上面、哪些在栈上面;局部变量是每个对象里面都有的)
一个线程对该局部变量的改变是不会影响到其他线程的操作的

代码如下:
在这里插入图片描述

运行结果(只截取一部分):
在这里插入图片描述

线程的同步

以模拟银行存取款为案例,一步一步剖析线程的同步问题

问题案例演示&问题提出&根源分析

案例演示&问题提出

代码:
在这里插入图片描述
运行结果:
在这里插入图片描述
银行存款1000,每次都取款800;上述的运行结果就是典型的线程同步问题,开启了两个线程,两个线程都在操作同一个成员变量,导致该现象产生

问题根源

最终的结果是银行卡的存款为负值
问题产生的根源:多个线程操作共享数据或者操作共享数据的线程代码;当一个线程在执行操作共享的多条代码过程中,其它线程也参与到了相应的运算中来,这就会导致线程安全问题的产生,也就导致了上述现象的产生

解决方案:加锁

方案概述

在线程使用一个资源时为其加锁即可。访问资源的第一个线程为其加上锁以后,其他线程就不能再使用那个资源,除非被解锁。同一时间段内只能有一个线程进行,其他线程要等待此线程完成之后才可以继续执行
将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码时其他线程是不可以参与运算的,必须要当前线程把这些代码都执行完毕后,其他线程才可以参与运算
保证取钱和修改余额同时完成
加锁有2种方式:

  • 同步方法
    使用synchronized去修饰需要同步的方法
  • 同步代码块
    使用同步代码块,synchronized(obj){},还需要一个同步监听对象
方案一:同步方法(对对象加锁)

基于前面的代码,只需要做如下的改动就行(通过synchronized关键字来完成对对象的加锁):
在这里插入图片描述
不管执行多少次,都是固定的结果:
在这里插入图片描述
虽然结果为负的,但其实是对的:

  • 在get()方法加synchronized之前,执行的时候会出现2个-600的情况,见上述案例,产生这样的原因是:当线程1执行完1次之后,money为200,还没有进行打印输出,这时候线程2再去运算了一次,money为-600,执行完成之后,线程1与2的打印命令才进行执行,因此出现了2个-600的情况
  • 而这次产生这样的原因:线程1执行完之后money为200,之后线程2去执行,执行完之后money为-600,由于对get()方法加了synchronized进行修饰,因此不管执行多少次,输出的顺序都是200、-600
方案二:同步代码块(对对象加锁)

代码:
在这里插入图片描述
在这里插入图片描述
运行结果:
在这里插入图片描述和同步方法一样的效果
this --> 对当前对象加锁
问题: 如果要求使用代码块的方式对对当前对象的class加锁,怎么写? -> 类名.class

同步方法 vs 同步代码块
  • 同步方法是一种粗粒度的并发控制,某一时刻,只能有一个线程执行该synchronized方法
  • 同步代码块则是一种细粒度的并发控制,只有将块中的代码同步,位于方法内、synchronized块之外的代码是可以被多个线程同时访问到的

线程同步的关键知识点

synchronized关键字解读:将对象上锁

Java中每个对象都有一个锁/Lock/Monitor,当访问某个对象的synchronized方法时,表示将该对象上锁;此时,其它任意的线程都没法再去访问该对象的synchronized这个方法
只有当第一个线程执行完毕之后或者是抛出异常,那么才会将该对象的锁给释放
如果这把锁释放了,后面的线程就能进来继续访问了
synchronized关键词言下之意就是同一时刻,只有一个线程才可以访问到这个方法

案例1:对同一对象产生的线程,当两个不同线程进行访问时

代码:
在这里插入图片描述
在这里插入图片描述
运行结果:
在这里插入图片描述
结论: 由于是对同一对象产生的线程,当两个不同线程进行访问的时候,谁先进入synchronized方法就将该Bank对象上锁了,其他线程就没有办法再进入该对象的任何同步方法了,所以只有当一个线程执行完毕或者抛出异常后第二个线程才能进行访问

案例2:当两个线程对两个不同对象进行访问操作时

代码:
在这里插入图片描述
在这里插入图片描述
相比于之前的2个线程对应1个对象来说,以前的情况肯定是谁先抢到资源就谁先进行访问
现在有2个不同的对象(每次new出来的东西都是不同的),2个线程分别对应2个对象进行操作
问题: 这2个线程之间有没有先来后到的顺序?
答案: 没有必然的关系,即便使用了synchronized关键词进行了修饰也没有任何的联系,都是各自管各自的进行运行
2个Bank就是2个对象,各自线程对各自对象进行处理,即使你的方法是一个同步方法,两者之间也没有任何的联系

运行结果:
在这里插入图片描述
各自线程对应各自的银行,取款800,各自银行中的存款都剩为200
结论: 由于是两个线程对两个不同对象进行访问操作;那么这2个线程就没有任何关联,各自访问各自的对象,互不干扰

案例3:同一对象生成的两个不同的线程,当两个不同的线程访问同一对象不同的synchronized方法时

现在是1个对象对应2个线程,而这2个线程访问的是同一个对象的不同的synchronized方法的情况
代码:
在这里插入图片描述
2个不同的线程(调用同一个对象的不同synchronized方法):
在这里插入图片描述
Bank类(含有2个synchronized方法):
在这里插入图片描述
运行结果:
在这里插入图片描述
同一个对象所生成的两个不同的线程,该两个不同的线程访问同一个对象的不同synchronized方法时,谁先进入第一个synchronized方法,那么该线程就将该对象上锁了,其他线程是没有办法再对该对象的任何synchronized方法进行访问

synchronized + static:类级别锁

代码:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
运行结果:
在这里插入图片描述
一个一个线程有顺序的执行,只有当一个线程完全执行完毕之后,下一个线程才会继续执行下去
结论: 如果synchronized修饰的是static的方法,那么它是一个类级别的锁(是对当前对象的class对象加锁),只要拿到一个,就会依次执行完;
虽然是针对两个不同对象生成的不同的线程,但是由于synchronized方法使用了static关键字进行修饰,表示将该对象的Class对象加锁,所以只有等一个线程全部执行完毕后,其他线程才能进入访问

一个加static,一个不加static(笔试题)

在校招和社招的时候在笔试的时候,常见到的一道经典考题,就是会将对象级别的锁和类级别的锁给结合在一起考察
代码:
在这里插入图片描述
在这里插入图片描述
运行结果:
先打印world,然后才打印hello
结论: static是给当前对象的Class对象上锁,而没有static的是给当前对象上锁,两把锁锁的对象不同,所以相互之间并没有影响
先执行t1.start(),因为sleep了5s还没有及时输出;同时t2.start()也在执行,是没有sleep直接输出的;因此,最终的结果是先打印world,后打印hello

线程同步总结

synchronized修饰方法:

  • 非静态方法:默认的同步监听器对象是this
  • 静态方法:默认的同步监听器对象是该方法所在类的Class对象
  • 如果一个对象有多个synchronized方法,某一时刻某个线程已经进入到了某个synchronized方法,那么在该方法没有执行完毕前,其他线程是无法访问该对象的任何synchronized方法的
  • 如果某个synchronized方法是static的,那么当线程访问该方法时,它的锁并不是synchronized方法所在的对象,而是synchronized方法所在的对象所对应的Class对象,因为Java中无论一个类有多少个对象,这些对象会对应唯一一个Class对象,因此当线程分别访问同一个类的两个对象的两个synchronized static方法时,他们的执行顺序也是有顺序的,也就是说一个线程先去执行方法,执行完毕后另一个线程才开始执行

若线程是实现方式(Runnable):

  • 同步代码块:同步监听对象可以选this、这个方法所在类的Class对象、任一不变对象
  • 同步方法:此时可以使用synchronized直接修饰run方法,因为同步监听器是this

若线程是继承方式(Thread):

  • 同步代码块:同步监听器可以选用该方法所在类的Class对象、任一不变对象
  • 同步方法:此时不能使用synchronized直接修饰run方法
    总结:只要是继承方式,不论是同步代码块还是同步方法均不能使用this

同步的利弊:

  • 好处:解决了线程的安全问题
  • 弊端:相对降低了效率,因为同步外的线程的都会判断同步锁
  • 前提:同步中必须有多个线程并使用同一个锁
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值