浅谈synchronized和lock

本文介绍了在多线程环境下,使用synchronized和Lock进行资源同步的原因和方法。同步代码块和同步方法是synchronized的两种形式,用于保证对共享资源的线程安全访问。而Lock接口(如ReentrantLock)提供了更细粒度的锁控制,需要手动解锁,具有更高的灵活性。文章通过示例分析了synchronized在不同情况下的效果,并展示了Lock的使用示例。
摘要由CSDN通过智能技术生成

浅谈synchronized和lock

1.1为什么要加锁?

多线程情况下,同时操作同一个共享资源的时候,可能会出现业务安全问题。这时候就需要给共享资源上把锁,只能一个进去,操作完后,再让下一个进去,防止共同操作出现问题。

1.2加锁的三种方法

同步代码块

同步方法

lock锁

1.2.1同步代码块

格式:

synchronized (//对象){
             //访问共享资源的核心代码
                    }

可以锁一个list等引用类型数据,但必须是同一个对象,否则无意义。

对于实例方法建议使用this作为锁对象

image-20230724160043287

对于静态方法建议使用类名.class 对象作为锁对象

image-20230724155507253

不推荐锁字符串

synchronized ("字符串"){
             //访问共享资源的核心代码
                    }

这样是没有任何意义,字符串不可变的且在字符串常量池中共享,那么相当于没有加锁。

用同步代码块做案例(看完1.2.2.2的案例再回看)

public class SynBlockTest {
    public static void main(String[] args) {
        // 1.定义抽奖池
        List<Integer> list = new ArrayList<>();
        Collections.addAll(list,10,5,20,50,100,200,500,800,2,80,300);
        // 2.定义线程类,可以接收线程名称和集合作为参数
        new MyThread("抽奖箱1",list).start();
        new MyThread("抽奖箱2",list).start();
    }
}
class MyThread extends Thread{
    private List<Integer> list;
    public MyThread(String name, List<Integer> list){
        super(name);
        this.list = list;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (list) {
                /*不加这个,list集合删除完了,第二线程进入list.get 会出错*/
                if (list.size() == 0) {
                    break;
                }
                    Random r = new Random();
                    int num = r.nextInt(list.size());
                    System.out.println(Thread.currentThread().getName() + "又产生了一个" + list.get(num) + "元大奖");
                    list.remove(num);
            }
        }
    }
}

锁代码块锁的是list,在代码中list只有一份,这样操作可以实现只让一个线程进去,另一个线程等待。

1.2.2同步方法

格式:

修饰符 synchronized 返回值类型 方法名称(形参列表) {
          操作共享资源的代码
     }

同步方法底层原理

  • 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
  • 如果方法是实例方法:同步方法默认用this作为的锁对象。
  • 如果方法是静态方法:同步方法默认用类名.class作为的锁对象。
1.2.2.1一个简单的案例

需求:

1.有一个抽奖池,该抽奖池中存放了奖励的金额,该抽奖池用一个集合表示,集合存储了10,5,20,50,100,200,500,800,2,80,300了这些数据。
2.创建两个抽奖箱(线程)设置线程名称分别为“抽奖箱1”,“抽奖箱2”,随机从集合中获取奖项元素(要求已经抽取过的不能再抽取)直到抽完为止并打印在控制台上

