72 java多线程_4 _线程安全、线程同步与线程通信

本文深入探讨了多线程环境下线程安全问题,包括死锁、同步机制、线程间通信方法,以及通过实例讲解如何避免常见问题,如存取钱、生产者消费者等场景中的数据不一致。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.线程安全问题

在这里插入图片描述

  • 需求: A线程将“Hello"存入数组; B线程将“World"存入数组。
  • 出现的问题:A线程查找下标0,此时这个位置是空的,但是还没有存入数据,时间片到期,然后B线程开始执行,查找到下标0,发现里面没有数据,就把World存入到下标为0的位置,之后轮到A线程执行时,因为前面A执行时,发现下标0这个位置还没有数据,就把Hello存入,导致World被覆盖
package com.wlw.thread;
import java.util.Arrays;
/**
 *多线程安全问题:
 * 两个线程同时向一个数组存入数据
 */
public class ThreadSafe {
  	private static int index = 0;
    public static void main(String[] args) throws Exception{
        //创建数组,共享资源
        String[] s = new String[5];

        //创建两个操作
        Runnable Arunnabe = new Runnable() {
            @Override
            public void run() {
                  s[index] = "Hello";
                  index++;
            }
        };
        Runnable Brunnabe = new Runnable() {
            @Override
            public void run() {
                s[index] = "World";
                index++;
            }
        };

        //创建线程
        Thread a = new Thread(Arunnabe,"A");
        Thread b = new Thread(Brunnabe,"B");

        //启动线程
        a.start();
        b.start();

        //加入线程,确保最后a,b线程执行完
        a.join();
        b.join();

        System.out.println(Arrays.toString(s));
    }
}
/*
多运行几次,你会发现结果并不都是我们设想的 -》[Hello, World, null, null, null]
,还会有-》[Hello, null, null, null, null] ,[World, null, null, null, null]
等.........
*/

1.1多线程安全问题:

  • 当多线程并发访问临界资源时,如果破坏原子操作,可能会造成数据不一 致。
  • 临界资源:共享资源(同一对象),一次仅允许-一个线程使用,才可保证其正确性。
  • 原子操作:不可分割的多步操作,被视作- -个整体,其顺序和步骤不可打乱或缺省。

2.线程安全

  • 思考:在程序应用中,如何保证线程安全?

    同步机制:一个线程A在访问共享资源时,别的线程等待,只有A线程执行完毕,别的线程才能执行

2.1同步机制:同步方式1

2.1.1同步代码块:
synchronized(临界资源对象/共享资源对象){ //对临界资源对象加锁
	//代码(原子操作)
}

注意:

  1. 每个对象都有一个互斥锁标记,用来分配给线程的
  2. 只有拥有对象互斥锁标记的线程,才能进入对该对象加锁的同步代码块
  3. 线程退出同步代码块,会释放相应的互斥锁标记。

上面数组问题的优化,同步机制:

package com.wlw.thread;
import java.util.Arrays;
/**
 *多线程安全问题:
 * 两个线程同时向一个数组存入数据
 */
public class ThreadSafe {
    private static int index = 0;
    public static void main(String[] args) throws Exception{
        //创建数组,共享资源
        String[] s = new String[5];

        //创建两个操作
        Runnable Arunnabe = new Runnable() {
            @Override
            public void run() {
                //同步代码块
                synchronized (s){
                    s[index] = "Hello";
                    index++;
                }
            }
        };
        Runnable Brunnabe = new Runnable() {
            @Override
            public void run() {
                //同步代码块
                synchronized (s) {
                    s[index] = "World";
                    index++;
                }
            }
        };

        //创建线程
        Thread a = new Thread(Arunnabe,"A");
        Thread b = new Thread(Brunnabe,"B");

        //启动线程
        a.start();
        b.start();

        //加入线程,确保最后a,b线程执行完
        a.join();
        b.join();

        System.out.println(Arrays.toString(s));
    }
}
/*
执行结果只有这两种:
[Hello, World, null, null, null]
[World, Hello, null, null, null]
*/

