第16章:多线程

16.1 线程概述

在这里插入图片描述

16.1.1 线程与进程
  1. 进程特点:
    1. 独立性:每个进程拥有自己私有地址,没有进程本身允许,其他进程无法访问该进程地址空间
    2. 动态性:程序为静态指令集合,程序运行起来变成进程
    3. 并发性:同一时刻只有一个进程执行,但多个进程快速轮换执行时,造成多个进程同时执行的效果
    4. 并行性与并发性区别:并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。同一时刻多个处理器处理多个进程在叫并行,一个cpu同一时间间隔处理多个线程叫并发,有几个cpu就可以同一时间并行执行几个进程
  2. 线程特点:
    1. 多线程扩展了多进程的概念,线程也被称作轻量级进程,进程初始化后,主线程就被创建,线程拥有自己的栈堆,自己的程序计数器和自己的局部变量,但不拥有系统资源,他与该进程的其他线程共享该进程的全部资源(共享变量及部分环境)
    2. 线程也是独立的,它不知道进程中是否有其他线程存在,线程是抢占式的,即当前运行线程在任何时候都可能被挂起,以便另一个线程运行,一个线程可以创建、撤销另一个线程,多个线程之间并发执行
16.1.2 多线程的优势
  1. 进程不可以共享内存,但线程可以
  2. 系统创建进程时需为该进程重新分配系统资源,创建线程代价会小的多,效率高
  3. java内置多线程功能,简化并发编程(如浏览器同时下载多个图片,服务器同时响应多个用户请求)

16.2 线程的创建及启动

16.2.1 继承Thread类创建线程类
package com.wsh.object;
//1. 继承Thread类
public class FirstThread extends Thread{
	private int i;
	//2. 重写run方法,run方法中为线程需完成的具体任务,所以run也称作线程的执行体
	@Override
	public void run(){
		
	}
	public static void main(String[] args) {
	    //3. 创建Thread子类的实例,并调用该对象的start方法来启动该线程
		new FirstThread().start();
		new FirstThread().start();
		//4. 也可以通过匿名内部类直接创建Thread的匿名的子类的对象来启动线程
		/*new Thread(){
		    public void run(){
		        
		    }
		}.start();*/
	}
}

//1.上面程序启动了三个线程,一个主线程,两个自己创建的子线程,主线程执行体为main方法的方法体,子线程的执行体为自身的run方法,这三个线程的执行体是交替运行的,不是一个运行完再运行另一个(主线程先运行一半,再运行几步子线程1,再运行几步子线程2,知道所有线程执行结束)
//2.Thread的方法
static Thread currentThread():返回当前正在执行的线程对象
getName():返回调用该方法的线程对象的名字,默认主线程名为"main",用户启动的多个子线程名依次为"Thread-0""Thread-1"
setName(String name):为线程对象设置名字,也可在创建线程时利用Thread(String name)构造器直接为Thread对象设置名字
//3.继承Thread类创建线程时,每创建一个线程对象就需创建一个FirstThread对象,而i是基本类型实例变量,所以这种创建线程的方式无法共享线程类的基本类型实例变量,即一个线程对象i值改变,另一个线程对象的i值不跟着改变
//4.但如果该实例变量是个引用类型,Account(自定义账户类),那么两个线程的该变量可以指向同一处内存,那么其中一个修改其值,另一个中的值也改变
16.2.2 实现Runnable接口创建线程类
package com.wsh.object;
//1. 定义Runnable接口的实现类,并重写该接口的run()方法,此run方法的方法体同样也是线程的执行体
public class SecondThread implements Runnable {
	private int i;
	@Override
	public void run() {

	}
	public static void main(String[] args) {
	    //2. 创建Runnable实现类的实例
		SecondThread st = new SecondThread();
		//3. 将此实例作为Thread构造器中的Runnable参数(target)来创建Thread对象,该Thread对象才是真正的线程对象,最后调用该线程对象的start方法
		new Thread(st).start();
		new Thread(st, "新线程2").start();
		//4. 使用匿名内部类实现
		/*new Thread(new Runnable() {
			@Override
			public void run() {
			
			}
		}, "新线程3").start();*/
	}
}

