2024年2月

自己常用的 TS 写法总结,应该会一直更新。可使用
TS在线编译
校验 TS 语法。

基本用法

普通

const num: number = 10
const isStop: boolean = false
const title: string = '常用TS总结'
const curName: null = null
const curType: undefined = undefined
const birthday: Date = new Date()

对象

// type
type LoginParams = {
  account: string
}
// interface
interface LoginParams {
  account: string
}

不确定是否有此属性用
?

interface Info {
  id: string
  name: string
  birthday?: Date
}
const curInfo: Info = { id: 'dqe2e', name: 'weizwz' }
console.log(curInfo?.birthday) // undefined

数组

const nums: number[] = [1, 2, 3]
const answer: boolean[] = [false, true, false]
const names: string[] = ['w', 'e', 'i']

对象数组

interface Info {
  id: string
}
const curInfos: Info[] = [ { id: 'dqe2e' }, { id: 'der24' } ]

函数

函数需要声明参数类型和返回值类型

// week: string 指参数的类型;最后的: string 指返回值类型
function getWeek(week: string): string {
  return '星期' + week
}

箭头函数

const getWeek = (week: string): string => {
  return '星期' + week
}
console.log(getWeek('六')) // '星期六'

没有返回值 用
void
表示

interface Cat {
  weight: 5
}
const getName = (obj: Cat): void => {
  console.log(obj.weight + '斤')
}
getName({ weight: 5 }) // '5斤'

any类型

any 类型表示没有任何限制,及时后续使用改变了类型也不会报错。但是并不推荐使用 any,否则使用 TS 也失去了意义。

let x: any = 'weizwz'
x = 1
console.log(x) // 不报错,输出 1

真正的使用场景可能是老项目的升级,你无法确定老旧的代码具体是什么类型;或者一些特殊情况,比如接口返回值类型不确定,或者后续使用时你要修改它的类型。

function getStatus(code: any): Boolean {
  return (code === '200' || code === 'ok' || code === 200 || code === true)
}
console.log(getStatus(400)) // false

类型联合
|

某个变量可能是多个类型中的一个,用
|
来分隔

type Id = string | number
type stringBoolean = '1' | '0'

类型交叉
&

类型交叉一般用于多个类型组成的一个新类型,用
&
来连接

type Name = { name: string };
type User = Name & { age: number };

const zhangSan: User = { name: '张三', age: 18 }

类型断言

手动指定类型,写法是
值 as 类型

<类型>值

为什么要手动指定类型,是在某些特定情况下,我们已经确定这种类型是可以这样操作,但是编译器不确定,会报错,所以我们使用类型断言去告诉编译器这样做没问题。

// 我们获取到 id = name 界面元素
const $name = document.getElementById("name")
// 不是所有界面元素都有 value 属性,在这里我们已经确实我们拿的是 输入框元素,
// 所以使用类型断言告诉编译器,如果存在这个元素,它一定是输入框元素,有 value 属性
if ($name) {
  ($name as HTMLInputElement).value
}

type 和 interface

type 命令用来定义一个类型的别名;
interface 用来声明对象结构。

区别

  • type 能表示的任何类型组合; interface 只能表示对象结构的类型
  • type 后面需要用 =;interface 后面不需要 =
  • interface 可以继承自(extends)interface 或对象结构的 type;type 可以通过 & 做对象结构的继承
  • 多次声明的同名 interface 会进行声明合并;type 不允许多次声明,一个作用域内不允许有多个同名 type

示例

type
使用

type stringBoolean = '1' | '0'

type Position = {
  x: number
  y: number
}

type Position3D = Position & { z: number }

const startPosition: Position = { x: 0, y: 10 }
const startPosition3D: Position3D = { x: 0, y: 10, z: 20 }

// type类型不允许多次声明
type Position = { z: number } // 报错,因为名为 Position 的类型已经被声明

interface
使用

interface Position { x: number }
interface Position { y: number }

const startPosition: Position = { x: 0, y: 10 }

// 同名但有相同属性,要求相同属性的类型要一致,否则会报错
interface Position { x: string } // 报错,与刚开始定义的 x 类型冲突

继承扩展

interface Position3D extends Position {
  z: number
}

const startPosition3D: Position3D = { x: 0, y: 10, z: 20 }

泛型

泛型一般用 T 表示,表示其中的参数/属性/返回值可以是任何类型,如果有多个泛型,可以使用其他字母。
主要使用场景:有些对象中的属性,或者方法里的参数,可能有多个类型,具体类型根据使用场景来定。

基础使用

// type 这里的<T>就相当于类型入参,实际使用时传入具体类型
type Empty<T> = T | undefined | null

const noData: Empty<[]> = []

多个泛型

interface Info<T, S> {
  name: string
  types: T[]
  weight: S
}

const tom: Info<string, number> = { name: 'tom', types: ['cat', 'animal'], weight: 5 }
const idx: Info<number, string> = { name: 'idx', types: [1], weight: 'first' }

函数

// 函数 <T>是泛型写法;arr: T[] 是参数类型;:T 是返回值类型
function getFirst<T>(arr: T[]): T {
  return arr[0]
}

箭头函数
<T,>
加逗号是为了避免编译程序把
<>
解析成
jsx

const getFirst = <T,>(arr: T[]): T => {
  return arr[0]
}

const arr: number[] = [1, 2, 3]
console.log(getFirst<number>(arr), getFirst(arr)) // <number>可省略,打印出来都是 1

嵌套

使用嵌套可以提供代码复用率,如果类型之间差别点太多就没必要了。

interface Tom<T> {
  name: string
  type: T
}
interface People {
  name: string
  type: 'person'
}
interface Cat {
  name: string
  type: 'animal'
}
// 我的兄弟 jakeTom
const myBrother: Tom<People> = {
  name: 'jakeTom',
  type: {
    name: 'my brother',
    type: 'person'
  }
}
// 我的猫咪 catTom
const myCat: Tom<Cat> = {
  name: 'catTom',
  type: {
    name: 'cat',
    type: 'animal'
  }
}

特殊用法

动态变量名

Record<Keys, Type>
返回一个对象类型,参数
Keys
用作键名,参数
Type
用作键值类型。

type stringKey = Record<string, string>
// 处理动态变量名
const list: stringKey = { img_1: 'img/1.png', img_2: 'img/2.png', img_3: 'img/3.png' }
for (const key in list) {
  console.log(list[key])
}
for(let i = 0; i < 3; i++) {
  console.log(list['img_' + (i + 1)])
}

vue3中的TS

响应式数据


xxx.d.ts
里定义类型

interface Account = {
  id: string
  name: string
}

在 vue 界面里使用

// 简单类型可省略类型声明
const loading = ref(false)
const user = ref<Account>({
  id: 'E2U1EU91U',
  name: 'weiz'
})

参数传递

父组件使用

<script setup lang="ts">
import { ref } from 'vue'
const user = ref<Account>({
  id: 'E2U1EU91U',
  name: 'weiz'
})
</script>
<template>
  <Children :account="user">
</template>

子组件接收参数

const props = defineProps<Account>()

如果没有声明
Account
,则可以具体定义

const props = defineProps<{
  id: string
  name: string
}>()

组件实例类型

InstanceType<T>
是 ts 自带的类型,能够直接获取组件完整的实例类型。

子组件

<script setup lang="ts">
const open = () => { console.log('打开')}
// 子组件暴露方法
defineExpose({
  open
})
</script>

父组件

<script setup lang="ts">
import { ref } from 'vue'
import Children from './Children.vue'

type ChildCtx = InstanceType<typeof Children>
// 要和子组件的 ref 名称一致
const childrenRef = ref<ChildCtx | null>(null)
// 调用子组件方法
const openChildren = () => {
 childrenRef.value?.open()
}
</script>
<template>
  <Children ref='childrenRef'/>
</template>

一、java锁存在的必要性

要认识java锁,就必须对2个前置概念有一个深刻的理解:
多线程

共享资源

对于程序来说,数据就是资源。

在单个线程操作数据时,或快或慢不存在什么问题,一个人你爱干什么干什么。

多个线程操作各自操作不同的数据,各干各的,也不存在什么问题。

多个线程对共享数据进行读取操作,我就四处看看,什么也不动,也不存在什么问题。

但如果
多个线程

共享数据
进行

操作,问题就来了。

经典库存问题:

mysql 记录剩余:1,redis 缓存记录剩余:1。

小明上网下单,后台程序检查 redis 记录存货剩 1 台,数据库执行 -1,但小明网太卡了,数据库刚执行完 -1,redis 没来得及更新成0,小红的华为5G直接下单,redis 剩1台,数据库-1,redis -1,下单成功一气呵成。结果就是2个人买了同一台手机。

