1.多线程介绍

Java对多线程与并发支持是非常友好,不仅提供了各种并发容器、锁机制实现,在JUC(java.util.concurrent)包提供了大量并发工具。使用多线程真的会比单线程执行效率更高吗?实际测试在数据量较少的情况下单线程的执行效率甚至有时比多线程高,产生原因可能是因为多线程运行时频繁进行上下文切换或使用不当造成执行效率低下,多线程是一把双刃剑,如何使用好它是一门学问。

1.1 进程、线程、协程的概念?

  • 进程(process):进程是指在系统中正在运行的一个应用程序,程序一旦运行就是进程。进程是系统进行资源分配的独立实体, 且每个进程拥有独立的地址空间。例如运行在电脑上的钉钉、QQ就是一个进程,一个进程可以包含数百个线程。
  • 线程(Thread):线程是操作系统进行运算的最小调度单位。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。线程拥有独立的内存空间,当线程需要获取其他线程的数据就需要线程通讯,线程下面还有更轻量的协程,一个线程可以包含数百个协程,go语言中Goroutine就是协程的实现。
  • 协程(Coroutines):是一种基于线程之上,但又比线程更加轻量级的线程(协程又被称为Fiber(纤程)),这种由开发者写程序来管理的轻量级线程叫做用户空间线程,具有对内核来说不可见的特性。

1.2 为什么要使用多线程?

多线程的优点:

  • 发挥CPU多核的优势,资源率利用更好。现代计算机都是采用多线程架构,如果使用单线程则会造成其他CPU核心空闲,无法发挥CPU多核优势。假如我们要计算一个复杂的任务,我们只用一个线程的话,CPU 只有一个 CPU 核心被利用到,而创建多个线程就可以让多个 CPU 核心被利用到,这样就提高了 CPU 的利用率。
  • 防止阻塞(异步或并行执行),提高性能。从程序执行效率来看,单核CPU并不能发挥多线程的优势,反而在单核CPU运行的多线程导致线程上下文的切换,从而影响执行效率。假设单核CPU使用单线程执行某个任务,如果一旦该线程发生阻塞就会影响后续任务的执行效率,而使用多线程并行执行任务能解决线程的阻塞。
  • 便于建模。假设有一个大任务需要执行,如果使用单线程建立整个程序模型就显得尤为麻烦,如果使用多线程将一个大任务拆分为多个子任务,每个子任务由不同的线程去建立程序模型并执行,那么大大降低程序的复杂度。

多线程的缺点:

  • 增加资源消耗。线程在运行的时,需要从计算机里获取一些资源,除了CPU,线程还需要一些内存来维持它本地的堆栈,还需要占用操作系统中的一些资源来管理线程。多线程也会增加上下文切换的开销,当CPU从执行一个线程切换到另外一个线程的时候,它需要存储当前线程的本地数据,程序指针等,然后载入另一个线程的本地数据,程序指针等,最后才开始执行,这种切换称为”上下文切换”CPU会在一个上下文中执行一个线程,然后切换到另外一个上下文中执行另一个线程。上下文的切换非常耗费系统资源,如果没有必要,应该减少上下文切换的发生。
  • 多线程可能会造成死锁。多个线程加锁竞争同一资源时,如果获取锁的线程无法及时释放锁,那么会导致其他线程无法获取锁资源,从而导致一直阻塞,发生死锁现象。
  • 多线程可能会造成线程安全问题。多个线程共享同一全局资源时,可能会发现数据安全问题,操作后的数据与预期数据不符,这就是线程安全问题。线程安全问题的解决办法有同步机制(synchronized)、线程本地存储(ThreadLocal)、加锁等方案。

1.3 并行、并发、异步概念

2.多线程基础

Thread(java.lang.Thread)类是程序中的执行线程,Java虚拟机允许应用程序同时运行多个执行线程。Thread API如下:

  1. int getPriority():获取当前线程优先级,优先级越高越容易先被执行,线程优先级默认为5,优先级最低是1,最高是10
  2. int getId():获取当前线程id
  3. String getName():获取当前线程名称。getId()和getName()通常用于线程调试。
  4. State getState():获取当前线程状态,返回线程状态枚举类,该枚举类定义在Thread类内部。
  5. ThreadGroup getThreadGroup():获取当前线程所属线程组,返回一个ThreadGroup对象。默认线程组的名称为"system",优先级为10
  6. StackTraceElement[] getStackTrace():获取当前线程堆栈信息,返回一个StackTraceElement数组,该数组包含了当前线程堆栈。