优化之前的四个窗口共卖100张票的问题:

package com.wlw.thread.caseDemo;
/**
 * 票类(共享资源)
 */
public class Ticket implements Runnable{

    int ticket = 100; //票总共100张

    //创建锁,引用对象都行
    //Object obj = new Object();

    //售票功能
    @Override
    public void run() {
        while (true){
            synchronized (this){ //this 代表当前对象 Ticket
                if(ticket <= 0){
                    break;
                }
                System.out.println(Thread.currentThread().getName()+"卖了第"+ticket+"张票");
                ticket--;
            }

        }
    }
}
package com.wlw.thread.caseDemo;

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

        // 创建实现类对象
        Ticket ticket = new Ticket();

        //创建线程对象
        Thread thread1 = new Thread(ticket,"窗口1");
        Thread thread2 = new Thread(ticket,"窗口2");
        Thread thread3 = new Thread(ticket,"窗口3");
        Thread thread4 = new Thread(ticket,"窗口4");

        //调用start()方法,启动线程
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();

        /*
        这时就不会出现多个窗口卖了同一张票,这是一个线程安全问题,已用同步机制解决
         */
    }
}

优化之前存钱取钱的问题:

package com.wlw.thread.caseDemo;
/**
 * 银行卡类
 */
public class BankCard {
    private Double money;

    public Double getMoney() {
        return money;
    }

    public void setMoney(Double money) {
        this.money = money;
    }
}

package com.wlw.thread.caseDemo;

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

        //1.创建银行卡
        BankCard bankCard = new BankCard();
        bankCard.setMoney(0.0);

        //2.创建两个操作,存取功能
        Runnable add = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    //同步机制
                    synchronized (bankCard){
                        bankCard.setMoney(bankCard.getMoney() + 1000);
                        System.out.println(Thread.currentThread().getName()+ "存了1000块钱,余额是:" +bankCard.getMoney());
                    }
                }
            }
        };
        Runnable sub = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    //同步机制
                    synchronized (bankCard) {
                        if (bankCard.getMoney() >= 1000) {
                            bankCard.setMoney(bankCard.getMoney() - 1000);
                            System.out.println(Thread.currentThread().getName() + "取了1000块钱,余额是:" + bankCard.getMoney());
                        } else {
                            System.out.println("余额不足,请充值");
                            i--;
                        }
                    }
                }
            }
        };

        //3.创建线程对象,并启动
         new Thread(add,"小李").start();
         new Thread(sub,"小月").start();

    }
}

2.1.2线程阻塞状态

在这里插入图片描述

2.2同步机制:同步方式2

2.2.1同步方法
synchronized 返回值类型 方法名称(形参列表o){//对当前对象(this)加锁
    //代码(原子操作)
}

注意:

  1. 只有拥有对象互斥锁标记的线程,才能进入对该对象加锁的同步方法中。
  2. 线程退出同步方法,会释放相应的互斥锁标记。

例子:四个窗口共卖100张票

package com.wlw.thread.caseDemo;

/**
 * 票类(共享资源)
 */
public class Ticket2 implements Runnable{

    int ticket = 100; //票总共100张

    //售票功能
    @Override
    public void run() {
        while (true){
            if(!sale()){
                break;
            }
        }
    }

    public synchronized boolean sale(){ //此时锁为this,如果该方法变为static的,锁为Ticket2.Class
        if(ticket <= 0){
            return false;
        }
        System.out.println(Thread.currentThread().getName()+"卖了第"+ticket+"张票");
        ticket--;
        return true;
    }
}
package com.wlw.thread.caseDemo;

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

        // 创建实现类对象
        Ticket2 ticket = new Ticket2();

        //创建线程对象
        Thread thread1 = new Thread(ticket,"窗口1");
        Thread thread2 = new Thread(ticket,"窗口2");
        Thread thread3 = new Thread(ticket,"窗口3");
        Thread thread4 = new Thread(ticket,"窗口4");

        //调用start()方法,启动线程
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();

    }
}

