Java多线程详解(线程同步)

2023-10-26

        嗨喽~小伙伴们我来了,

        上一章,我们通过几个例子(点击跳转)介绍了线程安全问题,而说到线程安全就不得不提到线程同步,它是解决线程安全的一种重要方法。本章就来简单地介绍一下线程同步

        从上一章的学习我们知道,当多个线程操作一个资源的时候,有可能由于线程的不确定切换出现数据不一致的安全问题,因此,我们要解决这个问题,就得想办法使得资源在某个时间戳只能被一个线程访问。基于这样的思想,我们提出了“队列与锁”的策略:

        通俗理解,就是将所有线程排成一个队列,给要共享的资源上把锁,只有线程获得该资源的锁之后,才能访问该资源

        这也是线程同步的基本思想。所谓线程同步,是指同一进程中的多个线程相互协调工作从而达到一致性。使用线程同步,在解决线程安全问题的同时还能提高程序的性能。

        基于“队列与锁”的思想,JDK中封装了一些用于实现线程同步的类和关键字。我们主要介绍synchronized 和 lock 两种。

一. synchronized

        我们知道,在java中,每个对象都有一把唯一的锁,这也是synchronized实现线程同步的基础。总的来说,synchronized实现线程同步主要有三种形式:

形式 特点
实例同步方法 锁的是当前实例对象,执行同步代码前必须获得当前实例的锁
静态同步方法 锁的是当前类的class对象,执行同步代码前必须获得当前类对象的锁
同步代码块 锁的是括号里的对象,对给定对象加锁,执行同步代码块必须获得给定对象的锁

        事实上,当两个线程同时对一个对象的某个方法进行调用时,只有一个线程能够抢到该对象的锁,因为一个对象只有一把锁。抢到该对象的锁之后,其他线程就无法访问该对象的所有synchronized方法,但仍可以访问该对象中的非synchronized方法

        下面,我们用代码来演示synchronized的三种用法。为了突出synchronized在线程安全方面的作用,我们采用对比(有synchronized和无synchronized)的方式来介绍。

        第一种,修饰实例同步方法。首先,来看一个简单的程序:



/**
 * @author sixibiheye
 * @date 2021/8/31
 * @apiNote synchronized用法举例
 */

public class Synchronized implements Runnable{
    //公共资源
    static int count = 0;
    public void increase(){
        count++;
    }
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        new Thread(new Synchronized(),"A").start();
        new Thread(new Synchronized(),"B").start();
        Thread.sleep(2000);
        System.out.println(count);
    }
}

        如果小伙伴们学习了上一章的内容,应该很容易看出这个程序是存在线程安全问题的,它的输出结果如下:

        如果小伙伴们不太理解的话,可以去看我的上一篇博文。 

        上一篇博文中说到,对于“count++”,JVM是这样处理的:

1. 某线程从内存中读取count值到寄存器

2. 某线程在寄存器中修改count的值

3. 某线程将修改后的count值写入内存

        简单解释一下,我们开启了两个线程去执行increase()方法,如果没有任何保护机制,假设,当count的值累加到1000时,A线程从主内存中读取到寄存器的count值为1000,执行完“count++”操作后,寄存器中的count值为1001,刚想写入内存,B线程正好抢到了CPU的使用权,开始执行run()方法,由于未写入内存,B线程从内存中读取到的count值为仍为1000,执行完“count++”操作后,B线程成功地将1001写入内存,接着A线程将自己寄存器中的count值1001写入内存(覆盖了B线程的1001),由此导致,虽然执行了两个线程,但count的值只累加了一次,这样的情况多发生几次,计算结果自然就低于20000了。

        为了避免以上情况发生,我们给increase()方法加上修饰符synchronized,使得两个线程无法同时调用increase()方法,以保证上面的三步中的任何一步都不会被另外一个线程打断。

         这样,“count++”操作就永远不会因线程切换而出错。代码如下:



/**
 * @author sixibiheye
 * @date 2021/8/31
 * @apiNote synchronized用法举例
 */

public class Synchronized implements Runnable{
    //公共资源
    static int count = 0;
    //synchronized实现线程同步
    public synchronized void increase(){
        count++;
    }
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        new Thread(new Synchronized(),"A").start();
        new Thread(new Synchronized(),"B").start();
        Thread.sleep(2000);
        System.out.println(count);
    }
}

        现在来看运行结果:

        没有任何问题。

        此外,使用synchronized时,有几个需要注意的地方,请看下面的代码:


/**
 * @author sixibiheye
 * @date 2021/8/31
 * @apiNote synchronized用法举例
 */

