当前位置: 首页 > news >正文

深入解析synchronized实现原理

一、synchronized 的定义与作用

为了解决线程安全问题,Java 提供了synchronized关键字,它就像是集市中的秩序维护者,为多线程环境带来了规则和秩序。​

synchronized关键字可以用来修饰方法或者代码块。当它修饰一个方法时,这个方法就成为了同步方法,同一时刻只能有一个线程能够进入该方法执行。当它修饰一个代码块时,这个代码块就被称为同步代码块,线程需要先获取到指定对象的锁,才能进入代码块执行 。​

通过这种方式,synchronized实现了线程之间的互斥访问,就像给共享资源加上了一把锁,同一时间只有拿到这把锁的线程才能访问共享资源,从而避免了多个线程同时访问和修改共享资源导致的数据不一致问题。同时,synchronized还具有可见性保障,当一个线程修改了共享变量并释放锁后,其他线程能够立即看到这个修改,确保了数据在多线程环境下的一致性和正确性。​

二、synchronized 的使用方式​

(一)同步方法​

在 Java 中,使用synchronized修饰方法是实现线程同步的一种常见方式,它又可细分为实例方法和静态方法。​

当synchronized修饰实例方法时,它的作用范围是整个方法体,锁对象是当前实例对象this。也就是说,当一个线程调用该实例的同步方法时,它会自动获取当前实例的锁,在该线程释放锁之前,其他线程无法访问该实例的任何同步方法 。​

来看一个简单的示例代码:

public class SynchronizedInstanceMethodExample {private int count = 0;public synchronized void increment() {count++;}public synchronized int getCount() {return count;}
}

在上述代码中,increment方法和getCount方法都被synchronized修饰,它们都是同步方法。当多个线程同时调用increment方法时,只有一个线程能够获得当前实例的锁并执行该方法,其他线程必须等待锁的释放。​

再看静态方法,当synchronized修饰静态方法时,它的作用范围同样是整个方法体,但锁对象是当前类的Class对象。这意味着,无论创建了多少个该类的实例,只要一个线程进入了静态同步方法,其他线程就无法同时进入该类的任何静态同步方法 。

public class SynchronizedStaticMethodExample {private static int count = 0;public static synchronized void increment() {count++;}public static synchronized int getCount() {return count;}
}

在这个例子中,increment方法和getCount方法是静态同步方法,锁对象是SynchronizedStaticMethodExample.class。多个线程调用这些静态方法时,会共享这个类锁,从而保证了线程安全。​

(二)同步代码块​

除了修饰方法,synchronized还可以用于修饰代码块,以实现更细粒度的同步控制。同步代码块的语法形式为:

synchronized(lockObject) {// 同步代码块
}

其中,lockObject是一个对象,也被称为锁对象。当线程进入同步代码块时,它需要先获取lockObject的锁,只有获取到锁的线程才能执行代码块中的内容,其他线程则被阻塞,直到锁被释放 。​

假设我们有一个场景,多个线程需要访问一个共享资源,但我们只想对部分代码进行同步控制,这时就可以使用同步代码块:

public class SynchronizedBlockExample {private int count = 0;private final Object lock = new Object();public void increment() {synchronized (lock) {count++;}}public int getCount() {return count;}
}

 

在上述代码中,increment方法包含一个同步代码块,锁对象是lock。这样,只有获得lock对象锁的线程才能执行count++操作,从而保证了对count变量的线程安全访问。​

使用同步代码块的好处在于可以精确控制同步的粒度,只对需要同步的代码进行加锁,减少锁的持有时间,从而提高程序的并发性能。例如,在一个包含大量非同步操作的方法中,如果只有一小部分代码涉及共享资源的访问,那么使用同步代码块将这部分代码包裹起来,而不是将整个方法声明为同步方法,能够显著减少线程等待的时间,提升系统的整体吞吐量。​

三、synchronized 的底层实现原理​

(一)Java 对象头与 Mark Word​

在 Java 中,当一个对象被创建时,它在内存中的布局主要由三部分组成:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding) 。​