public class SynMethonTest {
    static ArrayList<Integer> list = new ArrayList<>();
    public static void main(String[] args) throws InterruptedException {
        //抽奖池
        Collections.addAll(list, new Integer[]{10, 5, 20,50,100,200,500,800,2,80,300});
        //创建两个线程
        Thread2 thread1 = new Thread2("抽奖箱1", list);
        Thread2 thread2 = new Thread2("抽奖箱2", list);
        thread1.start();
        thread2.start();
    }
}
class Thread2 extends Thread{
  private  List list;
    //取名字,赋值list
    public Thread2(String name, List list) {
    super(name);
    this.list=list;
    }
    @Override
    public  void run() {
        //执行线程方法
        while (list.size()>0) {
            try {
                remove();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

    }
     synchronized void remove() throws InterruptedException {
        Random r = new Random();
        String name=Thread.currentThread().getName();
        if (list.isEmpty()){
            return;
        }
                int num = r.nextInt(list.size());
                System.out.println(name +"又产生了一个"+list.get(num)+"元大奖");
                list.remove(num);
    }
}

可以看看这写的是否有问题

答案是有问题。

运行后会发现索引出错

Exception in thread "抽奖箱2" java.lang.IndexOutOfBoundsException: Index 8 out of bounds 

那为什么会出现这个问题呢?不是给方法添加了锁了吗?

这时候可以回顾关于锁的知识。要锁同一个对象,这里锁方法,也就是锁实例方法,然后main方法中创建了两个Thread2线程对象,两个对象中运行两个实例方法,两个实例方法各不影响,所以 synchronized void remove() 方法并没有想象那样只有一个线程进来,另一个线程在锁方法外等待,而是能一起进去。

1.2.2.2debug多线程

断点main方法,右键断点,选择Thread,选择Done

image-20230724163531587

断点锁方法,重复操作

image-20230724163637347

执行完main方法

image-20230724164013902

选择线程一

image-20230724164129815

操作线程一进入方法,然后切换线程2

image-20230724164303672

image-20230724164413284

可以看到线程2也可以进入锁方法

image-20230724164436003

这里案例一开始为什么会出现索引越界就很明朗了,两个线程都能进入这个锁方法,并没有锁的效果。

1.2.2.3案例的代码的修改
public class SynMethonTest {
    /*如果要main现在用到类变量,要把这类变量变成静态的*/
    static ArrayList<Integer> list = new ArrayList<>();
    public static void main(String[] args) throws InterruptedException {
        //抽奖池
        Collections.addAll(list, new Integer[]{10, 5, 20,50,100,200,500,800,2,80,300});
        //创建两个线程
        Thread2 thread1 = new Thread2("抽奖箱1", list);
        Thread2 thread2 = new Thread2("抽奖箱2", list);
        thread1.start();
        thread2.start();
    }
}
class Thread2 extends Thread{
 static private  List list;
    //取名字,赋值list
    public Thread2(String name, List list) {
    super(name);
    this.list=list;
    }
    @Override
    public  void run() {
        //执行线程方法
        while (list.size()>0) {
            try {
                //不同类中怎么使用其他类的方法,只能静态、创对象?
                remove();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

    }
   static   synchronized void remove() throws InterruptedException {
        Random r = new Random();
        String name=Thread.currentThread().getName();
        if (list.isEmpty()){
            return;
        }
                int num = r.nextInt(list.size());
                System.out.println(name +"又产生了一个"+list.get(num)+"元大奖");
                list.remove(num);
    }
}

这里将线程内的remove 方法添加锁,同时声明为静态方法。静态方法在内存只有一份,则就满足操作同一个对象,锁就生效了。

image-20230724165223492

1.2.3Lock锁

格式:

  // 创建Lock对象
        Lock lock = new ReentrantLock();
      
         lock.lock(); // 加锁
            try {
                .....
            } finally {
                lock.unlock(); // 解锁
            }

用完一定释放锁,同try/catch/fianlly 方式保证能最后释放锁

上面案例用lock锁的做法

public class BlockTest{
    public static void main(String[] args) {
        // 抽奖池
        List<Integer> list = new ArrayList<>();
        Collections.addAll(list, 10, 5, 20, 50, 100, 200, 500, 800, 2, 80, 300);
        // 创建Lock对象
        Lock lock = new ReentrantLock();
        // 创建两个线程,传入同一个List和Lock对象
        Thread1 thread1 = new Thread1("抽奖箱1", list, lock);
        Thread1 thread2 = new Thread1("抽奖箱2", list, lock);
        thread1.start();
        thread2.start();
    }
}

class Thread1 extends Thread {
    private final String name;
    private final List<Integer> list;
    private final Lock lock;

    public Thread1(String name, List<Integer> list, Lock lock) {
        super(name);
        this.name = name;
        this.list = list;
        this.lock = lock;
    }

    @Override
    public void run() {
        // 执行线程方法
        while (true) {
            lock.lock(); // 加锁
            try {
                if (list.size() > 0) {
                    Random r = new Random();
                    int num = r.nextInt(list.size());
                    System.out.println(name + "又产生了一个" + list.get(num) + "元大奖");
                    list.remove(num);
                } else {
                    break;
                }
            } finally {
                lock.unlock(); // 解锁
            }
        }
    }
}

可发现,一样是产生两个实例,用lock锁并不会出现锁方法的问题。

线程一操作进入后,切换线程二无法进入

image-20230724170615521

1.3总结

如果能用锁代码块就代码块,如果考虑锁方法要考虑是否只有一份对象的问题。使用 synchronized 关键字修饰的同步方法是以实例为单位进行同步的,每个实例都有自己的锁。不同的实例之间互相不影响。而使用 lock 时,可以明确地创建一个锁对象,并且多个实例之间可以共享同一个锁对象,所以可以更加灵活地控制线程的同步。

使用lock锁必须要释放锁,用try/catch/finally 强制最后释放锁,防止死锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值