//这种方式创建线程类时,每次创建线程对象时传入的都是同一个线程类的对象,所以多个线程对象共享同一个线程类对象,即共享同一个线程类对象的成员变量i,所以当一个线程对象的线程类对象的成员变量i改变时,另一个的i也改变
16.2.3 使用Callable和Future创建线程
package com.wsh.object;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
//1. 实现Callable接口
public class ThirdThread implements Callable{
	//2. 重写call方法
	@Override
	public Integer call() throws Exception {
		return 0;
	}
	public static void main(String[] args) throws InterruptedException, ExecutionException {
		ThirdThread tt = new ThirdThread();
		FutureTask<Integer> task = new FutureTask<>(tt);
		new Thread(task,"有返回值的线程").start();
		//3. 使用匿名内部类实现
		/*new Thread(new FutureTask<Integer>(new Callable<Integer>() {
			@Override
			public Integer call() throws Exception {
				// TODO Auto-generated method stub
				return null;
			}
		}),"的").start();*/
		System.out.println(task.get());
	}
}

//1.使用Future和Callable创建线程会将call方法作为线程的执行体,且call方法可以有返回值,也可以声明抛出异常
//2.FutureTask的父接口Future中的方法
boolean cancel(boolean mayInterruptIfRunning):试图取消Future中关联的Callable任务,不会杀死线程
V get():返回Callable任务中call方法的返回值,调用该方法造成程序阻塞,即必须等子线程结束后才会获得返回值
V get(long timeout,TimeUnit unit):该方法最多阻塞timeout和unit指定的时间,如果经过指定的时间后Callable仍没有返回值,抛出TimeoutException
boolean isCancelled():如果Callable任务正常完成前取消其关联的Callable任务,则返回true
boolean isDone():如果Callable任务正常完成,返回true
16.2.4 创建线程的三种方式的对比
  1. 通过实现Runnable接口或Callable接口创建线基本相同,因为实际上FutureTask是Runnable的子类型,因此可归结成一种方式
  2. 采用Runnable、Callable接口创建线程的优缺点
    1. 优点:线程只实现Runnable和Callable接口,还可以继承其他类。多个线程能够共享一个target对象,即共享tartget对象的成员变量,适合多个相同线程处理同一份资源
    2. 缺点:编程复杂,想访问当前线程必须使用Thread.currentThread()方法,而对于继承Thread类来创建线程,直接使用this就可获取当前线程
  3. 一般都使用实现Runnable,Callable来创建线程

16.3 线程的生命周期

在这里插入图片描述

16.3.1 新建和就绪状态
  1. 新建:Thread th = new Thread();
  2. 就绪:th.start();
  3. 非新建状态的线程调用start方法引发IllegalThreadStateException
16.3.2 运行和阻塞状态

阻塞状态的进程只能变成就绪状态,不能直接变成运行状态

16.3.3 线程死亡

用线程对象的isAlive方法判断线程是否已经死亡,就绪、运行、阻塞状态下都会返回true,新建和死亡状态返回false,线程死亡后不会重新变成新建状态

16.4 控制线程

16.4.1 join线程
  1. 该方法允许线程A阻塞到另一个线程B执行完成
  2. 当一个线程a中调用了另一个线程b的join方法,那么a线程会被阻塞,直到b线程执行结束
  3. 使用join可以将大问题划分为很多小问题,每个小问题分配一个线程,当所有小问题得到处理后,再调用主线程进行下一步操作
public static void main(String[] args) throws InterruptedException {
    //主线程在执行jt.join后开始阻塞,直到ft线程执行完毕,即ft.join之前,主线程与ft线程会并行执行    
	FirstThread ft = new FirstThread();
		ft.start();
		ft.join();
	}
  1. join的三种重载形式
