synchronized 关键字

目录

  • 背景
  • 过程
    • 历史
    • 概念
    • 实际应用
      • 方法1:放方法名前形成同步方法;
      • 方法2:使用同步块修改上面的例子;
    • 应用方法
      • 锁住对象:
      • 锁住类:
  • 总结

背景

学习并发,为解决并发带来的问题,引入synchronized 关键字。

过程

历史

当谈到synchronized同步锁的历史时,我们需要回顾Java语言的发展和多线程编程的背景。synchronized关键字是Java中用于实现线程安全的最早和最常用的机制之一。以下是synchronized同步锁的历史和演变:

早期Java版本(Java 1.0至Java 1.4):
Java的早期版本并没有提供很多高级的并发处理工具。在这些版本中,synchronized是实现多线程同步的主要手段。当一个方法或代码块被synchronized修饰时,它就变成了一个临界区(Critical Section),只允许一个线程在同一时间访问该方法或代码块。其他线程需要等待锁释放后才能进入这个临界区。

然而,早期的synchronized存在一些问题。首先,它的使用相对复杂,需要手动管理锁的获取和释放,容易出现死锁和竞态条件。其次,如果一个方法被声明为synchronized,即使没有并发问题,也会降低程序的性能,因为其他线程必须等待锁的释放。

Java 5引入的改进(Java 5,发布于2004年):
随着Java 5的发布,引入了java.util.concurrent包,其中包含更高级的并发工具,如ReentrantLock和Semaphore等。这些工具相对于synchronized提供了更多的灵活性和功能,允许开发人员更精细地控制多线程代码。

ReentrantLock是一个可重入锁,可以替代synchronized关键字,具有更丰富的特性。它支持公平性(Fairness),可中断(Interruptibility),以及尝试非阻塞地获取锁等特性。ReentrantLock的灵活性比synchronized更高,但使用起来也更加复杂。

Java 6至Java 7:
在Java 6和Java 7中,并发工具得到了一些改进和优化,但在同步锁机制方面没有太多变化。synchronized仍然是大多数Java程序员处理多线程问题时的首选方式。

Java 8的新特性(Java 8,发布于2014年):
随着Java 8的发布,引入了java.util.concurrent包中的新功能,如StampedLock和CompletableFuture等,这些功能进一步改进了并发编程的体验。尽管如此,synchronized仍然在许多场景中被广泛使用,因为它在简单情况下非常方便和易于使用。

Java 9及以后的版本:
在Java 9及以后的版本中,synchronized并没有被弃用或取代。它仍然是Java中最简单和直观的同步机制。然而,对于更复杂的并发问题,java.util.concurrent包中的高级工具仍然是更好的选择。

总结:
synchronized同步锁是Java早期用于实现线程安全的主要手段。虽然它相对简单,但在复杂的并发场景下可能不够灵活。随着Java版本的演进,java.util.concurrent包中引入了更高级的并发工具,使得处理多线程问题更加方便和安全。然而,对于简单的多线程情况,synchronized仍然是一个有效的选择。

概念

synchronized是Java中的一个关键字,用于实现多线程之间的同步。它主要用于保证多个线程在访问共享资源时的安全性,避免出现竞态条件(Race Condition)和数据不一致等并发问题。

当一个方法或代码块被synchronized修饰时,它就成为一个临界区(Critical Section)。在同一时间,只允许一个线程进入这个临界区执行代码,其他线程需要等待该临界区的释放。这样做的目的是确保在任意时刻,只有一个线程在执行被保护的代码,从而避免多个线程同时访问共享资源而导致的数据冲突和不一致。

synchronized关键字可以用于两种方式:

修饰实例方法:当synchronized修饰实例方法时,锁定的是当前实例对象(this)。只有获得了该实例对象的锁的线程才能执行该方法,其他线程必须等待。

Copy code
public synchronized void someMethod() {
    // 临界区代码
}

修饰代码块:当synchronized修饰一个代码块时,需要指定一个对象作为锁。只有获得了该对象的锁的线程才能进入代码块执行,其他线程需要等待该对象锁的释放。

Copy code
public void someMethod() {
    synchronized (lockObject) {
        // 临界区代码
    }
}

