Java基础(十二)——多线程

线程总结

创建、启动线程

extends Thread:通过继承获取当前线程this(常省略)
implements Runnable|Callable:通过实现接口获取当前线程Thread.currenThread()

线程的声明周期

1) 新建new;
2) 就绪start(),但并未立即执行,让当前线程(主线程)睡眠1ms:Thread.sleep(1);
3) 运行:执行run()方法中的线程执行体;
4) 阻塞
5) 死亡

控制线程

join():由当前使用线程的程序调用线程对象.join(),调用join()方法的线程将先执行。
setDaemonThread()设置成后台线程。
sleep():暂停,进入阻塞。
yield():暂停,进入就绪。

同步

1) 同步代码块
同步方法:非static的同步监视器为this,即为调用该方法的对象
2) 同步锁(Lock):显式定义同步对象
Lock的实现类:ReentrantLock(可重入锁:一个线程可以对已被加锁的ReentrantLock锁再次加锁。)
ReadWriteLock(读写锁)的实现类:ReentrantReadWriteLock <- Java8新增的StampedLock可代替它。

线程通信

程序通常无法准确地控制线程的轮换执行,但java提供了一些机制来保证线程协调运行。

1) synchronized:wait()、notify()、notifyAll()。
2) Lock对象:await()、signal()、signalAll()。
3) BlockingQueue(阻塞队列)

ThreadGroup

可对多个线程同时控制

线程池

系统启动一个新线程的成本是比较高的,因为它涉及操作系统交互。当系统中需要创建大量生存周期短暂的线程时,更应该考虑用线程池。

线程安全

注意:
this关键字总是指向调用该方法的对象:
(1)在构造器中使用this引用时,this总是引用该构造器正在初始化的对象。对构造器正在初始化的对象的成员变量赋值。
(2)在方法中引用调用该方法的对象。它所代表的对象只能是当前类,只有该方法被调用时,this所代表的对象才被确定下来。谁在调用这个方法,this就代表谁。
super关键字
在子类中访问被覆盖的父类变量:super.a
子类构造器调用父类构造器:super(a),其中a是变量
类若没有提供构造器,使用它的静态方法来获取实例。

所有运行中的任务通常对应一个进程(Process)。当一个程序进入内存运行时,即变成一个进程。进程有一定的独立功能,是系统进行资源分配和调度的一个独立单位。
进程的三个特征:独立性、动态性、并发性。
并发性(concurrency)和并行性(parallel)不同。并行是指在同一时刻,有多条指令在多个处理器上同时执行;并发是指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得宏观上具有多个进程同时执行的效果。
对于一个CPU而言,它在某个时间点只能执行一个程序,即只能执行一个进程。
进程之间不能共享内存,但线程之间共享内存非常容易。

线程的创建和启动

所有的线程对象都必须是Thread类或其子类的实例。有三种方式创建线程类:

继承Thread类创建线程类

