Java并发编程(1)-并发基础

摘要

线程是操作系统运算调度的基本单元;线程是进程中的场景,线程在异步代码简化b,简化复杂系统开发以及发挥多核处理器优势上体现巨大优势,线程具有以下特点:

  1. 线程共享进程里面的资源(java堆、方法区)
  2. 线程具有自己的程序计数器、栈、局部变量表,用于线程切换时候的数据恢复。
  3. 同一个进程里面的线程可以被同时调度到多个cpu上。

于此同时线程也具有以下风险问题:

  1. 安全性问题:共享数据访问和修改数据不一致性
  2. 活跃性问题:线程之间可以存在死锁等
  3. 性能问题:多线程执行时候发生上下文切换时候会消耗资源、共享数据使用同步机制导致抑制编译器优化,内存缓存区中数据无效.

本节主要讲解Java并发编程里面的基础。

思维导图

Java并发编程(1)-并发基础_第1张图片

内容

线程跟并发编程

线程: 线程是操作系统运算调度的基本单位,线程是进程中的一个场景。
并发编程: 使用多线程技术,实现复杂系统开始,让多个线程同时运行,干各种事情,最终完成一套复杂系统。

1、Java并发主要运用在哪里?

在平时工作中,我们相对使用并发编程比较少,因为我们平时的业务系统开发基本上就是crud的过程。然后Java并发编程主要运用在分布式系统开发:1、底层分布式系统开发 2、分布式中间件开发 3、大型大数据系统开发。

2、创建和启用一个线程

创建启用线程有两种方式:通过类Thread实现和通过Runnable接口实现。

Thread类实现方式:

  1. new覆盖run方法,然后start

    new Thread(){
                @Override
                public void run() {
                    System.out.println("创建线程第一种方式");
                }
            }.start();
  2. 继承Thread类,覆盖run方法,然后start

    public class MyThread extends Thread{
            @Override
            public void run() {
                System.out.println("创建线程第二种方式");
            }
        }
        
        MyThread myThread = new MyThread();
        myThread.start();

Runnable接口实现方式: 通过Runnable,Runnable类似于一个策略的实现,我们new Thread时候传不同的Runnable,就相当于不同的策略。

  1. new一个run方法存在于Runnable接口,然后使用new Thread包裹再start。

    new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("创建线程第三种方式");
                }
            }).start();
  2. 实现一个Runnable接口覆盖run方法的类,然后使用new Thread包裹再start

    public class MyRunnable implements Runnable{
            @Override
            public void run() {
                System.out.println("创建线程第四种方式");
            }
        }
        
        Thread thread = new Thread(new MyRunnable());
        thread.start();

3、daemon线程

java线程里面分为用户线程(User Thread)跟守护线程(Daemon Thread)。
用户线程: 非daemon线程:主要完成业务代码模块线程,会阻止JVM进程的退出,平时创建的默认线程就是用户线程。
daemon线程: 守护线程,是一种支持性线程,主要用于程序中的后台调度及支持性工作。不会阻止JVM进程退出,当用户线程全都终结后,会自动Terminaed。

实例:
用户线程:

