GuangchaoSun's Blog

Java多线程基础总结

什么是线程?
线程表示一条单独的执行流,它有自己的程序执行计数器,有自己的栈。

创建线程的两种方式

继承Thread

1
2
3
4
5
6
7
8
9
10
11
public class MyThread extends Thread{
@Override
public void run{
System.out.println("hello");
}
}
public static void main(String[] args){
Thread thread = new Thread();
thread.start();
}

start表示启动该线程,使其成为一条单独的执行流,背后,操作系统会分配线程相关的资源,每个线程会有单独的程序执行计数器和栈,操作系统会把这个线程作为一个独立的个体进行调度,分配时间片让它执行,执行的起点就是run方法。

实现Runnable接口

通过继承Thread来实现线程虽然比较简单,但我们知道,Java中只支持单继承,每个类最多只能有一个父类,如果类已经有父类了,就不能再继承Thread,这时,可以通过实现java.lang.Runnable接口来实现线程。

1
2
3
4
public interface Runnable
{
void run();
}

通过接口实现下面的类

1
2
3
4
5
6
7
8
class MyRunnable implements Runnable
{
@Override
public void run()
{
task code
}
}

仅仅实现Runnable是不够的,要启动线程,还是要创建一个Thread对象,但传递一个Runnable对象,如下所示

1
2
3
4
5
public static void main(String[] args){
Runnable r = new MyRunnable();//创建一个类对象
Thread t = new Thread(r); //由Runnable创建一个Thread对象
t.start(); //启动线程
}

sleep方法

1
public static native void sleep(long millis) throw InterruptedException;

睡眠期间,该线程会让出CPU,但睡眠的时间不一定是确切的给定毫秒数,可能有一定的偏差,偏差与系统定时器和操作系统调度器的准确度和精度有关。

yield方法

1
public static native void yield();

调用该方法,是告诉操作系统的调度器,我现在不着急占用CPU,你可以先让其他线程运行。不过,这对调度器也仅仅是建议,调度器如何处理是不一定的,它可能完全忽略该调用。

join方法

让调用join的线程等待该线程结束,该方法的声明为:

1
public final void join() throws InterruptException;

共享内存及问题

共享内存

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
public class ShareMemoryDemo{
private static int shared = 0;
private static void incrShared(){
shared++;
}
static class ChildTread extends Thread{
List<String> list;
public ChildTread(List<String> list){
this.list = list;
}
@Override
public void run(){
incrShared();
list.add(Thread.currentThread().getName());
}
}
public static void main(String[] args) throws InterruptedException{
List<String> list = new ArrayList<String>();
Thread t1 = new ChildTread(list);
Thread t2 = new ChildTread(list);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(shared);
System.out.println(list);
}
}

在代码中,定义了一个静态变量shared和静态内部类ChildThread,在main方法中,创建并启动了两个ChildThread对象,传递了相同的list对象,ChildThread的run方法访问了共享的变量shared和list,main方法最后输出了共享的shared和list的值

通过这个例子,想强调说明执行流、内存和程序代码之间的关系

  • 该例中有三条执行流,一条执行main方法,另外两条执行ChildThread的run方法。
  • 不同执行流可以访问和操作相同的变量,如本例中的shared和list变量。
  • 不同执行流可以执行相同的程序代码,如本例中incrShared方法,ChildThread的run方法,被两条ChildThread执行流执行,incrShared方法是在外部定义的,但被ChildThread的执行流执行,在分析代码执行过程的时候,理解代码在被哪个线程执行是很重要的。
  • 当多条执行流执行相同的程序代码的时候,每条执行流都有相同的栈,方法中的参数和局部变量都有自己的一份。

竟态条件

race condition是指,当多个线程访问和操作同一个对象时,最终执行结果与执行时序有关。

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
public class CounterThread extends Thread{
private static int count = 0;
@Override
public void run(){
try{
Thread.sleep((int)(Math.random()*100));
}catch(InterruptedException e){
}
count++;
}
public static void main(String[] args) throws InterruptedException{
int num = 1000;
Thread[] threads = new Thread[num];
for (int i=0; i<num; i++) {
threads[i] = new CounterThread();
threads[i].start();
}
for (int i=0; i < num; i++) {
threads[i].join();
}
System.out.println(count);
}
}

期望的结果是1000,但运行出来的往往不是这个值,为什么呢?因为count++不是原子操作,它是分为三个步骤的:

  1. 取count当前的值
  2. 在当前值基础上加1
  3. 将新值重新赋值给counter

两个线程可能同时执行第一步,取到了相同的counter值,比如都取到了100,第一个线程执行完后counter变为101,而第二个线程执行完后还是101,最终的结果就与期望不符。

解决方法:

  • 使用synchronized关键字
  • 使用显式锁
  • 使用原子变量

内存可见性

多个线程可以访问和操作相同的变量,但一个线程对一个变量的修改,另一个线程不一定马上就能看到,甚至永远也看不到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class VisibilityDemo{
private static boolean shutdown = false;
static class HelloThread extends Thread{
@Override
public void run(){
while (!shutdown) {
//do nothinf
}
System.out.println("exit hello");
}
}
public static void main(String[] args) throws InterruptedException{
new HelloThread().start();
Thread.sleep(1000);
shutdown = true;
System.out.println("exit main");
}
}

在这个程序中,有一个共享的boolean变量shutdown,初始为false,HelloThread在shutdown不为true的情况下一直死循环,当shutdown为true时退出并输出”exit hello”,main线程启动HelloThread后睡了一会,然后设置shutdown为true,最后输出”exit main”。

期望的结果是两个线程都退出,但实际执行,很可能会发现HelloThread永远都不会退出,也就是说,在HelloThread执行流看来,shutdown永远为false,即使main线程已经更改为了true。

这是怎么回事呢?这就是内存可见性问题,在计算机系统中,除了内存,数据还会被缓存在CPU的寄存器以及各级缓存中,当访问一个变量时,可能直接从寄存器或CPU缓存中获取,而不一定到内存中去取,当修改一个变量时,也可能是先写到缓存中,而稍后才会同步更新到内存中。在单线程的程序中,这一般不是个问题,但在多线程的程序中,尤其是在有多CPU的情况下,这就是个严重的问题。一个线程对内存的修改,另一个线程看不到,一是修改没有及时同步到内存,二是另一个线程根本就没从内存读。

解决方法:

  • 使用volatile关键字
  • 使用synchronized关键字或显式锁同步

上线文切换

一个线程被切换出去后,操作系统需要保存它的当前上下文状态到内存,上下文状态包括当前CPU寄存器的值、程序计数器的值等,而一个线程被切换回来后,操作系统需要恢复它原来的上下文状态,整个过程被称为上下文切换,这个切换不仅耗时,而且使CPU中的很多缓存失效,是有成本的。

中断线程

线程中的run方法执行完毕,并由执行return语句返回时,或者出现了方法中未能捕获的异常,线程将会终止。

想知道线程的中断状态是否被置位:

1
2
3
while(!Thread.currentThread().isInterrupted() && more work to do){
do more work
}

java.lang.Thread

  • void interrupt()
    • 向线程发送中断请求。线程的中断状态被置为true。如果目前线程被一个sleep调用阻塞,那么,InterruptedException异常将被抛出
  • static boolean interrupted()
    • 测试当前线程是否被中断。这是一个静态方法。这一调用将会把当前线程的中断状态重置为false
  • boolean isInterrupted()
    • 测试线程是否被终止。不像静态的中断方法,这一调用不会改变线程的中断状态
  • static Thread currentThread()
    • 返回代表当前执行线程的Thread对象

线程状态

线程有六种状态:New, Runnable, Blocked, Waiting, Timed waiting(计时等待), Terminated(被终止)

一旦调用start方法,线程便处于Runnable状态。一个可运行的线程不一定处于运行状态,这取决于操作系统给线程提供的运行时间。

被阻塞线程和等待线程

  • 当一个线程试图获取一个内部的对象锁,而该锁被其他线程持有,则该线程进入阻塞状态。当其他线程释放该锁,并且线程调度器允许该线程持有该锁的时候,该线程变为非阻塞状态。
  • 当线程等待另一个线程通知调度器一个条件时,它自己进入等待状态。

线程属性

关于线程的优先级:默认情况下,一个线程继承它的父线程的优先级,可以用setPriority方法提高或降低任何一个线程的优先级。

signalAll方法不会立即激活一个等待线程。它仅仅解除等待线程的阻塞,以便这些线程可以在当前线程退出同步方法之后,通过竞争实现对对象的访问。