对象头又包含两部分重要信息:Mark Word 和类型指针。其中,Mark Word 是我们理解synchronized底层实现的关键。Mark Word 用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等 。它的长度在 32 位和 64 位的虚拟机(未开启压缩指针)中分别为 32bit 和 64bit 。为了更高效地利用这有限的空间,Mark Word 的内容会根据对象的状态而动态变化。​

在不同的锁状态下,Mark Word 存储的信息有所不同:​

  • 无锁状态:对象刚创建时,处于无锁状态,此时 Mark Word 中存储对象的哈希码、GC 分代年龄等信息,锁标志位为 “01” 。例如,在 32 位虚拟机中,Mark Word 的 32 位空间可能被划分为:25 位用于存储哈希码,4 位用于记录 GC 分代年龄,3 位作为标志位表示无锁状态 。​
  • 偏向锁状态:当一段同步代码一直被同一个线程访问时,该线程会自动获取锁,此时对象进入偏向锁状态。Mark Word 中会记录偏向线程的 ID、偏向时间戳等信息,锁标志位为 “101” 。这就好比一个专属通道,偏向锁线程可以直接进入,无需额外的锁获取操作,大大提高了效率。​
  • 轻量级锁状态:当有多个线程交替访问同步代码块时,偏向锁会升级为轻量级锁。此时 Mark Word 中存储指向线程栈中锁记录的指针,锁标志位为 “00” 。轻量级锁通过 CAS(Compare and Swap)操作来尝试获取锁,如果 CAS 操作成功,线程就获取到了锁;如果失败,则表示存在竞争,会进一步升级为重量级锁 。​
  • 重量级锁状态:当多个线程同时竞争同一把锁时,轻量级锁会膨胀为重量级锁。Mark Word 中存储指向重量级锁(Monitor)的指针,锁标志位为 “10” 。重量级锁依赖操作系统的互斥量(Mutex)来实现线程的阻塞和唤醒,会导致用户态和内核态的切换,开销较大 。​

不同的 JVM 对于对象头和 Mark Word 的实现可能会有细微差异。以 HotSpot VM 为例,它对对象头和 Mark Word 的结构和管理进行了优化,以提高性能和内存利用率。在开启指针压缩的 64 位 HotSpot VM 中,对象头中的类型指针占用 4 字节,Mark Word 占用 8 字节 。这种优化使得对象在内存中的布局更加紧凑,同时也保证了synchronized关键字在不同锁状态下的高效运作。​

(二)监视器锁(Monitor)​

Monitor 是 JVM 实现线程同步的核心机制,它就像是一个严密的守护卫士,负责管理和协调线程对共享资源的访问。每一个 Java 对象都可以关联一个 Monitor,当对象被synchronized关键字修饰时,就会涉及到 Monitor 的操作 。​

可以把 Monitor 想象成一个房间,房间里有一个主人(持有锁的线程),还有一些等待进入房间的客人(等待获取锁的线程)。当一个线程试图进入同步代码块或同步方法时,它需要先获取对应的 Monitor 锁,就如同客人要进入房间必须得到主人的许可。​

在 Monitor 内部,有几个关键的组成部分和工作机制:​

  • Owner:指向当前持有该 Monitor 锁的线程。当一个线程成功获取到 Monitor 锁时,它就成为了 Owner 。例如,线程 A 进入了一个被synchronized修饰的代码块,此时线程 A 就成为了该代码块所关联对象的 Monitor 的 Owner 。​
  • EntryList:是一个等待队列,当线程尝试获取 Monitor 锁失败时,它会被放入 EntryList 中等待 。就像一群客人在房间外排队等待进入,EntryList 中的线程处于阻塞状态,等待着 Owner 释放锁后重新竞争获取锁的机会 。​
  • WaitSet:也是一个等待队列,当线程在同步代码块中调用了wait()方法时,它会释放当前持有的 Monitor 锁,并进入 WaitSet 等待 。例如,线程 A 在同步代码块中调用了wait()方法,它会将 Monitor 的所有权交回,然后进入 WaitSet,此时其他线程就有机会获取 Monitor 锁并进入同步代码块 。当notify()或notifyAll()方法被调用时,WaitSet 中的线程会被唤醒,并重新进入 EntryList 参与锁的竞争 。​

