线程的深度剖析

线程和进程的区别和联系:

为啥要有线程?(1)并发编程,随着多核CPU的发展,成为刚需(2)多进程虽然也能实现并发编程效果,但是进程的创建和销毁,太重量了。

1.进程可以处理多并发编程的问题,但是进程太重,主要指它的资源分配和资源回收上,进程消耗资源多,速度慢,其创建、销毁、调度一个进程开销比较大。

2.线程的出现有效地解决了这一问题,既可以处理并发编程的问题,又因为它可以实现资源共享(主要指的是内存和文件描述符表),内存共享是指线程1new的对象在线程2,3,4里面可以直接用,文件描述符表共享指的是线程1打开的文件在线程2、3、4里面可以直接使用,第一个进程开销比较大,以后的进程都可以和第一个进程共享资源,这样就减少了资源的消耗,又提高了速度

 举一个比较生动的例子:

有一个人要做一百只鸡,为了加快速度我们可以考虑一下再来一个人一起做,给每个人安排一间屋子,一个桌子

但是这样就比较耗费资源,这就是多进程。但是如果我们让两个人在同一间屋子同一个桌子上做,这样可以减少资源的开销了,屋子桌子共享

这就是 多线程。

3.进程里面包含多个线程,每个线程对应一个PBC,同一个进程里的PCB之间pid相同,同一个进程里的内存指针和文件描述符号表一样。

4.进程里面有多个线程,每个线程独立在CPU上调度,每个线程有自己的执行逻辑,线程操作是系统调度执行的基本单位。

5.增加线程的数量也不意味着可以一直提高速度,CPU的核心数量有限。

6.线程安全问题:因为线程之间可以资源共享,容易产生资源争夺的问题,会导致线程的异常问题,进而带走整个进程。

一.线程的创建:

// 定义一个Thread类,相当于一个线程的模板
class MyThread01 extends Thread {
    // 重写run方法// run方法描述的是线程要执行的具体任务@Overridepublic void run() {
        System.out.println("hello, thread.");
    }
}
 

public class Thread_demo01 {
    public static void main(String[] args) {
        // 实例化一个线程对象
        MyThread01 t = new MyThread01();
        // 真正的去申请系统线程,参与CPU调度
        t.start();
    }
}
// 创建一个Runnable的实现类,并实现run方法
// Runnable主要描述的是线程的任务
class MyRunnable01 implements Runnable {
    @Overridepublic void run() {
        System.out.println("hello, thread.");
    }
}

public class Thread_demo02 {
    public static void main(String[] args) {
        // 实例化Runnable对象
        MyRunnable01 runnable01 = new MyRunnable01();
        // 实例化线程对象并绑定任务
        Thread t = new Thread(runnable01);
        // 真正的去申请系统线程参与CPU调度
        t.start();
    }
}
* 通过Thread匿名内部类的方法创建一个线程

public class Thread_demo03 {public static void main(String[] args) {
        Thread t = new Thread(){
            // 指定线程任务
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        };
        // 真正的申请系统线程参与CPU调度
        t.start();
    }
}
* 通过Runnable匿名内部类创建一个线程

public class Thread_demo04 {public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            // 指定线程的任务
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());                
            }
        });
        // 申请系统线程参与CPU调度
        t.start();
    }
}
* 通过Lambda表达式的方式创建一个线程

public class Thread_demo05 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            // 指定任务:任务是循环打印当前线程名
            while (true) {
                System.out.println(Thread.currentThread().getName());
                try {
                    // 休眠1000ms
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        // 申请系统线程参与CPU调度
        t.start();
    }
}

二.Thread类的几个常见方法:

getId()    获取id

getName() 获取线程名称

getState()获取当前线程的状态

getPriority()优先级

isDaemon()是否后台线程

isAlive()是否存活

isInterrupted() 是否被中断

三.线程的执行:

1.线程之间是并发执行的,并且是抢占式调度的。

public static void main(String[] args) {
        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                //System.out.println("hello");
                System.out.println("1");

            }
        });
        thread.start();
        //System.out.println("world");
        System.out.println("2");

    }

