Java 多线程编程4---同步与死锁

一个多线程的程序如果是通过实现Runable接口实现的,实现类中的属性可以被多个线程共享,这样就造成一个问题,如果这个多线程程序操作同一资源时就有可能出现资源同步的问题。例如之前的买票程序,如果多个线程同时操作时,就有可能出现卖出的票为负数的问题。

问题的引出

下面通过Runable接口实现多线程,并产生3个线程对象,同时卖掉这5张票。
实例:有问题的买票程序

package my.thread.sync;

class MyThread implements Runnable
{
    //有5张票
    private int ticket = 5; 
    public void run()
    {
        for (int i = 0; i < 100; i++)
        {
            if (ticket > 0)
            {
                try
                {
                    Thread.sleep(1000);
                } catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"卖票掉一张,余票" + ticket--);
            }
        }
    }
}
public class SyncDemo01 {
    public static void main(String args[])
    {
        MyThread mt = new MyThread();
        Thread t1 = new Thread(mt,"售票员A"); 
        Thread t2 = new Thread(mt,"售票员B"); 
        Thread t3 = new Thread(mt,"售票员C"); 
        //启动3个线程进行买票
        t1.start();
        t2.start();
        t3.start();
    }
}

一种运行结果:

售票员B卖票掉一张,余票5
售票员C卖票掉一张,余票4
售票员A卖票掉一张,余票4
售票员B卖票掉一张,余票3
售票员C卖票掉一张,余票2
售票员A卖票掉一张,余票3
售票员B卖票掉一张,余票1
售票员C卖票掉一张,余票0
售票员A卖票掉一张,余票-1

从运行结果中看一共有5张票,但是却卖掉了9次,而且结果出现了余票为负数的情况。下面来分析查产生这样我问题的原因。
上面卖票的操作步骤:
(1)判断票数是否大于0,如果票数大于0,则表示还有票可以卖。
(2)如果可以卖票,就把余票减一

但是,我们在上面的代码中,加入了延迟操作,那么一个线程有可能还没来得及把票数减1,其他线程就已经把票数减1了,这样就有可能出现票数为负数的情况。

使用同步解决问题

这里有两种方式可以结局资源的同步问题,一种是使用同步代码块完成,一种是使用同步方法完成。

使用同步代码块

所谓代码块就是使用{}括起来的一段代码,根据其位置和声明的不同,可以分为普通代码块,构造块,静态代码块3中,如果在代码块前面加上synchronized关键字,则称该代码块为同步代码块。同步代码块的格式如下

synchronized(同步对象)
{
需要同步的代码;
}

从上面可以同步代码块的格式,可以看出,在使用同步代码块的时候必须指定一个需要同步的对象,一般都将当前对象this设置成同步对象。
实例:使用同步代码块解决上述买票问题
使用同步代码块把上面买票的if语句包裹起来即可,完整代码如下。

package my.thread.sync;

class MyThread implements Runnable
{
    //有5张票
    private int ticket = 5; 
    public void run()
    {
        for (int i = 0; i < 100; i++)
        {
            //使用同步代码块,同步对象设置为当前对象
            synchronized (this)
            {
                if (ticket > 0)
                {
                    try
                    {
                        Thread.sleep(1000);
                    } catch (InterruptedException e)
                    {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"卖票掉一张,余票" + ticket--);
                }
            }
        }
    }
}
public class SyncDemo01 {
    public static void main(String args[])
    {
        MyThread mt = new MyThread();
        Thread t1 = new Thread(mt,"售票员A"); 
        Thread t2 = new Thread(mt,"售票员B"); 
        Thread t3 = new Thread(mt,"售票员C"); 
        //启动3个线程进行买票
        t1.start();
        t2.start();
        t3.start();
    }
}

运行结果:

售票员A卖票掉一张,余票5
售票员A卖票掉一张,余票4
售票员A卖票掉一张,余票3
售票员C卖票掉一张,余票2
售票员C卖票掉一张,余票1

多次运行,不管你怎么运行,结果都是只卖掉5张票。
上面的代码将判断余票if (ticket > 0)和修改票数ticket--这两个操作进行了同步,所以不会出现多次卖票的情况。
这里一定要注意,同步代码块中一定要包括取值和修改值两个操作,如果单独同步一个操作,将不是同步,错误的代码如下:

package my.thread.sync;

