首页 > 编程语言 >【Java】深入理解Java中的多线程同步机制

【Java】深入理解Java中的多线程同步机制

时间:2024-09-16 21:21:21浏览次数:9  
标签:同步 Java Thread synchronized add 线程 new 多线程 public

一、多线程的数据不一致

       当多个线程同时运行时,线程的调度由操作系统决定,程序本身无法决定。因此,任何一个线程都有可能在任何指令处被操作系统暂停,然后在某个时间段后继续执行。
       这个时候,一个在单线程模型下不存在的问题就会发生:如果多个线程同时读写共享变量,会出现数据不一致的问题,所以必须保证是原子操作。原子操作是指不能被中断的一个或一系列作

       通过加锁解锁的操作,即使在执行期线程被操作系统中断执行,其他线程也会因为无法获得锁导致无法进入此指令区间。只有执行线程将锁释放后,其他线程才有机会获得锁并执行。这种加锁和解锁之间的代码块我们称之为临界区(Critical section),任何时候临界区最多只有一个线程能执行

二、synchronized关键字

       保证一段代码的原子性就是通过加锁和解锁实现的。在Java 的多线程模型中使用 synchronized 关键字对一个对象进行加锁。

        解决多线程并发执行时的线程同步问题(不安全、不同步),例如:解决多线程递增&&递减:

        方式一:使用synchronized代码块

public class Main {
    public static void main(String[] args) throws InterruptedException {

        // 创建并启动2个线程,分别执行加和减的操作
        Thread add = new AddThread();
        Thread dec = new DecThread();

        add.start();
        dec.start();

        add.join();
        dec.join();
        System.out.println(Counter1.count);
    }
}

class Counter1 extends Thread {
    public static int count = 0;

    // 创建Object对象,用于实现同步锁
    public final static Object LOCK = new Object();
}

class AddThread extends Thread {
    public void run() {
        for (int i = 0; i < 100; i++) {
            synchronized (Counter1.LOCK) {  // 加锁
                Counter.count += 1;
            } // 释放锁
        }
    }
}

class DecThread extends Thread {
    public void run() {
        for (int i = 0; i < 100; i++) {
            synchronized (Counter1.LOCK) {  // 加锁
                Counter1.count -= 1;
            }
        } // 释放锁
    }
}

        方式二:使用this对象作为锁

public class Main {
    public static void main(String[] args) throws InterruptedException {

        Counter2 cou = new Counter2();

        // 创建并启动2个线程,分别执行加和减的操作
        Thread add = new Thread(()->{
            cou.add();
        });

        Thread dec = new Thread(()->{
            cou.dec();
        });

        add.start();
        dec.start();

        add.join();
        dec.join();

        System.out.println(Counter2.count);
    }
}

class Counter2{
    public static int count=0;

    // 递增
    // 在方法声明上使用synchronized关键字,对整个方法体进行加锁,使用this对象作为锁
    public synchronized void add(){
        for (int i=0;i<100;i++){
            Counter2.count+=1;
        }
    }

    // 递减
    // 作用同步
    public void dec(){
        synchronized (this){
            for (int i=0;i<100;i++){
                Counter2.count-=1;
            }
        }
    }
}

        方式三:使用Class对象作为锁

public class Main {
    public static void main(String[] args) throws InterruptedException {

        // 创建并启动2个线程,分别执行加和减的操作
        Thread add = new Thread(() -> {
            Counter3.add();
        });

        Thread dec = new Thread(() -> {
            Counter3.dec();
        });

        add.start();
        dec.start();

        add.join();
        dec.join();

        System.out.println(Counter3.count);
    }
}

class Counter3 {
    public static int count = 0;

    // 在静态方法声明上使用synchronized关键字,对整个方法体进行加锁
    // 使用Class对象作为锁
    public synchronized static void add() {
        for (int i = 0; i < 100; i++) {
            Counter3.count += 1;
        }
    }

    // 作用等同
    public static void dec() {
        synchronized (Counter3.class) {
            for (int i = 0; i < 100; i++) {
                Counter3.count -= 1;
            }
        }

    }
}

        方式四:使用具备原子性操作的参数类型

public class Main {
    public static void main(String[] args) throws InterruptedException {

        // 创建并启动2个线程,分别执行加和减的操作
        Thread add = new Thread(() -> {
            Counter4.add();
        });

        Thread dec = new Thread(() -> {
            Counter4.dec();
        });

        add.start();
        dec.start();

        add.join();
        dec.join();

        System.out.println(Counter4.count);
    }
}

class Counter4 {
    // 具备原子性操作的AtomicInteger
    public static AtomicInteger count = new AtomicInteger(0);

    public static void add() {
        for (int i = 0; i < 100; i++) {
            Counter4.count.incrementAndGet();  // 自增+1
        }
    }

    public static void dec() {
        for (int i = 0; i < 100; i++) {
            Counter4.count.decrementAndGet();  // 自减-1
        }
    }
}

        使用 synchronized 解决了多线程同步访问共享变量的正确性问题。但是,它的缺点是带来了性能下降。因为 synchronized 代码块无法并发执行。此外,加锁和解锁需要消耗一定的时间,所以,synchronized 会降低程序的执行效率。

       概括总结一下如何使用 synchronized:

  •        找出修改共享变量的线程代码块
  •        选择一个共享实例作为锁
  •        使用 synchronized(lockobject){ ... }

三、注意事项

1.抛出异常

        在使用 synchronized 的时候,不必担心抛出异常。因为无论是否有异常,都会在 synchronized 结束处正确释放锁。

2.不同的lock

       使用 synchronized 的时候,获取到的是哪个锁非常重要。锁对象如果不对,代码逻辑就不对。

3.不需要synchronized的操作

       JVM 规范定义了几种原子操作:

  • 基本类型( long 和 double 除外)赋值,例如:intn=m;

        long和 double是64 位数据,JVM 没有明确规定 64 位赋值操作是不是一个原子操作,不过在 x64 平台的 JVM 是把 long 和 double 的赋值作为原子操作实现的。

  • 引用类型赋值,例如:List<string>list=anotherList。

       注意:单条原子操作的语句不需要同步,但是,如果是多行赋值语句,就必须保证是同步操作。

四、案例

1.多线程售票

public class TicketPool  implements Runnable{

    // 当前剩余门票数
    private int ticketNum;

    // 创建公共票池,传入默认总门票数
    public TicketPool(int ticketNum){
        this.ticketNum=ticketNum;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"准备开始卖票");

        // 线程竞争CPU执行权(this锁)
        synchronized(Thread.currentThread()){
            while (true){
                if (ticketNum<=0){
                    System.out.println(Thread.currentThread().getName()+"卖完了");
                    return;
                } else{
                    System.out.println(Thread.currentThread().getName()+"卖了一张票,还剩"+(--ticketNum)+"张票");
                }

                try {
                    // 休眠过程中:当前线程不会让出持有的"this锁",此处为引发错误的原因
                    // Thread.sleep(1000);  // 不会释放锁

                    // 注意:等待过程中,当前线程让出持有的"this锁",允许其它线程参与竞争CPU执行权(this锁)
                    this.wait(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
public class SaleTicket {
    public static void main(String[] args) throws InterruptedException {
        // 创建一个公共票池
        TicketPool ticketPool = new TicketPool(50);

        // 模拟三个售票窗口
        Thread t1 = new Thread(ticketPool);
        Thread t2 = new Thread(ticketPool);
        Thread t3 = new Thread(ticketPool);

        t1.start();
        t2.start();
        t3.start();
    }
}

 2.多线程打印数字+字母

        Character类打印字母:

public class Character  implements Runnable{
    private Object lock;
    public Character(Object lock){
        this.lock=lock;
    }

    @Override
    public void run() {
        synchronized (lock){
            for (char i='A';i<='Z';i++){
                // 输出当前字母
                System.out.print(i);

                // "唤醒" 数字线程
                lock.notify();  // notifyAll  唤醒所有线程

                if (i<'Z'){
                    try {
                        // 字母线程,每打印一个字母,进入等待状态
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }

    }
}

        Number类打印数字:

public class Number implements Runnable {
    private Object lock;

    public Number(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock) {
            for (int i = 1; i < 53; i++) {
                // 有两个数字+字母之间输出1个空格,用于分隔
                if (i % 2 == 1) {
                    System.out.print(" ");
                }
                // 输出当前数字
                System.out.print(i);

                // 当前数字是偶数,需要进入等待状态
                if (i % 2 == 0) {
                    // "唤醒" 字母线程
                    lock.notify();   // notifyAll 唤醒所有线程
                    try {
                        lock.wait();  //当前线程进入等待状态,并释放锁,( 被唤醒后,继续从等待的地方接着执行)
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

            }
        }

    }
}

        测试类:

public class Test {
    public static void main(String[] args) {
        // 公共锁对象
        final Object LOCK = new Object();

        // 创建两个线程,分别执行Number和Character,共用“一把锁”
        Thread t1 = new Thread(new Number(LOCK));
        Thread t2 =new Thread(new Character(LOCK));

        t1.start();
        t2.start();
    }
}

五、总结

  • 多线程同时读写共享变量时,可能会造成逻辑错误,因此需要通过synchronized 同步;
  • 同步的本质就是给指定对象加锁,加锁后才能继续执行后续代码;
  • 注意加锁对象必须是同一个实例;
  • 对 JVM 定义的单个原子操作不需要同步。

标签:同步,Java,Thread,synchronized,add,线程,new,多线程,public
From: https://blog.csdn.net/weixin_71491685/article/details/142290911

相关文章

  • Java 双括号初始化(匿名内部类初始化)
    原文:Java:双括号初始化/匿名内部类初始化法ArrayList可以这样初始化://新建一个列表并赋初值A、B、CArrayList<String>list=newArrayList<String>(){{add("A");add("B");add("C");}};还有其他集合比如HashMap的初始化:Mapmap=newHashMap()......
  • 60.《Java集合框架-List-Set-Map》
    此篇所写不知道你们是否在网页开发的时候当看到要写Map集合什么HashMap之类的突然蒙了虽然之前学过突然让你调用方法懵了所以在此总结一下以备后需对比数组可存储任意类型对象且存储长度是可以变的集合类乃内存层面对数据进行存储数据库是持久层断电后仍长期存在......
  • java的方法和数组
    什么是方法呢?就类似c语言的函数                            返回类型 方法名   形式参数列表方法名最好使用小驼峰的形式,在java中方法不可以嵌套使用, 方法的调用:就是在main方法里面写上调用的方法名加......
  • mongo集群同步数据异常,手动同步节点副本数据
    转载请注明出处:数据同步方案当副本集节点的复制进程落后太多,以至于主节点覆盖了该节点尚未复制的oplog条目时,副本集节点就会变为“陈旧”。节点跟不上,就会变得“陈旧”。出现这种情况时,必须删除副本集节点的数据,然后执行初始同步,从而完全重新同步该节点。MongoDB提供了......
  • 重生之我在Java世界------学单例设计模式
    什么是单例设计模式?单例模式是面向对象编程中最简单却又最常用的设计模式之一。它的核心思想是确保一个类只有一个实例,并提供一个全局访问点。本文将深入探讨单例模式的原理、常见实现方法、优缺点,以及在使用过程中可能遇到的陷阱。单例模式的核心原理单例模式的实现主要......
  • 基于springboot的图书商城管理系统。Javaee项目,springboot项目。
    演示视频:基于springboot的图书商城管理系统。Javaee项目,springboot项目。项目介绍:采用M(model)V(view)C(controller)三层体系结构,通过Spring+SpringBoot+Mybatis+Maven+Layui+Thymeleaf来实现。MySQL数据库作为系统数据储存平台,实现了基于B/S结构的Web系统。界面简洁......
  • Springboot宠物领养管理系统。Javaee项目。Springboot项目。
    演示视频:Springboot宠物领养管理系统。Javaee项目。Springboot项目。项目介绍:采用M(model)V(view)C(controller)三层体系结构,通过Spring+SpringBoot+Mybatis+Vue+Maven来实现。MySQL数据库作为系统数据储存平台,实现了基于B/S结构的Web系统。界面简洁,操作简单。系统......
  • 基于springboot的家庭理财管理系统。Javaee项目,springboot项目。
    演示视频:基于springboot的家庭理财管理系统。Javaee项目,springboot项目。项目介绍:采用M(model)V(view)C(controller)三层体系结构,通过Spring+SpringBoot+Maven+Layui来实现。MySQL数据库作为系统数据储存平台,实现了基于B/S结构的Web系统。分为系统管理员,家主,用户三类身......
  • Springboot+vue汽车销售管理系统。Javaee项目,springboot vue前后端分离项目。
    演示视频:Springboot+vue汽车销售管理系统。Javaee项目,springbootvue前后端分离项目。系统介绍:本文设计了一个基于Springboot+vue的前后端分离的汽车销售管理系统,采用M(model)V(view)C(controller)三层体系结构,通过SpringBoot+Vue+maven+IDEA来实现。有经理和销售两种角......
  • Javaweb之SpringBootWeb案例之阿里云OSS服务集成的详细解析
     2.3.3集成阿里云oss对象存储服务的准备工作以及入门程序我们都已经完成了,接下来我们就需要在案例当中集成oss对象存储服务,来存储和管理案例中上传的图片。编辑在新增员工的时候,上传员工的图像,而之所以需要上传员工的图像,是因为将来我们需要在系统页面当中访问并展示员工的图像。......