public class Synchronized implements Runnable{
    public synchronized void running() throws InterruptedException {
        System.out.println("1");
        Thread.sleep(1000);
        System.out.println("2");
    }
    @Override
    public void run() {
        try {
            running();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        //用同一个类创建两个对象
        Synchronized sync1 = new Synchronized();
        new Thread(sync1).start();
        Synchronized sync2 = new Synchronized();
        new Thread(sync2).start();
    }
}

        如果我们使用不同的对象访问,那么结果可能是不同步的

        因为synchronized修饰实例方法时锁的对象是this对象,而使用两个对象去访问,不是同一把锁。如果我们用同一对象访问:

//只创建一个对象
Synchronized sync = new Synchronized();
new Thread(sync).start();
new Thread(sync).start();

        那结果是同步的:

          接着,我们来看synchronized修饰静态方法的例子:


/**
 * @author sixibiheye
 * @date 2021/8/31
 * @apiNote synchronized用法举例
 */

public class Synchronized implements Runnable{
    public static synchronized void running() throws InterruptedException {
        System.out.println("1");
        Thread.sleep(1000);
        System.out.println("2");
    }
    @Override
    public void run() {
        try {
            running();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        //用同一个类创建两个对象
        Synchronized sync1 = new Synchronized();
        new Thread(sync1).start();
        Thread.sleep(2000);
        Synchronized sync2 = new Synchronized();
        new Thread(sync2).start();
    }
}

        虽然有sync1,sync2两个对象,但是它们是同一个类对象(Synchronized.class)产生的,而synchronized修饰静态方法时锁的是 Synchronized.class,因此两个线程仍然是同步的:

         最后一种,synchronzied修饰同步代码块,它可以锁任何指定的对象,语法示例如下:

synchronized (this){
      System.out.println("1");
      Thread.sleep(1000);
      System.out.println("2");
}

        this指代当前实例对象,可以换为任何对象。

        那么为什么要使用同步代码块呢?

        在某些情况下,我们编写的方法体可能比较庞大,同时又有一些耗时的操作,如果对整个方法体进行同步,效率会大大降低,所以我们希望能够只同步必要的代码块,对于一些不需要同步的或者耗时较长的操作,放到同步代码块之外,比如:


/**
 * @author sixibiheye
 * @date 2021/8/31
 * @apiNote synchronized用法举例
 */

public class Synchronized implements Runnable{
    public void running() throws InterruptedException {
        for (int i = 0; i < 5; i++) {
            System.out.println("这是耗时操作。");
        }
        //需要同步的代码块写下面
        synchronized (this){
            System.out.println("1");
            Thread.sleep(1000);
            System.out.println("2");
        }
    }
    @Override
    public void run() {
        try {
            running();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Synchronized sync = new Synchronized();
        new Thread(sync).start();
        new Thread(sync).start();
    }
}

        运行结果如下:

        再运行一次:

        结果表明,需要同步的代码块确实实现了同步。 

二.  lock         

        前面使用的synchronized关键字可以实现多线程间的同步互斥,其实,在JDK1.5后新增的ReentrantLock 类同样可以实现这个功能,而且在使用上比 synchronized ​​​​​​​更为灵活。

        翻阅源码,可以看到 ReentrantLock 类实现了Lock接口:

         下面我们用ReentrantLock类来实现简单的线程同步:


import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author sixibiheye
 * @date 2021/8/31
 * @apiNote ReentrantLock实现线程同步
 */
public class LockDemo implements Runnable{
    private Lock lock = new ReentrantLock();
    /**
     * 
     * 按照规范,
     * lock()后面应紧跟try代码块,
     * 并将unlock()放到finally块的第一行
     * 
     */
    @Override
    public void run() {
        //上锁
        lock.lock();
        try {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + "线程中的i=" + i);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //释放锁
            lock.unlock();
        }
    }
    public static void main(String[] args) 
        new Thread(new LockDemo(),"A").start();
        new Thread(new LockDemo(),"B").start();
    }
}

        其实关于Lock 和 ReentrantLock,还有许多其他的用法,限于篇幅,就不一一介绍了,有兴趣的小伙伴们可以查阅相关资料。

        了解了synchronized和ReentrantLock,对于上一章提出的三个线程安全问题,便可以轻松地解决了。下面提供使用synchronized的解决方式:


/**
 * @author sixibiheye
 * @date 2021/8/28
 * @apiNote 线程安全问题一-------取钱问题
 */
public class UnsafeBank {

