《微服务实战》 第二十五章 Java多线程安全与锁

简介: 《微服务实战》 第二十五章 Java多线程安全与锁

前言

本章节介绍Java多线程安全与锁

1、Java多线程安全与锁

1.1、多线程安全问题

当多个线程同时操作同一个数据时,可能会出现数据不一样的情况,这就是线程安全问题。线程安全机制用于保证多个线程访问数据时的一致性.

1.2、线程安全问题三方面

  • 原子性
    一个线程对数据的操作对于其他的线程来说是原子的,要么操作完成,要么什么也没做;当一个线程在操作数据时,不允许其他的线程参与.
  • 可见性
    线程对共享数据的访问是否对其他的线程可见
  • 有序性
    指令重排序与内存重排序指令重排序是指CPU执行指令的顺序与程序的顺序可能不一样; 内存重排序是指内存访问顺序与感知顺序可能不一样。

1.2.1、共享数据在jvm中的表现

  • 每个线程都有独立的线程栈,不能相互访问线程栈的内容
  • 所有线程都能访问堆中的数据
  • 多个线程访问同事访问一个对象实例或者静态变量时,会出现安全问题。

1.2.2、内存抽象模型

  • 每个线程都有自己独立的工作内存
  • 线程1无法访问线程2的工作内存
  • 线程在访问共享数据时,会把主内存中的共享变量复制到自己的工作内存中,线程操作的是工作内存中数据的副本

1.3、Java中的锁

(多线程并发操作同一个数据可能会引发线程安全问题)

锁就是把多个线程对数据的并发操作转换为串行操作。

1.3.1、同一个JVM内锁

1.3.1.1、synchronized 关键字

  • 同步代码块
  • 同步实例方法
  • 同步类的静态方法
    当线程释放锁时,JMM(java内存模型)会把该线程对应的工作内存中的共享变量刷新到主内存中。当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内 存中读取共享变量。

**代码 使用synchronized **

package com.xxxx.reids.thread;
import lombok.AllArgsConstructor;
import lombok.Data;
/***
 * @title Account
 * @desctption 账户
 * @author Kelvin
 * @create 2023/5/29 11:43
 **/
@Data
@AllArgsConstructor
public class Account {
    /**
     * 余额
     */
    private Integer balance;
}
package com.xxxx.reids.thread;
import lombok.AllArgsConstructor;
import lombok.Data;
/***
 * @title Person
 * @desctption 人员信息
 * @author Kelvin
 * @create 2023/5/29 11:44
 **/
@Data
@AllArgsConstructor
public class Person {
    /**
     * 姓名
     */
    private String name;
    /**
     * 取款金额
     */
    private Integer drawAccount;
}
package com.hqyj.reids.thread;
import java.util.concurrent.locks.Lock;
/***
 * @title Draw
 * @desctption 取款
 * @author Kelvin
 * @create 2023/5/29 11:49
 **/
public class Draw implements Runnable{
    private Account account;
    private Person person;
    public Draw(Account account,Person person){
        this.account = account;
        this.person = person;
    }
    @Override
    public void run() {
       synchronized (this.account){
            while (true){
                //余额
                Integer balance = this.account.getBalance();
                if(balance < this.person.getDrawAccount()){
                    //余额不足
                    System.out.println(this.person.getName() + ",账户余额不足");
                    break;
                }else{
               /* try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }*/
                    //取款,减余额
                    this.account.setBalance(balance - this.person.getDrawAccount());
                    System.out.println(this.person.getName() + "取了"+this.person.getDrawAccount()+"钱,账户余额:" + this.account.getBalance());
                }
            }
        }
    }
}

测试

package com.xxxx.redis;
import com.xxxx.reids.thread.Account;
import com.xxxx.reids.thread.Draw;
import com.xxxx.reids.thread.Person;
import org.junit.Test;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/***
 * @title ThreadLockTest
 * @desctption 测试类
 * @author Kelvin
 * @create 2023/5/29 11:50
 **/
public class ThreadLockTest {
    @Test
    public void lock() {
        Lock lock = new ReentrantLock();
        Account account = new Account(100);
        Person zhangSan = new Person("张三", 20);
        Person liSi = new Person("李四", 15);
    new Thread(new Draw(account,zhangSan)).start();
        new Thread(new Draw(account,liSi)).start();
    }
}

**代码2 使用Lock **

package com.hqyj.reids.thread;
import java.util.concurrent.locks.Lock;
/***
 * @title Draw
 * @desctption 取款
 * @author Kelvin
 * @create 2023/5/29 11:49
 **/
public class Draw implements Runnable{
    private Account account;
    private Person person;
    private Lock lock ;
    public Draw(Account account,Person person,Lock lock){
        this.account = account;
        this.person = person;
        this.lock = lock;
    }
    @Override
    public void run() {
        lock.lock();
        //取款操作
        Integer drawAccmount = this.person.getDrawAccount();
        Integer balanceInit = this.account.getBalance();
        //System.out.print("原始值:" + balanceInit + " | ");
        if(balanceInit  < drawAccmount){
            //余额不足,不给取款
            System.out.println("取款余额不足");
            //break;
        }else{
            Integer balance = balanceInit - drawAccmount;
            this.account.setBalance(balance);
            System.out.println(this.person.getName() + "成功取款:" + drawAccmount + ",账户余额:" + balance);
        }
        lock.unlock();
    }
}
import com.xxxx.reids.thread.Account;
import com.xxxx.reids.thread.Draw;
import com.xxxx.reids.thread.Person;
import org.junit.Test;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/***
 * @title ThreadLockTest
 * @desctption 测试类
 * @author Kelvin
 * @create 2023/5/29 11:50
 **/