join():
join(long millis):等待millis毫秒,如果线程还没执行完成,跳过等待
join(long millis,int nanos):等待millis秒+nanos微秒,一般不用,因为系统无法精确到微秒
16.4.2 后台线程:也称守护线程、精灵线程
  1. jvm垃圾回收线程就是典型的后台线程
  2. 所有前台线程死亡后,后台线程自动死亡,虚拟机退出,后台线程收到jvm死亡通知后一定时间后死亡(不是立即死亡)
  3. Thread对象的setDaemon(true),可以将指定线程设定为后台线程,必须在start方法前进行调用,否则引发IllegalThreadStateException,isDemo方法可以判断是否为后台线程
16.4.3 线程睡眠:sleep方法
  1. sleep可以使当前线程进入阻塞状态
  2. 该方法为static方法,因此不能在一个线程中暂停另一个线程
static void sleep(long millis)static void sleep(long millis,int nanos):一般不用
16.4.4 线程让步:yield方法
  1. 该方法用于将运行中线程转为就绪状态
  2. yield方法为static方法
  3. 当某线程中调用了yield方法进入就绪状态后,只有优先级与当前线程相同,或优先级更高的线程才会获得机会,所以很有可能该线程马上又进入了运行状态
  4. sleep与yield区别
    1. sleep方法不会理会其他线程优先级
    2. sleep将线程转为阻塞状态,yield转为就绪状态
    3. sleep抛出InterruptException
    4. sleep方法移植性更好,通常不建议用yield控制并发线程的执行
16.4.5 改变线程的优先级
  1. 线程默认优先级与创建它的父线程优先级相同,默认情况main方法拥有普通优先级(值为5)
  2. setPriority(int newPriority):用来设定线程的优先级,newPriority为1到10之间的整数,也可以使用Thread的如下三个静态常量
Thread.MIN_PRIORITY:值为1
Thread.NORM_PRIORITY:值为5
Thread.MAX_PRIORITY:值为10
  1. 不同操作系统的优先级不同,不能很好的对应java中的10个优先级,所以为了程序具有很好的移植性,尽量使用Thread静态常量设定优先级

16.5 线程同步

  1. 在java虚拟机中,每个Object对象,都有对应的监视器Monitor与其关联
  2. 每个Object对象,又存在一个标志位(可以当做锁用),值为0或1
  3. 线程A运行到同步块时,监视器会检查对象C的标志位,如果为0,表明此同步块中存在其他线程B在运行,A将一直处于就绪状态,等到B线程执行完同步块中的代码,C对象的标志位被重置为1,此时线程A才可以将Object对象的标志位设置为0,并执行同步块中的代码,从而防止其他线程执行同步块中的代码
16.5.1 线程安全问题
//若两个线程同时访问这段代码,a线程进入,判断余额大于取的钱,进入if逻辑,但马上暂停1毫秒,此时线程b被调用,由于a此时还没有对余额进行更新,b也可以进入if代码块,最后导致account虽然只有1000,但是取出了两个800
if(account.getBalance()>=drawAmount){
    Thread.sleep();
    account.setBalance(account.getBalance()-drawAmount);
}
16.5.2 同步代码块
  1. 同步代码块与同步方法属于隐式锁:因为代码中不需要显式加锁、解锁
  2. 通常使用可能被并发访问的共享资源充当同步监视器
//account为同步监视器,线程会获取同步监视器的锁,对于同一同步监视器,同一时间只有一个线程获取它的锁
synchronized(account){
    if(account.getBalance()>=drawAmount){
        Thread.sleep();
        account.setBalance(account.getBalance()-drawAmount);
    }    
}
  1. 理解:这种方式类似一个房子(account)的门锁(account对象的标志位)开着,第一个人(线程)进入房子后锁门,此时其他人被拦在外面,无法进入,必须等第一个人出来(释放锁),此时门锁再次打开,其他人才能进入,当有人在这个房子内,是不影响其他人进入其他房子(另一个account对象)的进出。
  2. 解决线程安全问题应该符合如下逻辑,即同步代码块、同步方法、同步锁都满足该逻辑