2.1 创建线程的几种方式

2.1.1 继承Thread类重写run方法(不推荐)

实例化一个对象Thread就会创建一个线程, 重写的run()用于描述线程启动后的行为,Thread类的start()用于启动线程,实际上Thread类也实现了Runnable接口。不推荐继承Thread类创建线程,因为Java是采用单继承多实现的机制。

package com.fly.one;

public class Example {

    public static void main(String[] args) {
        // 实例化MyThread
        MyThread myThread = new MyThread();
        // 启动线程
        myThread.start();
    }

    // 声明一个静态内部类继承自Thread并重写run(),run()用于指定线程执行的任务
    public static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("run thread..."); // run thread...
        }
    }
}

2.2.2 实现Runable接口重写run方法(无需返回值时推荐)

Thread构造函数接收一个Runnable接口类型的参数,将实现了Runnable接口的类实例传入Thread构造方法就能得到一个Thread实例,通过调用Thread实例的start()即可启动线程。无需返回值时推荐使用Runable创建线程。

package com.fly.one;
public class Example {
    public static void main(String[] args) {
        // 实例化Thread,将实现Runnable接口传入作为Thread的构造函数
        Thread thread = new Thread(new MyThread());
        // 启动线程
        thread.start();
    }
    // 声明一个静态内部类实现Runnable接口并重写run(),run()用于指定线程执行的任务
    public static class MyThread implements Runnable {
        @Override
        public void run() {
            System.out.println("run thread..."); // run thread...
        }
    }
}

Runnable接口在JDK8中被设计为一个函数式接口(FunctionalInterface),只包含一个run()抽象方法,Runnable接口源码如下:

// @FunctionalInterface用于标识当前接口是一个函数接口,即接口里面只能有一个抽象方法
@FunctionalInterface 
public interface Runnable {
    public abstract void run();
}

由于Runnable是一个空接口所以可以使用Lambda写法。

package com.fly.one;

public class Example {

    public static void main(String[] args) {
        /**
         * 实例化Thread。Lambda写法,Runnable接口的run()采用箭头函数
         */
        Thread thread = new Thread(() -> {
            System.out.println("run thread..."); // run thread...
        });
        // 启动线程
        thread.start();
    }
}

2.2.3 实现Callable接口重写call()方法(需要返回值时推荐)

FutureTask的构造函数允许接收一个Callable接口类型的参数,通过FutureTask实例的run()就可以启动线程。FutureTask表示一个未来的任务,通常指异步任务。

