认识线程

image.png

什么是叫一个进程? 什么叫一个线程?

**

  • Program app —> QQ.exe

    • 进程:做一个简单的解释,你的硬盘上有一个简单的程序,这个程序叫QQ.exe,这是一个程序,

    这个程序是一个静态的概念,它被扔在硬盘上也没人理他,但是当你双击它,弹出一个界面输入账
    号密码登录进去了,OK,这个时候叫做一个进程。进程相对于程序来说它是一个动态的概念。

    • 线程:作为一个进程里面最小的执行单元它就叫一个线程,用简单的话讲一个程序里不同的执行路

    径就叫做一个线程

实例:什么叫做线程

  1. public class T1_WhatIsThread {
  2. private static int MAX = 10;
  3. private static class T1 extends Thread{
  4. @Override
  5. public void run() {
  6. for (int i = 0; i < MAX; i++) {
  7. try {
  8. TimeUnit.MICROSECONDS.sleep(1);
  9. } catch (InterruptedException e) {
  10. e.printStackTrace();
  11. }
  12. System.out.println("T1");
  13. }
  14. }
  15. }
  16. public static void main(String[] args) {
  17. //启动线程
  18. new T1().start();
  19. for (int i = 0; i < MAX; i++) {
  20. try {
  21. TimeUnit.MICROSECONDS.sleep(1);
  22. } catch (InterruptedException e) {
  23. e.printStackTrace();
  24. }
  25. System.out.println("main");
  26. }
  27. }
  28. }

**
观察上面程序的数据结果,你会看到字符串“T1”和“main”的交替输出,这就是程序中有两条不同的执行
路径在交叉执行,这就是直观概念上的线程,概念性的东西,理解就好,没有必要咬文嚼字的去背文字
的定义。

创建线程的几种方式

public class T2_HowToCreateThread {

     static class MyThread extends Thread{
        @Override
        public void run() {
            System.out.println("Hello MyThread");
        }
    }

     static class MyRun implements Runnable{

        @Override
        public void run() {
            System.out.println("Hello MyRun");
        }
    }

    static class Mycall implements Callable<String>{

        @Override
        public String call() throws Exception {
            System.out.println("Hello MyCall");
            return "success";
        }
    }

    public static void main(String[] args) {

         //启动线程的五种方式

        new MyThread().start();

        new Thread(new MyRun()).start();

        new Thread(()->{
            System.out.println("Hello lambda");
        }).start();

        Thread thread = new Thread(new FutureTask<String>(new Mycall()));
        thread.start();


        ExecutorService service = Executors.newCachedThreadPool();

        service.execute(()->{
            System.out.println("Hello ThreadPool");
        });

        service.shutdown();
    }
}

分享一道面试题

请你告诉我启动线程的三种方式 ?
**
你说第一个:new Thread().start();
第二个: new Thread(Runnable).start() 这没问题 ;
那第三个呢,要回答线程池也是用的这两种之一,他这么问有些吹毛求疵的意思,你就可以说通过线程池也可以启动一个新的线程
3:Executors.newCachedThreadPool()或者FutureTask + Callable

认识几个线程的方法

public class T3_Sleep_Yield_Join {


    public static void main(String[] args) {
        //testJoin();

        //  testSleep();

        testYield();
    }

