栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

【多线程深度剖析】

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

【多线程深度剖析】

进程的状态大致分为两种:就绪状态和阻塞状态. 上面所说的"就绪"和"阻塞"都是针对系统层面上的线程的状态(PCB) 在JAVA中Thread类中,对于线程的状态,又进一步的细分为多种状态 一.Thread类中的线程状态 1.NEW:安排了工作,还为开始行动

具体代码案例如下:

public class Demo1 {
    public static void main(String[] args) {
        Thread t = new Thread(() ->{
            System.out.println("hello thread");
        });
        System.out.println(t.getState());
    }
}

把Thread对象创建好了,但是还没有调用start~执行结果如下:

使用t.getState()获取线程的状态~

2.TERMINATED:工作完成了
public class Demo2 {
    public static void main(String[] args) {
        Thread t = new Thread(() ->{

        });
        t.start();
        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(t.getState());
    }
}

操作系统中的线程已经执行完毕销毁了,但是Thread对象还在,此时获取到的状态就是TERMINATED.

 3.RUNNABLE:可工作的,又可以分成正在工作中和即将开始工作

就绪状态.处于这个状态的线程,就是在就绪队列中.随时可以被调度到CPU上~~

如果代码中没有进行sleep,也没有进行其他的可能导致阻塞的操作,代码大概率是处在RUNNABLE状态的~~~

public class Demo3 {
    public static void main(String[] args) {
        Thread t = new Thread(() ->{

        });
        t.start();
        System.out.println(t.getState());
    }
}

 4.TIMED_WAITING:这几个都表示排队等着其他事情

代码中调用了sleep,就会进入到TIMED_WAITING,join(超时时间)

意思是当前的线程在一定时间之内是阻塞状态~~

public class Demo4 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() ->{
            while (true){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
        Thread.sleep(5000);
        System.out.println(t.getState());
    }
}

5.BLOCKED:这几个都表示排队等着其他事情

当前线程在等待锁,导致了阻塞.synchronized

6.WAITINT:这几个都表示排队等着其他事情

当前线程在等待唤醒,导致了阻塞.wait

二.线程状态转换简图

三.线程安全问题

 操作系统,调度线程的时候是随机的~~(抢占式执行)正是因为这样的随机性,就可能导致程序的执行出现一些bug~如果因为这样的调度随机性引入了bug,就认为代码是线程不安全的!!!

典型案例:

class Counter{
    public int count;

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

显然输出结果与预期结果10W存在差距. 此时程序就存在bug~~

问题原因:t1和t2是并发执行的,因此存在可能t1和t2在同一时刻对count进行自增,此时count就只会自增一次~~

四.对上述典型案例进行深入探究 

count++到底干了啥???

站在CPU的角度来看待,count++实际上是三个CPU指令!!!​​​

 正因为前面说的"抢占式执行",这就导致两个线程同时执行这三个指令的时候,顺序上充满了随机性~

 

 

 这5w对并发相加中,有时候可能是串行的(+2),有的时候是交错的(+1)

具体串行的有多少次,交错的有多少次,都是随机的~~

极端情况下:(都是极小概率情况下,如果发生,那你可以去买彩票了)

所有操作都是串行的,此时结果就是10w;

所有操作都是交错的,此时结果就是5w.

五. 多线程--解决线程安全问题~

解决上述问题的手段----加锁!!!

通过加锁,就限制了一次只能由一个线程来操作~

 刚才t1已经把锁给占用了,此时t2尝试lock就会发生阻塞~lock会一直阻塞,直到t1线程执行了unlock,

通过这里的阻塞,就把乱序的并发,变成一个串行操作.

并发性越高,速度越快,但是同行可能会出现一些问题,加了锁之后,并发成都就降低了,此时数据就更靠谱了,速度也就慢了~~~

Java中加锁的方式有很多种,最常用的是synchronized这样的关键字

给方法直接加上synchronized关键字,此时进入方法,就会自动加锁,离开方法,就会自动解锁~~~

当一个线程加锁成功的时候,其他线程尝试加锁,就会触发阻塞等待.(此时对应的线程,就处在BOLCKED状态)阻塞会一直持续到,占用锁的线程把锁释放为止~~~

class Counter{
    public int count;

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

加锁之后的执行结果就是10w!!!

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

(一)线程是抢占式执行,线程间的调度充满随机性.

(二)多个线程对同一个变量进行操作(如果是多个线程针对不同的变量进行修改,就没事!如果多个线程针对同一变量读,也没事!!!)

(三)针对变量的操作不是原子的~

       针对有些操作,比如读取变量的值,只是对应一条机器指令,此时这样的操作本身就可以视为是原子的~~

通过加锁操作,也就是把好几个指令打包成一个原子的操作.

(四)内存可见性,也会影响到线程安全!!!

例子:针对同一个变量,一个操作(t1)进行读操作(循环进行很多次),一个线程进行修改操作(t2)(合适的时候执行一次)

 此时一旦t1做出了大胆的假设,此时t2修改了count,t1就不能感知到了

public class Demo1 {
    public static int isQuit = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() ->{
            while (isQuit == 0){

            }
            System.out.println("循环结束,t线程结束");
        });
        t1.start();
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入isQuit的值:");
        isQuit = scanner.nextInt();
        System.out.println("main线程执行完毕");
    }
}

 此时查看执行结果:

 此时main线程修改了isQuit的值,t1线程仍然执行循环~~~

主要原因如下图:

 此时解决上述问题,有两种处理方式:

1.使用synchronized关键字

synchronized不光能保证指令的原子性,同时也能保证内存可见性~~

被synchronized包裹起来的代码,编译器就不敢轻易的做出上述假设~~相当于禁用了编译器优化~~

2.使用volatile关键字

volatile和原子性无关,但是能够保证内存可见性.禁止编译器做出上述优化,编译器每次执行判定相等,都会重新从内存读取isQuit的值~~~

(五).指令重排序,也会影响到线程安全问题~~~

指令重排序,也是编译器优化中的一种操作~~

编译器就会智能的调整这里代码的前后顺序,从而提高程序的效率~~

保证逻辑不变的前提,再去调整顺序~~

如果代码是单线程的程序,编译器的判定一般都是很准~~

如果代码是多线程的,编译器也可能产生误判~~`

synchronized不光能保证原子性,同时还能保证内存可见性,同时还能禁止指令重排序~~

七.synchronized的用法

synchronized(同步的)在多线程中,线程安全中,同步是指"互斥"

使用方式:

1.直接修饰普通的方法.

使用synchronized的时候,本质上是在针对某个"对象"进行加锁~~

当两个线程同时尝试对同一个对象加锁的时候,才有竞争.如果两个线程在针对两个不同的对象加锁,就没有竞争~~

 2.修饰一个代码块

需要显式指定针对哪个对象加锁.(Java中的任意对象都可以作为锁对象)

3.修饰一个静态方法

相当于针对当前类的类对象加锁

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/777243.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号