public class ThreadLockTest {
    @Test
    public void lock() {
        Lock lock = new ReentrantLock();
        Account account = new Account(110);
        Person zhangSan = new Person("张三", 17);
        Person liSi = new Person("李四", 12);
        new Thread(new Draw(account,zhangSan,lock)).start();
        new Thread(new Draw(account,liSi,lock)).start();
        new Thread(new Draw(account,zhangSan,lock)).start();
        new Thread(new Draw(account,liSi,lock)).start();
        new Thread(new Draw(account,zhangSan,lock)).start();
        new Thread(new Draw(account,liSi,lock)).start();
        new Thread(new Draw(account,zhangSan,lock)).start();
        new Thread(new Draw(account,liSi,lock)).start();
    }
}

死锁的例子

Java 死锁产生的四个必要条件:

  • 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
  • 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
  • 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
  • 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
/***
 * @title DieLock
 * @desctption 死锁案例
 * @author Kelvin
 * @create 2023/5/29 15:36
 **/
import java.util.concurrent.TimeUnit;
public class DieLock {
    private static Object object1 = new Object();
    private static Object object2 = new Object();
    public static void main(String[] args) {
        new Thread(){
            @Override
            public void run() {
                synchronized (object1){
                    System.out.println("Thread1 get object1");
                    try {
                        TimeUnit.MILLISECONDS.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (object2){
                        System.out.println("thread1 get object2");
                    }
                }
            }
        }.start();
        new Thread(){
            @Override
            public void run() {
                synchronized (object2){
                    System.out.println("Thread2 get object2");
                    try {
                        TimeUnit.MILLISECONDS.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (object1){
                        System.out.println("thread2 get object1");
                    }
                }
            }
        }.start();
    }
}

1.3.1.2、volatile关键字

volatile是java虚拟机提供的轻量级的同步机制

  • 保证可见性
  • 不保证原子性
  • 保证有序性

什么时候去使⽤Volatile?

  • 某个属性被多个线程共享,其中有⼀个线程修改了此属性,其他线程可以⽴即得到修改后的值,⽐如作为触发器,状态量标记,实现轻量级同步
  • volatile可以在单例双重检查中实现可⻅性和禁⽌指令重排序,可以解决单例双重检查对象初始化代码执⾏乱序问题,从⽽保证安全性。
public class SingletonObject {
    private static volatile SingletonObject singletonObject = new SingletonObject();
    private SingletonObject(){
    }
    public static synchronized SingletonObject getInstance(){
        if(singletonObject == null){
            singletonObject = new SingletonObject();
        }
        return singletonObject;
    }
}

1.3.1.3、volatile和synchronized区别

  • volatile只能修饰实例变量和类变量,只能作⽤于属性,⽽synchronized可以修饰⽅法和代码块。
  • volatile保证数据的可⻅性,⽤于禁⽌指令重排序,但是不保证原⼦性;⽽synchronized是⼀种互斥的机制。
  • volatile属性的读写操作都是⽆锁的,不需要花费时间在获取锁和释放锁上,所以说它是低成本的
  • volatile可以看做是轻量版的synchronized,volatile不保证原⼦性,但是如果是对⼀个共享变量进⾏多个线程的赋值,⽽没有其他的操作,那么就可以⽤volatile来代替synchronized,因为赋值本身是有原⼦性的,⽽volatile⼜保证了可⻅性,所以就可以保证线程安全了。

1.3.1.4、volatile能替代synchronized吗

  • volatile属性的读写操作都是⽆锁的,
  • 它没有提供原⼦性和互斥性,它不能替代synchronized
目录
相关文章
|
8天前
|
监控 Java
java异步判断线程池所有任务是否执行完
通过上述步骤,您可以在Java中实现异步判断线程池所有任务是否执行完毕。这种方法使用了 `CompletionService`来监控任务的完成情况,并通过一个独立线程异步检查所有任务的执行状态。这种设计不仅简洁高效,还能确保在大量任务处理时程序的稳定性和可维护性。希望本文能为您的开发工作提供实用的指导和帮助。
48 17
|
19天前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者
|
4天前
|
缓存 安全 算法
Java 多线程 面试题
Java 多线程 相关基础面试题
|
21天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
21天前
|
消息中间件 缓存 安全
Java多线程是什么
Java多线程简介:本文介绍了Java中常见的线程池类型,包括`newCachedThreadPool`(适用于短期异步任务)、`newFixedThreadPool`(适用于固定数量的长期任务)、`newScheduledThreadPool`(支持定时和周期性任务)以及`newSingleThreadExecutor`(保证任务顺序执行)。同时,文章还讲解了Java中的锁机制,如`synchronized`关键字、CAS操作及其实现方式,并详细描述了可重入锁`ReentrantLock`和读写锁`ReadWriteLock`的工作原理与应用场景。
|
21天前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
45 3
|
21天前
|
存储 安全 Java
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
116 2
|
25天前
|
Java
Java基础却常被忽略:全面讲解this的实战技巧!
本次分享来自于一道Java基础的面试试题,对this的各种妙用进行了深度讲解,并分析了一些关于this的常见面试陷阱,主要包括以下几方面内容: 1.什么是this 2.this的场景化使用案例 3.关于this的误区 4.总结与练习
|
19天前
|
缓存 安全 Java
【JavaEE】——单例模式引起的多线程安全问题:“饿汉/懒汉”模式,及解决思路和方法(面试高频)
单例模式下,“饿汉模式”,“懒汉模式”,单例模式下引起的线程安全问题,解锁思路和解决方法
|
19天前
|
Java 调度
【JavaEE】——线程的安全问题和解决方式
【JavaEE】——线程的安全问题和解决方式。为什么多线程运行会有安全问题,解决线程安全问题的思路,synchronized关键字的运用,加锁机制,“锁竞争”,几个变式