Java基础(十九)——线程安全(同步锁和监视器)、线程阻塞(sleep和wait)的不同区域表现形式、唤醒方式、lock(互斥锁)、公平锁

Java基础(十九)——

一、线程安全(同步锁和监视器)

1、引子——线程不安全

举个例子引入安全锁:
10家店,同时卖1000部电脑。换成代码就是10条线程,争抢一个资源(代码如下):
在这里插入图片描述

这样会造成一个很严重的问题,就是会出现多个线程同时争抢一个资源,且存在多个线程同时抢到这个资源:
在这里插入图片描述

这种现象就是线程不安全

2、线程安全问题

线程安全问题:多个线程同时访问异步临界资源,可能会造成数据混乱。
异步:大家一起访问。

解决方法:

1、同步锁:synchronized

1.1 同步代码块:
需要同步的代码放在同步代码块中,添加同步锁和同步监视器要求多个线程访问的时候,使用的是同一个对象的同
步监视器当有线程进入同代码块,其他线程则会等待进入同步代码块的线程执行完毕后才会进入。

1.2 同步方法:
被同步锁修饰的方法为同步方法,同步监视器为方法本身当一条线程调用同步方法时,其他线程则会等待同步方法
执行完再调用,要求方法为静态方法或者使用同一个对象

3、同步锁(同步代码块)解决线程安全

同步代码块:

synchronized ( 这里需要放入同步监视器 ) {
    
    }

之所以要放入同步监视器,是因为这里创建了 10 条线程,每条都是独立的。如果访问的不是同一个对象,各自访问不同的对象,那就毫无意义,实现不了同步。只有所有线程都访问同一个资源,才能实现同步。

一般会定义一个临界资源,充当同步监视器:
在这里插入图片描述
注意:这个同步监视器必须为对象,如果是静态常量也不行。

最终代码:

public class Homework {
    public static void main(String[] args) {
        ExecutorService loop = Executors.newFixedThreadPool(10);    //  在线程池开10条线程
        Sell sell = new Sell();
        for (int i = 1; i < 11; i++) {
            loop.submit(sell);
        }
        loop.shutdown();
    }
}
class Sell implements Runnable{
    static int num = 1;     //  独享一份空间
    static final Object obj = new Object();

    @Override
    public void run() {
        while (num < 1001){
            //  synchronized 这个是同步锁。    obj 这个是监视器
            synchronized (obj){
                if (num < 1001){
                    System.out.println("第"+(Thread.currentThread().getId()-10)+"家外星人店出售第"+num+"台电脑");
                }
                num++;
            }
        }
        System.out.println("第"+(Thread.currentThread().getId()-10)+"家外星人店已售罄");
    }
}

注意:锁要放在 if 判断语句的外面。如果放在里面,会出现溢出现象。因为会存在同时有几条线程进来,其中一条线程已经到1000了,而后面排队的线程会继续取资源,接着出现1001这种现象(最多不会超过1000 + 线程数量)。

经测试可知:最终效果实现了,没有出现线程安全问题。

4、同步锁(同步方法)解决线程安全

a、继承方式

只需要在方法返回值前面加个 synchronized ,即是同步方法。不过要注意的是,不能直接加到 run 方法里面,需要另外开个方法。不过此时还不够,因为缺少同步监视器。同步方法监视器就是同步方法本身。 且必须保证每个线程调用的都是同一个方法,所以这个方法是要静态的,才能发挥同步监视器的作用。

代码:

public class Demo01 {
    public static void main(String[] args) {
        for (int i = 1; i < 11; i++) {      //  开启10条线程
            new MyThread("第"+i+"家").start();
        }
    }
}

class MyThread extends Thread{
    static int computer = 1;
    private boolean flag = true;    //  开关。控制循环是否继续。

    public MyThread(String name){   //  调用父类有参构造方法,给线程新的名字。因为线程本身能够调用 getName()方法。
        super(name);
    }

    @Override
    public void run() {     //  重写 run 方法
        while (flag){       //  while 循环不能加在同步方法里面。
            flag = sell();
        }
    }

    //  同步方法的监视器为方法本身,且方法为静态的
    public synchronized static boolean sell(){
        if (computer<1001){
            System.out.println(Thread.currentThread().getName()+"外星人专卖店卖出第"+computer+"台电脑");
            computer++;
            return true;
        }else {
            System.out.println(Thread.currentThread().getName()+"外星人专卖店已售罄");
            return false;
        }
    }

b、实现方式

上面是通过继承的方式实现的。如果是实现接口的方式,下面这个地方要改:
在这里插入图片描述
这里可以有两个参数。

图解:
在这里插入图片描述

5、回顾 StringBuffer 和 StringBuilder

这两个以前提到过,看其底层代码可知哪个线程安全:
在这里插入图片描述
结论是:StringBuffer 安全。

二、让线程陷入阻塞的方法

1、了解wait()

前面学过,让线程陷入阻塞的有 sleep(),是通过 Thread 直接调用的。 wait()也能让线程陷入阻塞,通过对象调用。wait()来自 Object 类。

2、wait 和 sleep 在同步代码块内的不同表现

测试,wait 和 sleep 在同步代码块中的不同表现。如果线程陷入阻塞了,其他线程还能进来,说明该线程的资源被释放了;反之,如果不能进来,说明该线程的资源没有被释放,其他线程还在等待。

a、sleep 在同步区域内休眠

测试代码:

class MyThread extends Thread{  //  测试休眠是否会释放资源
    public void run(){
        synchronized (Demo01.obj){
            System.out.println(Thread.currentThread().getName()+"线程A开始休眠");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"线程A结束休眠");
        }
    }
}

