一个任务一般就是一个程序,每一个运行中的程序就是一个进程。当一个程序运行时,内部可能包含了多个顺序执行流,每一个顺序执行流就是一个线程。
java
当一个程序进入内存运行时,即变成一个进程。进程是处于运行过程当中的程序,而且具备必定的独立功能,进程是系统进行资源分配和调度的一个独立单位。面试
独立性:是系统独立存在的实体,拥有本身独立的资源,有本身私有的地址空间。在没有通过进程自己容许的状况下,一个用户的进程不能够直接访问其余进程的地址空间。算法
动态性:进程与程序的区别在于:程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集和,进程中加入了时间的概念。进程具备本身的生命周期和不一样的状态,这些都是程序不具有的。数据库
并发性:多个进程能够在单个处理器上并发执行,多个进程之间不会相互影响。编程
并行:指在同一时刻,有多条指令在多个处理上同时执行。(多核同时工做)数组
并发:指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具备多个进程同时执行的效果。(单核在工做,单核不停轮询)缓存
多线程扩展了多进程的概念,使得同一个进程能够同时并发处理多个任务。
线程(Thread)也被成为轻量级的进程,线程是进程执行的单元,线程在程序中是独立的、并发的执行流安全
当进程被初始化后,主线程就被建立了。绝大数应用程序只须要有一个主线程,但也能够在进程内建立多条的线程,每一个线程也是相互独立的。性能优化
一个进程能够拥有多个线程,一个线程必须有一个父进程。多线程
线程能够拥有本身的堆栈、本身的程序计数器和本身的局部变量,但不拥有系统资源,它与父进程的其余线程共享该进程所拥有的所有资源,所以编程更加方便。
线程是独立运行的,它并不知道进程中是否还有其余的线程存在。线程的执行是抢占式的,即:当前运行的线程在任什么时候候都有可能被挂起,以便另一个线程能够运行。
一个线程能够建立和撤销另外一个线程,同一个进程中多个线程之间能够并发执行。
线程的调度和管理由进程自己负责完成。
概括而言:操做系统能够同时执行多个任务,每一个任务就是进程;进程能够同时执行多个任务,每一个任务就是线程
进程之间不能共享内存,但线程之间共享内存很是容易
系统建立进程要为该进程从新分配系统资源,但建立线程的代价则小得多。所以多线程实现多任务并发比多线程的效率高。
Java语言内置了多线程功能支撑,简化了多线程的编程。
步骤:
① 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就表明了线程须要完成的任务,称为线程执行体
② 建立Thread子类的实例,即建立了线程对象
③ 调用线程对象的start()方法来启动该线程
示例:
// 经过继承Thread类来建立线程类
public class FirstThread extends Thread {
private int i ;
// 重写run方法,run方法的方法体就是线程执行体
public void run() {
for ( ; i < 100 ; i++ )
{
// 当线程类继承Thread类时,直接使用this便可获取当前线程
// Thread对象的getName()返回当前该线程的名字
// 所以能够直接调用getName()方法返回当前线程的名
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++)
{
// 调用Thread的currentThread方法获取当前线程
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 20)
{
// 建立、并启动第一条线程
new FirstThread().start();
// 建立、并启动第二条线程
new FirstThread().start();
}
}
}
}
复制代码
① 当Java程序开始运行后,程序至少会建立一个主线程,main()方法的方法体表明主线程的线程执行体
② 当线程类继承Tread类时,直接使用this便可以获取当前线程
③ 继承Thread类建立线程类,多个线程之间没法共享线程类的实例变量
① 定义Runnable接口的实现类,并重写该接口的run()方法
② 建立Runnable实现类的实例,并以此实例做为Thread的target来建立Tread对象,该Tread对象才是真正的线程对象
// 经过实现Runnable接口来建立线程类
public class SecondThread implements Runnable {
private int i ;
// run方法一样是线程执行体
public void run() {
for ( ; i < 100 ; i++ )
{
// 当线程类实现Runnable接口时,
// 若是想获取当前线程,只能用Thread.currentThread()方法。
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++)
{
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 20)
{
SecondThread st = new SecondThread(); // ①
// 经过new Thread(target , name)方法建立新线程
new Thread(st , "新线程1").start();
new Thread(st , "新线程2").start();
}
}
}
}
复制代码
① 实现Runnable接口建立线程类,必须经过Thread.currentThread()方法来得到当前线程对象
② 实现Runnable接口建立线程类,多个线程能够共享线程类的实例变量
Callable接口提供了一个call()方法,call()方法比run()方法更强大:
① call()方法能够由返回值
② call()方法能够声明抛出异常
① 建立Callable接口的实现类,并实现call()方法,该call()方法做为线程执行体,且该call()方法有返回值
② 使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值
③ 调用FutureTask对象的get()方法得到子线程执行结束的返回值
示例:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
//使用Callable接口和Future来建立线程
public class ThreadFuture {
//抛出异常
public static void main(String[] args) throws InterruptedException, ExecutionException {
//建立FutureTask对象,包装 Callable接口实例
FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>)()->{
int sum = 0;
for(int i = 0;i<100;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
sum += i;
}
//注意看这里有返回值
return sum;
});
//使用task做为 Thread类的target 来建立一个线程
Thread instance = new Thread(task);
//启动线程
instance.start();
//sleep一段时间,让上面的线程执行完毕
Thread.sleep(1000);
//这里能够调用task.get() 获取上面的那个线程的返回值
System.out.println("线程返回值:"+task.get());
}
}
复制代码
优势:
①实现的是接口,还能够继承其余类
② 多个线程能够共享同一个target对象,适合多个相同的线程来处理同一份资源的状况
缺点:
① 编程稍微复杂
② 获取当前线程必须用Thread.currentThread()方法来得到
优势:
①编程简单
② 获取当前线程,能够直接使用this来得到
缺点:
① 已经继承了Thread类,不能继承其余类
线程的生命周期要经历新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocke)和死亡(Dead)5种状态。
尤为是当线程启动之后,它不可能一直“霸占”着CPU独自运行,因此CPU须要在多条线程之间切换,因而线程状态也会屡次在运行、阻塞之间切换。
当程序使用new
关键字建立了一个线程以后,该线程就处于新建状态,此时它仅仅由Java虚拟机为其分配内存,而且初始化其成员变量的值。此时的线程对象没有表现出任何线程队动态特征,程序也不会执行线程的线程执行体。
当线程对象调用了start()
方法以后,该线程处于就绪状态,Java虚拟机会为其建立方法调用栈和程序计数器,处于这个状态中的线程并无开始运行,只是表示该线程能够运行了,至于该线程什么时候开始运行,取决于JVM里线程调度器的调度。
tips:
启动线程使用start()
方法,而不是run()
方法,若是调用run()
方法,则run()
方法当即就会被执行,并且在run()
方法返回以前,其余线程没法并发执行,也就是说,若是直接调用线程对象的run()
方法,系统把线程对象当成一个普通对象,而run()
方法也是一个普通方法,而不是线程执行体。
若是直接调用线程对象的run()
方法,则run()
方法里不能直接经过getName()
方法来得到当前执行线程的名字,而是须要使用Thread.currentThread()
方法先得到当前线程,再调用线程对象的getName()
方法来得到线程的名字。启动线程的正确方法是调用Thread
对象的start()
方法,而不是直接调用run()
方法,不然就变成单线程程序了。
调用了线程的run()
方法以后,该线程已经再也不处于新建状态,不要再次调用线程对象的start()
方法。
若是处于就绪状态的线程得到了CPU,开始执行run()
方法的线程执行体,则该线程处于运行状态。
但线程不可能一直处于运行状态,它在运行过程当中会被中断,从而进入一个阻塞的状态
当发生以下状况时,线程将会进入阻塞状态:
一、线程调用sleep()
方法主动放弃所占用的处理器资源。
二、线程调用了一个阻塞式IO方法,在该方法返回以前,该线程被阻塞。
三、线程试图得到一个同步监视器,但该同步监视器正被其余线程所持有。
四、线程在等待某个通知(notify)。
五、程序调用了线程的suspend()
方法将该线程挂起。但这个方法容易致使死锁,因此应该尽可能避免使用该方法。
针对上面几种状况,当发生以下特定的状况时能够解除上面的阻塞,让该线程从新进入就绪状态。
一、调用sleep()
方法的线程通过了指定时间。
二、线程调用的阻塞式IO方法已经返回。
三、 线程成功地得到了试图取得的同步监视器。
四、 线程正在等待某个通知时,其余线程发出了一个通知。
五、处于挂起状态的线程被调用了resume()
恢复方法。
从图中能够看出,线程从阻塞状态只能进入就绪状态,没法直接进入运行状态。
而就绪和运行状态之间的转换一般不受程序控制,而是由系统线程调度所决定。
当处于就绪状态的线程得到处理器资源时,该线程进入运行状态;当处于运行状态的线程失去处理器资源时,该线程进入就绪状态。
但有一个方法例外,调用yield()
方法可让运行状态的线程转入就绪状态。
线程会以以下三种方式结束,结束后就处于死亡状态。
run()
或call()
方法执行完成,线程正常结束。
线程抛出一个未捕获的Exception
或Error
。
直接调用该线程的stop()
方法来结束该线程——该方法容易致使死锁,一般不推荐使用。
tips:
一、当主线程结束时,其余线程不受任何影响,并不会随之结束。一旦子线程启动起来后,它就拥有和主线程相同的地位,它不会受主线程的影响。
二、为了测试某个线程是否已经死亡,能够调用线程对象的isAlive()
方法,当线程处于就绪、运行、阻塞三种状态时,该方法将返回true
;当线程处于新建、死亡两种状态时,该方法将返回false
。
三、不要试图对一个已经死亡的线程调用start()
方法使它从新启动,死亡就是死亡,该线程将不可再次做为线程执行。在线程已经死亡的状况下再次调用start()
方法将会引起IIIegalThreadException
异常。
四、不能对死亡的线程调用start()
方法,程序只能对新建状态的线程调用start()方法,对新建的线程两次调用start()方法也是错误的,会引起IIIegalThreadStateException
异常。
Thread
提供了让一个线程等待另外一个线程完成的方法:join()
方法。当在某个程序执行流中调用其余线程的join()
方法时,调用线程将被阻塞,直到被join()
方法加入的join
线程执行完为止。
join()
方法一般由使用线程的程序调用,以将大问题划分红许多小问题,每一个小问题分配一个线程。当全部的小问题都获得处理后,再调用主线程来进一步操做。
代码示例:
public class JoinThread extends Thread {
// 提供一个有参数的构造器,用于设置该线程的名字
public JoinThread(String name) {
super(name);
}
// 重写run()方法,定义线程执行体
public void run() {
for (int i = 0; i < 100 ; i++ )
{
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args)throws Exception {
// 启动子线程
new JoinThread("新线程").start();
for (int i = 0; i < 100 ; i++ )
{
if (i == 20)
{
JoinThread jt = new JoinThread("被Join的线程");
jt.start();
// main线程调用了jt线程的join()方法,main线程必须等jt执行结束才会向下执行
jt.join();
}
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
复制代码
有一种线程,它是在后台运行的,它的任务是为其余的线程提供服务,这种线程被称为“后台线程(Daemon Thread)”,又称为“守护线程”或“精灵线程”。JVM的垃圾回收线程就是典型的后台线程。
后台线程有个特征:若是全部的前台线程都死亡,后台线程会自动死亡。
调用Thread
对象的setDaemon(true)
方法可将指定线程设置成后台线程。
tips:
一、Thread
类还提供了一个isDaemon()
方法,用于判断指定线程是否为后台线程。
二、前台线程建立的子线程默认是前台线程,后台线程子线程默认是后台线程。
三、前台线程死亡后,JVM会通知后台线程死亡,但从它接收指令到作出响应,须要必定时间。
并且要将某个线程设置为后台线程,必须在该线程启动以前设置,也就是说,setDaemon(true)
必须在start()
方法以前调用,不然会引起llegalThreadStateException
异常。
若是须要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则能够经过调用Thread
类的静态sleep()
方法来实现。
yield()
方法是一个和sleep()
方法有点类似的方法,它也是Thread
类提供的一个静态方法,它也可让当前正在执行的线程暂停,但它不会阻塞该线程,它只是将该线程转入就绪状态。
yield()
只是让当前线程暂停一下,让系统的线程调度器从新调度一次,彻底可能的状况是:当某个线程调用了yield()
方法暂停以后,线程调度器又将其调度出来从新执行。
实际上,当某个线程调用了yield()
方法暂停以后,只有优先级与当前线程相同,或者优先级比当前线程更高的处于就绪状态的线程才会得到执行的机会。
关于sleep()
方法和yield()
方法的区别以下
sleep()
方法暂停当前线程后,会给其余线程执行机会,不会理会其余线程的优先级;但yield()
方法只会给优先级相同,或优先级更高的线程执行机会。
sleep()
方法会将线程转入阻塞状态,直到通过阻塞时间才会转入就绪状态;而yield()
不会将线程转入阻塞状态,它只是强制当前线程进入就绪状态。所以彻底有可能某个线程调用yield()
方法暂停以后,当即再次得到处理器资源被执行。
sleep()
方法声明抛出了InterruptedException
异常,因此调用sleep()
方法时要么捕捉该异常,要么显式声明抛出该异常;而yield()
方法则没有声明抛出任何异常。
sleep()
方法比yield()
方法有更好的可移植性,一般不建议使用yield()
方法来控制并发线程的执行。
经过Thread
类提供的setPriority(int newPriority)
、getPriority()
方法来设置和返回指定线程的优先级。
setPriority()
方法的参数能够是一个整数,范围是1~10之间,也可使用Thread
类的以下三个静态常量。
MAXPRIORITY:其值是10。
MIN PRIORITY:其值是1。
NORM_PRIORITY:其值是5。
为了解决多个线程访问同一个数据时,会出现问题,所以须要进行线程同步。就像前面介绍的文件并发访问,当有两个进程并发修改同一个文件时就有可能形成异常。
为了解决线程同步问题,Java的多线程支持引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块。同步代码块的语法格式以下:
synchronized(obj)
{.....
//此处的代码就是同步代码块
}
复制代码
上面语法格式中synchronized后括号里的obj就是同步监视器,上面代码的含义是:线程开始执行同步代码块以前,必须先得到对同步监视器的锁定。
任什么时候刻只能有一个线程能够得到对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定。
一般推荐使用可能被并发访问的共享资源充当同步监视器,代码示例以下:
public class DrawThread extends Thread {
// 模拟用户帐户
private Account account;
// 当前取钱线程所但愿取的钱数
private double drawAmount;
public DrawThread(String name , Account account , double drawAmount) {
super(name);
this.account = account;
this.drawAmount = drawAmount;
}
// 当多条线程修改同一个共享数据时,将涉及数据安全问题。
public void run() {
// 使用account做为同步监视器,任何线程进入下面同步代码块以前,
// 必须先得到对account帐户的锁定——其余线程没法得到锁,也就没法修改它
// 这种作法符合:“加锁 → 修改 → 释放锁”的逻辑
synchronized (account)
{
// 帐户余额大于取钱数目
if (account.getBalance() >= drawAmount)
{
// 吐出钞票
System.out.println(getName()
+ "取钱成功!吐出钞票:" + drawAmount);
try
{
Thread.sleep(1);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 修改余额
account.setBalance(account.getBalance() - drawAmount);
System.out.println("\t余额为: " + account.getBalance());
}
else
{
System.out.println(getName() + "取钱失败!余额不足!");
}
}
// 同步代码块结束,该线程释放同步锁
}
}
复制代码
同步方法就是使用synchronized
关键字来修饰某个方法,则该方法称为同步方法。
对于synchronized
修饰的实例方法(非static方法)而言,无须显式指定同步监视器,同步方法的同步监视器是this
,也就是调用该方法的对象。
经过使用同步方法能够很是方便地实现线程安全的类,线程安全的类具备以下特征。
该类的对象能够被多个线程安全地访问。
每一个线程调用该对象的任意方法以后都将获得正确结果。
每一个线程调用该对象的任意方法以后,该对象状态依然保持合理状态。
代码示例:
public class Account {
// 封装帐户编号、帐户余额两个成员变量
private String accountNo;
private double balance;
public Account(){}
// 构造器
public Account(String accountNo , double balance) {
this.accountNo = accountNo;
this.balance = balance;
}
// accountNo的setter和getter方法
public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
}
public String getAccountNo() {
return this.accountNo;
}
// 所以帐户余额不容许随便修改,因此只为balance提供getter方法,
public double getBalance() {
return this.balance;
}
// 提供一个线程安全draw()方法来完成取钱操做
public synchronized void draw(double drawAmount) {
// 帐户余额大于取钱数目
if (balance >= drawAmount)
{
// 吐出钞票
System.out.println(Thread.currentThread().getName()
+ "取钱成功!吐出钞票:" + drawAmount);
try
{
Thread.sleep(1);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 修改余额
balance -= drawAmount;
System.out.println("\t余额为: " + balance);
}
else
{
System.out.println(Thread.currentThread().getName()
+ "取钱失败!余额不足!");
}
}
// 下面两个方法根据accountNo来重写hashCode()和equals()方法
public int hashCode() {
return accountNo.hashCode();
}
public boolean equals(Object obj) {
if(this == obj)
return true;
if (obj !=null
&& obj.getClass() == Account.class)
{
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}
复制代码
上面程序中增长了一个表明取钱的draw()
方法,并使用了synchronized
关键字修饰该方法,把该方法变成同步方法。
该同步方法的同步监视器是this
,所以对于同一个Account
帐户而言,任意时刻只能有一个线程得到对Account
对象的锁定,而后进入draw()
方法执行取钱操做,这样也能够保证多个线程并发取钱的线程安全。
程序没法显式释放对同步监视器的锁定,线程会在以下状况下释放对同步监视器的锁定。
当前线程的同步方法、同步代码块执行结束,当前线程即释放同步监视器。
当前线程在同步代码块、同步方法中遇到break
、return
终止了该代码块、该方法的继续执行,当前线程将会释放同步监视器。
当前线程在同步代码块、同步方法中出现了未处理的Error 或Exception,致使了该代码块、该方法异常结束时,当前线程将会释放同步监视器。
当前线程执行同步代码块或同步方法时,程序执行了同步监视器对象的wait0方法,则当前线程暂停,并释放同步监视器。
在以下所示的状况下,线程不会释放同步监视器:
线程执行同步代码块或同步方法时,程序调用Thread.sleep()
、Thread.yield()
方法来暂停当前线程的执行,当前线程不会释放同步监视器。
线程执行同步代码块时,其余线程调用了该线程的suspend()
方法将该线程挂起,该线程不会释放同步监视器。固然,程序应该尽可能避免使用suspend()
和resume()
方法来控制线程。
Lock、ReadWriteLock是Java5提供的两个根接口,并为Lock提供ReentrantLock(可重入锁)实现类,为ReadWriteLock提供了ReentrantReadWriteLock 实现类。
Java8新增了新型的StampedLock类,在大多数场景中它能够替代传统的ReentrantReadWriteLock。
ReentrantReadWriteLock为读写操做提供了三种锁模式:Writing、ReadingOptimistic、Reading。
在实现线程安全的控制中,比较经常使用的是ReentrantLock(可重入锁)。使用该Lock对象能够显式地加锁、释放锁,一般使用ReentrantLock的代码格式以下:
public class Account {
// 定义锁对象
private final ReentrantLock lock = new ReentrantLock();
// 封装帐户编号、帐户余额的两个成员变量
private String accountNo;
private double balance;
public Account(){}
// 构造器
public Account(String accountNo , double balance) {
this.accountNo = accountNo;
this.balance = balance;
}
// accountNo的setter和getter方法
public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
}
public String getAccountNo() {
return this.accountNo;
}
// 所以帐户余额不容许随便修改,因此只为balance提供getter方法,
public double getBalance() {
return this.balance;
}
// 提供一个线程安全draw()方法来完成取钱操做
public void draw(double drawAmount) {
// 加锁
lock.lock();
try
{
// 帐户余额大于取钱数目
if (balance >= drawAmount)
{
// 吐出钞票
System.out.println(Thread.currentThread().getName()
+ "取钱成功!吐出钞票:" + drawAmount);
try
{
Thread.sleep(1);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 修改余额
balance -= drawAmount;
System.out.println("\t余额为: " + balance);
}
else
{
System.out.println(Thread.currentThread().getName()
+ "取钱失败!余额不足!");
}
}
finally
{
// 修改完成,释放锁
lock.unlock();
}
}
// 下面两个方法根据accountNo来重写hashCode()和equals()方法
public int hashCode() {
return accountNo.hashCode();
}
public boolean equals(Object obj) {
if(this == obj)
return true;
if (obj !=null
&& obj.getClass() == Account.class)
{
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}
复制代码
同步方法或同步代码块使用与竞争资源相关的、隐式的同步监视器,而且强制要求加锁和释放锁要出如今一个块结构中,并且当获取了多个锁时,它们必须以相反的顺序释放,且必须在与全部锁被获取时相同的范围内释放全部锁。
Lock提供了同步方法和同步代码块所没有的其余功能,包括用于非块结构的tryLock()
方法,以及试图获取可中断锁的lockInterruptibly()
方法,还有获取超时失效锁的tryLock(long,TimeUnit)
方法。
ReentrantLock
锁具备可重入性,也就是说,一个线程能够对已被加锁的ReentrantLock
锁再次加锁,ReentrantLock
对象会维持一个计数器来追踪lock()
方法的嵌套调用,线程在每次调用lock()
加锁后,必须显式调用unlock()
来释放锁,因此一段被锁保护的代码能够调用另外一个被相同锁保护的方法。
当两个线程相互等待对方释放同步监视器时就会发生死锁一旦出现死锁,整个程序既不会发生任何异常,也不会给出任何提示,只是全部线程处于阻塞状态,没法继续。
死锁示例:
有两个类 A 和 B ,这两个类每一个类都各含有两个同步方法,利用两个线程来进行操做。
首先线程1调用 A 类的同步方法 A1,而后休眠,此时线程2会开始工做,它会调用 B 类的同步方法 B1,而后也休眠。
此时线程1休眠结束,它继续执行方法 A1 ,A1的下一步操做是调用 B 中的同步方法 B2,由于此时 B 的对象示例正被线程2所占据,所以线程1只能等待对 B 的锁的释放。
此时线程2又苏醒了,它继续执行方法 B1,B1的下一步操做是调用 A 中的同步方法 A2,所以是 A 类的对象也被线程1给锁住了,所以线程2也只能等待,这样就形成了线程1和线程2相互等待,从而致使了死锁的发生。
代码示例:
//A类
class A {
public synchronized void foo( B b ) {
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 进入了A实例的foo()方法" ); // ①
try
{
Thread.sleep(200);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 企图调用B实例的last()方法"); // ③
b.last();
}
public synchronized void last() {
System.out.println("进入了A类的last()方法内部");
}
}
//B类
class B {
public synchronized void bar( A a ) {
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 进入了B实例的bar()方法" ); // ②
try
{
Thread.sleep(200);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 企图调用A实例的last()方法"); // ④
a.last();
}
public synchronized void last() {
System.out.println("进入了B类的last()方法内部");
}
}
//线程类
public class DeadLock implements Runnable {
A a = new A();
B b = new B();
public void init() {
Thread.currentThread().setName("主线程");
// 调用a对象的foo方法
a.foo(b);
System.out.println("进入了主线程以后");
}
public void run() {
Thread.currentThread().setName("副线程");
// 调用b对象的bar方法
b.bar(a);
System.out.println("进入了副线程以后");
}
//主函数
public static void main(String[] args) {
DeadLock dl = new DeadLock();
// 以dl为target启动新线程
new Thread(dl).start();
// 调用init()方法
dl.init();
}
}
复制代码
借助于Object
类提供的wait()
、notify()
和notifyAll()
三个方法。
这三个方法并不属于Thread
类,而是属于Object
类。但这三个方法必须由同步监视器对象来调用,这可分红如下两种状况。
对于使用synchronized修饰的同步方法,由于该类的默认实例(this)就是同步监视器,因此能够在同步方法中直接调用这三个方法。
对于使用synchronized修饰的同步代码块,同步监视器是synchronized后括号里的对象,因此必须使用该对象调用这三个方法。
关于这三个方法的解释以下:
wait()
:致使当前线程等待,直到其余线程调用该同步监视器的notify()
方法或notifyAll()
方法来唤醒该线程。
notify()
:唤醒在此同步监视器上等待的单个线程。若是全部线程都在此同步监视器上等待,则会选择唤醒其中一个线程。选择是任意性的。只有当前线程放弃对该同步监视器的锁定后(使用wait()
方法),才能够执行被唤醒的线程。
notifyAll
:唤醒在此同步监视器上等待的全部线程。只有当前线程放弃对该同步监视器的锁定后,才能够执行被唤醒的线程。
若是程序不使用synchronized 关键字来保证同步,而是直接便用Lock对象采保证同步,则系统中下存在隐式的同步监视器,也就不能使用wait()
、notify()
、notifyAll()
方法进行线程通讯了。
当使用Lock 对象来保证同步时,Java提供了一个Condition
类来保持协调,使用Condition
可让那些已经获得Lock对象却没法继续执行的线程释放Lock
对象,Condition
对象也能够唤醒其余处于等待的线程。
Condition
实例被绑定在一个Lock
对象上。要得到特定Lock
实例的Condition
实例,调用Lock
对象的newCondition()
方法便可。Condition
类提供了以下三个方法:
await()
:相似于隐式同步监视器上的wait()
方法,致使当前线程等待,直到其余线程调用该Condition
的signal()
方法或signalAll()
方法来唤醒该线程。
signal()
:唤醒在此Lock
对象上等待的单个线程。若是全部线程都在该Lock
对象上等待,则会选择唤醒其中一个线程。选择是任意性的。只有当前线程放弃对该Lock
对象的锁定后(使用await()
方法),才能够执行被唤醒的线程。
signalAIl()
:唤醒在此Lock
对象上等待的全部线程。只有当前线程放弃对该Lock
对象的锁定后,才能够执行被唤醒的线程。
public class Account {
// 显式定义Lock对象
private final Lock lock = new ReentrantLock();
// 得到指定Lock对象对应的Condition
private final Condition cond = lock.newCondition();
// 封装帐户编号、帐户余额的两个成员变量
private String accountNo;
private double balance;
// 标识帐户中是否已有存款的旗标
private boolean flag = false;
public Account(){}
// 构造器
public Account(String accountNo , double balance) {
this.accountNo = accountNo;
this.balance = balance;
}
// accountNo的setter和getter方法
public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
}
public String getAccountNo() {
return this.accountNo;
}
// 所以帐户余额不容许随便修改,因此只为balance提供getter方法,
public double getBalance() {
return this.balance;
}
public void draw(double drawAmount) {
// 加锁
lock.lock();
try
{
// 若是flag为假,代表帐户中尚未人存钱进去,取钱方法阻塞
if (!flag)
{
cond.await();
}
else
{
// 执行取钱
System.out.println(Thread.currentThread().getName()
+ " 取钱:" + drawAmount);
balance -= drawAmount;
System.out.println("帐户余额为:" + balance);
// 将标识帐户是否已有存款的旗标设为false。
flag = false;
// 唤醒其余线程
cond.signalAll();
}
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 使用finally块来释放锁
finally
{
lock.unlock();
}
}
public void deposit(double depositAmount) {
lock.lock();
try
{
// 若是flag为真,代表帐户中已有人存钱进去,则存钱方法阻塞
if (flag) // ①
{
cond.await();
}
else
{
// 执行存款
System.out.println(Thread.currentThread().getName()
+ " 存款:" + depositAmount);
balance += depositAmount;
System.out.println("帐户余额为:" + balance);
// 将表示帐户是否已有存款的旗标设为true
flag = true;
// 唤醒其余线程
cond.signalAll();
}
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 使用finally块来释放锁
finally
{
lock.unlock();
}
}
// 下面两个方法根据accountNo来重写hashCode()和equals()方法
public int hashCode() {
return accountNo.hashCode();
}
public boolean equals(Object obj) {
if(this == obj)
return true;
if (obj !=null
&& obj.getClass() == Account.class)
{
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}
复制代码
Java5提供了一个BlockingQueue
接口,虽然BlockingQueue
也是Queue
的子接口,但它的主要用途并非做为容器,而是做为线程同步的工具。
BlockingQueue
具备一个特征:
当生产者线程试图向BlockingOueue
中放入元素时,若是该队列已满,则该线程被阻塞;
当消费者线程试图从BlockingQueue
中取出元素时,若是该队列已空,则该线程被阻塞。
BlockingQueue
提供以下两个支持阻塞的方法。
put(E e):尝试把E元素放入BlockingQueue
中,若是该队列的元素已满,则阻塞该线程。
take()
:尝试从BlockingQueue
的头部取出元素,若是该队列的元素已空,则阻塞该线程。
BlockingQueue
继承了Queue
接口,固然也可以使用Queue
接口中的方法。这些方法概括起来可分为以下三组。
在队列尾部插入元素。包括add(E e)
、offer(E e)
和put(Ee)
方法,当该队列已满时,这三个方法分别会抛出异常、返回false、阻塞队列。
在队列头部删除并返回删除的元素。包括remove()
、poll()
和take()
方法。当该队列已空时,这三个方法分别会抛出异常、返回false、阻塞队列。
在队列头部取出但不删除元素。包括element()
和peek()
方法,当队列已空时,这两个方法分别抛出异常、返回false。
使用阻塞队列(BlockingQueue)来实现线程通讯,以消费者生产者为例:
//生产者类
class Producer extends Thread {
private BlockingQueue<String> bq;
public Producer(BlockingQueue<String> bq) {
this.bq = bq;
}
public void run() {
String[] strArr = new String[]
{
"Java",
"Struts",
"Spring"
};
for (int i = 0 ; i < 999999999 ; i++ )
{
System.out.println(getName() + "生产者准备生产集合元素!");
try
{
Thread.sleep(200);
// 尝试放入元素,若是队列已满,线程被阻塞
bq.put(strArr[i % 3]);
}
catch (Exception ex){ex.printStackTrace();}
System.out.println(getName() + "生产完成:" + bq);
}
}
}
//消费者类
class Consumer extends Thread {
private BlockingQueue<String> bq;
public Consumer(BlockingQueue<String> bq) {
this.bq = bq;
}
public void run() {
while(true)
{
System.out.println(getName() + "消费者准备消费集合元素!");
try
{
Thread.sleep(200);
// 尝试取出元素,若是队列已空,线程被阻塞
bq.take();
}
catch (Exception ex){ex.printStackTrace();}
System.out.println(getName() + "消费完成:" + bq);
}
}
}
//主程序
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();
// 启动一条消费者线程
new Consumer(bq).start();
}
}
复制代码
系统启动一个新线程的成本是比较高的,由于它涉及与操做系统交互。在这种情形下,使用线程池能够很好地提升性能,尤为是当程序中须要建立大量生存期很短暂的线程时,更应该考虑使用线程池。
与数据库链接池相似的是,线程池在系统启动时即建立大量空闲的线程,程序将一个Runnable
对象或Callable
对象传给线程池,线程池就会启动一个线程来执行它们的run()
或call()
方法。
当run()
或call()
方法执行结束后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个Runnable
对象的run()
或call()
方法。
除此以外,使用线程池能够有效地控制系统中并发线程的数量,当系统中包含大量并发线程时,会致使系统性能剧烈降低,甚至致使JVM崩溃,而线程池的最大线程数参数能够控制系统中并发线程数不超过此数。
在Java5之前,开发者必须手动实现本身的线程池;从Java5开始,Java内建支持线程池。
Java5新增了一个Executors
工厂类来产生线程池,该工厂类包含以下几个静态工厂方法来建立线程池。
newCachedThreadPool()
:建立一个具备缓存功能的线程池,系统根据须要建立线程,这些线程将会被缓存在线程池中。
newFixedThreadPool(int nThreads)
:建立一个可重用的、具备固定线程数的线程池。
newSingle ThreadExecutor()
:建立一个只有单线程的线程池,它至关于调用newFixedThread Pool()
方法时传入参数为1。
newScheduledThreadPool(int corePoolSize)
:建立具备指定线程数的线程池,它能够在指定延迟后执行线程任务。corePoolSize
指池中所保存的线程数,即便线程是空闲的也被保存在线程池内。
newSingle ThreadScheduledExecutor)
:建立只有一个线程的线程池,它能够在指定延迟后执行线程任务。
ExecutorService new WorkStealingPool(int parallelism)
:建立持有足够的线程的线程池来支持给定的并行级别,该方法还会使用多个队列来减小竞争。
ExecutorService new WorkStealingPool)
:该方法是前一个方法的简化版本。若是当前机器有4个CPU,则目标并行级别被设置为4,也就是至关于为前一个方法传入4做为参数。
上面7个方法中的前三个方法返回一个ExecutorService
对象,该对象表明一个线程池,它能够执行Runnable
对象或Callable
对象所表明的线程;
而中间两个方法返回一个ScheduledExecutorService
线程池,它是ExecutorService
的子类,它能够在指定延迟后执行线程任务;
最后两个方法则是Java8新增的,这两个方法可充分利用多CPU并行的能力。这两个方法生成的work stealing
池,都至关于后台线程池,若是全部的前台线程都死亡了,work stealing
池中的线程会自动死亡。
ExecutorService
表明尽快执行线程的线程池(只要线程池中有空闲线程,就当即执行线程任务)
程序只要将一个Runnable
对象或Callable
对象(表明线程任务)提交给该线程池,该线程池就会尽快执行该任务。
ExecutorService里提供了以下三个方法。
Future<?>submit(Runnable task)
:将一个Runnable
对象提交给指定的线程池,线程池将在有空闲线程时执行Runnable
对象表明的任务。其中Future
对象表明Runnable
任务的返回值,但run()
方法没有返回值,因此Future
对象将在run()
方法执行结束后返回null
。但能够调用Future
的isDone()
、isCancelled()
方法来得到Runnable
对象的执行状态。
<T>Future-T>submit(Runnable task,T result)
:将一个Runnable
对象提交给指定的线程池,线程池将在有空闲线程时执行Runnable对象表明的任务。其中result
显式指定线程执行结束后的返回值,因此Future
对象将在run()
方法执行结束后返回result
。
<T>Future-T>submit(Callable<T>task)
:将一个Callable
对象提交给指定的线程池,线程池将在有空闲线程时执行Callable
对象表明的任务。其中Future
表明Callable
对象里call()
方法的返回值。
ScheduledExecutorService
表明可在指定延迟后或周期性地执行线程任务的线程池,它提供了以下4个方法。
ScheduledFuture<V> schedule(Callable-V> callable,long delay,TimeUnit unit)
:指定callable
任务将在delay
延迟后执行。
ScheduledFuture<?>schedule(Runnable command,long delay,TimeUnit unit)
:指定command
任务将在delay
延迟后执行。
ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit)
:指定command
任务将在delay延迟后执行,并且以设定频率重复执行。也就是说,在initialDelay
后开始执行,依次在initialDelay+period、initialDelay+2*period…
处重复执行,依此类推。
ScheduledFuture<?>scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit)
:建立并执行一个在给定初始延迟后首次启用的按期操做,随后在每一次执行终止和下一次执行开始之间都存在给定的延迟。若是任务在任一次执行时遇到异常,就会取消后续执行;不然,只能经过程序来显式取消或终止该任务。
用完一个线程池后,应该调用该线程池的shutdown0方法,该方法将启动线程池的关闭序列,调用shutdown()
方法后的线程池再也不接收新任务,但会将之前全部已提交任务执行完成。当线程池中的全部任务都执行完成后,池中的全部线程都会死亡;
另外也能够调用线程池的shutdownNow()
方法来关闭线程池,该方法试图中止全部正在执行的活动任务,暂停处理正在等待的任务,并返回等待执行的任务列
表。
使用线程池来执行线程任务的步骤以下。
①调用Executors
类的静态工厂方法建立一个ExecutorService
对象,该对象表明一个线程池。
②建立Runnable
实现类或Callable
实现类的实例,做为线程执行任务。
③调用ExecutorService
对象的submit()
方法来提交Runnable
实例或Callable
实例。
④当不想提交任何任务时,调用ExecutorService
对象的shutdown()
方法来关闭线程池。
代码示例:
public class ThreadPoolTest {
public static void main(String[] args) throws Exception {
// 建立足够的线程来支持4个CPU并行的线程池
// 建立一个具备固定线程数(6)的线程池
ExecutorService pool = Executors.newFixedThreadPool(6);
// 使用Lambda表达式建立Runnable对象
Runnable target = () -> {
for (int i = 0; i < 100 ; i++ )
{
System.out.println(Thread.currentThread().getName() + "的i值为:" + i);
}
};
// 向线程池中提交两个线程
pool.submit(target);
pool.submit(target);
// 关闭线程池
pool.shutdown();
}
}
复制代码
Java7提供了ForkJoinPool
来支持将一个任务拆分红多个“小任务”并行计算,再把多个“小任务”的结果合并成总的计算结果。ForkJoinPool
是ExecutorService
的实现类,所以是一种特殊的线程池。
ForkJoinPool
提供了以下两个经常使用的构造器。
ForkJoinPool(int parallelism)
:建立一个包含parallelism
个并行线程的ForkJoinPool
。
ForkJoinPool()
:以Runtime.availableProcessors()
方法的返回值做为parallelism
参数来建立ForkJoinPool
。
Java8进一步扩展了ForkJoinPool
的功能,Java8为ForkJoinPool
增长了通用池功能。
ForkJoinPool
经过以下两个静态方法提供通用池功能。
ForkJoinPool commonPool()
:该方法返回一个通用池。通用池的运行状态不会受shutdown()
或shutdownNow()
方法的影响。固然,若是程序直接执行System.exit(0)
;来终止虚拟机,通用池以及通用池中正在执行的任务都会被自动终止。
int getCommonPoolParallelism()
:该方法返回通用池的并行级别。建立了ForkJoinPool
实例以后,就可调用ForkJoinPool
的submit(ForkJoin Task task)
或invoke(ForkJoinTask task)
方法来执行指定任务了。
其中ForkJoinTask
表明一个能够并行、合并的任务。
ForkJoinTask
是一个抽象类,它还有两个抽象子类:RecursiveAction
和Recursive Task
。
其中Recursive Task
表明有返回值的任务,而RecursiveAction
表明没有返回值的任务。
下面以执行没有返回值的“大任务”(简单地打印0-300的数值)为例,程序将一个“大任务”拆分红多个“小任务”,并将任务交给ForkJoinPool
来执行。
// 继承RecursiveAction来实现"可分解"的任务
class PrintTask extends RecursiveAction {
// 每一个“小任务”只最多只打印50个数
private static final int THRESHOLD = 50;
private int start;
private int end;
// 打印从start到end的任务
public PrintTask(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected void compute() {
// 当end与start之间的差小于THRESHOLD时,开始打印
if(end - start < THRESHOLD)
{
for (int i = start ; i < end ; i++ )
{
System.out.println(Thread.currentThread().getName() + "的i值:" + i);
}
}
else
{
// 若是当end与start之间的差大于THRESHOLD时,即要打印的数超过50个
// 将大任务分解成两个小任务。
int middle = (start + end) / 2;
PrintTask left = new PrintTask(start, middle);
PrintTask right = new PrintTask(middle, end);
// 并行执行两个“小任务”
left.fork();
right.fork();
}
}
}
/** * description: 主函数 **/
public class ForkJoinPoolTest {
public static void main(String[] args) throws Exception {
ForkJoinPool pool = new ForkJoinPool();
// 提交可分解的PrintTask任务
pool.submit(new PrintTask(0 , 300));
pool.awaitTermination(2, TimeUnit.SECONDS);
// 关闭线程池
pool.shutdown();
}
}
复制代码
上面定义的任务是一个没有返回值的打印任务,若是大任务是有返回值的任务,则可让任务继承Recursive Task<T>
,其中泛型参数T就表明了该任务的返回值类型。下面程序示范了使用Recursive Task
对一个长度为100的数组的元素值进行累加。
// 继承RecursiveTask来实现"可分解"的任务
class CalTask extends RecursiveTask<Integer> {
// 每一个“小任务”只最多只累加20个数
private static final int THRESHOLD = 20;
private int arr[];
private int start;
private int end;
// 累加从start到end的数组元素
public CalTask(int[] arr , int start, int end) {
this.arr = arr;
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
int sum = 0;
// 当end与start之间的差小于THRESHOLD时,开始进行实际累加
if(end - start < THRESHOLD)
{
for (int i = start ; i < end ; i++ )
{
sum += arr[i];
}
return sum;
}
else
{
// 若是当end与start之间的差大于THRESHOLD时,即要累加的数超过20个时
// 将大任务分解成两个小任务。
int middle = (start + end) / 2;
CalTask left = new CalTask(arr , start, middle);
CalTask right = new CalTask(arr , middle, end);
// 并行执行两个“小任务”
left.fork();
right.fork();
// 把两个“小任务”累加的结果合并起来
return left.join() + right.join(); // ①
}
}
}
/** * description: 主函数 **/
public class Sum {
public static void main(String[] args) throws Exception {
int[] arr = new int[100];
Random rand = new Random();
int total = 0;
// 初始化100个数字元素
for (int i = 0 , len = arr.length; i < len ; i++ )
{
int tmp = rand.nextInt(20);
// 对数组元素赋值,并将数组元素的值添加到sum总和中。
total += (arr[i] = tmp);
}
System.out.println(total);
// 建立一个通用池
ForkJoinPool pool = ForkJoinPool.commonPool();
// 提交可分解的CalTask任务
Future<Integer> future = pool.submit(new CalTask(arr , 0 , arr.length));
System.out.println(future.get());
// 关闭线程池
pool.shutdown();
}
}
复制代码
ThreadLocal,是Thread Local Variable(线程局部变量)的意思,它就是为每个使用该变量的线程都提供一个变量值的副本,使每个线程均可以独立地改变本身的副本,而不会和其余线程的副本冲突。从线程的角度看,就好像每个线程都彻底拥有该变量同样。
它只提供了以下三个public方法。
T get()
:返回此线程局部变量中当前线程副本中的值。
void remove()
:删除此线程局部变量中当前线程的值。
void set(T value)
:设置此线程局部变量中当前线程副本中的值。
代码示例:
/** * description: 帐户类 **/
class Account {
/* 定义一个ThreadLocal类型的变量,该变量将是一个线程局部变量 每一个线程都会保留该变量的一个副本 */
private ThreadLocal<String> name = new ThreadLocal<>();
// 定义一个初始化name成员变量的构造器
public Account(String str) {
this.name.set(str);
// 下面代码用于访问当前线程的name副本的值
System.out.println("---" + this.name.get());
}
// name的setter和getter方法
public String getName() {
return name.get();
}
public void setName(String str) {
this.name.set(str);
}
}
/** * description: 线程类 **/
class MyTest extends Thread {
// 定义一个Account类型的成员变量
private Account account;
public MyTest(Account account, String name) {
super(name);
this.account = account;
}
public void run() {
// 循环10次
for (int i = 0 ; i < 10 ; i++)
{
// 当i == 6时输出将帐户名替换成当前线程名
if (i == 6)
{
account.setName(getName());
}
// 输出同一个帐户的帐户名和循环变量
System.out.println(account.getName() + " 帐户的i值:" + i);
}
}
}
/** * description: 主程序 **/
public class ThreadLocalTest {
public static void main(String[] args) {
// 启动两条线程,两条线程共享同一个Account
Account at = new Account("初始名");
/* 虽然两条线程共享同一个帐户,即只有一个帐户名 但因为帐户名是ThreadLocal类型的,因此每条线程 都彻底拥有各自的帐户名副本,因此从i == 6以后,将看到两条 线程访问同一个帐户时看到不一样的帐户名。 */
new MyTest(at , "线程甲").start();
new MyTest(at , "线程乙").start ();
}
}
复制代码
程序结果如图:
分析:
上面Account
类中的三行粗体字代码分别完成了建立ThreadLocal
对象、从ThreadLocal
中取出线程局部变量、修改线程局部变量的操做。
因为程序中的帐户名是一个ThreadLocal
变量,因此虽然程序中只有一个Account
对象,但两个子线程将会产生两个帐户名(主线程也持有一个帐户名的副本)。
两个线程进行循环时都会在i=6
时将帐户名改成与线程名相同,这样就能够看到两个线程拥有两个帐户名的情形,如图所示。
从上面程序能够看出,实际上帐户名有三个副本,主线程一个,另外启动的两个线程各一个,它们的值互不干扰,每一个线程彻底拥有本身的ThreadLocal
变量,这就是ThreadLocal
的用途。
ThreadLocal
和其余全部的同步机制同样,都是为了解决多线程中对同一变量的访问冲突。
在普通的同步机制中,是经过对象加锁来实现多个线程对同一变量的安全访问的。该变量是多个线程共享的,因此要使用这种同步机制,须要很细致地分析在何时对变量进行读写,何时须要锁定某个对象,何时释放该对象的锁等。在这种状况下,系统并无将这份资源复制多份,只是采用了安全机制来控制对这份资源的访问而已。
ThreadLocal
从另外一个角度来解决多线程的并发访问,ThreadLocal
将须要并发访问的资源复制多份,每一个线程拥有一份资源,每一个线程都拥有本身的资源副本,从而也就没有必要对该变量进行同步了。
ThreadLocal
提供了线程安全的共享对象,在编写多线程代码时,能够把不安全的整个变量封装进ThreadLocal
,或者把该对象与线程相关的状态使用ThreadLocal
保存。
ThreadLocal
并不能替代同步机制,二者面向的问题领域不一样。同步机制是为了同步多个线程对相同资源的并发访问,是多个线程之间进行通讯的有效方式;
而ThreadLocal
是为了隔离多个线程的数据共享,从根本上避免多个线程之间对共享资源(变量)的竞争,也就不须要对多个线程进行同步了。
一般建议:
若是多个线程之间须要共享资源,以达到线程之间的通讯功能,就使用同步机制;若是仅仅须要隔离多个线程之间的共享冲突,则可使用ThreadLocal
。
像ArrayList
、LinkedList
、HashSet
、TreeSet
、HashMap
、TreeMap
等都是线程不安全的,也就是说,当多个并发线程向这些集合中存、取元素时,就可能会破坏这些集合的数据完整性。
若是程序中有多个线程可能访问以上这些集合,就可使用Collections
提供的类方法把这些集合包装成线程安全的集合。Collections
提供了以下几个静态方法。
<T>Collection<T>synchronizedCollection(Collection<T>c)
:返回指定collection对应的线程安全的collection。
static<T>List<T>synchronizedList(List<T>list)
:返回指定List对象对应的线程安全的List对象。
static<K,V>Map<K,V> synchronizedMap(Map<K,V>m)
:返回指定Map对象对应的线程安全的Map对象。
static<T>Set<T>synchronizedSet(Set<T>s)
:返回指定Set对象对应的线程安全的Set对象。
static<K,V>SortedMap<K,V>synchronizedSortedMap(SortedMap<K,V>m)
:返回指定SortedMap对象对应的线程安全的SortedMap对象。
static<T>SortedSet-T>synchronizedSortedSet(SortedSet<T>s)
:返回指定SortedSet对象对应的线程安全的SortedSet对象。
例如须要在多线程中使用线程安全的HashMap对象,则能够采用以下代码:
//使用Collections的synchronizedMap方法将一个普通的HashMap包装成线程安全的类
HashMap m=Collections.synchronizedMap(new HashMap());
复制代码
tips:
若是须要把某个集合包装成线程安全的集合,则应该在建立以后当即包装,如上程序所示,当HashMap
对象建立后当即被包装成线程安全的HashMap
对象。
线程安全的集合类可分为以下两类:
以Concurrent
开头的集合类,如ConcurrentHashMap
、ConcurrentSkipListMap
、ConcurrentSkip ListSet
、
ConcurrentLinkedQueue
和ConcurrentLinkedDeque
以CopyOnWrite
开头的集合类,如CopyOnWriteArrayList
、CopyOnWriteArraySet
其中以Concurrent
开头的集合类表明了支持并发访问的集合,它们能够支持多个线程并发写入访问,这些写入线程的全部操做都是线程安全的,但读取操做没必要锁定。
以Concurrent
开头的集合类采用了更复杂的算法来保证永远不会锁住整个集合,所以在并发写入时有较好的性能。
在默认状况下,ConcurrentHashMap
支持16个线程并发写入,当有超过16个线程并发向该Map
中写入数据时,可能有一些线程须要等待。实际上,程序经过设置concurrencyLevel
构造参数(默认值为16)来支持更多的并发写入线程。
与前面介绍的HashMap
和普通集合不一样的是,由于ConcurrentLinkedQueue
和ConcurrentHashMap
支持多线程并发访问,因此当使用迭代器来遍历集合元素时,该迭代器可能不能反映出建立迭代器以后所作的修改,但程序不会抛出任何异常。
Java8扩展了ConcurrentHashMap
的功能,Java8为该类新增了30多个新方法,这些方法可借助于Stream
和Lambda
表达式支持执行汇集操做。ConcurrentHashMap
新增的方法大体可分为以下三类:
forEach
系列 (forEach,forEachKey,forEach Value,forEachEntry)
search
系列 (search,searchKeys,search Values,searchEntries)
reduce
系列 (reduce,reduce ToDouble,reduce ToLong,reduceKeys,reduceValues)
除此以外,ConcurrentHashMap
还新增了mappingCount()
、newKeySet()
等方法,加强后的ConcurrentHashMap
更适合做为缓存实现类使用。
CopyOnWriteAtraySet
因为CopyOnWriteAtraySet
的底层封装了CopyOnWriteArmayList
,所以它的实现机制彻底相似于CopyOnWriteArrayList
集合。
对于CopyOnWriteArrayList
集合,,它采用复制底层数组的方式来实现写操做。
当线程对CopyOnWriteArrayList
集合执行读取操做时,线程将会直接读取集合自己,无须加锁与阻塞。
当线程对CopyOnWriteArrayList
集合执行写入操做时(包括调用add()、
remove()、
set()`等方法)该集合会在底层复制一份新的数组,接下来对新的数组执行写入操做。
因为对 CopyOnWriteArmayList
集合的写入操做都是对数组的副本执行操做,所以它是线程安全的。
须要指出的是,因为CopyOnWriteArrayList
执行写入操做时须要频繁地复制数组,性能比较差。
但因为读操做与写操做不是操做同一个数组,并且读操做也不须要加锁,所以读操做就很快、很安全。因而可知,CopyOnWriteArayList
适合用在读取操做远远大于写入操做的场景中,例如缓存等。
《Effective Java》、《现代操做系统》、《TCP/IP详解:卷一》、《代码整洁之道》、《重构》、《Java程序性能优化》、《Spring实战》、《Zookeeper》、《高性能MySQL》、《亿级网站架构核心技术》、《可伸缩服务架构》、《Java编程思想》
说实话这些书不少只看了一部分,我一般会带着问题看书,否则看着看着就睡着了,简直是催眠良药。
**最后,附一张面试前准备资料
有须要的伙伴私信我加入咱们群聊809389099便可免费领取哦