这种业务场景可以说比比皆是,所以要解决这种数据同步问题就要有对应的办法,所以发明了java锁这个工具来保证数据的一致性,举个例子:

在一个不分男女的公共厕所中上一把锁,有人进去,把门锁住,上完出来,把锁打开,以此类推。

二、2个重要的java锁

synchronized关键字

synchronized关键字是java开发人员最常用的给共享资源上锁的方式,也基本可以满足一般的进程同步要求,使用 synchronized 无需手动执行加锁和释放锁的操作,只需在需要同步的
代码块、普通方法、静态方法
上加入该关键字即可,JVM 层面会帮我们自动的进行加锁和释放锁的操作。

修饰普通方法

/**
 * synchronized 修饰普通方法
 */
public synchronized void method() {
    // ....
}

当 synchronized 修饰普通方法时,被修饰的方法被称为同步方法,其作用范围是整个方法,作用的对象是调用这个方法的对象。

修饰静态方法

/**
 * synchronized 修饰静态方法
 */
public static synchronized void staticMethod() {
    // .......
}

当 synchronized 修饰静态方法时,其作用范围是整个程序,这个锁对于所有调用这个锁的对象都是互斥的。

修饰普通方法 VS 修饰静态方法

创建一个类,其中有synchronized修饰的普通方法和synchronized修饰的静态方法。

