线程与进程理论知识入门(三)

线程的生命周期

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):表示该线程已经执行完毕。

状态之间的变迁如下如所示:

线程与进程理论知识入门(三)_第1张图片

 死锁:

是指两个或者两个以上的进程在执行过程中,由于竞争资源或者由于彼此通讯你而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。

举个例子:

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();
    }
}

其他线程安全问题:

活锁:

        两个线程在尝试拿锁的机制中,发生两者之间相互谦让,不断发生同一个线程总是拿到同一把锁,在尝试拿另外一把锁时因为拿不到,而将本来已经持有的锁释放的过程。

解决办法:每个线程休眠随机数,错开拿锁的时间,比如上面的采用尝试拿锁的机制。

线程饥饿:

        优先级低的线程,总是拿不到锁。

CAS基本原理:(compare and swap,比较和交换)

什么是原子操作?

假设有两个操作A和B,如果从执行A的线程来看,当另一个线程执行B时,要么将B全部执行完,要么完全不执行,那么A和B对彼此来说就是原子的。

如何实现原子操作?

其实我们可以通过锁的机制来实现,但是锁synchronized也有其缺点,那就是:

        1、可能死锁

        2、可能锁的代价比我们的业务逻辑代价还要大,锁的上下文切换(非常耗时)

        3、大量线程来竞争锁的话,导致CPU开销大

所以我们需要一种更加轻量级、有效、灵活的机制,这就是CAS。那么CAS,我们怎么理解呢?

看图:

线程与进程理论知识入门(三)_第2张图片

 看图有点难理解,举个例子:

模仿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 + '\'' +
                    '}';
        }
    }
}

你可能感兴趣的:(java,开发语言)