基本介绍
★程序(program)
为完成特定任务,用某种编程语言编写的一组指令的集合(代码)。
★进程
1)进程是指运行中的程序;在使用某个程序时,就启动了一个进程,操作系统就会为该进程分配内存空间。当使用另一个程序时,就启动了另一个进程,操作系统就会为该进程分配新的内存空间。
2)进程是程序的一次执行过程,或是正在运行的一个程序。是动态过程:程序的产生(启动)、存在(执行)、消亡(停止)的过程。
★线程
1)线程是由进程创建的,是进程的一个实体;
2)一个进程可以拥有多个线程。例如:同时可以下载多个软件,每个软件下载的时候都是一个线程。
线程概念
1)单线程:同一时刻只允许执行一个线程;
2)多线程:同一时刻,可以执行多个线程;
3)并发:同一时刻,多个任务交替执行,因为处理任务速度快看似是同时执行,单核CPU实现的多任务就是并发;
4)并行:同一时刻,多任务同时执行,多核CPU可以实现并行。
获取本机CPU数量代码
package com.pero.thread_;
/**
* 获取本机CPU数量
*
* @author Pero
* @version 1.0
*/
public class GetCPUNumber {
public static void main(String[] args) {
Runtime runtime = Runtime.getRuntime();
//获取本机CPU数量
int cpuNumber = runtime.availableProcessors();
System.out.println("本机CPU数量为:" + cpuNumber);
}
}
线程的基本使用
1)继承Thread类(实现了Runnable接口),重写run()方法;
2)实现Runnable接口(自定义线程类),重写run()方法。
测试代码(继承Thread类)
package com.pero.thread_;
/**
* @author Pero
* @version 1.0
*/
public class Thread01 {
public static void main(String[] args) throws InterruptedException {
Cat cat = new Cat();
cat.start(); //启动线程
//当main线程启动一个子线程Thread-0,主线程不会阻塞会继续执行
//打印主线程名称
int times = 1;
for (int i = 0; i < 8; i++) {
System.out.println("主线程继续执行,第"+(times++)+"次,"+"主线程名称:"+Thread.currentThread().getName());
Thread.sleep(1000);
}
}
}
//当一个类继承Thread类,那么该类也是一个线程类
//Thread类run()方法实现了Runnable接口的run()方法
class Cat extends Thread {
@Override
public void run() { //重写run()方法,写上自己的业务逻辑
int number = 1;
while(true){
//该线程每隔一秒,在控制台输出"喵喵,我是小猫咪"
System.out.println("喵喵,我是小猫咪"+(number++)+
"子线程名称:"+Thread.currentThread().getName());
//让该线程休眠1秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (number > 8){
break;
}
}
}
}
源码分析
多线程机制:
点击Run'Thread01.main()'时就启动了一个进程,然后进入主方法(进程开启了一个main线程),在main线程中执行start()方法,再启动一个"Thread-0"线程,main线程不会阻塞会继续执行,主线程和子线程交替执行(同时执行)。使用JConsole监控线程执行情况(在程序执行过程中,点击Terminal,输入jconsole即可显示Java监视和管理控制台)。主线程内容执行完毕后,子线程依旧可以继续执行。
为什么使用start()方法来启动线程?
如果不用start()方法来启动线程而是直接调用run()方法,那么在main线程中run()方法是一个普通方法,程序会一直执行run()方法直至run方法执行完毕后再调用后面的语句,在执行run()方法时其实一直都在主线程中,而不是启动另一个子线程。start()方法是真正启动新线程来执run()方法。
1)执行start()方法
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
//对于VM创建/设置的主方法线程或“系统”组线程,不会调用此方法。
//将来添加到该方法的任何新功能可能也必须添加到VM。
//首先判断当前是不是新线程,如果threadStatus = 0表示是,如果threadStatus != 0,
//表示不是新线程,抛出非法线程状态异常
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
//通知group该线程即将启动,以便将其添加到group的线程列表中,并减少group的未启动计数。
group.add(this);
boolean started = false;
try {
start0(); //该方法是本地方法(native)
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private volatile int threadStatus = 0;
void add(Thread t) {
synchronized (this) {
if (destroyed) {
throw new IllegalThreadStateException();
}
if (threads == null) {
threads = new Thread[4];
} else if (nthreads == threads.length) {
threads = Arrays.copyOf(threads, nthreads * 2);
}
threads[nthreads] = t;
// This is done last so it doesn't matter in case the
// thread is killed
nthreads++;
// The thread is now a fully fledged member of the group, even
// though it may, or may not, have been started yet. It will prevent
// the group from being destroyed so the unstarted Threads count is
// decremented.
nUnstartedThreads--;
}
}
//start0()方法是本地方法,由JVM机调用,底层由c/c++语言实现
//实现多线程的是start0()方法
private native void start0();
//★start()方法调用start0()方法后,该线程不一定会立刻执行,只是将线程变为可运行状态
// 具体什么时间运行,取决于CPU,由CPU统一调度
void threadStartFailed(Thread t) {
synchronized(this) {
remove(t);
nUnstartedThreads++;
}
}
测试代码(实现Runnable接口)
由于java是单继承机制,当一个类继承了一个父类后,无法在继承Thread类来创建线程,这时可以通过实现Runnable接口来创建线程。
package com.pero.thread_;
/**
* @author Pero
* @version 1.0
*/
public class Thread02 {
public static void main(String[] args) {
Tiger tiger = new Tiger();
//tiger.start(); //Tiger类中没有start()方法,所实现的接口Runnable中也没有该方法,无法调用
Thread thread = new Thread(tiger); //底层使用设计模式(代理模式)
thread.start();
}
}
class Tiger implements Runnable{
int count = 0;
@Override
public void run() {
while (true){
System.out.println("虎啸"+(++count)+Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (count == 8){
break;
}
}
}
}
源码分析
1)执行Thread(Runnable target)构造器
public Thread(Runnable target) { //传入实现了Runnable接口的对象
init(null, target, "Thread-" + nextThreadNum(), 0);
}
/* What will be run. */
private Runnable target;
private void init(ThreadGroup g, Runnable target, String name,
long stackSize) {
init(g, target, name, stackSize, null, true);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
if (name == null) { //线程名字
throw new NullPointerException("name cannot be null");
}
this.name = name;
Thread parent = currentThread();
SecurityManager security = System.getSecurityManager();
if (g == null) {
/* Determine if it's an applet or not */
/* If there is a security manager, ask the security manager
what to do. */
if (security != null) {
g = security.getThreadGroup();
}
/* If the security doesn't have a strong opinion of the matter
use the parent thread group. */
if (g == null) {
g = parent.getThreadGroup();
}
}
模拟简易的new Thread(Runnable target)机制
package com.pero.thread_;
/**
* @author Pero
* @version 1.0
*/
//线程代理类ThreadProxy
public class ThreadProxy implements Runnable{
public static void main(String[] args) {
Dog dog = new Dog();
ThreadProxy threadProxy = new ThreadProxy(dog);
threadProxy.start();
}
Runnable target = null;
//将实现Runnable接口的对象传入构造器
public ThreadProxy(Runnable target) {
this.target = target;
}
@Override
public void run() {
if (target != null){
target.run(); //动态绑定(运行类型是Dog)
}
}
public void start(){
start0(); //在Thread类中实现多线程方法
}
public void start0(){
run();
}
}
class Dog implements Runnable{
@Override
public void run() {
}
}
多线程执行
package com.pero.thread_;
/**
* main方法启动两个子线程
* @author Pero
* @version 1.0
*/
public class Thread03 {
public static void main(String[] args) {
Panda panda = new Panda();
Fish fish = new Fish();
Thread thread = new Thread(panda);
Thread thread1 = new Thread(fish);
thread.start();
thread1.start();
}
}
class Animal{ }
class Panda extends Animal implements Runnable{
int count = 0;
@Override
public void run() {
while (true){
System.out.println("熊猫睡懒觉"+ (++count)+Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (count == 80){
break;
}
}
}
}
class Fish extends Animal implements Runnable{
int count = 0;
@Override
public void run() {
while (true){
System.out.println("鱼儿在水里游"+(++count)+Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (count == 100){
break;
}
}
}
}
继承Thread与实现Runnable的区别
通过继承Thread类或者实现Runnable接口来创建线程,本质上没有区别,Thread本身就实现了Runnable接口,并通过调用start()方法来执行start0()方法,从而实现run()方法的内容。实现Runnable接口方式更加适合多个线程共享一个资源的情况,并且避免了单线程的限制。
代码测试(继承Thread类)
package com.pero.thread_;
/**
* 模拟售票窗口(100张门票),分别用继承Tread类和实现Runnable接口方式
*
* @author Pero
* @version 1.0
*/
public class SellTicket {
public static void main(String[] args) {
//抢占资源,票数的剩余数据出现异常
Ticket02 ticket01 = new Ticket02();
ticket01.start();
Ticket02 ticket02 = new Ticket02();
ticket02.start();
Ticket02 ticket03 = new Ticket02();
ticket03.start();
}
}
class Ticket02 extends Thread {
public static int ticketNum = 100;
@Override
public void run() {
while (true) {
if (ticketNum <= 0) {
System.out.println("票已售完");
break;
}
//如果还有票,卖出前休眠1秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("窗口:" + Thread.currentThread().getName() +
"售出了1张票" + "剩余票数" + (--ticketNum));
}
}
}
代码测试(实现Runnable接口)
package com.pero.thread_;
/**
* @author Pero
* @version 1.0
*/
public class SellTicketRunnable {
public static void main(String[] args) {
SellTicket01 sellTicket01 = new SellTicket01();
new Thread(sellTicket01).start(); //共用一个对象资源,依旧超卖
new Thread(sellTicket01).start();
new Thread(sellTicket01).start();
}
}
class SellTicket01 implements Runnable{
int count = 100;
@Override
public void run() {
while (true){
if (count <= 0){
System.out.println("票已售罄");
break;
}
//售票前休息一分钟
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("窗口:"+Thread.currentThread().getName()+
"售出1张票"+"剩余票数:"+(--count));
}
}
}
线程终止
1)当线程完成任务后,会自动退后。
2)还可以通过使用变量来控制run方法退出的方式停止线程,即通知方式。
package com.pero.thread_;
/**
* @author Pero
* @version 1.0
*/
public class ThreadExit_ {
public static void main(String[] args) {
System.out.println("主线程已开启");
T t = new T();
new Thread(t).start();
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("主线程休眠10秒钟结束");
//如果main线程控制其他线程终止,设置一个可修改的变量
t.setLoop(false);
}
}
class T implements Runnable {
public boolean loop = true;
private int count = 0;
@Override
public void run() {
while(loop){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("线程:"+Thread.currentThread().getName()+"第"+(++count)+"执行");
}
}
public void setLoop(boolean loop) {
this.loop = loop;
}
}
线程常用方法
1)setName()方法,设置线程名称,使之与参数name相同;
2)getName()方法,返回该线程名称;
3)start()方法,使该线程开始执行,java虚拟机底层调用该线程的start0()方法;
4)run()方法,调用该线程对象run()方法;
5)setPriority()方法,更改线程的优先级;
6)getPriority()方法,获取该线程的优先级;
7)sleep()方法,在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)
8)interrupt()方法,中断线程
package com.pero.thread_;
/**
* @author Pero
* @version 1.0
*/
public class ThreadMethod01 {
public static void main(String[] args) {
Test test = new Test();
test.setName("pero");
test.setPriority(Thread.MIN_PRIORITY);
test.start();
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName()+"休眠10秒");
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()+"执行中"+i);
}
test.interrupt();
}
}
class Test extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"执行中");
}
try {
System.out.println(Thread.currentThread().getName()+"休眠20秒");
Thread.sleep(20000);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()+"被唤醒");
}
}
}
9)yield()方法,线程的礼让。让出CPU,让其他线程执行,但礼让时间不确定,所以也不一定礼让成功;本线程中的类语句中写出。
10)join()方法,线程的插队。插队的线程一旦插队成功,则肯定先执行完插入的线程的所有任务;让别人插进来,在被插队的线程中的类语句中写出
yield()方法礼让代码
package com.pero.thread_;
/**
* @author Pero
* @version 1.0
*/
public class Test02 {
public static void main(String[] args) throws InterruptedException {
Test03 test03 = new Test03();
test03.start();
for (int i = 1; i <= 20; i++) {
Thread.sleep(1000);
System.out.println("主线程执行第"+i+"次");
if (i==5){
System.out.println("子线程插队先执行");
//test03.join(); //插队,子线程插队,子线程先执行完毕后主线程在执行
Thread.yield(); //礼让不一定成功,CUP资源丰富的情况下,礼让无法显现出来
System.out.println("主线程开始执行");
}
}
}
}
class Test03 extends Thread {
@Override
public void run() {
for (int i = 1; i <= 20; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("子线程执行了:" + i + "次");
}
}
}
join()方法插队代码
package com.pero.thread_;
/**
* @author Pero
* @version 1.0
*/
public class Test02 {
public static void main(String[] args) throws InterruptedException {
Test03 test03 = new Test03();
test03.start();
for (int i = 1; i <= 20; i++) {
Thread.sleep(1000);
System.out.println("主线程执行第"+i+"次");
if (i==5){
System.out.println("子线程插队先执行");
test03.join();
System.out.println("主线程开始执行");
}
}
}
}
class Test03 extends Thread {
@Override
public void run() {
for (int i = 1; i <= 20; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("子线程执行了:" + i + "次");
}
}
}
用户线程和守护线程
1)用户线程:也称工作线程,线程任务执行完毕或者以通知的方式结束;
2)守护线程:一般是为工作线程服务的,当所有的用户线程结束,守护线程会自动结束(常见的守护线程是垃圾回收机制)。
package com.pero.thread_;
/**
* 当所有的用户线程结束,守护线程会自动结束
*
* @author Pero
* @version 1.0
*/
public class ThreadMethod03 {
public static void main(String[] args) throws InterruptedException {
CycleThread cycleThread = new CycleThread();
Thread thread = new Thread(cycleThread);
//子线程变成守护线程,需要设置在启动线程之前
// 当所有的用户线程结束,守护线程会自动结束
thread.setDaemon(true);
thread.start();
for (int i = 1; i <= 10; i++) {
System.out.println("主线程执行第"+i+"次");
Thread.sleep(1000);
}
System.out.println("主线程执行完毕,子线程守护线程机制触发");
}
}
class CycleThread implements Runnable{
@Override
public void run() {
while (true){
try {
System.out.println("循环线程正在执行");
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
线程的生命周期
- 线程状态。 线程可以处于以下状态之一:
NEW
尚未启动的线程处于此状态。()RUNNABLE
在Java虚拟机中执行的线程处于此状态。(Ready和Running)BLOCKED
被阻塞等待监视器锁定的线程处于此状态。WAITING
正在等待另一个线程执行特定动作的线程处于此状态。TIMED_WAITING
正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。TERMINATED
已退出的线程处于此状态。
测试代码
package com.pero.thread_;
/**
* @author Pero
* @version 1.0
*/
public class ThreadState {
public static void main(String[] args) throws InterruptedException {
Test04 test04 = new Test04(); //状态:NEW
System.out.println(test04.getName()+"状态:"+test04.getState());
test04.start(); //状态:RUNNABLE
while (Thread.State.TERMINATED != test04.getState()){
System.out.println(test04.getName()+"状态:"+test04.getState());
Thread.sleep(500);
}
System.out.println(test04.getName()+"状态:"+test04.getState()); //状态:TERMINATED
}
}
class Test04 extends Thread{
@Override
public void run() {
while (true){
for (int i = 1; i <= 10; i++) {
System.out.println("线程:"+Thread.currentThread().getName()+"执行第"+i+"次");
try {
Thread.sleep(1000); //状态TIME_WRITING
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
break;
}
}
}
线程的同步
在多线程编程情况下,一些数据不允许被多个线程同时访问,此时就是用同步访问技术,保证数据在任何同一时刻,最多只能被一个线程访问,以保证数据的完整性;线程同步,也可以理解为,当有一个线程在对内存操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作。
同步具体方法-Synchronized
1.同步代码块
synchronized (对象) { //得到对象的锁才能操作同步代码
//需要被同步的代码
}
2.synchonized可以放在方法声明中,表示整个方法为同步方法
public synchronized void m(){
//需要被同步的代码
}
代码测试
package com.pero.thread_;
/**
* 解决售票问题同步的解决
*
* @author Pero
* @version 1.0
*/
public class SellTicketTest {
public static void main(String[] args) {
SellTicket02 sellTicket02 = new SellTicket02();
new Thread(sellTicket02).start();
new Thread(sellTicket02).start();
new Thread(sellTicket02).start();
}
}
class SellTicket02 implements Runnable {
private static int ticketNumber = 100; //共享数据(静态的)
private boolean loop = true;
@Override
public void run() { //同步方法
while (loop) {
sell();
}
}
public synchronized void sell() {
//因为多线程同步执行run()方法,不能够直接在run()方法上直接用synchronized修饰
//添加一个执行动作的方法,用synchronized修饰该方法,线程进入run()方法后调用
//本方法,就会执行同步操作
if (ticketNumber <= 0) {
System.out.println("售票结束");
loop = false;
return; //把break改为return;
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("窗口" + Thread.currentThread().getName() +
"售出1张票,剩余票数为:" + (--ticketNumber));
}
}
3.同步原理
多个线程同时想执行被synchronized修饰的方法或者代码块,首先访问锁(该锁放在对象之上),同一时间只有一个线程能够获得锁并执行代码块中的内容,其他线程被阻塞,待执行线程结束后,多个线程又进入锁获得的步骤(如果是公平锁则分配,如果是非公平锁则是非分配方式)。
互斥锁基本介绍:(可以在代码块上加锁,也可以在方法上加锁)
1)java中引入了对象互斥锁概念,来保证共享数据操作的完整性;
2)每个对象都对应一个可称为互斥锁的标记,用来保证在任意时刻只能有一个线程可以访问该对象;
3)关键字synchronized来与对象的互斥锁联系。当某个对象用synchronized修饰时,表明该对象的任意时刻只能由一个线程访问;
4)同步的局限性:导致程序的执行效率要降低;
5)同步方法(非静态的)的锁可以时this,也可是其他对象(要求是同一个对象);
6)同步方法(静态的)的锁为当前类本身。
在方法上加锁(非静态):
public synchronized void sell() { //同步方法,该锁在this对象中
if (ticketNumber <= 0) {
System.out.println("售票结束");
loop = false;
return; //把break改为return;
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("窗口" + Thread.currentThread().getName() +
"售出1张票,剩余票数为:" + (--ticketNumber));
}
在方法上加锁(静态):
//静态同步方法的锁是加在SellTicket02.class上的
//静态方法内部实现同步代码块,锁是加在SellTicket02.class对象上的
public /*synchronized*/ static void synchronizedAction(){
synchronized (SellTicket02.class){
}
}
在代码块上加锁:
Object object = new Object();
public /*synchronized*/ void sell() {
//同步代码块,所在this对象上(也可以在别的同一个对象上例如object,但是不能再多个对象上)
synchronized(/*this*/ object) {
if (ticketNumber <= 0) {
System.out.println("售票结束");
loop = false;
return; //把break改为return;
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("窗口" + Thread.currentThread().getName() +
"售出1张票,剩余票数为:" + (--ticketNumber));
}
}
注意细节:
1)同步方法如果没有使用static修饰,默认锁对象this;
2)如果方法使用static修饰,则默认锁对象为当前类.class
3)实现的落地步骤:①需要先分析上锁的代码;②选择同步代码块(效率相对来说比较高)或者同步方法;③要求多个线程的锁对象为同一个。
线程的死锁
多个线程都占用了对方的锁资源,但不肯相让,导致了死锁,在编程时要避免死锁的发生。
package com.pero.thread_;
/**
* 模拟死锁
*
* @author Pero
* @version 1.0
*/
public class DeadLock_ {
public static void main(String[] args) {
}
}
class DeadLockDemo extends Thread {
static Object object01 = new Object();
static Object object02 = new Object();
boolean flag;
public DeadLockDemo(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
//存在问题:
// 当一个线程中flag判断结果为true,线程持有object01的互斥锁,
//但是进入同步代码块的执行语句时,如果object02的互斥锁没有得到就会停滞在
//BLOCKED状态(阻塞中)。
//当另外一个线程中flag结果判定为false,线程持有object02的互斥锁,
//但是进入同步代码块的执行语句时,如果object02的互斥锁没有得到就会停滞在
//BLOCKED状态(阻塞中)。
//获得锁的线程无法执行完成代码内容,无法返回锁,则两个线程都会一直停滞在阻塞状态
if (flag) {
synchronized (object01) { //同步代码,对象互斥锁
System.out.println(Thread.currentThread().getName() + "进入1");
synchronized (object02) {
System.out.println(Thread.currentThread().getName() + "进入2");
}
}
}else {
synchronized (object02){
System.out.println(Thread.currentThread().getName() + "进入3");
synchronized (object01){
System.out.println(Thread.currentThread().getName() + "进入4");
}
}
}
}
}
释放锁的情况
1)当线程的同步方法、同步代码执行结束后会释放锁;
2)当线程在同步代码块、同步方法中遇到break和return会释放锁;
3)当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束会释放锁;
4)当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停并释放锁。
不会释放锁的情况
1)当线程执行同步代码块或者同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行,不会释放锁;
2)线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁。