多线程基础

一、start 方法启动一个线程

在前面的博客中,我们提到了关于在 Java 中创建一个线程所用的几种写法,主要通过 Thread 类来创建,然而,创建出来一个线程,线程就真正工作了吗?答案是否定的。
实际上,我们通过 start方法才能够让线程运行起来。

举个例子:我们在家里使用电饭煲煮饭,第一步,我们需要把插头插上,但插上后。电饭煲并没有直接工作,我们需要按 " 煮饭 " 这个按钮,电饭煲才能够工作,而这个 " 按钮 " 就相当于 start方法。

二、interrupt 方法中断一个线程

线程在通过 start方法启动之后,如果没有给它加上一些约束,这个线程就会一直运行,直到某项 " 任务 "结束,才会停下来。但有的时候,我们需要让线程停下来,需要它立即终止。

举个例子:无间道相信大家都看过,有一次反派在交易的时候,反派老大接收到信息《有内鬼,终止交易》,我们从反派的角度来看这件问题,如果他们不立即停止交易,是不是就被警察发现了一些赃物,那么对于反派来说,这需不需要及时止损呢?而实现中断一个线程的目的也是在于此,我们可以通过一些办法来根据我们的需求设定中断线程的时刻。

1. 方法一

使用一个 boolean 变量来作为循环结束标记。

程序清单1:

public class ThreadDemo1 {
    private static boolean flag = true;
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread() {
            @Override
            public void run() {
                while (flag) {
                    System.out.println("线程正在运行中...");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                System.out.println("线程结束!");
            }
        };
        thread.start();

        //主循环也等待三秒
        Thread.sleep(3000);

        //三秒之后,flag 改为 false,即中断当前线程
        flag = false;
    }
}

输出结果:

1

在程序清单1 中,当我们使用 start方法后,我们能够控制 main 方法中的程序和 run 方法中的程序之间并行运行,当三秒之后,我们将 flag 置为 false,这就明确了中断的时刻。

2. 方法二

方法一是程序员自己定义的变量作为循环标记,而我们还可以使用标准库里内置的标记。
获取线程内置的标记位: 用 isInterrupted( ) 来判定当前线程是不是应该要结束循环。
修改线程内置的标记位:Thread.interrupt( ) 来修改这个标记位。

程序清单2:

