三道java新手入门面试题,通往自由的道路--多线程

目录
  • 1. 你知道线程安全问题吗?
  • 2. 那如何解决线程安全问题呢?
  • 3. 那你讲下死锁是什么吧?
  • 总结

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方法中创建了三个线程去实现三个窗口买票的功能。最后我们来看看结果吧:

在你多运行几次,可以看到这样的现象:

  1. 卖出了不存在的票,比如0票与-1票,是不存在滴。而且这种情况根本不允许发生呀,谁会卖0张甚至-1张票呢。
  2. 出现多卖相同的票数,比如8和1这张票被卖了三回。那这就很过分了呦,一张票还可以卖三个人哈哈。

这些问题的发生就代表我们刚才是线程不安全的了。那线程安全问题具体是什么呢?我们可以总结得到:

是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

那可能发生线程安全问题的条件:

  1. 是否多线程环境下,单线程情况去对数据操作,当然是没什么问题滴啦!
  2. 是否存在共享变量,如上面的代码中,定义了一个全局变量ticket的演唱会门票,多个线程会共享这一变量。
  3. 是否存在多条语句操作共享数据,如上面的代码中,你在卖出票后,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锁出现的位置可以相对比较灵活,但是必须有释放锁的配合动作。

两者的区别:

  1. synchronized 它是Java中的关键字,而Lock是一个类,它是个接口一般我们会使用ReentrantLock来创建实例对象。synchronized 它可以修饰在类、方法、变量中,可以实现同步代码块,而ReentrantLock只适用于代码块锁。
  2. synchronized 操作的应该是对象头中 mark word,而ReentrantLock 底层调用的是 Unsafe 的park 方法加锁。
  3. ReentrantLock 必须手动获取与释放锁,而synchronized 不需要手动释放和开启锁。
  4. 两者都是可重入锁。可重入锁就是允许同一个线程多次获取同一把锁,如果某个线程已经获得某个锁,自己可以再次获取锁而不会出现死锁。而如果是不可锁重入的话,就会造成死锁。

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拿住了,也陷入了等待状态。此时状态就是两个人在互相等待对方结束释放锁,陷入了无限等待的状态。

而产生死锁的必要条件有:

两个或两个以上的线程在执行过程中,因争夺资源而造成了互相等待的状态。

  1. 互斥条件:线程对于所分配到的资源具有排它性,即一个资源只能被一个线程占用,直到被该线程(进程)释放。
  2. 请求与保持条件:一个线程因请求被占用资源而发生阻塞时,对已获得的资源保持不放,即一个线程在已经有一个资源的资格后,又提出了新的资源请求。
  3. 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  4. 循环等待条件:当发生死锁时,所等待的线程必定会形成一个资源的环形链(类似于死循环),造成永久阻塞。

总结

这篇文章就到这里了,如果这篇文章对你也有所帮助,希望您能多多关注我们的更多内容!

(0)

