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

java多线程(二)多线程同步与死锁

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

导读

该章主要围绕同步来介绍实现多线程由于共享资源出现的安全问题使用同步解决安全问题,由同步而产生的死锁问题。三个方面来介绍。

1. 多线程安全问题

举个例子:比如我卖100张票,通过三个窗口卖,那么可以用如下代码实现。

public class MultipleThread {
    public static void main(String[] args) {
        SingleThread st = new SingleThread();
        Thread t1 = new Thread(st);
        Thread t2 = new Thread(st);
        Thread t3 = new Thread(st);
        t1.start();
        t2.start();
        t3.start();

    }
}
class SingleThread implements Runnable {
    private int totalTicket = 100;

    @Override
    public void run() {
        while (true) {
            if (totalTicket > 0)
                               try {
                    Thread.sleep(10);
                } catch (Exception e) {
                }
                System.out.println(Thread.currentThread().getName() + " sell ticket " + totalTicket--);
            }
        }
    }
}
/*
Thread-0 sell ticket 1
Thread-2 sell ticket 0
Thread-1 sell ticket -1*/

这个程序可以通过三个窗口卖完100个票,但是可能出现问题是,比如我们卖到只剩一张票时,一个窗口卖了这张的时候,还没卖完,另一个窗口查系统的时候看到还剩一张,就也卖这张票,这时就可能出现一张票被多次售卖了。因为有共享数据,这就多线程出现了安全问题。
多线程安全问题原因:当多条线程同时操作一个共享数据时,一个线程还没执行完,另一个线程就也进来执行共享数据,就可能会出现共享数据错误。
解决方法:一个线程操作该共享数据时,比如ticket= 1,其他线程不能进来操作这个ticket,当该线程执行完之后,其他线程才能执行。

java提供的解决方法:使用同步

synchronized(对象)
{
    需要同步的代码(操作共享数据的代码)
}
public class MultipleThread {
    public static void main(String[] args) {
        SingleThread st = new SingleThread();
        Thread t1 = new Thread(st);
        Thread t2 = new Thread(st);
        Thread t3 = new Thread(st);
        t1.start();
        t2.start();
        t3.start();
    }
}
class SingleThread implements Runnable {
    //共享变量
    private int totalTicket = 100;
    //共享对象
    private Object o = new Object();
    @Override
    public void run() {
        while (true) {
            synchronized (o) {//同步必须是同一个对象
                if (totalTicket > 0) {
                    try {
                        Thread.sleep(10);
                    } catch (Exception e) {
                    }
                    System.out.println(Thread.currentThread().getName() + " sell ticket " + totalTicket--);
                }
            }
        }
    }
}

2. 同步详解

同步的前提:

  • 必须两个以上的线程
  • 多个线程使用同一个锁
  • 操作共享变量

同步的好处:解决了多线程安全问题。
弊端:每次都需要判断锁,会消耗资源

银行存钱案例:

介绍:银行有个总账,不同的人来存钱后会计入总账。假设有三个客户,每人每次来存100元,存三次,总账记录900元。
实现代码:

public class MultipleThread {
    public static void main(String[] args) {
        Customer customer = new Customer();
        Thread t1 = new Thread(customer);
        Thread t2 = new Thread(customer);
        Thread t3 = new Thread(customer);
        t1.start();//客户1跑线程1
        t2.start();
        t3.start();
    }
}

//银行总账类
class Bank {
    private int moneySum = 0;

    void addMoney(int n) {
        moneySum = moneySum + n;
        try {
            Thread.sleep(10);
        } catch (Exception e) {
        }
        System.out.println("sum = " + moneySum);
    }
}

//用户类:因为要创建多个用户执行多线程,所以该类实现Runnable
class Customer implements Runnable {
    //此对象为多线程共享对象
    private Bank bank = new Bank();
    //存钱,存三次
    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName() + " save money:");
            bank.addMoney(100);
        }
    }
}
/*
Thread-0 save money:
Thread-1 save money:
Thread-2 save money:
sum = 300
Thread-0 save money:
sum = 300
Thread-1 save money:
sum = 500
Thread-2 save money:
sum = 600
Thread-1 save money:
sum = 700
Thread-0 save money:
sum = 800
Thread-2 save money:
sum = 900
sum = 900
sum = 900
 */