1) 定义Thread类的子类,并重写该类的run()方法,run()方法的方法体就代表了线程需要完成的任务,因此run()方法称为线程执行体;
2) 创建线程对象,即创建Tread子类的实例;
3) 调用线程对象的start()方法来启动该线程。
Java程序开始运行时,至少会创建一个主线程,主线程的执行体是由main方法确定的,main方法的方法体就是主线程的线程执行体。

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
/*
* 通过继承Thread类来创建线程类
*/
public class FirstThread extends Thread{
private int i;
//重写run()方法,即重写线程执行体
public void run(){
//i变量是实例变量,不是局部变量。由于每次创建线程时都需要创建一个FirstThread对象,
//所以Thread-0与Thread-1之间不能共享实例变量。
for(; i < 10 ; i++){
//使用this可获取当前线程this.getName() this代表调用该方法的对象。
//调用getName()方法返回当前线程的名字
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args){
for(int i = 0; i < 10; i++){
//调用Thread类的currenThread()方法获取当前线程
System.out.println(Thread.currentThread().getName()
+ " " + i);
if(i == 5){
//创建并启动第一个线程
new FirstThread().start();
//创建并启动第二个线程
new FirstThread().start();
}
}
}
}

输出:

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
main 0
main 1
main 2
main 3
main 4
main 5
main 6
Thread-0 0
main 7
Thread-0 1
Thread-0 2
Thread-0 3
Thread-0 4
Thread-0 5
Thread-0 6
Thread-0 7
Thread-0 8
Thread-0 9
main 8
main 9
Thread-1 0
Thread-1 1
Thread-1 2
Thread-1 3
Thread-1 4
Thread-1 5
Thread-1 6
Thread-1 7
Thread-1 8
Thread-1 9

Thread.currentThread()方法:currentThread()是Thread类的静态方法,总是返回当前正在执行的线程对象。(静态方法,可直接由类调用)
getName():是Thread类的实例方法,返回调用该方法的线程名字。(实例方法,需要对象调用)
可用setName(String name)为线程设置名字。默认情况下,主线程名字为main,用户启动的多个线程的名字为Thread-0、Thread-1…。
使用继承Thread类的方法创建线程类时,多个线程之间无法共享线程类的实例变量。

实现Runnable接口创建线程类

1) 定义Ruannable接口的实现类,并重写该接口的run()方法;
2) 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。(Runnable对象仅仅作为Thread对象的target)
SecondThread st = new SecondThread();
new Thread(st);
或new Thread(st, “新线程1”); //可以在创建Thread对象时为该Thread对象指定一个名字
3) 调用线程对象的start()方法来启动线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
*使用Runnable接口创建线程类
*/
public class SecondThread implements Runnable{
private int i;
public void run(){
for(; i < 10; i++){
System.out.println(Thread.currentThread().getName()
+ " " + i);
}
}
public static void main(String[] args){
for(int i = 0; i < 10; i++){
System.out.println(Thread.currentThread().getName()
+ " " + i);
if(i == 5){
SecondThread st = new SecondThread();
new Thread(st, "线程1").start();
new Thread(st, "线程2").start();
}
}

}
}

多个线程可以共享同一个target,所以多个线程可以共享同一个线程类的实例变量。
通过继承来获得当前线程比较简单,直接用this就可以了;但通过Runnable接口来获得当前线程对象,则必须使用Thread.currentThread()方法

使用Callable接口和FutureTask创建线程(实现Callable接口与实现Runnable接口的方式基本相同)

1) 创建Callable接口的实现类,并实现call()方法,该方法将作为线程执行体,且该方法有返回值,允许声明抛出异常,在创建Callable实现类的实例;
Callable接口是函数式接口,可以通过Lambda表达式创建Callable对象。函数式接口 <-> Lambda表达式。若不使用Lambda表达式,要创建实例,需要先实现类实现Callable接口,再创建实现类的对象,较繁琐。
2) 使用FutureTask类包装Callable对象;
3) 使用FutureTask对象最为Thread对象的target创建并启动线程;
4) 调用FutureTask对象的get()方法获得子线程执行结束后的返回值。

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
/*
* 使用Callable接口和FutureTask创建线程
* (实现Callable接口与实现Runnable接口的方式基本相同)
*/
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class ThirdThread{
public static void main(String[] args){
//创建Callable对象
ThirdThread rt = new ThirdThread();
//使用Lamda表达式创建Callable<Integer>对象
//使用FutureTask来包装Callable对象
FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>)()->{
int i = 0;
for(; i < 10; i++){
System.out.println(Thread.currentThread().getName()
+ " " + i);
}
//call()方法可以有返回值
return i;
});
for(int i = 0; i < 10; i++){
System.out.println(Thread.currentThread().getName()
+ " " + i);
if(i == 5){
new Thread(task, "有返回值的线程").start();
}
}
try{
//获取子线程call()方法的返回值
System.out.println("子线程的返回值:" + task.get());
}
catch(Exception ex){
ex.printStackTrace();
}
}
}

函数式接口是只包含一个抽象方法的接口。使用匿名内部类来实例化函数式接口的对象,有了Lambda表达式,这一方式可以简化。

线程的声明周期

经历的状态:新建(new)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead)。
线程状态转换

新建

new创建线程,JVM为其分配内存,并初始化成员变量。

就绪

线程对象调用了start()方法后,该线程将处于就绪状态。(调用start()方法来启动线程,系统会把该run()方法当成线程执行体来处理。)JVM会为其创建方法调用栈和程序计数器。此时该线程还没运行,只与该线程何时开始运行,取决于JVM里线程调度器的调度。
如果希望调用子线程的start()方法后子线程立即开始执行,可以让当前线程(主线程)睡眠1毫秒:Thread.sleep(1);

运行

