线程(二)——线程安全

如何理解线程安全:

        多线程并发执行的时候,有时候会触发一些“bug”,虽然代码能够执行,线程也在工作,但是过程和结果都不符合我们的开发时的预期,所以我们将此类线程称之为“线程安全问题”。

        例如:在多个线程并发的时候,操作系统对多线程的调度特性会导致结果存在偶然性,这个偶然性可能很小,但也不小(eg:假设偶然性为0.1‰,并发的线程有200_000个,那出现的偶然性结果也会有200个,也就是每20w个用户就会影响到200个用户体验,如果是更大体量的那就影响更大了)

代码实例:如图,我们的实例预期结果本来应该是5000+5000 =10000

多次运行结果都不一样,为什么不一样呢?         问题的关键就在于——并发执行会有偶然性,如果是串行执行那么就不会有问题~

  进一步来体会并发执行的过程:

 从内核的时间轴来看线程代码的执行

 

        通过上述的这些问题,我们再细致的说说线程不安全的原因:

1、内核对线程调度的随机性(非人力能干涉的不可控因素~)

2、当前代码有多个线程对变量进行操作:(变量也可以是硬盘上的数据/网络上的数据)

        ①多个线程修改同一个变量——>不安全,代码在执行时如果被其他的线程抢占执行,那么结果很有可能就是错的~

        ②多个线程读取同一个变量——>没事儿~只读不改,就相当于每个线程在内存中拷贝一份这个变量过来~

        ③多个线程修改不同的变量——>没事儿~你改你的,我改我的,井水不犯河水~

        ④单个线程修改同一个变量——>没事儿~每改一次就从内存拿出来一次,改完就放回内存去~

 3、线程针对变量的修改不是原子性的(如果线程不是抢占式执行,那么没有原子性也没有关系~)

什么是原子性?

        所谓“原子性”,就是不可拆分的最小单位,也就是说,当对一个变量的修改是执行一个最小量级的命令——CPU指令,则称这个操作是具有原子性的。

        拿上述count++举例,count++这个语句在CPU内核中是分为三步实现:在内存中将count拿出来,在CPU寄存器上进行count+1,将计算结果放回内存。这一句简单的语句需要三个指令来实现,那么对count这个变量的修改就不具备原子性~

可见性:

        多个线程在修改同一个变量的时候,能够让其他的线程同时看见这个变量。

JMM里的模拟内存:

        每一个线程都有各自的一块工作内存,在线程创造的时候申请分配工作内存,线程销毁的时候释放工作内存。对一个变量进行修改不会直接在主内存内对变量进行修改,而是在主内存中拷贝一份到线程的工作内存中进行修改,然后进行数据更新后再写入主内存~

内存的可见性:

众所周知,每个线程都会申请一块属于自己的工作内存,对于数据的修改,线程总是先从主内存拷贝一份到工作内存,然后在寄存器上进行操作之后再将数据放回内存。

当有多个线程对同一个变量操作时,如何让两个线程的操作都是有效的?这时候就涉及到内存的可见性~

例:现有两个线程,t1线程只有在线程t2通知之后才会停下,而我们利用控制台输入一个非0的整数来控制t2通知t1

 static class Counter {
        public int count = 0;
    }
    public static void main(String[] args) throws InterruptedException {

        Counter counter = new Counter();
        Thread t1 = new Thread(()->{
            while(counter.count == 0)
            {

            }
            System.out.println(Thread.currentThread().getName()+"接到通知,马上停下来...");
        },"t1");
        Thread t2 = new Thread(()->{
            Scanner sc = new Scanner(System.in);
            System.out.println(Thread.currentThread().getName()+"发出停止通知...");
            counter.count = sc.nextInt();

        },"t2");
     t1.start();
     t2.start();


    }

可是结果却是t1没有收到t2的通知

为什么会这样?这是因为t2将修改之后的变量刷新到内存,但是这个结果没有在t1中同步刷新,所以就产生了上述的结果

如何解决这一点?用volatile修饰count即可~

拿上面的例子思考一下,如何避免获得上述这种抢占式执行的结果?

①要想避免这种抢占式执行产生的结果,最好的做法就是给线程上锁

②当两个线程执行过程要对同一个变量进行修改的时候,修改的那段代码可以加上同一个锁,这样就会阻塞其中一个线程让其等待,等锁内的线程执行完工作内容再接着执行另一个线程