    /**
     * Sleep,意思就是睡眠,当前线程暂停一段时间让给别的线程去运行。
     * Sleep是怎么复活的?
     * 由你的睡眠时间而定,等睡眠到规定时间自动复活
     */
    static void testSleep(){
        new Thread(()->{
            for (int i = 0; i < 100; i++) {
                System.out.println("A" + i);

                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    /**
     * Yield ,就是当前线程正在执行的时候停止下来进入等待队列,
     * 回到等待队列里在系统的调度算法里依然有可能把你刚回去的这个线程拿回来继续执行
     * 当然,更大的可能性是把原来等待的那些拿出一个来执行,所以yield的意思是我让出一下Cpu
     * 后面你们能不能抢到那我不管
     */
    static void testYield(){
        new Thread(()->{
            for (int i = 0; i < 100; i++) {
                System.out.println("A"+ i);

                if(i%10==0){
                    Thread.yield();
                }
            }
        }).start();


        new Thread(()->{
            for (int i = 0; i < 100; i++) {
                System.out.println("-------------B" + i);
                if(i%10==0){
                    Thread.yield();
                }
            }
        }).start();
    }

    /**
     * join, 意思就是在自己当前线程加入你调用Join的线程(),本线程等待。等调用的线程运行
     * 完了,自己再去执行。t1和t2两个线程,在t1的某个点上调用了t2.join,它会跑到t2去运行,t1等待t2运
     * 行完毕继续t1运行(自己join自己没有意义)
     */
    static void testJoin(){
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                System.out.println("A " + i);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });


        Thread thread1 = new Thread(() -> {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            for (int i = 0; i < 100; i++) {
                System.out.println("B" + i);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        thread.start();
        thread1.start();
    }
}

线程的状态

01-线程的基本概念 - 图2

常见的线程状态有六种:

  • 新建状态

    • 当我们new一个线程时,还没有调用start()该线程处于新建状态
  • Ready就绪状态

    • 线程对象调用 start()方法时候,他会被线程调度器来执行,也就是交给操作系统来执行了,那么操

作系统来执行的时候,这整个的状态叫Runnable,Runnable内部有两个状态(1)Ready就绪状

  • Running运行状态
    • 就绪状态是说扔到CPU的等待队列里面去排队等待CPU运行,等真正扔到

CPU上去运行的时候才叫Running运行状态。(调用yiled时候会从Running状态跑到Ready状态去,线
程配调度器选中执行的时候又从Ready状态跑到Running状态去)如果你线程顺利的执行完了就会进去

  • Teminated结束状态
    • (需要注意Teminated完了之后还可不可以回到new状态再调用start?这是不行的,完了这就是结束了)

在Runnable这个状态里头还有其他一些状态的变迁(4)TimedWaiting等待、(5)Waiting等待
(6)Blocked阻塞,在同步代码块的情况就下没得到锁就会阻塞状态,获得锁的时候是就绪状态运行。
在运行的时候如果调用了o.wait()、t.join()、LockSupport.park()进入Waiting状态,调用o.notify()、
o.notifiAll()、LockSupport.unpark()就又回到Running状态。TimedWaiting按照时间等待,等时间结
束自己就回去了,Thread.sleep(time)、o.wait(time)、t.join(time)、LockSupport.parkNanos()、
LockSupport.parkUntil()这些都是关于时间等待的方法。

问题1:哪些是JVM管理的?哪些是操作系统管理的?
**
上面这些状态全是由JVM管理的,因为JVM管理的时候也要通过操作系统,所以呢,那个是操作系统和
那个是JVM他俩分不开,JVM是跑在操作系统上的一个普通程序

问题2: 线程什么状态时候会被挂起?挂起是否也是一个状态?

Running的时候,在一个cpu上会跑很多个线程,cpu会隔一段时间执行这个线程一下,在隔一段时间执
行那个线程一下,这个是cpu内部的一个调度,把这个状态线程扔出去,从running扔回去就叫线程被
挂起,cpu控制它。

来看一下ThraedState这段代码

public class T4_ThreadState {

    static class Mythread extends Thread{
        @Override
        public void run() {
            System.out.println(this.getState());

            for (int i = 0; i < 100; i++) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(i);
            }
        }
    }

    public static void main(String[] args) {
        Mythread mythread = new Mythread();
        //怎么样得到这个状态呢?就是通过getState()这个方法
        System.out.println(mythread.getState());//它是一个New状态

        mythread.start();//到这start后完了以后是Runnable的状态
        try {
            mythread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //然后join之后,结束了是一个Timenated状态
        System.out.println(mythread.getState());
    }
}

synchronized关键字

第一个是多个线程去访问同一个资源的时候对这个资源上锁。
为什么要上锁呢?访问某一段代码或者某临界资源的时候是需要有一把锁的概念在这儿的

线程同步

image.png

比如:我们对一个数字做递增,两个程序对它一块儿来做递增,递增就是把一个程序往上加1啊,如果
两个线程共同访问的时候,第一个线程一读它是0,然后把它加1,在自己线程内部内存里面算还没有写
回去的时候而第二个线程读到了它还是0,加1在写回去,本来加了两次,但还是1,那么我们在对这个
数字递增的过程当中就上把锁,就是说第一个线程对这个数字访问的时候是独占的,不允许别的线程来
访问,不允许别的线程来对它计算,我必须加完1收释放锁,其他线程才能对它继续加。

实质上,这把锁并不是对数字进行锁定的, 你可以任意指定,想锁谁就锁谁。

我第一个小程序是这么写的 ,如果说你想上了把锁之后才能对count进行减减访问,你可以new一个
Object,所以这里锁定就是o,当我拿到这把锁的时候才能执行这段代码。是锁定的某一个对象,
synchronized有一个锁升级的概念,我们一会儿会讲到

public class T {
    private int count = 10;
    private Object o = new Object();

    public void m(){
        /**
         * 任何线程要执行下面的代码,必须先拿到o的锁
         */
        synchronized (o){
            count--;
            System.out.println(Thread.currentThread().getName() + "count = " + count);
        }
    }


    public static void main(String[] args) throws InterruptedException {
        T t = new T();

        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                t.m();
            }).start();
        }

        Thread.sleep(100);
        System.out.println(t.count);
    }
}

我们来谈一下synchronized它的一些特性。如果说你每次都定义个一个锁的对象Object o 把它new出来
那加锁的时候太麻烦每次都要new一个新的对象出来,所以呢,有一个简单的方式就是
synchronized(this)锁定当前对象就行

public class T1 {
    private int count = 10;
    public void m(){
        /**
         * 任何线程要执行下面的代码,必须先拿到this的锁
         */
        synchronized (this){
            count--;
            System.out.println(Thread.currentThread().getName() + "count = " + count);
        }
    }


    public static void main(String[] args) throws InterruptedException {
        T1 t = new T1();

        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                t.m();
            }).start();
        }