需要注意的是,synchronized虽然是最简单的同步机制,但它也有一些缺点。因为它是悲观锁(Pessimistic Lock),在竞争激烈的情况下,会导致其他线程长时间等待锁的释放,从而影响程序的性能。为了解决这个问题,Java引入了更高级的并发工具,如ReentrantLock等,以提供更多的灵活性和性能优化。然而,对于简单的多线程同步问题,synchronized仍然是一个有效的选择。

实际应用

java中cpu分给每个线程的时间片是随机的并且在java中好多都是多个线程共用一个资源,比如火车卖票,火车票是一定的,但卖火车票的窗口到处都有,每个窗口就相当于一个线程,这么多的线程共用所有的火车票这个资源。如果在一个时间点上,两个线程同时使用这个资源,那他们取出的火车票是一样的(座位号一样),这样就会给乘客造成麻烦。比如下面程序:

package com.pakage.ThreadAndRunnable;
 
public class Runnable_demo implements Runnable{
	private int ticket=10;
	public Runnable_demo(){		
	}
	@Override
	public void run() {
		for(int i=0;i<20;i++){
				if(this.ticket>0){
					//休眠1s秒中,为了使效果更明显,否则可能出不了效果
					try {
						Thread.sleep(1000);
					} catch (Exception e) {
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName()+"号窗口卖出:"+this.ticket--+"号票");
				}
			
		}
	}
	
	 public static void main(String args[]){
		 Runnable_demo demo=new Runnable_demo();
		 //基于火车票创建三个窗口
		 new Thread(demo,"a").start();
		 new Thread(demo,"b").start();
		 new Thread(demo,"c").start();
	 }
	
}

运行结果
在这里插入图片描述

我们可以看到c号窗口和和b号窗口都卖出了10号票,并且a号和b号窗口分别卖出了0号和-1号票。造成这种情况的原因是1、c线程和b线程在ticket=10的时候,c线程取出10号票以后,ticket还没来的及减1,b线程就取出了ticket此时ticket还等于10;2、在ticket=1时,c线程取出了1号票,ticket还没来的及减1,a、b线程就先后进入了if判断语句,这时ticket减1了,那么当a、b线程取票的时候就取到了0号和-1号票。

出现了上述情况怎样改变呢,我们可以这样做:当一个线程要使用火车票这个资源时,我们就交给它一把锁,等它把事情做完后在把锁给另一个要用这个资源的线程。这样就不会出现上述情况。 实现这个锁的功能就需要用到synchronized这个关键字。

方法1:放方法名前形成同步方法;

package synchronizedLock;

public class Runnable_demo1 implements Runnable{
    private int ticket=10;
    public Runnable_demo1(){
    }
    @Override
    public void run() {
        for(int i=0;i<20;i++){
            if(this.ticket>0){
                //休眠1s秒中,为了使效果更明显,否则可能出不了效果
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                this.sale();
            }

        }
    }

    public synchronized void sale(){
        if(this.ticket>0){
            System.out.println(Thread.currentThread().getName()+"号窗口卖出:"+this.ticket--+"号票");
        }
    }

    public static void main(String args[]){
        Runnable_demo1 demo=new Runnable_demo1();
        //基于火车票创建三个窗口
        new Thread(demo,"a").start();
        new Thread(demo,"b").start();
        new Thread(demo,"c").start();
    }

}

在这里插入图片描述

方法2:使用同步块修改上面的例子;

package synchronizedLock;

public class Runnable_demo2 implements Runnable{
    private int ticket=10;
    public Runnable_demo2(){
    }
    @Override
    public void run() {
        for(int i=0;i<20;i++){
            if(this.ticket>0){
                //休眠1s秒中,为了使效果更明显,否则可能出不了效果
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                this.sale();
            }

        }
    }

    public synchronized void sale(){
        if(this.ticket>0){
            System.out.println(Thread.currentThread().getName()+"号窗口卖出:"+this.ticket--+"号票");
        }
    }

    public static void main(String args[]){
        Runnable_demo2 demo=new Runnable_demo2();
        //基于火车票创建三个窗口
        new Thread(demo,"a").start();
        new Thread(demo,"b").start();
        new Thread(demo,"c").start();
    }

}

