JUC并发编程学习与实践

文章目录

  • 学习资料
  • 创建和运行线程
    • 方法一:直接使用Thread
    • 方法二:使用Runnable配合Thread
    • 方法三:FutureTask配合Thread
  • 线程的常见方法
    • start与run
    • sleep与yield
      • 线程的优先级
    • join方法详解
    • interrupt线程打断
      • interrupt线程打断后,线程不会终止运行
      • 两阶段终止模式
      • isInterrupted与interrupted
      • LockSupport.park()阻塞线程
    • 不推荐的方法
    • 主线程与守护线程
      • 其它线程
      • 守护线程
    • 线程的状态
      • 从操作系统层面分
      • 从Java API层面分
        • 示例代码
  • 共享模型之管程
    • 共享带来的问题
      • Java的体现
      • 临界区 Critical Section
      • 竞态条件 Race Condition
    • synchronized解决方案
      • 应用之互斥
      • synchronized
        • 语法
        • 解决
      • synchronized加在方法上
    • 变量的线程安全分析
      • 成员变量和静态变量是否线程安全?
      • 局部变量是否线程安全?
    • Monitor(锁)
    • 轻量级锁
    • 锁膨胀
    • 自旋优化
    • 偏向锁
      • 偏向状态
      • 撤销-调用对象hashCode
      • 撤销-其他线程使用对象
      • 批量重偏向
      • 批量撤销
    • Wait notify
      • 原理
      • API介绍
      • Wait notify的正确姿势
    • 模式
      • 同步模式之保护性暂停
        • 定义
        • 代码示例
      • 异步模式之生产者/消费者
        • 代码示例
    • Park & Unpark
      • 基本使用
      • 特点
      • 原理
    • 线程状态转换
      • 情况1 NEW --> RUNNABLE
      • 情况2 RUNNABLE <--> WAITING
      • 情况3 RUNNABLE < -- > WAITING
      • 情况4 RUNNABLE < -- > WAITING
      • 情况5 RUNNABLE < -- > TIMED_WAITING
      • 情况6 RUNNABLE < -- > TIMED_WAITING
      • 情况7 RUNNABLE < -- > TIMED_WAITING
      • 情况8 RUNNABLE < -- > TIMED_WAITING
      • 情况9 RUNNALE < -- > BLOCKED
      • 情况10 RUNNABLE < -- > TERMINATED
    • 活跃性
      • 死锁
      • 定位死锁


学习资料

【黑马程序员深入学习Java并发编程,JUC并发编程全套教程-哔哩哔哩】
【阿里巴巴Java开发手册】

创建和运行线程

方法一:直接使用Thread

package com.xz;

public class ThreadTest1 {

    public static void main(String[] args) {
        Thread thread = new Thread(){
            @Override
            public void run() {
                System.out.println("线程执行");
            }
        };
        thread.start();
    }
}

方法二:使用Runnable配合Thread

package com.xz;

public class ThreadTest2 {

    public static void main(String[] args) {
        Runnable runnable = () -> System.out.println("线程执行");
        Thread thread = new Thread(runnable);
        thread.start();
    }
}

方法三:FutureTask配合Thread

package com.xz;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class ThreadTest3 {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<Integer> task = new FutureTask<>(() -> {
            System.out.println("线程执行");
            return 100;
        });
        Thread thread = new Thread(task);
        thread.start();
        System.out.println(task.get());// 阻塞,等待FutureTask结果返回
    }
}

线程的常见方法

start与run

调用start()方法是启动一个新的线程。

调用run()方法相当于在当前线程中,调用run()方法,而不是再新启动一个线程。

start()方法不能被调用两次,否则会报错。

sleep与yield

调用sleep会让当前线程从Running运行状态进入Timed Waiting定时等待状态

其它线程可以使用interrupt方法打断正在睡眠的线程,这时sleep方法会抛出InterruptedException

睡眠结束后的线程未必会立刻得到执行。

Thread.sleep()在哪个线程中被调用,哪个线程就获得休眠。

建议用TimeUnit的sleep代替Thread的sleep来获得更好的可读性。
TimeUnit提供多种时间单位。
在这里插入图片描述
如下代码,效果相同的情况下,TimeUnit的可读性更好。
在这里插入图片描述
TimeUnit的sleep内部,其实调用的也是Thread的sleep方法。
在这里插入图片描述

调用yield会让当前线程从Running运行状态进入Runnable就绪状态,然后调度执行其它优先级的线程。如果这时没有同优先级的线程,那么不能保证让当前线程暂停的效果。

具体的实现依赖于操作系统的任务调度器。

线程的优先级

线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它。

如果cpu比较忙,那么优先级高的线程会获得更多的时间片,但cpu闲时,优先级几乎没作用。

join方法详解

package com.xz;

import java.util.concurrent.TimeUnit;

public class JoinTest {

    private static Integer r = 0;

    public static void main(String[] args) throws InterruptedException {
        System.out.println("开始");
        Thread t1 = new Thread(()->{
            System.out.println("线程开始");
            try {
                TimeUnit.MILLISECONDS.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("线程结束");
            r = 10;
        });
        t1.start();
        System.out.println("结果为:"+r);
        System.out.println("结束");
    }
}

输出结果

开始
线程开始
线程结束
结果为:0
结束

分析

因为主线程和线程t1是并行执行的,t1线程需要1毫秒后才能算出r=10
而主线程一开始就要打印r的结果,所以只能打印r=0

解决办法

用join,加在t1.start()之后即可,因为join方法的定义是等待该线程执行直到终止

package com.xz;

import java.util.concurrent.TimeUnit;

public class JoinTest {