public class SynchronizedUsage {
    /**
     * synchronized 修饰普通方法
     */
    public synchronized void method() {
        System.out.println("普通方法执行时间:" + LocalDateTime.now());
        try {
            // 休眠 3s
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * synchronized 修饰静态方法
     */
    public static synchronized void staticMethod() {
        System.out.println("静态方法执行时间:" + LocalDateTime.now());
        try {
            // 休眠 3s
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

测试

    public class Test01 {
        /**
         * 创建线程池同时执行任务
         */
        static ExecutorService threadPool = Executors.newFixedThreadPool(10);

        public static void main(String[] args) {

            // 执行两次静态方法
            threadPool.execute(() -> {
                SynchronizedUsage.staticMethod();
            });
            threadPool.execute(() -> {
                SynchronizedUsage.staticMethod();
            });

            // 执行两次普通方法
            threadPool.execute(() -> {
                SynchronizedUsage usage = new SynchronizedUsage();
                usage.method();
            });
            threadPool.execute(() -> {
                SynchronizedUsage usage2 = new SynchronizedUsage();
                usage2.method();
            });
        }
    }

结果

说明:

普通方法的2次调用归属于不同的对象,也就是不同的锁,所以执行的时候互不影响。

静态方法的2次调用归属于同一个类,也就是相同的锁,所以分先后执行,间隔3s。

修饰代码块

我们在日常开发中,最常用的是给代码块加锁,而不是给方法加锁,因为给方法加锁,相当于给整个方法全部加锁,这样的话锁的粒度就太大了,程序的执行性能就会受到影响,所以通常情况下,我们会使用 synchronized 给代码块加锁,它的实现语法如下:

public void classMethod() throws InterruptedException {
    // 前置代码...
    
    // 加锁代码
    synchronized (SynchronizedUsage.class) {
        // ......
    }
    
    // 后置代码...
}

从上述代码我们可以看出,相比于修饰方法,修饰代码块需要自己手动指定加锁对象,加锁的对象通常使用 this 或 xxx.class 这样的形式来表示,比如以下代码:

// 加锁某个类
synchronized (SynchronizedUsage.class) {
    // ......
}

// 加锁当前类对象
synchronized (this) {
    // ......
}

以上2中加锁方式类似于上文中普通方法与静态方法的区别,加锁当前类对象this只作用于当前对象,对象不同则锁不同,加锁某个类则作用于该类,同属于一个类的对象使用同一把锁。

创建一个类

    public class SynchronizedUsageBlock {
        /**
         * synchronized(this) 加锁
         */
        public void thisMethod() {
            synchronized (this) {
                System.out.println("synchronized(this) 加锁:" + LocalDateTime.now());
                try {
                    // 休眠 3s
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        /**
         * synchronized(xxx.class) 加锁
         */
        public void classMethod() {
            synchronized (SynchronizedUsageBlock.class) {
                System.out.println("synchronized(xxx.class) 加锁:" + LocalDateTime.now());
                try {
                    // 休眠 3s
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

测试

    public class Test02 {
        public static void main(String[] args) {
            // 创建线程池同时执行任务
            ExecutorService threadPool = Executors.newFixedThreadPool(10);

            // 执行两次 synchronized(this)
            threadPool.execute(() -> {
                SynchronizedUsageBlock usage = new SynchronizedUsageBlock();
                usage.thisMethod();
            });
            threadPool.execute(() -> {
                SynchronizedUsageBlock usage2 = new SynchronizedUsageBlock();
                usage2.thisMethod();
            });

            // 执行两次 synchronized(xxx.class)
            threadPool.execute(() -> {
                SynchronizedUsageBlock usage3 = new SynchronizedUsageBlock();
                usage3.classMethod();
            });
            threadPool.execute(() -> {
                SynchronizedUsageBlock usage4 = new SynchronizedUsageBlock();
                usage4.classMethod();
            });
        }
    }

结果

Lock接口

Lock接口及其相关的实现类是在JDK 1.8之后在并发包中新增的,最常用且常见的就是ReentrantLock。与synchronized不同的是,ReentrantLock在使用时需要显式的获取和释放锁。

虽然它缺少了隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、
可中断的获取锁
以及
超时获取锁
等多种synchronized关键字所不具备的同步特性。

Lock接口提供的synchronized不具备的特性

Lock接口中定义的方法

尽管java实现的锁机制有很多种,并且有些锁机制性能也比synchronized高,但还是强烈推荐在多线程应用程序中使用该关键字,因为实现方便,后续工作由jvm来完成,可靠性高。只有在确定锁机制是当前多线程程序的性能瓶颈时,才考虑使用其他机制,如ReentrantLock等。

三、java锁的核心分类

悲观锁

悲观锁总是假设最坏的情况,每次取数据时都认为其他线程会对数据进行修改,所以都会加锁,当其他线程想要访问数据时,都需要阻塞挂起。所以悲观锁总结为
悲观加锁阻塞线程


  • 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。

MySQL数据库中的表锁、行锁、读锁、写锁等,Java中,synchronized关键字和Lock的实现类都是悲观锁。

乐观锁

而乐观锁认为自己在使用数据时不会有别的线程修改数据,所以
不会添加锁
,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如
报错
或者
自动重试
)。总结为
乐观无锁回滚重试


  • 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。


  • 乐观锁天生免疫死锁。

乐观锁一般有2种实现方式:

CAS算法

即compare and swap 或者 compare and set,涉及到三个操作数,数据所在的内存值,预期值,新值。当需要更新时,判断当前内存值与之前取到的值是否相等,若相等,则用新值更新,若失败则重试,一般情况下是一个自旋操作,即不断的重试。

优点:

效率比较高,无阻塞,无等待,重试。

缺点:

ABA问题:
因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么CAS检查时发现它的值没有发生变化,但实际上发生了变化:A->B->A的过程。

循环时间长,开销大:
自旋CAS如果长时间不成功,会给CPU带来很大的执行开销。

只能保证一个共享变量的原子操作:
当对一个共享变量操作时,我们可以采用CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性。

版本号机制

一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加1。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

    update table 
    set x = x + 1, version = version + 1 
    where id = #{id} and version = #{version};

MybaisPlus对乐观锁的实现

1)在数据库中添加 version 字段,作为乐观锁的版本号

    --在数据库中的user表中添加一个version字段,用于实现乐观锁
    ALTER TABLE `user` ADD COLUMN `version` INT

2)在对应的实体类中添加 version 属性,并且在这个属性上面添加 @Version 注解

    @Data
    public class User {
        @TableId(type = IdType.AUTO)//主键自动增长
        private Long id;
        private String name;
        private Integer age;
        private String email;
     
        @TableField(fill = FieldFill.INSERT)//INSERT的含义就是添加,也就是说在做添加操作时,下面一行中的createTime会有值
        private Date createTime;
     
        @TableField(fill = FieldFill.INSERT_UPDATE)//INSERT_UPDATE的含义就是在做添加和修改时下面一行中的updateTime都会有值,因为是第一次添加,还没有做修改(一般都使用这个)
        private Date updateTime;
     
        @Version//版本号,用于实现乐观锁(这个一定要加)
        @TableField(fill = FieldFill.INSERT)//添加这个注解是为了在后面设置初始值,不加也可以
        private Integer version;
    }

3)写一个配置类,配置乐观锁插件

    @Configuration
    @MapperScan("cn.hb.mapper")
    public class MpConfig {
        //乐观锁插件
        @Bean
        public OptimisticLockerInterceptor optimisticLockerInterceptor() {
            return new OptimisticLockerInterceptor();
        }
    }

4)设置版本号 version 的初始值为1

5)向表中添加一条数据,看 version 的值是否为1

6)测试乐观锁,看 version 的值是否加1

四、java锁的其他分类

synchronized性能优化

锁膨胀/锁升级

JDK 6之前synchronized是一个独占式的悲观锁、重量级锁,效率偏低。JDK 6中为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。

所以目前锁一共有4种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁。锁状态只能升级不能降级。(注意无锁和偏向锁是同一级别,锁标志位都是01,二者之间不存在膨胀关系,可以理解为无锁状态是轻量锁的空闲状态)

偏向锁

在程序第一次执行到 synchronized 代码块的时候,锁对象变成 偏向锁 ,即偏向于第一个获得它的线程的锁。在程序第二次执行到改代码块时,线程会判断此时持有锁的线程是否就是它自己,如果是就继续往下面执行。值得注意的是,在第一次执行完同步代码块时,并不会释放这个偏向锁。从效率角度来看,如果第二次执行同步代码块的线程一直是一个,并不需要重新做加锁操作,没有额外开销,效率极高。

轻量级锁

当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。

这里不同情况需值得注意:当第二个线程想要获取锁时,且这个锁是偏向锁时,会判断当前持有锁的线程是否仍然存活,如果该持有锁的线程没有存活,那么偏向锁并不会升级为轻量级锁 。

重量级锁

若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。

当其他线程再次尝试获取锁的时候,发现现在的锁是重量级锁,此时线程都会进入阻塞状态。

锁消除

锁消除即删除不必要的加锁操作。JVM在运行时,对一些“在代码上要求同步,但是**被检测到不可能存在共享数据竞争情况”的锁进行消除。**根据代码逃逸技术,如果判断到一段代码中,
堆上的数据不会逃逸出当前线程
,那么就可以认为这段代码是线程安全的,无需加锁。

锁粗化

假设一系列的连续操作都会
对同一个对象反复加锁及解锁
,甚至加锁操作是出现在循环体中的,即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。

如果JVM检测到有一连串零碎的操作都是对同一对象的加锁,将会
扩大加锁同步的范围(即锁粗化)到整个操作序列的外部。

自适应自旋锁

自旋锁

现在绝大多数的个人电脑和服务器都是多路(核)处理器系统,如果物理机器有一个以上的处理器或者处理器核心,能让两个或以上的线程同时并行执行,就可以让后面请求锁的那个线程“稍等一会”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。自旋锁优点在于它避免一些线程的挂起和恢复操作,因为挂起线程和恢复线程都需要从用户态转入内核态,这个过程是比较慢的,所以通过自旋的方式可以一定程度上避免线程挂起和恢复所造成的性能开销。

自适应自旋锁:

但是,如果长时间自旋还获取不到锁,那么也会造成一定的资源浪费,所以我们通常会给自旋设置一个固定的值来避免一直自旋的性能开销。然而对于 synchronized 关键字来说,它的自旋锁更加的“智能”,synchronized 中的自旋锁是自适应自旋锁。

自适应自旋锁是指,**线程自旋的次数不再是固定的值,而是一个动态改变的值,这个值会根据前一次自旋获取锁的状态来决定此次自旋的次数。**比如上一次通过自旋成功获取到了锁,那么这次通过自旋也有可能会获取到锁,所以这次自旋的次数就会增多一些,而如果上一次通过自旋没有成功获取到锁,那么这次自旋可能也获取不到锁,所以为了避免资源的浪费,就会少循环或者不循环,以提高程序的执行效率。简单来说,
如果线程自旋成功了,则下次自旋的次数会增多,如果失败,下次自旋的次数会减少。

防止死锁

• 不要写嵌套锁,容易死锁

• 尽量少用同步代码块(Synchronized)

• 尽量使用ReentrantLock的tryLock方法设置超时时间,超时可以退出,防止死锁

• 尽量降低锁粒度,尽量不要几个功能一把锁

公平锁与非公平锁

当一个线程持有的锁释放时,其他线程按照先后顺序,先申请的先得到锁,那么这个锁就是公平锁。反之,如果后申请的线程有可能先获取到锁,就是非公平锁 。

Java 中的 ReentrantLock 可以通过其构造函数来指定是否是公平锁,
默认是非公平锁
。一般来说,使用非公平锁可以获得较大的吞吐量,所以
推荐优先使用非公平锁

synchronized 是一种非公平锁。

可重入锁和非可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中
ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

    public class Widget {
        public synchronized void doSomething() {
            System.out.println("方法1执行...");
            doOthers();
        }

        public synchronized void doOthers() {
            System.out.println("方法2执行...");
        }
    }

在上面的代码中,类中的两个方法都是被内置锁synchronized修饰的,doSomething()方法中调用doOthers()方法。因为内置锁是可重入的,所以同一个线程在调用doOthers()时可以直接获得当前对象的锁,进入doOthers()进行操作。

如果是一个
不可重入锁
,那么当前线程在调用doOthers()之前需要将执行doSomething()时获取当前对象的锁释放掉,实际上该对象锁已被当前线程所持有,且无法释放。所以此时会出现死锁。

独享锁与共享锁

独占锁是一种思想:只能有一个线程获取锁,以独占的方式持有锁。

Java中用到的独占锁:synchronized,ReentrantLock

共享锁是一种思想:可以有多个线程获取读锁,以共享的方式持有锁。

Java中用到的共享锁:ReentrantReadWriteLock。


往期推荐:


师爷,翻译翻译什么叫AOP


终于搞懂动态代理了!


学会@ConfigurationProperties月薪过三千


0.o?让我看看怎么个事儿之SpringBoot自动配置


不是银趴~是@Import!


Java反射,看完就会用

写在前面

偷懒,先写了数组,列表要画图,所以今天就先不写了

数组的定义

数组是由n个相同类型的数据元素构成的有限序列。每个数据元素被称为
一个数组元素
,每个元素在n个线性关系中的序号称为该元素的
下标
,下标的取值范围称为数组的
维界

数组与线性表的关系:数组是线性表的
推广
。一维数组可视为一个线性表,二维数组可视为其元素是定长数组的线性表。因此,除结构的初始化和销毁外,数组只会有存取元素和修改元素的操作。

数组的顺序存储

一维数组


\(A[0 \dots n-1]\)
为例,其存储结构关系式为:

\[LOC(a_i) = LOC(a_0) + i \times L(0 \leq i < n)
\]

其中,
\(L\)
是每个数组元素所占的存储单元。

多维数组

以二维数组为例。设二维数组的行下标与列下标的范围分别为
\([0, h_1]\)

\([0,h_2]\)

按行优先

先行后列,先存储行号较小的元素,行号相等先存储列号较小的元素。存储结构关系式为:

\[LOC(a_{i,j}) = LOC(a_{0,0})+[i \times(h_2+1) + j] \times L
\]

例如对于数组
\(A_{[2][3]}\)
。它按行优先方式在内存中的存储形式如下所示:

\[\left[
\begin{matrix}

a_{[0][0]} & a_{[0][1]} & a_{[0][2]} \\
a_{[1][0]} & a_{[1][1]} & a_{[1][2]} \\

\end{matrix}
\right]
\]

\(a_{[0][0]}\) \(a_{[0][1]}\) \(a_{[0][2]}\) \(a_{[1][0]}\) \(a_{[1][1]}\) \(a_{[1][2]}\)

列优先

存储结构关系式为:

\[LOC(a_{i,j}) = LOC(a_{0,0})+[j \times (h_1 + 1) + i] \times L
\]

例如对于数组
\(A_{[2][3]}\)
。它按行优先方式在内存中的存储形式如下所示:

\[\left[
\begin{matrix}

a_{[0][0]} & a_{[0][1]} & a_{[0][2]} \\
a_{[1][0]} & a_{[1][1]} & a_{[1][2]} \\

\end{matrix}
\right]
\]

\(a_{[0][0]}\) \(a_{[1][0]}\) \(a_{[0][1]}\) \(a_{[1][1]}\) \(a_{[0][2]}\) \(a_{[1][2]}\)

特殊矩阵的压缩存储

压缩存储:指为多个值相同的元素只分配一个存储空间,对零元素不分配空间;

特殊矩阵:指具有许多相同矩阵元素或零元素,并且这些相同矩阵元素或零元素的分布具有一定规律性的矩阵;

特殊矩阵的压缩存储:找出特殊矩阵中值相同的矩阵元素的分布规律,把那些呈现规律性分布的、值相同的多个矩阵元素压缩存储到一个存储空间中。

对称矩阵

对一个n阶矩阵
\(A\)
中的任意一个元素
\(a_{i,j}\)
都有
\(a_{i, j} = a_{j, i}(1 \leq i, j \leq n)\)
,则称其为对称矩阵

\[\left[
\begin{matrix}
a_{1,1} & a_{1,2} & \cdots & a_{1,n} \\
a_{2,1} & a_{2,2} & \cdots & a_{2,n} \\
\vdots & \vdots & \ddots & \vdots \\
a_{n,1} & a_{n,2} & \cdots & a_{n,n}
\end{matrix}
\right]
\]

很显然,对于n阶对称矩阵,上三角区所有元素和下三角区的对应元素相同,采用二维数组存放,会造成大范围的空间浪费,因此我们把其存放在
一维数组
\(B[\frac{n(n+1)}{2}]\)
中。

比如只存放下三角部分的元素:

在数组
\(B\)
中,位于元素
\(a_{i, j}\)
前的元素个数为:

第1行:1个(
\(a_{1,1}\)

第2行:2个(
\(a_{2,1},a_{2,2}\)

\(\dots\)


\(i-1\)
行:
\(i-1\)
个(
\(a_{i-1,1},a_{i-1,2} \dots ,a_{i-1,i-1}\)


\(i\)
行:
\(j-1\)
个(
\(a_{i,1},a_{i,2}, \dots , a_{i,j-1}\)

因此,元素
\(a_{i,j}\)
在数组
\(B\)
中的下标
\(k = 1 + 2 + \dots + (i - 1) + j - 1 = \frac{i(i - 1)}{2} + j - 1\)

因此,元素下标之间对应关系如下:

\[k =
\begin{cases}
\frac{i(i-1)}{2} + j - 1&, \qquad i \geq j \\
\frac{j(j-1)}{2} + i - 1&, \qquad i < j
\end{cases}
\]

三角矩阵

下三角矩阵

\[\left[
\begin{matrix}
a_{1,1} \\
a_{2,1} & a_{2,2} \\
\vdots & \vdots & \ddots \\
a_{n,1} & a_{n,2} & \cdots a_{n,n}
\end{matrix}
\right]
\]

上三角区为统一常量。元素下标之间的对应关系为:

\[k =
\begin{cases}
\frac{i(i-1)}{2} + j - 1 &, \qquad i \geq j \\
\frac{n(n-1)}{2} &, \qquad i < j
\end{cases}
\]

下标 0 1 2 3 4 5 \(\cdots\) \(\frac{n(n+1)}{2}\)
元素 \(a_{1,1}\) \(a_{2,1}\) \(a_{2,2}\) \(a_{3,1}\) \(a_{3,2}\) \(a_{3,3}\) \(\cdots\) \(a_{n,1}\) \(a_{n,2}\) \(\cdots\) \(a_{n,n}\) \(c\)
行号 第一行 第二行 第二行 第三行 第三行 第三行 \(\cdots\) 第n行 第n行 \(\cdots\) 第n行 常数项

上三角矩阵

\[\left[
\begin{matrix}
a_{1,1} & a_{1,2} & \cdots & a_{1,n} \\
& a_{2,2} & \cdots & a_{2,n} \\
& & \ddots & \vdots \\
& & & a_{n,n}
\end{matrix}
\right]
\]

与上文类似地,位于元素
\(a_{i,j}(i \leq j)\)
前面的元素个数为:

第1行:
\(n\)

第2行:
\(n-1\)

\(\dots\)


\(i-1\)
行:
\(n - i + 2\)


\(i\)
行:
\(j-1\)

因此,元素
\(a_{i,j}\)
在数组
\(B\)
中的下标
\(k = n + (n - 1) + \dots + (n - i + 2) + (j - i + 1) - 1\)

因此,元素下标之间对应关系如下:

\[k =
\begin{cases}
\frac{(i-1)(2n - i + 2)}{2} + j - i &, \qquad i \leq j \\
\frac{n(n+1)}{2} &, \qquad i > j
\end{cases}
\]

下标 0 1 \(\cdots\) \(\frac{n(n+1)}{2}\)
元素 \(a_{1,1}\) \(a_{1,2}\) \(\cdots\) \(a_{1,n}\) \(a_{2,2}\) \(a_{2,3}\) \(\cdots\) \(a_{2,n}\) \(\cdots\) \(a_{n,n}\) \(c\)
行号 第一行 第一行 第一行 第一行 第二行 第二行 第二行 第二行 \(\cdots\) 第n行 常数

三对角矩阵

对n阶矩阵
\(A\)
中的任意元素
\(a_{i,j}\)
,都有当
\(|i-j| >1\)
时,
\(a_{i,j} = 0\)

\[\left[
\begin{matrix}
a_{1,1} & a_{1,2} \\
a_{2,1} & a_{2,2} & a_{2,3} & & 0 \\
& a_{3,2} & a_{3,3} & a_{3,4} \\
& & \ddots & \ddots & \ddots \\
& 0 & & a_{n-1,n-2} & a_{n-1,n-1} & a_{n-1,n} \\
& & & & a_{n,n-1} & a_{n, n}
\end{matrix}
\right]
\]

稀疏矩阵

矩阵中非零元素的个数t,相对于矩阵元素的个数s来说非常少,即
\(s >> t\)
的矩阵称为
稀疏矩阵

我们可以用对应的三元组线性表来存储稀疏矩阵,如下例:

\[M =
\left[
\begin{matrix}
4 & 0 & 0 & 0 \\
0 & 0 & 6 & 0 \\
0 & 9 & 0 & 0 \\
0 & 23 &0 & 0
\end{matrix}
\right]
\]

对应的三元组为:

\[\left(
\begin{matrix}
i & j & a_{i,j} \\
0 & 0 & 4 \\
1 & 2 & 6 \\
2 & 1 & 9 \\
3 & 1 & 23
\end{matrix}
\right)
\]

下面,上代码,可以实现稀疏矩阵的输入、输出,稀疏矩阵对应三元组的加法、乘法、转置:

#include <stdio.h>
#include <stdlib.h>
#define MAXSIZE 10000

typedef int ElemType;

typedef struct {

    int i, j;
    ElemType e;

}Triple;


typedef struct {

    Triple data[MAXSIZE + 1];
    int mu, nu, tu;          //矩阵行数,列数和非0元个数

}TSMatrix;



//输入稀疏矩阵数据
void InPutM(TSMatrix& M) {
    printf("输入稀疏矩阵的 行数, 列数, 非0元个数 :\n");
    scanf_s("%d %d %d", &M.nu, &M.mu, &M.tu);
    printf("输入矩阵非0元素的 所在行i, 所在列j, 值e:\n");
    for (int k = 1; k <= M.tu; k++) {
        scanf_s("%d %d %d", &M.data[k].i, &M.data[k].j, &M.data[k].e);
    }
}



//打印稀疏矩阵三元组数据
void PrintM(TSMatrix T) {
    printf("  %d    %d    %d\n", T.mu, T.nu, T.tu);
    printf("  ------------\n");
    for (int k = 1; k <= T.tu; k++) {
        printf("  %d    %d    %d\n", T.data[k].i, T.data[k].j, T.data[k].e);
    }
}



//稀疏矩阵三元组加法
void AddSMatrix(TSMatrix a, TSMatrix b, TSMatrix& c) {
    int i = 0, j = 0, k = 0;
    ElemType v;                            //用于计算和
    if (a.mu != b.mu || a.nu != b.nu)       //两矩阵无法相加
        return;

    c.mu = a.mu;
    c.nu = a.nu;
    while (i < a.tu || j < b.tu)
    {
        //若行相等,看列
        if (a.data[i + 1].i == b.data[j + 1].i)
        {
            //行相同时的第一种情况
            if (a.data[i + 1].j < b.data[j + 1].j)
            {
                c.data[k + 1].i = a.data[i + 1].i;
                c.data[k + 1].j = a.data[i + 1].j;
                c.data[k + 1].e = a.data[i + 1].e;
                k++;
                i++;        //前往下一个a中的非0元
            }
            //行相同时的第二种情况
            else if (a.data[i + 1].j > b.data[j + 1].j)
            {
                c.data[k + 1].i = b.data[j + 1].i;
                c.data[k + 1].j = b.data[j + 1].j;
                c.data[k + 1].e = b.data[j + 1].e;
                k++;
                j++;        //前往下一个b中的非0元
            }
            //行相同的第三种情况
            else
            {
                v = a.data[i + 1].e + b.data[j + 1].e;
                if (v != 0)
                {
                    c.data[k + 1].i = a.data[i + 1].i;
                    c.data[k + 1].j = a.data[i + 1].j;
                    c.data[k + 1].e = v;
                    k++;
                }
                i++;
                j++;
            }
        }
        //若行不相同 的两种情况
        else if (i == a.tu || a.data[i + 1].i > b.data[j + 1].i && j != b.tu)
        {
            c.data[k + 1].i = b.data[j + 1].i;
            c.data[k + 1].j = b.data[j + 1].j;
            c.data[k + 1].e = b.data[j + 1].e;
            k++;
            j++;      //前往下一个b的非0元
        }
        else if (j == b.tu || a.data[i + 1].i < b.data[j + 1].i && i != a.tu)
        {
            c.data[k + 1].i = a.data[i + 1].i;
            c.data[k + 1].j = a.data[i + 1].j;
            c.data[k + 1].e = a.data[i + 1].e;
            k++;
            i++;      //前往下一个a的非0元
        }
    }
    c.tu = k;
}



//乘法辅助函数
int Getval(TSMatrix a, int i, int j) {
    int k = 1;
    while (k <= a.tu && (a.data[k].i != i || a.data[k].j != j))
        k++;
    if (k <= a.tu)
        return a.data[k].e;
    else
        return 0;
}



//稀疏矩阵三元组乘法
void MultSMatrix(TSMatrix a, TSMatrix b, TSMatrix& c) {
    int p = 0;
    ElemType s;
    if (a.nu != b.mu)
        return;

    for (int i = 1; i <= a.mu; i++) {
        for (int j = 1; j <= b.nu; j++) {
            s = 0;
            for (int k = 1; k <= a.nu; k++)
                s += Getval(a, i, k) * Getval(b, k, j);
            if (s != 0) {
                c.data[p + 1].i = i;
                c.data[p + 1].j = j;
                c.data[p + 1].e = s;
                p++;
            }
        }
    }
    c.mu = a.mu;
    c.nu = b.nu;
    c.tu = p;
}



//稀疏矩阵转置   (适用于 tu << mu × nu 的情况)
void TransposeSMatrix(TSMatrix M, TSMatrix& T) {
    T.mu = M.nu;                           //T行数等于原矩阵列数
    T.nu = M.mu;                           //T列数等于原矩阵行数
    T.tu = M.tu;
    if (!T.tu)
        return;

    int q = 1;                             //从列数小的开始,一一对应赋值
    for (int col = 1; col <= M.nu; ++col) {
        for (int p = 1; p <= M.tu; ++p) {
            if (M.data[p].j == col) {
                T.data[q].i = M.data[p].j;
                T.data[q].j = M.data[p].i;
                T.data[q].e = M.data[p].e;
                q++;
            }
        }
    }
}



//稀疏矩阵的快速转置算法
int cpot[MAXSIZE + 1], num[MAXSIZE + 1];   //辅助数组  
//cpot[col] 表示M中第col列第一个非0元在T.data中的位置
//num[col]  表示M中第col列中非0元的个数
void FastTransposeSMatrix(TSMatrix M, TSMatrix& T) {
    T.mu = M.nu;
    T.nu = M.mu;
    T.tu = M.tu;
    if (!T.tu)
        return;

    for (int col = 1; col <= M.mu; col++)
        num[col] = 0;                      //初始化为0

    for (int k = 1; k <= M.tu; k++)
        num[M.data[k].j]++;                //记录M.data[k].j列中非0元个数 (简易哈希表)

    cpot[1] = 1;                           //初始化第一个非0元的序号
    for (int col = 2; col <= M.mu; col++)   //求第col列中第一个非零元在T.data中的序号   
        cpot[col] = cpot[col - 1] + num[col - 1];

    for (int p = 1; p <= M.tu; p++) {
        int col = M.data[p].j;             //此时M对应三元组中的非0元的所在列
        int q = cpot[col];                  //q为当前非0元的应当放置的序号位置
        T.data[q].i = M.data[p].j;
        T.data[q].j = M.data[p].i;
        T.data[q].e = M.data[p].e;
        cpot[col]++;                       //cpot[col]++,对应下一个此列中非0元的序号
        //cpot[col]最后一直加到等于cpot[col + 1],第col列也就不会有更多的非0元了
    }
}




int main() {
    TSMatrix A, B, C, D;
    printf("输入稀疏矩阵A的三元组:\n");
    InPutM(A);
    PrintM(A);
    printf("\n输入稀疏矩阵B的三元组:\n");
    InPutM(B);
    PrintM(B);
    //printf("\n矩阵A与B相加得到矩阵C:\n");
    //AddSMatrix(A, B, C);
    //PrintM(C);
    printf("\n矩阵A与B相乘得到矩阵D:\n");
    MultSMatrix(A, B, D);
    PrintM(D);
    printf("\n");
    system("pause");
    system("cls");



    TSMatrix M, T, FT;
    printf("————稀疏矩阵转置测试————\n\n");
    InPutM(M);
    printf("\n稀疏矩阵转置前三元组: \n");
    PrintM(M);

    printf("\n稀疏矩阵转置结果: \n");
    TransposeSMatrix(M, T);
    PrintM(T);

    printf("\n稀疏矩阵的快速转置结果: \n");
    FastTransposeSMatrix(M, FT);
    PrintM(FT);
}

本文全面深入地探讨了对象存储服务(OSS)的核心技术、基础知识和高级功能。从媒体存储到数据备份,再到数据仓库与数据湖,我们不仅解析了OSS在各种应用场景下的关键角色,还深入讨论了其与机器学习、多媒体处理以及日志和监控等多个开发场景的结合。

关注【TechLeadCloud】,分享互联网架构、云服务技术的全维度知识。作者拥有10+年互联网服务架构、AI产品研发经验、团队管理经验,同济本复旦硕,复旦机器人智能实验室成员,阿里云认证的资深架构师,项目管理专业人士,上亿营收AI产品研发负责人。

file

一、引言

OSS概述

对象存储服务(Object Storage Service,简称OSS)作为云计算生态系统的一个关键组件,逐渐在全球范围内得到广泛应用。与传统的文件存储(File Storage)和块存储(Block Storage)不同,OSS通过提供高度分布式的存储解决方案,实现了数据的可伸缩性、持久性和访问性。在许多方面,OSS不仅是一种数据存储模式,还是一种全新的数据管理和分发理念。

数据的“解耦”思想

例如,在传统的文件存储系统中,数据通常与其元数据(如文件名、路径等)紧密耦合,这导致了数据的管理和扩展具有局限性。对象存储通过将数据和元数据“解耦”,每个数据对象都可以独立于其他对象存在。这种“解耦”思想为数据的水平扩展提供了极大的灵活性。比如,一个全球分布的企业可以将不同地理位置的数据中心通过OSS连接在一起,而不需要担心数据格式和结构的不一致性。

高可用性和灵活性

当谈到数据一致性和可用性时,对象存储同样具有出色的表现。以Amazon S3为例,其通过多区域(Multi-Region)的复制机制,确保了数据在发生硬件故障或者数据中心故障时仍然可用。而且,多数对象存储服务支持多版本控制,这意味着在错误删除或修改数据后,你还有机会恢复到之前的版本。

元数据的丰富性

在对象存储中,元数据不仅限于基础的文件属性(如名称、大小、类型等),还可以包括更为复杂和丰富的信息,如访问控制列表(ACLs)、数据生命周期策略和自定义标签等。这使得对象存储可以更为智能地管理数据,比如自动将长时间未访问的数据转移到低成本的存储层级。

综上所述,对象存储服务OSS以其独特的存储模型和灵活、高效的数据管理能力,正成为现代云计算和大数据应用的基础设施之一。在本文中,我们将深入探讨对象存储服务OSS的各个方面,包括其核心组件、架构设计、高级功能和性能优化等,以期为您提供一个全面且深入的理解。


二、基础知识

file

对象存储与块存储、文件存储的区别

在讨论对象存储服务(OSS)的具体细节之前,了解其与其他主流存储解决方案——块存储和文件存储——的区别是非常必要的。

数据组织和访问模式

  • 文件存储
    : 在文件存储系统中,数据按照文件和目录的形式进行组织,很像我们在个人电脑上看到的文件系统。这种方式易于理解,但在处理大规模数据时,可能会遇到性能瓶颈。

    例子
    : 网络共享盘(如NFS, SMB)就是文件存储的一个典型应用。

  • 块存储
    : 块存储将数据分成固定大小的“块”,并通过一个标识符进行索引。这种方法提供了高性能和低延迟的数据访问,但缺乏高级的数据管理和保护机制。

    例子
    : 系统级的磁盘阵列和SAN(Storage Area Network)通常使用块存储。

  • 对象存储
    : 对象存储则是一个更为抽象的概念,它将数据和元数据封装为一个“对象”,并通过唯一的对象标识符(Object ID)进行访问。这种方式使得数据能够在多个地理位置和跨多个存储介质进行高效管理。

    例子
    : Amazon S3, Google Cloud Storage和Azure Blob Storage都是典型的对象存储服务。

数据一致性模型

  • 文件存储

    块存储
    通常依赖于更为传统的一致性模型,如ACID事务。

  • 对象存储
    则更多地使用“最终一致性”模型,这允许在多个节点之间进行更高效的数据同步。

    例子
    : 在Amazon S3中,当你上传一个新对象后,该对象并不会立即在所有节点上可见,但最终会达到一致的状态。

扩展性和成本

  • 文件存储

    块存储
    在扩展性方面相对有限,通常需要大量的硬件和人力投入。

  • 对象存储
    则天然具有良好的水平扩展性,可以轻易地添加更多的存储节点,以适应不断增长的数据需求。

    例子
    : 使用对象存储,一家全球分布的公司可以不断地扩展其存储容量,而不需要更改其应用程序代码。

常用的OSS服务厂商

除了开源的对象存储解决方案,如OpenStack Swift和MinIO,还有多家云服务提供商提供了成熟的OSS服务。

  • Amazon S3
    : 作为对象存储服务的先驱,Amazon S3提供了丰富的功能和灵活的定价模型。
  • Azure Blob Storage
    : 微软的Azure Blob Storage也是一个功能丰富的OSS解决方案,特别是对于那些已经在Azure生态系统中的企业。
  • Google Cloud Storage
    : GCS提供了高性能和多种数据一致性选项,特别适用于大数据和机器学习应用。


三、OSS的核心组件和架构

file
理解对象存储服务(OSS)的核心组件和架构设计是掌握其运作机制的关键。本节将深入探讨这些方面。

核心组件

对象(Object)

  • 定义
    : 在OSS中,对象是存储的基本单元。每个对象包含数据、元数据和唯一标识符。

  • 应用场景
    : 比如,在一个医疗影像存储系统中,每张CT或MRI图像都可以作为一个对象存储在OSS中。

桶(Bucket)

  • 定义
    : 桶是用于组织对象的容器。每个桶内可以存储无数个对象,并可以设置独立的访问控制和存储策略。

  • 应用场景
    : 在一个大型电商网站中,你可能会有多个桶,如“用户数据”、“商品图片”和“交易记录”。

名称空间(Namespace)

  • 定义
    : 名称空间用于隔离不同应用或业务单位的存储环境。

  • 应用场景
    : 一个全球性的公司可能会使用不同的名称空间来区分其在不同地区或不同业务线的数据。

架构设计

分布式存储

  • 特点
    : 对象存储通常采用分布式架构,以支持大规模的数据存储和高并发访问。

  • 例子
    : Amazon S3使用分布式系统设计,通过多个数据中心在不同地理位置存储数据副本,以提高数据的可用性和持久性。

数据一致性与冗余

  • 一致性模型
    : 对象存储通常采用“最终一致性”模型,这意味着在数据被多次复制或移动后,所有副本最终会达到一致的状态。

    例子
    : 在Google Cloud Storage中,如果一个对象被多次修改,系统会保证所有读操作最终返回最后一次写入的结果。

  • 冗余策略
    : 为了提高数据的可靠性,对象存储通常会在多个物理位置存储数据的多个副本。

    例子
    : 在Azure Blob Storage中,你可以选择多种冗余选项,如本地冗余存储(LRS)、地理冗余存储(GRS)或读取访问地理冗余存储(RA-GRS)。

数据生命周期管理

  • 特点
    : 多数对象存储服务提供了数据生命周期管理功能,允许用户根据数据访问频率自动迁移数据到不同的存储层级。

  • 例子
    : 在Amazon S3中,可以通过配置生命周期策略,将30天未访问的数据自动转移到S3 Glacier,以减少存储成本。


四、OSS的高级功能

file
对象存储服务(OSS)不仅提供了基础的存储功能,还有多种高级功能以满足各种复杂应用场景的需求。下面将逐一介绍这些高级功能。

数据版本控制

版本管理策略

  • 定义
    : 数据版本控制允许您保存对象的多个版本,以防止不小心删除或覆盖。

  • 应用场景
    : 假设你管理一个在线文档编辑平台,使用版本控制,用户可以轻松恢复到文档的早期版本。

多版本并发控制(MVCC)

  • 特点
    : 多版本并发控制是一种数据一致性策略,用于在多用户并发访问下确保数据的一致性。

  • 例子
    : 在一个多人协作的项目管理工具中,多个用户可能同时修改一个任务的状态,MVCC确保每次只有一个用户的修改会被接受。

数据加密

客户端加密

  • 定义
    : 在上传到OSS之前,数据首先在客户端进行加密。

  • 应用场景
    : 在处理高度敏感信息,如医疗记录或金融数据时,客户端加密提供了额外的安全层。

服务端加密

  • 定义
    : 数据在写入OSS后,由OSS服务自动进行加密。

  • 应用场景
    : 如果你正在构建一个企业级的文件存储解决方案,服务端加密可以作为一个标准的安全措施。

数据分层和归档

自动归档

  • 定义
    : 根据配置的策略,不经常访问的数据会被自动转移到低成本的存储层。

  • 应用场景
    : 在媒体库中,过去季度的视频内容可以被自动归档,以减少存储成本。

热、冷、冷冻数据层

  • 特点
    : 对象存储通常提供多个存储层次,包括热层(经常访问)、冷层(不常访问)和冷冻层(几乎不访问)。

  • 例子
    : Amazon S3提供了S3 Standard(热层)、S3 Intelligent-Tiering、S3 One Zone-IA(冷层)、S3 Glacier和S3 Glacier Deep Archive(冷冻层)。

数据湖和大数据集成

数据湖架构

  • 定义
    : 数据湖是一个集中存储所有格式和来源的原始数据的存储库。

  • 应用场景
    : 大型企业可能会使用OSS作为其数据湖解决方案的一部分,以便集中管理来自不同业务单元的数据。

大数据集成

  • 特点
    : 多数对象存储服务提供了与大数据处理框架(如Hadoop和Spark)的集成选项。

  • 例子
    : 在Azure Blob Storage中,你可以使用Azure HDInsight服务来运行Hadoop、Spark或其他大数据工作负载。


五、作为开发者什么场景使用OSS

file
对象存储服务(OSS)具有高度的灵活性和可扩展性,因此在多种开发场景中都有应用。本节将深入探讨作为开发者,你可能在哪些具体场景中使用OSS。

媒体存储与分发

存储大型媒体文件

  • 定义
    : OSS提供了高效且成本效益的方法来存储大型的媒体文件,如视频、音频和图片。

  • 应用场景
    : 如果你正在开发一个视频流媒体服务,如Netflix或YouTube的类似产品,OSS可以作为后端存储。

CDN集成

  • 特点
    : 通过与内容分发网络(CDN)集成,OSS可以有效地分发媒体内容到全球范围。

  • 例子
    : 假设你有一个全球范围的新闻网站,使用OSS和CDN,可以确保用户无论身处何处都能快速访问到图片和视频内容。

数据备份与归档

灾难恢复

  • 定义
    : OSS允许你存储数据备份,以防数据中心故障或其他灾难情况。

  • 应用场景
    : 在开发一个电子医疗记录系统时,数据备份和恢复是非常关键的功能,OSS可以提供这种需求。

长期归档

  • 特点
    : OSS通常提供低成本的存储选项,适用于长期存储不经常访问的数据。

  • 例子
    : 如果你是一个科研人员,正在进行长期的气候研究,所有的原始数据和模型可以存储在OSS的低成本层。

企业数据仓库与数据湖

实时分析

  • 定义
    : OSS可以与各种大数据分析工具集成,提供准实时的数据处理和分析。

  • 应用场景
    : 在开发一个用于实时监控物联网(IoT)设备数据的应用时,OSS能存储大量的传感器数据并支持实时分析。

数据整合

  • 特点
    : 通过作为一个中心化的数据存储解决方案,OSS可以帮助企业整合来自不同来源的数据。

  • 例子
    : 假设你的公司有多个不同的业务部门,每个部门都有自己的数据存储需求和系统。OSS可以作为一个统一的数据存储和管理平台。

机器学习和人工智能

训练数据存储

  • 定义
    : 机器学习模型的训练需要大量的数据集。OSS提供了一个可扩展的解决方案来存储这些数据。

  • 应用场景
    : 如果你正在开发一个图像识别系统,数以百万计的图片可以存储在OSS中,用于模型训练。

模型版本管理

  • 特点
    : OSS允许你保存不同版本的模型,方便回滚和性能比较。

  • 例子
    : 在进行自然语言处理(NLP)相关的项目时,每次模型迭代都可以保存为一个新版本,以便进行A/B测试和性能跟踪。

多媒体处理

图像和视频转码

  • 定义
    : OSS经常与一些媒体处理服务集成,如图像或视频转码。

  • 应用场景
    : 在一个社交媒体应用中,用户上传的原始图片和视频可以存储在OSS中,并自动转码为适合网页和移动端的格式。

实时音视频处理

  • 特点
    : OSS也可以与实时音视频处理服务结合,提供如实时滤镜、合成等功能。

  • 例子
    : 在一个在线教育平台,讲师上传的课件和实时录屏可以保存在OSS,并进行实时的音视频处理,如添加字幕或水印。

日志和监控

存储应用日志

  • 定义
    : OSS可以用于存储应用日志,以进行后续的分析或监控。

  • 应用场景
    : 如果你正在开发一个复杂的电子商务网站,所有的用户行为和系统日志都可以实时写入OSS。

实时监控与警报

  • 特点
    : 通过与监控工具集成,OSS中的日志数据可以用于生成实时的监控仪表盘或警报。

  • 例子
    : 在一个金融交易平台中,所有的交易记录和安全事件都可以实时监控,以防范诈骗或安全攻击。

关注【TechLeadCloud】,分享互联网架构、云服务技术的全维度知识。作者拥有10+年互联网服务架构、AI产品研发经验、团队管理经验,同济本复旦硕,复旦机器人智能实验室成员,阿里云认证的资深架构师,项目管理专业人士,上亿营收AI产品研发负责人。
如有帮助,请多关注
TeahLead KrisChang,10+年的互联网和人工智能从业经验,10年+技术和业务团队管理经验,同济软件工程本科,复旦工程管理硕士,阿里云认证云服务资深架构师,上亿营收AI产品业务负责人。


引言


大家好,我是你们的老伙计秀才!今天带来的是[深入浅出Java多线程]系列的第九篇内容:synchronized与锁。大家觉得有用请点赞,喜欢请关注!秀才在此谢过大家了!!!

在现代软件开发中,多线程技术是提升系统性能和并发能力的关键手段之一。Java作为主流的编程语言,其内置的多线程机制为开发者提供了丰富的并发控制工具,其中synchronized关键字及其背后的锁机制扮演了至关重要的角色。理解并掌握synchronized的使用原理与特性,有助于我们设计出高效且线程安全的应用程序。

Java中的每个对象都可以充当一把锁,这意味着任何实例方法或静态方法可以通过
synchronized
关键字来实现同步控制,从而确保同一时间只有一个线程能访问临界资源。例如,一个简单的实例方法同步:

public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }
}

在这个例子中,
increment
方法被
synchronized
修饰,使得在同一时刻只能有一个线程对
count
变量进行递增操作,避免了数据竞争带来的不一致性问题。

同时,类锁的概念也是基于对象锁——类的Class对象同样可以作为锁,用于同步类的静态方法或某一特定对象实例上的代码块,如:

public class SharedResource {
    public static synchronized void modifyStaticData() {
        // 修改共享静态数据
    }
}

这里,
modifyStaticData
方法通过类锁保护了所有实例共享的静态资源,保证了在多线程环境下的数据安全性。

深入探究Java多线程中的
synchronized
关键字及锁机制,我们会发现Java虚拟机为了优化锁的性能,引入了偏向锁、轻量级锁和重量级锁等不同级别的锁状态,并且支持锁的自动升级和降级策略。这些机制能够根据实际的并发场景动态调整锁的表现形式,以最小化锁的获取和释放开销,进而提高系统的并发性能和响应速度。接下来,我们将逐一剖析这些概念和技术细节,以便更全面地理解和运用Java中的锁机制。


Java锁基础


在Java多线程编程中,锁机制是实现并发控制的核心手段之一。这里的“锁”基于对象的概念,任何Java对象都可以充当一把锁来保护共享资源的访问,确保同一时间只有一个线程可以执行临界区代码。synchronized关键字作为Java内置的关键同步工具,被广泛用于实现线程间的互斥操作。

synchronized关键字详解

synchronized
关键字主要有三种使用形式:

  1. 实例方法锁定
    :当
    synchronized
    关键字修饰实例方法时,它隐式地获取了当前对象实例作为锁:

    public class SynchronizedExample {
        private int counter;

        public synchronized void increment() {
            counter++;
        }
    }

    在上述代码中,
    increment
    方法被
    synchronized
    修饰,意味着每次仅有一个线程能执行该方法内部逻辑,即修改
    counter
    变量。

  2. 静态方法锁定
    :如果
    synchronized
    修饰的是静态方法,则锁对象为类的Class对象,所有实例共享这把锁:

    public class SynchronizedExample {
        private static int sharedCounter;

        public static synchronized void incrementStatic() {
            sharedCounter++;
        }
    }

    在这个例子中,对
    incrementStatic
    方法的访问将受到类锁的保护,确保在多线程环境下,对
    sharedCounter
    的更新是原子性的。

  3. 代码块锁定
    :通过
    synchronized
    关键字包裹一个代码块,显式指定锁对象:

    public class SynchronizedExample {
        private final Object lock = new Object();

        public void blockLockingMethod() {
            synchronized (lock) {
                // 临界区代码
            }
        }

    在这里,我们创建了一个独立的对象
    lock
    用作锁,只有获得了这把锁的线程才能执行代码块内的内容。

synchronized关键字保证了其修饰的方法或代码块在同一时间只能由单个线程访问,从而避免了因多个线程同时修改数据导致的数据不一致问题,有效地实现了多线程环境下的同步控制。随着JVM对锁性能优化的不断深入,还引入了偏向锁、轻量级锁和重量级锁等不同级别的锁状态,使得Java多线程同步更加灵活高效。


synchronized原理


在Java多线程编程中,synchronized关键字所实现的同步机制深入底层,与JVM内部对象头结构密切相关。每个Java对象都拥有一个对象头(Object Header),它是内存中存放对象元数据的地方,包含了对象的Mark Word区域,这个区域用于存储对象的hashCode、GC分代年龄以及锁状态等信息。

Java对象头与锁状态

对象头结构
:非数组类型的Java对象,其对象头占用2个机器字宽,对于32位系统是32位,64位系统则是64位。Mark Word中的一部分空间被用来记录锁的状态,包括无锁、偏向锁、轻量级锁和重量级锁四种状态。

长度 内容 作用
32/64bit Mark Word 存储对象的hashCode或锁信息等
32/64bit Class Metadata Address 存储到对象类型数据的指针
32/64bit Array length 数组的长度(如果是数组)

这里着重关注一些
Mark Word
的内容:

锁状态 29bit或者61bit 第1bit是否偏向锁 第2bit锁标志位
无锁 0 01
偏向锁 线程ID 1 01
轻量级锁 指向栈中锁记录的指针 此时第1bit不用于标识偏向锁 00
重量级锁 指向互斥量(重量级锁)的指针 此时第1bit不用于标识偏向锁 10

锁状态转换

  • 无锁状态 :没有任何线程持有该对象锁,所有线程都可以尝试修改资源。
  • 偏向锁 :当一个线程首次获得锁时,会将当前线程ID写入对象头的Mark Word中,后续进入同步代码块时只需检查是否为当前线程持有即可快速获取锁。例如,若只有一个线程长期访问某一对象,则可以避免不必要的CAS操作和自旋消耗。
class BiasedLockExample {
    private int count;

    public void increment() {
        synchronized (this) {
            count++;
        }
    }
}

在上述例子中,如果
increment
方法仅由一个线程执行,那么JVM可能会将对象标记为偏向锁,从而提高效率。

  • 轻量级锁 :当存在多个线程竞争同一锁,但实际发生锁竞争的概率较小的情况下,JVM使用轻量级锁来避免频繁的线程阻塞和唤醒开销。轻量级锁通过CAS操作试图将当前线程栈中的锁记录地址替换到对象头的Mark Word中,如失败则表明存在锁竞争,转而升级为自旋或重量级锁。
  • 重量级锁 :当锁竞争激烈时,轻量级锁无法满足需求,就会升级为依赖于操作系统的互斥量(mutex)实现的重量级锁。此时线程将被挂起,直到锁释放后重新调度,降低了CPU的利用率但确保了线程间互斥性。

Java虚拟机通过对象头的Mark Word动态调整锁状态以适应不同场景下的并发控制需求,实现了从偏向锁、轻量级锁到重量级锁的平滑过渡,有效提升了多线程环境下程序的性能表现。通过灵活运用和理解这些锁状态及其背后的原理,开发者能够更好地优化多线程应用中的同步逻辑。


Java锁升级机制


在Java多线程同步中,synchronized关键字实现的锁具有动态升级的能力,从偏向锁到轻量级锁再到重量级锁,根据竞争情况自动调整以优化性能。


偏向锁

偏向锁
是为了解决大多数情况下只有一个线程频繁获得锁的情况。当一个线程首次获取对象锁时,JVM会将其设置为偏向锁,并将该线程ID记录在对象头的Mark Word中。后续该线程再次进入同步代码块时,只需简单地验证Mark Word中的线程ID是否与当前线程一致即可快速获取锁。例如:

public class BiasedLockExample {
    private int sharedResource;

    public void access() {
        synchronized (this) {
            // 仅有一个线程长期访问此方法时,偏向锁生效
            sharedResource++;
        }
    }
}

如果其他线程尝试获取已被偏向的锁,系统会检查偏向锁是否有效并进行撤销操作,通过CAS尝试替换Mark Word的内容。若失败,则表明存在锁竞争,此时偏向锁升级至轻量级锁。
其操作流程如下图:

下图总结了偏向锁的获得和撤销流程:


轻量级锁

轻量级锁
主要应用于多个线程间交替访问同一对象但不存在大量持续竞争的场景。当线程试图获取锁时,它首先会在自己的栈帧中创建一个用于存储锁记录的空间(Displaced Mark Word),然后通过CAS操作尝试将对象头的Mark Word替换为指向锁记录的指针。成功则表示获得锁;否则,线程开始自旋(循环尝试获取锁)。

public class LightweightLockExample {
    private int sharedResource;

    public void access() {
        Object lock = new Object();
        synchronized (lock) {
            // 若多个线程短暂交替访问此方法,轻量级锁生效
            sharedResource++;
        }
    }
}

自旋次数并非固定不变,而是采用了适应性自旋策略,即根据历史成功率动态调整自旋次数。如果经过若干次自旋后仍未能获得锁,则轻量级锁升级为重量级锁。
轻量锁操作流程如下:


重量级锁

重量级锁
依赖于操作系统的互斥量(mutex)来实现线程间的互斥控制。当锁竞争激烈,轻量级锁无法满足需求时,锁状态会转换为重量级锁。这时,请求锁的线程会被挂起并放入等待队列中,直至持有锁的线程释放锁资源。

public class HeavyweightLockExample {
    private static final Object lock = new Object();

    public void concurrentAccess() {
        synchronized (lock) {
            // 若大量并发线程同时访问此方法,可能导致锁升级为重量级锁
            // 线程将被操作系统调度器挂起和唤醒
            performHeavyOperation();
        }
    }

    private void performHeavyOperation() {
        // 执行耗时较长的操作...
    }
}

重量级锁虽然会导致线程阻塞及上下文切换,但它确保了在高度竞争环境下的公平性和线程安全。当调用
wait()

notify()
方法时,即使原本是轻量级或偏向锁,也会先膨胀成重量级锁,以便正确管理线程的阻塞和唤醒状态。

总结来说,Java锁的升级机制是一种根据实际运行状况动态调整同步成本的技术手段,使得在多种并发场景下都能尽可能保持高效率和线程安全性。


锁对比与选择


在Java多线程同步中,有三种主要的锁类型:偏向锁、轻量级锁和重量级锁。每种锁都有其特定的适用场景及性能特性。

偏向锁

  • 优点 :当只有一个线程长期独占对象锁时,偏向锁几乎无额外开销,获取和释放锁的速度接近非同步方法调用。
  • 缺点 :当存在锁竞争或者程序执行过程中锁的所有者发生变化时,需要撤销偏向锁并升级为更高级别的锁,这个过程会产生额外的系统开销。
  • 适用场景 :适用于大部分时间只由一个线程访问同步块的场合。

案例:

public class BiasedLockExample {
    private int sharedResource;

    public void exclusiveAccess() {
        synchronized (this) {
            // 若只有主线程频繁访问此方法,则偏向锁效率高
            sharedResource++;
        }
    }
}

轻量级锁

  • 优点 :相比于重量级锁,轻量级锁通过自旋避免了线程上下文切换带来的开销,在没有其他线程竞争的情况下能快速获得锁,提高了程序响应速度。
  • 缺点 :如果多个线程同时争夺锁,轻量级锁会导致较多的CAS操作以及可能的长时间自旋等待,反而浪费CPU资源。
  • 适用场景 :适用于线程间对锁的竞争不激烈且锁持有时间较短的情况。

案例:

public class LightweightLockExample {
    private final Object lock = new Object();

    public void concurrentAccess() {
        synchronized (lock) {
            // 若并发线程交替短暂持有锁,轻量级锁效果好
            processData();
        }
    }

    private void processData() {
        // 执行一些快速计算或短期持有的共享资源访问...
    }
}

重量级锁

  • 优点 :确保了线程间的互斥性和公平性,不会因自旋消耗过多CPU资源,阻塞未获得锁的线程,保证了系统的稳定性。
  • 缺点 :获取和释放锁涉及操作系统层面的信号量操作,导致较大的上下文切换开销,因此在高并发、锁竞争激烈的场景下性能较低。
  • 适用场景 :适用于高度竞争性的环境,即大量并发线程同时请求同一锁资源的情况。

案例:

public class HeavyweightLockExample {
    private static final Object LOCK = new Object();

    public void criticalSection() {
        synchronized (LOCK) {
            // 在大量并发线程竞争同一锁时,重量级锁能确保公平性和稳定性
            accessSharedResource();
        }
    }

    private void accessSharedResource() {
        // 访问公共资源,如数据库连接、文件写入等耗时较长的操作...
    }
}

综上所述,根据应用中的具体并发模式和锁争用情况,合理选择合适的锁类型至关重要。在实际编程中,JVM会根据实际情况自动进行锁状态的调整和升级,但开发人员也应具备理解这些锁机制的能力,并适时调整JVM参数以优化程序性能。例如,若确定应用程序不存在偏向锁的优势场景,可考虑禁用偏向锁功能。


总结与建议

Java多线程中,synchronized关键字及锁机制的运用涉及到从偏向锁到轻量级锁再到重量级锁的动态升级过程。在设计并发程序时,理解并合理选择锁策略对于提高系统性能至关重要。

偏向锁
旨在优化单一线程访问临界区的场景,通过记录当前持有锁的线程ID来避免无竞争时的额外开销。但当其他线程尝试获取锁时,需撤销偏向锁,并可能升级为轻量级锁。

public class BiasedLockDemo {
    private int count;

    public void increment() {
        synchronized (this) {
            // 偏向锁适用于只有一个线程长期执行此方法的情况
            count++;
        }
    }
}

轻量级锁
利用CAS操作和自旋机制,减少线程阻塞带来的上下文切换成本,在低竞争环境下提升响应速度。然而,若存在持续锁竞争,过多的自旋可能导致CPU空耗,此时会转为重量级锁。

public class LightweightLockDemo {
    private final Object lock = new Object();

    public void process() {
        synchronized (lock) {
            // 在短暂且交替访问同步块的情况下,轻量级锁能提供较好的性能
            doWork();
        }
    }

    private void doWork() {
        // 执行快速计算或读取共享资源的操作...
    }
}

重量级锁
虽然开销较大,但确保了互斥性和公平性,尤其适合于高度竞争的同步场景。它通过操作系统互斥量实现,能够防止长时间占用CPU资源的自旋等待。

public class HeavyweightLockDemo {
    private static final Object LOCK = new Object();

    public void criticalSection() {
        synchronized (LOCK) {
            // 当多个线程频繁争夺同一锁资源时,重量级锁能提供稳定的保护
            accessSharedResource();
        }
    }

    private void accessSharedResource() {
        // 访问需要严格同步控制的公共资源...
    }
}

在实际开发中,JVM默认启用偏向锁和轻量级锁功能,但根据具体应用场景,可以通过调整JVM参数如
-XX:UseBiasedLocking

-XX:+/-UseLightweightLocking
等来控制锁行为。同时,关注代码结构,尽可能减少不必要的锁竞争,优化数据结构,是提高多线程程序效率的关键所在。通过深入理解锁升级机制和每种锁的特点,开发者可以更好地权衡并发处理中的性能和安全性问题。

本文使用
markdown.com.cn
排版