package com.fly.one;

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Example {
    public static void main(String[] args)  {
        Example.method01();
        Example.method02();
        Example.method03();
    }

    // 普通写法
    public static void method01(){
        // 将实现Callable接口的类传入FutureTask构造函数以创建FutureTask实例
        FutureTask task = new FutureTask(new MyCallable());
        task.run();
    }

    // Lambda写法
    public static void method02(){
        FutureTask task = new FutureTask(()->{
            System.out.println("call thread..."); // call thread...
            return null;
        });
        task.run();
    }
    // 获取Callable接口call()返回值
    public static void method03() {
        Integer[] nums= {1,2,3,4,5,6,7,8,9,10};
        FutureTask<List<Integer>> task = new FutureTask(()->{
            Stream<Integer> integerStream = Arrays.stream(nums).filter(item -> item % 2 == 0);
            return integerStream.collect(Collectors.toList());
        });
        task.run();
        try{
            // 通过get()获取call()返回值
            System.out.println("执行结果:"+task.get()); // 执行结果:[2, 4, 6, 8, 10]
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    // Callable接口可抛出异常并且有返回值
    public static class MyCallable implements Callable{
        @Override
        public Object call() throws Exception {
            System.out.println("call thread..."); // call thread...
            return null;
        }
    }
}

Runnable与Callable区别:

  • Callable接口的call方法具有返回值,而Runnable的run方法无返回值。Callable的返回值可以调用FutureTask.get()得到。
  • Callable接口的call方法可以抛出异常,而Runnable的run方法不能抛出异常。Runnable run()中抛出的异常只能在内部消化。

    2.2.4 使用线程池创建线程(频繁创建线程时推荐)

    当频繁创建或销毁线程时推荐使用线程池管理,因为频繁创建或销毁线程会造成巨大开销,使用线程池利用池化技术能减少创建或销毁线程的开销,池化技术的典型代表技术连接池、线程池、缓冲池。

Java提供了ExecutorService框架用于创建线程池,ExecutorService继承自Executor,用于提供管理Executor终止方法并可以生成Future用于跟踪一个或多个异步任务的进度的方法。Executor类提供了不同策略的线程池,例如newFixedThreadPool(创建固定线程数的线程池)、newSingleThreadExecutor(创建单个线程数的线程池)、newCachedThreadPool(创建具有缓存作用的线程池)、newScheduledThreadPool(创建具有线程调度策略的线程池)。本节并不会过多赘述线程池的细节部分,仅介绍如何使用线程池创建线程。线程池可以通过submit()或execute()传入一个Runnable接口来描述线程执行的任务。submit()不仅可以传入Runnable接口,也可以传入Callable接口,submit()的返回值类型是Future,而execute()无返回值。

package com.fly.one;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Example {
    public static void main(String[] args)  {
        ExecutorService executor = Executors.newCachedThreadPool();
        // 通过submit()提交一个Runnable接口
        executor.submit(()->{
            System.out.println("run thread..."); // run thread...
        });
        // 关闭线程池
        executor.shutdown();
    }
}

2.2 线程的状态

Thread类内部定义了一个枚举类用于枚举线程的生命周期,源码如下:

public class Thread{
    // ...省略其他代码
    public enum State {
        NEW,
        RUNNABLE,
        BLOCKED,
        WAITING,
        TIMED_WAITING,
        TERMINATED;
    }
}
  • NEW(新建状态):线程对象被创建时就会进入新建状态,例如:Thread thread=new Thread()
  • (就绪状态):也被称为”可执行状态”,当线程调用start()方法就会进入就绪状态,从而等待CPU的调度执行,例如:thread.start()。
  • RUNNABLE(运行状态):线程获取CPU的权限开始执行,注意的是线程只能从就绪状态进入运行状态。
  • BLOCKED(阻塞状态):阻塞状态是因为某种原因导致线程放弃CPU的执行,暂停线程的运行。知道线程进入就绪状态,然后就绪状态切换至运行状态线程才会执行。产生线程阻塞的3中原因:
    • 等待阻塞 — 通过调用线程的wait()方法,让线程等待某工作的完成。
    • 同步等待 — 线程获取synchronized同步锁失败(锁被其他对象引用了,未释放锁),从而导致线程阻塞
    • 其他阻塞 — 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
  • TERMINATED(死亡状态):当线程执行结束或执行出现异常退出run()方法,该线程退出生命周期。

    2.3 线程种类

    Java的线程种类可分为Daemon Thread(守护线程)和User Thread(用户线程,或叫非守护线程)。简单来说,守护线程是用户线程的保姆,在JVM(Java虚拟机)实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作,只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器)。守护线程也并非只有JVM内部提供,用户也可以将用户线程设置为守护线程。 ```java Thread daemonTread = new Thread();

// setDaemon(true)将当前线程设置为守护线程,setDaemon(false)将当前线程设置为用户线程 daemonThread.setDaemon(true);

// 判断当前线程是否是守护线程,返回true则是守护线程,否则是用户线程 daemonThread.isDaemon();

注意:

- thread.setDaemon(true)必须在thread.start()之前设置,否则会抛出一个IllegalThreadStateException异常。
- 在Daemon线程中产生的新线程也是Daemon的。 
- 不要认为所有的应用都可以分配给Daemon来进行服务,比如读写操作或者计算逻辑。 
<a name="ZgIDm"></a>
### 2.4 线程优先级
<a name="sPufi"></a>
#### 1.6 线程的三个特性
**(1).原子性**<br />线程的原子性是指在一次或多次的操作中,不受任何因素干扰,要么全部执行成功,要么全局执行失败。例如数据库的事务实现了原子性。<br />**(2).可见性**<br />在多线程环境下操作同一个全局变量,线程之间都是不可见(线程A做了什么操作线程B是不知道的),而线程的可见性是指多个线程操作同一个全局变量,其他线程能立马看到被修改的最新值。如果要深刻理解线程的可见性那么学习JMM(Java Memory Model,中文意思是Java内存模型)是必不可少的。<br />**(3).有序性**<br />在jVM编译运行时,动态编译器(jit:just in time)会对我们的代码进行优化,也就是指令重排序(指令重排序是指:程序的代码顺序跟实际执行顺序不一致)。
```java
int a=10;
int b=20;

上面的代码声明了2个变量,变量之间都没有依赖关系,在编译优化时可能int b=20;会先执行,int a=10;可能会后执行。

int a=10;
int b=a;

上面的代码声明了2个变量,变量之间是有依赖关系的,b依赖于a,所以在编译优化时会先执行int a=10;后将a的值再赋值给b变量。

1.7 使用多线程产生的问题及解决方案。

使用多线程会造成线程安全问题,线程安全问题是指多个线程共享同一全局变量导致的数据不一致问题,简单来说单线程和多线程执行的结果不一样。例如:设置一个全局变量int i=0;在单线程环境for循环累加1000次,运行多次的结果都是1000;但在多线程环境下for循环累加1000次的结果可能不是1000(运行多次看效果),原因是线程的可见性,因为线程是不可见的,假设当线程A修改i为1,此时经过CPU调度切换到线程B,线程B开始执行i+1,这时线程B是不知道线程A将i修改为1(线程之间不可见),线程B还认为i等于0,所以执行i+1操作,如此执行下来就会出现线程安全问题。
解决方法:
(1).使用synchronzied关键字。synchronzied能保证多个线程之间的互斥(同一时间某个线程获取到同步锁,其他线程进行阻塞,直到获取到锁的线程释放锁,然后再进行锁的竞争),无法禁止重排序,能保证线程的可见性,synchronzied会导致线程阻塞,从而影响执行效率。
(2).使用lock锁。
(3).在全局共享加上volatile关键字,保证线程之间的可见性,禁止指令重排序,但不保证线程的原子性。
(4).使用ThreadLocal存储全局共享数据,ThreadLocal是线程内部的存储类,通过它可以在指定的线程中存储数据,数据存储以后,只有指定的线程才可以获取存储的数据,其它线程则无法获取到数据。
(5).CAS无锁。

3.多线程的通讯

线程大致分为新建、就绪、运行、阻塞(等待)、死亡五种状态,如果一个线程孤立的运行那么它没有一点价值,线程之间的相互协作能使多线程带来巨大的价值,下面将介绍线程相关的API:

//---------------- Thread静态方法:
static native void sleep(long millis):使当前线程睡眠millis毫秒,使用sleep()不会使当前线程释放锁资源。

static native Thread currentThread():返回当前线程的引用。

static native void yield():使当前线程放弃cpu的控制权。有可能立刻又被重新选中继续执行,也可能给优先级更高的线程机会。

static boolean interrupted():检测当前线程是否已经中断(调用该方法后后就将该线程的中断标志位设置位false,所以连续两次调用该方法第二次肯定时false)。


//---------------- Thread实例方法:
final void join():在一个线程中使用另一个线程调用join(),会使这个线程进入等待状态,直到另一个线程执行结束。例如在线程a中调用线程b.join(),会导致线程a阻塞,直到线程b执行结束后才会恢复原状。

final synchronized void join(long millis):在一个线程中调用另一个线程的join(),会导致这个线程阻塞,直到另一个线程执行结束,如果到了设定的等待时间,即使另一个线程未执行结束,这个线程也会被唤醒。

void interrupt():将该线程中断(实际并不会中断,只是将中断标志设置为true),如果线程正处在sleep(),join(),wait()方法中时(也就是正在阻塞中)调用该方法,该方法会抛出异常。

boolean isInterrupted():检测该线程是否已经中断(对中断标志位不作处理)。

boolean isAlive():判断当前线程是否存活。

boolean isDaemon():判断当前线程是否是守护线程,通过通过setDaemon(true)将当前线程设置为守护线程。

final String getName():获取当前线程名称。

long getId():获取当前线程id。

State getState():获取当前线程的状态。State是一个枚举类,枚举项有NEW(新建)、RUNNABLE(运行中)、BLOCKED(阻塞)、WAITING(等待)、TIMED_WAITING(定时等待)、TERMINATED(结束)

final ThreadGroup getThreadGroup():获取当前线程所属线程组。

final int getPriority():获取当前线程的优先级,默认为5。

StackTraceElement[] getStackTrace():获取当前线程堆栈信息。

//---------------- Object实例方法(Java所有类都直接或间接的继承于Object类,所以其他类可以使用Object上的方法):
final native void wait(long timeout):使当前线程进入等待状态,可以指定当前线程等待时长,当等待时长结束后或调用notify、notifyAll()方法或当前线程中断,会使当前线程从等待状态变为wait()之前的原来的状态。注意:wait()方法只能在synchronized代码块中使用,当调用wait()进行线程等待时,必须要获取锁对象的控制权(对象监视器),例如synchronized (o){},o就是synchronized中所加的锁,所以调用wait()方法时必须使用o.wait(),否则将会抛出java.lang.IllegalMonitorStateException,表示非法监视器状态异常。调用wait()会释放锁资源。

final native void notify():随机唤醒一个处于等待状态的线程。当notify()与wait()配合使用时必须使用同一把对象锁,否则抛出java.lang.IllegalMonitorStateException。

final native void notifyAll():唤醒在同一个等待阻塞池中所有等待的线程。使用方式与notify()一样。

3.1 sleep()

3.2 yield()

//未使用Thread.yiled()
public static void test(){
    Thread thread01 = new Thread(() -> {
        for (int i = 0; i < 5; i++) {
            System.out.println("t1 i:" + i);
            if (i == 2) {
               System.out.println("t1 i==2");
            }
        }
    }).start();

   Thread thread01 = new Thread(() -> {
        for (int i = 0; i < 5; i++) {
            System.out.println("t2 i:" + i);
            if (i == 2) {
               System.out.println("t2 i==2");
            }
        }
    }).start();
}
/*执行结果为:
t1 i:0
t1 i:1
t1 i:2
t1 i==2
t1 i:3
t1 i:4
t2 i:0
t2 i:1
t2 i:2
t2 i==2
t2 i:3
t2 i:4
*/

//使用Thread.yiled()
public static void test(){
    Thread thread01 = new Thread(() -> {
        for (int i = 0; i < 5; i++) {
            System.out.println("t1 i:" + i);
            if (i == 2) {
               System.out.println("t1 i==2");
               Thread.yield(); //使当前线程交出cpu控制权给其他线程(t2线程)执行
            }
        }
    }).start();

   Thread thread01 = new Thread(() -> {
        for (int i = 0; i < 5; i++) {
            System.out.println("t2 i:" + i);
            if (i == 2) {
               System.out.println("t2 i==2");
               Thread.yield(); //使当前线程交出cpu控制权给其他线程(t1线程)执行
            }
        }
    }).start();
}

/** 执行结果为:
t1 i:0
t1 i:1
t1 i:2
t1 i==2
t2 i:0
t2 i:1
t1 i:3
t2 i:2
t1 i:4
t2 i==2
t2 i:3
t2 i:4
*/

解析:上面示例中使用了2个线程分别打印i循环5次的值,当i==2时会额外打印一句话,在未使用yield()示例中结果是顺序输出的,即t1线程执行完然后再执行t2线程,但在yield()示例中t1线程启动后拥有了cpu的控制权(打印t1 i:0、t1 i:1、t1 i:2、t1 i==2),当t1线程执行到if(i==2)中的Thread.yield()会导致t1线程交出cpu的控制权给其他线程,此时t2线程拥有cpu的控制权,当t2线程执行到if(i==2)中的Thread.yield()时会导致t2线程交出cpu控制权

3.3 wait()

//

3.4 notify()

3.5 notifyAll()

4.线程安全问题及解决办法

4.1 什么是线程安全问题?

线程安全问题是指多个线程共享同一全局或静态变量,在做写操作时(读操作不会发生线程安全),发生的数据不一致性问题,简单来说同样的数据在多线程环境下与单线程环境下结果不一致性。下面通过一个窗口买票的案例来还原线程安全问题。

public class Test02 {

    public static int count=10;
    public static void main(String[] args) {
        method01();
    }

    /**
     * 模拟窗口买票出现线程安全问题,运行多次会出现一个窗口卖了同样的票,打印结果如下,此时就出现了线程安全问题。
     */
    public static void method01(){
        Runnable runnable= () -> {
            while (count>0){
                try{
                    decrement();
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        };
        Thread threadA = new Thread(runnable, "线程A");
        Thread threadB = new Thread(runnable, "线程B");
        threadA.start();
        threadB.start();
    }

    public static void decrement(){
       if(count>0){
           --count;
           System.out.println(Thread.currentThread().getName()+"正在卖第"+count+"张票");
       }
    }
}

运行五次结果如下:

// 第一次,有重卖
线程B正在卖第8张票
线程A正在卖第8张票
线程B正在卖第7张票
线程B正在卖第5张票
线程B正在卖第4张票
线程B正在卖第3张票
线程B正在卖第2张票
线程B正在卖第1张票
线程B正在卖第0张票
线程A正在卖第6张票

// 第二次,无重卖
线程A正在卖第9张票
线程A正在卖第7张票
线程A正在卖第6张票
线程A正在卖第5张票
线程A正在卖第4张票
线程A正在卖第3张票
线程A正在卖第2张票
线程B正在卖第8张票
线程A正在卖第1张票
线程B正在卖第0张票

// 第三次,有重卖
线程A正在卖第8张票
线程A正在卖第7张票
线程A正在卖第6张票
线程A正在卖第5张票
线程A正在卖第4张票
线程A正在卖第3张票
线程A正在卖第2张票
线程B正在卖第8张票
线程A正在卖第1张票
线程B正在卖第0张票

// 第四次,无重卖
线程A正在卖第9张票
线程A正在卖第7张票
线程A正在卖第6张票
线程A正在卖第5张票
线程A正在卖第4张票
线程A正在卖第3张票
线程B正在卖第8张票
线程A正在卖第2张票
线程B正在卖第1张票
线程A正在卖第0张票

// 第五次,有重卖
线程A正在卖第8张票
线程B正在卖第8张票
线程A正在卖第7张票
线程B正在卖第6张票
线程A正在卖第5张票
线程A正在卖第3张票
线程A正在卖第2张票
线程B正在卖第4张票
线程A正在卖第1张票
线程B正在卖第0张票

从上面结果来看发现有多个窗口有售卖重复的票,此时就出现了线程安全问题。Java支持多个线程同时访问一个对象或对象的成员变量,由于线程有自己的存储空间,所以每个线程都可以拥有对变量的一份拷贝,虽然对象及对象的成员变量分配的内存是在共享内存中的,但是每个执行的线程仍可以拥有一份拷贝,其目的是加速程序的执行效率,
所以在程序执行过程中,一个线程访问到的数据并不是最新的。

解决线程安全的常用方案有使用synchronized关键字加锁、使用volatile关键字修饰全局变量、CAS无锁机制等等,本节将会介绍synchronized和volatile解决线程安全问题与实现原理。

4.2 从JMM理解线程安全问题

4.3 synchronized同步机制

synchronized译为同步的意思,synchronized关键字可以修饰方法或以同步块的形式使用。

// 形式1:修饰方法,被修饰的方法称为同步方法
public static synchronized void test01(){
    // 代码...
}

// 形式2:同步代码块,同步代码块中需要传入一个锁对象,下面示例代码块传入的是Object.class,你也可以传入String.class、Integer.class等等,也可以传入一个实例对象,Object o=new Object();也可以把o传入进去。
synchronized(Object.class){
    // 代码...
}

4.3