2.3同步规则

  1. 注意:

    • 只有在调用包含同步代码块的方法,或者同步方法时,才需要对象的锁标记。
    • 如调用不包含同步代码块的方法,或普通方法时,则不需要锁标记,可直接调用。
  2. 已知JDK中线程安全的类:

    • StringBuffer
    • Vector
    • Hashtable
    • 以上类中的公开方法,均为synchonized修饰的同步方法。

3.经典问题

3.1死锁

  1. 当第一个线程拥有A对象锁标记,并等待B对象锁标记,同时第二个线程拥有B对象锁标记,并等待A对象锁标记时,产生死锁。
  2. 一个线程可以同时拥有多个对象的锁标记,当线程阻塞时,不会释放已经拥有的锁标记,由此可能造成死锁。

实现死锁的代码(理解死锁就好):

package com.wlw.thread.casedemo02;
public class MyLock {
    //定义锁,两只筷子
    public static Object a = new Object();
    public static Object b = new Object();
}
package com.wlw.thread.casedemo02;
public class Boy extends Thread{
    @Override
    public void run() {
        synchronized (MyLock.a){
            System.out.println("男孩拿到了a");
            synchronized (MyLock.b){
                System.out.println("男孩拿到了b");
                System.out.println("男孩可以吃饭了...........");
            }
        }
    }
}
package com.wlw.thread.casedemo02;
public class Girl extends Thread{
    @Override
    public void run() {
        synchronized (MyLock.b){
            System.out.println("女孩拿到了b");
            synchronized (MyLock.a){
                System.out.println("女孩拿到了a");
                System.out.println("女孩可以吃饭了...........");
            }
        }
    }
}
package com.wlw.thread.casedemo02;
public class TestDeadLock {
    public static void main(String[] args) {
        //创建线程对象
        Boy boy = new Boy();
        Girl girl = new Girl();

        //启动线程
        boy.start();
        /*try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }*/
        girl.start();
    }
}
/*
运行发现,程序不会停止,男孩拿到了a,女孩拿到了b,就这样一值僵持下去
如果启动其中一个线程之后,休眠一会,再启动另一个线程,就可以正确执行了
*/

4.线程通信

4.1常用方法

  1. 等待: ( 锁.wait() )

    • public final void wait()
    • public final void wait(long timeout)
    • 必须在对obj加锁的同步代码块中。在一个线程中,调用obj.wait() 时,此线程会释放其拥有的所有锁标记。同时此线程阻塞在o的等待队列中。释放锁,进入等待队列。
  2. 通知:( 锁.notify() )

    • public final void notify() //唤醒等待线程
    • public final void notifyAll() //唤醒全部等待线程

4.2还是存取钱的例子(存一下,取一下):

package com.wlw.thread.casedemo03;

/**
 * 银行卡类
 */
public class BankCard {

    private double money;

    private boolean flag ; //true为有钱,false为没钱

