介绍与概念
Java 给多线程编程提供了内置的支持。 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。
进程:是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。
线程:是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源,但是每个线程有自己的程序计数器和栈区域。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。
虽然系统是把资源分给进程,但是CPU很特殊,是被分配到线程的,所以线程是CPU分配的基本单位。多线程能满足程序员编写高效率的程序来达到充分利用 CPU 的目的。
程序计数器:是一块内存区域,用来记录线程当前要执行的指令地址 。
栈:用于存储该线程的局部变量,这些局部变量是该线程私有的,除此之外还用来存放线程的调用栈祯。
堆:是一个进程中最大的一块内存,堆是被进程中的所有线程共享的。
方法区:则用来存放 VM 加载的类、常量及静态变量等信息,也是线程共享的 。
总结
简而言之,一个程序至少有一个进程,一个进程至少有一个线程。
线程的划分尺度小于进程,使得多线程程序的并发性高。
另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。
生命周期 线程是一个动态执行的过程,它也有一个从产生到死亡的过程。
新建状态
使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
就绪状态:
当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
运行状态:
如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
阻塞状态:
如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
死亡状态:
一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
问:Java 程序每次运行至少启动几个线程? 至少启动两个线程,每当使用 Java 命令执行一个类时,实际上都会启动一个 JVM,每一个JVM实际上就是在操作系统中启动一个线程,Java 本身具备了垃圾的收集机制。所以在 Java 运行时至少会启动两个线程,一个是 main 线程,另外一个是垃圾收集线程。
并发与并行
并发:是指同一个时间段内多个任务同时都在执行,并且都没有执行结束。并发任务强调在一个时间段内同时发起,而一个时间段由多个单位时间累积而成,所以说并发的多个任务在单位时间内不一定同时在执行 。
并行:是说在单位时间内多个任务同时在执行 。
在多线程编程实践中,线程的个数往往多于CPU的个数,所以一般都称多线程并发编程而不是多线程并行编程。
并发常见问题
线程安全
多线程共享数据,更新与获取到的数值不一致,导致数值错乱问题。
共享内存不可见性
Java 内存模型规定,将所有的变量都存放在主内存中,当线程使用变量时,会把主内存里面的变量复制到自己的工作空间或者叫作工作内存,线程读写变量时操作的是自己工作内存中的变量 。
但是实际情况会存在部分内存存在不共享情况,比如主内存、二级缓存是共享的,一级缓存不共享。获取数据一级>二级>主内存>数据库,在一级缓存是存在数据不一致情况。
synchronized的理解
这个内存语义就可以解决共享变量内存可见性问题。
进入synchronized块的内存语义是把在synchronized块内使用到的变量从线程的工作内存中清除,这样在synchronized块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取。退出synchronized块的内存语义是把在synchronized块内对共享变量的修改刷新到主内存。会造成上下文切换的开销,独占锁,降低并发性。
Volatile的理解
该关键字可以确保对一个变量的更新对其他线程马上可见。
当一个变量被声明为volatile时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。当其他线程读取该共享变量时,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。
volatile的内存语义和synchronized有相似之处,具体来说就是,当线程写入了volatile变量值时就等价于线程退出synchronized同步块(把写入工作内存的变量值同步到主内存),读取volatile变量值时就相当于进入同步块(先清空本地内存变量值,再从主内存获取最新值)。
volatile能保证原子性,但是对自增和自减的多原子操作不能保证原子性。
线程的优先级
每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。
Java 线程的优先级是一个整数,其取值范围是 1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。
默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。
具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。
创建一个线程 通过继承 Thread 类本身或者匿名实现 继承demo
1 2 3 4 5 6 public class ThreadDemo extends Thread { @Override public void run() { } }
1 2 3 4 5 public class Demo { public static void main (String [] args) throws Exception { new ThreadDemo().start(); } }
匿名实现demo
1 2 3 4 5 6 7 8 9 10 public class Demo { public static void main (String[] args) throws Exception { new Thread(){ @Override public void run () { } }.start(); } }
通过实现 Runnable 接口 1 2 3 4 5 6 7 public class RunnableDemo implements Runnable { @Override public void run () { } }
1 2 3 4 5 public class Demo { public static void main (String [] args) throws Exception { new Thread(new RunnableDemo()).start(); } }
通过 Callable 和 Future 创建线程 1 2 3 4 5 6 7 8 9 10 11 12 public class CallableDemo implements Callable <String > { @Override public String call () throws Exception { try { Thread.sleep(5000 ); } catch (InterruptedException e) { e.printStackTrace(); } return "1111" ; } }
1 2 3 4 5 public class Demo { FutureTask<String > futureTask = new FutureTask <>(new CallableDemo ()); new Thread (futureTask).start(); System.out.println(futureTask.get ()); }
Thread类详解 初始化方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 private void init (ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc) { if (name == null ) { throw new NullPointerException("name cannot be null" ); } this .name = name.toCharArray(); Thread parent = currentThread(); SecurityManager security = System.getSecurityManager(); if (g == null ) { if (security != null ) { g = security.getThreadGroup(); } if (g == null ) { g = parent.getThreadGroup(); } } g.checkAccess(); if (security != null ) { if (isCCLOverridden(getClass())) { security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION); } } g.addUnstarted(); this .group = g; this .daemon = parent.isDaemon(); this .priority = parent.getPriority(); if (security == null || isCCLOverridden(parent.getClass())) this .contextClassLoader = parent.getContextClassLoader(); else this .contextClassLoader = parent.contextClassLoader; this .inheritedAccessControlContext = acc != null ? acc : AccessController.getContext(); this .target = target; setPriority(priority); if (parent.inheritableThreadLocals != null ) this .inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); this .stackSize = stackSize; tid = nextThreadID(); }
构造方法 Thread的构造方法都调用init()方法 每个线程均分配一个name,默认为(Thread-自增数字)的组合
1 2 3 public Thread () { init(null , null , "Thread-" + nextThreadNum(), 0 ); }
1 2 3 public Thread (Runnable target ) { init(null , target , "Thread-" + nextThreadNum(), 0 ); }
1 2 3 Thread(Runnable target , AccessControlContext acc ) { init(null, target, "Thread-" + nextThreadNum() , 0 , acc); }
1 2 3 public Thread (ThreadGroup group, Runnable target ) { init(group, target , "Thread-" + nextThreadNum(), 0 ); }
1 2 3 public Thread (String name) { init(null , null , name, 0 ); }
1 2 3 public Thread (ThreadGroup group , String name) { init(group , null , name, 0 ); }
1 2 3 public Thread (Runnable target , String name) { init(null , target , name, 0 ); }
1 2 3 public Thread(ThreadGroup group, Runnable target , String name ) { init(group, target , name , 0 ); }
1 2 3 public Thread(ThreadGroup group , Runnable target , String name , long stackSize ) { init(group, target, name, stackSize); }
线程状态 1 2 3 4 5 6 7 8 public enum State { NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED; }
NEW
RUNNABLE
就绪状态,处于就绪状态的线程位于可运行池中,此时它只是具备了运行的条件,能否获得 CPU 的使用权开始运行,还需要等待系统的调度
线程正常运行。
BLOCKED
WAITING
等待资源,其他线程通知等。没有被唤醒,就无限制等待下去
TIMED_WAITING
区别于WAITING,添加了一个时间,过时间不在等待,回到RUNNABLE
TERMINATED
Start方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public synchronized void start ( ) { if (threadStatus != 0 ) throw new IllegalThreadStateException(); group .add (this ); boolean started = false ; try { start0(); started = true ; } finally { try { if (!started) { group .threadStartFailed(this ); } } catch (Throwable ignore) {} } }
1 private native void start0 () ;
yield方法 1 public static native void yield () ;
主动提示线程调度器当前线程愿意放弃当前CPU的使用。在资源充足时,调度器可以忽略这个提示。线程状态一直是RUNNABLE。
sleep方法 1 public static native void sleep (long millis) throws InterruptedException ;
1 2 3 4 5 6 7 8 9 10 11 12 public static void sleep (long millis , int nanos) throws InterruptedException { if (millis < 0 ) { throw new IllegalArgumentException("timeout value is negative" ); } if (nanos < 0 || nanos > 999999 ) { throw new IllegalArgumentException("nanosecond timeout value out of range" ); } if (nanos >= 500000 || (nanos != 0 && millis == 0 )) { millis ++; } sleep(millis ); }
sleep方法,有一个重载方法,sleep方法会释放cpu的时间片,但是不会释放锁,调用sleep()之后从RUNNABLE状态转为TIMED_WAITING状态。
join方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public final synchronized void join (long millis ) throws InterruptedException { long base = System.currentTimeMillis(); long now = 0 ; if (millis < 0 ) { throw new IllegalArgumentException("timeout value is negative" ); } if (millis == 0 ) { while (isAlive()) { wait(0 ); } } else { while (isAlive()) { long delay = millis - now; if (delay <= 0 ) { break ; } wait(delay ); now = System.currentTimeMillis() - base; } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 public final synchronized void join (long millis , int nanos) throws InterruptedException { if (millis < 0 ) { throw new IllegalArgumentException("timeout value is negative" ); } if (nanos < 0 || nanos > 999999 ) { throw new IllegalArgumentException( "nanosecond timeout value out of range" ); } if (nanos >= 500000 || (nanos != 0 && millis == 0 )) { millis ++; } join (millis ); }
1 2 3 public final void join () throws InterruptedException { join (0 ); }
在线程B中join线程A,线程B会进入等待,直到线程A结束或到达时间。在此期间,线程B的状态一直处于BLOCKED状态。
wait方法 1 public final native void wait (long timeout) throws InterruptedException ;
1 2 3 4 5 6 7 8 9 10 11 12 public final void wait (long timeout, int nanos) throws InterruptedException { if (timeout < 0 ) { throw new IllegalArgumentException("timeout value is negative" ); } if (nanos < 0 || nanos > 999999 ) { throw new IllegalArgumentException("nanosecond timeout value out of range" ); } if (nanos > 0 ) { timeout++; } wait(timeout); }
1 2 3 public final void wait () throws InterruptedException { wait(0 ); }
调用wait方法的当前线程一定要拥有对象的监视器锁。 也就是synchronized的对象锁。 当前线程调用wait后,会释放锁。 当前进程会进入等待队列中。不会被线程调度器调度。 有3中释放方式
当等待时间为0时,会无限等待下去,直到其他对象使用notify/notifyAll()方法进行唤醒。
当等待时间不为0时,直到等待时间到来,自动唤醒。
当其他的线程调用了interrupt方法来中断等待线程。会抛出InterruptedExcaption异常
当等待线程被唤醒,会从等待队列中移除且重新被线程调度器调度。重新进行锁的争抢,直到获取锁对象之后,继续执行等待之后的代码。
notify方法 1 public final native void notify () ;
通知可能等待该对象的对象锁的其他线程。由JVM(与优先级无关)随机挑选一个处于wait状态的线程。 在调用notify()之前,线程必须获得该对象的对象级别锁。 执行完notify()方法后,不会马上释放锁,要直到退出synchronized代码块,当前线程才会释放锁。 notify()一次只随机通知一个线程进行唤醒。
notifyAll()方法 1 public final native void notifyAll () ;
notify()差不多,只不过是使所有正在等待池中等待同一共享资源的全部线程从等待状态退出,进入可运行状态 让它们竞争对象的锁,只有获得锁的线程才能进入就绪状态 每个锁对象有两个队列:就绪队列和阻塞队列
就绪队列:存储将要获得锁的线程
阻塞队列:存储被阻塞的线程