重学 Java 基础之多线程基础(一)

2023-11-28 15:30:43 浏览数 (1)

1、多线程基础

1.1、什么是进程

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

狭义定义:进程是正在运行的程序的实例(an instance of a computer program that is being executed)。 广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。

重要概念

  • 每一个进程都有它自己的地址空间
  • 进行进程切换就是从正在运行的进程中收回处理器,然后再使待运行进程来占用处理器。
  • 单核 CPU 同一时刻只能运行一个线程,而进程是线程的容器。所以也是只能运行一个进程的。

调度算法:阿巴阿巴。

参考地址

1.2、什么是线程

线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。

线程状态
  • 创建(NEW):新创建了一个线程对象,但还没有调用start()方法。
  • 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。 线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
  • 阻塞(BLOCKED):表示线程阻塞于锁。
  • 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
  • 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
  • 终止(TERMINATED):表示该线程已经执行完毕。

参考地址

1.3、创建线程的方式
1.3.1、继承Thread类

步骤:

  • 继承 Thread
  • 重写 run 方法
  • 创建自定义类实例,运行 start() 方法启动线程。

示例:

代码语言:typescript复制
public class TestThread extends Thread{

    @Override
    public void run() {
        System.out.println(String.format("当前线程名称【%s】",Thread.currentThread().getName()));
    }

    public static void main(String[] args) {
        new TestThread().start();
    }
}
1.3.2、实现 Runnable 接口

步骤:

  • 实现 Runnable 接口
  • 重写 run 方法
  • 创建自定义类实例,创建Thread实例,将自定义类实例传入Thread实例构造器中,运行 start() 方法启动线程。

示例:

代码语言:typescript复制
public class TestRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println(String.format("当前线程名称【%s】",Thread.currentThread().getName()));
    }

    public static void main(String[] args) {
       new Thread(new TestRunnable()).start();
    }
}

也可以使用lambda表达式创建线程

代码语言:typescript复制
 public static void main(String[] args) {
        new Thread(()->{
            System.out.println(String.format("当前线程名称【%s】",Thread.currentThread().getName()));
        }).start();
    }
1.3.3、实现 Callable 接口

步骤:

  • 实现 Callable 接口
  • 重写 call() 方法
  • 创建自定义类实例,创建 FutureTask 实例,将自定义类示例传递给 FutureTask 实例的构造器,创建 Thread 实例,将 FutureTask 实例传递给 Thread 实例的构造器,运行 start() 方法启动线程。

示例:

代码语言:typescript复制
public class TestCallable implements Callable<Integer> {
    @Override
    public Integer call(){
        String name = Thread.currentThread().getName();
        System.out.println(String.format("当前线程名称【%s】",name));
        return 666;
    }

    public static void main(String[] args) {

        FutureTask<Integer> futureTask = new FutureTask<>(new TestCallable());
        new Thread(futureTask).start();

        try {
            Integer result = futureTask.get();
            System.out.println(String.format("线程返回的结果是%d",result));
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}
1.4、线程操作
1.4.1、线程休眠

** Thread.sleep()方法**

该方法的作用是让 Thread.currentThread()返回的线程暂停运行 n 毫秒。

示例:

代码语言:scss复制
 public static void main(String[] args) throws InterruptedException {
        System.out.println("当前执行线程的名称:" Thread.currentThread().getName());

        Thread.sleep(2000);

        new Thread(()->{
            System.out.println("线程名称:" Thread.currentThread().getName());
        }).start();
    }

主线程main先启动,然后由主线程去启动子线程,以上是先让主线程休眠2秒后再去启动子线程,如果没有休眠效果的话,程序就会立马先打印出 “线程名称XXX”,然后直接结束。

我们来思考一个问题,** 为什么 sleep() 方法会是静态的?**

我思考的是:为什么 sleep() 不是每个实例独有的,然后使用this.sleep() 就能让实例的线程休眠。sleep() 方法是让当前正在运行的线程暂停,但是在类的内部不能保证正在运行的就是当前实例的线程,下面就让我们来测试一下:

代码语言:c#复制
public class ThreadOperate {

    public static void main(String[] args) {
        new TestThread1().start();
    }


    public static class TestThread1 extends Thread {
        static {
            System.out.println("静态块线程名称:"   Thread.currentThread().getName());
        }

        @Override
        public void run() {
            System.out.println("线程名称:"   Thread.currentThread().getName());
        }
    }
}

# 打印结果

静态块线程名称:main
线程名称:Thread-0

我们发现静态块的代码是由主线程去执行。

我们有时候看多线程的代码会发现 Thread.sleep(0); 这个操作,让线程休眠 0 秒,这个操作的意义何在?以下的链接解释了这个的作用,我就不重复写了。

链接

不过以上链接的内容稍微有点问题,因为他说的是旧的window操作系统,现在的系统是抢占式多任务形式,一段时间后操作系统会剥夺线程的CPU使用权。具体可参照百度百科链接

** Thread.yield() **

当前方法的作用是让正在执行的线程放弃CPU资源。那它和sleep有什么区别呢?

yield会让出时间片让CPU去重新调度,但是sleep也会让出时间片让其他线程去执行。我们可以来测试一下。

代码语言:scss复制
public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            System.out.println("线程名称:"   Thread.currentThread().getName());
        }).start();
        Thread.sleep(10000);
        System.out.println(777);
    }

如果线程不会让出时间片,那么主线程会一直占有执行权,执行完毕后才会让其他线程继续执行,那么打印的顺序就会是 “777” 先打印,“线程名称” 后打印,但实际上的结果恰恰相反的。

那么实际的区别是什么呢?

1、sleep()方法声明抛出InterruptedException;而yield()方法没有声明任何异常 2、线程执行sleep()方法后转入阻塞(blocked)状态;执行yield()方法后进入就绪(ready)状态。 3、sleep()方法给其他线程运行机会时,不考虑线程的优先级,因此会给低优先级的线程运行机会;yield()方法只会给相同优先级或更高优先级的线程运行机会。

这些结论对不对还不知道,我们到了相关的知识点再进行测试。

1.4.2、线程中断

** interrupt() **

我们通过 interrupt() 去中断线程。但实际上它不会真正的去停止线程的运行,只是做了一个停止的标志。不信我们可以试试:

代码语言:typescript复制
 public static void main(String[] args) {
        Thread.currentThread().interrupt();
        System.out.println(666);
    }

如果它是终止线程的执行的话,那么 “666” 就不会被打印出来,但是它打印出来了,所以线程没有被终止。那为什么要有这个方法呢,直接 stop() 停止线程不香吗? 好吧,实际上在项目中还是建议用 interrupt() 方法的。为什么呢?

举个例子: 一辆小汽车要去洗车店洗车,内外都要清洗的。然后车已经到了洗车店,车里的人还没下来,但是店员已经拿着工具已经来准备洗车了,你是准备人跟车一起洗呢,还是一起洗呢?

stop() 就相当于->店员:你人不用下来了,和车一块洗了吧。而interrupt() 相当于->店员:我要开始洗车了。但是它还没动手,等着你自己做一些其他动作,比如:人从车上下来!

既然标识了停止状态,那么我们怎么判断线程到底有没有停止呢?

这就要说到 “Thread.interrupted() 与 Thread.currentThread().isInterrupted()” 了,用他们去获取线程的状态,那么他们有什么区别呢?

interrupted() 方法会清除停止标识的状态,而 isInterrupted() 是直接获取不做清除处理。我们可以看下相应的源码:

代码语言:typescript复制
public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }

public boolean isInterrupted() {
        return isInterrupted(false);
    }

可以发现 interrupted 实际上也是调用了 isInterrupted 方法,但是它传递了一个清除状态的参数过去,并且清除的是 “Thread.currentThread()” 也就是正在运行线程的状态,我们可以来试下:

代码语言:c#复制
public static void main(String[] args) {
        Thread.currentThread().interrupt();
        System.out.println(Thread.interrupted());
        System.out.println(Thread.interrupted());
    }

# 打印
true
false


public static void main(String[] args) {
        Thread.currentThread().interrupt();
        System.out.println(Thread.currentThread().isInterrupted());
        System.out.println(Thread.currentThread().isInterrupted());
    }

# 打印
true
true

public static void main(String[] args) {
        Thread.currentThread().interrupt();
        System.out.println(Thread.interrupted());
        System.out.println(Thread.currentThread().isInterrupted());
    }

# 打印
true
false

以上实验可以得出结论,interrupted() 方法会清除停止标识的状态,而 isInterrupted() 是直接获取不做清除处理(当然也可以自己新建一个线程对象,然后通过线程对象去interrupt()等,我是嫌弃麻烦,就直接获取主线程了)。再看下一个:

代码语言:c#复制
 public static void main(String[] args){
        Thread.currentThread().interrupt();
        TestThread1 thread = new TestThread1();
        thread.start();
        System.out.println("主线程结束");
        System.out.println(Thread.currentThread().isInterrupted());
    }


    public static class TestThread1 extends Thread{
        static {
            System.out.println(Thread.interrupted());
        }
        @Override
        public void run() {
            System.out.println("线程结束");
        }
    }

#打印
true
主线程结束
false
线程结束

可以知道 “Thread.interrupted()”清除的是正在运行的线程状态。

我们再来思考一个问题:如果线程interrupt后,在线程停止运行时打印出来的是true还是false呢?我们来看下:

代码语言:text复制
public class ThreadOperate {

    public static void main(String[] args) throws InterruptedException {
        TestThread1 thread = new TestThread1();
        thread.start();
        thread.interrupt();
        System.out.println(thread.isInterrupted());
        Thread.sleep(3000);
        System.out.println(thread.isInterrupted());
        System.out.println("主线程结束");
    }


    public static class TestThread1 extends Thread {

        @Override
        public void run() {
            System.out.println("线程结束");
        }
    }
}

#打印:
true
线程结束
false
主线程结束

我们发现,当线程结束后也会将状态清除掉。

最后我们再来验证一下上个部分的结论:sleep()方法声明抛出InterruptedException;而yield()方法没有声明任何异常。

代码语言:typescript复制
public static void main(String[] args) {
        try {
            Thread.currentThread().interrupt();
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

#打印

java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.rookie.stream.download.thread.ThreadOperate.main(ThreadOperate.java:11)

public static void main(String[] args) {
        try {
            Thread.sleep(1000);
            Thread.currentThread().interrupt();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

#没打印

 public static void main(String[] args) {
        Thread.currentThread().interrupt();
        Thread.yield();
        
    }

看来这个结论没问题。

说到底 interrupt 是标记清除状态,然后我们通过去判断这个状态的同时去做一些后续清理操作,然后再手动停止线程。比如在线程的run方法里面判断状态,状态为真时,清理,然后return结束线程。

** wait() 与 notify() **

我就写个标题~具体内容在 “锁” 章节讨论。

1.4.3、线程优先级

优先级

每个线程都具有优先级,范围是1-10,优先级越高,线程就可能越先运行。当然这个跟CPU调度有关,不是说你优先级越高,你的任务就能优先运行,这个还跟一些时间复杂度等等因素有关,优先级只能相对于更加优先而已,我们来看几个例子:

代码语言:text复制
public static void main(String[] args) {
         Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 100; i  ) {
                System.out.print(String.format("线程1:%d      ", i));
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 100; i  ) {
                System.out.print(String.format("线程2:%d      ", i));
            }
        });


        thread1.setPriority(10);

        thread2.setPriority(1);

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

两个线程的时间复杂度是一样的,只不过优先级一个是最高一个是最低,然后我们看下运行结果:

代码语言:txt复制
线程2:0      线程2:1      线程2:2      线程2:3      线程2:4      线程2:5      线程1:0      线程2:6      线程1:1      线程2:7      线程1:2      线程2:8      线程1:3      线程2:9      线程1:4      线程2:10      线程1:5      线程2:11      线程1:6      线程2:12      线程1:7      线程2:13      线程1:8      线程2:14      线程1:9      线程2:15      线程1:10      线程2:16      线程1:11      线程2:17      线程1:12      线程2:18      线程1:13      线程2:19      线程1:14      线程2:20      线程1:15      线程2:21      线程1:16      线程2:22      线程1:17      线程2:23      线程2:24      线程2:25      线程2:26      线程2:27      线程2:28      线程2:29      线程2:30      线程2:31      线程2:32      线程2:33      线程2:34      线程2:35      线程2:36      线程2:37      线程2:38      线程2:39      线程2:40      线程2:41      线程2:42      线程2:43      线程1:18      线程1:19      线程1:20      线程2:44      线程1:21      线程2:45      线程1:22      线程2:46      线程1:23      线程2:47      线程1:24      线程2:48      线程1:25      线程2:49      线程1:26      线程2:50      线程1:27      线程2:51      线程1:28      线程2:52      线程1:29      线程2:53      线程1:30      线程2:54      线程1:31      线程2:55      线程1:32      线程2:56      线程1:33      线程2:57      线程1:34      线程2:58      线程1:35      线程2:59      线程1:36      线程2:60      线程1:37      线程2:61      线程1:38      线程2:62      线程1:39      线程2:63      线程1:40      线程2:64      线程1:41      线程2:65      线程1:42      线程2:66      线程1:43      线程1:44      线程1:45      线程1:46      线程1:47      线程1:48      线程1:49      线程1:50      线程1:51      线程1:52      线程1:53      线程1:54      线程1:55      线程1:56      线程1:57      线程1:58      线程1:59      线程1:60      线程1:61      线程1:62      线程1:63      线程1:64      线程1:65      线程1:66      线程1:67      线程1:68      线程1:69      线程1:70      线程1:71      线程1:72      线程1:73      线程1:74      线程1:75      线程1:76      线程1:77      线程1:78      线程1:79      线程1:80      线程1:81      线程1:82      线程1:83      线程1:84      线程1:85      线程1:86      线程1:87      线程1:88      线程1:89      线程1:90      线程1:91      线程1:92      线程1:93      线程1:94      线程1:95      线程1:96      线程1:97      线程1:98      线程2:67      线程1:99      线程2:68      线程2:69      线程2:70      线程2:71      线程2:72      线程2:73      线程2:74      线程2:75      线程2:76      线程2:77      线程2:78      线程2:79      线程2:80      线程2:81      线程2:82      线程2:83      线程2:84      线程2:85      线程2:86      线程2:87      线程2:88      线程2:89      线程2:90      线程2:91      线程2:92      线程2:93      线程2:94      线程2:95      线程2:96      线程2:97      线程2:98      线程2:99

我们发现,虽然是先启动2,刚开始是2先运行,并且是占优势的,然后到了5的时候,两个线程之间就开始切换了,资源看起来是均等。然后又是2占优势,到了66的时候,线程1占到了主要优势,一路向前,到最后是线程2最后完成任务。虽然线程1是后来发力的,但是最终还是优于线程2先执行完。

优先级的继承性

在Java当中,谁启动的它,那么它的优先级就和启动的线程优先级一致。

代码语言:text复制
public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
        });

        Thread thread2 = new Thread(() -> {
        });

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

        System.out.println(Thread.currentThread().getPriority());
        System.out.println(String.format("线程1优先级:%d",thread1.getPriority()));
        System.out.println(String.format("线程2优先级:%d",thread2.getPriority()));
    }