    private static Integer r = 0;

    public static void main(String[] args) throws InterruptedException {
        System.out.println("开始");
        Thread t1 = new Thread(()->{
            System.out.println("线程开始");
            try {
                TimeUnit.MILLISECONDS.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("线程结束");
            r = 10;
        });
        t1.start();
        t1.join();
        System.out.println("结果为:"+r);
        System.out.println("结束");
    }
}

输出结果

开始
线程开始
线程结束
结果为:10
结束

join(最大等待毫秒)如果join加了参数,则意味着最多等待多少毫秒,超过这个时间则停止等待,如果再不超过这个时间的时候,线程结束了,则join立马成功退出,而不会强行等待到最大等待毫秒。

interrupt线程打断

package com.xz;

import java.util.concurrent.TimeUnit;

public class InterruptTest {

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        t1.start();
        t1.interrupt();
        System.out.println("获取打断标记:"+t1.isInterrupted());
        TimeUnit.SECONDS.sleep(1);
        System.out.println("如果sleep、wait、join后,再次获取打断标记则为假:"+t1.isInterrupted());
    }
}

运行结果,强行打断会抛出异常

Exception in thread "Thread-0" java.lang.RuntimeException: java.lang.InterruptedException: sleep interrupted
	at com.xz.InterruptTest.lambda$main$0(InterruptTest.java:12)
	at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: java.lang.InterruptedException: sleep interrupted
	at java.base/java.lang.Thread.sleep(Native Method)
	at java.base/java.lang.Thread.sleep(Thread.java:337)
	at java.base/java.util.concurrent.TimeUnit.sleep(TimeUnit.java:446)
	at com.xz.InterruptTest.lambda$main$0(InterruptTest.java:10)
	... 1 more
获取打断标记:true
如果sleep、wait、join后,再次获取打断标记则为假:false

interrupt线程打断后,线程不会终止运行

interrupt线程打断后,线程不会终止运行,需要开发者自行判断,如下,先通过Thread.currentThread()获取当前线程,然后通过isInterrupted()方法获取打断标记,进行判断程序是否终止运行。

package com.xz;

public class InterruptTest {

    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            while (true) {
               boolean interrupted = Thread.currentThread().isInterrupted();
               if(interrupted){
                   break;
               }
            }
        },"t1");
        t1.start();
        t1.interrupt();
    }
}

两阶段终止模式

在这里插入图片描述

package com.xz;

import java.util.concurrent.TimeUnit;

public class TwoPhaseTerminationTest {

    public static void main(String[] args) {
        try {
            TwoPhaseTermination twoPhaseTermination = new TwoPhaseTermination();
            twoPhaseTermination.start();
            TimeUnit.SECONDS.sleep(5);
            twoPhaseTermination.stop();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

}

class TwoPhaseTermination {
    private Thread monitor;

    // 开启线程
    public void start() {
        monitor = new Thread(() -> {
            System.out.println("进入线程");
            while (true) {
                Thread current = Thread.currentThread();
                if(current.isInterrupted()){
                    System.out.println("料理后事");
                    break;
                }
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    current.interrupt();// 异常并不能影响程序执行,并且会重置线程打断状态,需要再次设置打断状态,否则会不断的执行
                }
                System.out.println("执行监控记录");
            }
        });
        monitor.start();
    }

    // 关闭线程
    public void stop() {
        monitor.interrupt();
    }
}

执行结果

进入线程
执行监控记录
执行监控记录
执行监控记录
执行监控记录
java.lang.InterruptedException: sleep interrupted
	at java.base/java.lang.Thread.sleep(Native Method)
	at java.base/java.lang.Thread.sleep(Thread.java:337)
	at java.base/java.util.concurrent.TimeUnit.sleep(TimeUnit.java:446)
	at com.xz.TwoPhaseTermination.lambda$start$0(TwoPhaseTerminationTest.java:34)
	at java.base/java.lang.Thread.run(Thread.java:833)
执行监控记录
料理后事

isInterrupted与interrupted

isInterrupted判断是否被打断,不会清除打断标记。在这里插入图片描述

interrupted判断当前线程是否被打断,会清除打断标记。在这里插入图片描述

LockSupport.park()阻塞线程

当打断状态为true时,LockSupport.park()失效,只有当打断状态为false时,LockSupport.park()才会终止执行。

package com.xz;

import java.util.concurrent.locks.LockSupport;

public class LockSupportParkTest {

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            System.out.println("打断状态:"+Thread.currentThread().isInterrupted());
            LockSupport.park();
            System.out.println("打断状态:"+Thread.currentThread().interrupted()+",并重置为false");
            LockSupport.park();
            System.out.println("打断状态:"+Thread.currentThread().isInterrupted());
        });
        t1.start();
        t1.interrupt();
    }
}

执行结果

打断状态:true
打断状态:true,并重置为false

不推荐的方法

还有一些不推荐使用的方法,这些方法已经过时,容易破坏同步代码块,造成线程死锁。
stop()停止线程运行
suspend()挂起(暂停)线程运行
resume()恢复线程运行

主线程与守护线程

默认情况下,java程序需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即时守护线程的代码没有执行完,也会强制结束。

