Java中线程的状态分为6中:
1、初始化(new):创建一个新线程对象,但是没还有调用start()方法。
2、运行(runnable):Java线程中将就绪(ready)和运行中(running)两者状态统称为“运行”。
线程对象在创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程获得CPU的时间片后变为运行中状态(running)。
3、阻塞(blocked):表示线程阻塞于锁。
4、等待(waiting):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
5、超时等待(time_waiting):进入该状态的线程不同于waiting,它可以在指定的时间后自行返回
6、终止(terminated):表示该线程已经执行完毕。
状态之间的变迁如下如所示:
是指两个或者两个以上的进程在执行过程中,由于竞争资源或者由于彼此通讯你而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。
举个例子:
A、B两位厨师同时想炒辣椒炒肉(假设辣椒和肉可以多次使用),这时候A先抢到了辣椒,B抢到了肉;但是两个人都想做辣椒炒肉,这样的话,A抢到了辣椒想要肉,B抢到了肉想要辣椒,在这个同时想做辣椒炒肉的事情上A和B就产生了死锁。怎么解决这个问题呢?
1、假如这时候来了个送菜的,正好送的是肉;B本来抢到的是肉,于是这个肉就归A了。这样A就可以做辣椒炒肉了
2、这是老板来了,说必须先有辣椒才能做辣椒炒肉,这种情况下,A和B谁先抢到辣椒,谁就可以做辣椒炒肉了,另外一个就等着,这样也不会产生死锁。
总结一下:
1、死锁必然是多个操作者(M>=2),争夺多个资源(N>2),而且N<=M才会发生。
2、争夺资源的顺序不对,如果争夺的顺序是一样的,也不会产生死锁
3、争夺者拿到手的资源不放手
学术话的定义叫:
1、互斥条件:就是指进程对锁分配到的资源进行排他性使用,即在一段时间内某资源只能由一个进程占用,如果还有其他进程请求资源,则请求者只能等待,直至占有资源的进程用完后释放。
2、请求和保持:指某个进程已经拿到了一个资源,但是有提出了新的资源请求(请求),但是该资源被其他进程占有了,此时请求进程阻塞,但又对自己获得的资源保持不放。
3、不剥夺条件:指进程已获得的资源,在未用完之前,不能被剥夺,只能在使用完之后自己释放。
4、环路等待:指发生死锁时,必然存在一个进程--资源的环形链,即进程集合{P0,P1,P2,P3,P4}中的P0等待P1,P1等待P2.............P4等待P0。
代码如下:
package com.example.retrofit.ex2;
import com.example.retrofit.cn.tools.SleepTools;
public class DeadLockTest {
private static Object objectA= new Object();
private static Object objectB= new Object();
public static class Threaduse01 extends Thread{
@Override
public void run() {
synchronized (objectA){
System.out.println(Thread.currentThread().getName()+"objectA");
// 休眠100ms,是为了条件需要,不加休眠的话,可能Threaduse01就一次性拿到了
// objectB、objectA两把锁,就不能重现死锁的条件了
SleepTools.ms(100);
synchronized (objectB){
System.out.println(Thread.currentThread().getName()+"objectB");
}
}
}
}
public static class Threaduse02 extends Thread{
@Override
public void run() {
synchronized (objectB){
System.out.println(Thread.currentThread().getName()+"objectA");
// 休眠100ms,是为了条件需要,不加休眠的话,可能Threaduse02就一次性拿到了
// objectB、objectA两把锁,就不能重现死锁的条件了
SleepTools.ms(100);
synchronized (objectA){
System.out.println(Thread.currentThread().getName()+"objectB");
}
}
}
}
public static void main(String[] args){
new Threaduse01().start();
new Threaduse02().start();
}
}
只要打破四个必要条件之一就能优先的预防死锁:
1、打破互斥条件:改造独占性的资源为虚拟资源,大部分资源已无法改造
2、打破不可剥夺条件:当一进程占有一独占资源后又去申请一独占资源而无法满足,则推出占有的资源
3、打破占有且申请条件:采用资源预先分配策略,即进程运行前申请全部资源,满足则运行,不然就等待,这样就不会保持和请求了
4、打破循环等待:实现资源有序分配策略,对所有设备实现分类编号,所有进程只能采用安许号递增的形式申请资源。
避免死锁常见的算法有有序资源分配法、银行家算法。
1、线程不工作但是整个程序还是活着的
2、没有任何异常信息可以供我们检查
3、一旦程序发生死锁,是没有任何办法恢复的,只能重启程序,这对于已经发布的程序来说,是个很严重的问题。
解决死锁:
1、内部通过顺序比较,确定哪所的顺序
代码如下:
package com.example.retrofit.ex2;
import com.example.retrofit.cn.tools.SleepTools;
public class DeadLockTest {
private static Object objectA= new Object();
private static Object objectB= new Object();
public static class Threaduse01 extends Thread{
@Override
public void run() {
synchronized (objectA){
System.out.println(Thread.currentThread().getName()+"objectA");
// 休眠100ms,是为了条件需要,不加休眠的话,可能Threaduse01就一次性拿到了
// objectB、objectA两把锁
SleepTools.ms(100);
synchronized (objectB){
System.out.println(Thread.currentThread().getName()+"objectB");
}
}
}
}
public static class Threaduse02 extends Thread{
@Override
public void run() {
synchronized (objectA){
System.out.println(Thread.currentThread().getName()+"objectA");
// 休眠100ms,是为了条件需要,不加休眠的话,可能Threaduse02就一次性拿到了
// objectB、objectA两把锁
SleepTools.ms(100);
synchronized (objectB){
System.out.println(Thread.currentThread().getName()+"objectB");
}
}
}
}
public static void main(String[] args){
new Threaduse01().start();
new Threaduse02().start();
}
}
2、采用尝试拿锁的机制(trylock),采用显示调用锁ReentrantLock,代码如下:
package com.example.retrofit.ex2.lock;
import com.example.retrofit.cn.tools.SleepTools;
import com.example.retrofit.concurrent.theory.aqs.ReenterSelfLock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TryLockTest {
private static Lock objectA= new ReentrantLock();
private static Lock objectB= new ReentrantLock();
public static class Threaduse01 extends Thread{
@Override
public void run() {
while (true){
if (objectA.tryLock()){
System.out.println(Thread.currentThread().getName()+"objectA");
try {
if (objectB.tryLock()){
try {
System.out.println(Thread.currentThread().getName()+"objectB");
break;
}finally {
objectB.unlock();
}
}
}finally {
objectA.unlock();
}
}
SleepTools.ms(10);
}
}
}
public static class Threaduse02 extends Thread{
@Override
public void run() {
while (true){
if (objectB.tryLock()){
System.out.println(Thread.currentThread().getName()+"objectB");
try {
if (objectA.tryLock()){
try {
System.out.println(Thread.currentThread().getName()+"objectA");
break;
}finally {
objectA.unlock();
}
}
}finally {
objectB.unlock();
}
}
// 假如不休眠的话,会产生活锁
SleepTools.ms(10);
}
}
}
public static void main(String[] args){
new Threaduse01().start();
new Threaduse02().start();
}
}
其他线程安全问题:
活锁:
两个线程在尝试拿锁的机制中,发生两者之间相互谦让,不断发生同一个线程总是拿到同一把锁,在尝试拿另外一把锁时因为拿不到,而将本来已经持有的锁释放的过程。
解决办法:每个线程休眠随机数,错开拿锁的时间,比如上面的采用尝试拿锁的机制。
线程饥饿:
优先级低的线程,总是拿不到锁。
什么是原子操作?
假设有两个操作A和B,如果从执行A的线程来看,当另一个线程执行B时,要么将B全部执行完,要么完全不执行,那么A和B对彼此来说就是原子的。
如何实现原子操作?
其实我们可以通过锁的机制来实现,但是锁synchronized也有其缺点,那就是:
1、可能死锁
2、可能锁的代价比我们的业务逻辑代价还要大,锁的上下文切换(非常耗时)
3、大量线程来竞争锁的话,导致CPU开销大
所以我们需要一种更加轻量级、有效、灵活的机制,这就是CAS。那么CAS,我们怎么理解呢?
看图:
看图有点难理解,举个例子:
模仿count=0,count=count++:
假如A、B、C、D四个人身上一开始带了0块钱,他们一起去预定买包子(包子初始价格为0块钱,每预定一个人,价格+1)。这时候A和老板说,这个包子我预定了,老板答应,A的任务完成了,包子价格这是是1块钱;B这时候和老板说要预定包子,老板说包子价格1块钱,B、C、D这时候身上只有0块钱,于是他们都去借1块钱,B这时候和老预定包子,老板答应了,B的任务完成了,包子价格为2块钱;轮到C、D预定的时候,发现包子要2块钱,于是C、D就又去借钱1块钱,C预定了包子,包子价格为3块钱,C完成;D又跑去借1块钱钱,预定包子,包子为4块钱,D完成。
CAS实现原子操作的三大问题:
1、ABA问题
因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果原来一个值的A,变成了B,又变回了A,那么使用CAS进行检查时会发现它的值没有发生变化,但实际上却变化了。
ABA问题的解决思路就是使用版本号,在变量前面追加版本号,每次变量更新的时候把版本号+1,那么A->B-C就变成了1A->2B->3C了。
举个例子:
假如你在上班的时候,桌子上放了一杯水,这时候你去上个厕所,正好旁边的同事口渴了,喝了你杯子里面的水,又重新给你倒了一杯水,你回来一看谁还在,拿起来就喝,如果不管水中间被人喝过,只关心水还在,这就是ABA问题。
如果你想解决这个问题,那么在被子上放一个纸条,初始值为0,别人喝水的话麻烦先做个累加才能喝水。
2、循环时间长,开销大
自旋CAS如果长时间不成功,会给CPU带来非常大的消耗
3、只能保证一个共享变量的原子操作
如果想改变多个变量可以使用AtomicReference,封装成对象处理;当然有些逻辑封装成对象处理不了,这时候就需要synchronized来处理了
JDK中常见的相关原子操作类有:
AtomicInteger、AtomicArray、AtomicReferene、AtomicStampedReferene、AtomicMarkbleReferene
AtomicStampedReferene:利用版本戳的形式记录了每次改变后的版本号,这样就不会存在ABA问题了。AtomicStampedReferene是使用的pair的int stamp作为计数器使用,AtomicMarkbleReferene是pair使用的是boolean mark。还是水杯那个例子,AtomicStampedReferene可能关心的是动了几次杯子,AtomicMarkbleReferene关心的是杯子释放有被动过。
代码示例:
package com.example.retrofit.ex2.cas;
import java.util.concurrent.atomic.AtomicReference;
public class AtomicReferenceTest {
public static AtomicReference reference = new AtomicReference<>();
public static void main(String[] args){
Student student1 = new Student(11,"mick");
reference.set(student1);
Student student2 = new Student(15,"killa");
reference.compareAndSet(student1,student2);
System.out.println(student1.toString());
System.out.println(reference.get().toString());
Student student3 = new Student(20,"huni");
// 模拟多线
new Thread(){
@Override
public void run() {
// 这个地方的expect参数需要是比较和交换之后的数据:student2
//也就是说比较交换之后要更新数据,否则CAS失败
// 大家可以把student2改外student1试试看
boolean set = reference.compareAndSet(student1,student3);
System.out.println("compareAndSet = "+set+" reference.get()= "+reference.get().toString());
}
}.start();
}
public static class Student{
private int age;
private String name;
public Student(int age, String name) {
this.age = age;
this.name = name;
}
public int getAge() {
return age;
}
public String getName() {
return name;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
}