像这段代码我们无法确定是先打印“1”还是先打印“2”

2.start 方法和run()方法的区别:

run()方法里面是要执行的任务,我们创建了一个thread,只是把任务梳理好了,start方法才真正去创建线程,让内核创建一个PCB,此时PCB才表示一个真正的线程,去执行run()方法里面的任务

3.线程一旦执行完,内核里的pcb就会释放,操作系统里面的线程也就没了

public static void main(String[] args) {
        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                //System.out.println("hello");
                System.out.println("1");

            }
        });
        thread.start();
        //System.out.println("world");
        System.out.println("2");

    }

像这段代码执行main方法的线程在执行完打印“2”这条语句后就没了,像thread线程执行完run方法,也就没了,但thread这个对象还存在,当它不指向任何对象时,就会被GC回收,之所以PCB消亡,而代码中thread对象还存在,是因为java中的对象的生命周期,自有其规则,这个生命周期和系统内核里的线程并非完全一致,内核里的线程释放的时候,无法保证java代码中thread对象也立即释放,因此此时需要通过特定的状态,来把thread对象标识成’无效‘,也是不能重新start的,一个线程,只能start一次。

4.isAlive()方法用来判断系统里面的线程是不是真正创建好了。

Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(1);
            }
        });
        thread.start();

            try {
                Thread.sleep(1000);
                System.out.println(thread.isAlive());
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

    }

如果thread的run还没跑,此时thread.isAlive()返回的是false,如果thread的run正在跑,此时返回的是true,如果thread里的run跑完了,此时返回的是false,此时内核里的pcb就释放了,操作系统里的线程也就没了。

5.public static void sleep(long millis)

让线程休眠,本质上就是让当前sleep的线程,暂时不参与CPU的调度执行(把这个线程PCB放到一个表示阻塞状态的队列中)等到sleep的时间到,操作系统会把这个PCB拿回到就绪队列中。

public static void main(String[] args) {
        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 0;
                for (i = 0; i < 3; i++) {


                    System.out.println("hello");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }
            }



        });
        thread.start();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("world");
    }

四.线程的终止:

我们通常使用interrupt()方法来终止线程,但是终不终止还是线程说了算。Thread提供了内置的标志位,可以使用isInterruptted方法判断标志位,使用interrupt方法来设置标志位(还能把线程从休眠中唤醒)

public static void main4(String[] args) {
        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                while(!Thread.currentThread().isInterrupted())
                {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println(1);
                }

            }
        });
        thread.start();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        thread.interrupt();
        //System.out.println(2);
    }

 在这段代码中Thread.currentThread()表示来获取当前的线程的引用,isInterrupted()表示是否受到了要终止的通知。

一开始的时候isInterrupted()是false,进入到while循环里面,当执行main函数的线程,执行到thread.interrupt()的时候thread线程此时被通知要终止,此时isInterrupted()被设置为true,按照正常情况而言此时while循环是进不去的,但是如果线程在进行sleep,就会触发异常,把刚才的isInterrupted()再设置回false,此时还可以进行循环,这时要不要终止线程,其实取决于我们,如果加上一个break,跳出循环,那么线程也就执行完了。

线程的等待:控制两个线程的结束顺序:

 Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
               int i=0;
               for(i=0;i<8;i++)
               {
                   System.out.println(1);
               }
            }
        });
        thread.start();
        System.out.println(3);
        try {
            thread.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(2);
    }

原本main函数的线程在执行完start语句的时候,main函数线程要去执行下边的代码,tthread这个线程要去执行run()方法里面的任务,但是当main函数线程执行到hread.join()这条语句表示执行main函数的线程在这里等待一下,先等thread这个线程执行完,再执行。

五.操作系统的内核:

就绪队列:

这个链表里的PCB都是''随叫随到的状态'' ,就绪状态

阻塞队列:

是指当线程调佣sleep时,此时这个线程会进入休眠的状态,那么它会进入到阻塞队列中