其它线程

package com.xz;

import java.util.concurrent.TimeUnit;

public class DaemonTest {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(100); //睡眠100秒
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        t1.start();
        System.out.println("方法结束");
    }
}

执行结束,线程并没有退出,而是继续执行自己的代码。
在这里插入图片描述

守护线程

通过setDaemon(true);设置为守护线程。

package com.xz;

import java.util.concurrent.TimeUnit;

public class DaemonTest {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(100); //睡眠100秒
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        t1.setDaemon(true);// 设置为守护线程
        t1.start();
        System.out.println("方法结束");
    }
}

不管守护线程是否执行结束,其它线程结束时,守护线程直接结束。
在这里插入图片描述

注意
垃圾回收线程就是一种守护线程。
Tomcat中的Acceptor和Poller线程都是守护线程,所以Tomcat接收到shutdown命令后,不会等待它们处理完当前请求。

线程的状态

从操作系统层面分

从操作系统层面来描述的话,总共分为五种状态。
在这里插入图片描述

【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联。

【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行。

【运行状态】指获取了CPU时间片运行中的状态,当CPU 时间片用完,会从【运行状态】 转换至【可运行状态】,会导致线程的上下文切换。

【阻塞状态】如果调用了阻塞API,如 BIO 读写文件,这时该线程实际不会用到CPU,会导致线程上下文切换,进入【阻塞状态】。等 BIO操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们。

【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其他状态。

从Java API层面分

从Java API层面来描述的话,根据Thread.State枚举,分为六种状态。
在这里插入图片描述

NEW 线程刚被创建,但是还没有调用start()方法。

RUNNABLE 当调用了start()方法之后,注意,JavaAPI层面的 RUNNABLE 状态涵盖了操作系统层面的【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在Java 里无法区分,仍然认为是可运行)。

BLOCKED,WAITING,TIMED_WAITING 都是Java API层面对【阻塞状态】的细分,后面会在状态转换一节详述。

TERMINATED 当线程代码运行结束。

示例代码
package com.xz;

import java.util.concurrent.TimeUnit;

public class StateTest {

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            System.out.println("running...");
        },"t1");

        Thread t2 = new Thread(() -> {
            while (true) {

            }
        },"t2");
        t2.start();

        Thread t3 = new Thread(() -> {
            System.out.println("running...");
        },"t3");
        t3.start();

        Thread t4 = new Thread(() -> {
            synchronized (StateTest.class) {
                try {
                    TimeUnit.SECONDS.sleep(10000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"t4");
        t4.start();

        Thread t5 = new Thread(() -> {
            try {
                t2.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        },"t5");
        t5.start();

        Thread t6 = new Thread(() -> {
            synchronized (StateTest.class) {
                try {
                    TimeUnit.SECONDS.sleep(10000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"t6");
        t6.start();

        System.out.println("t1 state:"+t1.getState());
        System.out.println("t2 state:"+t2.getState());
        System.out.println("t3 state:"+t3.getState());
        System.out.println("t4 state:"+t4.getState());
        System.out.println("t5 state:"+t5.getState());
        System.out.println("t6 state:"+t6.getState());
    }
}

运行结果

running...
t1 state:NEW
t2 state:RUNNABLE
t3 state:TERMINATED
t4 state:TIMED_WAITING
t5 state:WAITING
t6 state:BLOCKED

共享模型之管程

共享带来的问题

Java的体现

两个线程对初始值为0的静态变量一个做自增,一个做自减,各做5000次,结果是0吗?

static int counter = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for(int i=0; i<5000; i++){
                counter ++;
            }
        },"t1");

        Thread t2 = new Thread(() -> {
            for(int i=0; i<5000; i++){
                counter --;
            }
        },"t2");

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("counter="+counter);
    }

多次运行结果

counter=-1809
counter=271
counter=-2332
counter=-2849
counter=-1071

临界区 Critical Section

一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

例如,下面代码中的临界区


    static int counter = 0;

    static void increment()
    // 临界区
    {
        counter++;
    }

    static void decrement()
    // 临界区
    {
        counter--;
    }

竞态条件 Race Condition

多个线程在临界区内执行,由于代码的执行序列不同而导致结果也无法预测,称之为发生了竞态条件

synchronized解决方案

应用之互斥

为了避免临界区的竞态条件发生,有多种手段可以达到目的。
1、阻塞式的解决方案:synchronized、Lock
2、非阻塞式的解决方案:原子变量

本次使用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其他线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。

注意
虽然java中互斥和同步都可以采用synchronized关键字来完成,但它们还是有区别的:
1、互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区的代码。
2、同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点。

synchronized

同一时刻只能有一个线程获得锁,假设线程1获得了此锁,那么线程2想获得锁的时候只能等待,线程1执行完毕后,会唤起线程2,此时线程2才能获得此锁,只能一个线程执行完毕后,另一个线程才能开始执行。并且只有synchronized锁中的是同一个对象,才具有互斥性。 为确保锁中的是同一个对象,建议在对象上加final关键词,确保对象不可变。

语法
synchronized(对象)
{
	临界区
}
解决

    static int counter = 0;
    static final Object romm = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for(int i=0; i<5000; i++){
                synchronized (romm) {
                    counter ++;
                }
            }
        },"t1");

        Thread t2 = new Thread(() -> {
            for(int i=0; i<5000; i++){
                synchronized (romm) {
                    counter --;
                }
            }
        },"t2");

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("counter="+counter);
    }

多次运行结果

counter=0
counter=0
counter=0
counter=0
counter=0

在这里插入图片描述
在这里插入图片描述

synchronized加在方法上

class SynchronizedTest {

    public synchronized void test(){
        
    }
}

等价于

class SynchronizedTest {

    public void test(){
        synchronized (this) {
            
        }
    }
}
class SynchronizedTest {

    public synchronized static void test(){
        
    }
}

等价于

class SynchronizedTest {

    public static void test(){
        synchronized (SynchronizedTest.class) {

        }
    }
}

变量的线程安全分析

成员变量和静态变量是否线程安全?

如果它们没有共享,则线程安全。

如果它们被共享了,根据它们的状态是否能够改变,又分两种情况。
1、如果只有读操作,则线程安全。
2、如果有读写操作,则这段代码是临界区,需要考虑线程安全。

局部变量是否线程安全?

局部变量是线程安全的。

但局部变量引用的对象则未必。
1、如果该对象没有逃离方法的作用范围,它是线程安全的。
2、如果该对象逃离方法的作用范围,需要考虑线程安全。

Monitor(锁)

Monitor被翻译为监视器管程

每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量锁)之后,该对象头的Mark Word中就被设置指向Monitor对象的指针。