b、wait 在同步区域内等待

class MyThread2 extends Thread{ // 测试等待是否会释放资源
    public void run(){
        synchronized (Demo01.obj){
            System.out.println(Thread.currentThread().getName()+"线程Z开始等待");
            try {
                Demo01.obj.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"线程Z结束等待");
        }
    }
}

结果:
在这里插入图片描述

可以看到,其他线程进来了。

c、结论——同步区域内

经测试得出结论:
线程在同步区域内进入 休眠 状态,线程不会释放资源。

线程在同步区域内进入 等待 状态,线程会释放资源。

d、wait()必须通过同步监视器的对象进行调用

在这里插入图片描述
这里可以看到,都是同一个对象。如果换一个对象,也同样是静态常量,就会报错:
在这里插入图片描述

在这里插入图片描述

然后运行结果:
在这里插入图片描述

报错了。

3、wait 和 sleep 在同步区域外中的不同表现

a、sleep

在同步区域外是否可以休眠。

以往使用 sleep 的时候,就没有同步区域,那时候也没有报错。
在这里插入图片描述

b、wait

在这里插入图片描述
可以看到,报错了。

c、结论

wait 只能在同步区域中使用。

4、不同对象无法在同步中使用 wait()

准确点说:同一个对象的同步方法里面使用当前对象调用。

这里直接说能够实现的方法:

通过实现接口的方式实现线程,然后通过 this 的方式去调用 wait()方法,才能成功调用,其他方式都不行。

代码:

class MyThread2 implements Runnable{	//	通过实现接口的方式。继承方式不行
    @Override
    public void run(){
        method();
    }

    private synchronized void method() {	//	这里没有 static
        System.out.println(Thread.currentThread().getName()+"开始等待");
        try {
            this.wait();	//	这里只能用 this 。如果是通过类直接调用同步监视器,不行。
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"结束等待");
    }
}

5、sleep 和 wait 的各自唤醒方法

直接上结论:

sleep :顾名思义,休眠一段时间会自己唤醒。所以 sleep 会自动唤醒。

wait : 需要 notify() 或者 notifyAll() 来唤醒。其中, notifyAll()是唤醒全部线程;而 notify 是华唤醒一条线程。

a、唤醒一条线程

在这里插入图片描述
在这里插入图片描述
结果:
在这里插入图片描述

b、唤醒全部线程

在这里插入图片描述

三、lock——手动上锁(互斥锁/可重入锁)

1、初识 lock

先看下有什么方法:
在这里插入图片描述

2、lock 的使用

需要实例化对象:
在这里插入图片描述

然后给需要上锁的代码块上锁:
在这里插入图片描述
这样,设置好代码以后,就能实现同步的效果。

同样是用卖外星人电脑举例子。这里采用继承的方式创建线程:

public class Demo01 {
    public static void main(String[] args) {
        for (int i = 1; i < 11; i++) {      //  开启10条线程
            new MyThread("第"+i+"家").start();
        }
    }
}

class MyThread extends Thread{
    static int computer = 1;
    private static ReentrantLock lock = new ReentrantLock();    //  如果通过线程的方式创建线程,为了保证每一条线程用的都是同一个锁,这里需要加 static

    public MyThread(String name){   //  调用父类有参构造方法,给线程新的名字。因为线程本身能够调用 getName()方法
        super(name);
    }

    @Override
    public void run() {     //  重写 run 方法
        while (true){
            lock.lock();    //  手动上锁
            try {
                if (computer<1001){
                    System.out.println(Thread.currentThread().getName()+"外星人专卖店卖出第"+computer+"台电脑");
                    computer++;
                }else {
                    System.out.println(Thread.currentThread().getName()+"外星人专卖店已售罄");
                    break;
                }
            }finally {
                lock.unlock();  //  既然最终都要执行这个解锁,就放到 finally 里面
            }
        }
    }

四、公平锁

1、了解公平锁和非公平锁

这里需要解释下公平锁和非公平锁的概念:

公平:每条线程轮流拿取资源,每条线程都有份,就是公平锁。

非公平:可能存在一条线程拿取多个资源,就是非公平锁。非公平锁的线程拿取资源是随机的

下图这种就是不公平的:
在这里插入图片描述
电脑都被第二家店铺垄断了。

2、公平锁的创建方式

公平锁的创建方式很简单:
在这里插入图片描述
只需要在创建 lock 的时候,里面传递 true 这个参数,就是公平锁了。

3、对公平锁的误解

有时候创建了公平锁,10条线程,但是拿取资源的时候并不是每个资源都轮流拿,这是怎么回事呢?

首先,这里确实是公平锁了。不能出现每条线程轮流拿取资源是因为有的线程太快了,在其他线程还没创建的时候,就立马又回来拿资源。可以手动增加线程的工作时间,跳到一个适当的空隙,就能看到每条线程轮流拿取资源。但要承认的事实是,确实这里已经是公平锁。

举个例子:几个人轮流买奶茶,第一个人买完奶茶走了,但是第二个人离奶茶店还有点距离,第一个人买完就立马转身回来买第二杯奶茶。如果增加第一个人买完奶茶后回奶茶店买奶茶的距离,就能看到轮流买奶茶的景象。就是这个道理。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>