public class ThreadDemo2 {
    public static void main(String[] args)  {
        Thread thread = new Thread() {
            @Override
            public void run() {
                //默认情况下,isInterrupted() 的值为 false
                while (!Thread.currentThread().isInterrupted()) {
                    System.out.println("进程正在运行中...");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("线程结束!");
            }
        };
        thread.start();

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //将标记位 isInterrupted() 的值设为 ture
        thread.interrupt();

    }
}

输出结果:

2

我们查看输出结果,会发现,这个 interrupt( ) 方法并没有将 while 循环中的标记位置为 true,它只是在睡眠的时候,报出了一个异常。这就导致了,线程并没有终止,还在继续运行。

因为这里的 interrupt( ) 方法可能有两种行为:
① 如果当前线程正在运行中,此时就会修改 Thread.currentThread( ).isInterrupted( ) 这个标记位为 true
② 如果当前线程正在 sleep / wait / 等待锁…此时会触发 InterruptedExcepition

所以我们在上面的 try - catch 异常的时候,可以添加 break,直接就可以跳出循环了。

try {
	Thread.sleep(1000);
} catch (InterruptedException e) {
	e.printStackTrace();
	break;
}

输出结果:
2

isInterrupted( ) 这个是 Thread 的实例方法,和这个方法还有一个类似的:interrupted( ) 方法这个是 Thread 的类方法( static )

但是这两者有个区别:
使用这个静态的方法,会自动清除标记位。
例如,调用 interrupt( ) 方法,把标记位设为true,就应该结束循环。
当调用静态的 interrupted( ) 方法来判定标记位的时候,就会返回 true,同时就会把标记位再改回成false,下次再调用 interrupted( ) 就返回false.
如果是调用非静态的 isInterrupted( ) 来判定标记位,也会返回true。同时不会对标记位进行修改,后面再调用 isInterrupted() 的时候就仍然是返回 true.

3

三、join 方法等待一个线程

5

程序清单3:

public class ThreadDemo3 {
    public static void main(String[] args) {
        Thread t1 = new Thread() {
            @Override
            public void run() {
                int count = 0;
                while (count < 5){
                    count++;
                    System.out.println("线程运行中...");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("线程运行结束!");
            }
        };

        t1.start();

        try {
            System.out.println("-----join 执行开始-----");
            t1.join();
            System.out.println("-----join 执行结束-----");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

输出结果:

1

在程序清单3 中,我们可以看到 join 方法就是用来等待当前线程结束的。
执行 start 方法的时候,就会立刻启动一个新的线程。同时 main 方法这个线程也立刻往下执行,在执行到 t1.join 方法之前,都会正常地按顺序执行,而当执行到 t1.join 方法的时候就发现,当前 t1 线程还在运行中…那么只要 t1 正在运行中,join 方法就会一直阻塞等待,直到 t1 线程执行结束,即 run方法执行完了,才会正常地按顺序执行 t1.join 方法后面的语句。

程序清单4:

public class ThreadDemo4 {
    public static void main(String[] args) {
        Thread t1 = new Thread() {
            @Override
            public void run() {
                int count = 0;
                while (count < 5){
                    count++;
                    System.out.println("线程运行中...");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("线程运行结束!");
            }
        };

        t1.start();

        try {
            Thread.sleep(7000);
            System.out.println("-----join 执行开始-----");
            t1.join();
            System.out.println("-----join 执行结束-----");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

输出结果:

2

在程序清单4 中,我们在 join 方法之前设置了 7 秒,可以发现,线程的 run 方法走了 5 秒,那么还剩两秒才走到主函数中的 join 方法,等真正到了 join 方法的时候,此时的 join 方法就会迅速执行,不会等待…因为 join 方法存在的意义,就是为了在当前线程运行的过程中,进行阻塞等待。

这其实不难理解,举个例子:就和家长接幼儿园小朋友放学一样,当学校 16:00 放学,如果家长 15:50 到了幼儿园门口,那么他一定要多等 10分钟,才能接到小朋友。而如果家长 16:10 到了幼儿园门口,那么他就不用等待了,直接接小朋友回家就行了。

四、Thread.currentThread( ) 方法获取当前线程的引用

程序清单5:

public class ThreadDemo5 {
    public static void main(String[] args) {
        Thread t = new Thread("我的名字叫十七") {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
                System.out.println(this.getName());
            }
        };
        t.start();
    }
}

输出结果:

1

在程序清单5 中,我们可以看到 Thread.currentThread( ) 可以获取到当前线程的实例,这在 JavaSE 课程中类与对象那一章节的 this 的思想相同,而 this 表示当前对象的引用。在程序清单5 中,两者可以替换,是因为我们创建线程的时候,是通过子类继承 Thread 类的形式。

程序清单6:

public class ThreadDemo6 {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
                System.out.println(this.getName()); //error
            }
        }, "我的名字叫十七");

        t.start();
    }
}

在程序清单6 中,会报错,我已经通过注释标明出来了, Why?因为此时的 this 表示的是 Runnable类 的子类的引用,我们创建线程的时候,是将其子类作为 Thread 构造方法的参数,两者大不相同。所以,这种情况下使用 this ,是不能够获取到当前线程的引用的。

总之,我们还是使用 Thread.currentThread() 来拿到当前线程的引用,这样就可以兼容一切情况了。

五、sleep 方法休眠线程

//time 的单位一般设定为毫秒
Thread.sleep( time );

休眠线程是使用 sleep( ) 方法的,直接往里面填休眠时间即可,一般单位采用毫秒。等程序运行到休眠代码的时候,就停下来,停下来的时间就是我们自己设置的,之后再正常运行。

六、线程的状态

1
2

NEW:Thread对象创建出来了,但是内核的 PCB 还没创建出来。

RUNNABLE:当前的 PCB 被创建出来了,但这个 PCB 处于就绪状态( 即随时待命的状态 ),这个线程可能是正在CPU上运行,也可能是在就绪队列中排队…

TIMED _WAITING:表示当前的 PCB 在阻塞队列中等待,这样的等待是一个 " 带有结束时间 " 的等待,而 Thread.sleep( ) 方法就会触发这个状态。

WAITHNG:线程中如果调用了 wait 方法,也会阻塞等待。此时处在 WAITING 状态( 死等 ),除非是其他线程唤醒了该线程。WAITING 和 TIMED _WAITING 两者不同,前者一直在等待,而后者会有一个等待时间的上限。

BLOCKED:线程中尝试进行加锁,结果发现锁已经被其他线程占用了,此时该线程也会阻塞等待,而这个等待就会在其他线程释放锁之后,被唤醒。

TERMINATED:表示当前 PCB 已经结束了,Thread对象还在,此时调用获取状态,得到的就是这个状态。

以上6 个状态,其实可以总结划分成4 个基本状态:
① NEW:未开始状态
② RUNNABLE:就绪状态
③ TIMED _WAITING / WAITHNG / BLOCKED:阻塞等待状态
④ TERMINATED:结束状态

理解线程的状态,最大的意义在于调试一些多线程的程序,比方说:你在打游戏的时候,程序卡死了…这个时候我们得看看是网络崩了,还是程序更新问题,还是内存问题,等等…
而我们必须明确:上面的 6 个状态都是 Java 中的 Thread 类的状态,和操作系统内部 PCB 里面的状态并不完全一致。

程序清单7:

public class ThreadDemo7 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread() {
            @Override
            public void run() {
                while (! Thread.currentThread().isInterrupted()) {

                }
            }
        };

        System.out.println("start 方法之前的状态:" + t.getState());
        t.start();
        System.out.println("start 方法之后的状态:" + t.getState());
        t.interrupt();
        Thread.sleep(1000);
        System.out.println("interrupt 方法之后的状态:" + t.getState());
    }
}

输出结果:

1

七、线程安全问题( 经典面试题 )

由于多线程并发执行,导致了代码中出现了 bug,这样的情况就称为 " 线程不安全 "。

在程序清单8 中,我们创建两个线程 t1 和 t2,分别让 t1 和 t2 各跑 5w 次,加起来就是 10w 次,然而,结果并不是我们所想的那样,最终的 conut 值并不是 10000,这就体现了线程不安全。

程序清单8:

public class ThreadDemo8 {
    static class Counter {
        public int count = 0;
        //让 count 变量自增
        public void increase() {
            count++;
        }
    }
    static Counter counter = new Counter();

