【Java并发编程】线程的同步

一、基础概念

1. 并发

多个线程访问同一个对象,并且某些线程还想修改这些对象

2. 线程同步

线程同步是一种等待机制, 通过在访问时加入锁机制 synchronized 来实现,线程同步会导致一些性能问题(性能倒置等)

3. 队列和锁

线程同步需要队列和锁,每个对象都有自己的锁

二、案例

1. 多线程买票问题

线程不安全代码,会出现超卖现象

/**
 * @description: 使用多线程,模拟三个窗口同时售票
 * @author: Liuwanqing
 * @date: 2022-10-14 16:42
 */
public class  SellTicket {
    public static void main(String[] args) {
        SellTickets02 sellTickets02 = new SellTickets02();

//        SellTickets01 sellTickets01 = new SellTickets02();
//        SellTickets01 sellTickets02 = new SellTickets02();
//        SellTickets01 sellTickets03 = new SellTickets02();

//        sellTickets01.start();
//        sellTickets02.start();
//        sellTickets03.start();

        // 创建 3 个线程进行买票
        Thread thread1 = new Thread(sellTickets02);
        Thread thread2 = new Thread(sellTickets02);
        Thread thread3 = new Thread(sellTickets02);

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

// 使用 Thread 方式
class SellTickets01 extends Thread {
    private static int tikectNum = 100; // 多线程共享的 num

    @Override
    public void run() {
        while (true) {
            if (tikectNum <= 0) {
                System.out.println("票卖完了~~~");
                break;
            }
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("窗口 " + Thread.currentThread().getName() + " 售出了一张票,还剩的票数为" + (--tikectNum) );
        }
    }
}
// 使用实现 Runnable 接口的方式
class SellTickets02 implements Runnable {
    private static int tikectNum = 100; // 多线程共享的 num

    @Override
    public void run() {
        while (true) {
            if (tikectNum <= 0) {
                System.out.println("票卖完了~~~");
                break;
            }
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("窗口 " + Thread.currentThread().getName() + " 售出了一张票,还剩的票数为" + (--tikectNum) );
        }
    }
}

2. 银行取钱问题

/**
 * @Author: WanqingLiu
 * @Date: 2023/02/02/15:03
 * 银行取钱问题
 */
public class Draw {
    public static void main(String[] args) {
        Account account = new Account("晚晴", 100);
        DrawThread wanqing = new DrawThread(account, 10);
        DrawThread xinyu = new DrawThread(account, 10);
        wanqing.start();
        xinyu.start();
    }
}

// 银行账户类
class Account {
    String name; // 账户名
    int nowNum; // 账户余额

    public Account(String name, int nowNum) {
        this.name = name;
        this.nowNum = nowNum;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getNowNum() {
        return nowNum;
    }

    public void setNowNum(int nowNum) {
        this.nowNum = nowNum;
    }
}



// 取钱线程
class DrawThread extends Thread {
    Account account; // 取钱的账户
    int drawNum; // 取走的钱
    public DrawThread(Account account, int drawNum){
        this.account = account;
        this.drawNum = drawNum;
    }
    @Override
    public void run() {
        if ((account.getNowNum() - drawNum) < 0) {
            System.out.println("余额不足");
            return;
        }
        try { // 放大问题的发生性 *
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        account.setNowNum(account.getNowNum() - drawNum);
        System.out.println("当前余额为 :" + account.getNowNum());
    }
}

3. 线程不安全的集合

ArrayList - 线程不安全

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        // 开启 10000 个线程,向 list 中加入 10000 个数据
        for (int i=0; i<10000; i++){
            // 两个线程覆盖了同一个位置
            new Thread(() ->{
                list.add(Thread.currentThread().getName());
            }).start();
        }
        // 输出并不为 10000 , 因为两个线程同时加入时,加入的元素覆盖了同一个位置
        System.out.println(list.size());
    }

三、同步方法

1. synchronized

synchronized 默认锁的是对象本身(this),我们可以通过同步块锁任何对象,锁的对象应该是多线程操作的那个变化的量,如在银行取钱中,我们要锁的是要变化的账户对象

- (1)同步方法

同步方法实现线程安全的买票

package concurrency;

/**
 * @description: 使用多线程,模拟三个窗口同时售票
 * @author: Liuwanqing
 * @date: 2022-10-14 16:42
 */
public class  SellTicket {
    public static void main(String[] args) {
        SellTickets02 sellTickets02 = new SellTickets02();
        // 创建 3 个线程进行买票
        Thread thread1 = new Thread(sellTickets02);
        Thread thread2 = new Thread(sellTickets02);
        Thread thread3 = new Thread(sellTickets02);

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

// 使用 Thread 方式
class SellTickets01 extends Thread {
    private static int tikectNum = 100; // 多线程共享的 num

    @Override
    public synchronized void run() {
        while (true) {
            if (tikectNum <= 0) {
                System.out.println("票卖完了~~~");
                break;
            }
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("窗口 " + Thread.currentThread().getName() + " 售出了一张票,还剩的票数为" + (--tikectNum) );
        }
    }
}
// 使用实现 Runnable 接口的方式
class SellTickets02 implements Runnable {
    private static int tikectNum = 100; // 多线程共享的 num

    @Override
    public synchronized void run() {
        while (true) {
            if (tikectNum <= 0) {
                System.out.println("票卖完了~~~");
                break;
            }
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("窗口 " + Thread.currentThread().getName() + " 售出了一张票,还剩的票数为" + (--tikectNum) );
        }
    }
}

- (2)同步块

- 同步块解决银行取钱问题
/**
 * @Author: WanqingLiu
 * @Date: 2023/02/02/15:03
 * 银行取钱问题
 */
public class Draw {
    public static void main(String[] args) {
        Account account = new Account("晚晴", 100);
        DrawThread wanqing = new DrawThread(account, 10);
        DrawThread xinyu = new DrawThread(account, 10);
        wanqing.start();
        xinyu.start();
    }
}

// 银行账户类
class Account {
    String name; // 账户名
    int nowNum; // 账户余额

    public Account(String name, int nowNum) {
        this.name = name;
        this.nowNum = nowNum;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getNowNum() {
        return nowNum;
    }

    public void setNowNum(int nowNum) {
        this.nowNum = nowNum;
    }
}

// 取钱线程
class DrawThread extends Thread {
    Account account; // 取钱的账户
    int drawNum; // 取走的钱
    public DrawThread(Account account, int drawNum){
        this.account = account;
        this.drawNum = drawNum;
    }
    @Override
    public void run() {
        // 锁住我们要操作的 account
        synchronized (account) {
            if ((account.getNowNum() - drawNum) < 0) {
                System.out.println("余额不足");
                return;
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            account.setNowNum(account.getNowNum() - drawNum);
            System.out.println("当前余额为 :" + account.getNowNum());
        }
    }
}

- 同步块解决 List 不安全问题
    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<>();
        for (int i=0; i<10000; i++){
            // 两个线程覆盖了同一个位置
            new Thread(() ->{
                synchronized (list) {
                    list.add(Thread.currentThread().getName());
                }
            }).start();
        }
        Thread.sleep(1000); // 让主线程睡一会,保证能执行完
        System.out.println(list.size());
    }

CopyOnWriteArrayList 是 JUC 包提供的线程安全集合

    public static void main(String[] args) throws InterruptedException {
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
        for (int i=0; i<10000; i++){
            // 两个线程覆盖了同一个位置
            new Thread(() ->{
                list.add(Thread.currentThread().getName());
            }).start();
        }
        Thread.sleep(1000); // 让主线程睡一会,保证能执行完
        System.out.println(list.size());
    }

四、死锁

死锁描述的是一种多个进程被无限期阻塞,它们中的一个或者多个都在等待某一资源被释放,导致程序不能正常终止的情况 —— 就是线程相互等待对方的资源

/**
 * @Author: WanqingLiu
 * @Date: 2023/02/02/15:51
 * 化妆模拟死锁
 */
public class DeadLock {

    public static void main(String[] args) {
        Makeup girl1 = new Makeup(0, "晚晴");
        Makeup girl2 = new Makeup(1, "婉晴");
        girl2.start();
        girl1.start();
    }
}
// 口红类
class LipStick{

}
// 镜子
class Mirror{

}
// 化妆线程
class Makeup extends Thread {
    static LipStick lipStick = new LipStick();
    static Mirror mirror = new Mirror();

    int choice;
    String girlName;

    public Makeup(int choice, String girlName){
        this.choice = choice;
        this.girlName = girlName;
    }

    @Override
    public void run() {
        // 化妆操作
        try {
            makeup();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    private void makeup() throws InterruptedException {
        if(choice == 0){ // 口红
            synchronized (lipStick) {
                System.out.println(this.girlName + "获得口红的锁");
                Thread.sleep(1000); // 一秒钟之后,她想获得镜子的锁
                synchronized (mirror) {
                    System.out.println(this.girlName + "获得镜子的锁");
                }
            }
        } else { // 镜子
            synchronized (mirror) {
                System.out.println(this.girlName + "获得镜子的锁");
                Thread.sleep(1000); // 一秒钟之后,她想获得口红的锁
                synchronized (lipStick) {
                    System.out.println(this.girlName + "获得口红的锁");
                }
            }
        }
    }
}

五、 Lock 锁

1. 基础概念

Lock 锁是 JDK 5.0 开始提供的更加强大的同步机制,其是一个显式同步锁,同步锁通过 Lock 对象充当( java.util.concurrent.locks

ReentrantLock 类实现了 Lock,其与 synchronized 具有相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是 ReentrantLock ,可以显式加锁、释放锁。

2. 使用 ReentrantLock 可重入锁解决买票问题

从下面的代码中,我们也可以看出 Lock 锁是显式的加锁,显式的释放锁 。

import java.util.concurrent.locks.ReentrantLock;

/**
 * @Author: WanqingLiu
 * @Date: 2023/02/04/10:18
 * 测试Lock锁
 */
public class TestLock {

    public static void main(String[] args) {
        TestLock2 testLock2 = new TestLock2();
        new Thread(testLock2).start();
        new Thread(testLock2).start();
        new Thread(testLock2).start();
    }

}

class TestLock2 implements Runnable {
    private static int tikectNum = 100; // 多线程共享的 num

    // 定义 lock 锁
    private final ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            try {
                lock.lock();
                if (tikectNum <= 0) {
                    System.out.println("票卖完了~~~");
                    break;
                }
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("窗口 " + Thread.currentThread().getName() + " 售出了一张票,还剩的票数为" + (--tikectNum));
            } finally {
                lock.unlock();
            }
        }
    }
}

3. synchronized 与 Lock 对比

  • Lock 是显式锁,即需要手动开关锁;synchronized 是隐式锁,即出了作用域锁自动释放
  • Lock 只具有代码块锁,而 synchronized 还具有方法锁
  • 使用 Lock 锁,JVM 花费更少的时间调度线程,性能更好,并且具有更好的扩展性(子类)
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值