icode icode
首页
  • Android学习

    • 📁基础内容
    • 📺AndroidCore
    • 🎨Android-UI
    • 🏖️Components
    • 📊Fragment
    • 🔗网络操作
    • 🔏异步机制
    • 📦数据存储
    • 🗃️Gradle
  • 学习笔记

    • 『框架』笔记
    • 『Kotlin』笔记
    • 《Vue》笔记
    • 《Git》学习笔记
    • 『Bug踩坑记录』
  • ListView
  • RecyclerView
  • ViewPager
  • Java笔记

    • 🟠JavaSE
    • 🟢JavaWeb
    • 🔴JavaEE
    • ⚪JavaTopic
    • 🍳设计模式
  • 计算机基础

    • 📌计算机网络
    • 🔍数据结构
    • 📦数据库
    • 💻OS
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
  • 关于

    • 📫关于我
  • 收藏

    • 网站
    • 资源
    • Vue资源
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

iqqcode

保持对技术的探索实践与热爱
首页
  • Android学习

    • 📁基础内容
    • 📺AndroidCore
    • 🎨Android-UI
    • 🏖️Components
    • 📊Fragment
    • 🔗网络操作
    • 🔏异步机制
    • 📦数据存储
    • 🗃️Gradle
  • 学习笔记

    • 『框架』笔记
    • 『Kotlin』笔记
    • 《Vue》笔记
    • 《Git》学习笔记
    • 『Bug踩坑记录』
  • ListView
  • RecyclerView
  • ViewPager
  • Java笔记

    • 🟠JavaSE
    • 🟢JavaWeb
    • 🔴JavaEE
    • ⚪JavaTopic
    • 🍳设计模式
  • 计算机基础

    • 📌计算机网络
    • 🔍数据结构
    • 📦数据库
    • 💻OS
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
  • 关于

    • 📫关于我
  • 收藏

    • 网站
    • 资源
    • Vue资源
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • JavaSE

    • 面向对象

    • 常用API

    • 集合类

    • 多线程

      • Thread

        • 进程与线程
        • 黄牛卖票和模拟龟兔赛跑问题分析
        • 线程常用方法
        • 线程停止及守护线程
        • synchronized关键字解决线程安全问题
          • 1. 同步问题
          • 2. 锁的实现
            • 同步代码块
            • 同步方法
            • 二者之间的区别
          • 3. 对象锁全局锁
            • 案例说明
        • synchnized原理及优化
        • 线程间的通信
        • 死锁
        • Lock体系
        • ReentrantLock
        • 线程池使用
        • 线程池原理及配置
        • Java线程池的四种用法与使用场景
        • ThreadLocal
      • JUC

    • JDK8新特性

    • IO流

    • JVM

  • JavaWeb

  • JavaEE

  • JavaTopic

  • 设计模式

  • 计算机基础

  • Java后端
  • JavaSE
  • 多线程
  • Thread
iqqcode
2021-06-17
目录

synchronized关键字解决线程安全问题

# 1. 同步问题

多线程编程的三大问题:

  • 分工:把不同的工作分配给不同的线程执行,提高效率
  • 同步:多个线程操作同一个资源(共享资源)
  • 互斥:多线程并发时,某一时刻只能有一个线程访问资源

共享资源带来的并发问题 (opens new window)(👈详细的分析点击文章)

【并发问题的引出----12306卖票】

卖票案例出现了线程的安全问题,出现了不存在的票和重复的票,这就属于线程的安全性问题,这在现实生活中(12306卖票)时不允许存在的。

在这里插入图片描述 假如你卡里有500w,你和你女朋友同时到银行去取钱。你到柜台取300万的同时,你女朋友拿着卡到自动取款机取400万。因为在同一时刻,你们共同的账户都有500万,你们取的金额都小于500万。但是,有一个人取的同时,另一个人一定是不能同时操作的,不然取出的就是700万了。