    public static void main(String[] args) {

        //线程1 自增 5w 次
        Thread t1 = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 50000; i++) {
                    counter.increase();
                }
            }
        };

        //线程2 自增 5w 次
        Thread t2 = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 50000; i++) {
                    counter.increase();
                }
            }
        };

        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(counter.count);
    }
}

输出结果:

这是我从 IDEA 编译器截图的五个输出数据,可以发现每次数据的结果都不一样。

1

刚才的代码中,两个线程并发的自增了 5w 次,
在 t1 和 t2 各自的 5w 次里面,有多少次触发了类似于上面的" 线程不安全问题 ",我们并不确定,因为这是随机的结果,我们并不能通过数据得出一个准确的结果。
这就导致了:最终 count 的结果,我们就不确定,但可以知道的是,最终结果一定是 5w-10w之间。

极端情况下:
① 由于并列执行,如果我们很倒霉,每次自增都触发了线程不安全,结果就正好是 5w

② 而如果我们很幸运,每次自增都没触发线程不安全( 都是串行执行的 ) , 结果就正好是 10w
但是实际上,到底多少次是并列执行,多少次是串行执行,我们没办法不确定…

情况一是正常情况下,或者说此次的 count 自增未触发线程不完全,这种情况其实就相当于串行执行,线程1 把自己的所有步骤都执行完了,再轮到线程2 执行自己的所有步骤。

1

