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

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

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

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

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

  • 超卖问题分析
  • 同步代码块
    • 同步代码块的两个小细节
    • 同步方法
    • 探讨StringBuffer与StringBuilder
  • Lock锁

超卖问题分析

在上一篇文章的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关键字,即每个方法都是同步方法:
多线程&JUC:解决线程安全问题——synchronized同步代码块、Lock锁_第1张图片
而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();
            }
        }
    }
}

你可能感兴趣的:(多线程&JUC,安全,java,面试,javase,JUC)