class MyThread implements Runnable
{
    // 有5张票
    private int ticket = 5;
    public void run()
    {
        for (int i = 0; i < 100; i++)
        {
            // 使用同步代码块,同步对象设置为当前对象

            if (ticket > 0)
            {
                try
                {
                    Thread.sleep(1000);
                } catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
                synchronized (this)
                {
                    System.out.println(Thread.currentThread().getName()
                            + "卖票掉一张,余票" + ticket--);
                }
            }
        }
    }
}
public class SyncDemo01 {
    public static void main(String args[])
    {
        MyThread mt = new MyThread();
        Thread t1 = new Thread(mt, "售票员A");
        Thread t2 = new Thread(mt, "售票员B");
        Thread t3 = new Thread(mt, "售票员C");
        // 启动3个线程进行买票
        t1.start();
        t2.start();
        t3.start();
    }
}

运行结果:

售票员C卖票掉一张,余票5
售票员B卖票掉一张,余票4
售票员A卖票掉一张,余票3
售票员C卖票掉一张,余票2
售票员B卖票掉一张,余票1
售票员A卖票掉一张,余票0
售票员C卖票掉一张,余票-1

上面的同步代码块只同步了对票数减1的操作,而不同步票数判断的操作,所以达不到同步的效果。使用时一定要在把判断操作修改操作成对放入到同步代码块中个,不然达不到同步的效果。

使用同步方法

除了可以将需要的代码设置成同步代码块之外,还可使用synchronized关键字将一个方法声明成同步方法。声明同步方法的格式如下。

synchronized 方法返回值 方法名称(参数列表)
{
//方法体
}

实例:使用同步方法实现卖票的同步操作

package my.thread.sync;
class MyThread1 implements Runnable
{
    // 有5张票
    private int ticket = 5;
    public void run()
    {
        for (int i = 0; i < 100; i++)
        {
            saleTicket();
        }
    }
    public synchronized void saleTicket()
    {
        if (ticket > 0)
        {
            try
            {
                Thread.sleep(100);
            } catch (InterruptedException e)
            {
                e.printStackTrace();
            }

            System.out.println(
                    Thread.currentThread().getName() + "卖票掉一张,余票" + ticket--);
        }
    }
}
public class SyncDemo2 {
    MyThread1 mt=new MyThread1();
    Thread th1=new Thread(mt,"售票员A");
    Thread th2=new Thread(mt,"售票员B");
    Thread th3=new Thread(mt,"售票员C");
}

一次运行结果:

售票员A卖票掉一张,余票5
售票员C卖票掉一张,余票4
售票员C卖票掉一张,余票3
售票员C卖票掉一张,余票2
售票员B卖票掉一张,余票1

从程序的运行结果中可以发现,上面的代码完成了与之前同步代码块同样的功能。

使用同步代码块还是使用同步方法

同步代码块,同步方法,静态同步方法使用的锁

  • 同步代码块使用的锁是任意的对象。
  • 同步方法使用的锁是当前对象this
  • 使用static关键字修饰的静态同步方法使用的是该类所在的字节码文件对象,格式为类名.class。

同步代码块和同步方法的区别

  • 同步方法默认用this或者当前类class对象作为锁;
  • 同步代码块可以选择加锁的对象;
  • 同步代码块比同步方法要更细颗粒度,我们可以选择只同步会发生同步问题的关键代码而不是整个方法

使用同步代码块还是使用同步方法比较好

  同步方法直接在方法上加synchronized实现加锁,同步代码块则在方法内部加锁,很明显,同步方法锁的范围比较大,而同步代码块范围要小点。
  而且同步是一个开销很大的操作,因此尽量减小同步的区域。所以通常没有必要同步整个方法,使用同步代码块同发生同步问题的关键代码即可。
所以考虑性能,最好使用同步代码块从而减少锁定范围以提高并发效率。

死锁

同步可以保证资源共享操作的正确性,但是过多同步也会产生问题,例如会产生死锁。所谓死锁,就是指两个线程都在等待彼此先完成,造成程序的卡着无法往下运行。一般死锁都是在程序运行时出现的,发生在两个线程相互持有对方正在等待的东西(实际是两个线程共享的东西)。只要有两个线程和两个对象就可能产生死锁。
实例:死锁例子

package my.thread.deadlock;