情况二的情况就不同了,线程1 和 线程2 并发执行时,我们预期的结果是 count = 2,也就是说,count 应该自增了两次,但最终的结果却是 count = 1.

2

情况三和情况四又是不同的情况了

3

综上所述,线程之间并发式运行的时候,它们对应的执行顺序是怎样的,作为程序员,我们并不确定,而在操作系统调度线程的时候,是按 " 抢占式执行 " 的方式实现的。某个线程什么时候能上 CPU 执行,什么时候切换出 CPU,是完全不确定的。而上面的四种情况只是所有情况中的冰山一角,其实还有很多种排列组合的形式。因此,我们猜想的结果也就不可预期了。

1. 产生线程不安全的原因

原因一:线程之间是抢占式执行的

线程之间是抢占式执行的 ( 根本原因,线程不安全的万恶之源 ),这就会导致两个线程里面操作的先后顺序我们人为无法确定,这样的随机性,就是导致线程安全问题的根本所在。而这条原因我们无力改变,因为本质上这是操作系统内核实现的。

原因二:多个线程修改同一个变量所造成的结果

多个线程修改同一个变量所造成的结果 ( 这个和代码的写法密切相关 )

然而,下面两种形式并不会产生产生线程不安全:
① 一个线程修改同一个变量,没有线程安全问题。( 或者说,多个线程修改自己的变量 ),不涉及并发,结果是确定的。
② 多个线程读取同一个变量,也没有线程安全问题。读只是单纯地把数据从内存放到 CPU中,不管怎么读,内存的数据始终不变。

所以为了规避线程不安全,就可以尝试变换代码的组织形式,达到上面两种情况的形式。然而,变换代码的组织形式不是万能的,因为有的业务场景具有特定的组织形式。

原因三:不具有原子性

像 " ++ "这样自增的操作,本质上是三个步骤,( 先是 CPU 从内存中读,接着在 CPU 中实现自增操作,最后再通过 CPU 写回给内存 ),而像 " = " 这样的赋值操作,本质上就只是一个步骤,这就是一个 " 原子性 " 的操作。刚刚我们创建两个线程来让 count 自增 10w 次,这就不是一个 " 原子性 " 的操作。
而我们后面可以通过 " 加锁 " 的方式,把整个执行流程变成原子性。

原因四:不具有内存可见性

我们假设一个场景,让线程1 和 线程2 并发地来自增一个变量 count,线程1 只涉及 CPU 和 内存之间的读操作,线程2 不仅涉及读操作,而且涉及修改操作,即自增操作,那么本质上,这符合线程安全。(一个线程读,一个线程改,满足线程安全)
但我们看到下图的情况中,编译器优化让 线程2 省略了中间所有读操作,这就导致了线程1 在编译器优化的前后,读到的值不同,我已通过绿色圈圈标明出来。

1

我们要想让多线程执行过程中,满足内存可见性,后面可以用 volatile 关键字来处理这个问题。本质上就是去除编译器优化的效果,这是硬件问题,而我们想要达到准确的结果,就必须为程序加上约束。

原因五:指令重排序

指令重排序,这也是和编译器的优化相关。

编译器会自动调整执行指令的顺序,以达到提高执行效率的效果。而调整的前提是,保证指令的最终效果是不变的。如果当前的逻辑只是在单线程下运行,编译器判定顺序是否影响结果,就很容易。如果当前的逻辑可能在多线程下运行,编译器判定顺序是否影响结果,就可能出错。

八、解决线程不安全

程序清单9:

public class Test {
    static class Counter {
        public int count = 0;
        //让 count 变量自增
        synchronized public void increase() {
            count++;
        }
    }
    static Counter counter = new Counter();

    public static void main(String[] args) {

        //线程1 自增 5w 次
        Thread t1 = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 50000; i++) {
                    counter.increase();
                }
            }
        };

        //线程2 自增 5w 次
        Thread t2 = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 50000; i++) {
                    counter.increase();
                }
            }
        };

        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(counter.count);
    }
}