    //存钱(没有钱才会存)
    public synchronized void add(double m){//锁为this
        if(flag){//有钱,不能存
            try {
                this.wait(); //锁.wait(),将线程放入等待队列,同时释放锁和cpu
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        money = money + m;
        System.out.println(Thread.currentThread().getName()+"存了1000块钱,余额为:"+money);
        flag = true;// 存完钱了,变为有钱了
        this.notify();//锁.notify(),唤醒等待队列中的线程
    }

    //取钱,有钱才能取
    public synchronized void sub(double m){
        if (!flag){ //没钱不能取
            try {
                this.wait();//锁.wait(),将线程放入等待队列,同时释放锁和cpu
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        money = money - m;
        System.out.println(Thread.currentThread().getName()+"取了1000块钱,余额为"+money);
        flag = false;//取完钱了,变没钱了
        this.notify();//锁.notify(), 唤醒等待队列中的线程

    }
}
package com.wlw.thread.casedemo03;

public class AddMoney implements Runnable{

    private BankCard card;
    public AddMoney(BankCard card){
        this.card = card;
    }
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            card.add(1000.0);
        }
    }
}
package com.wlw.thread.casedemo03;

public class SubMoney implements Runnable {

    private BankCard card;
    public SubMoney(BankCard card){
        this.card = card;
    }
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            card.sub(1000.0);
        }
    }
}
package com.wlw.thread.casedemo03;

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

        //创建一张卡
        BankCard card = new BankCard();

        //创建操作
        AddMoney addMoney = new AddMoney(card);
        SubMoney subMoney = new SubMoney(card);

        //创建线程
        Thread add = new Thread(addMoney,"小李");
        Thread sub = new Thread(subMoney,"小月");

        //启动线程
        add.start();
        sub.start();
    }
}

4.3多存多取问题与全部等待问题

  1. 多存多取问题分析:两个人A、B存,两个人C、D取,有这样的情况,一开始C、D去取钱,因为没钱,所以C、D都进入等待队列;下一个A存钱(1000元),成功,唤醒了C;C抢到了cpu,取钱(0元),成功,唤醒了D;D抢到了cpu,取钱,D是可以成功的,因为D是从wait()语句,开始执行的,它不会再去判断标记,所以此时卡里的钱为(-1000元)

  2. 解决:将同步方法里的 if 变为 while

在这里插入图片描述

  1. 全部等待问题分析:将同步方法里的 if 变为 while 之后,还会出现一个这样的情况:一开始,C去取钱,因为没钱不能取,进入等待队列,释放锁和cpu;D去取钱,因为没钱不能取,进入等待队列,释放锁和cpu;A去存钱(1000元),成功,唤醒了C;B去存钱,因为有钱不能存,进入等待队列,释放锁和cpu;A去存钱,因为有钱不能存,进入等待队列,释放锁和cpu;C去取钱,成功(0元),唤醒了D(问题就来了);此时只有C、D可以抢cpu,但是卡里已经没钱了,不管谁抢到,都会进入等待队列,最终全部都进入了等待队列。

  2. 解决:将notify() 变为notifyAll() ,全部唤醒

    在这里插入图片描述

最终代码:

package com.wlw.thread.casedemo03;

/**
 * 银行卡类
 */
public class BankCard {

    private double money;

    private boolean flag ; //true为有钱,false为没钱

    //存钱(没有钱才会存)
    public synchronized void add(double m){//锁为this
        while (flag){//有钱,不能存
            try {
                this.wait(); //锁.wait(),将线程放入等待队列,同时释放锁和cpu
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        money = money + m;
        System.out.println(Thread.currentThread().getName()+"存了1000块钱,余额为:"+money);
        flag = true;// 存完钱了,变为有钱了
        this.notifyAll();//锁.notify(),唤醒等待队列中的线程
    }

    //取钱,有钱才能取
    public synchronized void sub(double m){
        while (!flag){ //没钱不能取
            try {
                this.wait();//锁.wait(),将线程放入等待队列,同时释放锁和cpu
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        money = money - m;
        System.out.println(Thread.currentThread().getName()+"取了1000块钱,余额为"+money);
        flag = false;//取完钱了,变没钱了
        this.notifyAll();//锁.notify(), 唤醒等待队列中的线程

    }
}
package com.wlw.thread.casedemo03;

public class AddMoney implements Runnable{

    private BankCard card;
    public AddMoney(BankCard card){
        this.card = card;
    }
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            card.add(1000.0);
        }
    }
}
package com.wlw.thread.casedemo03;

public class SubMoney implements Runnable {

    private BankCard card;
    public SubMoney(BankCard card){
        this.card = card;
    }
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            card.sub(1000.0);
        }
    }
}
package com.wlw.thread.casedemo03;

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

        //创建一张卡
        BankCard card = new BankCard();

        //创建操作
        AddMoney addMoney = new AddMoney(card);
        SubMoney subMoney = new SubMoney(card);

        //创建线程
        Thread xiaoli = new Thread(addMoney,"小李");
        Thread xiaoyue = new Thread(subMoney,"小月");
        Thread mingming = new Thread(addMoney,"明明");
        Thread lili = new Thread(subMoney,"丽丽");

        //启动线程
        xiaoli.start();
        mingming.start();
        xiaoyue.start();
        lili.start();
    }
}

