浅墨散人 浅墨散人
  • 基础
  • 设计模式
  • JVM
  • Maven
  • SpringBoot
  • 基础
  • Flask
  • Diango
  • Pandas
  • SqlAlchemy
  • Sqoop
  • Flume
  • Flink
  • Hadoop
  • Hbase
  • Hive
  • Kafka
  • Kylin
  • Zookeeper
  • Tez
  • MySQL
  • Doris
  • Chrome
  • Eclipse
  • IDEA
  • iTerm2
  • Markdown
  • SublimeText
  • VirtualBox
  • WebStrom
  • Linux
  • Mac
  • Hexo
  • Git
  • Vue
  • VuePress
  • 区块链
  • 金融
数据仓库
数据治理
读书笔记
关于我
GitHub (opens new window)
  • 基础
  • 设计模式
  • JVM
  • Maven
  • SpringBoot
  • 基础
  • Flask
  • Diango
  • Pandas
  • SqlAlchemy
  • Sqoop
  • Flume
  • Flink
  • Hadoop
  • Hbase
  • Hive
  • Kafka
  • Kylin
  • Zookeeper
  • Tez
  • MySQL
  • Doris
  • Chrome
  • Eclipse
  • IDEA
  • iTerm2
  • Markdown
  • SublimeText
  • VirtualBox
  • WebStrom
  • Linux
  • Mac
  • Hexo
  • Git
  • Vue
  • VuePress
  • 区块链
  • 金融
数据仓库
数据治理
读书笔记
关于我
GitHub (opens new window)
  • Java基础

    • Java基础
    • CSS自定义滚动条样式
    • Java发送短信
    • 线程的优先级
    • 线程的同步之Synchronized在单例模式中的应用
    • 线程的同步之Synchronized的使用
      • 一、介绍
        • 1.1 synchronized关键字可以锁定的部分
      • 二、实例
        • 1、未使用synchronized锁的情况
        • 2、使用synchronized关键字锁定方法:
        • 3、使用synchronized锁定代码块:锁定当前对象
        • 4、使用synchronized锁定代码块:锁定部分代码块
        • 5、使用synchronized锁定部分资源:只锁定num变量
      • 三、总结
    • 线程的基本概念
    • 线程的死锁
    • 线程的状态和常用操作
  • Java
  • Basic
2016-03-29
目录

线程的同步之Synchronized的使用

# 一、介绍

线程的同步:一般的并发指的就是多个线程访问同一份资源。多个线程同时访问(修改)同一份资源的话,就会有可能造成资源数据有误。 如果多个线程访问多个不同资源,就不会造成线程同步。 如果要解决这个问题,就需要对线程使用同步存取。java中提供了一个synchronized关键字来对方法或者某个块加锁。从而达到锁定某个区域,不可 同时修改以免数据有误的情况。

# 1.1 synchronized关键字可以锁定的部分

# 1.1.1 锁定方法

在方法上加入synchronized关键字就表明在使用该方法的时候需要获取相应的锁。

# 1.1.2 锁定块

锁定块的参数需要是对象,不可是基本类型数据

synchronized(引用类型变量 | this | 对象.class){
  //逻辑代码
}
1
2
3

每个Java对象都可以用作一个实现同步的锁,这些锁被称为内置锁(Intrinsic Lock)或者监视器锁(Monitor Lock),如果是静态的synchronized方法需要以class对象作为锁。 Java的内置锁相当于一种互斥体(或互斥锁),这意味着最多只有一个线程能持有这种锁。当线程A尝试获取一个由线程B持有的锁时,线程A必须等待或者阻塞。知道线程B释放这个锁,如果B永远不释放锁,那么A也将永远等下去。 由于每次只能有一个线程执行内置锁保护的代码块,因此,由这个锁保护的同步代码块会以原子方式执行,多个线程在执行该代码块时也不会相互干扰。 上图表示非同步线程和同步线程的比较,可以看出非同步的时候,线程1和线程2都是在同一个时间段访问同一个transter方法,而使用了同步之后,线程2如果想调用transter方法就必须等待线程1调用完成后才可执行。