signal随机解除等待集中某个线程的阻塞状态。

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
51
52
53
54
55
56
57
58
59
60
61
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Created by 11981 on 2017/3/7.
*/
public class Bank {
private final double[] accounts;
private Lock bankLock;
private Condition sufficientFunds;
//Constructs the bank
public Bank(int n, double initialBalance)
{
accounts = new double[n];
for (int i = 0;i < accounts.length; i++)
accounts[i] = initialBalance;
//构建一个可以被用来保护临界区的可重入锁
bankLock = new ReentrantLock();
//使用条件对象来管理锁
sufficientFunds = bankLock.newCondition();
}
public void transfer(int from, int to, double amount) throws InterruptedException
{
bankLock.lock();
try{
while (accounts[from] < amount)
sufficientFunds.await();//将该线程放到等待集中
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.out.printf("%10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf("Total balance: %10.2f%n",getTotalBalance());
sufficientFunds.signalAll();//解除等待集中所有线程的阻塞状态
}
finally {
bankLock.unlock();
}
}
public double getTotalBalance(){
bankLock.lock();
try{
double sum = 0;
for (double a : accounts)
sum += a;
return sum;
}
finally {
bankLock.unlock();
}
}
public int size(){
return accounts.length;
}
}

总结:

  • 锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码
  • 锁可以管理试图进入被保护代码段的线程
  • 锁可以拥有一个或多个相关的条件对象
  • 每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程

synchronized**

使用synchronized关键字时,每一个对象有一个内部锁,并且该锁有一个内部条件。由锁来管理那些试图进入synchronized方法的线程,由条件来管理那些调用wait的线程。

内部锁和条件的局限性:

  • 不能中断一个正在试图获得锁的线程
  • 试图获得锁时不能设定超时
  • 每个锁仅有一个单一条件,可能是不够的

Volatile域

  • 多处理的计算机能够暂时在寄存器或本地内存缓存区中保存内存中的值。结果是,运行在不同处理器上的线程可能在同一个内存位置取到不同的值——可见性问题
  • 编译器可以改变指令的执行的顺序以使吞吐量最大化。这种顺序上的变化不会改变代码语义,但是编译器假定内存的值仅仅在代码中有显式的修改指令时才会改变。然而,内存的值可以被另一个线程改变——有序性问题
  • 但volatile不能保证有序性

volatile关键字为实力域的同步访问提供了一种免锁机制。如果声明一个域为volatile,那么编译器和虚拟机就知道该域可能被另一个线程并发更新的。

线程局部变量(ThreadLocal):
在线程中共享变量是有风险的,有时候为了避免共享变量,使用ThreadLocal辅助类可以为各个线程提供各自的实例。

线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。

ThreadLocal是一种以空间换时间的做法,在每个Thrad里面维护了一个ThreadLocal.ThreadLocalMap把数据进行隔离,数据不共享,自然没有线程安全方面的问题。

1
2
3
4
5
6
7
8
public static final ThreadLocal<SimpleDateFormat> dateFormat =
new ThreadLocal<SimpleDateFormat>()
{
protected SimpleDateFormat initialValue()
{
return new SimpleDateFormat("yyyy-MM-dd");
}
};

tryLock方法试图申请一个锁,在成功获得锁后返回true,否则,立即返回false,而且线程可以立即离开去做其他事。而且如果线程在等待获得一个锁时被中断,将抛出InterruptedException异常。

读/写锁的必要步骤

构造一个ReentrantReadWriteLock对象

1
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

抽取读锁和写锁:

1
2
private Lock readLock = rwl.readLock();
private Lock writeLock = rwl.writeLock();

对所有的获取方法加锁:

1
2
3
4
5
public double getTotalBalance(){
readLock.lock();
try {...}
finally {readLock.unlock();}
}

对所有的方法修改方法加写锁:

1
2
3
4
5
6
public void transfer(...)
{
writeLock.lock();
try {...}
finally {writeLock.unlock();}
}

如何安全地挂起线程?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private volatile boolean suspendRequested = false;
private Lock suspendLock = new ReentrantLock();
private Condition suspendCondition = suspendLock.newCondition();
public void run(){
while(...){
...
if (suspendRequested) {
suspendLock.lock();
try{while(suspendRequested) suspendCondition.await();}
} finally{suspendLock.unlock();}
}
}
public void requestSuspend() {suspendRequested = true;}
public void requestResume(){
suspendRequested = false;
suspendLock.lock();
try{suspendCondition.signalAll();}
finally{suspendLock.unlock();}
}