        Thread.sleep(100);
        System.out.println(t.count);
    }
}

如果你要是锁定当前对象呢,你也可以写成如下方法。synchronized方法和synchronized(this)执行这
段代码它是等值的

synchronized锁的是对象不是代码

public class T2 {

    private int count = 10;

    public synchronized void m() { //等同于在方法的代码执行时要synchronized(this)
        count--;
        System.out.println(Thread.currentThread().getName() + " count = " + count);
    }

    public static void main(String[] args) throws InterruptedException {
        T2 t = new T2();

        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                t.m();
            }).start();
        }

        Thread.sleep(100);
        System.out.println(t.count);
    }


}

我们知道静态方法static是没有this对象的,你不需要new出一个对象来就能执行这个方法,但如果这个
这个上面加一个synchronized的话就代表synchronized(T3.class)。这里这个synchronized(T3.class)锁的
就是T类的对象

public class T3 {
    private static int count = 10;

    /**
     * 这里等同于synchronized(T3.class)
     */
    public synchronized static void m(){
        count--;
        System.out.println(Thread.currentThread().getName() + " count = " + count);
    }

    public  static void mm(){
        synchronized(T3.class) {
            count--;
            System.out.println(Thread.currentThread().getName() + " count = " + count); 
        }

    }
}

T3是单例吗?
**
一个class load到内存它是不是单例的,想想看。一般情况下是,如果是在同一个ClassLoader空间那它
一定是。不是同一个类加载器就不是了,不同的类加载器互相之间也不能访问。所以说你能访问它,那
他一定就是单例

下面程序:很有可能读不到别的线程修改过的内容,除了这点之外count减减完了之后下面的count输
出和你减完的结果不对,很容易分析:如果有一个线程把它从10减到9了,然后又有一个线程在前面一
个线程还没有输出呢进来了把9又减到了8,继续输出的8,而不是9。如果你想修正它,前面第一个是在
上面加volatile,改了马上就能得到。


/**
 * 分析一个这个程序的输出
 *
 * @author liuscoding
 */
public class T implements Runnable {

    private /*volatile*/ int  count = 100;

    @Override
    public  /*synchronized*/  void run() {
        count--;
        System.out.println(Thread.currentThread().getName()+"    count = " + count);
    }


    public static void main(String[] args) {
        T t = new T();
        for (int i = 0; i < 100; i++) {
            new Thread(t,"Thread"+ i).start();
        }
    }
}

另外这个之外还可以加synchronized,加了synchronized就没有必要在加volatile了,因为
synchronized既保证了原子性,又保证了可见性。