# 二、实例

这里以12306抢票代码为例来说明线程同步的synchronized关键字的使用。

# 1、未使用synchronized锁的情况

首先来看未使用synchronized的情况会是什么样? 抢票的线程代码:

class Web12306 implements Runnable{
    private int num=10;//总共10张票
    private boolean flag = true;
    @Override
    public void run() {
        while(flag){
            //黄牛抢到了3    农民工抢到了1 黄牛抢到了0  程序员抢到了-1
            test1();//线程不安全,数据不准确:结果有-1值
        }
    }
    //1、线程不安全
    public void test1(){
        if (num<=0) {
            flag = false;
            return;//跳出循环,结束
        }
        try {
            Thread.sleep(500);//模拟延时
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"抢到了"+num--);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

测试线程代码:

public class SynDemo1 {
    public static void main(String[] args) {
        //真实角色
        Web12306 web = new Web12306();
        //代理角色
        Thread proxy1 = new Thread(web,"黄牛");
        Thread proxy2 = new Thread(web,"程序员");
        Thread proxy3 = new Thread(web,"农民工");
        proxy1.start();
        proxy2.start();
        proxy3.start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

测试结果如下:可以看出最后的结果会出现0和-1这样错误的数据。

黄牛抢到了10
农民工抢到了8
程序员抢到了9
黄牛抢到了7
农民工抢到了6
程序员抢到了5
黄牛抢到了4
农民工抢到了3
程序员抢到了2
黄牛抢到了1
农民工抢到了0
程序员抢到了-1
1
2
3
4
5
6
7
8
9
10
11
12

为什么会出现这样的数据呢? 因为现在三个线程都启动了,都是在运行状态中访问test1方法,修改其中的num值。因为他们三个会同时都会进入该方法的情况,所以修改的数据也会出现当:黄牛抢走了1,这时候农民工和程序员还在test1方法里,他俩也会对num进行--操作。所以,最后的结果就是0和-1

# 2、使用synchronized关键字锁定方法:

线程修改抢票代码,在test1方法上加入synchronized关键字,使该方法锁定。调用时需要先获取锁(线程安全)

class Web12306 implements Runnable{
    private int num=10;//总共10张票
    private boolean flag = true;
    @Override
    public void run() {
        while(flag){
            test2();//线程安全,数据准确
        }
    }
    //2、方法锁:加上synchronized表示线程安全的
    public synchronized void test2(){
        if (num<=0) {
            flag = false;
            return;//跳出循环,结束
        }
        try {
            Thread.sleep(500);//模拟延时
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"抢到了"+num--);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

继续使用上面的main方法测试,测试结果如下:抢票结果正确没问题。

黄牛抢到了10
黄牛抢到了9
黄牛抢到了8
黄牛抢到了7
黄牛抢到了6
黄牛抢到了5
黄牛抢到了4
黄牛抢到了3
黄牛抢到了2
农民工抢到了1
1
2
3
4
5
6
7
8
9
10

# 3、使用synchronized锁定代码块:锁定当前对象

继续修改抢票代码,在方法内部使用synchronized锁定块

class Web12306 implements Runnable{
    private int num=10;//总共10张票
    private boolean flag = true;
    @Override
    public void run() {
        while(flag){
            test3();//线程安全,数据准确
        }
    }
    //3、锁定块:当前对象也就是Web12306
    public void test3(){
        synchronized(this){//锁定当前对象
            if (num<=0) {
                flag = false;
                return;//跳出循环,结束
            }
            try {
                Thread.sleep(500);//模拟延时
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"抢到了"+num--);
        }
    }
}
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

继续使用上面的main方法测试,测试结果如下:抢票结果正确没问题。

黄牛抢到了10
黄牛抢到了9
黄牛抢到了8
黄牛抢到了7
黄牛抢到了6
黄牛抢到了5
黄牛抢到了4
黄牛抢到了3
黄牛抢到了2
农民工抢到了1
1
2
3
4
5
6
7
8
9
10

# 4、使用synchronized锁定代码块:锁定部分代码块

可以看出test3方法是使用synchronized关键字锁定了整个方法区域。那如果就只锁定一部分呢?这里假如只锁定if(num<=0)这个判断部分

class Web12306 implements Runnable{
    private int num=10;//总共10张票
    private boolean flag = true;
    @Override
    public void run() {
        while(flag){
            test4();//线程不安全,数据不准确:出现-1 【锁定范围不正确】
        }
    }
    //4、使用synchronized锁定部分资源
    public void test4(){
        synchronized(this){
            if (num<=0) {
                flag = false;
                return;//跳出循环,结束
            }
        }//只锁定到此
        try {
            Thread.sleep(500);//模拟延时
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"抢到了"+num--);
    }
}
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

使用main方法测试结果如下:可以看出最后的结果同样也会出现0和-1这样错误的数据。

黄牛抢到了10
农民工抢到了8
程序员抢到了9
黄牛抢到了7
农民工抢到了6
程序员抢到了5
黄牛抢到了4
农民工抢到了3
程序员抢到了2
黄牛抢到了1
农民工抢到了0
程序员抢到了-1
1
2
3
4
5
6
7
8
9
10
11
12

**分析下为什么会出现这样的结果?**我们知道test4中只锁定了if这部分。假设现在程序num现在等于1

  1. 此时线程A,B,C三个线程都会进入到12行,if判断的部分。A先进来拿到了锁,判断此时num=1 。然后释放锁走到18行,try的部分
  2. 线程A在18行try部分并没有对num--操作。此时线程B也进入到了12行拿到了锁。也到了18行。现在18行是A,B两个线程。A往下执行拿走了num 等线程B再去拿num的时候,num已经等于0了。
  3. 同理,C再去拿num的时候num已经是0-1 = -1了。

# 5、使用synchronized锁定部分资源:只锁定num变量

由于synchronized的参数需要是对象,所以把基本类型包装成引用类型

class Web12306 implements Runnable{
    private int num=10;//总共10张票
    private boolean flag = true;
    @Override
    public void run() {
        while(flag){
            test5();//线程不安全,数据不准确:出现重复数据【锁定范围不正确】
        }
    }
    //5、使用synchronized锁定部分资源:锁定num变量
    public void test5(){
        synchronized((Integer)num){
            if (num<=0) {
                flag = false;
                return;//跳出循环,结束
            }
        }
        try {
            Thread.sleep(500);//模拟延时
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"抢到了"+num--);
    }
}
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

使用main方法测试结果如下:可以看出最后的结果会出现重复数据(两个6)锁定资源不正确也是线程不安全的

黄牛抢到了10
农民工抢到了9
程序员抢到了8
农民工抢到了7
黄牛抢到了6
程序员抢到了6
黄牛抢到了5
农民工抢到了4
程序员抢到了3
黄牛抢到了2
程序员抢到了1
农民工抢到了0
黄牛抢到了-1
1
2
3
4
5
6
7
8
9
10
11
12
13

# 三、总结

  1. synchronized关键字表示锁,可以加在方法上或者一个代码块中 synchronized(引用类型变量 | this | 对象.class){ //需要锁的区域 }
  2. 不加synchronized关键字的方法是线程不安全的 加了synchronized表示线程安全,线程安全的话会降低效率。因为共享的资源被加了锁,会有锁等待时间
  3. 在加synchronized代码块的时候需要注意,注意锁的范围。 范围太大----->会降低效率。范围太小------>线程不安全
#java#Thread
最后更新时间: 2022/7/23 10:17:11
线程的同步之Synchronized在单例模式中的应用
线程的基本概念

← 线程的同步之Synchronized在单例模式中的应用 线程的基本概念→

最近更新
01
分区分桶
08-21
02
数据模型(重要)
08-21
03
安装和编译
08-21
更多文章>
Theme by Vdoing
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式