java多线程的安全问题与死锁(面向厕所编程)

大纲:java线程知识体系

这是不安全的问题代码

/*
Windows模拟的是售票窗口类
共享数据:多个线程共同操作的数据,即本案例中的tocketNum
 */
public class Windows implements Runnable {
    
    
    private static int ticketNum = 10;

    @Override
    public void run() {
    
    
        String name = Thread.currentThread().getName();
        while (true){
    
    
            if(ticketNum > 0) {
    
    
                try {
    
    
                    //这一步是为了演示错票,原理是当前线程进入了if语句陷入沉睡的时候票被卖光,
                    //然后当该线程苏醒时再来一次ticketNum--产生0号这个非法票
                    Thread.sleep(100);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                System.out.println(name +"卖出第" + ticketNum + "张票");
                ticketNum--;
            }
        }
    }
}
/*
在main中,windows可以理解为一个售票站点,总共十张票,这个站点开了三个售票窗口window1,window2,window3
到时间获后,用户涌入从这个三个窗口抢票。这是个不安全的售票程序,所以抢票过程中会出现重票,错票....
 */
class ThreadTest{
    
    
    public static void main(String[] args) {
    
    
        Windows windows = new Windows();
        Thread window1 = new Thread(windows);
        window1.setName("窗口1");
        Thread window2 = new Thread(windows);;
        window2.setName("窗口2");
        Thread window3 = new Thread(windows);
        window3.setName("窗口3");
        window2.start();
        window1.start();
        window3.start();
    }
}

最终会出现重票,错票,漏票或者三种情况都有
在这里插入图片描述
其中最极端的状况是卖出两张错票(卖出0号票和-1号票)
在这里插入图片描述
在ticketNum值为1的状态时,三个线程同时进入了if语句,最后输出1,0,-1
0和-1是错票,就是如上图分析的结果

线程安全问题是由共享数据造成的,就好比上公厕,如果每个坑位都是私人vip专属的那么就不会出现线程安全问题。但现实中无论多少人(线程)排队,只要你锁好厕门(资源锁)就不会出现线程安全问题,无论排队的多么着急都要一个个来。反之,你在一个生意爆满的公厕上厕所不锁门,然后三四个人(线程)涌入并同时操作一个坑位(共享数据)才真的会出问题…
解决方案是每个坑位都要有一把门锁,无论谁抢到坑位,第一时间先上锁防止别人进来打扰你,然后你就可以在上锁期间为所欲为。

方法一、同步代码块

class Windows implements Runnable {
    
    
    private static int ticketNum = 10;
    //这个位置才能保证锁的唯一性
    Object obj = new Object();
    @Override
    public void run() {
    
    
        String name = Thread.currentThread().getName();
        while (true){
    
    
            //这里也推荐用this(this代表的是main中的windows对象)或Windows.class
            synchronized (obj){
    
    
                if(ticketNum > 0) {
    
    
                    try {
    
    
                        //这一步是为了演示错票,原理是当前线程进入了if语句陷入沉睡的时候票被卖光,
                        //然后当该线程苏醒时再来一次ticketNum--产生0号这个非法票
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    System.out.println(name + "卖出第" + ticketNum + "张票");
                    ticketNum--;
                }
            }
        }
    }
}

同步方法二 :同步方法

/*
思路与同步代码块一致,同步方法是将操作共享数据的代码提取出来定义为synchronized类型的方法
同步方法默认用的同步锁是this(main中的windows对象),不需要显示声明
静态同步方法默认监视器:this
非静态同步方法监视器:class对象
 */
public class Windows implements Runnable {
    
    
    private static int ticketNum = 10;
    private Object obj = new Object();