相关推荐

  • 每日六道java新手入门面试题,通往自由的道路--线程池

    目录 1. 你知道线程池吗?为什么需要使用线程池? 小结: 2. 你知道有多少种创建线程池的方式 3. 线程池的五种状态你有了解吗? 4. 你知道ThreadPoolExecutor的构造方法和参数吗 5. 你可以说下线程池的执行过程原理吗 6. 能否写一个简单线程池的demo? 总结 1. 你知道线程池吗?为什么需要使用线程池? 在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源. 而在Java中, JVM 中每创建和销毁线程就需要资源和时间的损耗了,

  • 每日六道java新手入门面试题,通往自由的道路--JVM

    目录 1. JVM是如何判断对象是否可回收 2. 你知道有什么垃圾回收的常见算法吗? 3. 你知道有什么垃圾收集器吗? 4. 那你知道什么时候才会触发Full GC 5. JVM中四种引用你有了解过吗? 6. 说说你知道的几种主要的JVM参数 1.堆设置 2.收集器设置 3.并行收集器设置 4.并发收集器设置 5.JVM 调优的参数 总结 1. JVM是如何判断对象是否可回收 垃圾收集器在做垃圾回收的时候,首先需要判断一个对象是存活状态还是死亡状态,死亡的对象将会被标识为垃圾数据并等待收集器进行

  • 每日几道java新手入门面试题,通往自由的道路

    目录 1.请你说一下什么是面向对象? 2.请你简述一下面向对象的三个基本特征? 3.为什么说 Java 是一种半解释半编译的程序设计语言呢? 4.请你说一下Java中的8大基本类型是那些? 5.请你讲讲抽象类和接口有什么区别? 6.请判断当一个对象被当作参数传递给一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递? 7.请你说一下JVM/JRE/JDK的区别? 8.请你说一下方法重载和方法重写的区别? 9.请你说一下List接口和Set接口的区别? 1

  • 三道java新手入门面试题,通往自由的道路--JVM

    目录 1. 你知道JVM内存模型吗? 2. 你知道重排序是什么吗? 3. happens-before是什么,和as-if-serial有什么区别 总结 1. 你知道JVM内存模型吗? 在Java的并发中采用的就是JVM内存共享模型即JMM(Java Memory Model),它其实是是JVM规范中所定义的一种内存模型,跟计算机的CPU缓存内存模型类似,是基于CPU缓存内存模型来建立的,Java内存模型是标准化的,屏蔽掉了底层不同计算机的区别. 那我们先来讲下计算机的内存模型: 其实早期计算机

  • 每日六道java新手入门面试题,通往自由的道路

    目录 1. 面试第一步,自我介绍. 2. 请你简述下对面向对象的理解,面向对象和面向过程的区别? 3. 那你再讲下面向对象的特征有哪些方面 4. 请问:我们是否可以继承 String类? 5. 我再问下final.finally.finalize这三者有什么区别吗? 6. 请讲下String和 StringBuilder.StringBuffer之间的区别? 总结 1. 面试第一步,自我介绍. 这个自我介绍,在整个面试当中可以说是第一步,如果你能把你想说的重点说出来,把面试官带到你准备好的技术点

  • 每日六道java新手入门面试题,通往自由的道路--多线程

    目录 1. 你可以讲下进程与线程的区别?为什么要用多线程? 2. 什么是上下文切换? 3. 说说你知道的几种创建线程的方式 4. 昨天你讲到创建线程后使用start方法去调用线程,为什么run方法不行呢?有什么区别? 5. 你知道你开启一个线程后,它的状态有那些吗? 6. 既然讲到超时方法,那你讲下sleep和wait的区别和他们需要怎样唤醒 总结: 1. 你可以讲下进程与线程的区别?为什么要用多线程? 进程:进程是程序的一次执行过程,是系统运行程序的基本单位. 线程:单个进程中执行中每个任务就

  • 每日六道java新手入门面试题,通往自由的道路第二天

    目录 1. 你可以讲下你对String类有什么其他的了解吗? 2. == 和 equals 的区别 3. String s= new String("nz")创建了几个字符串对象? 4. 你可以讲下JVM的运行时数据区或者说内存结构吗? 5. 类加载过程 6. 而其中类加载器是什么,那有哪些呢? 总结 1. 你可以讲下你对String类有什么其他的了解吗? 在看String的源码过程中,可以发现String 内部实际存储结构为 char数组,在String中有几个比较重要的构造函数:

  • 三道java新手入门面试题,通往自由的道路--锁+Volatile

    目录 1. 你知道volatile是如何保证可见性吗? 小结: 2. 悲观锁和乐观锁可以讲下你的理解吗? 3. 你还知道什么其他的锁吗? 总结 1. 你知道volatile是如何保证可见性吗? 我们先看一组代码: public class VolatileVisibleDemo { public static boolean initFlag = false; public static void main(String[] args) { new Thread(new Runnable() {

  • 三道java新手入门面试题,通往自由的道路--多线程

    目录 1. 你知道线程安全问题吗? 2. 那如何解决线程安全问题呢? 3. 那你讲下死锁是什么吧? 总结 1. 你知道线程安全问题吗? 线程安全问题:一般指在多线程模式下,多个线程对同一个共享数据进行操作时,第一个线程还没来得及更新共享数据,从而导致另外一个线程没得到最新的数据,并更新数据,从而产生线程安全问题.比较常见的场景有买票. 我举个例子吧: 需求:比如买周杰伦演唱会的门票,此时有三个窗口同时卖总共100张票.窗口就是线程对象,而100张票的资源,此时就相当于多个线程去抢占cpu的资源去

  • 三道MySQL新手入门面试题,通往自由的道路

    目录 1. 讲讲你认识MySQL锁吧 2. 你知道什么是事务.四大特性.隔离级别吗? 3. MyISAM 和 InnoDB 存储引擎的区别 总结 1. 讲讲你认识MySQL锁吧 对于数据库来讲,读写都是非常频繁的吧,在并发量来的时候,在进行读写操作时,可能会产生数据的不一致,这时候需要一些机制来保证访问的次序,所以锁就可以在一定限度保护它的一致性. 首先我们可以按锁的粒度分: 1.表级锁:它上锁是锁住的整张表,当下一个事务来访问的时候,必须等到当前事务把锁释放了,才能对表进行操作访问. 特点:表

随机推荐