三道java新手入门面试题,通往自由的道路--多线程
作者:太子爷哪吒 发布时间:2023-05-24 23:12:51
1. 你知道线程安全问题吗?
线程安全问题:一般指在多线程模式下,多个线程对同一个共享数据进行操作时,第一个线程还没来得及更新共享数据,从而导致另外一个线程没得到最新的数据,并更新数据,从而产生线程安全问题。比较常见的场景有买票。
我举个例子吧:
需求:比如买周杰伦演唱会的门票,此时有三个窗口同时卖总共100张票。窗口就是线程对象,而100张票的资源,此时就相当于多个线程去抢占cpu的资源去抢对票的使用权。
嘿嘿,这是在大学时期和MyGirl去看滴,现在想去看也没办法了。话不多说,我们还是来看看代码吧:
public class SellTicketDemo {
public static void main(String[] args) {
// 创建线程任务对象
Ticket ticket = new Ticket();
//创建三个窗口对象
Thread thread = new Thread(ticket, "窗口1");
Thread thread2 = new Thread(ticket, "窗口2");
Thread thread3 = new Thread(ticket, "窗口3");
//同时卖票
thread.start();
thread2.start();
thread3.start();
}
}
// 创建Ticket实现Runnale
class Ticket implements Runnable {
private int ticket = 100; // 100张周杰伦演唱会门票
// 执行买票的逻辑
@Override
public void run() {
// 注意每个窗口都有卖票的权利
while (true) {
if (ticket > 0) { // 有票 可以卖
// 出票: 因为进来买票,总有出票,总会慢慢没票的吧
try {
// 这里采用sleep稍微等待下,模拟一下出票时间 。
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 获取当前线程对象的名字
System.out.println(Thread.currentThread().getName() + "正在卖:" + ticket--);
}
}
}
}
这里讲下: 我们创建了Ticket实现Runnable接口,并重写里面的run方法实现买票的功能,并定义了一个共享的变量。并在main方法中创建了三个线程去实现三个窗口买票的功能。最后我们来看看结果吧:
在你多运行几次,可以看到这样的现象:
卖出了不存在的票,比如0票与-1票,是不存在滴。而且这种情况根本不允许发生呀,谁会卖0张甚至-1张票呢。
出现多卖相同的票数,比如8和1这张票被卖了三回。那这就很过分了呦,一张票还可以卖三个人哈哈。
这些问题的发生就代表我们刚才是线程不安全的了。那线程安全问题具体是什么呢?我们可以总结得到:
是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
那可能发生线程安全问题的条件:
是否多线程环境下,单线程情况去对数据操作,当然是没什么问题滴啦!
是否存在共享变量,如上面的代码中,定义了一个全局变量ticket的演唱会门票,多个线程会共享这一变量。
是否存在多条语句操作共享数据,如上面的代码中,你在卖出票后,ticket变量肯定需要减少滴呀,所以对ticket进行了减的操作了。
2. 那如何解决线程安全问题呢?
我们可以引入线程同步解决线程安全问题。在上面卖票问题中,多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了同步机制(synchronized
)来解决。
即具体的解决思路是这样的:
1.窗口1线程进入买票的时候,窗口2和窗口3线程只能在外等着,此时是不能进行买票的操作的,只能等待窗口1买票完毕后,就进入到窗口1和窗口2和窗口3再次抢占cpu的资源去执行卖票功能。
2.也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。
而synchronized
关键字又给我们提供几种方法呢:
1.实现同步代码块:synchronized
关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
2.实现同步方法:使用synchronized
修饰的方法,就叫做同步方法,保证一个线程执行该方法的时候,其他线程只能在方法外等着。
public class SynchronizedDemo {
// 加在方法上 实际是对this对象加锁
private synchronized void synchronizedTest() {
}
// 同步代码块,锁对象可以是任意的,可以使用this,或者类.class对象,或者任意对象都可以
private void synchronizedTest2(){
synchronized (this){
}
}
// 加在静态方法上 实际是对类对象加锁
private synchronized static void synchronizedTest3() {
}
}
除了synchronized 关键字,还有Lock 锁,与此种方法需要自己定义锁的释放位置。
Lock lock = new ReentrantLock();
lock.lock(); // 自己定义开启锁位置
try {
System. out. println("我们获得了锁");
} catch (Exception e) {
} finally {
System. out. println("我们释放了锁");
lock.unlock();// 需要自己定义释放锁位置,不然会存在死锁问题。
}
那我们总结下:
我们怎么保证安全问题:
1.使用synchronized关键字,实现同步代码块或者同步方法。达到保证一个线程对资源操作的时候,其他线程只能等待。
2.用手动锁 Lock,使用Lock锁出现的位置可以相对比较灵活,但是必须有释放锁的配合动作。
两者的区别:
synchronized 它是Java中的关键字,而Lock是一个类,它是个接口一般我们会使用ReentrantLock来创建实例对象。synchronized 它可以修饰在类、方法、变量中,可以实现同步代码块,而ReentrantLock只适用于代码块锁。
synchronized 操作的应该是对象头中 mark word,而ReentrantLock 底层调用的是 Unsafe 的park 方法加锁。
ReentrantLock 必须手动获取与释放锁,而synchronized 不需要手动释放和开启锁。
两者都是可重入锁。可重入锁就是允许同一个线程多次获取同一把锁,如果某个线程已经获得某个锁,自己可以再次获取锁而不会出现死锁。而如果是不可锁重入的话,就会造成死锁。
3. 那你讲下死锁是什么吧?
死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程(线程)。
举个例子:你和你女朋友吵架,你们开始互掐头发,你揪着她秀长的长发,而她揪着你寸头,你们俩疼痛不比,但是彼此都是暴脾气,彼此发狠的说你放不放,不放我也不放,看先疼死了!此时你们互相观望着对方,喊着誓不放手。现在就相当产生了死锁现象,互相等待。
我们来简单演示下线程死锁的代码吧:
public class DeadlockDemo {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
new Thread(myRunnable, "看戏观众1看到").start();
new Thread(myRunnable, "看戏观众2看到").start();
}
}
class MyRunnable implements Runnable {
Object me = new Object();
Object myGirl = new Object();
@Override
public void run() {
synchronized (me) {
System.out.println(Thread.currentThread().getName() + "me:我要掐死你!你放不放呀");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "me:你放手我就放手!");
synchronized (myGirl) {
System.out.println(Thread.currentThread().getName() + "me:你倒是快放手呀!你不疼吗?");
}
}
synchronized (myGirl) {
System.out.println(Thread.currentThread().getName() + "myGirl:老娘我才要掐死你!你还不放?你今晚谁地板吧!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "myGirl:是你放手才对!");
synchronized (me) {// t1 , objB, 拿不到A锁,等待
System.out.println(Thread.currentThread().getName() + "myGirl:看来你今晚要睡地板啊");
}
}
}
}
先看看结果:
首先可以看到我们的进程还在运行状态,但是都不往下运行代码了,为什么呢,我们来分析来:
此时的状态是线程1在执行第二个synchronized (myGirl) 这个同步代码块里中,还在等待me放手的时候,此时线程2又进来了,执行了第一个synchronized (me)这一个同步代码块中,相当于把me这个锁锁住了。
而线程1此时想进入第二个synchronized (me)的时候,发现这个me的锁被人拿了就陷入等待状态,而线程2发现要进入到第一个synchronized (myGirl) 中,myGirl又被线程1拿住了,也陷入了等待状态。此时状态就是两个人在互相等待对方结束释放锁,陷入了无限等待的状态。
而产生死锁的必要条件有:
两个或两个以上的线程在执行过程中,因争夺资源而造成了互相等待的状态。
互斥条件:线程对于所分配到的资源具有排它性,即一个资源只能被一个线程占用,直到被该线程(进程)释放。
请求与保持条件:一个线程因请求被占用资源而发生阻塞时,对已获得的资源保持不放,即一个线程在已经有一个资源的资格后,又提出了新的资源请求。
不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
循环等待条件:当发生死锁时,所等待的线程必定会形成一个资源的环形链(类似于死循环),造成永久阻塞。
来源:https://blog.csdn.net/weixin_45766180/article/details/118164492


