多线程的安全问题

中断线程:通过调用interrupt()方法通知中断(); 注意该方法不是真实的直接中断,而是告诉某个线程需要进行中断,具体是否要中断,由该线程自己决定();
线程的真实的直接中断方法: stop() (该方法已过期弃用)

中断线程:(1)线程启动以后,中断标志位 = false
(2)在线程运行态中,处理线程中断,需要通过自行判断中断的标志位,来进行中断的处理逻辑。通过方法判断:Thread.currentThread().isInterrupted() / Thread.interrupted();
(3)如果线程因调用了wait()/join()/sleep()而处于阻塞状态,此时调用interrupt中断线程的话,线程会在这三个阻塞方法所在的代码行,直接抛出InterruptedException异常,并且抛出异常后,会重置线程的中断标志位为false;
(4)static void interrupted()方法也可以返回中断标志位,并重置标志位。而void isInterrupted()方法只会返回中断标志位(true/false)
(5)自定义中断标志位满足不了线程处于阻塞状态时的中断操作

观察线程不安全

public class UnsafeThread {

    private static final int NUM = 20;
    private static final int COUNT = 10000;
    private static volatile int SUM;


    public static void main(String[] args) {

        // 同时启动20个线程,每个线程对同一个变量执行操作,循环10000,每次循环++操作
        for(int i = 0; i < NUM; i++){
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    for(int j = 0; j < COUNT; j++){
                        SUM++;
                    }
                }
            });
            t.start();
        }
        while (Thread.activeCount() > 1){
            Thread.yield();
        }
        //1.不是预期的结果200000
        System.out.println(SUM);
    }
}

运行结果:
在这里插入图片描述
可以发现结果不是预测的200000
这是因为SUM++操作可分解为三条指令。
1.读取主内存中的SUM变量,并复制到线程自己的工作内存
2.SUM = SUM + 1
3写回主内存
同样的当两个线程都读取到主内存中的数据,比如是1,当线程1进入代码后拷贝SUM = 1 到自己的工作内存,执行+1操作后,还没写入主内存时,线程二有进入主内存拷贝SUM = 1到自己的工作内存执行+1操作,这时候就会出现两个线程都执行了+1操作,但是他们写回主内存的值是SUM = 2,而不是我们预测的SUM = 3,造成了线程不安全:共享变量发生了修改的丢失。

线程不安全的原因:原子性 可见性 顺序性
原子性:就以卖火车票为例,如果有两个客户端同时在卖票,而当只有一张票的时候,此时客户端A判断剩余票数大于0,将票卖掉,还没有更新数据库的时候,客户端B检查了票数,发现剩余票数大于0,于是又卖了一次,这就出现了一张票被卖了两次的情况。
那么什么是原子性呢?
我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还
没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。
那我们应该如何解决这个问题呢?是不是只要给房间加一把锁,A 进去就把门锁上,其他人是不是就进不来了。这样
就保证了这段代码的原子性了。
有时也把这个现象叫做同步互斥,表示操作是互相排斥的。

特殊的原子性代码(分解指令操作可能存在编译为class文件时,也可能存在cpu执行指令的时候)
(1)n++,n–,++n,–n都不是原子性需要分解为三条指令:从内存读取变量到cpu,修改变量,写回到主内存
(2)对象的new操作:Object obj = new Object(); 分解为三条指令:分配对象的内存,初始化对象,将对象赋值给变量。

可见性:为了提高效率,JVM在执行过程中,会尽可能的将数据从主内存中复制到线程自己的工作内存中执行,但这样会造成一个问题,每个线程不能看到别的线程的变量,造成共享变量在多线程之间不能及时看到改变,这个就是可见性问题

顺序性:如果一段代码是这样的:

  1. 去前台取下 U 盘
  2. 回家里去取银行卡
  3. 去前台取下快递
  4. 去银行取钱
    如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2 ->4的方式执行,也是没问题,可以少跑一次前台。这种叫做指令重排序
    但是在多线程下,由于每个线程看不到其他线程的操作,每个线程优化就会出现在线程A还没有取卡的情况下,线程B就直接去取钱了,这就出现了错误。

