线程基础(二)

本文探讨了Java中线程安全问题,包括在多窗口售票场景下可能出现的重复票和0票现象,并提出了三种解决方案:单窗口、有序多窗口以及使用synchronized和Lock。详细分析了synchronized和Lock的使用及其区别,并介绍了死锁的概念、产生条件及解决策略。最后,对比了Runnable和Callable接口以及synchronized和volatile关键字的不同。
摘要由CSDN通过智能技术生成

活动地址:CSDN21天学习挑战赛

一.线程的安全问题

1.1 需求

假设现在有个电影院,播放一部很欢迎的电影
《阿伟的故事》,那么我们该怎么去售票呢?

开一个窗口?,这样会不会卖的太慢了哇,那要是开多个窗口,就涉及到了数据交互的问题了,我们该怎么去解决?

在这里插入图片描述

1.1.1 第一种解决办法:

在这里插入图片描述

用一个窗口售票,这样一定是没有问题的

1.1.2 第二种解决办法

在这里插入图片描述

多窗口,但是是有序的,每个窗口售卖固定且分配好的的票数。
注意:

因为没有出现数据共享,所以这种情况也是没有任何问题的

1.1.3 第三种解决办法,实际情况

在这里插入图片描述
存在的问题:

1.出售重复的票
2.出售不存在票:0

需求分析

1.每一个窗口就是一个线程 使用线程来表示多窗口售票
2.只有出现数据共享的时候才会产生问题。
3.使用Runnable(或者callable)方式来创建线程,因为这两种方式可以创建多个线程
3.定义一个变量来表示票数
4.循环售票

线程创建类

public static class  MyRunnable implements  Runnable{
        private  int count =100;

        @Override
        public void run() {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //使用循环一直买票
            while (true){
                if (count > 0){
                    if (count > 0){
                        System.out.println(Thread.currentThread().getName()+"\t"+"正在购买第"+count+"张票");
                    }
                    count -- ;
                }
            }

        }
    }

测试类

 public static void main(String[] args) {
        //实例化MyRunnable对象
         MyRunnable runnable = new MyRunnable();
        //实例化线程
        Thread t1 = new Thread(runnable); t1.start();
        //实例化线程
        Thread t2 = new Thread(runnable); t2.start();
        // 实例化线程
        Thread t3 = new Thread(runnable); t3.start(); }

    }

结果
注意:结果黄黄在这里不予展示,自己测试的话多测试几次,最好多打开几个应用去测试。重复的票还算容易测试出来,0票非常难测试出来。

1.1.3.1 出现重复的票的原因

当线程一输出完这句话,或者正在输出这一句话的时候
在这里插入图片描述
这个时候线程二进来了,他也来输出这句话,即抢占资源
在这里插入图片描述
这时候,即线程一正在打印这句话,且还没有去执行
在这里插入图片描述
操作的时候,线程二进来了,也去打印。
故此,两者打印的票数相同。

1.1.3.2 出现0票的原因

同理,输出-1票的原因也很简单

1.当票数为1的时候,线程一抢到cup的执行权,执行打印语句
2.这时候线程二进来了,抢到了cpu的执行权,只是进入选择结构中,
3.线程一枪到了cpu执行权,执行count -- ;
4.线程二抢到cup的资源出售不存在的票

二.线程安全的解决办法

解决上述问题的根本:线程的操作只能让一个线程执行操作,一个线程执行完成操作之后,另一个线程才能执行操作。

2.1 解决办法一: synchronized锁对象

1.语法:
synchronized(锁对象){
可能产生问题的代码
}
2.注意点
A.锁的对象可以是任意的对象
B.所有线程锁的对象必须是同一个对象
.
3.作用:
解决数据共享安全的问题

线程定义类

public class two implements  Runnable {

    //定义一个变量来记录票数
    private    int count =100;
    //定义一个锁对象,可以是任意对象
    private  Object object =new Object();

    @Override
    public void run() {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //使用循环买票
        while(true){
            synchronized (object){
                if (count>0){
                    System.out.println(Thread.currentThread().getName() + "\t" + "two您购买的是第" + count + "张票");
                    count --;
                }
            }
        }
    }
}

测试类

 public static void main(String[] args) {
        //实例化MyRunnable对象
        two two = new two();
        //实例化线程
        Thread t1 = new Thread(two);
        t1.start();
        //实例化线程
        Thread t2 = new Thread(two);
        t2.start();
        // 实例化线程
        Thread t3 = new Thread(two);
        t3.start();
    }

结果
在这里插入图片描述
不难发现,上面的结果都是顺序输出的,也没有执行抢占,这是为什么呢

原因分析
在这里插入图片描述
总结:同步代码块,保证只有一个线程在此段时间内能够执行线程操作。

2.2 解决办法二:同步方法 (还是synchronize锁)

1.语法: 
访问修饰符 synchronized 返回值类型 方法的名称(参数列表) { 
			方法体 
			retrun 返回值 
} 
2.说明:同步方法可以是普通的成员方法 也可以是静态的方法 
3.说明:
	 A.成员方法的锁的对象就是this 
	 B.静态方法锁的对象是当前类的class对象

线程实现类

public class MyRunnable implements Runnable {

    private static int count = 100;

    @Override
    public void run() {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        while (true) {
            synchronized (MyRunnable.class) {
                if (count > 0) {
                    System.out.println(Thread.currentThread().getName() + "\t" + "您 购买是第" + count + "张票");
                    count--;
                }
            }
        }
    }