加锁--修改--释放锁
16.5.3 同步方法
//实际上是调用该方法的对象(this)所关联的监视器,监视该对象的标志位
public synchronized void draw(double drawAmount){
    
}
  1. 不可变类总是线程安全的,因为其内成员变量根本无法修改,可变类如果想线程安全,只需将修改其对象的成员变量的方法(因为成员变量已经private修饰)改为同步方法即可,例如本例中Account类为可变类,只需将修改balance成员变量的方法draw改为同步方法即可
  2. 不要对可变类的所有方法都同步,会极大的影响效率,只对那些会改变共享资源的方法进行同步,如Account类中String类型的accountNo实例变量就无需同步,因为压根不会在进程中去修改他,如果可变类有两种运行环境,单线程与多线程,那么应该为该可变类提供两种版本,例如jdk中的StringBuilder(线程不安全版本,适用单线程)和StringBuffer(线程安全版本,适用多线程)
16.5.4 同步监视器锁
  1. 同步监视器释放锁的情况
    1. 当前线程同步方法、同步代码块执行结束(包括正常结束、出现break、return结束、出现未处理的Error或Exception结束)
    2. 执行同步监视器对象的wait方法(不是线程对象的wait),则当前线程对象暂停并释放同步监视器的锁
  2. 不释放锁的情况
    1. Thread.sleep()或Thread.yield()
    2. suspend方法。尽量不用suspend和resume方法控制线程,容易导致死锁
16.5.5 同步锁
  1. 同步锁为显式锁,Lock对象代表锁
  2. java5提供Lock和ReadWriteLock两个根接口,其实现类分别为ReentrantLock(可重入锁)和ReentrantReadWriteLock,在实现线程安全的控制中,一般使用ReentrantLock
class X{
    //同步锁这种方式,监视器监视其关联的Object对象中的成员变量Lock对象,对于每个X对象,只持有唯一一个lock对象,所以对于唯一一个X对象(该对象调用method),只有一个线程可以获取该锁
    private final ReentrantLock lock = new ReentrantLock();
    public void method() {
        lock.lock();
        try{
            //需要保证线程安全的代码,例如取钱
        }
        finally{
            //一般使用finally块来确保锁一定被释放
            lock.unclock();
        }
    }
}
  1. 同步代码块、同步方法、同步锁的区别
    1. 同步方法、同步代码块编程方便,可以避免很多涉及锁的常见编程错误
    2. 同步锁提供了其他二者所没有的功能,编程灵活
//如果有线程已获取该锁,则返回false,防止单笔业务重复提交(如定时任务)
tryLock()
//如果已有线程获取该锁的方法,等待timeout个TimeUnit,如果之后当前线程仍无法获取该锁,则返回false,可以防止由于处理不当导致的阻塞(大家都在等待该锁,导致线程队列溢出),
tryLock(long timeout, TimeUnit unit)
//该方法也是加锁,但如果调用线程的interrupt方法,该方法会立即释放线程获取的锁,并抛出InterruptedException(防止不正常操作长时间占用锁)
lockInterruptibly()
  1. 可重入锁
public void A(){
    lock.lock();
    //一段被锁保护的代码中可以调用另一个被相同锁保护的方法,该锁就叫可重入锁,不可重入锁不能调用被相同锁保护的方法,因为会发生死锁,synchronized也是可重入锁
    B();
    lock.unlock();
}
public void B(){
    lock.lock();
    lock.unlock();
}
16.5.6 死锁
  1. 死锁与锁等待(阻塞)不是一个概念
  2. 两个线程相互等待对方释放锁时就会产生死锁,一旦死锁,程序既不会异常也不会给出提示,所有线程处于阻塞状态,无法继续
