Home Java并发编程笔记(线程篇)
Post
Cancel

Java并发编程笔记(线程篇)

什么是线程

线程是操作系统调度的最小单元。线程通常也被称为是轻量级的进程,在一个进程中可以创建多个线程,每个线程有自己的计数器,堆栈和局部变量,并且可以访问共享变量。处理器可以在线程上高速切换,感觉就像是不同的线程在同时执行。

随着硬件的发展,基于多核处理器以及超线程技术,计算机现今更擅长并行计算。如何利用好处理器上多个核心成了现在的主要问题。

一个线程在一个时刻只能运行在一个处理器核心上,因此单线程的程序只会用到多核处理器的一个核心,而多线程程序,基于多核处理器,通常可以有效提升程序的运行效率。

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 }
View Code

 

在构造完线程后,调用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"
View Code

从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对象的交互。

 

This post is licensed under CC BY 4.0 by the author.

【算法】字符串匹配之Z算法

关于Java中继承多接口同名方法的问题