我们在程序清单9 中,为 increase 方法加上了 synchronized 修饰符,可以看到输出结果就是 10000.

输出结果:

2

上述解决线程不安全,就是通过 " 原子性 " 这样的切入点来解决问题,synchronized 的英文原意为 【adj. 同步的】。而它在计算机中的术语,我们可以将它理解成 " 互斥 "。

如果两个线程同时并发地尝试调用这个 synchronized 修饰的方法,此时一个线程会先执行这个方法,另外一个线程会等待,等到第一个线程方法执行完了之后,第二个线程才会继续执行这个方法。而这个过程实际上就相当于" 加锁 " 和 " 解锁 "。

举个例子,你去银行 ATM 取钱,当你进入隔间的时候,需要把门带上,而后面的人需要排队等待。这就像 synchronized 的加锁一样。synchronized 本质上就是,将 " 并发执行 " 变成 " 串行执行 ",这样一来,速度就会降低。在刚刚银行的例子中,假设你的账户有10000 元,你取出了 5000元,本来你应该剩余 5000 元的,但你取钱之后发现就剩了 1000元,这是什么感受?所以当你需要一些准确无误的结果的时候,你必须这样。那么使用 synchronized 关键字的场景实际上就是:两个线程竞争同一把锁,而可能出现阻塞的场景。这就像下图一样,一家小银行,只有一个 ATM 机。

2

1. synchronized 关键字

在 Java 中,进入 synchronized 修饰的方法,就相当于加锁;出了 synchronized 修饰的方法,就相当于解锁。如果当前是已经加锁的状态,其他的线程就无法执行这里的逻辑,就只能阻塞等待。

在上面的代码中,synchronized 被用来修饰方法,而它还能用来修饰代码块。
当它用来修饰代码块的时候,我们需要显示地在 ( ) 中指定一个加锁的对象,如果 synchronized 直接修饰的是非静态方法,相当于加锁的对象就是 this.

public void increase() {
    synchronized (this) {
        count++;
    }
}

1

所谓的 " 加锁操作 " 其实就是把一个指定的锁对象中的锁标记设为 true.
所谓的 " 解锁操作 " 其实就是把一个指定的锁对象中的锁标记设为false.
如果两个线程尝试针对同一个锁对象进行加锁,此时一个线程会先获取到锁,另外一个线程就阻塞等待。这就和上面的例子差不多,假设银行只有一台 ATM,那么许多人就要排队等待。
如果两个线程,尝试针对两个不同对象进行加锁,此时两个线程都能获取到各自的锁,互不冲突。这就和银行有多台 ATM 一样,不同的人去不同的隔间取钱。

拓展:Java 中任意的对象,都可以作为 " 锁对象 ",这一点就和其他语言的设定不一样,例如 C++,Python,GO … 这些语言的加锁操作,就只能针对特定的对象加锁。

2. synchronized 主要的三个特性

(1) 互斥

互斥这一特性即对应到程序清单9 中的代码,也就是说,它解决了线程安全问题中的非原子性,即表示一个线程在执行某一步骤的过程中,在执行完之前,其他线程阻塞等待。

(2) 刷新内存,保证内存的可见性

刷新内存这一特性,指的是:synchronized 还能刷新内存,解决内存可见性的问题。举个例子:一个线程修负责改,一个负责线程读取。由于编译器的优化,可能把一些中间环节的 LOAD 和 RETURN 操作取消掉了,此时读的线程可能读到的就是未修改的结果。加上 synchronized 之后,就会禁止编译器优化,保证每次进行操作的时候,都会把数据真的从内存读,也真的写回内存中。这样一来,同样地,程序运行速度会变慢,但是求得的结果和预期之间较为准确。

这解决了线程安全问题中的内存不可见性。

(3) 可重入( 防止死锁 )

对于第三点,我们对 increase 方法加了 synchronized,同时,在方法的里面,我们又加了一个 synchronized,在 Java 中,其实这样没有问题,它防止了死锁现象。

2