在这里插入图片描述

应用方法

锁住对象:

package synchronizedLock.ObjectLock;

/**
 * @BelongsProject: JAVAtest
 * @BelongsPackage: synchronizedLock
 * @Author: GuoYuan.Zhao
 * @Description: 描述什么人干什么事儿
 * @CreateTime: 2023-07-26 09:08
 * @Version: 1.0
 */

public class TestSynchronized {

//1
    public synchronized void minus() {
        int count = 5;
        for (int i = 0; i < 5; i++) {
            count--;
            System.out.println(Thread.currentThread().getName() + " - " + count);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
            }
        }
    }

第一种情况:
两个线程 thread1 和 thread2,同时访问对象的方法,由于该方法是 synchronized 关键字修饰的,那么这两个线程都需要获得该对象锁,一个获得后另一个线程必须等待。所以我们可以猜测运行结果应该是,一个线程执行完毕后,另一个线程才开始执行,运行例子,输出打印结果如下:

Thread-0 - 4
Thread-0 - 3
Thread-0 - 2
Thread-0 - 1
Thread-0 - 0
Thread-1 - 4
Thread-1 - 3
Thread-1 - 2
Thread-1 - 1
Thread-1 - 0


    //2
//    public synchronized void minus() {
//        int count = 5;
//        for (int i = 0; i < 5; i++) {
//            count--;
//            System.out.println(Thread.currentThread().getName() + " - " + count);
//            try {
//                Thread.sleep(500);
//            } catch (InterruptedException e) {
//            }
//        }
//    }
//
//    public synchronized void minus2() {
//        int count = 5;
//        for (int i = 0; i < 5; i++) {
//            count--;
//            System.out.println(Thread.currentThread().getName() + " - " + count);
//            try {
//                Thread.sleep(500);
//            } catch (InterruptedException e) {
//            }
//        }
//    }


2、两个方法都加锁
两个线程调用
Thread-0 - 4
Thread-0 - 3
Thread-0 - 2
Thread-0 - 1
Thread-0 - 0
Thread-1 - 4
Thread-1 - 3
Thread-1 - 2
Thread-1 - 1
Thread-1 - 0

    //3
//    public synchronized void minus() {
//        int count = 5;
//        for (int i = 0; i < 5; i++) {
//            count--;
//            System.out.println(Thread.currentThread().getName() + " - " + count);
//            try {
//                Thread.sleep(500);
//            } catch (InterruptedException e) {
//            }
//        }
//    }
//
//    public void minus2() {
//        int count = 5;
//        for (int i = 0; i < 5; i++) {
//            count--;
//            System.out.println(Thread.currentThread().getName() + " - " + count);
//            try {
//                Thread.sleep(500);
//            } catch (InterruptedException e) {
//            }
//        }
//    }


3、一个加锁一个不加
Thread-1 - 4
Thread-0 - 4
Thread-1 - 3
Thread-0 - 3
Thread-1 - 2
Thread-0 - 2
Thread-1 - 1
Thread-0 - 1
Thread-1 - 0
Thread-0 - 0

可以看到,结果是交替的,说明线程是交替执行的,说明如果某个线程得到了对象锁,但是另一个线程还是可以访问没有进行同步的方法或者代码。进行了同步的方法(加锁方法)和没有进行同步的方法(普通方法)是互不影响的,一个线程进入了同步方法,得到了对象锁,其他线程还是可以访问那些没有同步的方法(普通方法)。当获取到与对象关联的内置锁时,并不能阻止其他线程访问该对象,当某个线程获得对象的锁之后,只能阻止其他线程获得同一个锁。






}

package synchronizedLock.ObjectLock;

/**
 * @BelongsProject: JAVAtest
 * @BelongsPackage: synchronizedLock
 * @Author: GuoYuan.Zhao
 * @Description: 描述什么人干什么事儿
 * @CreateTime: 2023-07-26 09:08
 * @Version: 1.0
 */

public class Run {

    public static void main(String[] args) {

//1
        final TestSynchronized test = new TestSynchronized();

        Thread thread1 = new Thread(new Runnable() {

            @Override
            public void run() {
                test.minus();
            }
        });

        Thread thread2 = new Thread(new Runnable() {

            @Override
            public void run() {
                test.minus();
            }
        });

        thread1.start();
        thread2.start();

    }


