多线程&JUC:解决线程安全问题——synchronized同步代码块、Lock锁

👨‍🎓作者简介:一位大四、研0学生,正在努力准备大四暑假的实习
🌌上期文章:多线程&JUC:线程的生命周期与安全问题
📚订阅专栏:多线程&JUC
希望文章对你们有所帮助

上一部分讲解了面试可能会问的线程的生命周期,并且演示了超卖问题来讲解多线程并发的安全问题,超卖问题这是一个经典例子,这里会解释一下解决的方法。
如果是想要解决集群下的线程安全问题,可以学习我在做Redis项目的时候的解决方法:
Redis:原理速成+项目实战——Redis实战8(基于Redis的分布式锁及优化)
Redis:原理速成+项目实战——Redis实战9(秒杀优化)

感兴趣还可以看看如何使用异步下单来实现秒杀,这些实现其实都跟线程的思想都是相关的:
Redis:原理速成+项目实战——Redis实战10(Redis消息队列实现异步秒杀)

超卖问题分析

在上一篇文章的demo中,发现了线程安全问题,不仅同样的票出现了多次,还出现了超出范围的票。
可以看关键的两条代码:

ticket++;
System.out.println("在卖第" + ticket + "张票");

由于CPU执行代码的过程中,其执行权随时会被其他的线程抢走,所以这样的代码会出现一些问题:假设线程1已经执行完了ticket++,还没来得及执行输出语句,线程2就参与了ticket++的操作,这时候就有可能出现输出同一张票的情况。而当ticket=99的时候,若三个线程同时进入if条件,这时候就很可能出现ticket>100的情况,也就是超卖现象。

同步代码块

由于上述的问题,我们可以想到一个方案,就是当有线程抢夺到CPU执行权的时候,将执行的代码全部锁起来,使得其他线程无法执行代码,这样就不会发生上面的问题。
将其锁起来,需要使用到关键字synchronized,格式如下:

synchronized(){
	//操作共享数据的代码
}

因此接下来需要编写一下这个锁对象,需要满足以下特点:

1、锁默认打开,有一个线程进去了,锁自动关闭
2、里面的代码全部执行完毕,线程出来,锁自动打开

这个锁对象,只要能保证是唯一的,那么锁对象可以非常随意的去定义,这种方式就叫作同步代码块,代码如下:

public class MyThread extends Thread {

    static int ticket = 0;

    //锁对象,一定要是唯一的,可以加static关键字
    static Object obj = new Object();

    @Override
    public void run() {
        while(true){
            synchronized (obj) {
                if(ticket < 100){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    ticket++;
                    System.out.println(getName() + "正在卖第" + ticket + "张票!");
                }else{
                    break;
                }
            }
        }
    }
}

同步代码块的两个小细节

1、synchronize这个关键字,我写到了while里面,这部分是不能写到while外面的,不然的话,就会出现100张票只被1个窗口卖光,显然是不符合现实场景的。

2、锁对象必须要是唯一的,学操作系统的时候就学过临界资源,意思其实是一样的,因此可以发现上面代码中的锁对象obj是加上了static关键字的。除了这种方法,其实更常见的方法是使用字节码对象,因为字节码对象是唯一的,因此上述的锁可以写成:

synchronized (MyThread.class){
	//...
}

同步方法

如果我们要将一个方法里面的所有方法都锁起来,那就没必要锁代码片段,而是锁住整个方法了。
同步方法,就是把synchronized关键字加到方法上,格式:

修饰符 synchronized 返回值类型 方法名(方法参数) {…}

同步方法有2个特点:

1、同步方法会锁住方法里面所有的代码
2、锁对象不能自己指定,而是java自己默认规定好的:
(1)非静态方法:this
(2)静态方法:当前类的字节码文件对象

3窗口卖100张票的问题也可以用同步方法来解决:

1、定义MyRunnable类,实现Runnable接口,而里面的ticket没必要再设置成静态的了,因为主程序中只会将MyRunnable类创建一次,作为一个参数传递到线程中。

public class MyRunnable implements Runnable{

    int ticket = 0;

    @Override
    public void run() {
        while (true){
            if (method()) break;
        }
    }

    //这里的锁对象为this,由于主程序中MyRunnable对象是唯一的,因此锁对象也是唯一的
    private synchronized boolean method() {
        if (ticket == 100){
            return true;
        }else{
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            ticket++;
            System.out.println(Thread.currentThread().getName() + "在卖第" + ticket + "张票");
        }
        return false;
    }
}

2、编写测试类代码:

	public static void main(String[] args) {
        MyRunnable mr = new MyRunnable();

        Thread t1 = new Thread(mr);
        Thread t2 = new Thread(mr);
        Thread t3 = new Thread(mr);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }

探讨StringBuffer与StringBuilder

我自己在使用字符串拼接的时候,很喜欢使用StringBuilder,而且也阅读过底层的源码,这是一种效率很高的方式,而如果打开api帮助文档,可以发现StringBuffer和StringBuilder几乎是一样的方法,完成的功能也是一样的,而java为什么要设置两个功能一样的类呢?

打开StringBuffer的底层源码,我们可以发现StringBuffer的所有方法都有带有synchronized关键字,即每个方法都是同步方法:
在这里插入图片描述
而StringBuilder底层是没有这个关键字的,因此StringBuffer在多线程下是安全的,满足了线程同步的特点。