当一个人操作账户时,银行为了防止数据错误,另一个人是没法取钱的,此时这个资源被锁上了。得等到你女朋友先取完钱,将账户金额扣除400万之后,你才能去取钱。


那么,怎样来解决线程的安全性问题呢?

我们可以给线程加个锁🔗,独占资源,来保证一个线程在访问共享数据的时候,无论是否失去了对CPU的执行权,让其他的线程只能等待当前占有CPU的线程执行完其所有的操作,其他线程才能获取资源(等待当前线程卖完票,其他线程在进行卖票)

锁🔗怎么来实现呢?可以通过synchronized关键字为程序逻辑上锁

synchronized关键字:

synchronized控制对 “对象” 的访词,每个对象对应一把锁。把synchronized理解为一把锁🔒,锁的是线程对象,所以也称为对象锁。

CPU会给被调度的线程发一把钥匙🔑,当前线程只有在获取到了这把锁的钥匙🔐之后,才能进入到同步方法或者同步代码块中共享数据。

锁🔒具有独占性,当线程A获取到对象锁之后,其他线程即使得到了CPU的调度,也领到了钥匙📌,但是取没法打开。只有当线程A执行完其线程任务之后,其他线程才能拿到钥匙📌访问资源。

每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞。方法一旦执行,就独占该锁直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。

这样做的好处是保证了数据的安全性(买票不会出现重复票和不存在的票),劣势就是影响执行效率。

synchronized解决线程安全问题:

  • 使用同步代码块
  • 使用同步方法

# 2. 锁的实现

# 同步代码块

在这里插入图片描述 【12306卖票修改】

在这里插入图片描述

# 同步原理的分析

假设CPU调度线程的顺序是线程A、线程B、线程C

在这里插入图片描述

  1. 线程A先抢到了CPU的执行权,进入while循环,遇到了synchronized代码块
  2. 线程A会获取到synchronized代码块的锁对象,进入到同步代码块中执行
  3. 线程A在睡眠时,线程B抢到了CPU的执行权
  4. 进入到了while循环,遇到了同步代码块。此时发现对象锁被线程A占有,线程B没法获取无法进入同步代码块。此时线程B处于阻塞中,一直等到线程A归还锁对象
  5. 同理线程C也处于阻塞中
  6. 一直到线程A执行完同步代码中的代码,会把锁对象归还给同步代码块。线程B才能获取到锁对象,进入到同步中执行

同步保证了只能有一个线程可以在同步中执行共享数据,保证了安全

但是程序频繁的判断锁、获取锁、释放锁,程序的效率会降低

总结

同步中的线程没有执行完毕不会释放锁,同步外的线程没有锁不能进入同步代码块,处于阻塞状态


# 同步方法

  • 直接在方法声明上使用synchronized,此时表示同步方法在任意时刻只能有一个线程进入
  • 同步方法锁的是this当前对象
/**
 * @Author: Mr.Q
 * @Date: 2020-05-26 16:36
 * @Description:同步方法
 */

class Web1230 implements Runnable {

    private int tickets = 20;

    @Override
    public void run() {
        while (tickets > 0) {
            this.sale();
        }
    }

    //此处通过同步方法加上锁
    public synchronized void sale() {
        //this表示当前对象
        synchronized (this) {
            //在此同步代方法中,只有一个线程在跑
            if (tickets >= 0) {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    System.err.println("执行线程任务时出现了异常...");
                }
                System.out.println(Thread.currentThread().getName() + "还剩" + tickets-- + "张票");
            }
        }
    }
}