    public synchronized void show(){
    
    
        String name = Thread.currentThread().getName();
        if(ticketNum > 0) {
    
    
            try {
    
    
                //这一步是为了演示错票,原理是当前线程进入了if语句陷入沉睡的时候票被卖光,
                //然后当该线程苏醒时再来一次ticketNum--产生0号这个非法票
                Thread.sleep(100);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println(name +"卖出第" + ticketNum + "张票");
            ticketNum--;
        }
    }
    @Override
    public void run() {
    
    
        while (true){
    
    
            show();
        }
    }
}

同步方法三 ReetrantLock锁

/*
synchronized vs ReetrantLock
共同点:二者都可以解决线程安全问题
不同:前者同步范围是固定的,需要执行完相应的同步代码块时自动释放锁资源,
而后者无论是上锁还是解锁都是需要手动指定的,这就意味着后者用起来更加灵活
 */
public class Windows implements Runnable {
    
    
    private static int ticketNum = 10;
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
    
    
       while (true){
    
    
           try {
    
    
               //代码运行到此处上同步锁
               lock.lock();
               String name = Thread.currentThread().getName();
               if(ticketNum > 0) {
    
    
                   try {
    
    
                       //这一步是为了演示错票,原理是当前线程进入了if语句陷入沉睡的时候票被卖光,
                       //然后当该线程苏醒时再来一次ticketNum--产生0号这个非法票
                       Thread.sleep(100);
                   } catch (InterruptedException e) {
    
    
                       e.printStackTrace();
                   }
                   System.out.println(name +"卖出第" + ticketNum + "张票");
                   ticketNum--;
               }
           } catch (Exception e) {
    
    
               e.printStackTrace();
           } finally {
    
    
               //解锁
               lock.unlock();
           }
       }
    }
}

死锁
两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程

例如:你(线程1)占据了坑位(s1)但你没纸(s2),外面的人(线程2)有纸(s2)但坑位(s1)被你占了,你打算让他给你纸你再给他坑位,但他要求你让出坑位再给你纸,大家都在等待对方先放弃,这样你俩都僵持住,无法完成"任务"。

/*
1 该案例基本每次运行都会死锁,死锁的运行结果就是控制台不进行任何输出
2 我们用sleep方法可以大大提高提高死锁概率,不加sleep也有可能发生死锁,只是概率很低
*/
public class DeadLockTest {
    
    
        public static void main(String[] args) {
    
    
            StringBuilder s1 = new StringBuilder();
            StringBuilder s2 = new StringBuilder();
            new Thread(new Runnable() {
    
    
                @Override
                public void run() {
    
    
                    synchronized (s1){
    
    
                        s1.append("a");
                        s2.append("1");
                        //CPU快速的线程切换和数据处理能力使得上锁解锁都是一瞬间完成,基本不会产生死锁,除非使用sleep先卡住上面的程序
                        //基于CPU的快速执行,我们在第一个线程t1中获取了锁s1时相应的t2获取了锁资源s2,但由于此时t1想要的s2在t2手中,
                        //t2想要的s1在t1手中,他们不会主动释放锁也得不到锁;造成死锁
                        try {
    
    
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
    
    
                            e.printStackTrace();
                        }
                        synchronized (s2){
    
    
                            s1.append("b");
                            s2.append("2");
                            System.out.println(s1);
                            System.out.println(s2);
                        }
                    }
                }
            }).start();

            new Thread(new Runnable() {
    
    
                @Override
                public void run() {
    
    
                    synchronized (s2){
    
    
                        s1.append("c");
                        s2.append("3");
                        synchronized (s1){
    
    
                            s1.append("d");
                            s2.append("4");
                            System.out.println(s1);
                            System.out.println(s2);
                        }
                    }
                }
            }).start();
        }
}

总结:
锁的使用: 同步方法<同步代码块<lock

分不清静态同步方法和普通同步方法的看我的这篇博客:Synchronized同步静态方法和非静态方法、同步块。同样的案例,同样的风格,通俗易懂;

同步方法vs同步代码块同步方法,它们所拥有的锁就是该方法所属的类的对象锁,换句话说,也就是this对象,如果是静态同步方法那么它的锁就是当前的运行时类对象,也就是当前类的class类型对象;而同步块可以更加精确的选择对象锁②同步方法使用关键字synchronized修饰方法而后者使用synchornize修饰代码块 ③同步块可以更精确的控制锁的作用域,锁的作用域就是从锁被获取到其被释放的时间。同步方法锁的作用域是整个方法,这可能导致其锁的作用域可能太大降低程序的运行效率

同步方法工作原理:因为同步方法的工作原理是基于java的每个对象都具有同步锁,调用同步方法的同时就会获取到到当前对象的同步锁,所以普通的同步方法默认使用this,所有的非静态同步方法用的都是同一把锁——实例对象本身。静态同步方法(static本身就不能和this扯上关系)默认使用当前类的class对象。如果获取不到相关的锁那么当前线程就会处于阻塞状态;普通同步方法锁住整个方法,静态同步方法锁住整个类(直接锁类对象相当于这把锁对全部的线程对象都有效)

同步代码块工作原理:被修饰的代码块相当于被加上了内置锁;实际开发中,我们们通常都会将共享资源的维护与操作单独定义成一个共享资源类(Clerk),该类中的方法必须是同步的才能有条不紊的被多线程访问,所以,这些共享方法中锁的都是当前资源类对象this =>不同的对象有各自的数据,为了统一管理我们就把clerk的获取设置为单例模式资源都是在run方法中调用共享资源类的对象的方法;
补充:synchronized只能修饰代码块和方法不能修饰构造器,因为构造器本身就是用来初始化对象的(分配空间 + 写入对象值),该过程由于JVM工作机制问题本身就类似于同步,所以不能被synchronized修饰

Lock vs 同步①同步方法全局只有一把锁和一把钥匙,最多能用两个线程(生产者消费者案例)协同工作,而lock更为强大,可以为每一个对象分配一把锁,当前线程执行完毕可以(根据钥匙condition)任意选择其中的线程将其唤醒并工作,也就是lock可以使n个线程顺序执行
②lock需要手动解锁unlock而同步方式是方法/代码块执行完自动解锁 =>lock方法更具有灵活性,lock是主流的同步方法,例如CurrentHashMap就是使用了lock技术

猜你喜欢

转载自blog.csdn.net/wwwwwww31311/article/details/113399955