这个链表的PCB,都是阻塞状态,不参与CPU的调度执行。PCB是使用链表来组织的,并不是简单的链表。一旦这个线程被唤醒,不再处于休眠的状态,这个PCB会回到就绪队列,但是不会马上就会被调度,要考虑到调度的开销,比如调用sleep(1000),对应的线程PCB就要在阻塞队列中待1000ms这么久,当这个PCB回到了就绪队列,会被立刻调度吗?虽然是sleep(1000),但是实际上要考虑到调度的开销,对应的线程是无法在唤醒之后就立即执行的,实际上的时间间隔大概率要大于1000ms。

六.线程的状态

反映的是当前线程的调度情况。

1.NEW  创建了Thread对象,但是还没调用start(内核里还没创建对应的PCB)

public static void main(String[] args) {
        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(1);
            }
        });
        System.out.println(thread.getState());
        thread.start();

    }

2.TERMINATED  表示内核中的pcb已经执行完了,但是Thread对象还在。

 public static void main(String[] args) {
        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(1);
            }
        });
        thread.start();
        try {
            thread.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(thread.getState());

    }
}

3.RUNNABLE 可运行的包括两部分,正在CPU上执行的或者在就绪队列上的,随时可以去CPU上执行

public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                //System.out.println(1);
                int i = 0;
                for (i = 0; i < 1000000; i++) {
                    ;
                }
            }
        });
        System.out.println("开始前"+thread.getState());
        thread.start();

            System.out.println("进行中"+thread.getState());
        try {
            thread.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("结束了"+thread.getState());

    }



}

4.WAITING     wait方法触发的线程阻塞

5.TIMED_WAITING   sleep触发的,线程阻塞

public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                //System.out.println(1);
                try {
                    Thread.sleep(1000000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

            }
        });
        thread.start();
        for (int i = 0; i < 100; i++) {
            System.out.println(thread.getState());
        }
    }

6.BLOCKED

synchronized  触发的,线程阻塞。

4 5  6 这三种状态都是阻塞状态,是由不同原因阻塞而成的。

六.多线程的意义:

我们可以通过代码来感受一下,单个线程和多个线程之间,执行速度的区别。

比如让实现一个变量自增200亿的操作。单个线程:

public static void main(String[] args) {
        long a=0;
        long i=0;
        long start=System.currentTimeMillis();
        for(i=0;i<200_0000_0000L;i++)
        {
            a++;
        }
        long end=System.currentTimeMillis();
        System.out.println(end-start);

    }

它的执行时间为: 

而如果我们让两个线程去完成的话,一个线程干自增100亿的活,那么时间会不会缩短呢?

