当前位置:首页 » 《我的小黑屋》 » 正文

Java多线程(二) —— join、线程的状态与synchronized

15 人参与  2024年10月27日 18:01  分类 : 《我的小黑屋》  评论

点击全文阅读


join

我们可以通过使用join() 方法来进行线程等待的操作:

public class Demo2 {    public static void main(String[] args) throws InterruptedException {        Thread t1 = new Thread(() -> {            System.out.println("t1 线程启动");        });        Thread t2 = new Thread(() -> {            System.out.println("t2 线程启动");        });        t1.start();        t2.start();        t1.join();        t2.join();        System.out.println("Main 线程启动");    }}

在这里插入图片描述


在这里插入图片描述

上面前两个方法我们经常使用到,第一个方法在上面已经演示过了,第二个方法是设置了时间,也就是最多等待 x 毫秒就不会继续等待了,如果一个线程因为有些原因而始终处于阻塞状态的话,为了避免程序持续运行,我们会设置一下等待时间,避免整个程序都挂了。

后面的方法则是设置了 ns 级别,但是我们很少会用到,因为我们大部分人的电脑的操作系统都不是实时操作系统的,由于线程存在调度,也就是线程调度过来了并不会立即就会执行可能会晚几纳秒这样的,但是这种延迟我我们是可以接受的,所以最后一个方法我们几乎不会使用。


现在来处理一下谁等谁的问题,上面的代码中我们实在 main 方法里使用了 t1.join() 和 t2.join(),也就是 main 线程等 t1 和 t2 线程的结束,我们才执行 main 线程

也就是说在哪个线程里使用 xxx.join() ,意味着 这个线程 等 xxx

我们也可以让 t1 线程等 main 线程:

public class Demo2 {    public static void main(String[] args) throws InterruptedException {        Thread cur = Thread.currentThread();        Thread t1 = new Thread(() -> {            try {                cur.join();            } catch (InterruptedException e) {                throw new RuntimeException(e);            }            System.out.println("t1 线程启动");        });        t1.start();        for (int i = 0; i < 3; i++) {            System.out.println("Hello Main");        }    }}

在这里插入图片描述

线程的状态

在这里插入图片描述

new: 表明线程对象被创建出来了

runnable:说明线程正在执行(执行状态)或者处于要执行的状态(就绪状态)

wating:表明线程正在等待某些事情,例如上面的 join() 方法,一个线程等待另一个线程的结束。

time_wating:表明线程正在等待某些事情,但是是由等待时间限制的,也就是说,这不是死等,时间一到,这个线程就会被执行,例如上面的 join(ms) 方法,一个线程等待另一个线程 x 毫秒。

blocked:说明线程此时处于阻塞状态,这个阻塞状态是由于没有获取到锁而陷入的阻塞状态,和上面提到的 wating 和 time_wating 是由区别的。

terminated:表明线程终止。可以是正常执行完的终止也可以是我们手动的终止。

上一篇文章中我们学过的 isAlive() 方法,可以认为是 线程处于不是NEW和TERMINATED的状态 都是活着的。

synchronized

引入

public class Demo3 {    private static int count = 0;    public static void main(String[] args) throws InterruptedException {        Thread t1 = new Thread(() -> {            for (int i = 0; i < 50000; i++) {                count++;            }        });        Thread t2 = new Thread(() -> {            for (int i = 0; i < 50000; i++) {                count++;            }        });        t1.start();        t2.start();        t1.join();        t2.join();        System.out.println("count = " + count);    }}

大家来思考一下运行结果是什么?


在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

你会发现每一次运行的结果都是不一样的。


我们站在CPU 的角度来分析上面的代码,首先 count++ 是有三条指令,首先加载 count 的数值,然后进行 +1,最后保存 count 的数值。

由于线程的调度是随机的就会出现下面的情况,这里我举一个例子供大家参考:
在这里插入图片描述
在上面的调度中,t1 先加载 count = 0, 然后 t1 被调度走了,t2 带哦都过来了,此时 t2 也执行了 加载 count = 0; 然后 t2 被调度走了,此时 t1调度过来将 count + 1 = 1, 之后又被调度走了,t2 来了执行 add ,count + 1 = 1,然后进行 save 保存数据,此时 count 被修改为 1,然后 t1 调度过来了,进行 save 操作,这时候将 count 修改为 1,之后进行保存

此时你会发现,上面进行了两个 ++ 操作,但是 count 却变为了 1,这就是线程安全问题

我们再来分析一下,我们最后得到的count 的范围有可能为 10 w 吗?
答案是有可能的,但是概率极低,只要每一个线程的三条指令都是串行执行的话,就会得到正确的结果 10w