死锁:
第一次加锁,加锁成功。
第二次再尝试针对这个线程加锁的时候,此时对象头的锁标记已经是true,按照咱们之前的理解,其他线程就要阻塞等待,等待这个锁标记被改成 false,然后再重新竞争这个锁…所以说:本质上第 ② 步不会执行,因为它正在阻塞等待,那么我们就走不到最后那个花括号( 红色箭头 ),这样一来,对于第一个 synchronized 来说,我们就无法 " 解锁 ",这样就会造成死锁。

然而,在 Java 中,它就是为了防止程序员犯错,所以体现了可重入性,解决了当前逻辑的死锁现象。

3. synchronized 的使用示例

(1) 直接修饰普通方法

将 SynchronizedDemo 对象加锁

public class SynchronizedDemo {
	public synchronized void method() {
	
	}
}

这个时候如果两个线程并发地调用这个方法,此时是否会触发锁竞争,就要看实际的锁对象是否是同一个了。

(2) 修饰静态方法

将 SynchronizedDemo 类的对象加锁

public class SynchronizedDemo {
	public synchronized static void method() {
	
	}
}

由于类对象是单例的,两个线程并发调用该方法,一定会触发锁竞争。因为 static 修饰的方法直接关联到类。

(3) 修饰代码块

① 将当前对象加锁

public class SynchronizedDemo {
	public void method() {
		synchronized (this) {
		
		}
	}
}

② 将类的对象加锁

public class SynchronizedDemo {
	public void method() {
		synchronized (SynchronizedDemo.class) {
		
		}
	}
}

4. volatile 关键字

程序清单10:

public class Test {
    static class Counter {
        public int flag = 0;
    }

    public static void main(String[] args) {
        Counter counter = new Counter();

        Thread t1 = new Thread() {
            @Override
            public void run() {
                while (counter.flag == 0){

                }
                System.out.println("循环结束...");
            }
        };
        t1.start();

        Thread t2 = new Thread() {
            @Override
            public void run() {
                Scanner scanner = new Scanner(System.in);
                System.out.println("请输入一个整数: ");
                counter.flag = scanner.nextInt();
            }
        };
        t2.start();
    }
}

输出结果:

如果正常情况下,按照上面的代码,我们输入的值为 1,即 flag 不为 0,那么就会输出
【循环结束…】,可是我们可以看到输出结果什么都没有。

1

图解分析:
2

static class Counter {
	volatile public int flag = 0;
}

所以我们就将 flag 加上 volatile 修饰即可,一旦加上 volatile 关键字之后,此时后续针对 flag 的读写操作,就能保证一定是操作内存了。

新的输出结果:

3

总结:volatile 关键字用法较为单一,它只能用来修饰属性 / 成员变量。它可以保证内存可见性,但是保证不了原子性。

程序清单11:

public class Test {
    static class Counter {
        public int flag = 0;
    }

    public static void main(String[] args) {
        Counter counter = new Counter();

        Thread t1 = new Thread() {
            @Override
            public void run() {
                while (counter.flag == 0){
                    synchronized (counter) {
                        if (counter.flag != 0) {
                            break;
                        }
                    }
                }
                System.out.println("循环结束...");
            }
        };
        t1.start();

        Thread t2 = new Thread() {
            @Override
            public void run() {
                Scanner scanner = new Scanner(System.in);
                System.out.println("请输入一个整数: ");
                counter.flag = scanner.nextInt();
            }
        };
        t2.start();
    }
}

输出结果:

2

我们可以看到,当我们使用 synchronized 关键字,也是可以保证内存可见性。

总而言之,volatile 关键字和编译器优化密切相关,而编译器优化是一个相当复杂的事情,在我们写出的代码后,编译器优化或不优化,什么时候优化、又什么时候不优化、优化到什么程度,这都是很难去控制的事情。所以还是在日常开发中,多写代码,多总结一些经验才能够慢慢熟悉。一般来说,如果某个变量,在一个线程中读,另一个线程中写,这个时候大概率需要使用 volatile.

volatile关键字 与 JMM 内存模型

