多线程基础总结

之前有看过很多线程类的资料,但总是有一种雾里看花的感觉。基本的知识只能说是了解,但是基本的使用还有些困难。之前看了《JAVA编程思想》里面的介绍。感觉还是有些模糊,这里先对《疯狂java讲义》(这本书感觉应该是java入门级的扛把子吧!哈哈~)中的知识做一个整理吧!毕竟线程这块,就目前阶段用的还是比较少的,原因不是场景少,而是会用的人少。单线程的程序能力是有限的,很多工具用到了多线程,但是里面已经写好了,我们自然也就不关心了。例如:网络编程中的ServerSocket。这里先做一个知识点的整理,以后在抽时间做一些线程的项目加深一下理解。

概念

进程

进程可以广义的理解为电脑中的一个应用、一个程序。大多数操作系统都支持多进程并发运行,这给我们感觉是这些进程都在同时进行工作,但是实际上对于一个CPU而言,它在某个时间点只能进行一个程序,然后CPU不断的在这些进程之间轮换执行。

线程

线程是进程的执行单元,在一个进程中可以同时并发的处理多个任务。

  • 单线程
    单线程就是顺序的执行一个执行流。
  • 多线程
    多线程则是在一个可以包含多个执行流的程序中,多个顺序流之间互不干扰、独立并发的执行。
    细节:(一个线程必须有一个父进程,线程之间可以拥有独立的堆栈,程序计数器和自己的局部变量,但是不用于独立的系统资源,线程之间的资源是共享的

    并行与并发

  • 并发
    同一个时刻只能有一条指令执行,但是多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果。
  • 并行
    在同一个时刻,有多条指令在多个处理器上同时执行。

    优势

    线程之间共享系统分配给一个进程的虚拟空间,以及资源。
    线程之间能够共享内存,但是进程之间不能共享内存。
    系统创建进程时需要为该进程重新分配系统资源,但是线程的单价就很小,因此使用多线程来实现多任务并发比多进程的效率高。
    (读到这里,我可能明白了,之前的一个疑惑。假设我同时有n个馒头,我可以开多个吃的线程,来消灭它。当然也可以重复开多个进程来消灭它。上述才是线程的真正优势)

    线程的创建和启动

    (之前有总结过一个,说实话忘了)

    类继承Thread

  • 创建一个子类,继承Thread(extends Thread)
  • 重写类的run()方法,这就是告诉程序,我是干啥的。
  • 创建子类的实例,并调用start()方法来启动该线程。

    实现Runnable接口(implement Runnable)

  • 创建一个类实现Runnable接口创建线程类

  • 重写run()方法
  • 创建一个实现类的实例(一次就好)
  • 以这个实例作为Thread的目标来创建Thread对象(new Thread(target))
  • 调用线程的start()方法,来启动该线程。

注意:进行多线程编程的时候,要记得java程序运行时默认的主线程,main()方法是主线程的线程执行体。

使用Callable 和 Future创建线程

上面实现的Run方法都没有返回值,那么如何才能实现一个有返回值的方法呢
Java5 开始Java提供了一个Callable的接口,提供了一个call()方法可以作为线程的执行体。可以有返回值 或者抛出异常
实现步骤如下:

  • 创建一个类实现Callable,实现call()方法,该call()方法作为线程的执行体,并且call()有返回值
  • 创建一个Callable实现类的实例,实现FutureTask类包装Callable对象,这个FutureTask对象(接口)封装了Callable对象的call()方法的返回值。
    FutureTask tast = new FutureTask(实例类名);
  • 使用FutureTask对象作为Th re a d对象的target创建并启动新的线程
  • 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

    三种方式的对比

    方式一 : 实现Runable和实现Callable接口可以归纳为一种实现方式
    方式二:继承Thread类

方式一

  • 优势
    1、可以继承其他的类
    2、可以共享同一个target对象,非常适合多个相同的线程来处理同一份资源的情况
  • 劣势
    编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()

方式二

  • 优势
    代码简单,访问当前线程只需使用this
  • 劣势
    线程已经继承了类,因此不能在继承其他父类

线程的生命周期

这一块对于以后做关于多线程的项目应该是非常重要的。
线程的生命周期主要分为一下五个方面:

  1. 新建(New 使用new关键字创建对象,就是处于新建状态)、
  2. 就绪(Runable 调用线程的start()方法,就处于就绪状态)、
  3. 运行(run JVM里面的线程调度器 调度run方法执行就 处于运行状态)、
  4. 阻塞(Blocked )、当线程运行后,这个线程几乎不会一直执行的,而是由底层的调度策略决定的(一般使用抢占式的方式,即线程争抢有限的执行资源)
    线程阻塞的主要原因有:
    线程调用sleep();线程调用了一个I/O方法;线程试图获得一个同步监视器;线程等待某个通知;程序调用了线程的suspend()方法(resume()恢复)将线程挂起

  5. 死亡(dead 线程结束处于死亡状态;stop()方法被调用;抛出异常)
    当主线程结束后,其他线程并不会受影响,子线程跟主线程的地位是一样的
    注意:不要直接调用重写的run()方法,直接调用方法会让程序直接运行一次后结束,不是以线程状态进行执行的;要调用线程的start()方法。
    线程的五个状态之间是可以相互转化的。
    在这里插入图片描述

    线程的控制

    join

    使用方式

    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
    public class Test  extends Thread{

    public Test(String name) {
    super(name);
    }
    @Override
    public void run() {
    // TODO Auto-generated method stub
    for(int i=0;i<100;i++) {
    System.out.println(getName()+" "+i);
    }
    }
    public static void main(String[] args) throws InterruptedException {
    //启动一个子线程
    new Test("新的线程").start();
    for(int i=0;i<100;i++) {
    if(i==20) {
    Test te = new Test("join的线程");
    te.start();
    te.join();
    }
    System.out.println(Thread.currentThread().getName()+" "+i);
    }
    }
    }

执行结果:
新的线程 15
main 19
….
join的线程 0
新的线程 24
join的线程 1
新的线程 25

join的线程 99
main 20

1
2
总结:从上述实验可以看出加入join线程的效果为,先是main主线程与新的线程抢占式执行,然后到main线程执行到19的时候,join的线程开始与新的线程交替执行,main线程等待join线程完全执行完毕后在继续执行。
这是一个让一个线程等待另一个线程完成的方法。

后台线程

该类线程是在程序后台运行的,目的是为其他的线程提供服务的。例如jvm的垃圾回收机制就是后台线程,后台线程在前台线程全部执行结束后会自动的死亡

1
2
将一个线程设置为后台线程的方法为:
setDaemon(true)

注意:该方法必须设置在start()方法之前。

线程睡眠sleep()

sleep(long mills) 让当前正在执行的线程暂停mills毫秒,并进入阻塞状态。

线程让步 yield

yield方法与sleep方法有点相似,该方法可以让程序停下来,但是不会阻塞该线程,而是让该线程进入到就绪状态,然后由线程调度器调度。当某个线程调用了yield方法之后,线程调度器又将其调度出来重新继续执行。
当该线程暂停后,只有优先级比当前线程高的线程或者同等级别的才可以获得执行的机会。
因此yield线程是将程序转化为就绪状态,因此也可能立刻获得执行的机会继续执行,比较难控制。

线程优先级

setPriority(int newPriority)
设置线程优先级
getPriority()
获得线程优先级

线程优先级的范围为(1~10)高优先级的线程将会获得更多的执行机会。

线程同步

问题:当有两个进程并发修改同一个文件的时候就有可能造成异常
线程A访问资源D的时候 ,资源够用,但是A还没消耗资源;此时线程B也在请求D,资源显示够用!此时,A已经消耗了资源,已经不够B使用的了。从而出现错误。

同步代码块(监视器)

synchronized(obj){}
同步代码块就像是一个锁,一个线程使用资源的时候,资源上锁,只有该线程使用结束后,其他线程才能访问。
注意:监视器的作用是阻止两个线程对同一个资源进行并发访问,因此,通常可能被并发访问的资源当作同步监视器。

同步方法

使用同步方法可以非常便捷的实现线程安全的类,线程安全的类具有如下特征:

  • 该类的对象可以被多个线程安全的访问
  • 每个线程调用该对象的任意方法后都能得到正确的结果
  • 每个线程调用该类对象任意方法之后,该对象状态依然保持合理状态
    上面的监视器在程序执行完成后会自动释放

    同步锁

    Java5 开始设计了更为强大的线程同步机制–显示的定义同步锁对象来实现同步。
    每次只能有一个对象对Lock加锁,线程开始访问资源之前需要先获得Lock对象

ReentrantLock(可重用锁) 通常可以显示地加锁、释放锁
使用的代码格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class X
{
//定义锁对象
private final ReentrantLock lock = new ReentrantLock();
public void m ()
{
lock.lock();
try{
//需要保证线程安全的代码
}finally
{
lock.unlock();
}
}
}

死锁

线程之间互相等待对方释放锁

线程通信

线程在程序中执行的时候,通常是透明的,程序通常很难控制线程的轮换执行,我们可以通过一些机制来保证线程协调运行。

使用传统的线程通信

传统的线程通信是指使用Object类的wait(),notify()和notifyAll()三个方法。这三个方法必须使用同步监视器来调用,也就是使用synchronized的锁对象。

  • wait()
    wait()方法是当前线程等待
  • notify()
    唤醒等待的线程,如果有多个线程,则随机的唤醒一个
  • notifyAll()
    唤醒所有等待的线程

下面是使用实例:目的是保证取钱必须在存钱之后才能进行执行。
银行账户

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
62
63
64
public class Account {
private String accountNo;
private double balance;
//表识账户中是否还有存款的标志
private boolean flag = false;

public Account() {
}
//构造器
public Account(String accountNo, double balance) {
super();
this.accountNo = accountNo;
this.balance = balance;
}

public double getBalabce() {
return this.balance;
}
//设置取钱操作
public synchronized void draw(double drawA) {
try {
if(!flag) {
wait();
}else {
if(balance>=drawA) {
balance -= drawA;
//执行取钱操作
System.out.println(Thread.currentThread().getName()+"取钱"+drawA);
flag = false;
//唤醒其他的线程
notifyAll();
}else {
flag = false;
//唤醒其他的线程
notifyAll();
System.out.println("余额不足");
}

}

} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

//设置存钱的操作
public synchronized void deposite(double desm) {
try {
if(flag) {
wait();
}else {
System.out.println(Thread.currentThread().getName()+"存钱"+desm);
balance+=desm;
flag = true;
notifyAll();
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

}

取钱线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class DrawThread extends Thread{
private Account aco;
private double drawacound;

public DrawThread(String name,Account aco, double drawacound) {
super(name);
this.aco = aco;
this.drawacound = drawacound;
}

@Override
public void run() {
// TODO Auto-generated method stub
for(int i=0;i<100;i++) {
aco.draw(drawacound);
}
}
}

存钱线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class DepositeThread extends Thread{
private Account account;
private double maony;
public DepositeThread(String name,Account account, double maony) {
super(name);
this.account = account;
this.maony = maony;
}
@Override
public void run() {
// TODO Auto-generated method stub
for(int i=0;i<100;i++) {
account.deposite(maony);
}
}
}

使用Condition来控制线程的执行

如果没有使用synchionized来控制线程,而是使用lock对象,那么就不存在使用监视器的同步对象了。
java提供了一个Condition类来保持协调,使用Condition对象能够让那些获得lock对象,却无法继续执行的的线程释放Lock对象,也可以唤醒其他处于等待的线程。
Condition对象被绑定在Lock对象上,因此获得Co ndition对象可以通过调用Lock对象的newCondition()即可
这里的区别就是 隐式同步监视器使用的当前类,但是Condition使用的当前的Lock对象

  • await()
    对应隐式同步监视器上的wai t()方法
  • signal()
    对应于隐式同步监视器上的notify()方法
  • signalAll()
    对应于隐式同步监视器上的notifyAll()方法
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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Account {
private final Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
private String accountNo;
private double balance;
//表识账户中是否还有存款的标志
private boolean flag = false;

public Account() {
}
//构造器
public Account(String accountNo, double balance) {
super();
this.accountNo = accountNo;
this.balance = balance;
}

public double getBalabce() {
return this.balance;
}
//设置取钱操作
public void draw(double drawA) {
try {
lock.lock();
if(!flag) {
condition.await();
}else {
if(balance>=drawA) {
balance -= drawA;
//执行取钱操作
System.out.println(Thread.currentThread().getName()+"取钱"+drawA);
flag = false;
//唤醒其他的线程
condition.signalAll();
}else {
flag = false;
//唤醒其他的线程
condition.signalAll();
System.out.println("余额不足");
}

}

} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally {
lock.unlock();
}
}

//设置存钱的操作
public void deposite(double desm) {
try {
lock.lock();
if(flag) {
condition.await();
}else {
System.out.println(Thread.currentThread().getName()+"存钱"+desm);
balance+=desm;
flag = true;
condition.signalAll();
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally {
lock.unlock();
}
}

}

可以看出这部分的逻辑与上面的逻辑完全一样,只不过第一个是隐式的,第二个是显式的

使用阻塞队列控制线程通信

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

队列支持的方法

  • 队列尾部插入元素 add(),offer(),put()
  • 队列头部删除并返回元素 remove(),poll(),take()
  • 队列头部取出元素但不删除元素element()和peek()

五个实现类

  • ArrayBlockingQueue:基于数组的队列
  • LinkedBlockingQueue:基于链表的队列
  • PriorityBlockingQueue:并不是标准的队列,不是取出队列中存在时间最长的元素,而是队列中值最小的元素
  • SynchronousQueue:同步队列。该队列的存取操作必须交替进行
  • DelayQueue 要求集合元素都必须实现delay接口
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
import java.util.concurrent.BlockingQueue;

public class DrawThread extends Thread{
//消费者线程
private BlockingQueue<String> bq;

public DrawThread(BlockingQueue<String> bq) {
super();
this.bq = bq;
}
@Override
public void run() {
// TODO Auto-generated method stub
while(true) {
System.out.println(getName()+"消费者准备消费集合元素");
try {
Thread.sleep(200);
bq.take();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
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
25
26
27
import java.util.concurrent.BlockingQueue;

public class DepositeThread extends Thread{
//生产者线程
private BlockingQueue<String> bq;

public DepositeThread(BlockingQueue<String> bq) {
super();
this.bq = bq;
}
@Override
public void run() {
// TODO Auto-generated method stub
String [] strArr = new String[] {"java","python","spring"};
for(int i=0;i<99999;i++) {
System.out.println(getName()+"生产者准备生产集合元素!");
try {
Thread.sleep(200);
bq.put(strArr[i%3]);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(getName()+"生产完成"+bq);
}
}
}
1
2
3
4
5
6
public static void main(String[] args) {
BlockingQueue<String> bq = new ArrayBlockingQueue<String>(1);
new DrawThread(bq).start();
new DrawThread(bq).start();
new DepositeThread(bq).start();
}

线程组和未处理异常

Java使用ThreadGroup来表示线程,它可以对一批线程进行分类管理,Ja va允许程序直接对线程组进行控制。对线程组的控制相当于同时控制这一批线程。
默认情况下,子线程和创建它的父线程属于同一个线程组。
一旦线程加入了指定的组后,该线程将一直属于该线程组,直至线程死亡,线程运行中,不能改变线程的运行组。

Thread类提供了构造器来设置线程属于哪一个线程组

  • Thread(ThreadGrounp group,Runable target):以target的run()方法作为线程执行体创建新的线程,属于group组。
  • Thread(ThreadGrounp group,Runable target,String name):线程名为name
  • Thread(ThreadGrounp group,String name) 创建新的线程,新线程的名字为name,属于group组

提供了一个getThreadGroup()方法来获得线程属于的线程组。

ThreadGroup类提供了两个简单的构造器来创建实例

  • ThreadGroup(String name) :以指定的线程组的名字来创建新的线程
  • ThreadGroup(ThreadGroup parent,String name):以指定的名字,指定的父线程组创建一个新的线程组。
    线程组的名字可以通过getName()方法来获取

ThreadGroup类提供了如下几个方法来操作整个线程组里的所有线程

  • int activeCount():返回此线程组中活动的线程的数目
  • interrupt():中断此线程组中所有的线程
  • isDaemon():判断该线程是否是后台线程组
  • setDaemon(boolean daemon):把该线程组设置成后台线程组。
  • setMaxPriority(int pri):设置线程组的最高优先级

一个有用的异常处理方法
(这块用到的时候 在看吧!!!想到这里似乎也该复习复习异常处理这块嘞)

  • void uncaughtExeption(Thread t,Throwable e):该方法可以处理该线程组内的任意线程所抛出的未处理异常

线程池

对于池的概念,应该都不是很陌生了。通俗的理解就是一个大的容器,线程池无疑就是装线程的容器。那么为什么需要线程池的存在呢?
程序创建一个新的线程本身就是一个成本比较高的操作,涉及与操作系统的交互。在这种情况下,使用线程池能够很好的提升性能,尤其是程序中需要创建大量的生存期很短的线程的时候,线程池启动的时候会初始化一批Runable或者Callable的对象,这类对象执行结束后也不会死亡,而是转为空闲状态。(线程池可以有效的控制 系统中并发线程的数量,当系统中包含大量的线程的时候,会导致系统性能的下降,线程池可以有效的控制系统中并发线程的数量)
下面是主要的线程池的创建方法:

返回ExecutorService

  • newCachedThreadPool(int nThreads):创建一个可重用的,具有固定线程数量的线程池。
  • newSingleThreadPool(int nThreads):创建一个只有单线程的线程池
  • new CachedThreadPool():创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会被缓存在线程池中

    返回一个ScheduledExecutorService

  • newScheduledThreadPool(int corePoolSize) 创建具有指定线程数的线程池,它可以指定延迟后执行线程任务
  • newSingleThreadScheduledExecutor():创建只有一个线程的线程池,它可以在指定延迟后执行线程任务。

    操作线程池的方法,使用的时候直接查找开发javaAPI就可以了!

    线程池的关闭

    当用完一个线程池后,要对线程池进行关闭

  • shutdown()
    调用这个方法,会让线程池不在接收新的任务,但是会将以前的任务执行完毕
  • shutdownNow()
    这个方法暂停所有的正在执行的任务,暂停正在等待的任务,并返回等待执行的任务列表

java7新增加的ForkJoinPool

为了充分利用计算机硬件多CPU或者多核的优势,软件与硬件想使用。ForkJoinPool支持将一个任务拆分成多个“小任务”并行计算,再把多个“小任务”的结果合并成总的计算结果。

常用的构造器

  • ForkJoinPool(int parallelism):创建一个包含p个并行线程的ForkjoinPool
  • ForkJoinPool():以Runtime.availableProcessors()方法的返回值作为p参数来创建ForkJoinPool.

调用方法

  • submit(ForkJoinTask task)
  • invoke(ForkJoinTask task)

    ForkJoinTask抽象类

  • RecursiveAction抽象子类:代表没有返回值的任务
  • RecursiveTask 抽象子类:有返回值的任务
    返回值的结果使用Future future 进行接收

使用实例

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
import java.util.concurrent.RecursiveAction;

public class PrintTask extends RecursiveAction {
//每个小任务最多执行的次数
private static final int TH = 50;
private int start;
private int end;
public PrintTask(int start,int end) {
this.start = start;
this.end = end;
}

@Override
protected void compute() {
// TODO Auto-generated method stub
if(end - start <TH) {
for(int i = start;i<end;i++) {
System.out.println(Thread.currentThread().getName()+"i的值为:"+i);
}
}else {
int middle = (start+end)/2;
PrintTask left = new PrintTask(start, middle);
PrintTask right = new PrintTask(middle, end);
//并行执行两个小任务
left.fork();
right.fork();
}
}

}

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;

public class Test {
public static void main(String[] args) throws InterruptedException {
ForkJoinPool pool = new ForkJoinPool();
pool.submit(new PrintTask(0, 300));
//关于此方法
pool.awaitTermination(2, TimeUnit.SECONDS);
pool.shutdown();
}
}

awaitTermination连接
上述代码是将打印0-300的任务,分解成了两个小任务,分解后的任务调用fork(),进行并行执行。

‘ForkJoinPool-1-worker-3i的值为:211
ForkJoinPool-1-worker-3i的值为:212
ForkJoinPool-1-worker-7i的值为:95
ForkJoinPool-1-worker-0i的值为:241’
在我机器上的运行结果如上,最后的显示为0i - 7i ,我的电脑是4核的,单个cpu,不知为啥是7。后面的数字是没有顺序的,因为,分解后的程序是并行执行的。

线程相关类

ThreadLocal

在共享资源处定一个ThreadLocal类就相当于为每一个线程都创建了一个线程的局部变量,每一个线程都可以独享自己的副本。

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
public class Account {
private ThreadLocal<String> local = new ThreadLocal<String>();
public Account(String name) {
this.local.set(name);
System.out.println("+++++"+local.get());
}
public String getLocal() {
return local.get();
}
public void setLocal(String name) {
this.local.set(name);;
}
}

public class PrintTask extends Thread {
private Account account;
public PrintTask(String name,Account ac) {
super(name);
this.account = ac;
}
@Override
public void run() {
// TODO Auto-generated method stub
for(int i= 0;i<100;i++) {
if(i==20) {
account.setLocal(currentThread().getName());
}
System.out.println(account.getLocal()+"账户的i值"+i);
}
}
}

public class Test {
public static void main(String[] args) throws InterruptedException {
//启动两个线程 两个线程共享一个account
Account acc = new Account("初始名");
PrintTask pT = new PrintTask("线程A", acc);
PrintTask pt2 = new PrintTask("线程B", acc);
pT.start();
pt2.start();
}
}

可以看到刚开始的第一行的输出初始名是因为初始化Ac count类的时候,执行的。但是后面在获取ThreadLocal名的时候就是null值了,直到i的值执行到20的时候,两个线程独自的将自己的ThreadLocal名修改成了自己的名字。

1
2
3
4
5
6
7
8
+++++初始名
null账户的i值0
null账户的i值0
null账户的i值1
...
线程B账户的i值23
null账户的i值19
线程A账户的i值20

包装线程不安全的类

我们所使用的LinkedList,ArrayList,HashSet,TreeSet,HashMap,TreeMap等都是线程不安全的。因此就需要把这些集合封装成线程安全的。
Collections提供的静态方法把这些集合包装成线程安全的集合,并返回
具体的方法就不详细介绍了。用到在说大体结构是:
static Oject synchronizedObject(Object obj)

线程安全的集合类

从java5开始,java.util.concurrent包下提供了大量的支持高并发访问的集合接口和实现类。

  • 以Concurrent开头的集合类:更实用于写操作
  • 以CopyOnWrite开头的集合类:更实用于读操作