public static void main(String[] args) {

        Thread thread1=new Thread(new Runnable() {
            @Override
            public void run() {
                long d=0;
               long i=0;
               for(i=0;i<100_0000_0000L;i++) {
                   d++;


               }
            }
        });
        Thread thread2=new Thread(new Runnable() {
            @Override
            public void run() {

                long c=0;
                long k=0;
                for(c=0;c<100_0000_0000L;c++)
                {
                    k++;
                }

            }
        });
        long beg=System.currentTimeMillis();
        thread1.start();
        thread2.start();
        try {
            thread1.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        try {
            thread2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        long end=System.currentTimeMillis();
        System.out.println(end-beg);



    }

我们来看一下它执行完的时间:

我们可以看到两个线程一起完成这个自增操作确实要比一个线程完成这个自增操作花费的时间少,可能有人会问为什么两个线程执行的时间不是一个线程执行的时间的一半呢?首先呢多线程之所以块是因为它可以充分利用多核心CPU的资源,但是因为这两个线程在实际调度的过程中,这些次调度,有些是并发执行的(在一个核心上),有些是并行执行的(正在在两个CPU)上,我们没法报证这些次调度都是并行的,到底是多少次并发,多少次并行,取决于系统的配置。也取决于当前程序的运行环境。

下面我们再来看一下,当代码中出现两个join的时候,实际上的一个调度情况

  long beg=System.currentTimeMillis();
        thread1.start();
        thread2.start();
        try {
            thread1.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        try {
            thread2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        long end=System.currentTimeMillis();
        System.out.println(end-beg);



    }

 我们看到当主函数main的线程执行完thread1.start这条语句时,thread1这个线程被真正创建,然后它去干它的活,main的线程执行thread2.start后,thread2这个线程也去干它的活,然后当main线程执行到thread1.join时,main函数线程停下来,等待thread1这个线程干完活,等thread1这个线程干完活后,main函数这个线程继续执行,这时候又遇到了thread2.join,需要等待thread2干完活,再继续执行,这里会存在一种情况,就是当thread2比thread1先干完活,那么main线程只需等待thread1干完活后执行就可以了,不需要再等待thread2了。多个线程同时执行,最终的时间,就是最慢的线程的时间,另外在谁中调用join,就是让谁这个线程等待,比如main调用t1,join就是让main来等待t1,如果是t2调用t1.join,就是让t2等待t1.

七.线程安全

多线程带来的风险,线程安全,根本原因是多线程的抢占式执行,带来的随机性!!!

我们来看一下一个线程安全问题的代码:

public class count {
    public static int count=0;
    public static void add()
    {
        count++;
    }
    public static void main(String[] args) {
        Thread thread1=new Thread(new Runnable() {
            @Override
            public void run() {
                int i=0;
                for(i=0;i<10000;i++)
                {
                    add();
                }

            }
        });
        Thread thread2=new Thread(new Runnable() {
            @Override
            public void run() {
                int i=0;
                for(i=0;i<10000;i++)
                {
                    add();
                }
            }
        });
        thread1.start();
        thread2.start();
        try {
            thread1.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        try {
            thread2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(count);

    }
}

像这段代码,thread1和thread2分别对count执行了一万次自增操作,理论上最终打印出来count的值应该是两万。

但是我们看一下运行结果:

我们看到结果不是两万,下面我们详细地来介绍一下原因。

首先呢?我们先了解一下++操作本质上要分成三步:1.先把内存中的值,读取到CPU的寄存器中,也就是load操作2.把CPU寄存器里的数值进行+1运算   add  3.把得到的结果写到内存中,这三个操作,就是CPU上执行的三个操作,视为是机器语言。

如果是两个线程并发的执行count++,此时就相当于两组load add save进行执行,此时不同的线程调度顺序 就可能会产生一些结果上的差异,

这是一种可能的调度顺序,由于线程之间是随机 调度的,导致此处的调度顺序充满其他的可能性。

自增两次结果为2,这种情况是正确的,没有线程安全问题!!! 

但是如果是这种情况:

这时我们进行了两次自增,count确为1,预期与实际不符,这时候线程是不安全的。 

线程安全问题的原因:

1.根本原因:抢占式执行,随机调度

2.优化结构:多个线程同时修改一个变量,一个线程修改一个变量,多个线程读取同一个变量,没事,多个线程修改多个不同的变量,也没事。

3.原子性:

如果修改操作是非原子性的,容易出现线程安全问题,count++可以拆分成load、add、save三个操作,如果++操作是原子性的,此时线程安全问题,也就解决了。

4.内存可见性问题

5.指令重排序

如何从原子性入手,解决线程安全问题。加锁!!!!通过加锁,把不是原子的,转成原子的。

加锁,说是保证原子性,其实不是说让这里的三个操作一次完成,也不是这三步操作过程中不进行调度,而是想让其他也想操作的线程阻塞等待了。

加锁的关键字为synchronized

 public static int count=0;
    public synchronized static void add()
    {
        count++;
    }
    public static void main(String[] args) {
        Thread thread1=new Thread(new Runnable() {
            @Override
            public void run() {
                int i=0;
                for(i=0;i<10000;i++)
                {
                    add();
                }

            }
        });
        Thread thread2=new Thread(new Runnable() {
            @Override
            public void run() {
                int i=0;
                for(i=0;i<10000;i++)
                {
                    add();
                }
            }
        });
        thread1.start();
        thread2.start();
        try {
            thread1.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        try {
            thread2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(count);

    }
}

如果我们在add()方法前面加上synchronized,此时我们再运行。

此时打印出来的就是20000,符合预期。 


猜你喜欢

转载自blog.csdn.net/m0_70386582/article/details/127982262