处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体。
当一个线程开始运行后,它不可能一直处于运行状态(除非它的线程执行体足够短),线程在运行时需要被中断,目的是是其他线程获得执行的机会。
所有现代的桌面和操作系统都采用抢占式调度策略,系统会给每个可执行的线程一小段时间来处理任务,当该段时间用完后,系统会剥夺该线程所占用的资源,让其他线程获得执行的机会。系统会考虑线程的优先级。

阻塞

死亡

控制线程

joint线程

由使用线程的程序调用joint()方法,则调用线程(如main线程)将会被阻塞,直到被join()方法加入的join线程执行完为止。

后台线程(Daemon Thread)

setDaemonThread()设置成后台线程。
JVM的垃圾回收线程就是后台线程。

sleep()暂停,进入阻塞。

yield():暂停,线程让步,进入就绪。

改变线程优先级

MAX_PRIORITY(10)、MIN_PRIORITY(1)、NORM_PRIORITY(5)。

线程同步

一个账户多个人银行取钱问题:当两个进程并发修改同一个文件时,容易出现该问题。

同步代码块

Java多线程引入了同步监视器,使用同步监视器的通用方法是同步代码块:

1
2
3
4
synchronized(obj){

//同步代码块
}

obj就是同步监视器:阻止两个线程对同一个共享资源进行并发访问
线程开始执行同步代码块之前,必须先获得同步监视器的锁定。
任何时候只能有一个线程可以获得同步监视器的锁定。

同步方法

对于synchronized修饰的实例方法(没有static修饰的方法)无需显示指定同步监视器,同步方法的同步监视器是this,也就是调用该方法的对象。

释放同步监视器的锁定

同步锁(Lock)

Java5提供了功能更强大的同步机制:通过显式定义同步锁对象来实现同步,同步锁由Lock对象充当。
Lock提供了比synchronized代码块和synchronized方法更广泛的锁定操作。
Lock是控制多个线程对共享资源进行访问的工具,每次只能有一个线程对Lock对象加锁
Java5提供的两个根接口:Lock、ReadWriteLock,并为Lock提供ReentrantLock(可重入锁)实现类,为ReadWriteLock提供ReentrantReadWrieteLock实现类。Java8新增StampedLock类,大多数场景下可代替ReentrantReadWrieteLock实现类。
可重入即一个线程可以对已被加锁的ReentrantLock再次加锁。
在实现线程安全的控制中,ReentrantLock(可重入锁)对象可以显示地加锁、释放锁。通常代码格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
class X{
private final ReentrantLock lock = new ReentrantLock(); //定义锁对象

public void m(){ //定义需要保证线程安全的方法
lock.lock(); //加锁
try{
//需要保证线程安全的代码
}finally{ //使用finally块来保证释放锁,finally用于回收在try块中打开的物理资源
lock.unlock();
}
}
}

同步锁与使用同步方法有点类似,只是使用Lock时显式使用Lock对象作为同步锁,而使用同步方法时系统隐式使用当前对象作为同步监视器。

死锁

两个线程相互等待对方释放同步监视器时就会发生死锁。Java虚拟机没有检测,也没有采取措施来处理死锁情况,因此多线程编程时应该采取措施释放死锁。
一旦死锁发生,所有线程处于阻塞状态,无法运行。程序不会发生任何异常,程序不会向下执行,也不会给出任何提示。
由于Thread类的suspend()方法也很容易导致死锁,因此java推荐不适用这个方法暂停线程的执行。

线程通信

程序通常无法准确地控制线程的轮换执行,但java提供了一些机制来保证线程协调运行。

传统的线程通信

Object类的wait()、notify()、notifyAll()方法,这三个方法必须由同步监视器对象来调用。
例注意点:取钱线程已经执行结束,等待其他线程来取钱,并不是等待其他线程释放同步监视器。不要把程序阻塞和死锁等同

使用Condition控制线程通信

不使用synchronized关键字来保证同步,而是直接使用Lock对象来保证同步,则系统中不存在隐式的同步监视器。
当使用Lock对象来保证线程同步是,Java提供了一个Condition类来保持协调。获取Lock实例的Condition实例,调用Lock对象的newCondition()方法即可。
Condition类的await()、signal()、signalAll()方法。
这种控制线程同步方式与synchronized关键字来控制线程同步(即1)的步骤基本相似。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private final Lock lock = new ReentrantLock();
private final Condition cond = lock.newCondition();