public class SyncMethod {
    public static void main(String[] args) {
        Web1230 run = new Web1230();
        new Thread(run,"线程A").start();
        new Thread(run,"线程B").start();
        new Thread(run,"线程C").start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# 静态同步方法

静态的同步方法锁对象是谁?

  • 不能是this,this是创建对象之后产生的,静态方法优先于对象。
  • 静态方法的锁对象是本类的class属性--->>class文件对象(反射)

在这里插入图片描述 我们可以理解为将同步代码块加到了方法中,此时this对象变成了类的class属性(通过反射来获取)


# 二者之间的区别

synchronized关键字可应用在方法级别 [ 粗粒度锁 ] 或者是代码块级别 [ 细粒度锁 ]

  • 同步方法直接在方法上加synchronized实现加锁,锁的是类的对象;
  • 同步代码块则在方法内部加锁,锁的目标更明确

很明显,同步方法锁的范围比较大,而同步代码块范围要小点。

同步的范围越大,性能就越差。一般需要加锁进行同步的时候,肯定是范围越小越好,这样性能更好


# 3. 对象锁全局锁

synchronized对象锁也叫同步锁,锁的哪个对象,保护的哪个资源

synchronized说明

锁当前this对象

class Sync {
    //成员方法
    public synchronized void method() {
        
    }
}

=======等价于=========
 
class Sync {
    //当前this对象
    public void method() {
        synchronized (this) {
            
        }
    }
}    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

锁当前类对象,全局锁,锁类对应的class对象

class Sync {
    //静态方法
    public synchronized static void method() {
        
    }
}

=======等价于=========
 
class Sync {
    //静态方法
    public static void method() {
        //类对象
        synchronized (Sync.class) {
            
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 案例说明

不加static,锁的是当前的this对象

  • 本案例并没有锁住当前的this对象,synchronized加和不加没区别
  • 同理同步方法也是如此,没锁住
  • 要想实现同步,采用static方法全局锁或者同步块锁Class对象
class Sync implements Runnable{
    @Override
    public void run() {
        Sync sync = new Sync();
        sync.test();
    }

    //锁的是Sync的对象
    public synchronized void test() {
        System.out.println("test->开始,当前线程为: " + Thread.currentThread().getName());
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("test-->>结束,当前线程为: " + Thread.currentThread().getName());
    }
}

public class ObjectSync {
    public static void main(String[] args) {
        Sync mythread = new Sync();
        new Thread(mythread,"A").start();
        new Thread(mythread,"B").start();
        new Thread(mythread,"C").start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

A,B,C三个一组出现表明:没有加static的synchronized没有锁住当前对象

通过上述代码以及运行结果我们可以发现,没有看到synchronized起到作用,结果为三个线程同时运行test方法。

因为同步方法是粗粒度锁,实际上,synchronized(this){ 代码块上锁 }以及非static的同步方法,只能防止多个线程同时执行同一个对象的同步代码段。即本质上是三个线程属于不同的对象,不同的对象同时执行同一个方法,synchronized锁住的是括号里的当前对象,而不是代码。所以并不会产生竞争的并发效果,简单的理解为是并行的,结果为三个一组出现。

对于非static的synchronized同步方法,锁的就是对象本身也就是this。


加上static,静态同步方法上锁,是全局锁。锁的是当前类的Class对象,类的Class对象只有一个

在这里插入图片描述

对比运行结果:

不加static A,B,C三个一组的出现
加上static A,B,C三个不固定出现

那么此时我们在同步方法上加static关键字变成的全局锁,或者是用同步代码块来明确指定锁的目标来解决此问题...

当synchronized锁住一个对象后,别的线程如果也想拿到这个对象的锁,就必须等待这个线程执行完成释放锁,才能再次给对象加锁,这样才达到线程同步的目的。

要搞清楚synchronized到底锁住的是什么,最经典的就是八锁问题了。


下一节来唠唠synchronized的底层实现原理 (opens new window)

编辑 (opens new window)
上次更新: 2021/06/27, 10:49:09
线程停止及守护线程
synchnized原理及优化

← 线程停止及守护线程 synchnized原理及优化→

最近更新
01
匿名内部类
10-08
02
函数式接口
10-08
03
ARouter-Kotlin踩坑
10-05
更多文章>
Theme by Vdoing | Copyright © 2021-2023 iqqcode | MIT License | 备案号-京ICP备2021028793号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×