什么是线程
线程是操作系统调度的最小单元。线程通常也被称为是轻量级的进程,在一个进程中可以创建多个线程,每个线程有自己的计数器,堆栈和局部变量,并且可以访问共享变量。处理器可以在线程上高速切换,感觉就像是不同的线程在同时执行。
随着硬件的发展,基于多核处理器以及超线程技术,计算机现今更擅长并行计算。如何利用好处理器上多个核心成了现在的主要问题。
一个线程在一个时刻只能运行在一个处理器核心上,因此单线程的程序只会用到多核处理器的一个核心,而多线程程序,基于多核处理器,通常可以有效提升程序的运行效率。
Java程序本身就为多线程程序,来看下面的程序,用于打印当前线程。
1 import java.lang.management.ManagementFactory; 2 import java.lang.management.ThreadInfo; 3 import java.lang.management.ThreadMXBean; 4 5 public class ShowThread { 6 public static void main(String[] args) { 7 ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); 8 ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false); 9 for (ThreadInfo threadInfo : threadInfos) { 10 System.out.println(threadInfo.getThreadId() + " " + threadInfo.getThreadName()); 11 } 12 } 13 }
本地输出结果为:
4 Signal Dispatcher
3 Finalizer
2 Reference Handler
1 main
线程的优先级
操作系统会为每个线程分出一个个时间片,当某个线程时间片用完了,就会进行线程调度,并等待下次分配。线程能够分配到多少时间片决定了能够用到的处理器资源的多少,而线程优先级就是影响能够用到多少资源的关键因素。
Java线程用一个整型变量priority来控制线程优先级,总共分为10档,1-10。可以通过setPriority(int)来修改线程优先级。线程的默认优先级为5。通常会为频繁阻塞的线程设定较高优先级,为偏重计算的线程设定较低优先级。不同的JVM与操作系统,对线程规划是不同的,不保证会考虑线程优先级的设定。
线程的状态
Java线程的声明周期中总共有6种不同的状态,在某一时刻,一个线程只可能处于其中一种状态
| NEW | 线程被构建的初始状态,还没有调用start()方法 |
| RUNNABLE | 运行状态,对应于操作系统中的就绪与运行 |
| BLOCKED | 阻塞状态,线程被锁阻塞 |
| WAITING | 等待状态,表示线程在等待其他线程的动作(如通知与中断) |
| TIME_WAITING | 超时等待状态,线程在指定的时间会自行返回 |
| TERMINATED | 终止状态,线程任务已经执行完毕 |
守护线程
守护线程是一种支持型的线程,主要用于后台调度以及其他支持性工作。当JVM中不存在非守护线程时,JVM会退出。
在一个线程启动之前,可以通过Thread.setDaemon(true)设置其为守护线程。
JVM退出时,守护线程中的finally块未必会被执行。
如下的代码片段,将一个线程设置为守护线程。在代码运行结束后,控制台上没有任何输出。因为main线程在新建线程启动后结束,此时JVM没有非守护线程,JVM将会退出,新建线程中的finally因为线程的直接终止而并未被执行。
1 public class DaemonThread { 2 public static void main(String[] args) { 3 Thread thread=new Thread(new Runnable() { 4 @Override 5 public void run() { 6 try { 7 Thread.sleep(1000); 8 } catch (InterruptedException e) { 9 e.printStackTrace(); 10 }finally { 11 System.out.println("守护线程的finally块被执行"); 12 } 13 } 14 }); 15 thread.setDaemon(true); 16 thread.start(); 17 } 18 }
当我们将上述代码的第15与第16行进行交换,即在启动线程后,设置其为守护线程时,main线程中出现了java.lang.IllegalThreadStateException异常,设定守护线程失败。之后新建线程中的finally块被执行,屏幕输出相关语句。
线程的启动
在构造线程对象时,需要提供线程所需要的属性如线程组、优先级、是否为守护线程等。
如下代码为JDK 8u91中java.lang.Thread中初始化的相关代码,中文注释为笔者所加,由于有些冗长,所以对代码进行了折叠。