public void draw(double drawAmount){
lock.lock();
try{
if(!flag){ //如果没有存款
cond.await()
} else{
//取钱
cond.signalAll(); //唤醒其它线程
}
} catch(InterruptedException ie){
...
} finally{
lock.unlock();
}
}

使用阻塞队列(BlockingQueue)控制线程

Java5提供了一个BlockingQueue接口,它是Queue的子接口,但主要作用不是作为容器,而是作为线程同步工具。
BlockingQueue的特征:当生产者试图向BlockingQueue中放入元素时,如队列已满,则该线程被阻塞;当消费者试图从BlockingQueue中取出元素时,如队列为空,则该线程被阻塞。

BlockingQueue包含的方法之间的对应关系
BlockingQueue与其实现类之间的类图
以ArrayBlockingQueue测试阻塞队列的put()方法的用法:

1
2
3
4
5
6
7
8
9
public class BlockingQueueTest{
public static void main(String[] args)
throws Exception{
BlockingQueue<Stirng> bq = new ArrayBlockingQueue<>(2);//定义长度为2的阻塞队列
bq.put(“Java”);
bq.put(“Java”);
bq.put(“Java”); //阻塞线程
}
}

对于上面程序,队列未满放入元素时,使用put()、add()、offer()方法的效果完全一样。放入两个元素后,如果使用put()方法放入元素会阻塞线程;使用add()方法会引发异常;使用offer()方法会返回false,元素不会放入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.concurrent.BlockingQueue;
/*
*Producer线程
*/
class Producer extends Thread{
private BlockingQueue<String> bq;
public Producer(BlockingQueue<String> bq){
this.bq = bq;
}
public void run(){
String[] strArr = new String[]{
"元素1", "元素2","元素3"
};
for (int i = 0; i < 10; i++){
System.out.println(getName() + "生产者准备生产元素集合");
try{
Thread.sleep(200);
bq.put(strArr[i % 3]);
}
catch(Exception e){e.printStackTrace();}
System.out.println(getName() + "生产完成:" + bq);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.concurrent.BlockingQueue;
/*
*Consumer线程
*/
class Consumer extends Thread{
private BlockingQueue<String> bq;
public Consumer(BlockingQueue<String> bq){
this.bq = bq;
}
public void Producer(BlockingQueue<String> bq){
this.bq = bq;
}
public void run(){
while(true){
System.out.println(getName() + "消费者准备消费元素集合");
try{
Thread.sleep(200);
bq.take();
}
catch(Exception e){e.printStackTrace();}
System.out.println(getName() + "消费完成:" + bq);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
/*
*利用BlockingQueue实现线程通信
*由于队列长度为1,因此3个生产者线程无法连续放入元素,
*必须要等消费者线程取出一个元素后,3个生产者线程之一才能放入元素
*/
public class BlockingQueueTest2{
public static void main(String[] args){
//创建一个容量为1的BlockingQueue
BlockingQueue<String> bq = new ArrayBlockingQueue<>(1);
//启动3个生产者线程
new Producer(bq).start();
new Producer(bq).start();
new Producer(bq).start();
//启动1个消费者线程
new Consumer(bq).start();
}
}

上面程序,Thread-0、Thread-1、Thread-2线程都必须等到Thread-3执行后才能执行。

线程组和未处理的异常

ThreadGroup表示线程组,它可以对一批线程进行分类管理,对线程组的控制相当于程序直接同时控制这批线程中途不可改变线程所属线程组,直到该线程死亡
默认情况下,A线程创建了B线程,在没有指定B线程线程组的情况下,子线程B线程和创建它的父线程A同属一个线程组
ThreadGroup的void uncaughtException(Thread t, Throwable e)方法,处理线程组内任意线程所抛出的未处理异常。t表示出现异常的线程,e表示该线程抛出的异常。
Thread.UncaughtExceptionHandler是Thread类的一个静态内部借款,该接口内只有一个方法:void uncaughtException(Thread t, Throwable e)方法。
ThreadGroup类实现了Thread.UncaughtExcepitonHandler接口,因此每个线程所属的线程组将会作为默认的异常处理器。
当一个线程抛出未处理异常时,通常的线程组处理异常的流程如下:

1) 若设置了默认异常处理器(由setDefaultUncaughtExceptionHandler()方法设置的异常处理器),调用该异常处理器来处理异常。
2) 若该线程有父线程,调用父线程组的uncaughtExcepiton()方法来处理异常。
3) 如果该异常对象时ThreadDeath的对象,不做任何处理,否则System.err打印错误输出流,并结束该线程。

1
2
3
4
5
6
7
8
9
/*
*定义异常处理器
*/
//ThreadGroup类实现Thread.UncaughtExceptionHandler接口
class MyExHandler implements Thread.UncaughtExceptionHandler{
public void uncaughtException(Thread t, Throwable e){
System.out.println(t + "线程出现了异常:" + e );
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
/*
*为主线程设置异常处理器,当主线程运行时抛出未处理异常时,
*该异常处理器将会起作用
*/
public class ExHandler{
public static void main(String[] args){
//设置主线程的异常处理器
Thread.currentThread().setUncaughtExceptionHandler
(new MyExHandler());
int a = 5 / 0;
System.out.println("程序正常结束");
}
}

Thread.currentThread()表示当前线程,放在main()方法中就表示主线程。
虽然程序中粗体diamante指定了异常处理器对未捕获的异常进行处理,但程序依然不会结束,这与catch不同:catch捕获的异常不会向上传播给调用者,但使用异常处理器对异常进行处理后会将异常传播给上一级调用者。

线程池

系统启动一个新线程的成本是比较高的,因为它涉及操作系统交互。当系统中需要创建大量生存周期短暂的线程时,更应该考虑用线程池。除此之外,使用线程池还可有效控制系统中并发线程的数量。

Java8改进的线程池

与数据库连接池类似的是,当系统启动时,线程池创建大量空闲的线程,程序将一个Runnable对象会Callable对象传给线程池,线程池会启动给一个线程来执行它们的run()方法和call()方法,当run()方法和call()方法结束后,程序并不会死亡,而是再次返回线程池中成为空闲状态,等待下一次程序将一个Runnable对象会Callable对象传给线程池。
Executors工厂类来产生线程池。
使用线程池来执行线程任务的步骤:

1) 调用Executors类的静态工厂方法创建一个ExecutorService对象(7中创建方式),该对象代表一个线程池;
2) 创建Runnable实现类或Callable实现类的实例,作为线程执行任务;
3) 调用ExecutorService对象的submit()方法来提交Runnable实现类或Callable实现类的实例;(7中提交方式)
4) 当不想提交任何任务时,调用ExecutorService对象的shutdown()方法来关闭线程池。
关于上述第2)步,创建了实现类之后没有直接像之前一样创建线程、执行线程来执行实例对象所代表的任务,而是通过3)4)步的线程池来执行该任务。
System.exit(0); //终止虚拟机

