多线程基础-线程安全

2024/06/20 Java

线程安全问题-抢票 – 同步代码块解决

注:这里为了避免数据量大,只写了三张牌;如果需要自行演示查看,建议扩大到100张票 如下图所示,票池中有三张票(三个线程),期望大家能够正常凭借手速抢票:

picture not found
public class MyTicket implements Runnable {
    private int ticket = 3;

    @Override
    public void run() {

        while (true) {
            if (ticket < 0 || ticket == 0) {
                return;
            }
            System.out.println(Thread.currentThread().getName() + "抢到了第" + ticket + "张票");
            --ticket;
        }
    }
}

public class MyTicket_Main {

    public static void main(String[] args) throws InterruptedException {
        MyTicket myTicket = new MyTicket();

        Thread person01 = new Thread(myTicket, "用户1");
        Thread person02 = new Thread(myTicket, "用户2");
        Thread person03 = new Thread(myTicket, "用户3");

        person01.start();
        person02.start();
        person03.start();
    }
}

// 执行结果:
/**
 * 用户2抢到了第3张票
 * 用户1抢到了第3张票
 * 用户3抢到了第3张票
 * 用户1抢到了第1张票
 * 用户2抢到了第2张票
 */

执行代码发现,结果并不是我们期望的,用户2、用户1、用户3抢到了同一张票,这是不允许的。

可以通过现实中的例子解决该问题:

上厕所的时候,我们将厕门关闭上锁的方式来保持一个人独占厕所;当结束,打开锁,就算想继续占用,也需要重新排队

synchronized关键字就相当于一把锁,当然,必须保证每一次都是同一把锁

public class MyTicket implements Runnable {
    private int ticket = 3;

    private final Object object = new Object();

    @Override
    public void run() {

        while (true) {
            synchronized (object) {
                // 每一个和公共变量相关的都应该被包裹在同步代码块中
                if (ticket < 0 || ticket == 0) {
                    return;
                }
                System.out.println(Thread.currentThread().getName() + "抢到了第" + ticket + "张票");
                --ticket;
            }
        }
    }
}

public class MyTicket_Main {

    public static void main(String[] args) throws InterruptedException {
       MyTicket myTicket = new MyTicket();

        Thread person01 = new Thread(myTicket, "用户1");
        Thread person02 = new Thread(myTicket, "用户2");
        Thread person03 = new Thread(myTicket, "用户3");

        person01.start();
        person02.start();
        person03.start();
    }
}
/**
 * 用户1抢到了第3张票
 * 用户1抢到了第2张票
 * 用户1抢到了第1张票
 */

发现,已经实现了目标,只不过全是由用户1抢到了票,我们可以使用sleep来降低每一个用户获取到线程的频率

public class MyTicket implements Runnable {
    private int ticket = 3;

    private final Object object = new Object();

    @Override
    public void run() {

        while (true) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            synchronized (object) {
                if (ticket < 0 || ticket == 0) {
                    return;
                }
                System.out.println(Thread.currentThread().getName() + "抢到了第" + ticket + "张票");
                --ticket;
            }
        }
    }
}
/**
 * 用户3抢到了第3张票
 * 用户2抢到了第2张票
 * 用户1抢到了第1张票
 */

因为CPU执行的速度很快,在100ms的时间段中,三个用户都进入了run方法,其中某一个,假设为A用户,开始执行同步代码块中的内容;其余两个用户进入等待,直到A用户让出CPU资源,其余用户之一开始执行代码块.

问题:为什么不使用继承Thread的方式来写呢?

同一个Thread实例,只能start一次;如果要创建三个线程,那么只能创建三个Thread实例,就会出现一个问题,每一个实例对象,都有成员变量ticket=3;

也就是说每一个人都有三张票,和题目就无关了,所以使用Runnable接口。

public class MyTicket extends Thread {
    private int ticket = 3;

    @Override
    public void run() {

        while (true) {
            if (ticket < 0 || ticket == 0) {
                return;
            }
            System.out.println(Thread.currentThread().getName() + "抢到了第" + ticket + "张票");
            --ticket;
        }
    }
}
public class MyTicket_Main {

    public static void main(String[] args) throws InterruptedException {
//       MyTicket myTicket = new MyTicket();

        MyTicket person01 = new MyTicket();
        MyTicket person02 = new MyTicket();
        MyTicket person03 = new MyTicket();

        person01.start();
        person02.start();
        person03.start();
    }
}
// 运行结果:
/**
 * Thread-0抢到了第3张票
 * Thread-2抢到了第3张票
 * Thread-1抢到了第3张票
 * Thread-2抢到了第2张票
 * Thread-2抢到了第1张票
 * Thread-0抢到了第2张票
 * Thread-1抢到了第2张票
 * Thread-0抢到了第1张票
 * Thread-1抢到了第1张票
 */

注意:这里强调的是同一把锁,如果使用的是多把锁,那么依然会出现线程安全问题

比如:通过继承Thread类来创建多线程。那么需要创建多个Thread实例,三个实例都各自由一个Object对象,是三把锁

public class MyTicket implements Runnable {
    private int ticket = 3;

    // 使用列表、数组仍然视为同一把锁,因为是相同的对象
    private List<String> object = new ArrayList<>(100);

    @Override
    public void run() {

        while (true) {

            synchronized (object) {
                if (ticket < 0 || ticket == 0) {
                    return;
                }
                System.out.println(Thread.currentThread().getName() + "抢到了第" + ticket + "张票");
                --ticket;
            }
        }
    }
}

线程安全问题-抢票 – 同步方法解决

两种同步方法

普通同步方法

正常的方法声明如下:

修饰符 返回类型 方法名(参数列表) { } public void function(String agr) { }

普通同步方法声明如下:

修饰符 synchronized 返回类型 方法名(参数列表) { } public synchronized void function() { }

public class MyTicket implements Runnable {
    private int ticket = 3;

    @Override
    public synchronized void run() {

        while (true) {
            if (ticket < 0 || ticket == 0) {
                return;
            }
            System.out.println(Thread.currentThread().getName() + "抢到了第" + ticket + "张票");
            --ticket;
        }
    }
}
public class MyTicket_Main {

    public static void main(String[] args) throws InterruptedException {
        MyTicket myTicket = new MyTicket();

        Thread person01 = new Thread(myTicket, "用户1");
        Thread person02 = new Thread(myTicket, "用户2");
        Thread person03 = new Thread(myTicket, "用户3");

        person01.start();
        person02.start();
        person03.start();
    }
}


// 也可以将MyTicket写作

@Override
public void run() {
    while (true) {
        synchronized (this) {
            if (ticket < 0 || ticket == 0) {
                return;
            }
            System.out.println(Thread.currentThread().getName() + "抢到了第" + ticket + "张票");
            --ticket;
        }
    }
}

同步方法实际上,就是synchronized(this), 即当前对象为锁;

我们在main函数中只创建了一个MyTicket实例,所以三个线程共享的是同一把锁

静态同步方法
public class MyTicket implements Runnable {
    private static int ticket = 3;

    @Override
    public void run() {
        sub();
    }

    public static synchronized void sub() {
        while (true) {
            if (ticket < 0 || ticket == 0) {
                return;
            }
            System.out.println(Thread.currentThread().getName() + "抢到了第" + ticket + "张票");
            --ticket;
        }
    }
}

静态同步方法,实际上,就是synchronized(MyTicket.class)

线程安全问题-抢票 – Lock锁解决

Search

    Table of Contents