1 private void init(ThreadGroup g, Runnable target, String name, 2 long stackSize, AccessControlContext acc) { 3 if (name == null) { 4 throw new NullPointerException("name cannot be null"); 5 } 6 7 this.name = name.toCharArray(); 8 // 父线程为当前线程 9 Thread parent = currentThread(); 10 SecurityManager security = System.getSecurityManager(); 11 if (g == null) { 12 /* Determine if it's an applet or not */ 13 14 /* If there is a security manager, ask the security manager 15 what to do. */ 16 if (security != null) { 17 g = security.getThreadGroup(); 18 } 19 20 /* If the security doesn't have a strong opinion of the matter 21 use the parent thread group. */ 22 if (g == null) { 23 g = parent.getThreadGroup(); 24 } 25 } 26 27 /* checkAccess regardless of whether or not threadgroup is 28 explicitly passed in. */ 29 g.checkAccess(); 30 31 /* 32 * Do we have the required permissions? 33 */ 34 if (security != null) { 35 if (isCCLOverridden(getClass())) { 36 security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION); 37 } 38 } 39 40 g.addUnstarted(); 41 this.group = g; 42 // 设定守护线程属性 43 this.daemon = parent.isDaemon(); 44 // 设定优先级 45 this.priority = parent.getPriority(); 46 if (security == null || isCCLOverridden(parent.getClass())) 47 this.contextClassLoader = parent.getContextClassLoader(); 48 else 49 this.contextClassLoader = parent.contextClassLoader; 50 this.inheritedAccessControlContext = 51 acc != null ? acc : AccessController.getContext(); 52 this.target = target; 53 setPriority(priority); 54 if (parent.inheritableThreadLocals != null) 55 this.inheritableThreadLocals = 56 ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); 57 /* Stash the specified stack size in case the VM cares */ 58 this.stackSize = stackSize; 59 60 /* Set thread ID */ 61 tid = nextThreadID(); 62 }
在构造完线程后,调用start()方法可以启动这个线程,start()方法的含义是当前线程同步通知JVM,一旦线程规划器空闲,应当立即启动调用start()方法的线程。
线程的中断
一个线程中可以通过对其他线程调用interrupt()方法进行中断操作。被请求中断的线程会通过检查自身是否被中断来响应请求
volatile关键字
volatile通常被认为是一种轻量级的synchronized,字面上它表示易变的,在并发编程中,它保证了共享变量的可见性。所谓可见性指的是,某个线程对变量进行操作后,其他线程能够读取到操作后的最新结果。
CPU通常不会直接与内存通信,内存中的数据首先会被读取到缓存中进行读写。当对声明了volatile的变量进行写操作时,JVM会向处理器发送一条Lock前缀的指令,表示将变量锁在的缓存行数据写回内存中。
然而volatile并不能保证线程安全,来看下面的例子
1 public class VolatileTest { 2 private static volatile Integer num = 0; 3 4 public static void main(String[] args) throws InterruptedException { 5 Thread[] threads = new Thread[10]; 6 for (int i = 0; i < 10; i++) { 7 threads[i] = new Thread(new Runnable() { 8 @Override 9 public void run() { 10 for (int k = 0; k < 10000; k++) { 11 num++; 12 } 13 } 14 }); 15 threads[i].start(); 16 } 17 for (int i = 0; i < 10; i++) { 18 threads[i].join(); 19 } 20 System.out.println(num); 21 } 22 }
这一段代码的期望运行结果应当是输出100000,然而实际测试发现是一个小于100000的数字,说明voilatile不能保证++操作的原子性。
volatile关键字修饰的变量被读或者被写是使用锁来同步,具有原子性,并且对于volatile修饰变量的读总是可以读到任意线程对这个变量(当时)最新的写入。但是对于读+写这样的复合操作是不具有原子性的。
synchronized关键字
synchronized关键字通常用于修饰方法或者代码块,而方法的话可以修饰静态与非静态方法。
对于修饰代码块,需要指定加锁的对象,可以添加对象锁也可以添加类锁。
对于修饰静态方法,相当于对当前类加锁。
对于修饰实例方法,相当于对当前实例加锁。
为了研究synchronized,编写如下的代码:
1 public class SyncTest { 2 public static void main(String[] args) { 3 synchronized(SyncTest.class) { 4 5 } 6 go(); 7 } 8 public static synchronized void go () { 9 10 } 11 }
接下来,用javap -v 对上例的.class文件进行解析,得到如下的信息(由于过长,进行了折叠):

1 public class SyncTest 2 minor version: 0 3 major version: 52 4 flags: ACC_PUBLIC, ACC_SUPER 5 Constant pool: 6 #1 = Methodref #4.#23 // java/lang/Object."<init>":()V 7 #2 = Class #24 // SyncTest 8 #3 = Methodref #2.#25 // SyncTest.go:()V 9 #4 = Class #26 // java/lang/Object 10 #5 = Utf8 <init> 11 #6 = Utf8 ()V 12 #7 = Utf8 Code 13 #8 = Utf8 LineNumberTable 14 #9 = Utf8 LocalVariableTable 15 #10 = Utf8 this 16 #11 = Utf8 LSyncTest; 17 #12 = Utf8 main 18 #13 = Utf8 ([Ljava/lang/String;)V 19 #14 = Utf8 args 20 #15 = Utf8 [Ljava/lang/String; 21 #16 = Utf8 StackMapTable 22 #17 = Class #15 // "[Ljava/lang/String;" 23 #18 = Class #26 // java/lang/Object 24 #19 = Class #27 // java/lang/Throwable 25 #20 = Utf8 go 26 #21 = Utf8 SourceFile 27 #22 = Utf8 SyncTest.java 28 #23 = NameAndType #5:#6 // "<init>":()V 29 #24 = Utf8 SyncTest 30 #25 = NameAndType #20:#6 // go:()V 31 #26 = Utf8 java/lang/Object 32 #27 = Utf8 java/lang/Throwable 33 { 34 public SyncTest(); 35 descriptor: ()V 36 flags: ACC_PUBLIC 37 Code: 38 stack=1, locals=1, args_size=1 39 0: aload_0 40 1: invokespecial #1 // Method java/lang/Object."<init>":()V 41 4: return 42 LineNumberTable: 43 line 1: 0 44 LocalVariableTable: 45 Start Length Slot Name Signature 46 0 5 0 this LSyncTest; 47 48 public static void main(java.lang.String[]); 49 descriptor: ([Ljava/lang/String;)V 50 flags: ACC_PUBLIC, ACC_STATIC 51 Code: 52 stack=2, locals=3, args_size=1 53 0: ldc #2 // class SyncTest 54 2: dup 55 3: astore_1 56 4: monitorenter 57 5: aload_1 58 6: monitorexit 59 7: goto 15 60 10: astore_2 61 11: aload_1 62 12: monitorexit 63 13: aload_2 64 14: athrow 65 15: invokestatic #3 // Method go:()V 66 18: return 67 Exception table: 68 from to target type 69 5 7 10 any 70 10 13 10 any 71 LineNumberTable: 72 line 3: 0 73 line 5: 5 74 line 6: 15 75 line 7: 18 76 LocalVariableTable: 77 Start Length Slot Name Signature 78 0 19 0 args [Ljava/lang/String; 79 StackMapTable: number_of_entries = 2 80 frame_type = 255 /* full_frame */ 81 offset_delta = 10 82 locals = [ class "[Ljava/lang/String;", class java/lang/Object ] 83 stack = [ class java/lang/Throwable ] 84 frame_type = 250 /* chop */ 85 offset_delta = 4 86 87 public static synchronized void go(); 88 descriptor: ()V 89 flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED 90 Code: 91 stack=0, locals=0, args_size=0 92 0: return 93 LineNumberTable: 94 line 10: 0 95 } 96 SourceFile: "SyncTest.java"
从main方法中,可以看到第4条指令monitorenter和第6条指令monitorexit,它们分别表示监视器进入与监视器退出,也就是获取锁与释放锁。
从go方法的flags中可以看到ACC_SYNCHRONIZED,表示此方法为同步方法。
无论是同步方法还是同步代码块,本质都是线程需要获取到对象的监视器才可以进入,否则将被阻塞,变为BLOCKED状态,进入同步队列SynchronizedQueue中。
来看下面的代码:
1 public class SynchronizedTest { 2 private static Integer num = 0; 3 4 public static void main(String[] args) throws InterruptedException { 5 Thread[] threads = new Thread[10]; 6 for (int i = 0; i < 10; i++) { 7 threads[i] = new Thread(new Runnable() { 8 @Override 9 public void run() { 10 synchronized(num) { 11 for (int k = 0; k < 10000; k++) { 12 num++; 13 } 14 } 15 } 16 }); 17 threads[i].start(); 18 } 19 for (int i = 0; i < 10; i++) { 20 threads[i].join(); 21 } 22 System.out.println(num); 23 } 24 }
上例的代码看似对num变量进行了加锁,以保证任意时刻只有一个线程能访问包含num++;的代码块,然而实质上,这段代码的输出结果并不会是100000,也就是线程不安全。
这是为什么呢?问题的关键在于第2行与第12行。在第2行,我们定义了一个Integer对象num,而第12行,我们在一个synchronized块中执行num++操作,实质上,对Integer对象进行++操作,每一次将会产生一个新的Integer对象,也就是说,其实整个代码运行的过程中每一次上锁的对象其实都是不同的,并且num引用一直在发生变化。
那么为了试验一下,编写如下代码测试是否在循环结束后,线程持有原先的num的锁。
1 public class SynchronizedTest { 2 private static Integer num = 0; 3 4 public static void main(String[] args) throws InterruptedException { 5 Thread[] threads = new Thread[10]; 6 for (int i = 0; i < 10; i++) { 7 threads[i] = new Thread(new Runnable() { 8 @Override 9 public void run() { 10 synchronized(num) { 11 Integer numCopy = num; 12 for (int k = 0; k < 10000; k++) { 13 num++; 14 } 15 System.out.println(Thread.currentThread().getName() + " " + Thread.holdsLock(num) + " " + 16 Thread.holdsLock(numCopy)); 17 } 18 } 19 }); 20 threads[i].start(); 21 } 22 for (int i = 0; i < 10; i++) { 23 threads[i].join(); 24 } 25 System.out.println(num); 26 } 27 }
输出结果如下:
Thread-0 false true
Thread-2 false true
Thread-1 false true
Thread-3 false true
Thread-4 false false
Thread-5 false true
Thread-6 false true
Thread-7 false false
Thread-8 false false
Thread-9 false true
69262
也就是说,确实在循环之后,线程不持有现在num对象的锁,因为num早就已经变了很多次了,但是程序的第13行不是已经在同步块的一开始就把当时的num赋值给numCopy了吗。为什么10个线程的第二个布尔判断,有的是true有的是false呢?这是个比较tricky的地方,但是也很容易想到,因为在进入num同步块之后还未完成第13行的赋值时,有可能其他线程已经更新了num引用,所以numCopy引用未必是进入同步块时上锁的那个num对象。
上面代码的num变量并未被volatile修饰,如果加上volatile后,代码能够产生期望的结果100000吗?答案也是否定的,还是因为volatile只是保证了单读与单写是原子性的操作,但是++这样的复合操作,不具有原子性。
要让上面的代码产生期望的结果100000,并做到线程安全方法其实不止一种。
来看下面的代码,使用的是静态同步方法
1 public class SynchronizedTest { 2 private static volatile Integer num = 0; 3 4 private static synchronized void add() { 5 num++; 6 if (!Thread.holdsLock(SynchronizedTest.class)) { 7 throw new RuntimeException(); 8 } 9 } 10 11 public static void main(String[] args) throws InterruptedException { 12 Thread[] threads = new Thread[10]; 13 for (int i = 0; i < 10; i++) { 14 threads[i] = new Thread(new Runnable() { 15 @Override 16 public void run() { 17 for (int k = 0; k < 10000; k++) { 18 add(); 19 } 20 } 21 }); 22 threads[i].start(); 23 } 24 for (int i = 0; i < 10; i++) { 25 threads[i].join(); 26 } 27 System.out.println(num); 28 } 29 }
静态同步方法,相当于对类加锁,进入静态同步方法的线程必须获得类锁。上面的代码无异常地运行出了100000,验证了所有进入同步方法的线程都首先得要获得SynchronizedTest的类锁。
等待与通知
在java.lang.Object中,具有如下几个等待与通知相关的方法:
| notify() | 通知一个在对象上等待的线程使其从wait()方法返回,前提是其取得对象的锁 |
| notifyAll() | 通知所有在该对象上等待的线程 |
| wait() | 使得调用方法的线程进入WAITING状态直到被其他线程通知或者被终端,在调用方法后,会释放对象的锁 |
| wait(long) | 超时等待一段时间,参数为毫秒,如果没有通知就超时返回 |
| wait(long,int) | 对超时时间纳秒级的精细控制 |
当一个线程A调用某个对象O的wait()方法后,进入等待状态,其他线程B调用对象O的notify()/notifyAll()方法,线程A在被通知后,会从O的wait()方法中返回,继续后续的操作。等待与通知在这个过程中就像是信号开关一般完成A与B线程通过O对象的交互。
