线程同步问题

一、线程同步

  • 多个线程操作一个对象

1.并行并发

  • 并行是指两个或者多个事件在同一时刻发生,而并发是指两个或多个事件在同一时间间隔发生。
  • 并发:同一个对象被多个线程同时操作

2.线程同步

  • 现实生活中,我们会遇到“同一个资源,多个人都想使用”的问题,比如,食堂排队打饭,最天然的解决方法就是排队,一个个来。
  • 处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象,这时候就需要线程同步,线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池,形成队列,等待前面线程使用完毕,下一个线程再使用。

3.队列和锁

  • 队列+锁(类比上厕所,进去后要锁门才能保证安全)才能保证线程同步的安全性
  • 由于同一进程的多个线程共享一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被 访问时的正确性,在访问时加入锁机制 synchronized,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可,存在以下问题
    • 一个线程持有锁会导致其他所有需要此锁的线程挂起
    • 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题
    • 如果一个优先级高的线程等待一个优先级㐴的线程释放锁,会导致优先级倒置,引起性能问题。

二、三大线程不安全案例

1.模拟买票

  • 线程不安全,票num可能出现负数

  • package safe;
    
    //模拟买票,线程不安全
    public class TicketsBuy {
        public static void main(String[] args) {
            BuyTicket ticket=new BuyTicket();
            new Thread(ticket,"我").start();
            new Thread(ticket,"你").start();
            new Thread(ticket,"黄牛党").start();
        }
    }
    
    class BuyTicket implements Runnable{
        private int ticketsNum=10;
        boolean flag=true;//通过外部判断线程停止
        @Override
        public void run() {
            while(flag){
                buy();
            }
        }
        public void buy(){
           if (ticketsNum<=0) {
               flag = false;
               return;//结束
           }
            try {
                Thread.sleep(100);//模拟延时,放大问题
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"买到了第"+ticketsNum--+"张票");
        }
    }
    
    
    
  • image-20220419172509485

  • 不安全的原因:

    • 三个人可能同时或其中两个人拿到了票,到最后一张票时,三个人发现都还可以拿,所以三个人都各拿了一张,最后导致票数为负数。同时操作了一块内存。

2.模拟银行取钱

  • 会出现负数,线程不安全,同时对一个对象进行操作了

    package safe;
    //模拟两个人同时去银行取钱,对同一个账户进行操作
    public class unSafeBank {
        public static void main(String[] args) {
            Account account=new Account(100,"10086");
            BankDrawing you=new BankDrawing(account,50,"你");
            BankDrawing me=new BankDrawing(account,100,"我");
            you.start();
            me.start();
        }
    }
    //账户
    class Account{
        int moneyNum;
        String cardID;
        public Account(int moneyNum, String cardID) {
            this.moneyNum = moneyNum;
            this.cardID = cardID;
        }
    }
    //模拟银行取钱
    class BankDrawing extends Thread{
        Account account;//账户
        //取了多少钱
        int drawingMoney;
        //目前手里有多少钱
        int nowMoney;
        public BankDrawing(Account account,int drawingMoney,String name){
            super(name);//传给父类的构造方法
            this.account=account;
            this.drawingMoney=drawingMoney;
        }
        @Override
        public void run() {
            if (account.moneyNum-drawingMoney<0){
                System.out.println("账户里的钱不够了");
                return;
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            account.moneyNum=account.moneyNum-drawingMoney;
            nowMoney=nowMoney+drawingMoney;
            System.out.println("账户余额为:"+account.moneyNum);
            //this.getName()=Thread.currentThread().getName(),因为继承了Thread
            System.out.println(this.getName()+"目前手里有"+nowMoney+"钱");
        }
    }
    
  • image-20220419213703669

3.arraylist是线程不安全的

package safe;

import java.util.ArrayList;

public class unsafeList {
    public static void main(String[] args) {
        ArrayList<String> arrayList=new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                arrayList.add(Thread.currentThread().getName());
            }).start();
        }
        try {
            Thread.sleep(10);//如果休眠时间足够长,结果为10000
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(arrayList.size());//输出结果不是10000,还有未执行完成的
    }
}

