Java多线程-03-线程同步

目录

一.线程简介

二.线程创建

三.线程状态

四.线程同步

五.线程协作

四.线程同步(重点+难点)


线程同步机制

image-20220414201711320
image-20220414201647562

我说:线程同步是为了实现多个线程操作同一个资源,同时避免因并发问题导致的数据紊乱


解决并发问题的思路(什么是线程同步)

image-20220414202805362

队列和锁

  • 线程排队

  • 每个对象都有一把锁

生活例子类比:

排队上厕所,每个人锁上门使用厕所

线程同步的实现条件:

队列 + 锁


锁机制虽然解决了多个线程访问同一对象时的正确性,但也带来了一些问题


image-20220414203807977

三大不安全案例

1.不安全的买票

public class Unsafe_BuyTicket {
    public static void main(String[] args) {
        BuyTicket buyTicket = new BuyTicket();

        new Thread(buyTicket,"A").start();
        new Thread(buyTicket,"B").start();
        new Thread(buyTicket,"C").start();
    }
}

class BuyTicket implements Runnable{
    private int ticketNumber = 10;
    private boolean flag = true;//外部停止
    @Override
    public void run() {
        while(true)     buy();
    }

    private void buy(){
        //判断是否有票
        if(ticketNumber <= 0){
            flag = false;
            return;
        }

        //模拟延时
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //买票
        System.out.println(Thread.currentThread().getName() + "-->买到了第" + ticketNumber-- + "张票");
    }
}
image-20220417105625952

我的解释:

1.为什么会出现多个线程买到同一张票?

image-20220417105851352

在某一时刻A,B,C都"看到"ticketNumber=10,于是把10"拷贝"到自己的工作内存,如果B先买了第10张票,那么实际的剩余票数应该为9,但是A,C的工作内存尚未"更新"还认为ticketNumber=10,所以A,C认为自己应该买第10张票

2.为什么会出现负数?

private void buy(){
    //判断是否有票
    if(ticketNumber <= 0){
        flag = false;
        return;
    }

    //买票
    System.out.println(Thread.currentThread().getName() + "-->买到了第" + ticketNumber-- + "张票");
}

ticketNumber = 1,A,B,C都判断有票,B买了第1张,C买了第0张,A买了第-1张


2.不安全的取钱

public class Unsafe_Bank {
    public static void main(String[] args) {
        //账户
        Accout accout = new Accout("买房存款", 100);

        //取钱业务
        //业务1:小明在accout中取50
        Drawing draw_money_business_1 = new Drawing("小明", accout , 50);
        //业务1:小红在accout中取100
        Drawing draw_money_business_2 = new Drawing("小红", accout , 100);

        //执行业务
        draw_money_business_1.start();
        draw_money_business_2.start();
    }
}

//银行账户
class Accout{
    public String accountName;
    public int money;

    public Accout(String accountName, int money){
        this.accountName = accountName;
        this.money = money;
    }
}

//银行:模拟取钱(Drawing)
class Drawing extends Thread{
    //要取哪个账户里的钱
    private Accout accout;
    //要取多少钱
    private int wantDraw;
    //成功取到多少钱
    private int successDrawed = 0;

    //构造器(“谁”要在哪个“账户”取多少“钱”)
    public Drawing(String personName, Accout accout, int wantDraw){
        //调用父类的构造器,线程的名字 = personName
        super(personName);
        this.accout = accout;
        this.wantDraw = wantDraw;
    }

    //取钱
    @Override
    public void run() {
        //判断能不能取
        if(accout.money - wantDraw < 0){
            //this.getName() = Thread.currentThread().getName()
            System.out.println(this.getName() + "-->" + accout.accountName + "账户余额不足");
            return;
        }

        //模拟延时(所有线程都会停在这一步,增加后面代码发生并发问题的概率)
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        accout.money = accout.money - wantDraw;//更新账户余额
        successDrawed = wantDraw;

        System.out.println(accout.accountName + "账户余额为:" + accout.money
                + "      " + this.getName() + "-->取到了" + successDrawed);
    }
}

image-20220417150235678 image-20220417150311476

3.不安全的集合

public class Unsafe_List {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();

        //创建10000条线程并把线程的名字添加到list
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }
        
        //让主线程睡一会再输出结果,保证其他的线程都跑完了(把自己的名字加到list里)
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(list.size());
    }
}

image-20220417155813656


为什么list的size不是10000?

我的解释:

在同一瞬间有多个线程在操作list的同一个位置

例如A,B同时往list的第995个位置add数据,A写完后主线程还没来得及执行i++,B就又往995写了(把A的覆盖掉了)

正确的情况应该类似于A写995位置,B写996位置


同步方法及同步块

image-20220417161407719


同步方法

image-20220417161445106


image-20220417161646322


同步块

image-20220417163055132


把上一节中三个不安全的例子改为安全的

1.安全的买票