Monitor结构如下
在这里插入图片描述
1、刚开始 Monitor 中 Owner 为 null。

2、当Thread-2 执行 synchronized(obj)就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一个Owner。

3、在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入EntryList BLOCKED。

4、Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的。

5、图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲 wait-notify 时会分析。

注意:
1、synchronized必须是进入同一个对象的monitor才有上述效果。
2、不加synchronized的对象不会关联监视器,不遵从以上规则。

轻量级锁

轻量级锁的使用场景:如果一个对象虽然有多线程访问,但多线程访问的时间可能是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。

轻量级锁对使用者是透明的,即语法仍然是使用synchronized。

假设有两个方法


    static final Object obj = new Object();

    public static void method1(){
        synchronized (obj) {
            // 同步块 A
            method2();
        }
    }

    public static void method2(){
        synchronized (obj) {
            // 同步块 B
        }
    }

创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word。
在这里插入图片描述

让锁记录中Object reference指向锁对象,并尝试用cas替换Object的Mark Word,将Mark Word的值存入锁记录。
在这里插入图片描述

如果cas替换成功,对象头中存储了锁记录地址和状态00,表示由该线程给对象加锁,这时图示如下。
在这里插入图片描述

如果cas失败,有两种情况
1、如果其它线程已经持有了该Object的轻量级锁,这时表明有竞争,进入锁膨胀过程。
2、如果自己执行了synchronized锁重入,那么再添加一条Lock Record作为重入的计数。
在这里插入图片描述

当退出synchronized代码块(解锁时)如果有取值为null的锁记录,表示有重入,这时重置锁记录,表示重入计数减一。
在这里插入图片描述
当退出synchronized代码块(解锁时)锁记录的值不为null,这时使用cas将Mark Word的值恢复给对象头。
成功,则解锁成功。
失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程。

锁膨胀

如果再尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

	static final Object obj = new Object();

    public static void method1(){
        synchronized (obj) {
            // 同步块
        }
    }

当Thread-1进行轻量级加锁时,Thread-0已经对该对象加了轻量级锁。
在这里插入图片描述
这时Thread-1加轻量级锁失败,进入锁膨胀流程。
1、即Object对象申请Monitor锁,让Object指向重量级锁地址。
2、然后自己进入Monitor的EntryList BLOCKED。
在这里插入图片描述
当Thread-0退出同步块解锁时,使用cas将Mark Word的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照Monitor地址找到Monitor对象,设置Owner为null,唤醒EntryList中的BLOCKED线程。

自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。

自旋重试成功的情况
在这里插入图片描述

自旋重试失败的情况
在这里插入图片描述
在Java 6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次,反之,就少自旋甚至不自旋,总之,比较智能。

自旋占用CPU时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势。

Java 7之后不能控制是否开启自旋功能。

偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍需要执行CAS操作。

Java 6中引入了偏向锁来做进一步优化:只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有。

例如:

    static final Object obj = new Object();

    public static void m1(){
        synchronized (obj) {
            // 同步块 A
            m2();
        }
    }

    public static void m2(){
        synchronized (obj) {
            // 同步块 B
            m3();
        }
    }

    public static void m3(){
        synchronized (obj) {
            // 同步块 C
        }
    }

在这里插入图片描述

在这里插入图片描述

偏向状态

在这里插入图片描述
一个对象创建时:

如果开启了偏向锁(默认开启),那么对象创建后,markword值为0x05即最后3位为101,这时它的thread、epoch、age都为0。

偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加VM参数 -XX:BiasedLockingStartupDelay=0来禁用延迟。

如果没有开启偏向锁,那么对象创建后,markword值为0x01即最后3位为001,这时它的hashcode、age都为0,第一次用到hashcode时才会赋值。

添加VM参数-XX:-UseBiasedLocking禁用偏向锁。

撤销-调用对象hashCode

调用了对象的hashCode,但偏向锁的对象MarkWord中存储的线程id,如果调用hashCode会导致偏向锁被撤销。
1、轻量级锁在锁记录中记录hashCode。
2、重量级锁会在Monitor中记录hashCode。