这时我们发现,操作共享数据出现问题,打印出了错误的银行总账。这是因为我们操作共享数据时没有进行同步,也就是说多个线程同时操作一个共享变量。
解决:
**先明确共享数据,然后明确多线程代码中哪些语句是操作共享数据的。**如果弄错了同步的语句块,可能会造成多线程编程串行程序而顺序执行就不是并发了。

我们发现
private Bank bank = new Bank();
private int moneySum = 0;
是共享数据
操作共享数据的语句为:
bank.addMoney(100);以及他内部语句

然后我们可以通过同步代码块和同步函数来进行解决。

1. 同步代码块

同步代码块中的对象是唯一的,他称为一个同步锁(互斥锁),当一个线程持有该锁,其他线程不能够使用该锁。也就是通过这个唯一锁每次持有该锁的线程才能执行代码。一个线程执行完之后释放该锁,其他线程来拿到锁继续执行。

synchronized(对象)
{
    需要同步的代码(操作共享数据的代码)
}
public class MultipleThread {
    public static void main(String[] args) {
        Customer customer = new Customer();
        Thread t1 = new Thread(customer);
        Thread t2 = new Thread(customer);
        Thread t3 = new Thread(customer);
        t1.start();//客户1跑线程1
        t2.start();
        t3.start();
    }
}

//银行总账类
class Bank {
    private int moneySum = 0;

    void addMoney(int n) {
        moneySum = moneySum + n;
        try {
            Thread.sleep(10);
        } catch (Exception e) {
        }
        System.out.println("sum = " + moneySum);
    }
}

//用户类:因为要创建多个用户执行多线程,所以该类实现Runnable
class Customer implements Runnable {
    //此对象为多线程共享对象
    private Bank bank = new Bank();
    Object O = new Object();
    //存钱,存三次
    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            synchronized (O) {
                System.out.println(Thread.currentThread().getName() + " save money:");
                bank.addMoney(100);
            }
        }
    }
}
/*
Thread-0 save money:
sum = 100
Thread-0 save money:
sum = 200
Thread-0 save money:
sum = 300
Thread-2 save money:
sum = 400
Thread-2 save money:
sum = 500
Thread-2 save money:
sum = 600
Thread-1 save money:
sum = 700
Thread-1 save money:
sum = 800
Thread-1 save money:
sum = 900
 */

2. 同步函数

我们还可以直接将bank.addMoney方法定义为同步方法,这样我们就不需要同步代码块,不用创建同步对象锁了。他的写法更简洁。同步函数需要被对象调用,所以同步函数使用的锁就是this对象。在下面的实例中this指向的是唯一的customer对象,所以锁是相同的对象。

public class MultipleThread {
    public static void main(String[] args) {
        Customer customer = new Customer();
        Thread t1 = new Thread(customer);
        Thread t2 = new Thread(customer);
        Thread t3 = new Thread(customer);
        t1.start();//客户1跑线程1
        t2.start();
        t3.start();
    }
}

//银行总账类
class Bank {
    private int moneySum = 0;
    //同步方法
    synchronized void addMoney(int n) {
        System.out.println(Thread.currentThread().getName() + " save money:");
        moneySum = moneySum + n;
        try {
            Thread.sleep(10);
        } catch (Exception e) {
        }
        System.out.println("sum = " + moneySum);
    }
}

//用户类:因为要创建多个用户执行多线程,所以该类实现Runnable
class Customer implements Runnable {
    //此对象为多线程共享对象
    private Bank bank = new Bank();
    //存钱,存三次
    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
                bank.addMoney(100);
        }
    }
}
/*
Thread-0 save money:
sum = 100
Thread-0 save money:
sum = 200
Thread-0 save money:
sum = 300
Thread-2 save money:
sum = 400
Thread-2 save money:
sum = 500
Thread-2 save money:
sum = 600
Thread-1 save money:
sum = 700
Thread-1 save money:
sum = 800
Thread-1 save money:
sum = 900
 */

