Java线程安全相关理论

多线程原因

说线程产生的原因之前,我们先说进程的产生。我们都知道刚开始电脑特别大,而且特别贵,最主要还特别慢,那时的cpu速度和外面的硬盘速度差不多。但人们对速度的追求是无情的,随着cpu的频率奔跑,于是硬盘明显跟不上,于是有了内存做缓存。有了内存有人想同时执行多个任务,因为单个任务老是会因为等IO中断浪费时间,于是把任务保存在内存里。便有了多进程。可以在等IO的时候执行另一个任务。
线程同理是为了把进程的执行顺序进行控制,使得cpu充分利用。并且因为现在的UI系统,所以需要线程来实现耗时操作后台运行。

线程安全

有了多线程之后于是就有了进程类似的安全问题。如何达到线程安全呢,线程安全:在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。如何保证呢,其实就三点:

原子性,就是指有一个操作,像boolean的操作都是原子的,而i++不是,他是先读后加再写回去。
可见性:这个可以看下图的java内存模型,可以知道每个线程都有自己的工作内存,所以为了统一就需要一个更改后其他都知道,也就是可见性。为啥会有这个模型,因为内存已经满足不了我们对速度的追求了,想当年比尔盖茨说有640k就够了,而现在手机都上8G了。可见人们对速度的追求有多恐怖。即使这样还不行,所以cpu就有了三级缓存。当然这模型只是规范,千万不要理解成具体实现。

这里写图片描述

有序性:话说这是啥玩意,原来我们代码执行的时候并不是按我们写的顺序来执行的,同样因为人们对速度的追求,在编译器、cpu流水线及内存系统都会有重排序。他们遵循的是as-if-serial原则,也就是只保证单线程状态下结果不变。那怎么办呢,不急。上面的java内存模型给了我们一些承诺,也就是happens-before的8项原则:
  1. 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
  2. 锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作;
  3. volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
  4. 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
  5. 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
  6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
  7. 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
  8. 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始。

感觉8个原则都是废话啊,其实大多数情况只要关心原子性,也就是直接加锁。
唯一有用的也就第三个了,Java中最神秘的Volatile关键字,话说神秘是因为老没用!通过第三个可以知道volatile能达到可见性,而JMM原子也保证其有序性。但是还要保证对变量的操作是原子性的,所以你写i++;Volatile也帮不了你啊。

互斥与同步

线程互斥:由独占性资源导致同时只允许一个访问者对其进行访问。如对资源同时访问就会导致结果的不可预测性。这时我们加锁达到互斥访问叫做线程互斥。
线程同步:线程按预定的先后次序进行运行,如A线程需要B线程生成的一个对象。则要等待B生成后才运行。实现这种次序我们叫线程同步。JAVA语言提供了wait()、notify()和notifyAll()三个方法供线程在临界段中使用。

这里写图片描述
可以看到线程互斥与同步都会使一个线程进入阻塞状态,如不合理就会产生下面的死锁。

死锁与阻塞

阻塞: 一个线程在执行过程中暂停,以等待某个条件的触发。Java使用抢占式线程调度,高优先级可以抢在低之前(跟具体实现有关)。故不存在一个线程因处理东西多而长时间阻塞其他线程的情况。
死锁:两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象。

产生死锁的四个必要条件:
(1) 互斥条件:一个资源每次只能被一个线程使用。
(2) 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不剥夺条件:线程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。

死锁处理方式:

(1) 不处理,鸵鸟算法。我们大部分人用的。
(2) 死锁预防:破坏导致死锁必要条件中的任意一个就可以预防死锁。打破互斥条件:改造独占性资源为虚拟资源,大部分资源已无法改造。打破不可抢占条件:当一进程占有一独占性资源后又申请一独占性资源而无法满足,则退出原占有的资源。打破占有且申请条件:采用资源预先分配策略,即进程运行前申请全部资源,满足则运行,不然就等待,这样就不会占有且申请。打破循环等待条件:实现资源有序分配策略,对所有设备实现分类编号,所有进程只能采用按序号递增的形式申请资源。
(3) 死锁避免: 避免是指进程在每次申请资源时判断这些操作是否安全,例如,使用银行家算法。
(4)检测解除:检测到思索后重新分配资源。
死锁预防与避免的区别:预防是静态的破坏必要条件,较易实现,效率较差。避免是运行的判断资源分配后是否有可能死锁,不易实现,效率较好。而检测解除效率最高但也最难实现。

猜你喜欢

转载自blog.csdn.net/wanyouzhi/article/details/80899049