猜你喜欢
- 写在前面在平时的开发之中,我们需要对于数据加载的情况进行展示:空数据网络异常加载中等等情况现在设置页面状态的方式有多种,由于笔者近期一直在使
- 一、Java类的加载顺序引用1个网上的经典例子,并做稍许改动,以便大家更好地理解。public class Animal {
- 前言公司最近在开发中遇到一个问题,在弄帖子的发布与回复问题,然后再iOS端和Android端添加表情的时候都会出错Caused by: ja
- 前言本节主要介绍异步编程中Task、Async和Await的基础知识。什么是异步?异步处理不用阻塞当前线程来等待处理完成,而是允许后续操作,
- 首先在xml文件添加Spinner控件:<Spinner &n
- DateTime.Now.ToString("dddd",new System.Globalization.Cultur
- resultMap的Colum和property属性1: resultMap标签当我们的数据库字段与实体类的属性不一致时,就需要使用该标签进
- 谷歌官方推出了一种侧滑菜单的实现方式(抽屉效果),即 DrawerLayout,这个类是在Support Library里的,需要加上and
- 借用@Caching实现入参是基本类型的:@Caching(evict={@CacheEvict(value = Cache.CONSTAN
- 和大家一起分享一下学习经验,如何实现Android文件下载进度显示功能,希望对广大初学者有帮助。先上效果图:上方的蓝色进度条,会根据文件下载
- 找入口对 Spring 有一定基础的同学一定知道,请求入口是DispatcherServlet,所有的请求最终都会落到doDispatch方
- 本文实例为大家分享了Android下载进度监听和通知的具体代码,供大家参考,具体内容如下下载管理器关于下载进度的监听,这个比较简单,以apk
- 关于 Kotlin 开发使用 Kotlin 开发 Android App 在 Java 工程师群体中变得越来越流行。如果你由于某些原因错过了
- 最近项目中需要实现定时执行任务,比如定时计算会员的积分、调用第三方接口等,由于项目采用spring框架,所以这里结合spring框架来介绍。
- 下面对Java反射的基础知识进行说明和总结:首先定义一个MyBase类,其中有私有字段,也有公有字段。同时也有公有方法和私有方法。MyBas
- 前言:有时候我们在用Spring Aop面向切面编程,需要获取连接点(JoinPoint)方法参数名、参数值。环境:Mac OSXIntel
- 三态的基本概念1, 临时状态(Transient):也叫自由态,只存在于内存中,而在数据库中没有相应数据。用new创建的对象,它没有持久化,
- 一、基本定义Arrays类,全路径java.util.Arrays,主要功能为操作数组,Arrays类的所有方法均为静态方法,所以调用方式全
- 基于Java的简单的企业员工管理系统,供大家参考,具体内容如下首先创建了一个员工类定义员工应有的属性:工号、姓名、性别、职位、年龄、工资、部
- 获取整个屏幕的宽度的实例Point p = new Point();//获取窗口管理器WindowManager wm = (WindowM