静态同步方法使用的锁是该方法所在类的字节码文件对象,也就是和下面方法块相同

synchronized(Bank.class)
{
    需要同步的代码(操作共享数据的代码)
}

面试题:单例设计模式安全问题

单例设计模式实际上是指不允许直接创建该类对象,把构造方法设置为私有,是通过调用该类方法来创建对象实例。
饿汉式

class Single{
    private static final Single s = new Single();
    private Single(){}
    public static Single getInstance(){
        return s;
    }
}

懒汉式:使用延迟加载,但是由于共享变量可能出现安全问题,通过加同步块来解决,同步块的锁为该类的字节码文件对象。

class Single {
    private static Single s = null;

    private Single() {
    }
    public static Single getInstance() {
        if (s == null) {
            synchronized (Single.class) {
                if (s == null)
                    s = new Single();
            }
        }
        return s;
    }
}

3. 死锁

同步可能产生死锁。
一个线程有一个锁,不放自己的锁要到另一个线程执行,而另一个线程也有一个锁不放,两者要相互访问但是都不放自己的锁就会产生死锁问题。当出现死锁程序会卡死。
通常由于同步中嵌套同步产生。
下面实例产生死锁:

class ProductThreadA implements Runnable {
    @Override
    public void run() {
//这里一定要让线程睡一会儿来模拟处理数据 ,要不然的话死锁的现象不会那么的明显.
//这里就是同步语句块嵌套,首先获得对象锁lockA,然后执行一些代码,随后我们需要对象锁lockB去执行另外一些代码.
        synchronized (LockTest.lockA) {

            System.out.println("ThreadA lock  lockA");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (LockTest.lockB) {

                System.out.println("ThreadA lock  lockB");

                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

class ProductThreadB implements Runnable {
    //我们线程B的拿锁顺序相反,我们首先需要对象锁lockB,然后需要对象锁lockA.
    @Override
    public void run() {
        synchronized (LockTest.lockB) {

            System.out.println("ThreadB lock  lockB");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (LockTest.lockA) {

                System.out.println("ThreadB lock  lockA");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

public class LockTest {
    //首先我们先定义两个final的对象锁.可以看做是共有的资源.
    static final Object lockA = new Object();
    static final Object lockB = new Object();

    public static void main(String[] args) {
        //运行两个线程
        Thread threadA = new Thread(new ProductThreadA());
        Thread threadB = new Thread(new ProductThreadB());
        threadA.start();
        threadB.start();
    }

}
/*
ThreadA lock  lockA
ThreadB lock  lockB
 */

我们可以看到当一个线程持有锁A而要去拿锁B,而另一个线程持有锁B要去拿锁A,两者都还没有释放锁,这时就会造成死锁,程序会卡死在这里。

避免死锁

所以避免死锁是非常重要的,如果一个线程每次只能获得一个锁,那么就不会产生锁顺序的死锁。尽量保证一个线程每次仅使用一个锁。
如果必须要使用两个锁,下面介绍两种方法来避免死锁

1. 锁的顺序

如果必须获取多个锁,那么在设计的时候需要充分考虑不同线程之前获得锁的顺序。按照上面的例子,两个线程获得锁的时序图如下:
java多线程(二)多线程同步与死锁
而我们可以改一下获取锁的顺序:
java多线程(二)多线程同步与死锁
这样一个线程总会等待另一个线程释放同一个锁时候才执行,这就不会出现死锁情况。

2. 超时放弃

当使用synchronized关键词提供的内置锁时,只要线程没有获得锁,那么就会永远等待下去,那就死锁了。
我们可以使用Lock接口提供了boolean tryLock(long time, TimeUnit unit) throws InterruptedException方法,该方法可以按照固定时长等待锁,因此线程可以在获取锁超时以后,主动释放之前已经获得的所有的锁。通过这种方式,也可以很有效地避免死锁。
时序图如下:
java多线程(二)多线程同步与死锁