synchronized关键字:

        对于多个线程针对同一个对象,我们如果想要保证程序的原子性,那么就得给这个对象加一个锁,而synchronized就是干这件事的~

       进入synchronized(){}的作用域中表示加锁,执行完作用域中的代码就进行解锁。

        如果synchronized针对的是多个对象,那么就不会产生锁竞争,也就不会出现阻塞等待,线程各自干各自的活儿。

public class demo2 {
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Object locker2 = new Object();
        Thread t1 = new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"加锁前....");
            synchronized (locker)
            {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println(Thread.currentThread().getName()+"解锁后....");
        },"线程1");
        Thread t2 = new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"加锁前....");
            synchronized (locker2)
            {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println(Thread.currentThread().getName()+"解锁后....");
        },"线程2");

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

        t1.join();
        t1.join();
    }
}

结果就是两个线程是并发执行的,各干各的,没有产生阻塞等待~

synchronized具有不可抢占性——即如果有人已经持有这把锁,那么在这把锁释放之前其他的线程是拿不到的。

用实例体会一下抢锁的过程: 


public class demo3 {

        public static void main(String[] args) throws InterruptedException {
            Object locker = new Object();
            Thread t1 = new Thread(()->{
                synchronized (locker)
                {
                    System.out.println(Thread.currentThread().getName()+"抢到锁进行加锁....");
                }
            },"线程1");
            Thread t2 = new Thread(()->{
                synchronized (locker)
                {
                    System.out.println(Thread.currentThread().getName()+"抢到锁进行加锁....");
                }
            },"线程2");
          Thread t3 = new Thread(()->{
                synchronized (locker)
                {
                    System.out.println(Thread.currentThread().getName()+"抢到锁进行加锁....");
                }
            },"线程3");
            t1.start();
            t2.start();
            t3.start();
        }
    }

通过结果,我们可以明白——针对同一个对象进行加锁,谁能先拿到锁是随机的。但是,为什么这里的线程1能一直先拿到锁?这是因为后面t2、t3线程启动需要时间,在这短短的启动时间里,t1可以先获得锁~

在其他的语言中,加锁操作并不是一个synchronized就能搞定,而是线程{     lock();   其他代码.....;   unlock();   }~有时候往往会把unlock()给忘了,这时候就出错了,而synchronized在封装过程中这些都帮我们写好了,我们直接用没有后顾之忧~

wait()和notify():

调用wait()方法干的事:

①让当前线程进行阻塞

②释放当前的锁

③满足一定条件被唤醒,重新尝试获取这个锁(不一定唤醒了就能获取的到,依旧是和其他线程抢占执行)

class WaitTask implements Runnable{
    private Object locker  = new Object();

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

    @Override
    public void run() {
        synchronized (locker)
        {
            try {
                System.out.println("开始阻塞...");
                locker.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

        }
    }
}
class NotifyTask implements Runnable{
    private Object locker = new Object();
    public NotifyTask(Object locker)
    {
       this.locker = locker;
    }
    @Override
    public void run() {
        synchronized (locker)
        {
               locker.notify();
            System.out.println("线程已经被唤醒...");
        }
    }
}
public class demo1 {
    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 NotifyTask(locker));

        t1.start();
//        t2.start();
//        t3.start();
        Thread.sleep(5000);
        t4.start();
    }
}

唤醒线程的方法:①调用该对象的notify()方法;②wait()等待超时;③其他线程调用Interrupted方法,抛出InterruptedExption异常。 

        notify()方法是唤醒等待中的线程,当有多个线程处于wait()时,由线程调度器随机挑选一个进行唤醒(依旧是没有先到先得的原则)
wait()要搭配synchronized一起使用,不然就会抛异常
当线程调用wait()之后需要调用notify()来唤醒线程,不然就是让程序死等

notify()一次只能唤醒一个线程,而notifyAll()能够一次性唤醒所有线程
一次性唤醒所有等待的线程之后依旧是抢占式执行,依旧有先后执行顺序

区分wait()和sleep():
wait()是用于线程之间的通信,而sleep()只是单纯地让线程阻塞一段时间
1.wait()需要和synchronized搭配使用,而sleep不需要
2.wait()是Object类的方法,而sleep()是Thread类的静态方法


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值