        public static synchronized void showInfo() {
            if (count > 0) {
                System.out.println(Thread.currentThread().getName() + "\t" + "您购买是 第" + count + "张票");
                count--;
            }
        }
    }

测试类

ublic static void main(String[] args) {
        //实例化MyRunnable对象
        MyRunnable two = new MyRunnable();
        //实例化线程
        Thread t1 = new Thread(two);
        t1.start();
        //实例化线程
        Thread t2 = new Thread(two);
        t2.start();
        // 实例化线程
        Thread t3 = new Thread(two);
        t3.start();
    }

测试结果:
在这里插入图片描述
依旧是有序的队列,且不发生抢占资源的情况。

2.3第三种解决办法:Lock锁

1.Lock 简介 
A.Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。 
B.此实现允许更灵活的结构 
2. 实现类ReentrantLock 
3.方法

注意两个方法

方法一:void lock() 获取锁
方法二:void unlock() 释放锁

线程类

public class MyRunnable implements Runnable {

    //定义一个变量来记录票数
    private int count = 1000;
    //实例化Lock锁对象
    private Lock l = new ReentrantLock();

    @Override
    public void run() {
        //获取锁对象
        l.lock();
        try {
            Thread.sleep(500);
            //使用循环一直买票
            while (true) {
                if (count > 0) {
                    System.out.println(Thread.currentThread().getName() + "\t" + "您购买是第" + count + "张 票");
                    count--;
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //释放锁对象
            l.unlock();
        }
    }
}

测试类

public static void main(String[] args) {
            //实例化MyRunnable对象
            MyRunnable two = new MyRunnable();
            //实例化线程
            Thread t1 = new Thread(two);
            t1.start();
            //实例化线程
            Thread t2 = new Thread(two);
            t2.start();
            // 实例化线程
            Thread t3 = new Thread(two);
            t3.start();
        }

结果
在这里插入图片描述

三.死锁:

3.1什么是死锁

1.死锁:
 A占用B的锁对象 B占用A的锁对象 A与B互不谦让 A与B出现同时等待的状态
 2.生活中: 
卢伟亮与黄自强在一个宿舍 宿舍中只有一双拖鞋 两人都想出去买东西 卢伟亮霸占左拖鞋 黄自强 霸占右拖鞋 互补谦让 都出不去

3.2四个必要条件才能产生死锁

产生死锁的四个必要条件
(1)互斥条件:进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源
(2)请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此事请求阻塞,但又对自己获得的资源保持不放
(3)不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放
(4)环路等待条件:是指进程发生死锁后,必然存在一个进程–资源之间的环形链

3.3解决死锁的方法

处理死锁的基本方法
1.预防死锁:通过设置一些限制条件,去破坏产生死锁的必要条件
2.避免死锁:在资源分配过程中,使用某种方法避免系统进入不安全的状态,从而避免发生死锁
3.检测死锁:允许死锁的发生,但是通过系统的检测之后,采取一些措施,将死锁清除掉
4.解除死锁:该方法与检测死锁配合使用

四.速记

4.1Runnable 和 Callable 的 区别?

=相同点==
1)两者都是接口
2)两者都可以用来编写多线程代码
3)两者都可以调用Thread.start()来启动线程
不同点==
1)最大的区别,Runnable没有返回值,而实现Callable接口的任务线程能返回执行结果
2)Callable接口实现类中的run方法允许异常向上抛出,可以在内部处理,try catch,但是Runnable
接口实现类中run方法的异常必须在内部处理,不能抛出
3)Runnable可以作为Thread构造器的参数,通过开启新的线程池来执行,也可以通过线程池来执行;
而Callable只能通过线程池来执行。

4.2 synchronized 和 volatile 的区别?

1)volatile是变量修饰符,而synchronized则可以修饰代码块或方法。
2)volatile不需要加锁,比synchronized更轻量级,不会阻塞线程;而synchronized加锁,会导致线程
阻塞(互斥性)
3)synchronized既能够保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性。
4)synchronized会执行编译优化(对字节码重新排序),编译优化有可能导致运行过程中异常。而
volatile禁止字节码重新排序,不会引发编译优化导致的异常。
原子性:一个操作不能被打断,要么全部执行完毕,要么不执行
可见性:一个线程对共享变量做了修改之后,其他的线程立即能够看到(感知到)该变量这种修改(变
化)。
并发编程三大特征:原子性。可见性,有序性
volatile作用:修饰变量,这个变量就会满足可见性与有序性,当线程修改这个变量的时候,别的线程
就会感知到这个变量进行了修改,

4.3 synchronized 和 Lock 的区别?

1)语法不同:
synchronized 是Java的关键字或修饰符,在jvm层面上,修饰方法或代码块。
Lock不是修饰符,是一个接口(加锁的工具类,该类提供很多方法加锁、释放锁等)tryLock()
unLock()
2)释放锁不同():
synchronized 获取锁的线程执行完同步代码,释放锁,且线程执行发生异常,jvm会自动让线程释
放锁(不管成功还是失败,都会自动释放锁)
Lock必须手动在finally中释放锁(必须手动释放锁)
3)死锁情况不同(
):
synchronized 在发生异常时候会自动释放占有的锁,因此不会出现死锁;
Lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生
4)锁判断不同:
synchronized 无法判断当前线程的上锁状态
Lock可以判断当前线程的上锁状态 tryLock unLock isLock
5)锁类型不同:
synchronized是可重入 不可判断 非公平
Lock:是可重入 可判断 可公平
公平性(线程等待时间长,优先获取锁)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值