volatile 这里涉及到一个重要的知识点,JMM ( Java Memory Model ) 内存模型,当代码中需要读一个变量的时候,不一定是真的在读内存。可能这个数据已经在 CPU 或 cache 中缓存着了,这个时候就可能绕过内存,直接从 CPU 或 cache 中来取这个数据。

然而,JMM 针对计算机的硬件结构又进行了一层抽象 ( 主要就是因为 Java 要考虑到跨平台的问题,要能支持不同的计算 ) 所以,

JMM 把CPU的寄存器,cache 统称为 " 工作内存 ",而工作内存一般不是真正的内存。
JMM 把真正的内存称为 " 主内存 "。

CPU 在和内存交互的时候,经常会把主内存的内容拷贝到工作内存,然后再对数据进行操作,最后才写回到主内存。而这个过程中就非常容易出现数据不一致的情况,这种情况在编译器开启优化的时候会特别严重。

而 volatile 或 synchronized 关键字能够强制保证操作数据的时候,操作的是内存,在生成的java 字节码中强制插入一些 " 内存屏障 " 的指令,而这些指令的效果,就是强制同步主内存和工作内存的内容。

5. synchronized 和 volatile 的区别和联系( 经典面试题 )

① synchronized 既能保证内存可见性,又能保证原子性。
② volatile 只能保证内存可见性,保证不了原子性。

九、wait 和 notify 方法

说明

在 Java 中,wait 和 notify 方法必须要搭配使用,才能合理地协调多个线程之间的执行先后顺序。此外, wait 和 notify 方法必须要针对同一个对象使用。wait 和 notify 都是 Object 类的方法,比如线程1 中的对象1 调用了 wait 方法,必须要有个线程2 也调用对象1 的 notify 方法,才能唤醒线程1。这就是规则。
而如果是线程2,调用了对象2 的 notfiy 方法,就无法唤醒线程1.

1. wait 方法

我们使用 wait 方法,它在 Java 底层中,做了一下三件事:

(1) wait 方法让当前线程阻塞等待,因为 CPU 在进行线程调度的时候,是从就绪队列中,找一个 PCB 到 CPU 上执行。所以 wait 方法就是将这个线程的 PCB 从就绪队列拿到等待对列中,并准备接受通知。

1

(2) wait 方法释放当前锁,要想使用 wait / notify,必须搭配 synchronized,需要先获取到锁,才有资格谈 wait,所以在有锁的状态下,wait 方法其实执行了释放锁操作。释放锁的目的就是为了给其他线程让路,也就是说:释放锁之后,所有处于就绪队列的线程需要重新竞争。

举个例子:银行用户去 ATM 取钱,用户1 在取钱的时候,发现 ATM 机子里面没钱了,所以银行就叫来了工作人员,工作人员提了一大袋子,将现金放进去,然而在这期间,用户1 一定要出来,工作人员才能把钱放进去,也就是说,锁需要打开。而当工作人员将钱处理好之后,用户需要重新竞争这把锁,因为可能有存钱的、有查看存款的…

2

(3) 使用wait 方法,当满足一定的条件被唤醒时,重新尝试获取到这个锁。

public class Test {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        object.wait();
    }
}

输出结果:

当我们使用 wait 方法不加锁的时候,我们会发现编译器报异常,异常的英文为:非法监视状态。而 synchronized 也叫做监视器锁。

1

2. notify 方法

关于 notify 的使用:

(1) notify 方法也要使用 synchronized 关键字进行加锁操作。
(2) notify 方法实际上一次只唤醒一 个线程,当有多个线程都在等待中,调用 notify 方法就相当于随机唤醒了一个线程,而其他线程都保持原状。
(3) notify 方法这是通知对方被唤醒,但调用 notify 本身的线程并不是立即释放锁,而是要等待当前的 synchronized 代码块执行完才能释放锁。

程序清单12:

public class Test {
    static class WaitTask implements Runnable {
        private Object locker = null;
        public WaitTask (Object locker) {
            this.locker = locker;
        }

        @Override
        public void run() {
            synchronized (locker) {
                System.out.println(" wait 开始...");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(" wait 结束!");
            }
        }
    }