class A{
    public synchronized void foo(B b){
        Thread.sleep(200);
        b.last();
    }
    public synchronized void last(){
        
    }
}
class B{
    public synchronized void bar(A a){
        Thread.sleep(200);
        a.last();
    }
    public synchronized void last(){
        
    }
}
public static void main(String[] args) throws InterruptedException {
		A a = new A();
		B b = new B();
		//同时启动两个线程,分别执行a.foo(b)和b.bar(a),a.foo(b)先对a进行加锁,然后等待0.2s,然后b.bar(a)开始执行,对b加锁,并等待0.2s,之后a.foo(b)中的b.last()开始执行,想获取b的锁,但b已被加锁,只能等待,同理b.bar(a)中a.last()想对a加锁,却发现a已被加锁,此时两个线程互相等待对方释放锁,造成死锁
		new Thread(){
			@Override
			public void run(){
				try {
				    a.foo(b);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		}.start();
		new Thread(){
			@Override
			public void run(){
				try {
					b.bar(a);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		}.start();
	}

16.6 线程通信

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

16.6.1 传统的线程通信
  1. 同步监视器对象(Object类)的方法:wait、notify、notifyAll
//导致当前线程等待,并释放当前线程对该同步监视器的锁,直到其他线程调用该同步监视器的notify或notifyAll方法唤醒该线程
wait()wait(long a)
//唤醒等待此同步监视器的锁的单个线程,如果有多个线程等待,任意唤醒其中一个,一个线程只有被wait后才可以被notify
notify()
//唤醒所有等待此同步监视器的锁的线程
notifyAll()
//notify、notifyAll区别:对于notify方法,当多个线程在等待时,其中一个线程A被唤醒后,如果A中不再调用notify方法,则其他等待的线程永远不会被唤醒,对于notifyAll:所有线程都被唤醒,其中一个获得同步监视器的锁,该线程执行完毕后,其他线程不用再被唤醒,直接随机获取锁并执行。一个线程只有被唤醒才有可能重新获取锁,没有被唤醒就一定无法获取锁,wait后的线程在等待被唤醒而不是在等待锁,notifyAll后所有等待被唤醒的线程开始都变为等待锁
16.6.2 使用Condition来控制线程通信
  1. 当直接使用Lock对象保证线程同步时,系统中不存在隐式的同步监视器对象,只有锁本身,所以无法使用wait、notify、notifyAll等方法来进行线程通信,于是java提供了Condition类来代替同步监视器的功能控制线程通信
  2. private final Condition cond = lock.newCondition();
  3. Condition的方法
await()
signal()
signalAll()
16.6.3 使用阻塞队列(BlockingQueue)控制线程通信
  1. 虽然BlockingQueue为Queue的子接口,但其主要功能不是作为容器,而是作为线程同步工具,其特点为当线程试图向其中放入元素时,如果该队列已满,则该线程被阻塞,当试图从中取出元素时,如果该队列为空,该线程也被阻塞
  2. BlockingQueue提供的支持阻塞的方法
void put(E e)
E take()
  1. BlockingQueue也可以使用Queue的方法来插入取出元素,但不会被阻塞
  2. BlockingQueue常用实现类:ArrayBlockingQueue、LinkedBlockingQueue

16.7 线程组与未处理的异常

16.7.1 java使用ThreadGroup表示线程组

线程组可以对一批线程分类管理,用户创建的所有线程都属于指定的线程组,如果没有显式指定线程组,则该线程属于默认线程组,默认情况下子线程和创建他们的父线程处于同一个线程组。一个线程加入某线程组后一直属于该线程组,直到该线程死亡。线程运行中途不能改变其所属的线程组

16.7.2 指定线程组下创建线程
Thread(ThreadGroup group,Runnable target)
Thread(ThreadGroup group,Runnable target,String name)
Thread(ThreadGroup group,String name)
16.7.3 线程获取其所在线程组
//由于线程不能改变其所属线程组,所以没有setThreadGroup方法
ThreadGroup getThreadGroup()
16.7.4 ThreadGroup构造器
ThreadGroup(String name)
//以name为名,parent为父线程组来创建线程组
ThreadGroup(ThreadGroup parent,String name)
16.7.5 ThreadGroup的方法
//获取线程组的名,不允许修改
getName()
//返回线程组中活动线程的数目
int activeCount()
//中断该线程组中所有线程
interrupt()
判断是否为后台线程组
isDaemon()
//设置为后台线程组,后台线程组中最后一个线程结束后或被销毁后,后台线程组自动销毁
setDaemon()
//设置线程组中线程的最高优先级
setMaxPriority(int pri)
16.7.6 jvm对run方法中未处理的异常的处理方式
  1. 异常处理器为Thread.UncaughtExceptionHandler,其使用uncaughtException方法对异常处理
//为线程对象设置异常处理器
setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)
//为线程类的所有线程对象设置默认的异常处理器
static setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)
  1. 实际上ThreadGroup实现了异常处理器接口,即其内也有处理异常的uncaughtException方法,该方法可以被重写
public class ThreadGroupTest extends ThreadGroup{
    @Override
    public void unCaughtException(Thread t,Throwable e){
        
    }
}
  1. 处理流程
    1. jvm首先使用当前线程对象设置的异常处理器(setUncaughtExceptionHandler)处理异常
    2. 如果未设置,调用其所在线程组对象的uncaughtException方法处理异常
  2. 线程组默认的uncaughtException方法处理流程如下
    1. 如果该线程有父线程组,调用其父线程组的uncaughtException方法处理异常
    2. 如果没有,查找当前线程类设置的异常处理器(setDefaultUncaughtExceptionHandler),调用其uncaughtException方法
    3. 如果线程对象、线程类、父线程组,都没设置异常处理器对象,那么,如果该异常对象为ThreadDeath的对象,不做任何处理,否则,打印跟踪栈信息,结束进程
  3. 即使异常处理器处理了run中的异常,线程中异常后的代码也不会正常执行,与try…catch处理异常的方式不同

16.8 线程池

线程池在系统启动时即刻创建大量空闲线程,程序将一个Runnable或Callable对象传给线程池,线程池会启动一个线程执行他们的call或run方法,当run、call方法结束后,线程不会立即死亡,而是再次返回线程池中成为空闲状态,等待下一个run或call方法执行。除此之外,使用线程池可以利用其最大线程参数,有效控制并发线程数,因为系统中如果包含大量并发线程时,会导致系统性能急剧下降,甚至导致jvm崩溃

  1. Executors:创建线程池的工厂类
//创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会缓存在线程池中
ExecutorService newCachedThreadPool()
//创建一个可重用的具有固定线程数的线程池
ExecutorService newFixedThreadPool(int nThreads)
//相当于cnewFixedThreadPool(1);
ExecutorService newSingleThreadExecutor()
//创建具有指定线程数的线程池,它可以在指定延迟后执行线程任务,corePoolSize为线程池中保存的线程数,即使线程空闲,也被保存在线程池中
ScheduledExecutorService newScheduledThreadPool(int ccorePoolSize)
//相当于newScheduledThreadPool(1)
ScheduledExecutorService newSingleThreadScheduledExecutor()
  1. ExecutorService:代表只要线程池中有空闲的线程,就立即执行线程任务执行线程的线程池
//将Runnable对象交给指定线程池,线程池在空闲时执行Runnable对象代表的任务,其中Future对象代表Runnable任务的返回值,由于run方法没有返回值,所以Future对象在run方法结束后返回null。但可以调用Future的isDone()、isCancelled方法来获得Runnable对象的执行状态
Future<?> submit(Runnable task)
//系统内部会将Runnable对象转为一个Callable对象,result就是Callable中call方法返回值
<T>Future<T> submit(Runnable task,T result)
//Future抽象为Callable对象里call方法返回,其get()方法可以得到call方法真正返回值
<T>Future<T> submit(Callable<T> task)
  1. ScheduledExecutorService:代表可在指定延迟后周期性地执行线程任务的线程池,其提供了四个方法
//指定callable任务在delay个unit延迟后开始执行
ScheduledFuture<V> schedule(Callable<V> callable,long delay,TimeUnit unit)
ScheduledFuture<?> schedule(Runnable command,long delay,TimeUnit unit)
//command在initialDelay后开始执行,以后每隔period后重复执行
ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit)
//command任务在initialDelay后开始执行,以后每一次执行结束和下一次执行开始之间都有dealy延迟,如果任务在一次执行时遇到异常,会取消后续执行,否则只能通过程序显式取消或终止该任务
scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit)
  1. 用完一个线程池后,应调用线程池的shutdown方法,此后线程池不再接收新任务,但会将以前所有已提交的任务执行完成,当线程池中所有任务执行完成后,池中所有线程死亡,shutdownNow:试图停止所有正在执行的活动任务,暂停处理正在等待的线程任务,并返回等待执行的Runnable任务集合