public class DeadLock implements Runnable {
    public String name;
    public boolean flag;
    // 静态对象是类的所有对象共享的
    private static Object object1 = new Object();
    private static Object object2 = new Object();
    @Override
    public void run()
    {
        System.out.println("flag=" + flag);
        if (flag)
        {
            // 同步代码块1
            synchronized (object1)
            {
                System.out.println(Thread.currentThread().getName()
                        + "成功持有object1对象的锁,成功进入同步代码块1中");
                try
                {
                    System.out.println(
                            Thread.currentThread().getName() + "睡眠500毫秒");
                    Thread.sleep(500);
                } catch (Exception e)
                {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()
                        + "睡眠结束,正在获取object2对象的锁...");
                // 同步代码块2
                synchronized (object2)
                {
                    System.out.println(Thread.currentThread().getName()
                            + "成功持有object2对象的锁,成功进入同步代码块2中");
                    System.out.println("1");
                }
            }
        }
        if (!flag)
        {
            // 同步代码块3
            synchronized (object2)
            {
                System.out.println(Thread.currentThread().getName()
                        + "成功持有object2对象的锁,成功进入同步代码块3");
                try
                {
                    System.out.println(
                            Thread.currentThread().getName() + "睡眠500毫秒");
                    Thread.sleep(500);
                } catch (Exception e)
                {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()
                        + "睡眠结束,正在获取object1对象的锁...");
                // 同步代码块4
                synchronized (object1)
                {
                    System.out.println(Thread.currentThread().getName()
                            + "成功获取object1对象的锁,成功进入同步代码块4中");
                    System.out.println("0");
                }
            }
        }
    }

    public static void main(String[] args)
    {

        DeadLock A = new DeadLock();
        DeadLock B = new DeadLock();
        A.flag = true;
        B.flag = false;
        new Thread(A, "线程A").start();
        new Thread(B, "线程B").start();

    }
}

运行结果:

《Java 多线程编程4---同步与死锁》

分析

  1. 线程A启动,由于A对象的flag为true,且此时object1对象的锁还没有任何被线程持有,所以线程A就马上持有object1对象的锁,然后进入同步代码块1中去执行里面的代码,然后线程A睡眠500毫秒。
  2. 然后线程B启动,由于B对象的flag为false,且此时object2对象的锁还没有被任何线程持有,所以线程B很愉快的持有object2对象的锁,然后进入同步代码块3中去执行里面的代码,然后线程B睡眠500毫秒。
  3. 线程A睡眠结束后,就需要进入同步代码块2中去执行,此时就需要持有object2对象的锁,但是由于线程B还没走出同步代码块3中,也就是说object2对象的锁还被线程B持有。只有等到线程B执行执行完毕同步代码块3中的代码,线程B才会释放object2对象的锁。所以,此时线程A无法获取到object2对象的锁,线程A要等待线程B执行完同步代码块3中的所有代码,然后把object2的锁释给线程A。
  4. 线程B睡眠结束后,想要进入同步代码块4中去执行,此时就需要持有object1对象的锁,但是此时线程A还在同步代码块中等待线程B释放object2给它,所以线程A没有执行完同步代码块1中的内容,线程A将继续占有object1对象的锁。所以线程B需要等待线程A执行完同步块1中的所有代码,然后吧object1对象的锁释放给线程B.
  5. 好的,现在的解说是,线程A等着线程B执行完毕释放object2对象的锁,而线程B也在等待线程A执行完毕释放object2对象的锁。线程A等待线程B,线程B等待线程A。线程A和线程B相互等待,这样造成了死锁。

    产生死锁的四个必要条件

    虽然进程(线程)在运行过程中,可能发生死锁,但死锁的发生也必须具备一定的条件,死锁的发生必须具备以下四个必要条件。
    1.互斥条件
    指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程(线程)占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
    2.请求和保持条件
    指进程(线程)已经持有至少一个资源,但又提出了新的资源请求,而该资源已被其它进程(线程)占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
    3.不可剥夺条件
    指进程(线程)已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
    4.循环等待条件
    指在发生死锁时,必然存在一个相互等待的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个被P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源,也就是若干进程之间形成一种头尾相接的循环等待资源关系。
    分析上述的代码,使用使用了同步代码块,就满足了1.互斥条件,2.请求与和保持条件,3.不可剥夺条件。此时在使用两个相互嵌套的同步代码块,
    第一个嵌套的同步代码块的锁对象由外到内的顺序是:object1,object2.
    第二个嵌套的同步代码块的锁对象由外到内的顺序是:object2,object1.
    进入第一个嵌套的同步代码块中的线程A必然持有object1对象的锁了,之后他想要持有object2对象的锁。
    而进入第二个嵌套的同步代码块的线程B必然持有object2对象的锁了,之后他又想要持有object1对象的锁。
    这样就造成了 线程A等待线程B,线程B等待线程A 这就产生死锁的第四个条件:循环等待,所以上面程序出现了死锁。

死锁的避免

上面四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
参考博客https://blog.csdn.net/silence723/article/details/52036609

    原文作者:java锁
    原文地址: https://blog.csdn.net/qq_21808961/article/details/81043760
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