        //2   3
//        final TestSynchronized test = new TestSynchronized();
//
//        Thread thread1 = new Thread(new Runnable() {
//
//            @Override
//            public void run() {
//                test.minus();
//            }
//        });
//
//        Thread thread2 = new Thread(new Runnable() {
//
//            @Override
//            public void run() {
//                test.minus2();
//            }
//        });
//
//        thread1.start();
//        thread2.start();
//    }








//    }


}

锁住类:

package synchronizedLock.ClassLock;

/**
 * @BelongsProject: JAVAtest
 * @BelongsPackage: synchronizedLock.ClassLock
 * @Author: GuoYuan.Zhao
 * @Description: 描述什么人干什么事儿
 * @CreateTime: 2023-07-26 09:36
 * @Version: 1.0
 */

public class TestSynchronized {

//1
//    public static synchronized void minus() {
//        int count = 5;
//        for (int i = 0; i < 5; i++) {
//            count--;
//            System.out.println(Thread.currentThread().getName() + " - " + count);
//            try {
//                Thread.sleep(500);
//            } catch (InterruptedException e) {
//            }
//        }
//    }
Thread-0 - 4
Thread-0 - 3
Thread-0 - 2
Thread-0 - 1
Thread-0 - 0
Thread-1 - 4
Thread-1 - 3
Thread-1 - 2
Thread-1 - 1
Thread-1 - 0

可以看到,类锁和对象锁其实是一样的,由于静态方法是类所有对象共用的,所以进行同步后,该静态方法的锁也是所有对象唯一的。每次只能有一个线程来访问对象的该非静态同步方法。


    //2

    public static synchronized void minus() {
        int count = 5;
        for (int i = 0; i < 5; i++) {
            count--;
            System.out.println(Thread.currentThread().getName() + " - " + count);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
            }
        }
    }

    public synchronized void minus2() {
        int count = 5;
        for (int i = 0; i < 5; i++) {
            count--;
            System.out.println(Thread.currentThread().getName() + " - " + count);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
            }
        }
    }
//Thread-1 - 4
Thread-0 - 4
Thread-0 - 3
Thread-1 - 3
Thread-0 - 2
Thread-1 - 2
Thread-0 - 1
Thread-1 - 1
Thread-1 - 0
Thread-0 - 0

可以看到两个线程是交替进行的,也就是说类锁和对象锁是不一样的锁,是互相独立的。

}

package synchronizedLock.ClassLock;

/**
 * @BelongsProject: JAVAtest
 * @BelongsPackage: synchronizedLock.ClassLock
 * @Author: GuoYuan.Zhao
 * @Description: 描述什么人干什么事儿
 * @CreateTime: 2023-07-26 09:36
 * @Version: 1.0
 */

public class Run {

    public static void main(String[] args) {
//
//        Thread thread1 = new Thread(new Runnable() {
//
//            @Override
//            public void run() {
//                TestSynchronized.minus();
//            }
//        });
//
//        Thread thread2 = new Thread(new Runnable() {
//
//            @Override
//            public void run() {
//                TestSynchronized.minus();
//            }
//        });
//
//        thread1.start();
//        thread2.start();
//




      //2



            final TestSynchronized test = new TestSynchronized();

            Thread thread1 = new Thread(new Runnable() {

                @Override
                public void run() {
                    TestSynchronized.minus();
                }
            });

            Thread thread2 = new Thread(new Runnable() {

                @Override
                public void run() {
                    test.minus2();
                }
            });

            thread1.start();
            thread2.start();

        }

    }


总结

synchronized 关键字主要有以下几种用法:

  • 非静态方法的同步;
  • 静态方法的同步;
  • 代码块。

非静态方法的同步:
非静态方法的同步是针对实例对象的,即在同一时间内只允许一个线程访问该实例对象的同步方法。

静态方法的同步:
静态方法的同步是针对类的,即在同一时间内只允许一个线程访问该类的静态同步方法。

代码块:
synchronized关键字还可以用于代码块,这时候需要指定一个对象作为锁。在同一时间内,只允许一个线程进入该代码块执行。

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

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