ExecutorService pool = Executors.newFixedThreadPool(6);
Runnable target = new Runnable(){
    @Override
	public void run() {
	// TODO Auto-generated method stub
	}
};
pool.submit(target);
pool.submit(target);
pool.shutdown();

16.9 线程相关类

16.9.1 ThreadLocal类
  1. 简化多线程编程时的并发访问,简便地隔离多线程程序的竞争资源。即可以在线程类中添加类型为ThreadLocal的成员变量,存入该成员变量内的变量对于不同线程对象是完全隔离的
  2. 我们系统中FCRThreadAttribute用的就是这种方式,但当时出现问题为,某个变量被放入类型为ThreadLocal的成员变量内后,没有清理,而实际上线程处理完毕后,并没有销毁,而是放回线程池,导致再拿出该线程时,原存放的变量值没有被正确清理
  3. ThreadLocal的方法
//向ThreadLocal的对象中存放值,以当前线程对象作为key,value作为value,存入ThreadLocal.ThreadLocalMap中,从而保证不同的线程对象会对应不同的value
void set(T value)
//返回ThreadLocal.ThreadLocalMap中key值为当前线程对象的value值
T get()
//删除ThreadLocal.ThreadLocalMap中以当前线程对象为key的键值对
void remove()
  1. ThreadLocal对象在线程中必须被set后才能get到值,否则get返回null
  2. 若多个线程之间共享资源,以达到线程之间的通信功能,就使用同步机制,如果仅需要隔离多个线程之间的共享冲突,则使用ThreadLocal
16.9.2 包装线程不安全的集合
16.9.3 线程安全的集合类
  1. Java5开始java.util.concurrent包下提供了大量支持高效并发访问的集合接口和实现类,主要分为以下两类
    1. Concurrent开头:适合并发写入
      1. ConcurrentLinkedQueue:访问快、无需等待、不允许使用null
      2. ConcurrentHashMap:支持16个线程并发写入,超过16个进程访问时,一些线程等待,可以设置concurrencyLevel构造参数支持更多线程并发写入
      3. 以上两种集合的迭代器无法反映创建迭代器后的修改,程序也不会抛异常
    2. CopyOnWrite开头:适合并发读取
      1. CopyOnWriteArraySet:底层封装了CopyOnWriteArrayList,实现机制与其类似
      2. CopyOnWriteArrayList:读取时访问集合本身,无需加锁与阻塞,当写入集合时,集合会在底层复制一个新数组,接下来对新数组进行写入,由于写入时需要频繁复制数组,性能差,但读取快,因此该集合适用于读取远大于写入的场景,例如缓存
发布了32 篇原创文章 · 获赞 0 · 访问量 942

猜你喜欢

转载自blog.csdn.net/hanzong110/article/details/102565232