5.生产者消费者问题

package com.wlw.thread.casedemo04;
/**
 * 面包类
 */
public class Bread {

    private int id;
    private String producterName;

    public Bread() {}

    public Bread(int id, String producterName) {
        this.id = id;
        this.producterName = producterName;
    }

    public int getId() { return id;}

    public void setId(int id) {this.id = id;}

    public String getProducterName() {return producterName;}

    public void setProducterName(String producterName) {
        this.producterName = producterName;
    }

    @Override
    public String toString() {
        return "Bread{" +
                "id=" + id +
                ", producterName='" + producterName + '\'' +
                '}';
    }
}
package com.wlw.thread.casedemo04;
public class BreadCon {
    //装面包的容器,也是缓冲区
    private Bread[] cons = new Bread[6];
    //下标
    private int index = 0;

    //存面包
    public synchronized void input(Bread bread){//锁 this
        //判断容器满吗
        while(index >= 6){
            try {
                this.wait(); //加入等待队列,释放锁和cpu
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        cons[index] = bread;
        System.out.println(Thread.currentThread().getName()+"生产了第"+bread.getId()+"个面包");
        index++;

        this.notifyAll();//唤醒等待队列里的线程
    }

    //吃面包
    public synchronized void output(){
        //判断容器里还有面包吗
        while(index <= 0){
            try {
                this.wait();//加入等待队列,释放锁和cpu
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        index--;
        Bread bread = cons[index];
        System.out.println(Thread.currentThread().getName()+"消费了第"+bread.getId()+"个面包"+",生产者:"+bread.getProducterName());
        cons[index] = null;

        this.notifyAll();//唤醒等待队列里的线程
    }

}
package com.wlw.thread.casedemo04;
public class Product implements Runnable{
    private BreadCon breadCon;
    public Product(BreadCon breadCon) {
        this.breadCon = breadCon;
    }

    //生产面包
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            breadCon.input(new Bread(i,Thread.currentThread().getName()));
        }
    }
}
package com.wlw.thread.casedemo04;
public class Consume implements Runnable{
    private BreadCon breadCon;
    public Consume(BreadCon breadCon) {
        this.breadCon = breadCon;
    }

    //消费面包
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            breadCon.output();
        }
    }
}
package com.wlw.thread.casedemo04;
public class Test {
    public static void main(String[] args) {
        //创建容器
        BreadCon breadCon = new BreadCon();

        //创建操作
        Product product = new Product(breadCon);
        Consume consume = new Consume(breadCon);

        //创建线程
        Thread xiaoli = new Thread(product, "小李");
        Thread xiaoyue = new Thread(consume, "小月");
        Thread mingming = new Thread(product, "明明");
        Thread lili = new Thread(consume, "丽丽");

        //启动线程
        xiaoli.start();
        xiaoyue.start();
        mingming.start();
        lili.start();
    }
}

6.小结

  1. 线程的创建:

    • 方式1:继承Thread类
    • 方式2:实现Runnable接口 (-一个任务Task),传入给Thread对象并执行。
  2. 线程安全:

    • 同步代码块:为方法中的局部代码 (原子操作) 加锁。
    • 同步方法:为方法中的所有代码 (原子操作) 加锁。
  3. 线程间的通信:

    • wait() / wait(long timeout); // 等待
    • notify() / notifyAll(); // 通知
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

悬浮海

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值