多线程-线程安全问题

线程安全问题

在多个线程同时访问一个相同的资源的时候会发生线程安全问题。
举个栗子:
买票问题,三个窗口进行买票。

public class ThreadSafe {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        Thread t1 = new Thread(ticket);
        Thread t2 = new Thread(ticket);
        Thread t3 = new Thread(ticket);

        t1.setName("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");

        t1.start();
        t2.start();
        t3.start();
    }
}
class Ticket implements Runnable{
    
    private int ticket = 10;//有10张票
    
    @Override
    public void run() {
        while (true){
            if(ticket > 0){
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "窗口:" + ticket--);
            }else {
                break;
            }
        }
    }
}

运行结果:

很明显可以看出,在三个线程同时去访问Ticket类的时候,票的数量出现的重复错误(结果为0)的情况。

为什么会出现这种情况呢?

因为线程是并发的,并发就是三个线程同时进行。比如窗口一进入run方法,然后窗口二也进入了run方法,然后两个同时操作ticket的数量,所以数量出现重复。

如何解决呢?

解决线程安全有3中方法:使用同步代码块、同步方法、Lock锁。

1、同步代码块

同步代码块就是将操作共享资源的代码放入由synchronized修饰的代码块中。

在使用同步代码块的时候需要使用一个锁将代码块锁起来,只允许一个线程进行访问。线程在进行操作数据前获得锁,操作结束后将锁释放。

任何对象都可以是锁对象,但是锁对象必须是唯一的
在这里我使用了当前这个对象来作为锁对象,因为我只声明了一个ticket对象,该对象是唯一的。
也可以在Ticket类中声明一个对象,作为锁对象。

public class ThreadSafe {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        Thread t1 = new Thread(ticket);
        Thread t2 = new Thread(ticket);
        Thread t3 = new Thread(ticket);

        t1.setName("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");

        t1.start();
        t2.start();
        t3.start();
    }
}
class Ticket implements Runnable{

    private int ticket = 10;//有10张票
    //Object obj = new Object();
    @Override
    public void run() {
        while (true) {
            synchronized (this) {//或者使用synchronized(obj)
                if (ticket > 0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "卖:" + ticket-- + "号票");
                } else {
                    break;
                }
            }
        }
    }
}

运行结果:

有结果可以看到,我们加上同步代码块之后,显示的结果就是正确的。

2、同步方法

在方法上加上synchronized关键字即可。同步方法默认的锁对象是当前对象即this对象。

class Ticket implements Runnable{

    private int ticket = 10;//有10张票

    @Override
    public void run() {
        while (true) {
            sell();
        }
    }
    private synchronized void sell(){
        if (ticket > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "卖:" + ticket-- + "号票");
        }
    }
}

运行结果:

注意:在使用继承Thread类来创建线程的时候同步方法和同步代码块也会出现安全问题。因为默认使用this为锁对象,在运行的时候创建了三个ticket对象,所以三个线程使用的锁对象不一样。
这里使用同步代码块和同步方法的结果都是一样的,这里就不展示同步方法的代码了。

public class ThreadSafe {
    public static void main(String[] args) {

        Ticket t1 = new Ticket();
        Ticket t2= new Ticket();
        Ticket t3 = new Ticket();

        t1.setName("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");

        t1.start();
        t2.start();
        t3.start();
    }
}
class Ticket extends Thread{

    private int ticket = 10;//有10张票

    @Override
    public void run() {
        while (true) {
            synchronized (this) {
                if (ticket > 0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "卖:" + ticket-- + "号票");
                } else {
                    break;
                }
            }
        }
    }
}

运行结果:

从结果可以看出来,每个窗口都卖了10张票,因为三个线程有着不同的锁。
解决办法就是使用当前类作为锁对象。

synchronized(Ticket.class){
    //执行语句...
}

为什么使用Ticket.class可以呢?
因为Ticket.class返回的是一个Class类的对象。该对象时唯一的。

3、使用Lock锁

使用Lock锁来解决线程安全问题时,需要使用到Lock对象中的两个方法:
lock() 获得锁
unlock() 释放锁

class Ticket extends Thread{

    private int ticket = 10;//有10张票
    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            lock.lock();
            try {//使用try-finally可以保证所被释放
                if (ticket > 0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "卖:" + ticket-- + "号票");
                } else {
                    break;
                }
            }finally {
                lock.unlock();
            }
        }
    }
}

运行结果:

Lock和synchronized的区别?
synchronized会自动释放锁,Lock需要手动启动锁和释放锁。

三种方式优先级

Lock锁 > 同步代码块 > 同步方法

原文地址:https://www.cnblogs.com/Z-Dey/p/12892216.html