    public static void main(String[] args) {
        //账户
        Account account = new Account(100,"买房基金");
        //你和你的妻子都要取钱
        Drawing you = new Drawing(account,50,"你");
        Drawing girlFriend = new Drawing(account,100,"妻子");
        you.start();
        girlFriend.start();
    }
}

//账户
class Account{
    int money; //余额
    String name; //卡名
    public Account(int money,String name){
        this.money = money;
        this.name = name;
    }
}

//银行
class Drawing extends Thread{
    Account account; //账户
    int drawingMoney; //取的钱
    int nowMoney; //现手上有的钱
    public Drawing(Account account,int drawingMoney,String name){
        super(name);
        this.account = account;
        this.drawingMoney = drawingMoney;
    }
    @Override
    public void run() {
        //注意锁的是account对象,而不是this
        synchronized (account){
            if(account.money - drawingMoney < 0){
                System.out.println(Thread.currentThread().getName() + "钱不够了,取不了!");
                return;
            }
            try {
                sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //卡内余额
            account.money = account.money - drawingMoney;
            //手里的钱
            nowMoney = nowMoney + drawingMoney;
            //打印
            System.out.println(account.name + "余额为:" + account.money);
            System.out.println(Thread.currentThread().getName() + "手里的钱:" + nowMoney);
        }
        }
}


/**
 * @author sixibiheye
 * @date 2021/8/27
 * @apiNote 线程安全问题一-------买票问题
 */
public class UnsafeBuyTicket {
    public static void main(String[] args) {
        BuyTicket buyTicket = new BuyTicket();
        new Thread(buyTicket,"小红").start();
        new Thread(buyTicket,"小白").start();
        new Thread(buyTicket,"小黑").start();
    }
}

class BuyTicket implements Runnable{
    //票数
    private int tickets = 10;
    //线程停止的标志位
    private boolean flag = true;
    //直接同步实例方法即可
    private synchronized void buy() throws InterruptedException {
        //判断是否有票
        if(tickets <= 0){
            flag = false;
            return;
        }
        //模拟延时,保证多人都能买到票
        Thread.sleep(20);
        //买票
        System.out.println(Thread.currentThread().getName() + "拿到了第" + tickets-- +"张票");

    }
    @Override
    public void run() {
        while (flag){
            try {
                buy();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}


import java.util.ArrayList;
import java.util.List;

/**
 * @author sixibiheye
 * @date 2021/8/28
 * @apiNote 线程安全问题一-------列表问题
 */

public class UnsafeList {
    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<String>();
        for (int i = 0; i < 10000; i++) {
            new Thread( () -> {
                //同步需要修改的list对象
                synchronized (list){
                    list.add(Thread.currentThread().getName());
                }
            }).start();
        }
        //sleep保证上述for循环跑完再输出
        Thread.sleep(3000);
        //输出列表大小
        System.out.println(list.size());
    }
}

        小伙伴们可以思考一下如果使用ReentrantLock应该如何解决。

        下一章,我们将着重介绍Thread类中的一些常用方法(点击跳转),好啦~本章的内容到这就结束了,喜欢的小伙伴们点个赞鼓励支持一下吧~ 

         

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Java多线程详解(线程同步) 的相关文章

随机推荐

  • 新闻列表案例(前端html,css)

    去掉列表默认的样式 无序和有序列表前面默认的列表样式 在不同浏览器显示效果不一样 而且也比较难看 所以 我们一般上来就直接去掉这些列表样式就行了 li list style none 代码如下
  • 看spring cloud开源项目Pig的云踩坑记

    最近看到一个有趣的开源项目pig 主要的技术点在认证授权中心 spring security oauth zuul网关实现 Elastic Job定时任务 趁着刚刚入门微服务 赶快写个博客分析一下 此篇文章主要用于个人备忘 如果有不对 请批
  • 汇编语言如何输出结果_量子计算遇上高性能计算系列(五)初识量子汇编语言...

    汇编语言是直接工作在硬件之上的最底层的编程语言 众所周知 计算机中所有的数据和指令都是由0和1组成的 例如 01010000 机器指令在CPU上运行的时候就是一组电平脉冲信号 而01010000对于人们来讲太难理解 因此 就产生出更加便于人
  • react做表格和分页功能

    import React memo useState useEffect from react import Table Pagination from antd import IDefaultParam from topFilter to
  • ASyncSocket库

    iphone的标准推荐CFNetwork C库编程 但是编程比较烦躁 在其它OS往往用类来封装的对Socket函数的处理 比如MFC的CAsysncSocket 在iphone也有类似于开源项目 cocoa AsyncSocket库 官方网
  • LLaMA模型加载报错_sentencepiece.SentencePieceProcessor_LoadFromFile(self, arg) TypeError: not a string

    tokenizer LlamaTokenizer from pretrained lora model path lora model path这一项不是string类型 运行命令有参数项目为 lora model ziqingyang c
  • html js动态时间轴,jQuery时间轴插件timeline.js

    插件描述 timeline js是一款jQuery时间轴插件 通过timeline js插件 你可以很容易的制作出水平或垂直时间轴效果 并可以像幻灯片一样前后切换时间点 简要教程 timeline js是一款jQuery时间轴幻灯片插件 通
  • C51单片机重要知识点总结

    文章目录 文章目录 00 写在前面 01 C51基本数据类型总结 02 C51数据类型扩充定义 03 关于单片机 04 单片机工作的基本时序 05 单片机复位 06 80C51的中断系统 07 定时器 08 串口通信 09 C语言基础 10
  • Unable to cast object of type ‘System.Byte‘ to type ‘System.Boolean‘

    mysql数据库 存储的一个字段类型为tinyint 查询数据的时候设置的数据类型bool 查询结果报错 解决方法 将为空的数据都设为0 查询资料时 tinyint在查询时会自动对应成bool类型 问题原因时数据不可为空 即使设置成bool
  • Hi3516Dv300 平台使用MIPI Tx点屏

    背景 公司新做了一块3516Dv300的开发板 其中有MIPI Tx接口 刚好公司库房还有好几百块的LCD屏 LCD屏是800x480的 还是原装屏 不用掉怪可惜的了 所以就让硬件的同事化了个转接板 使用的芯片是ICN6211 这货最大分辨
  • python pip 安装 删除缓存(cache)

    今天pip安装包时 一直使用缓存 非常不爽 pip删除缓存 安装操作 pip no cache dir install 包名 If using pip 6 0 or newer try adding the no cache dir opt
  • 什么是1G/2G/3G/4G/5G

    什么是1G 2G 3G 4G 5G 参考 http www 360doc com content 14 1213 22 5458405 432718054 shtml 介绍 1G 表示第一代移动通讯技术 在20世纪80年代前期 第一代模拟制
  • CIMCO Edit2022安装教程(非常详细)从零基础入门到精通,看完这一篇就够了(附安装包)

    软件下载 软件 CIMCO Edit 版本 2022 语言 简体中文 大小 251 79M 安装环境 Win11 Win10 Win8 Win7 硬件要求 CPU 2 0GHz 内存 4G 或更高 下载通道 百度网盘丨64位下载链接 htt
  • CommonJS是啥东西嘞

    AMD AMD要用define定一个模块 define dep1 dep2 function dep1 dep2 return function 包目录 package json包 bin用于可的目 lib用于JavaScript的目 do
  • sqli-libs基础篇总结(1-22)

    1 关于sqli labs 这个是sql注入的靶场 可以在git上下载 2 题目简介 前面的1 22题都是sql注入的基础题目 覆盖范围很广 不过都是针对mysql数据库的 1 4题 union注入 5 8题 布尔盲注 9 10题 延时盲注
  • sql server备份及导出表数据和结构

    一 备份表数据及结构 select into new table name from old tablename 二 导出表数据及结构 1 选中要导出的数据库 gt 任务 gt 生成脚本 或者在任务里面有生成脚本这个选项 好好找找能找到的
  • 高清变脸更快更逼真!比GAN更具潜力的可逆生成模型来了

    昨天上市即破发的小米 今天上午股价大涨近10 这下雷军要笑了 而且可以笑得更灿烂 更灿烂是什么样 来 我们用OpenAI刚刚发布的人工智能技术 给大家展示一下 当然这个最新的技术 不止这点本事 它的 想象力 很强大的说 比如 留胡子的硬汉版
  • 关于eclipse项目栏关闭项目不想再看到

    前言 如果你用是什么IntelliJ IDEA我这篇文章你就不用看了 我的建议还是用IDEA我也喜欢用 但是因为我们老师电脑卡的原因 这个编辑器比较吃配置所以用的eclipse 以前还用的myeclipse虽然我对编辑器没什么要求 但是我用
  • Jmeter常用线程组设置策略

    一 前言 在JMeter压力测试中 我们时常见到的几个场景有 单场景基准测试 单场景并发测试 单场景容量测试 混合场景容量测试 混合场景并发测试以及混合场景稳定性测试 在本篇文章中 我们会用到一些插件 在这边先给大家列出 Custom Th
  • Java多线程详解(线程同步)

    嗨喽 小伙伴们我来了 上一章 我们通过几个例子 点击跳转 介绍了线程安全问题 而说到线程安全就不得不提到线程同步 它是解决线程安全的一种重要方法 本章就来简单地介绍一下线程同步 从上一章的学习我们知道 当多个线程操作一个资源的时候 有可能由