重排序:线程内代码是JVM,CPU都进行重排序,给我们的感觉是线程内的代码都是有序的,是因为重排序优化方案会保证线程内代码执行的依赖关系。线程内看自己的代码运行,都是有序;但是看其他的代码运行,都是无序的。

那么怎么解决这些问题呢:继续看一段代码:

public class SafeThread {
    private static final int NUM = 20;
    private static final int COUNT = 10000;
    private static int SUM;

    /**
     * 等同于
     * synchronized(SafeThread.class){}
     */
    public static synchronized void increment(){  //对当前类对象进行加锁
        SUM++;
    }
//    public static void increment(){
//        synchronized (SafeThread.class){
//            SUM++;
//        }
//    }

    /**
     * 等同于
     * synchronized(this){}
     */
    // new SafeThread().increment2();
    public synchronized void increment2(){ //对this对象进行加锁
        SUM++;
    }
//    public void increment2(){ //对this对象进行加锁
//        synchronized (this){
//           SUM++;
//        }
//    }

    public static void main(String[] args) {

        // 同时启动20个线程,每个线程对同一个变量执行操作,循环10000,每次循环++操作

        for(int i = 0; i < NUM; i++){
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    for(int j = 0; j < COUNT; j++){
//                        increment(); //@1
                        //@1和@2加@3的效果等同
                        synchronized (SafeThread.class){  //@2
                            SUM++; //@3
                        }
//                        synchronized (this){  //@4 对Runnable对象进行加锁,
//                                              // 而每个对象都new了一个Runnable对象,
//                                              // 不是竞争同一把锁
//                            SUM++; //@5
//                        }
                    }
                }
            });
            t.start();
        }
        while (Thread.activeCount() > 1){
            Thread.yield();
        }
        System.out.println(SUM);
    }
}

运行之后发现结果就是我们预期的200000:
在这里插入图片描述
这是因为我们使用了synchronized关键字,synchronized(三大特性都能保证)
synchronized的底层是使用操作系统的mutex lock实现的。
当线程释放锁时,JMM会把该线程对应的工作内存中的共享变量刷新到主内存中
当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。
synchronized加锁操作关注点:(1)对哪一个对象进行加锁(一个对象只有一把锁)
如上述代码第@4行,synchronized (this) this表示对当前对象进行加锁,代码中的当前对象正是Runnable对象,根据代码不难发现每个线程都new了一个Runnable对象,也即是每个线程都将获取锁,不能达到所有线程竞争同一把锁的情况,因此这样是不行的,因为**(2)只有同一个对象才会有同步互斥的作用**。但是如果我们将这几行代码提出来,如public void increment2()这个方法,这里的this对象就代指SafeThread.class对象。
(3)对于synchronized内的代码来说,在同一个时间点只有一个线程在运行(没有并发,并行。
4)运行的线程越多,性能下降越快(归还对象锁的时候就有越多的线程不停的在被唤醒,阻塞状态切换)

synchronized总结:
进入synchronized代码行时,需要获取对象锁:
获取成功,往下执行代码;获取失败,阻塞在synchronized代码行
退出synchronized代码块或synchronized方法:
1,退回对象锁; 2.通知JVM及系统,其他线程可以来竞争这把锁

synchronized的有序性是指线程之间运行同步代码块是按序执行的,不是指令级别的,与volatile的禁止指令重排序不同

volatile —保证可见性,保证有序性
禁止指令重排序
建立内存屏障
所以如上代码,即使我们给SUM加上volatile关键字也不能保证最后结果,因为volatile不能保证原子性。
因此,volatile修饰的变量,进行赋值不能依赖变量(常量赋值可以保证线程安全)

使用场景:volatile可以结合线程加锁的一些手段,提高线程效率,只是变量的读取,常量赋值,可以不加锁,而是使用volatile,可以提高效率

多线程的操作考虑:1.安全 2.效率
在保证安全的情况下尽可能地提高效率:
1.代码执行时间比较长,考虑多线程(线程的创建,销毁的时间消耗)
2.如果不能保证安全,所有的代码都没有意义 — 先保证安全在保证效率。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

运笔如飞

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

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

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

打赏作者

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

抵扣说明:

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

余额充值