欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-05-22 11:20:51
...

一个多线程的程序如果是通过实现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