线程获取、持有和释放锁的过程如下:​

  1. 获取锁:当线程执行到synchronized修饰的同步代码块或同步方法时,它会尝试获取对应的 Monitor 锁。如果 Monitor 的进入数为 0(即锁未被占用),则该线程成功获取到锁,将进入数设置为 1,并成为 Monitor 的 Owner 。如果线程已经是该 Monitor 的 Owner,再次进入时,进入数会加 1,这体现了synchronized的可重入性 。例如,线程 A 进入一个同步方法,获取了 Monitor 锁,进入数变为 1;当线程 A 在该方法中再次调用另一个被synchronized修饰的方法时,由于它已经是 Owner,进入数会变为 2 。​
  2. 持有锁:在持有锁期间,线程可以执行同步代码块或同步方法中的内容,其他线程无法获取该 Monitor 锁,只能在 EntryList 中等待 。​
  3. 释放锁:当线程执行完同步代码块或同步方法,或者在代码块中调用了wait()方法时,它会释放 Monitor 锁,将进入数减 1 。当进入数减为 0 时,Monitor 锁被完全释放,EntryList 中的线程可以竞争获取锁 。例如,线程 A 执行完同步方法后,进入数减 1 变为 0,此时 Monitor 锁被释放,EntryList 中的线程 B 有机会竞争获取该锁 。​

(三)字节码指令层面分析​

从字节码指令的角度来看,synchronized关键字在同步代码块和同步方法上的实现方式略有不同,但都依赖于 Monitor 机制 。​

对于同步代码块,使用javap命令反编译包含同步代码块的类文件,可以看到它是通过monitorenter和monitorexit指令来实现同步的 。monitorenter指令指向同步代码块的开始位置,当线程执行到该指令时,会尝试获取对应的 Monitor 锁;monitorexit指令则指明同步代码块的结束位置,当线程执行到该指令时,会释放获取到的 Monitor 锁 。在同步代码块中,无论正常执行结束还是出现异常,都会执行monitorexit指令来确保锁的正确释放,这就是为什么会有两个monitorexit指令,一个用于正常退出,一个用于异常退出 。例如:

public class SynchronizedBlockExample {private Object lock = new Object();public void method() {synchronized (lock) {// 同步代码块System.out.println("Inside synchronized block");}}
}

反编译后的字节码片段如下:

public void method();descriptor: ()Vflags: ACC_PUBLICCode:stack=2, locals=3, args_size=10: aload_01: getfield #2 // Field lock:Ljava/lang/Object;4: dup5: astore_16: monitorenter7: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;10: ldc #4 // String Inside synchronized block12: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V15: aload_116: monitorexit17: goto 2520: astore_221: aload_122: monitorexit23: aload_224: athrow25: return

在这段字节码中,6: monitorenter指令表示进入同步代码块,尝试获取lock对象的 Monitor 锁;16: monitorexit和22: monitorexit指令分别用于正常退出和异常退出时释放锁 。​

对于同步方法,反编译后可以发现,它并没有直接使用monitorenter和monitorexit指令,而是在方法的访问标志(flags)中设置了ACC_SYNCHRONIZED标识 。JVM 在调用方法时,会检查该方法是否设置了ACC_SYNCHRONIZED标志,如果设置了,就会在方法调用前自动获取对应的 Monitor 锁,在方法执行结束后释放锁 。如果是实例方法,JVM 会尝试获取实例对象的锁;如果是静态方法,JVM 会尝试获取当前类的 Class 对象的锁 。例如:

public class SynchronizedMethodExample {public synchronized void method() {// 同步方法System.out.println("Inside synchronized method");}
}

反编译后的字节码片段如下:

public synchronized void method();descriptor: ()Vflags: ACC_PUBLIC, ACC_SYNCHRONIZEDCode:stack=2, locals=1, args_size=10: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #3 // String Inside synchronized method5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: return

在这段字节码中,flags: ACC_PUBLIC, ACC_SYNCHRONIZED表示该方法是一个同步方法,JVM 会根据这个标志来自动进行锁的获取和释放操作 。​

四、synchronized 的锁升级机制​

在 JDK 1.6 之前,synchronized 被称为重量级锁,每次加锁和解锁都需要进行内核态和用户态之间的转换,开销较大。为了减少这种性能消耗,Java 在 1.6 版本引入了偏向锁和轻量级锁的概念 ,使得 synchronized 的锁状态可以根据竞争情况进行动态升级,从而提高了并发性能。锁升级的路径为:无锁→偏向锁→轻量级锁→重量级锁 ,且锁只能升级不能降级,这种策略旨在提高获得锁和释放锁的效率 。​

(一)偏向锁​

偏向锁是为了优化单线程重复访问同步资源的场景而设计的。在大多数情况下,锁不仅不存在竞争,而且常常被同一个线程获取。如果一个线程每次获取锁都使用重量级锁,开销将会非常大。偏向锁就像是为这个线程量身定制的专属通道,当它首次访问同步代码块并获取锁时,JVM 会利用 CAS 操作在对象的 Mark Word 中记录下当前线程的 ID 和偏向锁标记位(通常设置为 1) 。​

此后,当这个线程再次访问该同步代码块时,它只需检查对象头中的线程 ID 是否与自己的 ID 相同。如果相同,就可以直接获得锁,无需再进行 CAS 操作,也不需要进行额外的同步开销,整个过程几乎没有性能损耗 。这就好比一个人进入自己的专属房间,无需敲门等待,直接就能进入。​

然而,偏向锁并非一直有效。当有其他线程尝试竞争该锁时,就意味着出现了锁竞争的情况,偏向锁无法应对这种多线程竞争的场景 。此时,偏向锁会被撤销(这一过程需要等待全局安全点,确保所有线程都暂停),并尝试升级为轻量级锁 。比如,原本只有一个人使用的专属房间,突然来了其他人也要进入,这时就不能再维持专属的状态,需要重新调整进入规则,升级为更能适应多人竞争的轻量级锁机制。​

(二)轻量级锁​

当偏向锁被撤销后,锁会升级到轻量级锁状态,它主要适用于多线程交替执行同步代码块的场景 。在轻量级锁状态下,JVM 会在当前线程的栈帧中创建一个锁记录(Lock Record),并将对象头中的 Mark Word 复制到该锁记录中,同时对象头中会有一个指针指向这个锁记录 。​

当线程尝试获取轻量级锁时,会使用 CAS 操作将对象头中的 Mark Word 替换为指向自己栈帧中锁记录的指针 。如果 CAS 操作成功,就表示该线程成功获取到了锁,此时对象处于轻量级锁状态,锁标志位变为 “00” 。如果 CAS 操作失败,说明存在竞争,有其他线程已经持有了该对象的轻量级锁 。​

在这种情况下,获取锁失败的线程并不会立即阻塞,而是进入自旋(Spinning)状态,即不断尝试重新获取锁 。自旋的目的是为了避免线程切换带来的性能开销,因为线程切换涉及到操作系统层面的操作,开销相对较大 。就像几个人在轮流尝试进入一个房间,当一个人发现门暂时打不开时,他不会立刻离开,而是在门口不断尝试开门,直到成功进入 。​

默认情况下,自旋的次数为 10 次,用户可以通过 - XX:PreBlockSpin 选项来进行更改 。在 JDK 1.7 之后,引入了自适应自旋,JVM 会根据以往的运行信息来判断一个锁是否可以轻松地通过自旋获取到,如果是,会允许其他获取该线程的锁自旋更多的次数,反之 JVM 会直接挂起尝试获取这个锁的线程 。​

但是,如果自旋超过一定次数(通常是 10 次)仍未获取到锁,或者有其他线程参与锁竞争,表明竞争较为激烈,轻量级锁无法满足需求,就会膨胀为重量级锁 。​

(三)重量级锁​

当轻量级锁无法满足并发需求时,锁会升级为重量级锁,它适用于高并发竞争激烈的场景 。在重量级锁状态下,Mark Word 中存储指向 Monitor 对象(由 C++ 实现)的指针,锁标志位变为 “10” 。​

当一个线程尝试获取重量级锁时,如果该锁已经被其他线程持有,那么当前线程会进入阻塞状态,被放入 Monitor 的 EntryList 等待队列中,由操作系统负责调度和唤醒 。只有当持有锁的线程释放锁之后,操作系统会从 EntryList 中唤醒一个线程,使其有机会重新尝试获取锁 。​

由于重量级锁依赖于操作系统的互斥量(Mutex)或其他同步机制来实现线程的阻塞和唤醒,这涉及到用户态和内核态的切换,开销相对较大 。就好比在一个非常拥挤的房间门口,人们需要排队等待进入,当门被占用时,其他人只能在门外等待,而且等待和进入的过程需要由一个权威的管理者(操作系统)来协调,这个协调过程需要花费较多的精力和时间 。在高并发竞争激烈的场景下,频繁的线程阻塞和唤醒会导致系统性能大幅下降,因此应尽量避免过度使用重量级锁 。​

五、synchronized 的优化策略​

(一)自旋锁与自适应自旋​

在多线程编程中,线程的阻塞和唤醒涉及到操作系统层面的操作,需要进行用户态和内核态的切换,这种上下文切换的开销相对较大。自旋锁便是为了解决这一问题而出现的一种优化策略。​

当一个线程尝试获取轻量级锁失败时,它并不会立即被阻塞,而是进入自旋状态。在自旋状态下,线程会在一个循环中不断地尝试获取锁,而不是将自己挂起等待。这就好比一个人去敲门,发现门暂时打不开,他没有离开,而是在门口不断敲门,直到门被打开。这样做的好处是避免了线程被阻塞后重新调度所带来的开销,因为线程的阻塞和唤醒需要操作系统进行大量的额外工作,包括保存和恢复线程的上下文信息等。​

默认情况下,自旋的次数为 10 次,用户可以通过-XX:PreBlockSpin选项来进行更改。在 JDK 1.7 之后,引入了自适应自旋,使得自旋的次数不再是固定的。JVM 会根据以往的运行信息来判断一个锁是否可以轻松地通过自旋获取到 。如果在过去的某个锁竞争中,线程通过自旋成功获取到了锁,那么 JVM 会认为这个锁在未来也很可能可以通过自旋获取,于是会允许其他获取该线程的锁自旋更多的次数 。反之,如果某个锁在过去的自旋尝试中很少成功,JVM 会直接挂起尝试获取这个锁的线程,避免不必要的自旋浪费 CPU 资源。例如,在一个电商系统的库存管理模块中,如果多个线程频繁地竞争库存锁,但每次竞争的时间都很短,使用自适应自旋可以大大提高系统的并发性能 。​

(二)锁消除​

锁消除是 JIT 编译器在编译阶段执行的一项优化技术,它的核心原理基于逃逸分析。在 Java 中,当一个对象在方法内部被创建,并且这个对象的引用不会被传递到方法外部,也不会被其他线程访问时,我们就说这个对象没有逃逸出该方法。​

JIT 编译器在编译过程中,会对代码进行逃逸分析。如果它发现某个同步块中的锁对象没有逃逸出当前线程,即该锁对象只在当前线程中被使用,那么编译器就可以确定这个锁对象不可能被其他线程竞争,此时同步操作实际上是多余的 。基于此,JIT 编译器会自动将这些不必要的同步操作消除,从而减少锁的获取和释放带来的开销,提高程序的执行效率。​

来看一个具体的例子:

public class LockEliminationExample {public void method() {StringBuffer sb = new StringBuffer();for (int i = 0; i < 10; i++) {sb.append(i);}String result = sb.toString();}
}

在上述代码中,StringBuffer内部的方法是同步的,比如append方法。但是,sb是在method方法内部创建的局部变量,它不会逃逸出该方法,也就是说,只有当前线程可以访问sb。因此,JIT 编译器在编译时可以通过逃逸分析判断出这个StringBuffer对象上的同步操作是不必要的,进而将其消除 。这样,在运行时就不会执行这些同步操作,提高了代码的执行速度。​

(三)锁粗化​

锁粗化与锁消除相反,它是将多个连续的锁操作合并为一个更大范围的锁操作,目的是减少频繁加锁和解锁带来的开销。​

在某些情况下,开发者可能会在一段代码中频繁地对同一个对象进行加锁和解锁操作,例如在一个循环中。虽然每次加锁和解锁的时间可能很短,但是频繁的这些操作累积起来也会带来可观的性能开销。​

JVM 会检测到这种情况,并将多个连续的对同一对象的加锁和解锁操作合并为一个,将锁的范围扩大到整个操作序列的外部 。以字符串拼接为例:

public class LockCoarseningExample {private String result = "";private final Object lock = new Object();public void concatenateStrings() {for (int i = 0; i < 1000; i++) {synchronized (lock) {result += i;}}}
}

在上述代码中,每次循环都对lock对象进行加锁和解锁操作,这是非常低效的。JVM 会将锁粗化,将锁的范围扩大到整个循环外部,就像下面这样:

public class LockCoarseningExample {private String result = "";private final Object lock = new Object();public void concatenateStrings() {synchronized (lock) {for (int i = 0; i < 1000; i++) {result += i;}}}
}

通过锁粗化,减少了锁的获取和释放次数,从而提高了程序的性能。这种优化在循环操作中对同一对象频繁加锁的场景下尤为有效,可以显著减少锁竞争和上下文切换带来的开销 。​

六、总结

在 Java 多线程编程的领域中,synchronized关键字无疑是保障线程安全的中流砥柱 。通过深入剖析其实现原理,我们了解到它基于 Java 对象头中的 Mark Word 和监视器锁(Monitor)机制,巧妙地实现了线程间的同步与互斥 。从字节码指令层面来看,无论是同步代码块的monitorenter和monitorexit指令,还是同步方法的ACC_SYNCHRONIZED标识,都精准地控制着线程对共享资源的访问 。​

同时,synchronized的锁升级机制,从偏向锁到轻量级锁再到重量级锁的逐步转换,使其能够根据不同的竞争场景,智能地选择最适合的锁策略,从而显著提升了并发性能 。而自旋锁、锁消除、锁粗化等优化策略的引入,更是进一步减少了锁操作带来的开销,让synchronized在高并发环境下也能游刃有余 。​

在实际开发中,我们需要根据具体的业务场景和并发需求,合理地使用synchronized关键字 。例如,在单线程重复访问同步资源的场景下,偏向锁能够发挥出最大的优势;而在多线程交替访问的场景中,轻量级锁则是更好的选择 。同时,我们也要注意避免锁竞争过于激烈,尽量减小锁的粒度,合理设置锁的范围,以提高程序的并发性能 。​

http://www.lqws.cn/news/527725.html

相关文章:

  • 【2-入门与调试设置】1.坐标辅助器与轨道控制器
  • 英特尔汽车业务败走中国,喊出“All in”才过两个月
  • 观测云产品更新 | 外部数据源、日志、监控、事件、基础设施等
  • TCP 协议安全性全面分析:漏洞、应用场景与防护策略
  • 芯谷科技--降压型DC-DC转换器D4005
  • [OS_27] 现代应用程序架构
  • ESP32 VSCODE进入menuconfig时ESP-IDF idf.py menuconfig卡进度条,setuptools版本太高解决方法
  • 小程序学习笔记:实现上拉触底加载随机颜色案例全解析
  • 深度剖析 Apache Pulsar:架构、优势与选型指南
  • 图像质量对比感悟
  • [论文阅读] 人工智能 + 软件工程 | AI 与敏捷开发的破局之路:从挫败到成功的工作坊纪实
  • 推荐一个前端基于vue3.x,vite7.x,后端基于springboot3.4.x的完全开源的前后端分离的中后台管理系统基础项目(纯净版)
  • HTML 按钮单击事件示例
  • 2-深度学习挖短线股-4-预测数据计算
  • 前端项目3-01:登录页面
  • 实测推荐:一款能看4K直播的万能播放器,支持多端同步
  • 全面比较帮你确定何时选择SLM而非LLM
  • C# .NET Framework 中的高效 MQTT 消息传递
  • React HOC(高阶组件-补充篇)
  • Django 零基础起步:开发你的网站第一步
  • IDE如何快速切换JLINK版本
  • Redis 持久化
  • Axure版AntDesign 元件库-免费版
  • 广州华锐互动:技术与创意双驱动的 VR 先锋​
  • Python 中的 random 模块
  • 49-有效的字母异位词
  • 设计模式精讲 Day 14:命令模式(Command Pattern)
  • Web基础关键_001_HTML(一)
  • docker环境下java参数传递与获取
  • FANUC机器人教程:用户坐标系标定及其使用方法