在调用hashCode后使用偏向锁,记得去掉-XX:-UseBiasedLocking

撤销-其他线程使用对象

当有其他线程使用偏向锁对象时,会将偏向锁升级为轻量级锁。

批量重偏向

如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍有机会重新偏向T2,重偏向会重置对象的Thread ID。

当撤销偏向锁阈值超过20次后,jvm会这样觉得,我是不是偏向错了呢?于是会给这些对象加锁时重新偏向至加锁线程。

批量撤销

当撤销偏向锁阈值超过40次后,jvm会觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的。

Wait notify

原理

在这里插入图片描述
Owner 线程发现条件不满足,调用wait方法,即可进入WaitSet变为WAITING状态。

BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU时间片。

BLOCKED线程会在Owner线程释放锁时唤醒。

WAITING线程会在Owner线程调用notify或notifyAll时唤醒,但唤醒后并不意味着立即获得锁,仍需进入EntryList重新竞争。

API介绍

obj.wait()让进入object监视器的线程到waitSet等待。

obj.notify()在object上正在waitSet等待的线程中挑一个唤醒。

obj.notifyAll()让object上正在waitSet等待的线程全部唤醒。

它们都是线程之间进行协作的手段,都属于Object对象的方法。必须获得此对象的锁,才能调用这几个方法。

package com.xz;

import java.util.concurrent.TimeUnit;

public class TestWaitNotify {

    final static Object obj = new Object();

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            synchronized (obj) {
                System.out.println("执行t1线程");
                try {
                    obj.wait();// 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t1其他代码");
            }
        },"t1").start();

        new Thread(() -> {
            synchronized (obj) {
                System.out.println("执行t2线程");
                try {
                    obj.wait();// 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t2其他代码");
            }
        },"t2").start();

        TimeUnit.SECONDS.sleep(2);
        System.out.println("唤起obj上其它线程");
        synchronized (obj) {
            obj.notify();// 唤醒obj上一个线程
            // obj.notifyAll();// 唤醒obj上所有等待线程
        }
    }
}

obj.notify();运行结果

执行t1线程
执行t2线程
唤起obj上其它线程
t1其他代码

obj.notifyAll();运行结果

执行t1线程
执行t2线程
唤起obj上其它线程
t1其他代码
t2其他代码

wait()方法会释放对象的锁,进入WaitSet等待区,从而让其他线程就机会获得对象的锁。无限制等待,直到notify()为止。

wait(long n)有时限的等待,到n毫秒后结束等待,或是被notify。

Wait notify的正确姿势

Sleep(long n)和wait(long n)的区别
1、sleep是Thread方法,而wait是Object的方法。
2、sleep不需要强制和synchronized配合使用,但wait需要和synchronized一起使用。
3、sleep在睡眠的同时,不会释放对象锁的,但wait在等待的时候会释放对象锁。
4、他们的状态都是TIME_WAITING。

synchronized (lock) {
   while (条件不成立) {
        lock.wait();
    }
    // 干活
}

// 另一个线程
synchronized (lock) {
    lock.notifyAll();
}

模式

同步模式之保护性暂停

定义

即Guarded Suspension,用在一个线程等待另一个线程的执行结果。

要点:
1、有一个结果需要从一个线程传递到另一个线程,让他们关联同一个GuardedObject。
2、如果结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者)。
3、JDK中,join的实现、Future的实现,采用的就是此模式。
4、因为要等待另一方的结果,因此归类到同步模式。

在这里插入图片描述

代码示例
package com.xz;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

public class GuardedObjectTest {

    public static void main(String[] args) {
        GuardedObject guardedObject = new GuardedObject();
        new Thread(() -> {
            System.out.println("等待结果");
            List<String> list = (List<String>) guardedObject.get(10000);
            if(list!=null){
                System.out.println("结果大小:"+list.size());
            }else{
                System.out.println("超时结束");
            }
        },"t1").start();

        new Thread(() -> {
            try {
                System.out.println("开始下载");
                guardedObject.complete(download());
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        },"t2").start();
    }

    // 模拟下载
    public static List<String> download() throws IOException {
        HttpURLConnection conn = (HttpURLConnection) new URL("https://www.baidu.com/").openConnection();
        List<String> lines = new ArrayList<>();
        BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8));
        String line;
        while ((line = reader.readLine()) != null) {
            lines.add(line);
        }
        return lines;
    }
}

class GuardedObject {
    // 结果
    private Object response;

    /**
     * 获取结果
     * @param timeout 最大等待时间:毫秒
     * @return
     */
    public Object get(long timeout){
        synchronized (this) {
            // 开始时间
            long begin = System.currentTimeMillis();
            // 经历时间
            long passedTime = 0;
            while (response == null) {
                long waitTime = timeout - passedTime;// 线程最大等待时间,防止虚假唤醒,不能每次都等待相同的时间
                // 经历时间超过了最大等待时间,退出循环
                if(passedTime >= timeout){
                    break;
                }
                try {
                    this.wait(waitTime);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                // 求得经历时间
                passedTime = System.currentTimeMillis() - begin;
            }
            return response;
        }
    }

    /**
     * 赋值
     * @param response
     */
    public void complete(Object response){
        synchronized (this) {
            // 给结果成员变量赋值
            this.response = response;
            this.notifyAll();
        }
    }
}

执行结果

等待结果
开始下载
结果大小:3

异步模式之生产者/消费者

1、与前面的保护性暂停中的GuardObject不同,不需要产生结果和消费结果的线程 一 一 对应。
2、消费队列可以用来平衡生产和消费的线程资源。
3、生产者仅负责结果数据,不关心数据该如何处理,而消费者专心处理结果数据。
4、消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据。
5、JDK中各种阻塞队列,采用的就是这种模式。
在这里插入图片描述

代码示例
package com.xz;

import java.util.LinkedList;

public class MessageTestMQ {