#打印

5
线程1优先级:5
线程2优先级:5

输出的结果都是5,优先级一致。

为什么主线程的优先级是5呢?

我们来看下源码:

代码语言:text复制
/**
     * The minimum priority that a thread can have.
     */
    public final static int MIN_PRIORITY = 1;

   /**
     * The default priority that is assigned to a thread.
     */
    public final static int NORM_PRIORITY = 5;

    /**
     * The maximum priority that a thread can have.
     */
    public final static int MAX_PRIORITY = 10;

源码里注释标明了,5是默认的优先级。

但优先级真的决定优先处理完的结果吗?

我们再看看以下的例子:

代码语言:text复制
public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            try {
                for (int i = 0; i < 10; i  ) {
                    Thread.sleep(100);
                    System.out.print(String.format("线程1:%d      ", i));
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        Thread thread2 = new Thread(() -> {
            try {
                for (int i = 0; i < 10; i  ) {
                    Thread.sleep(50);
                    System.out.print(String.format("线程2:%d      ", i));
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

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

        thread1.setPriority(10);
        thread2.setPriority(5);

    }

两个线程优先级相差不大,且线程1先启动,再看下结果:

代码语言:txt复制
线程2:0      线程1:0      线程2:1      线程2:2      线程1:1      线程2:3      线程2:4      线程1:2      线程2:5      线程2:6      线程1:3      线程2:7      线程2:8      线程1:4      线程2:9      线程1:5      线程1:6      线程1:7      线程1:8      线程1:9

发现线程1在运行时效上也没占多大优势。

1.5、线程同步

多线程可以将一个任务分成多个线程去做,这样大大的节省了时间,但是也会带来一些问题。

比如 “脏读” 问题。两个线程同时对数据进行读取操作,读取到了一致的数据,但是在一个线程修改数据后,由于还需要进行同步的原因,另外一个线程不知道数据已经被修改,导致它操作了一个无效的数据,这就是线程不安全的原因。

我们可以尝试一下:

代码语言:java复制
public class TestThread {

    public static int i = 10;

    public static void main(String[] args) {
        Thread1 thread1 = new Thread1();
        Thread1 thread2 = new Thread1();
        thread1.start();
        thread2.start();
    }

    public static class Thread1 extends Thread{
        @Override
        public void run() {

            try {
                while (TestThread.i > 0){
                    System.out.println(TestThread.i);
                    TestThread.i--;
                    Thread.sleep(200);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

按照我们的预料,打印输出的应该是 10-1,但是结果却是和我们想想的不一致:10 10 8 8 6 5 4 3 2 1 。这个就是线程抢占不同步的问题(为什么要使用sleep呢?因为打印的数字太少,防止一一个线程给打印完了)。我们再来看看下一个。

代码语言:java复制
public class TestThread {

    public static void main(String[] args) {
        Thread1 thread1 = new Thread1();
        Thread1 thread2 = new Thread1();
        thread1.start();
        thread2.start();
    }

    public static class Thread1 extends Thread{

        private int i = 10;
        @Override
        public void run() {
            try {
                while (i > 0){
                    System.out.print(String.format("线程:%s:%d    ",Thread.currentThread().getName(),i));
                    i--;
                    Thread.sleep(500);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

打印:线程:Thread-0:10    线程:Thread-1:10    线程:Thread-1:9    线程:Thread-0:9    线程:Thread-0:8    线程:Thread-1:8    线程:Thread-0:7    线程:Thread-1:7    线程:Thread-0:6    线程:Thread-1:6    线程:Thread-0:5    线程:Thread-1:5    线程:Thread-0:4    线程:Thread-1:4    线程:Thread-1:3    线程:Thread-0:3    线程:Thread-1:2    线程:Thread-0:2    线程:Thread-1:1    线程:Thread-0:1

代码语言:java复制
public class TestThread {

    public static void main(String[] args) {
        Thread1 thread1 = new Thread1();
        Thread thread2 = new Thread(thread1);
        Thread thread3 = new Thread(thread1);
        thread2.start();
        thread3.start();
    }

    public static class Thread1 implements Runnable{
        private int i = 10;
        @Override
        public void run() {
            try {
                while (i > 0){
                    System.out.print(String.format("线程:%s:%d    ",Thread.currentThread().getName(),i));
                    i--;
                    Thread.sleep(500);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

打印:线程:Thread-0:10    线程:Thread-1:10    线程:Thread-1:8    线程:Thread-0:8    线程:Thread-0:6    线程:Thread-1:6    线程:Thread-1:4    线程:Thread-0:3    线程:Thread-0:2    线程:Thread-1:2

为什么两个感觉差不多的代码运行出来却是差这么多呢?因为线程不安全是发生在多个线程对同一资源进行写操作的时候出现的,第一份代码每个线程操作的都是自己的对象,而第二份代码操作的是同一个对象。

然后再将代码改一下:

代码语言:java复制
public class TestThread {

    public static void main(String[] args) {
        Thread1 thread1 = new Thread1();
        Thread thread2 = new Thread(thread1);
        Thread thread3 = new Thread(thread1);
        thread2.start();
        thread3.start();
    }

    public static class Thread1 implements Runnable{
        @Override
        public void run() {
            int i = 10;
            try {
                while (i > 0){
                    System.out.print(String.format("线程:%s:%d    ",Thread.currentThread().getName(),i));
                    i--;
                    Thread.sleep(500);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}


打印:线程:Thread-0:10    线程:Thread-1:10    线程:Thread-1:9    线程:Thread-0:9    线程:Thread-1:8    线程:Thread-0:8    线程:Thread-1:7    线程:Thread-0:7    线程:Thread-1:6    线程:Thread-0:6    线程:Thread-1:5    线程:Thread-0:5    线程:Thread-1:4    线程:Thread-0:4    线程:Thread-1:3    线程:Thread-0:3    线程:Thread-1:2    线程:Thread-0:2    线程:Thread-1:1    线程:Thread-0:1

因为局部变量存储在每个线程独有的存储区域,对于其他线程来说是不可见的,所以局部变量不会有线程安全问题。

**所以得出结论是:**线程不安全往往发生在类变量或者是实例变量,方法内的变量反而是线程安全的。我们只存在读的时候都是线程安全的,如果在读的时候存在写,那么可能就会出现线程不安全,所以要保证线程安全,就必须把读写分开,读时不能存在写,写时不能存在读。

那么如何保证线程安全呢?

接下来讲第一种方法:synchronized

1.5.1、对象锁
代码语言:java复制
public class SyncThread {

    public static void main(String[] args) {
        TestThread1 testThread1 = new TestThread1();

        Thread thread1 = new Thread(testThread1);

        Thread thread2 = new Thread(testThread1);

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

    public static class TestThread1 implements Runnable {
        
        private int i = 10;

        synchronized private void reduce() {
            try {
                String text = String.format("线程:%s打印->%d    ", Thread.currentThread().getName(),this.i);
                System.out.print(text);
                Thread.sleep(500);
                this.i--;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void run() {
            while (this.i > 0) {
                reduce();
            }
        }
    }
}

打印:线程:Thread-1打印->10    线程:Thread-1打印->9    线程:Thread-1打印->8    线程:Thread-1打印->7    线程:Thread-1打印->6    线程:Thread-1打印->5    线程:Thread-1打印->4    线程:Thread-1打印->3    线程:Thread-1打印->2    线程:Thread-1打印->1    线程:Thread-0打印->0

现在就很乖了,线程 0 发现如果线程 1 正在执行(拿不到锁),它会等线程 1 先执行完后在执行(释放锁)。这个是对象锁,在实例方法前加上 synchronized 关键字,因为这个方法是由对象实例去调用,所以叫做对象锁。

我们思考一个问题,如果一个类里面有两个同步方法 A 、B 并且都是对象锁,那么如果一个线程访问 A 方法然后拿到对象锁,另外一个线程访问B方法然后去执行会怎么样?

代码语言:typescript复制
public class SyncThread {

    public static void main(String[] args) {
        TestThread1 testThread1 = new TestThread1();

        Thread thread1 = new Thread(testThread1);

        Thread thread2 = new Thread(testThread1);

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

    public static class TestThread1 implements Runnable {

        private int i = 10;

        private boolean r = true;

        synchronized private void reduce() {
            r = false;
            try {
                String text = String.format("[线程]:%s打印->%d    ", Thread.currentThread().getName(), this.i);
                System.out.println(text);
                Thread.sleep(2000);
                this.i--;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        synchronized private void add() {
            try {
                String text = String.format("【线程】:%s打印->%d    ", Thread.currentThread().getName(), this.i);
                System.out.println(text);
                Thread.sleep(3000);
                this.i  ;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }


        @Override
        public void run() {
            if (r) {
                reduce();
            } else {
                add();
            }
        }
    }
}

打印:
[线程]:Thread-0打印->10    
【线程】:Thread-1打印->9 

我们发现线程在访问另外一个同步方法的时候,也会被阻塞,只有在第一个线程结束后才打印出结果,而将 add 方法的 synchronized 去掉,第二行就立马打印出来了,所以对象锁会影响同一个类的同步方法访问,当然这个锁必须是相同的( r 的一些其他问题我没有考虑)。

代码语言:text复制
private void reduce() {
            synchronized (this) {
                r = false;
                try {
                    String text = String.format("[线程]:%s打印->%d    ", Thread.currentThread().getName(), this.i);
                    System.out.println(text);
                    Thread.sleep(2000);
                    this.i--;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        }

synchronized (this){} 与上面在方法上加 synchronized 是等效的,同样是对象锁,锁的是当前对象。只不过下面这种更加灵活,在方法上加锁锁的是整个方法的代码块,而在部分代码上加锁锁的是部分代码,其他代码是异步操作。在线程等待获取锁的时候会被阻塞,也就是不再执行其他代码,直到获取到锁或者其他操作。

1.5.2、类锁

类锁和对象锁差不多,只不过锁的对象由类的示例变成了类本身,同步的范围也变得更大。

代码语言:java复制
public class SyncThread {

    public static void main(String[] args) {
        TestThread1 testThread1 = new TestThread1();
        TestThread1 testThread4 = new TestThread1();

        Thread thread1 = new Thread(testThread1);

        Thread thread2 = new Thread(testThread4);

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

    public static class TestThread1 implements Runnable {

        private int i = 10;

        private void reduce() {
            synchronized (TestThread1.class) {
                try {
                    String text = String.format("[线程]:%s打印->%d    ", Thread.currentThread().getName(), this.i);
                    System.out.println(text);
                    Thread.sleep(2000);
                    this.i--;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        }


        @Override
        public void run() {
            reduce();
        }
    }
}

我们将 synchronized (TestThread1.class)换成 synchronized (this)会发现两行数据差不多是一样的时间打印出来的,而还原回去发现第二行数据延迟了2秒的时间打印。如果其他线程访问 reduce 方法且操作的 TestThread1 类型,那么多个线程就会变成同步执行。

1.5.3、其他对象锁

这个也差不多

代码语言:text复制
Test test = new Test();
synchronized (test) {}

既然方法上有同步,那么属性上可以同步吗?请看下集!

我正在参与2023腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!

0 人点赞