当我们实现需求的时候,如果是多线程的,就使用StringBuffer,否则就使用StringBuilder(StringBuffer是会损耗一些时间的)。

Lock锁

synchronized的锁对象是自动开关的,而Lock锁可以时间手动的开关锁,Lock的实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作。

Lock中提供了获得锁和释放锁的方法:

void lock():获得锁
void unlock():释放锁

Lock是接口,不能直接实例化,所以要采用它的实现类ReentrantLock来实例化,直接使用它的空参构造即可。

使用Lock锁,则MyRunnable类(若是MyThread类,由于会被创建多次,锁又必须要唯一,那么Lock前面就得加上static)应修改为:

public class MyRunnable implements Runnable{

    int ticket = 0;

    Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true){
            lock.lock();
            if (ticket == 100){
                break;
            }else{
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                ticket++;
                System.out.println(Thread.currentThread().getName() + "在卖第" + ticket + "张票");
            }
            lock.unlock();
        }
    }
}

这样写,线程安全问题确实不会发生,但是程序却没办法终止。
因为当我们的票数为100时,我们直接break跳出循环了,所以没有执行释放锁的语句,其他的线程就在while循环里面一直等待锁的释放,这显然不合理,一种简单的解决方法是在if里面继续加一条释放锁的语句:

if(ticket == 100){
	lock.unlock();
	break;
}

这样的方式固然可行,但是这写了两次unlock不是很符合规范。
更规范的方式是使用try...catch...finally,无论如何,程序最终都必须要执行finally里面的语句,上述代码最终可以改写为:

public class MyRunnable implements Runnable{

    int ticket = 0;

    Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true){
            try {
                lock.lock();
                if (ticket == 100){
                    break;
                }else{
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    ticket++;
                    System.out.println(Thread.currentThread().getName() + "在卖第" + ticket + "张票");
                }
            } catch (RuntimeException e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();
            }
        }
    }
}
  • 21
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: JUCJava.util.concurrent的缩写,提供了许多并发编程的工具类,其中就包括了解决多线程原子性问题的类。 在JUC中,提供了多个原子类,例如AtomicInteger、AtomicLong等,这些类可以保证对其操作的原子性,也就是说,对它们进行读写操作时,不会出现数据不一致的情况。 下面是一个使用AtomicInteger解决多线程原子性问题的示例代码: ```java import java.util.concurrent.atomic.AtomicInteger; public class AtomicExample { private AtomicInteger count = new AtomicInteger(0); public void increment() { count.incrementAndGet(); } public int getCount() { return count.get(); } } ``` 在这个示例中,count是一个AtomicInteger类型的变量,它的incrementAndGet()方法可以保证对它进行操作的原子性,即使有多个线程同时对它进行操作,也不会出现数据不一致的情况。 因此,使用JUC提供的原子类可以很方便地解决多线程原子性问题。 ### 回答2: JUCJava Util Concurrent)是Java并发实用工具包,在解决多线程原子性问题上提供了丰富的解决方案。下面是JUC中常用的两种解决方案,以代码示例的形式展示。 1. synchronized关键字 synchronized关键字是Java中最基本的同步机制,通过给关键代码块或方法加,确保同一时间只能有一个线程执行该代码块或方法,以实现原子性操作。 ```java public class Counter { private int count; public synchronized void increment() { count++; } } ``` 2. Atomic类 Atomic类是JUC中提供的一组原子操作类,它们利用底层的CAS(Compare and Swap)机制实现原子性操作。CAS机制通过比较内存中的值与期望值,若相等则修改为新值,若不相等则重新尝试,直至更新成功。Atomic类可实现基本类型和引用类型的原子操作。 ```java import java.util.concurrent.atomic.AtomicInteger; public class Counter { private AtomicInteger count = new AtomicInteger(0); public void increment() { count.incrementAndGet(); } } ``` 以上是JUC解决多线程原子性问题的两个常用方案。synchronized关键字通过加实现,而Atomic类则利用CAS机制实现,二者都能保证多线程环境下的原子性操作。根据具体的业务场景和性能要求,选择合适的方式解决多线程原子性问题。 ### 回答3: JUCJava并发编程工具包)是Java提供的用于解决多线程并发问题的工具包,其中包含了很多用于处理线程安全的类和接口。 JUC解决多线程原子性问题的方式主要是通过提供原子类来实现。原子类是一种可以单独访问和修改的变量类型,它们可以以原子方式执行操作,保证了操作的原子性。 下面是一个使用JUC提供的原子类AtomicInteger来解决多线程原子性问题的示例代码: ```java import java.util.concurrent.atomic.AtomicInteger; public class AtomicityExample { private static AtomicInteger counter = new AtomicInteger(0); public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(new IncrementTask()); Thread thread2 = new Thread(new IncrementTask()); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println("Counter: " + counter); } static class IncrementTask implements Runnable { @Override public void run() { for (int i = 0; i < 10000; i++) { counter.incrementAndGet(); // 使用原子方式将当前值加1 } } } } ``` 在上述示例代码中,使用AtomicInteger类来声明了一个原子变量counter。在IncrementTask任务中,每次循环通过调用incrementAndGet()方法对counter的值进行原子自增操作。 使用JUC提供的原子类可以确保多线程环境下对变量的操作是原子性的,避免了出现竞态条件等线程安全问题

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

布布要成为最负责的男人

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

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

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

打赏作者

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

抵扣说明:

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

余额充值