    public static void main(String[] args) {
        MessageQueue messageQueue = new MessageQueue(2);
        for(int i=0; i<3; i++){
            int id = i;
            new Thread(() -> {
                messageQueue.put(new Message(id,"值"+id));
            },"生产者").start();
        }

        new Thread(() -> {
            while (true) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                messageQueue.take();
            }
        },"消费者").start();
    }
}

// 消息队列类,java线程之间通信
class MessageQueue {

    // 消息的队列集合
    private LinkedList<Message> list = new LinkedList<>();

    // 队列容量
    private int capcity;

    public MessageQueue(int capcity) {
        this.capcity = capcity;
    }

    // 获取消息
    public Message take(){
        // 检查队列是否为空
        synchronized (list) {
            while (list.isEmpty()) {
                try {
                    System.out.println("队列为空,消费者线程等待");
                    list.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            // 从队列的头部获取消息并返回
            Message message = list.removeFirst();
            System.out.println("已消费消息:"+message.toString());
            list.notifyAll();
            return message;
        }
    }

    // 存入消息
    public void put(Message message){
        synchronized (list) {
            // 检查队列是否已满
            while (list.size() == capcity) {
                try {
                    System.out.println("队列已满,生产者线程等待");
                    list.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            // 将消息加入队列尾部
            list.addLast(message);
            System.out.println("已生产消息:"+message.toString());
            list.notifyAll();
        }
    }
}

// 声明final,不能有子类,不会存在被子类覆盖父类的方法,线程安全更加稳固
final class Message {
    private int id;
    private Object value;

    // 仅创建构造器,不创建set方法,仅创建的时候设置值,整个类不可变,线程安全
    public Message(int id, Object value) {
        this.id = id;
        this.value = value;
    }

    public int getId() {
        return id;
    }

    public Object getValue() {
        return value;
    }

    @Override
    public String toString() {
        return "Message{" +
                "id=" + id +
                ", value=" + value +
                '}';
    }
}

执行结果

已生产消息:Message{id=1, value=1}
已生产消息:Message{id=0, value=0}
队列已满,生产者线程等待
已消费消息:Message{id=1, value=1}
已生产消息:Message{id=2, value=2}
已消费消息:Message{id=0, value=0}
已消费消息:Message{id=2, value=2}
队列为空,消费者线程等待

Park & Unpark

基本使用

它们是LockSupport类中的方法

// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象);

先park再unpark,如果出现先unpark再park就会导致失效,从而径直向下执行。

package com.xz;

import java.util.concurrent.locks.LockSupport;

public class TestParkUnPark {

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            System.out.println("start...");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("park...");
            LockSupport.park();
            System.out.println("resume...");
        });
        t1.start();

        Thread.sleep(1000);
        System.out.println("unpark...");
        LockSupport.unpark(t1);
    }
}

执行结果

start...
unpark...
park...
resume...

特点

与Object的wait & notify相比

wait,notify和notifyAll必须配合Object Monitor一起使用,而unpark不必。

park & unpark是以线程为单位来【阻塞】和【唤醒】线程,而notify只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么【精确】。

park & unpark可以先unpark,而wait & notify不能先notify。

原理

每个线程都有自己的一个Parker对象,由三部分组成_counter,_cond和_mutex。
在这里插入图片描述
1、当前线程调用Unsafe.park()方法。

2、检查_counter,本情况为0,这时,获得_mutex互斥锁。

3、线程进入_cond条件变量阻塞。

4、设置_counter = 0。
在这里插入图片描述
1、调用Unsafe.unpark(Thread_0)方法,设置_counter为1。

2、唤醒_cond条件变量中的Thread_0.

3、Thread_0恢复运行。

4、设置_counter为0。

线程状态转换

在这里插入图片描述

情况1 NEW --> RUNNABLE

当调用t.start()方法时,由NEW --> RUNNABLE

情况2 RUNNABLE <–> WAITING

t线程用synchronized(obj)获取了对象锁后

调用obj.wait()方法时,t线程从RUNNABLE --> WAITING

调用obj.notify(),obj.notifyAll(),t.interrupt()时:
1、竞争锁成功,t线程从WAITING --> RUNNABLE。
2、竞争锁失败,t线程从WAITING --> BLOCKED

情况3 RUNNABLE < – > WAITING

当前线程调用t.join()方法时,当前线程从RUNNABLE – > WAITING,注意是当前线程t线程对象的监视器上等待。

t线程运行结束,或调用了当前线程的interrupt()时,当前线程从WAITING – > RUNNABLE。

情况4 RUNNABLE < – > WAITING

当前线程调用LockSupport.park()方法会让当前线程从RUNNABLE --> WAITING。

调用LockSupport.unpark(目标线程)或调用线程的interrupt()会让目标线程从WAITING – > RUNNABLE。

情况5 RUNNABLE < – > TIMED_WAITING

t线程用synchronized(obj)获取了对象锁后

调用obj.wait(long n)方法时,t线程从RUNNABLE – > TIMED_WAITING
t线程等待时间超过了n毫秒,或调用obj.notify(),obj.notifyAll(),t.interrupt()时:
1、竞争锁成功,t线程从TIMED_WAITING – > RUNNABLE。
2、竞争锁失败,t线程从TIMED_WAITING – > BLOCKED。

情况6 RUNNABLE < – > TIMED_WAITING

当前线程调用t.join(long n)方法时,当前线程从RUNNABLE – TIMED_WAITING,注意当前线程t线程对象的监视器上等待。

当前线程等待超过了n毫秒,或t线程运行结束,或调用了当前线程的interrupt时,当前线程从TIMED_WATING – > RUNNABLE。

情况7 RUNNABLE < – > TIMED_WAITING

当前线程调用Thread.sleep(long n),当前线程从RUNNABLE - > TIMED_WAITING。

当前线程等待时间超过了n毫秒,当前线程从TIMED_WAITING – > RUNNABLE。

情况8 RUNNABLE < – > TIMED_WAITING

当前线程调用LockSupport.parkNanos(long nanos)或LockSupport.parkUntil(long millis)时,当前线程从RUNNABLE – > TIMED_WAITING。

调用LockSupport.unpark(目标线程)或调用了线程的interrupt(),或是等待超时,会让目标线程从TIMED_WAITING --> RUNNABLE。

情况9 RUNNALE < – > BLOCKED

t线程用synchronized(obj)获取了对象锁时如果竞争失败,从RUNNABLE – > BLOCKED。

持obj锁线程的同步代码块执行完毕,会唤醒该对象上所有BLOCKED的线程重新竞争,如果其中**
t线程**竞争成功,从BLOCKED – > RUNNABLE,其他失败的线程仍然BLOCKED。

情况10 RUNNABLE < – > TERMINATED

当前线程所有代码运行完毕,进入TERMINATED。

活跃性

死锁

有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁。

t1线程获得A对象锁,接下来想获取B对象的锁
t2线程获得B对象锁,接下来想获取A对象的锁

例如:

package com.xz;

public class TestDeadLock {