public class T implements Runnable {

    private  int  count = 100;

    @Override
    public  synchronized  void run() {
        count--;
        System.out.println(Thread.currentThread().getName()+"    count = " + count);
    }


    public static void main(String[] args) {
        T t = new T();
        for (int i = 0; i < 100; i++) {
            new Thread(t,"Thread"+ i).start();
        }
    }
}

如下代码:同步方法和非同步方法是否可以同时调用?就是我有一个synchronized的m1方法,我调用
m1的时候能不能调用m2,拿大腿想一想这个是肯定可以的,线程里面访问m1的时候需要加锁,可是
访问m2的时候我又不需要加锁,所以允许执行m2。
这些小实验的设计是比较考验功力的,学习线程的时候自己要多动手进行试验,任何一个理论,都可以
进行验证:

同步和非同步同时执行

/**
 * 同步和非同步方法是否可以同时调用
 */
public class T {

    public synchronized void m1(){
        System.out.println(Thread.currentThread().getName() + "m1 start....");

        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + "m1 end....");

    }


    public void m2(){
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName()+"m2");
    }


    public static void main(String[] args) {
        T t = new T();

        new Thread(t::m1,"m1:-----------").start();
        new Thread(t::m2,"m2:-----------").start();
    }
}

我们来看一个synchronized应用的例子
**
我们定义一个Class账户,有名称、余额。写方法给那个用户设置它多少余额,读方法通过这个名字得到余额值。如果我们给写方法加锁,给读方法不加锁,你的业务允许产生这种问题?业务说我中间读到了一些不太好的数据也没关系,如果不允许客户读到中间不好的数据就有问题,正因为我们加了锁的方法和不加锁的方法同时可以运行。

问题 比如说:张三,给他设置100块钱启动了,睡了1毫秒之后呢去读它的值,然后在睡2秒再去读它的值这个时候你会看到读到的值有问题,原因是在设定的过程中this.name你中间睡了一下,这个过程当中我模拟了一个线程来读,这个时候调用的是getBlance()方法,而调用这个方法的时候是不用加锁的,所以说我不需要等你整个过程执行完就可以读到你中间结果产生的内存,这个现象叫做脏读,这问题的产生就是sychronized方法和非synchronzied方法是同时运行的。解决就是把getBlance加上synchronized就可以了,如果你的业务允许脏读,就可以不用加锁,加锁之后的效率低下。

/**
 * 面试题: 模拟银行账户
 * 
 * 对业务写方法加锁
 * 对业务读方法不加锁
 * 这样行不行?
 * 
 * 容易产生脏读问题(dirtyRead)
 */
public class Account {
    private String name;
    private double balance;


    public synchronized void set(String name,double balance){
        this.name = name;

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        this.balance = balance;
    }

    public /*synchronized*/ double getBalance(String name){
        return this.balance;
    }


    public static void main(String[] args) {
        Account account = new Account();
        new Thread(()->account.set("zhangsan",100.0)).start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(account.getBalance("zhangsan"));


        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(account.getBalance("zhangsan"));
    }
}

再来看synchonzied 的另外一个属性:可重入,是synchronized必须了解的一个概念。

锁重入

如果是一个同步方法调用另外一个同步方法,有一个方法加了锁,另外一个方法也需要加锁,加的是同一把锁也是同一个线程,那这个时候申请仍然会得到该对象的锁,比如说是synchronized可重入的,有一个方法m1是synchronized,有一个方法m2也是synchronized,m1里能不能调m2。 我们m1开始的时候这个线程得到了这把锁,然后在m1里面调用m2,如果说这个时候不允许任何线程再来拿这把锁的时候就死锁了。这个时候调m2它发现是同一个线程,因为你m1也需要申请这把锁,它发现是同一线程申请的这把锁,允许,可以没问题,这就叫可重入锁。


/**
 *  一个同步方法可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到
 *  该对象的锁。
 *
 *  也就是说synchronized获得锁是可重入的
 */
public class T {

    synchronized void m1(){
        System.out.println("------------------m1 start----------------");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        m2();
        System.out.println("------------------m1 end------------------");

    }

    synchronized void m2(){
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("m2");
    }

    public static void main(String[] args) {
        new T().m1();
    }
}