    static class NotifyTask implements Runnable {
        private Object locker = null;

        public NotifyTask (Object locker) {
            this.locker = locker;
        }

        @Override
        public void run() {
            synchronized (locker) {
                System.out.println(" notify 开始");
                locker.notify();
                System.out.println(" notify 结束");

            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        //Object 对象的创建,就是为了能够方便地对线程进行加锁 / 通知操作
        Object locker = new Object();
        Thread t1 = new Thread (new WaitTask(locker));
        Thread t2 = new Thread (new NotifyTask(locker));
        t1.start();
        Thread.sleep(3000);
        t2.start();
    }
}

输出结果:

2

程序清单13:

public class Test {
    static class WaitTask implements Runnable {
        private Object locker = null;
        public WaitTask (Object locker) {
            this.locker = locker;
        }

        @Override
        public void run() {
            synchronized (locker) {
                System.out.println(" wait 开始...");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(" wait 结束!");
            }
        }
    }

    static class NotifyTask implements Runnable {
        private Object locker = null;

        public NotifyTask (Object locker) {
            this.locker = locker;
        }

        @Override
        public void run() {
            synchronized (locker) {
                System.out.println(" notify 开始");
                locker.notify();
                System.out.println(" notify 结束");

            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Thread t1 = new Thread (new WaitTask(locker));
        Thread t2 = new Thread (new WaitTask(locker));
        Thread t3 = new Thread (new WaitTask(locker));
        Thread t4 = new Thread (new WaitTask(locker));
        Thread t5 = new Thread (new NotifyTask(locker));

        t1.start();
        t2.start();
        t3.start();
        t4.start();
        Thread.sleep(3000);
        t5.start();
    }
}

输出结果:

1

在上图中,我们发现 4个 线程正在阻塞等待中,而只唤醒了 1个 线程,也就是说,t2,t3,t4 都在阻塞等待中,这也就说明了 notify 方法一次只能唤醒一个线程。
而 notifyAll 方法顾名思义,它的存在,就可以一次唤醒所有线程。

程序清单14:

public class Test {
    static class WaitTask implements Runnable {
        private Object locker = null;
        public WaitTask (Object locker) {
            this.locker = locker;
        }

        @Override
        public void run() {
            synchronized (locker) {
                System.out.println(" wait 开始...");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(" wait 结束!");
            }
        }
    }

    static class NotifyTask implements Runnable {
        private Object locker = null;

        public NotifyTask (Object locker) {
            this.locker = locker;
        }

        @Override
        public void run() {
            synchronized (locker) {
                System.out.println(" notifyAll 开始");
                locker.notifyAll();
                System.out.println(" notifyAll 结束");

            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Thread t1 = new Thread (new WaitTask(locker));
        Thread t2 = new Thread (new WaitTask(locker));
        Thread t3 = new Thread (new WaitTask(locker));
        Thread t4 = new Thread (new WaitTask(locker));
        Thread t5 = new Thread (new NotifyTask(locker));

        t1.start();
        t2.start();
        t3.start();
        t4.start();
        Thread.sleep(3000);
        t5.start();
    }
}

输出结果:

3

3. notify 和 notifyAll 的区别

notify 是随机唤醒等待队列中的一个线程,其他线程还是乖乖等着。
notifyAll 是一下唤醒所有线程,但这些线程需要重新竞争锁。

十、wait 和 sleep 的区别和联系 ( 面试题 )

① sleep 操作是指定一个固定时间来阻塞等待,而 wait 既可以指定时间,也可以无限等待。

② sleep 唤醒通过时间到或 interrupt 唤醒 ,而 wait 唤醒也可以通过时间到或 interrupt 唤醒,但 wait 通常需要使用 notify 搭配

③ wait 主要的用途就是为了协调线程之间的先后顺序,这样的场景并不适合使用 sleep,sleep 只是单纯让该线程休眠,其并不涉及到多个线程的配合。

④ wait 是 Object 类的方法,而 sleep 是 Thread 类的方法。

⑤ wait 执行了释放锁操作,而sleep 不释放锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

十七ing

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值