synchronized 同步方法,锁的是this (拥有这个同步方法的类)

public class Safe_BuyTicket {
    public static void main(String[] args) {
        BuyTicket buyTicket = new BuyTicket();

        new Thread(buyTicket,"A").start();
        new Thread(buyTicket,"B").start();
        new Thread(buyTicket,"C").start();
    }
}

class BuyTicket implements Runnable{
    private int ticketNumber = 10;
    private boolean flag = true;//外部停止
    @Override
    public void run() {
        while(true)     buy();
    }

    //synchronized 同步方法,锁的是this,即BuyTicket
    private synchronized void buy(){
        //判断是否有票
        if(ticketNumber <= 0){
            flag = false;
            return;
        }

        //模拟延时
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //买票
        System.out.println(Thread.currentThread().getName() + "-->买到了第" + ticketNumber-- + "张票");
    }
}

2.安全的取钱

我说

同步块 synchronized (Obj){}

Obj是要锁的实例对象

同步块会被线程按序执行,A把这个块跑完了,B才能去跑,即同步块只允许有一个线程在执行块内代码

如何判断锁哪个对象?

这个对象会被多个线程修改

同步块中应该放哪些代码?

对这个对象进行修改操作的代码

public class Safe_Bank {
    public static void main(String[] args) {
        //账户
        Accout accout = new Accout("买房存款", 100);

        //取钱业务
        //业务1:小明在accout中取50
        Drawing draw_money_business_1 = new Drawing("小明", accout , 50);
        //业务1:小红在accout中取100
        Drawing draw_money_business_2 = new Drawing("小红", accout , 100);

        //执行业务
        draw_money_business_1.start();
        draw_money_business_2.start();
    }
}

//银行账户
class Accout{
    public String accountName;
    public int money;

    public Accout(String accountName, int money){
        this.accountName = accountName;
        this.money = money;
    }
}

//银行:模拟取钱(Drawing)
class Drawing extends Thread{
    //要取哪个账户里的钱
    private Accout accout;
    //要取多少钱
    private int wantDraw;
    //成功取到多少钱
    private int successDrawed = 0;

    //构造器(“谁”要在哪个“账户”取多少“钱”)
    public Drawing(String personName, Accout accout, int wantDraw){
        //调用父类的构造器,线程的名字 = personName
        super(personName);
        this.accout = accout;
        this.wantDraw = wantDraw;
    }

    //取钱
    @Override
    public void run() {

        //同步块,是对account对象进行修改,所以要锁account
        //同步块中的代码会被线程按序执行,A把这个块跑完了,B才能去跑
        synchronized (accout){
            //判断能不能取
            if(accout.money - wantDraw < 0){
                //this.getName() = Thread.currentThread().getName()
                System.out.println(this.getName() + "-->" + accout.accountName + "账户余额不足");
                return;
            }

            //模拟延时(所有线程都会停在这一步,增加后面代码发生并发问题的概率)
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            accout.money = accout.money - wantDraw;//更新账户余额
            successDrawed = wantDraw;

            System.out.println(accout.accountName + "账户余额为:" + accout.money
                    + "      " + this.getName() + "-->取到了" + successDrawed);
        }

    }
}

3.安全的集合

public class Safe_List {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();

        //创建10000条线程并把线程的名字添加到list
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                //锁list
                synchronized (list){
                    list.add(Thread.currentThread().getName());
                }
            }).start();
        }

        //让主线程睡一会再输出结果,保证其他的线程都跑完了(把自己的名字加到list里)
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(list.size());
    }
}

小结

  • 同步方法

    synchronized作为关键字修饰方法,锁的是this

  • 同步块

    synchronized (Obj){},锁的是Obj


  • 锁谁?

    被多个线程修改的对象

  • 涉及修改被锁对象的代码


死锁

image-20220417174515853


死锁举例

public class DeadLock {
    public static void main(String[] args) {
        WeaponChoice player_1_Choice = new WeaponChoice("player-1", 0);
        WeaponChoice player_2_Choice = new WeaponChoice("player-2", 1);

        player_1_Choice.start();
        player_2_Choice.start();
    }
}

//武器一:步枪(rifle)
class Rifle{

}

//武器二:手枪(pistol)
class Pistol{

}

//武器库中只有一把手枪和一把步枪
//有两种选择方式
//choice = 1 使用手枪1秒 然后使用步枪
//choice = 0 使用步枪1秒 然后使用手枪
class WeaponChoice extends Thread{

    //只有一把手枪和一把步枪,需要的资源只有一份,用static来修饰
    static Rifle rifle = new Rifle();
    static Pistol pistol = new Pistol();

    private String playerName;
    private int choiceCode;

    public WeaponChoice(String playerName, int choiceCode){
        this.playerName = playerName;
        this.choiceCode = choiceCode;
    }

