摘要
线程是操作系统运算调度的基本单元;线程是进程中的场景,线程在异步代码简化b,简化复杂系统开发以及发挥多核处理器优势上体现巨大优势,线程具有以下特点:
- 线程共享进程里面的资源(java堆、方法区)
- 线程具有自己的程序计数器、栈、局部变量表,用于线程切换时候的数据恢复。
- 同一个进程里面的线程可以被同时调度到多个cpu上。
于此同时线程也具有以下风险问题:
- 安全性问题:共享数据访问和修改数据不一致性
- 活跃性问题:线程之间可以存在死锁等
- 性能问题:多线程执行时候发生上下文切换时候会消耗资源、共享数据使用同步机制导致抑制编译器优化,内存缓存区中数据无效.
本节主要讲解Java并发编程里面的基础。
思维导图
内容
线程跟并发编程
线程: 线程是操作系统运算调度的基本单位,线程是进程中的一个场景。
并发编程: 使用多线程技术,实现复杂系统开始,让多个线程同时运行,干各种事情,最终完成一套复杂系统。
1、Java并发主要运用在哪里?
在平时工作中,我们相对使用并发编程比较少,因为我们平时的业务系统开发基本上就是crud的过程。然后Java并发编程主要运用在分布式系统开发:1、底层分布式系统开发 2、分布式中间件开发 3、大型大数据系统开发。
2、创建和启用一个线程
创建启用线程有两种方式:通过类Thread实现和通过Runnable接口实现。
Thread类实现方式:
new覆盖run方法,然后start
new Thread(){ @Override public void run() { System.out.println("创建线程第一种方式"); } }.start();
继承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,就相当于不同的策略。
new一个run方法存在于Runnable接口,然后使用new Thread包裹再start。
new Thread(new Runnable() { @Override public void run() { System.out.println("创建线程第三种方式"); } }).start();
实现一个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());
}
我们发现上面主线程执行后,由于用户线程并没有退出,整个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();
}
}
}
}
}
}
从上图可以知道:当我们用户线程Terminated时候,守护自动退出。
区别于用户线程:
- 创建方式:默认创建线程为用户线程,使用setDaemon后的线程是后台线程.
- 工作职责:用户线程属于程序业务一部分;不可或缺;后台线程是重要性能不大的线程.
- 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();
Thread parent = currentThread();
上面native方法不是java来实现的,是java本地方法,是比较底层的跟操作系统相关机制来实现的。
Thread parent = currentThread()代表你创建线程的时候,获取到的是currentThread(),是当前创建你的那个线程,比如说一般来说就是那个main线程,main线程在创建thread线程,所以说此时创建线程的过程中,获取到的currentThread()就是main线程。
你创建一个线程的时候,默认他的父线程就是创建他的那个线程,比如main线程创建thread线程,此时thread线程的父线程就是main线程.
上面这段代码的意思,就是说threadGroup是不指定时候,他就会自动给你创建一个,给你分配一个线程组,每个线程必须属于一个ThreadGroup的。如果你没有指定线程组,那么你默认的线程组就是父线程的线程组。
上面代码:默认情况下,如果你没有指定你是否为daemon的话,那么你的daemon的状态是由父线程决定的,就是说如果你的父线程是daemon线程,那么你也是daemon线程;同理,你的优先级如果没有指定的话,那么就跟父线程的优先级保持一致。
/* Set thread ID */
tid = nextThreadID();
每个线程其实都有一个线程id,threadId,第一个分配的线程,它的id是1,之后的线程是2,3,4,5,这样子,依次分配各个线程的id。我们可以发现threadSeqNumber存在线程可见性问题与原子性问题,可见行在并发读写时候,使用volatile。原子性问题使用synchronized。
总结:
Thread初始化的过程蕴含的你需要知道的一些点:
(1)创建你的线程,就是你的父线程
(2)如果你没有指定ThreadGroup,你的ThreadGroup就是父线程的ThreadGroup
(3)你的daemon状态默认是父线程的daemon状态
(4)你的优先级默认是父线程的优先级
(5)如果你没有指定线程的名称,那么默认就是Thread-0格式的名称
(6)你的线程id是全局递增的,从1开始
It is never legal to start a thread more than once.
永远都不能对一个线程多次调用和执行start()方法,这个是不对的。
if (threadStatus != 0)
throw new IllegalThreadStateException();
上面代码标示:如果你的线程一旦执行过一次以后,那么他的threadStatus就一定会变为非0的一个状态,如果threadStatus是非0的状态,说明他之前已经被执行过了,所以这里会有一个判断,如果你对一个线程多次执行start()方法。
group.add(this);
这个group就是之前给分配的,如果你自己指定了那么就是你自己创建的那个ThreadGroup,否则的话就是你的父线程的threadGroup,这行代码,其实就是将当前线程加入了他属于的那个线程组。
代码里面start0();
其实就是native代码:private native void start0();会结合底层的一些代码和机制,实际的启动一个线程。
一旦是start0()成功的启动之后,他就会去执行我们覆盖掉的那个run()方法,或者是如果你传入进去的是那个Runnalbe对象,人家就会执行那个Runnable对象的方法。
但是如果你是
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实现线程休眠。
底层是本地方法,调用的是底层操作系统。
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。
以下我们在主线程里面定义了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();
}
}
应用: 微服务注册中心客户端start方法执行:
注册线程.join()然后执行心跳线程.start()
线程打断-interrupt
线程打断:设置线程的标记位:我们可以根据此标记位做业务.一般跟sleep一起运用比较多。
自身判断
应用: 基于interrupt实现微服务优雅关闭心跳线程:Register-Client里面shutdown();