    public static void main(String[] args) {
        test1();
    }

    private static void test1(){
        Object A = new Object();
        Object B = new Object();
        Thread t1 = new Thread(()->{
            synchronized (A) {
                System.out.println("lock A");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (B) {
                    System.out.println("lock B");
                    System.out.println("操作...");
                }
            }
        },"t1");

        Thread t2 = new Thread(()->{
            synchronized (B) {
                System.out.println("lock B");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (A) {
                    System.out.println("lock A");
                    System.out.println("操作...");
                }
            }
        },"t2");
        t1.start();
        t2.start();
    }
}

定位死锁

检测死锁可以使用jconsole工具

启动TestDeadLock死锁进程,用jconsole追踪TestDeadLock进程
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

或者使用jps定位进程id,再用jstack定位死锁
在这里插入图片描述
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/401862.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

4.7 Verilog 循环语句

关键词&#xff1a;while, for, repeat, forever Verilog 循环语句有 4 种类型&#xff0c;分别是 while&#xff0c;for&#xff0c;repeat&#xff0c;和 forever 循环。循环语句只能在 always 或 initial 块中使用&#xff0c;但可以包含延迟表达式。 while 循环 while 循…

科普|什么是数据脱敏

在当今数字化的时代&#xff0c;数据已经成为企业的重要资产和核心竞争力。然而&#xff0c;随着数据量的不断增加&#xff0c;数据安全和隐私保护问题也日益突出。 什么是数据脱敏呢&#xff1f; 数据脱敏&#xff0c;也称为数据去隐私化或数据匿名化&#xff0c;是一种将敏感…

electron学习和新建窗口

首先我们要先下载electron npm install --save-dev electron 建立入口文件main.js 新建一个入口文件 main.js&#xff0c;然后导入eletron新建一个窗口。 const { app, BrowserWindow, ipcMain } require("electron"); const path require("path");func…

JavaWeb——002JS Vue快速入门

目录 一、JS快速入门​编辑 1、什么是JavaScript?​编辑 2、JS引入方式​编辑 2.1、示例代码 3、JS基础语法 3.1、书写语法 3.2、变量​编辑 3.3、数据类型 3.4、运算符​编辑 3.5、流程控制语句​编辑 4、JS函数 4.1、第一种函数定义方式 function funcName(参数…

C#知识点-15(匿名函数、使用委托进行窗体传值、反射)

匿名函数 概念&#xff1a;没有名字的函数&#xff0c;一般情况下只调用一次。它的本质就是一个方法&#xff0c;虽然我们没有定义这个方法&#xff0c;但是编译器会把匿名函数编译成一个方法 public delegate void Del1();//无参数无返回值的委托public delegate void Del2(s…

Linux 安装RocketMQ

官网&#xff1a; https://rocketmq.apache.org/zh/安装RocketMQ 5.2.0 wget https://dist.apache.org/repos/dist/release/rocketmq/5.2.0/rocketmq-all-5.2.0-bin-release.zip unzip rocketmq-all-5.2.0-bin-release.zip#启动之前修改jvm启动内存 cd bin #修改&#xff1a;…

车辆管理系统设计与实践

车辆管理系统是针对车辆信息、行驶记录、维护保养等进行全面管理的系统。本文将介绍车辆管理系统的设计原则、技术架构以及实践经验&#xff0c;帮助读者了解如何构建一个高效、稳定的车辆管理系统。 1. 系统设计原则 在设计车辆管理系统时&#xff0c;需要遵循以下设计原则&…

顺序表经典算法及其相关思考

27. 移除元素 - 力扣&#xff08;LeetCode&#xff09; 思路一 利用顺序表中的SLDestroy函数的思想&#xff0c;遇到等于val值的就挪动 思路二 双指针法&#xff1a;不停的将和val不相等的数字往前放。此时的des更像一个空数组&#xff0c;里面存放的都是和val不相等、能够存…

【Rust敲门砖】 Windows环境下配置及安装环境

一、安装C环境 rust底层是依赖C环境的连接器&#xff0c;所以需要先安装C/C编译环境, 有两种选择:安装微软的msvc或者安装mingw/cygwin。 如果使用msvc的Visual Studio&#xff0c;只需要安装好C/C编译环境,然后一路默认就行了&#xff0c;缺点是体积比较大&#xff0c;下载安…

YOLO v9 思路复现 + 全流程优化

YOLO v9 思路复现 全流程优化 提出背景&#xff1a;深层网络的 信息丢失、梯度流偏差YOLO v9 设计逻辑可编程梯度信息&#xff08;PGI&#xff09;&#xff1a;使用PGI改善训练过程广义高效层聚合网络&#xff08;GELAN&#xff09;&#xff1a;使用GELAN改进架构 对比其他解法…

day16_map课后练习 - 参考答案

文章目录 day16_课后练习第1题第2题第3题第4题第5题第6题 day16_课后练习 第1题 开发提示&#xff1a;可以使用Map&#xff0c;key是字母&#xff0c;value是该字母的次数 效果演示&#xff1a;例如&#xff1a;String str “Your future depends on your dreams, so go to …

KafKa3.x基础

来源&#xff1a;B站 目录 定义消息队列传统消息队列的应用场景消息队列的两种模式 Kafka 基础架构Kafka 命令行操作主题命令行操作生产者命令行操作消费者命令行操作 Kafka 生产者生产者消息发送流程发送原理生产者重要参数列表 异步发送 API普通异步发送带回调函数的异步发送…

【springBoot】springAOP

AOP的概述 AOP是面向切面编程。切面就是指某一类特定的问题&#xff0c;所以AOP也可以理解为面向特定方法编程。AOP是一种思想&#xff0c;拦截器&#xff0c;统一数据返回和统一异常处理是AOP思想的一种实现。简单来说&#xff1a;AOP是一种思想&#xff0c;对某一类事务的集…

(提供数据集下载)基于大语言模型LangChain与ChatGLM3-6B本地知识库调优:数据集优化、参数调整、Prompt提示词优化实战

文章目录 &#xff08;提供数据集下载&#xff09;基于大语言模型LangChain与ChatGLM3-6B本地知识库调优&#xff1a;数据集优化、参数调整、提示词Prompt优化本地知识库目标操作步骤问答测试的预设问题原始数据情况数据集优化&#xff1a;预处理&#xff0c;先后准备了三份数据…

C#使用一个泛型方法操作不同数据类型的数组

目录 一、泛型方法及其存在的意义 二 、实例 1.源码 2.生成效果 再发一个泛型方法的示例。 一、泛型方法及其存在的意义 实际应用中&#xff0c;查找或遍历数组中的值时&#xff0c;有时因为数组类型的不同&#xff0c;需要对不同的数组进行操作&#xff0c;那么,可以使用…

Java学习-21 网络编程

什么是网络编程&#xff1f; 可以让设备中的程序与网络上其他设备中的程序进行数据交互&#xff08;实现网络通信的&#xff09; 基本的通信架构 基本的通信架构有2种形式: CS架构(Client客户端/Server服务端) BS架构(Browser浏览器/Server服务端)。 网络通信三要素 IP …

ATCoder Beginnner Contest 341 A~G

A.Print 341&#xff08;模拟&#xff09; 题意&#xff1a; 给定一个正整数 N N N&#xff0c;输出由 N N N个0和 ( N 1 ) (N1) (N1)个1交替组成的字符串。 分析&#xff1a; 按题意模拟即可 代码&#xff1a; #include<bits/stdc.h>using namespace std;int mai…

TestNG与ExtentReport单元测试导出报告文档

TestNG与ExtentReport集成 目录 1 通过实现ITestListener的方法添加Reporter log 1.1 MyTestListener设置 1.2 输出结果 2 TestNG与ExtentReporter集成 2.1 项目结构 2.2 MyExtentReportListener设置 2.3 单多Suite、Test组合测试 2.3.1 单Suite单Test 2.3…

十七、多线程

一、目标 理解线程的概念掌握线程的创建和启动了解线程的状态掌握线程调度的常用方法掌握线程的同步理解线程安全的类型 二、进程、线程、多线程的理解 进程&#xff1a;应用程序的执行实例、有独立的内存空间和系统资源 线程&#xff1a;CPU调度和分派的基本单位、进程中执行运…

2023数据要素市场十大关键词

2023数据要素市场十大关键词 导读 2023年即将过去。一年之前&#xff0c;《中共中央国务院关于构建数据基础制度更好发挥数据要素作用的意见》&#xff08;简称“数据二十条”&#xff09;正式对外发布&#xff0c;为数据要素市场的建设举旗定向。 图片 2023年是“数据二十条…