相关文章

React Dva项目中模仿网络请求数据方法

我们都已经选择react了 那么自然是一个前后端分离的开发形式 至少我在公司中 大部分时候是前后端同时开发的 一般你在开发界面没有接口直接给你 但你可以和后端约定数据格式 然后在前端模拟数据 我们在自己的Dva项目中 在根目录下的 mock 目录下创建一个js文件 我这里叫 filmDa…

ES6 (js)

学习了很多vue的视频&#xff0c;还有nuxt的&#xff0c;还是不会。 还是要学ES6 本文的大部分出自小马老师的资料&#xff0c;还有曾大佬的文章 变量&#xff08;Let 和 const&#xff09; 在es6中&#xff0c;多用let 和const 来声明变量类型。因为var 会提前声明&#xff0…

flink cdc环境搭建

1.下载flink https://archive.apache.org/dist/flink/flink-1.12.2/ 2.修改flink-conf.yaml #根据自己电脑核数修改&#xff0c;这里我设置为4&#xff0c;因为系统分配了4核 jobmanager.rpc.address: localhost #主机名根据自己设定 taskmanager.numberOfTaskSlots: 4 3.下载…

mysql进阶2——prosysql实现mysql读写分离

文章目录 一、读写分离方案类型1.1 最简单的读写分离1.2 多个读组或写组的分离模式 二、案例2.1 初始化操作2.2 mysql主添加proxysql连接用户2.3 Proxysql添加连接mysql集群参数2.4 添加健康检测用户2.5 添加读写分离的路由规则2.6 验证 一、读写分离方案类型 基本了解&#xf…

【uniapp】更改富文本编辑器图片大小

代码块 //<view v-html"productDetails"></view><rich-text :nodes"productDetails"></rich-text>// 假设htmlContent字段是后台返回的富文本字段var htmlContent res.result.productDetailsconst regex new RegExp(<img, gi…

STM32MP157驱动开发——按键驱动(休眠与唤醒)

文章目录 “休眠-唤醒”机制&#xff1a;APP执行过程内核函数休眠函数唤醒函数 休眠与唤醒方式的按键驱动程序(stm32mp157)驱动程序框架button_test.cgpio_key_drv.cMakefile修改设备树文件编译测试 “休眠-唤醒”机制&#xff1a; 当应用程序必须等待某个事件发生&#xff0c…

爬虫001_Pip指令使用_包管理工具_pip的使用_和源的切换---python工作笔记019

scrapy是一个爬虫的框架 确认一下pip这个python中的包管理工具是否已经安装好了 python的环境变量配置完了以后,还需要配置一下pip的环境变量 把这个目录配置好,这个pip的环境变量的配置很简单不多说了. 我们用pip安装一下包,我们安装到上面这个路径里面,就是python的安装路…

Qt - .ui 文件的使用

文章目录 目录工具栏Dock Widget代码控制 ui添加资源添加文件 目录 子目录只能输入英文&#xff0c;想要显示中文&#xff0c;可以修改右下方表中的 text 属性&#xff1a; 工具栏 让工具栏共用 菜单栏的 new 和 open&#xff0c;只需将下方列表的控件&#xff0c;拖拽到工具栏…

【RS】基于规则的面向对象分类

ENVI使用最多的工具就是分类&#xff0c;这也是很多卫星影像的用途。在ENVI中有很多分类工具&#xff0c;如最基础的监督分类&#xff08;最大似然法、最小距离、支持向量机、随机森林&#xff09;、非监督分类&#xff08;K-means、IsoData&#xff09;&#xff0c;还有面向对…

安卓开发后台应用周期循环获取位置信息上报服务器

问题背景 最近有需求&#xff0c;在APP启动后&#xff0c;退到后台&#xff0c;还要能实现周期获取位置信息上报服务器&#xff0c;研究了一下实现方案。 问题分析 一、APP退到后台后网络请求实现 APP退到后台后&#xff0c;实现周期循环发送网络请求。目前尝试了两种方案是…

随笔--更改已经启动中的容器的配置文件