public static void main(String[] args) {
        System.out.println("主线程开始: "+Thread.currentThread().getName() +" id:"+Thread.currentThread().getId());

        new Thread(){
            @Override
            public void run() {
                while (true){
                    try {
                        System.out.println("线程执行...+ "+Thread.currentThread().getName() +" id:"+Thread.currentThread().getId());
                        Thread.sleep(5*1000);
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
            }
        }.start();
        System.out.println("主线程结束: "+Thread.currentThread().getName() +" id:"+Thread.currentThread().getId());
    }

image.png
我们发现上面主线程执行后,由于用户线程并没有退出,整个JVM进程不会退出。

守护线程:

public class DaemonThreadDemo {

    public static void main(String[] args) {
        System.out.println("主线程开始: "+Thread.currentThread().getName() +" id:"+Thread.currentThread().getId());
        MyDaemon myDaemon = new MyDaemon();
        myDaemon.start();
        System.out.println("主线程结束: "+Thread.currentThread().getName() +" id:"+Thread.currentThread().getId());
    }


    public static class MyDaemon{

        private Daemon daemon;

        public void start(){
            this.daemon.start();
        }
        public MyDaemon(){
            this.daemon = new Daemon();
            this.daemon.setDaemon(true);
        }
        public class Daemon extends Thread{
            @Override
            public void run() {
                while (true){
                    try {
                        System.out.println("守护线程执行..."+Thread.currentThread().getName() +" id:"+Thread.currentThread().getId());
                        Thread.sleep(5*1000);
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

Java并发编程(1)-并发基础_第2张图片
从上图可以知道:当我们用户线程Terminated时候,守护自动退出。

区别于用户线程:

  1. 创建方式:默认创建线程为用户线程,使用setDaemon后的线程是后台线程.
  2. 工作职责:用户线程属于程序业务一部分;不可或缺;后台线程是重要性能不大的线程.
  3. main线程关联:用户线程会阻止JVM的退出。daemon线程不会阻止JVM退出;当用户线程都退出后daemon线程主动退出.

5、父线程/线程组

父线程:每个线程都有一个父线程,就是指代在哪个线程里面创建线程.

线程组:每个线程都有一个线程组;线程默认的线程组是父线程的线程组.

实例:

public class ThreadGroupDemo {
    public static void main(String[] args) {
        System.out.println("当前主线程: "+Thread.currentThread().getName() +" id:"+Thread.currentThread().getId()+" 父线程:"+Thread.currentThread().getThreadGroup());
        /**
         * 用户线程
         */
        new Thread(){
            @Override
            public void run() {
                System.out.println("用户线程: "+Thread.currentThread().getName() +" id:"+Thread.currentThread().getId()+" 父线程:"+Thread.currentThread().getThreadGroup());
            }
        }.start();
    }
}

输出:

当前主线程: main id:1 父线程:java.lang.ThreadGroup[name=main,maxpri=10]
用户线程: Thread-0 id:21 父线程:java.lang.ThreadGroup[name=main,maxpri=10]

1、实际上在java里,每个线程都有一个父线程的概念,就是在哪个线程里创建这个线程,那么他的父线程就是谁。举例来说,java都是通过main启动的,那么有一个主要的线程就是mian线程。在main线程里启动的线程,父线程就是main线程,就这么简单。

2、然后每个线程都必然属于一个线程组,默认情况下,你要是创建一个线程没指定线程组,那么就会属于父线程的线程组了,main线程的线程组就是main ThreadGroup。咱们来随手写一段代码看看不就得了

3、在java里面,线程都是有名字的,默认情况下,main线程的名字就是叫main。你其他的其他线程的名字,一般是叫做Thread-0之类的。当然我们也可以自己设置自己创建的线程名字。

6、线程优先级

1、理论上可以让优先级高的线程先尽量多执行,但是其实一般实践中很少用到这个东西,因为这是理论上的,可能你设置了优先级,人家cpu结果也还是没按照这个优先级来执行线程。

2、优先级一般是在1~10之间;默认优先级是5。

3、ThreadGroup也可以指定优先级,线程优先级不能大于ThreadGroup的优先级。

7、Thread源码分析

我们查看以下代码:

      Thread thread = new Thread() {
                @Override
                public void run() {
                    System.out.println("线程开始: " +
                            Thread.currentThread().getName() +
                            " id:" + Thread.currentThread().getId());
                }
            };
            thread.start();

线程初始化:
Java并发编程(1)-并发基础_第3张图片

Thread parent = currentThread();
Java并发编程(1)-并发基础_第4张图片
上面native方法不是java来实现的,是java本地方法,是比较底层的跟操作系统相关机制来实现的。
Thread parent = currentThread()代表你创建线程的时候,获取到的是currentThread(),是当前创建你的那个线程,比如说一般来说就是那个main线程,main线程在创建thread线程,所以说此时创建线程的过程中,获取到的currentThread()就是main线程。

你创建一个线程的时候,默认他的父线程就是创建他的那个线程,比如main线程创建thread线程,此时thread线程的父线程就是main线程.

Java并发编程(1)-并发基础_第5张图片
上面这段代码的意思,就是说threadGroup是不指定时候,他就会自动给你创建一个,给你分配一个线程组,每个线程必须属于一个ThreadGroup的。如果你没有指定线程组,那么你默认的线程组就是父线程的线程组。

Java并发编程(1)-并发基础_第6张图片
上面代码:默认情况下,如果你没有指定你是否为daemon的话,那么你的daemon的状态是由父线程决定的,就是说如果你的父线程是daemon线程,那么你也是daemon线程;同理,你的优先级如果没有指定的话,那么就跟父线程的优先级保持一致。

 /* Set thread ID */
 tid = nextThreadID(); 

每个线程其实都有一个线程id,threadId,第一个分配的线程,它的id是1,之后的线程是2,3,4,5,这样子,依次分配各个线程的id。我们可以发现threadSeqNumber存在线程可见性问题与原子性问题,可见行在并发读写时候,使用volatile。原子性问题使用synchronized。

Java并发编程(1)-并发基础_第7张图片

总结:
Thread初始化的过程蕴含的你需要知道的一些点:
(1)创建你的线程,就是你的父线程
(2)如果你没有指定ThreadGroup,你的ThreadGroup就是父线程的ThreadGroup
(3)你的daemon状态默认是父线程的daemon状态
(4)你的优先级默认是父线程的优先级
(5)如果你没有指定线程的名称,那么默认就是Thread-0格式的名称
(6)你的线程id是全局递增的,从1开始

线程启动:
Java并发编程(1)-并发基础_第8张图片

It is never legal to start a thread more than once.

永远都不能对一个线程多次调用和执行start()方法,这个是不对的。

    if (threadStatus != 0)
        throw new IllegalThreadStateException();  
        

上面代码标示:如果你的线程一旦执行过一次以后,那么他的threadStatus就一定会变为非0的一个状态,如果threadStatus是非0的状态,说明他之前已经被执行过了,所以这里会有一个判断,如果你对一个线程多次执行start()方法。
Java并发编程(1)-并发基础_第9张图片

group.add(this);

这个group就是之前给分配的,如果你自己指定了那么就是你自己创建的那个ThreadGroup,否则的话就是你的父线程的threadGroup,这行代码,其实就是将当前线程加入了他属于的那个线程组。

代码里面start0();
其实就是native代码:private native void start0();会结合底层的一些代码和机制,实际的启动一个线程。

一旦是start0()成功的启动之后,他就会去执行我们覆盖掉的那个run()方法,或者是如果你传入进去的是那个Runnalbe对象,人家就会执行那个Runnable对象的方法。

Java并发编程(1)-并发基础_第10张图片

但是如果你是
new Thread(new Runnable() {
public void run() {
}
}).start();

传递进去了一个Runnable对象,就是在thread类里是target的东西,会判断一下,如果target不为null的话,那么此时就会执行target的run方法。反之,如果你是直接自己用Thread类继承了一个子类的话,那么你会重写这个run()方法,start0()启动线程之后,就会来执行你的run()方法。

总结
线程启动里需要注意的几个点:
(1)一旦启动了线程之后,就不能再重新启动了,多次调用start()方法,因为启动之后,threadStatus就是非0的状态了,此时就不能重新调用了。
(2)你启动线程之后,这个线程就会加入之前处理好的那个线程组中。
(3)启动一个线程实际上走的是native方法,start0(),会实际的启动一个线程。
(4)一个线程启动之后就会执行run()方法。

8、线程操作

线程休眠-sleep

线程休眠: 使用Thread的静态方法sleep实现线程休眠。
Java并发编程(1)-并发基础_第11张图片

底层是本地方法,调用的是底层操作系统。

JDK 1.5之后就引入了TimeUnit这个类,很方便

TimeUnit.HOURS.sleep(1)
TimeUnit.MINUTES.sleep(5)
TimeUnit.SECONDS.sleep(30)
TimeUnit.MILLISECONDS.sleep(500)

如果用TimeUnit的话,你在外面怎么配?你要是配置休眠5分钟,还得加一个单位,代码里要判断一下你休眠的时间单位,如果是分钟,那么还得用TimeUnit.MINIUTE来进行休眠,不太方便。一般使用Thread.sleep(millis)比较多。

应用:
注册中心:监控线程定时检查服务失效

阻塞线程优先执行其他线程-join

阻塞当前线程优先执行其他线程:有时候我们需要实现线程1执行完毕再执行线程2.我们使用join。
Java并发编程(1)-并发基础_第12张图片

以下我们在主线程里面定义了2个线程,只有当线程1执行完毕之后才会去执行线程2.

 public class Demo {
    public static void main(String[] args) throws Exception{
        Thread thread1 = new Thread() {
            @Override
            public void run() {
                try {
                    System.out.println("线程开始1: " +
                            Thread.currentThread().getName() +
                            " id:" + Thread.currentThread().getId());
                    Thread.sleep(5*1000);
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        };
        Thread thread2 = new Thread() {
            @Override
            public void run() {
                try {
                    System.out.println("线程开始2: " +
                            Thread.currentThread().getName() +
                            " id:" + Thread.currentThread().getId());
                    Thread.sleep(1*1000);
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        };
        thread1.start();
        thread1.join();
        thread2.start();
    }
}

Java并发编程(1)-并发基础_第13张图片

应用: 微服务注册中心客户端start方法执行:
注册线程.join()然后执行心跳线程.start()

线程打断-interrupt

线程打断:设置线程的标记位:我们可以根据此标记位做业务.一般跟sleep一起运用比较多。
自身判断
Java并发编程(1)-并发基础_第14张图片

应用: 基于interrupt实现微服务优雅关闭心跳线程:Register-Client里面shutdown();

你可能感兴趣的:(java并发)