那count 有可能小于 5 w 吗?
有可能,假设一共有 5 次 ++ 操作,t1 线程执行 3 次,t2 线程执行 2 次:
在这里插入图片描述
5 次 ++ ,最后得到 2
10 w 次++ ,最后是可能得到小于 5 w 的结果的,但是这个概率极低

synchronized 的使用

为了避免上面的线程安全问题,我们可以进行加锁操作,Java 给我们提供了 synchronized 关键字,让我们可以对代码进行加锁操作:

在这里插入图片描述

public class Demo3 {    private static int count = 0;    public static void main(String[] args) throws InterruptedException {        Object locker = new Object();        Thread t1 = new Thread(() -> {            for (int i = 0; i < 50000; i++) {                synchronized(locker) {                    count++;                }            }        });        Thread t2 = new Thread(() -> {            for (int i = 0; i < 50000; i++) {                synchronized(locker) {                    count++;                }            }        });        t1.start();        t2.start();        t1.join();        t2.join();        System.out.println("count = " + count);    }}

在这里插入图片描述


synchronized(对象) {    //需要加锁的代码}

一般情况下,我们的 synchronized 的括号内部需要传入一个锁对象,这个所对象可以为任意对象,但是这里建议大家自己创建一个专门用来作锁的对象,专象专用

在Java中,如果两个及以上的线程对同一个对象都有加锁需求的话,当谁拥有这个对象的锁的时候,就可以进行sychronized 里面的操作了,如果没有拥有这个对象的锁,就会处于阻塞状态,直到获得这个对象的锁。

这里需要注意如果加锁的对象不是同一个的话,那么这些线程是不会发生互斥关系的
只有当两个及以上的线程对同一个对象加锁的时候,才会产生互斥(锁冲突 / 锁竞争)


synchronized 的其他用法:
可以修饰方法:

class Test {    private static int count = 0;    synchronized public void add() {        count++;    }    public int getCount() {        return count;    }}public class Demo4 {    public static void main(String[] args) throws InterruptedException {        Test test = new Test();        Thread t1 = new Thread(() -> {            for (int i = 0; i < 50000; i++) {                test.add();            }        });        Thread t2 = new Thread(() -> {            for (int i = 0; i < 50000; i++) {                test.add();            }        });        t1.start();        t2.start();        t1.join();;        t2.join();        System.out.println("count = " + test.getCount());    }}

在这里插入图片描述

你也可以在方法里面写 synchronized:

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

括号里面记得写 this,表示当前加锁的对象

实质上这种写法其实和上面直接对类的方法加 synchronized 是一样的,都是对 this 加锁


当然也可以对进行方法使用 synchronized ,这样就是对类对象进行加锁

class Counter {    private static int count = 0;    synchronized public static void add() {        count++;    }    public static int getCount() {        return count;    }}public class Deno5 {    public static void main(String[] args) throws InterruptedException {        Thread t1 = new Thread(() -> {            for (int i = 0; i < 50000; i++) {                Counter.add();            }        });        Thread t2 = new Thread(() -> {            for (int i = 0; i < 50000; i++) {                Counter.add();            }        });        t1.start();        t2.start();        t1.join();        t2.join();        System.out.println("count =" + Counter.getCount());    }}

在这里插入图片描述

synchronized 的可重入性

public class Demo1 {    public static void main(String[] args) {        Object locker = new Object();        Thread t1 = new Thread(() -> {            synchronized(locker) {                synchronized (locker) {                    System.out.println("t1 线程启动");                }            }        });        t1.start();    }}

我们看一下上面的代码,你会发现一个线程内部对一个对象进行了两次加锁,我们回到锁的性质来看,当一个对象被加锁之后,其他线程要想获得这个锁就必须阻塞等待,那么在这个线程里,按照理论来说,这个线程会陷入死锁状态,第一次已经对 locker 加过锁了,第二次就应该是无法获得 locker 这个锁对象了,但是事实并非如此:

在这里插入图片描述

线程依然能正常执行,这是因为Java 的synchronized 带有可重入特性,简单来说,就是在同一个线程里,可以对已经获得的锁对象进行重复加锁,这样就避免因为一个线程对同一个对象重复加锁产生的死锁问题了。

在可重入下,什么时候解锁?
当然是第一个synchronized 的最后一个括号出来就会自动解锁,因为两个synchronized 之间可能存在代码,加锁加锁就一次性锁住

如何实现可重入锁?
可重入特性是在同一个线程对同一个锁对象进行加锁,那么我们只要在锁内部记录当前是哪个线程持有这把锁,在后续加锁的时候,就进行判定。
使用计数器来统计括号,遇到左括号就++, 遇到右括号就–,等到 count == 0 的时候就直接解锁

不要认为 JVM 分不出是不是 synchronized 的括号,在 .java 编译成 .class 字节码文件的时候,不同的括号都有不同的含义,对应的字节码是不一样的。


点击全文阅读


本文链接:http://zhangshiyu.com/post/178672.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1