Java8增强的ForkJoinPool

为了充分利用多CPU、多核CPU的优势,java7提供了ForkJoinPool来支持将一个任务拆分成多个“小任务”并行计算,再把多个“小任务”的结果合并成总的计算结果。ForkJoinPool是ExecutorService的实现类,因此是一种特殊的线程池。

线程池工具类的类图
RecursiveAction抽象类代表没有返回值的任务,如打印;RecursiveTask代表有返回值的任务,如将多个数相加。

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
import java.util.concurrent.RecursiveAction;
/*
*执行没有返回值的任务,将任务分解。
*并将任务交给ForkJoinPool来执行。
*/
//RecursiveAction是抽象类,
//继承RecursiveActio类来实现任务分解,无返回值
class PrintTask extends RecursiveAction{
//每个小任务打印5个数
private static final int THRESHOLD = 5;
private int start;
private int end;
//构造器:打印从start到end的任务
public PrintTask(int start, int end ){
this.start = start;
this.end = end;
}
@Override
protected void compute(){
if (end - start < THRESHOLD){
for (int i = start; i < end; i++){
System.out.println(Thread.currentThread().getName()
+ "的i值" + i);
}
}
else{
//要打印的数超过50,则分解任务
int middle = (start + end) / 2;
PrintTask left = new PrintTask(start, middle);
PrintTask right = new PrintTask(middle, end);
//并行执行这两个小任务
left.fork();
right.fork();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;
/*
* 打印20个数
*/
public class ForkJoinPoolTest{
public static void main(String[] args)
throws Exception{
//创建ForkJoinPool类线程池
ForkJoinPool pool = new ForkJoinPool();
//提交可分解的PrintTask任务
pool.submit(new PrintTask(0, 20));
pool.awaitTermination(2, TimeUnit.SECONDS);
//关闭线程池
pool.shutdown();
}
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ForkJoinPool-1-worker-2的i值7
ForkJoinPool-1-worker-1的i值17
ForkJoinPool-1-worker-0的i值5
ForkJoinPool-1-worker-3的i值2
ForkJoinPool-1-worker-0的i值6
ForkJoinPool-1-worker-1的i值18
ForkJoinPool-1-worker-2的i值8
ForkJoinPool-1-worker-1的i值19
ForkJoinPool-1-worker-0的i值0
ForkJoinPool-1-worker-3的i值3
ForkJoinPool-1-worker-0的i值1
ForkJoinPool-1-worker-1的i值15
ForkJoinPool-1-worker-2的i值9
ForkJoinPool-1-worker-1的i值16
ForkJoinPool-1-worker-0的i值12
ForkJoinPool-1-worker-0的i值13
ForkJoinPool-1-worker-3的i值4
ForkJoinPool-1-worker-0的i值14
ForkJoinPool-1-worker-2的i值10
ForkJoinPool-1-worker-2的i值11

可见,ForkJoinPool启动了4个线程来执行这个打印任务,这是因为计算机的CPU是四核的。而且可以看出这20个数不是连续打印的,而是进行了分集。

线程相关类

ThreadLocal

java为线程安全提供了一些工具类,如ThreadLocal类(提供了泛型支持),它代表一个线程局部变量,避免并发访问线程安全问题。
通过使用ThreadLocal类可以简化多线程编程时的并发访问,可以很简捷地隔离多线程程序的资源竞争。
本质:为每个使用该变量的线程都提供一个变量值的副本,使每个线程都可以独立地改变自己的副本,不会和其他线程的副本冲突。
同步机制与ThreadLocal不同:同步机制是为了同步对个线程对相同资源的并发访问,是多个线程之间进行通行的有效方式;而ThreadLocal是为了给多个线程的数据共享,从根本上解决多个线程之间对共享资源(变量)的竞争,也就不需要多个线程进行同步。
通常建议:如果多个线程之间需要共享资源,以达到线程之间的通信功能,就使用同步机制;
如果仅仅需要隔离多个线程之间的共享冲突,则可以使用ThreadLocal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Account{
//定义一个ThreadLocal类型的变量,是一个线程局部变量
//每个线程都会保留一个副本
private ThreadLocal<String> name = new ThreadLocal<>();
public Account(String str){ //初始化name成员变量的构造器
this.name.set(str); //设置此线程局部变量name中 当前线程副本的值
//访问当前线程副本的值
System.out.println(this.name.get());
}
public String getName(){
return name.get();
}
public void setName(String str){
this.name.set(str);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyTest extends Thread{
//定义一个Account类型的成员变量
private Account account;
public MyTest(Account account, String name){
super(name);
this.account = account;
}
public void run(){
for (int i = 0; i < 10; i++){
if ( i == 6){
//当i=6将账户名替换成当前线程名
account.setName(getName());
}
System.out.println(account.getName()
+ "账户的i值" + i);
}
}
}
1
2
3
4
5
6
7
8
public class ThreadLocalTest{
public static void main(String[] args){
Account at = new Account("初始名");
//启动两个线程,共享一个账户名
new MyTest(at, "线程甲").start();
new MyTest(at, "线程乙").start();
}
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
初始名   //主线程访问账户名有值
null账户的i值0 //第2个线程启动后账户名为null,这是因为账户名是一个副本
null账户的i值1
null账户的i值2
null账户的i值3
null账户的i值4
null账户的i值5
线程乙账户的i值6 //第2个线程在i==6后看到账户名已经有值了
线程乙账户的i值7
线程乙账户的i值8
线程乙账户的i值9
null账户的i值0
null账户的i值1
null账户的i值2
null账户的i值3
null账户的i值4
null账户的i值5
线程甲账户的i值6
线程甲账户的i值7
线程甲账户的i值8
线程甲账户的i值9

可以看出账户名有3个副本,它们的值互不干扰,各自完全拥有自己的ThreadLocal变量,这就是ThreadLocal变量的用户。

包装线程不安全的集合

ArraList、LinkedList、HashSet、TreeSet、HashMap、TreeMap等都是线程不安全的集合。线程不安全:当多个并发线程向这些集合中存、取元素时,可能会破坏这些集合的数据完整性。
使用Collections提供的类方法(静态方法)把这些集合包装成线程安全的。
如需要在多线程中使用线程安全的HashMap集合:

1
HashMap m = Collections.synchronizedMap(new HashMap()); //在集合创建后立即包装