三、线程同步方法及代码块

  • 由于我们可以通过private关键字来保证数据对象只能被方法(get\set)访问,所以我们只需要针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种方法:synchronized方法及synchronized块

    public synchronized void method(){}
    
  • synchronized方法控制对“对象”的访问,每个对象都有一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程阻塞,方法一但执行,就独占该锁,直到方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。

  • 缺陷:若将一个大的方法声明为synchronized将会影响效率。

  • 方法里面需要修改的内容才需要锁,锁得太多,浪费资源。

  • 同步块:

    • synchronized(obj){}
    • obj称为同步监视器
      • obj可以是任何对象,但是推荐使用共享资源作为同步监视器
      • 同步方法中无需指定同步监视器,因为同步方法的同步监视器是this,就是这个对象本身,或者是class(反射)
    • 同步监视器的执行过程
      • 1.第一个线程访问,锁定同步监视器,执行其中的代码
      • 2.第二个线程访问,发现同步监视器被锁定,无法访问
      • 3.第一个线程访问完毕,解锁同步监视器
      • 4.第二个线程访问,发现同步监视器没有锁,然后锁定并访问
  • 针对上面三个案例进行同步

    • 取票:

      • package safe;
        //模拟买票,线程不安全
        public class TicketsBuy {
            public static void main(String[] args) {
                BuyTicket ticket=new BuyTicket();
                Thread you= new Thread(ticket, "你");
                Thread me = new Thread(ticket, "我");
        
                Thread party = new Thread(ticket, "黄牛党");
                you.setPriority(5);
                me.setPriority(5);
                party.setPriority(5);
                you.start();
                me.start();
                party.start();
            }
        }
        
        class BuyTicket implements Runnable{
            private int ticketsNum=1000;
            boolean flag=true;
            @Override
            public  void run() {
                while(flag){
                    try {
                        buy();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    
                }
            }
            public synchronized void buy() throws InterruptedException {
               if (ticketsNum<=0) {
                   flag = false;
                   return;//结束
               }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"买到了第"+ticketsNum--+"张票");
            }
        }
        //如果sleep是在buy里,则线程拿到锁执行完成后会由CPU调度的执行哪个线程,三个线程都在run方法里的buy前面进行等待
        
      • package safe;
        
        //模拟买票,线程不安全
        public class TicketsBuy {
            public static void main(String[] args) {
                BuyTicket ticket=new BuyTicket();
                Thread you= new Thread(ticket, "你");
                Thread me = new Thread(ticket, "我");
        
                Thread party = new Thread(ticket, "黄牛党");
                you.setPriority(5);
                me.setPriority(5);
                party.setPriority(5);
                you.start();
                me.start();
                party.start();
            }
        }
        
        class BuyTicket implements Runnable{
            private int ticketsNum=1000;
            boolean flag=true;
            @Override
            public  void run() {
                while(flag){
                    try {
                        buy();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            public synchronized void buy() throws InterruptedException {
               if (ticketsNum<=0) {
                   flag = false;
                   return;//结束
               }
                
                System.out.println(Thread.currentThread().getName()+"买到了第"+ticketsNum--+"张票");
            }
        }
        //如果放在run方法里,cpu调度线程,第一个线程进入拿到锁,第二三线程在buy方法前排队等待,当第一个线程执行完buy释放锁,其他线程依次执行,然后线程休眠一秒
        
    • 银行取钱:

      • package safe;
        //模拟两个人同时去银行取钱,对同一个账户进行操作
        public class unSafeBank {
            public static void main(String[] args) {
                Account account=new Account(100,"10086");
                BankDrawing you=new BankDrawing(account,50,"你");
                BankDrawing me=new BankDrawing(account,100,"我");
                you.start();
                me.start();
            }
        }
        //账户
        class Account{
            int moneyNum;
            String cardID;
            public Account(int moneyNum, String cardID) {
                this.moneyNum = moneyNum;
                this.cardID = cardID;
            }
        }
        //模拟银行取钱
        class BankDrawing extends Thread{
            Account account;//账户
            //取了多少钱
            int drawingMoney;
            //目前手里有多少钱
            int nowMoney;
            public BankDrawing(Account account,int drawingMoney,String name){
                super(name);//传给父类的构造方法
                this.account=account;
                this.drawingMoney=drawingMoney;
            }
            @Override
            public  void run() {
                synchronized (account) {
                    if (account.moneyNum - drawingMoney < 0) {
                        System.out.println("账户里的钱不够了");
                        return;
                    }
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    account.moneyNum = account.moneyNum - drawingMoney;
                    nowMoney = nowMoney + drawingMoney;
                    System.out.println("账户余额为:" + account.moneyNum);
                    //this.getName()=Thread.currentThread().getName(),因为继承了Thread
                    System.out.println(this.getName() + "目前手里有" + nowMoney + "钱");
                }
            }
        }
        //此时应该锁住的是Account对象而不是BankDrawing对象(this)
        
    • arraylist

      • package safe;
        
        import java.util.ArrayList;
        
        public class unsafeList {
            public static void main(String[] args) {
                ArrayList<String> arrayList=new ArrayList<>();
                for (int i = 0; i < 10000; i++) {
                    new Thread(()->{
                        synchronized (arrayList) {
                            arrayList.add(Thread.currentThread().getName());
                            System.out.println(Thread.currentThread().getName());
                        }
                    }).start();
        
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
        
                System.out.println(arrayList.size());
            }
        }
        //锁住arraylist对象就能保证arraylist是安全的
        

四、JUC编程

1.CopyOnWriteArrayList(并发工具包)

package safe;

import java.util.concurrent.CopyOnWriteArrayList;
//CopyOnWriteArrayList是一种线程安全的arraylist
public class JUCTest {
    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());
    }
}
//callable接口也是在java.util.concurrent包中

五、死锁

  • 多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。某一个同步块同时拥有“两个以上对象的锁”时,就可能发生“死锁”问题。
  • 产生死锁的四个必要条件:
    • 互斥条件:一个资源每次只能被一个进程使用
    • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
    • 不剥夺条件:进程已获得的资源,在未使用之前,不能强行剥夺 。
    • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
package safe;


//模拟死锁,死锁就是多个线程互相抱着对方需要的资源,形成僵持
public class DeadLock {
    public static void main(String[] args) {
        Makeup ping = new Makeup(0, "宋平");
        Makeup ping1 = new Makeup(1, "sp");
        ping.start();
        ping1.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 {
            girlMakeUp();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    //互相持有对方的锁,需要拿到对方的资源,形成死锁
    private void girlMakeUp() throws InterruptedException {
        if (choice==0){
            //此时已经锁了一个lipStick
            synchronized (lipStick){
                System.out.println(this.girlName+"拿到了口红的锁");
                Thread.sleep(1000);
                //拿另一个锁
                synchronized (mirror){
                    System.out.println(this.girlName+"拿到了镜子的锁");
                }
            }
        }else{
            //此时已经锁了一个lipStick
            synchronized (mirror){
                System.out.println(this.girlName+"拿到了镜子的锁");
                Thread.sleep(2000);
                synchronized (lipStick){
                    System.out.println(this.girlName+"拿到了口红的锁");
                }
            }
        }
    }

}
  • 解决方案
package safe;


//模拟死锁,死锁就是多个线程互相抱着对方需要的资源,形成僵持
public class DeadLock {
    public static void main(String[] args) {
        Makeup ping = new Makeup(0, "宋平");
        Makeup ping1 = new Makeup(1, "sp");
        ping.start();
        ping1.start();
        //两个线程已经分别拥有两个不同对象的锁,执行完成synchonized代码块中的内容后,释放锁,另一个对象便可以使用锁,因此不会造成死锁
    }
}

//口红类
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 {
            girlMakeUp();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    //互相持有对方的锁,需要拿到对方的资源,形成死锁
    private void girlMakeUp() throws InterruptedException {
        if (choice==0){
            //此时已经锁了一个lipStick
            synchronized (lipStick){
                System.out.println(this.girlName+"拿到了口红的锁");
                Thread.sleep(1000);
            }
            //拿另一个锁
            synchronized (mirror){
                System.out.println(this.girlName+"拿到了镜子的锁");
            }
        }else{
            //此时已经锁了一个lipStick
            synchronized (mirror){
                System.out.println(this.girlName+"拿到了镜子的锁");
                Thread.sleep(2000);
            }
            synchronized (lipStick){
                System.out.println(this.girlName+"拿到了口红的锁");
            }
        }
    }

}

六、Lock锁

  • 从JDK1.5开始,java提供了更强大的线程同步机制----通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
  • java.util.concurrent.locks.Lock(JUC包中)接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源前应先获得Lock对象。
  • ReentrantLock(可重入锁)类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁
  • ReentrantLock的加锁和释放锁一般写在try和finally代码块中
package safe;
import java.util.concurrent.locks.ReentrantLock;
//lock锁,显示调用释放锁,与synchornized效果差不多
public class ThreadLockTest {
    public static void main(String[] args) {
        LockTest lockTest = new LockTest();
        new Thread(lockTest, "王运好").start();
        new Thread(lockTest, "韩邦胜").start();
        new Thread(lockTest, "李洪军").start();
    }
}
//模拟多个线程买票
class LockTest implements Runnable {
    private final ReentrantLock lock=new ReentrantLock();
    int ticketNum = 1000;
    boolean falg = true;
    @Override
    public void run() {
        while (falg) {
            buy();
        }
    }
    private  void buy() {
            try{
                lock.lock();
                if (ticketNum > 0) {
                    System.out.println(Thread.currentThread().getName() + "--------" + ticketNum--);
                }else {
                    falg=false;
                }
            }finally {
                lock.unlock();
            }
        }
}
  • synchronized和Lock的对比
    • Lock是显式锁(手动开户和关闭锁)。synchronized是隐式锁,出了作用域自动释放
    • Lock只有代码块锁,synchronized有代码块锁和方法锁
    • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)。
    • 优先使用顺序
      • Lock->同步代码块(已经进入了方法体,分配了相应资源)->同步方法(在方法体之外)
posted @   是韩信啊  阅读(50)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
点击右上角即可分享
微信分享提示