模拟一个父类子类的概念,父类synchronized,子类调用super.m的时候必须得可重入,否则就会出问题(调用父类是同一把锁)。所谓的重入锁就是你拿到这把锁之后不停加锁加锁,加好几道,但锁定的还是同一个对象,去一道就减个1,就是这么个概念。

/**
 *  一个同步方法可以调用另外一个同步,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁
 *
 *  也就是说synchronized 获得的锁是可重入的
 *
 *  这里是继承中有可能发生的情形 子类调用父类的同步方法
 */
public class T {

    synchronized void m(){
        System.out.println("--------------m start---------------");

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("--------------m end---------------");
    }

    public static void main(String[] args) {
        new TT().m();
    }
}

class TT extends T {

    @Override
    synchronized void m() {
        System.out.println("-----------child m start----------------");
        super.m();

        System.out.println("-----------child m end ----------------");
    }
}

下面再看,异常锁
**
看这个小程序,加了锁 不断执行 synchronized void m() , while(true) 不断执行,线程启动,count++如果等于5的时候人为的产生异常。这时候如果产生任何异常,就会出现什么情况呢?就会被原来的那些个准备拿到这把锁的程序乱冲进来,程序乱入。这是异常的概念。

/**
 * 程序在执行过程中,如果出现异常,默认情况锁会被释放。
 *
 * 所以,在并发处理的过程中,有异常要多加小心,不然可能发生不一致的情况。
 *
 * 比如,在一个web app处理过程中,多个servlet线程共同访问同一个资源,这时如果异常处理不合适
 *
 * 在第一个线程中抛出异常,其它线程就会进入同步代码区,有可能会访问到异常产生的数据。
 * 
 * 因此要非常小心的处理同步业务逻辑中的异常。
 */
public class T {

    int count = 0;

    synchronized void m(){
        System.out.println(Thread.currentThread().getName() + "start");

        while(true){
            count++;
            System.out.println(Thread.currentThread().getName()+"count = " + count);

            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            if( count == 5 ){
                /**
                 * 此处抛出异常,锁将被释放,要想不被释放,可以在这里进行catch,然后让循环继续
                 */
                int i = 1/0;
                System.out.println(i);
            }
        }
    }

    public static void main(String[] args) {
        T t = new T();

        Runnable runnable  = () -> t.m();
        new Thread(runnable,"t1").start();

        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(runnable,"t2").start();
    }

}

synchronized的底层实现
**

  • 早期,JDK早期的时候,这个synchronized的底层实现是重量级的,重量级到这个synchronized都是要去找操作系统去申请锁的地步,这就会造成synchronized效率非常低,java后来越来越多的处理高并发的程序的时候,很多程序员都不满意,说这个synchronized用的太重了,我没办法,就要开发新的框架,不用你原生的了。

  • 改进,后来的改进才有了锁升级的概念

这个锁升级的概念呢,是这样的,原来呢都要去找操作系统,要找内核去申请这把锁,到后期做了synchronized的一些改进,它的效率比原来要改变了不少,改进的地方。当我们使用synchronized的时候HotSpot的实现是这样的: 上来之后第一个去访问某把锁的线程,比如synchronized(Object),来了之后现在这个Object的头上面markword记录这个线程。(如果只有第一个线程的时候实际上是没有给这个Object加锁的,在内部实现的时候只是记录这个线程的ID(偏向锁))

偏向锁如果有线程争用的话,就升级为自旋锁。

自旋锁转圈十次之后,升级为重量级锁,重量级锁就是去操作系统那里去申请资源。这是一个锁升级的过程。

参考:https://blog.csdn.net/baidu_38083619/article/details/82527461

需要注意并不是CAS的效率就一定比系统锁要高,这个要区分实际情况:

执行时间短(加锁代码),线程数少,用自旋。
执行时间长,线程数多,用系统锁。

内容回顾

  • 线程的概念 、启动方式、常用方法
  • synchronzied(Object)不能用String常量 Integer Long
  • 线程同步 -synchronize
    • 锁的是对象不是代码
    • this XX.class
    • 锁定方法 非锁定方法 可以同时执行
    • 锁升级
      • 偏向锁 记录这个线程的id
      • 自旋锁 如果线程争用,就升级为自旋锁(线程数量少)
      • 重量级锁 10次(线程数量多)