Java 从多线程到并发编程(六)—— 并发 同步 锁 阻塞 synchronized四种应用形式

前言 ´・ᴗ・`

  • 继上一次我们主要学习了线程礼让yield 线程强制执行join 线程自我阻塞sleep等相关的知识

  • 本节将会帮助你了解…

    • 并发 同步 锁 阻塞 互斥 临界资源 synchronized概念浅析
    • synchronized 四种 实现形式的理解以及demo

毕竟线程不讲 同步与线程安全 等于没怎么讲2333

这一节可以说是解决生产者消费者问题的基础之一,好好学:)

并发 不安全案例

在前面我们有个例子 用Runnable 实现 三个robot抢票这个案例 如代码:

package com.base.threadlearning.threadVSrunnable;



public class RunnableTicket implements Runnable{
    private static final int TICKETS=10;
    private int tickets;
    private boolean flag;

    RunnableTicket(){
        tickets=TICKETS;
        flag=true;
    }
    public void setFlag(Boolean flag){
        this.flag = flag;
    }

    @Override
    public void run(){
        while(flag){
            if(tickets<=0) break;

            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+" get the "+tickets--+" tickets");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        RunnableTicket ticket_station = new RunnableTicket();
        String[] Robot_array = {"Robot A","Robot B","Robot C"};

        for(String i : Robot_array){
            new Thread(ticket_station,i).start();
        }
        Thread.sleep(4000);
        TicketController ticketController = new TicketController(ticket_station);
        new Thread(ticketController).start();
    }
}

这里多个线程访问同一个对象资源 被称为并发

那么这里我们的逻辑 处理并发 导致最后票竟然会出现-1这种情况,如图:
在这里插入图片描述

但流程控制中我们有
在这里插入图片描述
当小于等于0的时候停止挖票的逻辑 所以这个处理并发的逻辑问题出在哪?
在这里插入图片描述
这里 线程执行相互独立 假设有一个线程A跑到1这个地方 这时tickets 给我们公开的信息是还剩一张 但实际上线程A应该算是拿到票了 只不过等待一下网络请求的200ms

就好像你买东西 都把东西放到收银台了(预定了) 就差付钱了

这时线程B看到 诶 还有1张 我还可以抢 于是他也开抢 他也预定了这张票

这就相当于本来只有一张票 却好像有两张票都能被预定一样

如果还有线程CDEFG 那要多少有多少 —— 只要第一个线程A没有更新tickets值

或许有个思路就是 我预约完立即更新tickets值 但实际上 因为tickets值在服务器 你客户端请求服务器当然需要时间 所以不可能实现

那么就只剩下一个更加直接的方法 上锁 也被称为同步

说白了 好像上厕所 咱们一个一个来 上厕所锁门 后面的人就得等着 不能蜂拥而上 只能串行执行

线程同步 互斥体 阻塞 锁 感性理解

既然如此 我们只能想上厕所一样 排好队 这可以被称为 在对象的等待池中 形成队列

还记得第一篇文章讲进程的时候 我们说单核CPU只能同时为一个进程提供计算服务
而其他进程只能干瞪眼 乖乖排队就绪 直到自己的优先级达到了才行 这里 这个特点上也非常类似

如果你想让一个方法 实现像厕所一样的效果 也就是调用此方法必须同步执行 调用线程只能一个一个操作

其实我们这种同步方式也有个名字 称为互斥体,它实现了“互相排斥”(mutual exclusion)同步的简单形式(所以名为互斥体(mutex)),其定义就是 互斥体禁止多个线程同时进入受保护的代码“临界区”(critical section),线程与线程之间互斥

我们称 占用当前对象的线程 也就是上到厕所的线程 拿到了锁 即你终于可以上厕所的时候 上了一把锁一样
上完厕所后你就释放(release)了锁(解开了锁 否则别人上不了)

另一个角度来说 上不了厕所 在等的那批人 也就是被阻塞了 被门挡住了

这个时候就非常低效 因为等待的人只能无所事事

当然如果他们顺便玩玩手机刷刷抖音 那就说明他们是多线程操作——上厕所的线程阻塞了 不妨碍其他刷抖音的线程跑起来啊

https://www.cnblogs.com/weibanggang/p/9470718.html

Synchronized——同步的简单实现

我们可以使用synchronized 这个关键字 实现互斥 线程同步的效果 这个关键字可以用于方法、代码块、类、静态方法(用大括号括起来即可)

但是无论如何 其修饰的 或者说作用的根本 是对象

比方说 如果两个线程各自作用一个对象 那么你再synchronized这两个对象 这两个线程依然可以交替 “多线程”的“并行”执行

因为每个对象(比如 类似我们之前的案例 两个卖票站)只遇到一个线程在抢票 线程与线程之间并不能形成互斥

但是如果你理解为作用的是那个方法 你会以为同步的是方法那个函数对象 那你就以为这里线程会同步执行

事实上我们确实只需要保证所谓线程是互斥的 访问 临界资源 即一个一个来上厕所 所以锁的是临界资源 锁的是坑位 不是锁方法 或者代码块本身

我们直接上例子你就明白了:
这里 三个挖票机器人vs一个售票处

package com.base.threadlearning.threadState;
// 队列+锁机制 保证线程安全 否则同时操作同一个对象并发就会出问题
// 线程持有锁 会导致其他线程挂起 time wait
// 优先级高的线程等待一个优先级低的线程释放锁 会导致优先级倒置!

public class ThreadSync {
    public static void main(String[] args) {
        // 同一个ticket station
        ticket station = new ticket();

        new Thread(station,"a ").start();
        new Thread(station,"b ").start();
        new Thread(station,"c ").start();
    }

}


class ticket implements Runnable{
    private int ticket = 10;
    boolean flag = true;
    @Override
    public void run() {
        while (flag){
            try {
                buy();
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
    
    private synchronized void buy() throws InterruptedException {
        if(ticket<=0) {
            flag = false;
            return;
        }
        Thread.sleep(100);
        System.out.println(Thread.currentThread().getName()+"get "+ticket--);
    }
}

这里执行确实是同步执行的 三个robot互斥 结果如下:

a get 10
c get 9
b get 8
c get 7
a get 6
c get 5
b get 4
c get 3
a get 2
c get 1

Process finished with exit code 0

那三个挖票机器人VS三个售票处 各自在各自的售票处挖 1v1

public class ThreadSync {
    public static void main(String[] args) {
        // 同一个ticket station
//        ticket station = new ticket();

        new Thread(new ticket(),"a ").start();
        new Thread(new ticket(),"b ").start();
        new Thread(new ticket(),"c ").start();
    }
}
// 其他部分代码相同 就不写了

结果:

b get 10
c get 10
a get 10
a get 9
c get 9
b get 9
c get 8
a get 8
b get 8
a get 7
b get 7
c get 7
c get 6
b get 6
a get 6
a get 5
c get 5
b get 5
c get 4
a get 4
b get 4
a get 3
c get 3
b get 3
a get 2
c get 2
b get 2
a get 1
c get 1
b get 1

我们可以看到 三个线程并没有互斥 彼此并不干扰 而且如果你三个三个看 (每个售票处的1号票 2号票······)你可以发现robot顺序不同 bca - > acb -> bac 这也表现了线程并没有同步执行

那么我们这里修饰了buy方法 如果换成run 会怎么样?

synchronized的四种修饰形式解读

这里我给你个表:
作用对象就是那些本应该互斥的线程 线程(挖票机)调用售票处的方法(售票终端)进行挖票 或者说正在等厕所坑位空闲的我。。。

上锁对象就是 所谓临界资源 需要保护的资源 也就是厕所坑位

修饰内容上锁对象作用对象(互斥体)
代码块(同步代码块)代码调用这个代码块的对象
方法(同步方法)方法调用这个方法的对象
静态方法静态方法所属类的所有对象实例(静态方法属于类信息)
括号范围类的所有对象实例

上面这张表就是四种修饰形式

那你说改成run有区别吗?答案是没有

因为作用(使同步)的对象还是那三个挖票机 而临界资源也没什么本质变化 因为是三个售票处 三个售票处各自tickets-- 以及网络请求300ms延时 所以出来的效果一样的 如下:

a get 10
b get 10
c get 10
b get 9
a get 9
c get 9
a get 8
b get 8
c get 8
c get 7
a get 7
b get 7
b get 6
c get 6
a get 6
b get 5
c get 5
a get 5
c get 4
b get 4
a get 4
a get 3
b get 3
c get 3
b get 2
a get 2
c get 2
c get 1
a get 1
b get 1

还是各自为政 各挖各的售票处

那如果对同一个售票处 我修饰run会怎么样?也即是真正的并发情况下

这时代码如下:

package com.base.threadlearning.threadState;

public class ThreadSync {
    public static void main(String[] args) {
        // 同一个ticket station
        ticket station = new ticket();

        new Thread(station,"a ").start();
        new Thread(station,"b ").start();
        new Thread(station,"c ").start();
    }

}
class ticket implements Runnable{
    private int ticket = 10;
    boolean flag = true;
    @Override
    public synchronized void run() {
        while (flag){
            try {
                buy();
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

    private void buy() throws InterruptedException {
        if(ticket<=0) {
            flag = false;
            return;
        }
        Thread.sleep(100);
        System.out.println(Thread.currentThread().getName()+"get "+ticket--);
    }
}

这时的结果也是绝了:

a get 10
a get 9
a get 8
a get 7
a get 6
a get 5
a get 4
a get 3
a get 2
a get 1

感觉a承包了这个station售票处。。为什么?

我们这里的临界资源 上锁的东西变为整个售票处 意味着只要a挖票没挖完 永远不释放售票处的锁 那么b c都别想进售票处操作

而前面我们上锁的只是买票的函数接口buy而已 也就是你a买完一张 就把锁释放了
接下来我们a b c都可以买 全看调度器怎么抉择而已

同步代码块的使用

这个和同步方法差不多·一般是做等效 比如 我们利用同步代码块等效一下 同步run方法 就可以这么写:

class ticket implements Runnable{
    private int ticket = 10;
    boolean flag = true;
    @Override
    public synchronized void run() {
        synchronized (this){
            while (flag){
                try {
                    buy();
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }


    }

那么为啥还要用同步代码块呢?我的理解是 很多情况下函数并非最小功能的执行单位 或者说需求同步的最小单位 那么函数粒度不够 就用同步代码块即可

比如 函数可能涉及很多局部变量 但是真正需要同步上锁的只有计算部分 那干脆保护更核心的部分好了:)

同步“静态”对象

所谓静态的对象 就是类和静态的方法

为什么说是静态的 因为类的属性以及其静态方法是写死在class字节码文件里面的 这些配置不变

因此就会有很有意思的情况
比如对于第三种方式 当你synchronized一个静态方法 然后线程使用它的时候 这个静态方法本来作为一个类似工具 用来即时计算的东西 但实际上却进行了同步 里面的数据是被保护的 我们看个例子

package com.base.threadlearning.threadState;

public class ThreadSync {
    public static void main(String[] args) {

        new Thread(new ticket(),"a ").start();
        new Thread(new ticket(),"b ").start();
        new Thread(new ticket(),"c ").start();
    }

}
class MathTool{
    private static int count=30;

    public synchronized static void getNum(String threadName) throws InterruptedException {
        System.out.println(threadName+" getNum "+count--);
    }

}

class ticket implements Runnable{
    private int ticket = 10;
    boolean flag = true;
    @Override
    public synchronized void run() {
        synchronized (this){
            while (flag){
                try {
                    buy();
                    // request time
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private void buy() throws InterruptedException {
        if(ticket<=0) {
            flag = false;
            return;
        }
        Thread.sleep(100);
        String threadName = Thread.currentThread().getName();
        System.out.println(threadName+" get "+ticket--);
        MathTool.getNum(threadName);
    }
}

MathTool这个类作为工具类 有静态方法供人调用 结果就可以实现三个售票处总共票被挖取的倒计效果(实时显示三个站还剩多少票)

a  get 10
b  get 10
c  get 10
a  getNum 30
c  getNum 29
b  getNum 28
c  get 9
a  get 9
b  get 9
c  getNum 27
b  getNum 26
a  getNum 25
b  get 8
a  get 8
c  get 8
b  getNum 24
c  getNum 23
a  getNum 22
b  get 7
c  get 7
a  get 7
b  getNum 21
a  getNum 20
c  getNum 19
b  get 6
a  get 6
c  get 6
b  getNum 18
c  getNum 17
a  getNum 16
b  get 5
a  get 5
c  get 5
b  getNum 15
c  getNum 14
a  getNum 13
a  get 4
c  get 4
b  get 4
a  getNum 12
b  getNum 11
c  getNum 10
c  get 3
a  get 3
b  get 3
c  getNum 9
b  getNum 8
a  getNum 7
b  get 2
c  get 2
a  get 2
b  getNum 6
a  getNum 5
c  getNum 4
b  get 1
b  getNum 3
c  get 1
a  get 1
c  getNum 2
a  getNum 1

如果不同步这个静态方法 那么count这个变量便不再受到保护 (count只有一个 因为是静态的)
在这里插入图片描述
同步静态的方法 我们只能保护方法里面的变量 但是如果同步类 那效果是 —— 保护所有类实例 换言之 只要是使用了这个类字节码的类实例 ClassName.class 都用的同一把锁~

类锁

前面三种基本上都是针对一个对象 比如一个方法 一个代码块 又称为对象的内置锁单个对象上锁

但是这里是类锁 所有类实例共用一把锁 相当于一群实例共有的锁


这里我出道题: 如何让三个售票处 所有的票 都是一张一张卖的 比如一个售票处10张票 那么就会30张票一张一张出 这不是三个售票处 10张票售卖还能并行三线程

限定条件:

  • 主线程长这样的(就是之前 三个售票处那种demo)
public class ThreadSync {
    public static void main(String[] args) {

        new Thread(new ticket(),"a ").start();
        new Thread(new ticket(),"b ").start();
        new Thread(new ticket(),"c ").start();
    }

}
  • 在之前三个售票处的demo基础上 只更改两行代码

类锁demo答案

我给上代码 你们看看功能实现了没:

package com.base.threadlearning.threadState;

public class ThreadSync {
    public static void main(String[] args) {

        new Thread(new ticket(),"a ").start();
        new Thread(new ticket(),"b ").start();
        new Thread(new ticket(),"c ").start();
    }

}


class ticket implements Runnable{
    private int ticket = 10;
    boolean flag = true;
    @Override
    public void run() {
        synchronized (ticket.class) {
            while (flag) {
                try {
                    buy();
                    // request time
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private void buy() throws InterruptedException {

            if (ticket <= 0) {
                flag = false;
                return;
            }
            Thread.sleep(100);
            String threadName = Thread.currentThread().getName();
            System.out.println(threadName + " get " + ticket--);
        }

}
a  get 10
a  get 9
a  get 8
a  get 7
a  get 6
a  get 5
a  get 4
a  get 3
a  get 2
a  get 1
c  get 10
c  get 9
c  get 8
c  get 7
c  get 6
c  get 5
c  get 4
c  get 3
c  get 2
c  get 1
b  get 10
b  get 9
b  get 8
b  get 7
b  get 6
b  get 5
b  get 4
b  get 3
b  get 2
b  get 1

怎么实现的呢?原因全在这两行:

synchronized (ticket.class) {

}

发现了没有 无论多少售票处 没有意义 因为售票处实例都被同一把锁锁住了
因此 就好像有个农贸市场 售票市场 里面三个售票处 你人只能一次进去一个 当然你选哪家售票处 那随便(这个demo就默认选一家了)

又因为 我们控制的范围是run 所以只有robot a用完了整个售票市场 他挖完自己所有所需的票 b c才能进去

总结 ´◡`

这四种实现形式都有相应的应用 活学活用很难:) 不过再难 看着看着就会了 加油奥利给

有几个冷知识需要注意

  • synchronized关键字不能继承 你父类的方法synchronized与我子类的super无瓜!
  • 在定义接口方法时不能使用synchronized关键字
  • 构造方法不能使用synchronized关键字,但可以使用synchronized代码块包起来

然后就是这张表了:

修饰内容上锁对象作用对象(互斥体)
代码块(同步代码块)代码调用这个代码块的对象
方法(同步方法)方法调用这个方法的对象
静态方法静态方法(主要是里面的变量)所属类的所有对象实例(静态方法属于类信息)
括号范围类的所有对象实例

下一节 我们会聊到synchronize怎么结合和wait/notify 机制,解决生产者消费者问题

Java 从多线程到并发编程(七)—— wait notify 生产者消费者问题

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值