    @Override
    public void run() {
        try {
            equipmentWeapon();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    //给玩家装备分配武器
    private void equipmentWeapon() throws InterruptedException {
        //先使用1秒步枪 再使用手枪
        if(choiceCode == 0){

            //一个同步块拥有两个对象的锁 rifle 和 pistol
            synchronized (rifle){
                System.out.println(this.playerName + "-->获得了 步枪 的锁");

                Thread.sleep(1000);//睡眠1s 可以放大死锁发生的可能性(两个线程都锁住了对方期待的资源)

                synchronized (pistol){
                    System.out.println(this.playerName + "-->获得了 手枪 的锁");
                }
            }

        }
        //先使用1秒手枪 再使用步枪
        if(choiceCode == 1){

            //一个同步块拥有两个对象的锁 rifle 和 pistol
            synchronized (pistol){
                System.out.println(this.playerName + "-->获得了 手枪 的锁");

                Thread.sleep(1000);//睡眠1s 可以放大死锁发生的可能性(两个线程都锁住了对方期待的资源)

                synchronized (rifle){
                    System.out.println(this.playerName + "-->获得了 步枪 的锁");
                }
            }

        }
    }
}

问题代码

rifle和pistol资源都只有一个

A先拿了rifle的锁,B先拿了pistol的锁

之后A想要pistol的锁,B想要rifle的锁

对A而言,现在pistol是被B锁住的所以A要等待

对B而言,现在rifle是被A锁住的所以B要等待

A无法得到pistol的锁,同步块代码没有执行完,也就无法释放rifle的锁

B无法得到rifle的锁,同步块代码没有执行完,也就无法释放pistol的锁

形成死锁

// A线程
...
//一个同步块拥有两个对象的锁 rifle 和 pistol
synchronized (rifle){
    System.out.println(this.playerName + "-->获得了 步枪 的锁");
    
    Thread.sleep(1000);//睡眠1s 可以放大死锁发生的可能性(两个线程都锁住了对方期待的资源)
    
    synchronized (pistol){
        System.out.println(this.playerName + "-->获得了 手枪 的锁");
    }
}
...
---------------------------------------------
// B线程
...
//一个同步块拥有两个对象的锁 rifle 和 pistol
synchronized (pistol){
    System.out.println(this.playerName + "-->获得了 手枪 的锁");
    
    Thread.sleep(1000);//睡眠1s 可以放大死锁发生的可能性(两个线程都锁住了对方期待的资源)
    
    synchronized (rifle){
        System.out.println(this.playerName + "-->获得了 步枪 的锁");
    }
}
...

如何修改

我说:

线程一次只拿一个锁,将自己持有的锁释放了才能去拿别的锁

简单的来说就是:同步块里面不能有同步块

// A线程
...
    synchronized (rifle){
        System.out.println(this.playerName + "-->获得了 步枪 的锁");
        Thread.sleep(1000);//睡眠1s 可以放大死锁发生的可能性(两个线程都锁住了对方期待的资源)
    }

    synchronized (pistol){
        System.out.println(this.playerName + "-->获得了 手枪 的锁");
    }
...
    
------------------------------------------------------
    
// B线程
...
    synchronized (pistol){
        System.out.println(this.playerName + "-->获得了 手枪 的锁");
        Thread.sleep(1000);//睡眠1s 可以放大死锁发生的可能性(两个线程都锁住了对方期待的资源)
    }

    synchronized (rifle){
        System.out.println(this.playerName + "-->获得了 步枪 的锁");
    }
...
image-20220417190208579

避免死锁的方法

image-20220417190425580

(ps:字打错了是线程不是进程)


Lock锁

image-20220417191244643


常用的是

ReentrantLock(可重入锁

ReentrantLock类实现了Lock接口,可以显式加锁,释放锁


推荐格式

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

...

    try {
        lock.lock();//加锁

        //被锁机制保障线程安全的代码

    }finally {
        lock.unlock();//解锁
    }

还是以买票为例

public class Lock_Test {
    public static void main(String[] args) {
        BuyTicket buyTicket = new BuyTicket();

        new Thread(buyTicket,"A").start();
        new Thread(buyTicket,"B").start();
        new Thread(buyTicket,"C").start();
    }
}

class BuyTicket implements Runnable{
    private int ticketNumber = 10;
    private boolean flag = true;//外部停止

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

    @Override
    public void run() {
        while(true)     buy();
    }

    private void buy(){

        try {
            lock.lock();//加锁

            //判断是否有票
            if(ticketNumber <= 0){
                flag = false;
                return;
            }
            //模拟延时
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //买票
            System.out.println(Thread.currentThread().getName() + "-->买到了第" + ticketNumber-- + "张票");

        }finally {
            lock.unlock();//解锁
        }
    }
}

对比

image-20220417213136084


posted @   Cornfield_Chase  阅读(30)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 上周热点回顾(3.3-3.9)
· AI 智能体引爆开源社区「GitHub 热点速览」
点击右上角即可分享
微信分享提示