文章目录 docker 容器的配置信息地址修改文件映射 docker 容器的配置信息地址 # 一般在 sudo su cd /cd /var/lib/docker/containers/{容器id}/ # 查看容器的id,CONTAINER ID就是容器id的前部分 docker ps修改文件映射 进入容器的配置文件位置一般包含这些文件 # 先stop容器…

Docker--harbor私有仓库部署与管理

目录 一、构建私有库 1.下载 registry 镜像 2.在 daemon.json 文件中添加私有镜像仓库地址 3.运行 registry 容器 4.为镜像打标签 5.上传到私有仓库 6.列出私有仓库的所有镜像 7.列出私有仓库的centos镜像有哪些tag 8.测试私有仓库下载 二、Harbor 简介 1.什么是Harb…

iOS--属性关键字

定义 chat&#xff1a; 在iOS开发中&#xff0c;属性关键字是用于声明类的属性&#xff08;实例变量&#xff09;的修饰符。属性关键字可以影响属性的访问权限、内存管理和生成相关的getter和setter方法。 属性关键字有哪些&#xff1f; 分类属性关键字原子性atomic、nonato…

Spring MVC-基础概念(定义+创建和连接+@RequestMappring的描述)

目录 1.什么是Spring MVC&#xff1f; 2. MVC 和 Spring MVC 的关系 3.Spring MVC 项目创建 4. RequestMappring实现用户和程序的映射 4.1 RequestMappring 注解解释 4.2 方法1: RequestMapping(“/xxx”) 4.4 RequestMapping(method xxxx, value “xxx”) 是POST/GET…

协作实现时序数据高效流转链路 | 7.20 IoTDB X RocketMQ 技术沙龙线上直播回顾

7 月 20 日&#xff0c;IoTDB X RocketMQ 技术沙龙线上直播圆满结束。工业物联网时序数据库研发商天谋科技、云原生事件流平台 Apache RocketMQ 社区的四位技术专家&#xff0c;针对实时数据接入、多样数据处理与系统的高扩展、高可靠特性的数据流转处理平台实现难点&#xff0…

Java日志框架JUL、Log4j、logback、log4j2使用

随着软件系统的发展系统业务越来越多、逻辑越来越复杂、代码量越来越多&#xff0c;伴随着容易出现的bug也会越来越多&#xff0c;不论是开发测试阶段还是生产阶段都需要将这些错误及时的捕捉记录下来&#xff0c;方便解决这些问题&#xff0c;否则针对出现的异常无从下手&…

【三维点云处理】顶点、面片、邻接矩阵、邻接距离矩阵以及稀疏存储概念

文章目录 vts和faces基础知识vertices-节点&#xff08;3是点的三维坐标&#xff09;faces-面片&#xff08;3是构成三角形面片的3个点&#xff09; 邻接矩阵邻接距离矩阵&#xff08;NN500&#xff09;稀疏矩阵 vts和faces基础知识 vertices-节点&#xff08;3是点的三维坐标…

【JavaEE初阶】HTTP协议

文章目录 1. HTTP概述和fiddler的使用1.1 HTTP是什么1.2 抓包工具fiddler的使用1.2.1 注意事项1.2.2 fiddler的使用 2. HTTP协议格式2.1 HTTP请求格式2.1.1 基本格式2.1.2 认识URL2.1.3 方法 2.2 请求报头关键字段2.3 HTTP响应格式2.3.1 基本格式2.3.2状态码 1. HTTP概述和fidd…

【博客682】k8s apiserver bookmarks机制以更高效检测变更

k8s apiserver bookmarks机制以更高效检测变更 list-watch背景&#xff1a; List-Watch 是kubernetes中server和client通信的最核心的机制&#xff0c; 比如说api-server监听etcd&#xff0c; kubelet监听api-server&#xff0c; scheduler监听api-server等等&#xff0c;其实…

任务的创建与删除

Q: 什么是任务&#xff1f; A: 任务可以理解为进程/线程&#xff0c;创建一个任务&#xff0c;就会在内存开辟一个空间。 比如&#xff1a; 玩游戏&#xff0c;打篮球&#xff0c;开车&#xff0c;都可以视为任务。 Windows 系统中的 MarkText 、谷歌浏览器、记事本&#xff0…