文章目录
几乎所有的操作系统都支持同时运行多个任务, 一个任务通常就是一个程序, 每个运行中的程序就是一个进程。 当一个程序运行时, 内部可能包含了多个顺序执行流, 每个顺序执行流就是一个线程。
线程和进程
几乎所有的操作系统都支持进程的概念, 所有运行中的任务通常对应一个进程( Process)。 当一个程序进入内存运行时, 即变成一个进程。 进程是处于运行过程中的程序, 并且具有一定的独立功能, 进程是系统进行资源分配和调度的一个独立单位。
一般而言, 进程包含如下三个特征:
- 独立性: 进程是系统中独立存在的实体, 它可以拥有自己独立的资源, 每一个进程都拥有自己私有的地址空间。 在没有经过进程本身允许的情况下, 一个用户进程不可以直接访问其他进程的地址空间。
- 动态性: 进程与程序的区别在于, 程序只是一个静态的指令集合, 而进程是一个正在系统中活动的指令集合。 在进程中加入了时间的概念。 进程具有自己的生命周期和各种不同的状态, 这些概念在程序中都是不具备的。
- 并发性: 多个进程可以在单个处理器上并发执行, 多个进程之间不会互相影响。
线程是进程的组成部分, 一个进程可以拥有多个线程, 一个线程必须有一个父进程。 线程可以拥有自己的堆栈、 自己的程序计数器和自己的局部变量, 但不拥有系统资源, 它与父进程的其他线程共享该进程所拥有的全部资源。
关于进程和线程,教材上的解释比较抽象,下面的图解比较清晰易懂:
- 计算机的核心是CPU,它承担了所有的计算任务。它就像一座工厂,时刻在运行。
- 假定工厂的电力有限,一次只能供给一个车间使用。也就是说,一个车间开工的时候,其他车间都必须停工。背后的含义就是,单个CPU一次只能运行一个任务。
- 进程就好比工厂的车间,它代表CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。
- 一个车间里,可以有很多工人。他们协同完成一个任务。
- 线程就好比车间里的工人。一个进程可以包括多个线程。
- 车间的空间是工人们共享的,比如许多房间是每个工人都可以进出的。这象征一个进程的内存空间是共享的,每个线程都可以使用这些共享内存。
- 可是,每间房间的大小不同,有些房间最多只能容纳一个人,比如厕所。里面有人的时候,其他人就不能进去了。这代表一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。
- 一个防止他人进入的简单方法,就是门口加一把锁。先到的人锁上门,后到的人看到上锁,就在门口排队,等锁打开再进去。这就叫"互斥锁"(Mutual exclusion,缩写 Mutex),防止多个线程同时读写某一块内存区域。
- 还有些房间,可以同时容纳n个人,比如厨房。也就是说,如果人数大于n,多出来的人只能在外面等着。这好比某些内存区域,只能供给固定数目的线程使用。
- 这时的解决方法,就是在门口挂n把钥匙。进去的人就取一把钥匙,出来时再把钥匙挂回原处。后到的人发现钥匙架空了,就知道必须在门口排队等着了。这种做法叫做"信号量"(Semaphore),用来保证多个线程不会互相冲突。
不难看出,mutex是semaphore的一种特殊情况(n=1时)。也就是说,完全可以用后者替代前者。但是,因为mutex较为简单,且效率高,所以在必须保证资源独占的情况下,还是采用这种设计。
归纳起来可以这样说: 操作系统可以同时执行多个任务, 每个任务就是进程; 进程可以分成多个部分, 每个部分就是线程。
线程的创建和启动
Java 使用 Thread 类代表线程, 所有的线程对象都必须是 Thread 类或其子类的实例。 每个线程的作用是完成一定的任务, 实际上就是执行一段程序流( 一段顺序执行的代码)。 Java 使用线程执行体来代表这段程序流。
继承 Thread 类创建线程类
通过继承 Thread 类来创建并启动多线程的步骤如下:
- 定义Thread类的子类,并重写Thread类的run()方法,run()方法的方法体就代表了线程需要完成的任务,所以将run()方法称为线程执行体。
- 创建Thread子类的实例,即创建了线程对象。
- 调用线程的start()方法来启动该线程。
继承Thread类并创建并启动多线程实例:
// 通过继承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();
}
}
}
}
上面程序只显式地创建并启动了 2 个 线 程,但实际上程序有 3 个线程, 即程序显式创建的 2 个子线程和主线程。
程序可以通过 setName(String name)方法为线程设置名字, 也可以通过getName()方法返回指定线程的名字。 在默认情况下, 主线程的名字为 main, 用户启动的多个线程的名字依次为 Thread-0、 Thread-1 , Thread-2 、 … 、 Thread-n”等。
Thread-0 和 Thread-1 两个线程输出的 i 变量不连续—注意:i 变量是 FirstThread 的实例变量, 而不是局部变量, 但因为程序每次创建线程对象时都需要创建一个FirstThread 对象, 所以 Thread-0 和 Thread-1 不能共享该实例变量。
使用继承Thread 类的方法来创建线程类时, 多个线程之间无法共享线程类的实例变量。
API:java.lang.Thread
实现Runnable接口创建线程类
实现 Runnable 接口来创建并启动多线程的步骤如下。
- 定义 Runnable 接口的实现类, 并重写该接口的 run()方法, 该 run()方法的方法体同样是该线程的线程执行体。
- 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
- 调用线程对象的 start()方法来启动该线程。
实现 Runnable 接口来创建并启动多线程实例:
// 通过实现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();
}
}
}
}
从图 中的可以看出, 两个子线程的 i 变量是连续的, 也就是采用 Rimnable 接口的方式创建的多个线程可以共享线程类的实例变量。 这是因为在这种方式下,
程序所创建的 Runnable 对象只是线程的 target, 而多个线程可以共享同一个 target,所以多个线程可以共享同一个线程类( 实际上应该是线程的 target 类) 的实例变量。
使用 Callable 和 Future 创建线程
从 Java 5 开始, Java 提供了 Callable 接口, 该接口就像是 Runnable 接口的增强版, Callable 接口提供了一个 call()方法可以作为线程执行体, 但 call()方法比 run()方法功能更强大。
- call()方法可以有返回值。
- call()方法可以声明抛出异常。
Java 5 提供了 Future 接口来代表 Callable 接口里 call()方法的返回值, 并为 Future 接口提供了一个FutureTask 实现类, 该实现类实现了 Future 接口, 并实现了 Runnable 接口 可以作为 Thread 类的 target。
在 Future 接口里定义了如下几个公共方法来控制它关联的 Callable 任务:
- boolean cancel(boolean maylnterruptlfRunning): 试图取消该 Future 里关联的 Callable 任务。
- V get(): 返回 Callable 任务里 call()方法的返回值。 调用该方法将导致程序阻塞, 必须等到子线程结束后才会得到返回值。
- V get(long timeout, TimeUnit unit): 返回 Callable 任务里 call()方法的返回值。 该方法让程序最多阻塞 timeout 和 unit 指定的时间, 如果经过指定时间后 Callable 任务依然没有返回值, 将会抛出TimeoutException 异常。
- boolean isCancelled(): 如果在 Callable 任务正常完成前被取消, 则返回 true。
- boolean isDone(): 如果 Callable 任务己完成, 则返回 true。
创建并启动有返回值的线程的步骤如下。
- 创建 Callable 接口的实现类, 并实现 call()方法, 该 call()方法将作为线程执行体, 且该 call()方法有返回值, 再创建 Callable 实现类的实例。 从 Java 8 开始, 可以直接使用 Lambda 表达式创建 Callable对象。
- 使用 FutureTask 类来包装 Callable 对象, 该 FutureTask 对象封装了该 Callable 对象的 call()方法的返回值。
- 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
- 调用 FutureTask 对象的 get()方法来获得子线程执行结束后的返回值。
下面程序通过实现 Callable 接口来实现线程类, 并启动该线程:
ThirdThread.java
import java.util.concurrent.*;
public class ThirdThread
{
public static void main(String[] args)
{
// 创建Callable对象
ThirdThread rt = new ThirdThread();
// 先使用Lambda表达式创建Callable<Integer>对象
// 使用FutureTask来包装Callable对象
FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>)() -> {
int i = 0;
for ( ; i < 100 ; i++ )
{
System.out.println(Thread.currentThread().getName()
+ " 的循环变量i的值:" + i);
}
// call()方法可以有返回值
return i;
});
for (int i = 0 ; i < 100 ; i++)
{
System.out.println(Thread.currentThread().getName()
+ " 的循环变量i的值:" + i);
if (i == 20)
{
// 实质还是以Callable对象来创建、并启动线程
new Thread(task , "有返回值的线程").start();
}
}
try
{
// 获取线程返回值
System.out.println("子线程的返回值:" + task.get());
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
}
上面程序中使用 Lambda 表达式直接创建了 Callable 对象, 这样就无须先创建 Callable 实现类, 再创建 Callable 对象了。 实现 Callable 接口与实现 Runnable 接口并没有太大的差别, 只是 Callable 的 call()方法允许声明抛出异常, 而且允许带返回值。
API:java.uitl.concurrent.Callable
API:java.util.concurrent.Future
API:java.util.concurrent.FutureTask
创建线程的三种方式对比
通过继承 Thread 类或实现 Runnable、 Callable 接口都可以实现多线程, 不过实现 Runnable 接口与实现 Callable 接口的方式基本相同, 只是 Callable 接口里定义的方法有返回值, 可以声明抛出异常而己。 因此可以将实现 Runnable 接口和实现 Callable 接口归为一种方式。 这种方式与继承 Thread 方式之间的主
要差别如下。
采用实现 Runnable、 Callable 接口的方式创建多线程的优缺点:
- 线程类只是实现了 Runnable 接口或 Callable 接口, 还可以继承其他类。
- 在这种方式下, 多个线程可以共享同一个 target 对象, 所以非常适合多个相同线程来处理同一份资源的情况, 从而可以将 CPU、 代码和数据分开, 形成清晰的模型, 较好地体现了面向对象的思想。
- 劣势是编程稍稍复杂,如果需要访问当前线程,则必须使用 Thread.currentThread()方法。
采用继承Thread类优缺点:
- 劣势是因为线程类已经继承了Thread类,所以不能再继承其它父类。
- 又是是, 编写简单, 如果需要访问当前线程, 则无须使用Thread.currentThread()方法, 直接使用this 即可获得当前线程。
鉴于上面分析, 因此一般推荐采用实现 Runnable 接口、 Callable 接口的方式来创建多线程。
线程的生命周期
当线程被创建并启动以后, 它既不是一启动就进入了执行状态, 也不是一直处于执行状态, 在线程的生命周期中, 它要经过新建(New)、 就绪(Runnable )、 运行(Running)、 阻塞(Blocked)和死亡(Dead) 5种状态。
新建和就绪状态
当程序使用 new 关键字创建了一个线程之后, 该线程就处于新建状态, 此时它和其他的 Java 对象一样,由 Java 虚拟机为其分配内存, 并初始化其成员变量的值。 此时的线程对象没有表现出任何线程的动态特征, 程序也不会执行线程的线程执行体。
当线程对象调用了 start()方法之后, 该线程处于就绪状态, Java 虚拟机会为其创建方法调用栈和程序计数器, 处于这个状态中的线程并没有开始运行, 只是表示该线程可以运行了。 至于该线程何时幵始运行, 取决于 JVM 里线程调度器的调度。
运行和阻塞状态
如果处于就绪状态的线程获得了 CPU,开始执行 run()方法的线程执行体, 则该线程处于运行状态,如果计算机只有一个 CPU, 那么在任何时刻只有一个线程处于运行状态。 当然, 在一个多处理器的机器上, 将会有多个线程并行( 注意是并行: parallel ) 执行; 当线程数大于处理器数时, 依然会存在多个线程在同一个 CPU 上轮换的现象。
当一个线程开始运行后, 它不可能一直处于运行状态( 除非它的线程执行体足够短, 瞬间就执行结束了), 线程在运行过程中需要被中断, 目的是使其他线程获得执行的机会, 线程调度的细节取决于底层平台所采用的策略。 对于采用抢占式策略的系统而言, 系统会给每个可执行的线程一个小时间段来处理任务; 当该时间段用完后, 系统就会剥夺该线程所占用的资源, 让其他线程获得执行的机会。 在选择下一个线程时, 系统会考虑线程的优先级。
所有现代的桌面和服务器操作系统都采用抢占式调度策略, 但一些小型设备如手机则可能采用协作式调度策略, 在这样的系统中, 只有当一个线程调用了它的 sleep()或 yield()方法后才会放弃所占用的资源—也就是必须由该线程主动放弃所占用的资源。
当发生如下情况时, 线程将会进入阻塞状态:
- 线程调用 sleep()方法主动放弃所占用的处理器资源。
- 线程调用了一个阻塞式 IO 方法, 在该方法返回之前, 该线程被阻塞。
- 线程试图获得一个同步监视器, 但该同步监视器正被其他线程所持有。
- 线程在等待某个通知( notify)。
- 程序调用了线程的 suspend()方法将该线程挂起。 但这个方法容易导致死锁, 所以应该尽量避免使用该方法。
当前正在执行的线程被阻塞之后, 其他线程就可以获得执行的机会。 被阻塞的线程会在合适的时候重新进入就绪状态, 注意是就绪状态而不是运行状态。 也就是说, 被阻塞线程的阻塞解除后, 必须重新等待线程调度器再次调度它。
针对上面几种情况, 当发生如下特定的情况时可以解除上面的阻塞, 让该线程重新进入就绪状态:
- 调用 sleep()方法的线程经过了指定时间。
- 线程调用的阻塞式 IO 方法己经返回。
- 线程成功地获得了试图取得的同步监视器。
- 线程正在等待某个通知时, 其他线程发出了一个通知。
- 处于挂起状态的线程被调用了 resumeO恢复方法。
线程状态转换图
线程从阻塞状态只能进入就绪状态,无法直接进入运行状态,而就绪和运行状态之间的转换通常不受程序控制, 而是由系统线程调度所决定, 当处于就绪状态的线程获得处理器资源时, 该线程进入运行状态; 当处于运行状态的线程失去处理器资源时, 该线程进入就绪状态。 但有一个方法例外, 调用 yield()方法可以让运行状态的线程转入就绪状态。
线程死亡
线程会以如下三种方式结束, 结束后就处于死亡状态。
- run()或 call()方法执行完成, 线程正常结束。
- 线程抛出一个未捕获的 Exception 或 Error。
- 直接调用该线程的 stop()方法来结束该线程—该方法容易导致死锁, 通常不推荐使用。
为了测试某个线程是否己经死亡, 可以调用线程对象的 isAliveO方法, 当线程处于就绪、 运行、 阻塞三种状态时, 该方法将返回 true; 当线程处于新建、 死亡两种状态时, 该方法将返回 false。
控制线程
join 线程
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);
}
}
}
上面程序中一共有 3 个线程, 主方法开始时就启动了名为“ 新线程” 的子线程, 该子线程将会和main 线程并发执行。 当主线程的循环变量 i 等于 20 时, 启动了名为“ 被 Join 的线程” 的线程, 该线程不会和 main 线程并发执行, main 线程必须等该线程执行结束后才可以向下执行。 在名为“ 被 Join 的线程” 的线程执行时, 实际上只有 2 个子线程并发执行, 而主线程处于等待状态。
join()方法有如下三种重载形式。
- join(): 等待被 join 的线程执行完成。
- join(long millis): 等待被 join 的线程的时间最长为millis 毫秒。 如果在 millis 毫秒内被 join 的线程还没有执行结束, 则不再等待。
- join(long millis,int nanos): 等待被 join 的线程的时间最长为 millis 毫秒加nanos毫微妙。
后台线程
为其它的线程提供服务,在后台运行的线程,被称为“后台线程 ( Daemon Thread )”, 又称为“ 守护线程” 或“ 精灵线程”。 JVM 的垃圾回收线程就是典型的后台线程。
后台线程有个特征: 如果所有的前台线程都死亡, 后台线程会自动死亡。
调用 Thread 对象的 setDaemon(true)方法可将指定线程设置成后台线程。
Thread 类还提供了一个 isDaemonO方法, 用于判断指定线程是否为后台线程。
线程睡眠: sleep
如果需要让当前正在执行的线程暂停一段时间, 并进入阻塞状态, 则可以通过调用 Thread 类的静态 sleep()方法来实现。 sleep()方法有两种重载形式。
- static void sleep(long millis): 让当前正在执行的线程暂停 millis 毫秒, 并进入阻塞状态, 该方法受到系统计时器和线程调度器的精度与准确度的影响。
- static void sleep(long millis,int nanos): 让当前正在执行的线程暂停 millis 毫秒加并进入阻塞状态, 该方法受到系统计时器和线程调度器的精度与准确度的影响。
线程睡眠实例:
/**
* 一个计数器,计数到100,在每个数字之间暂停1秒,每隔10个数字输出一个字符串
*
*/
public class SleepThread extends Thread{
public void run() {
for (int i = 0; i < 100; i++) {
if ((i) % 10 == 0) {
System.out.println("-------" + i);
}
System.out.print(i);
try {
Thread.sleep(1);
System.out.print(" 线程睡眠1毫秒!\n");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
new SleepThread().start();
}
}
改变线程优先级
线程的让步是通过Thread.yield()来实现的。yield()方法的作用是:暂停当前正在执行的线程对象,并执行其他线程。
每个线程执行时都具有一定的优先级, 优先级高的线程获得较多的执行机会,而优先级低的线程则获得较少的执行机会。每个线程默认的优先级都与创建它的父线程的优先级相同, 在默认情况下, main 线程具有普通优先级, 由 main 线程创建的子线程也具有普通优先级。
Thread 类提供了 setPriority(int newPriority)、 getPriority()方法来设置和返回指定线程的优先级, 其中 setPriority()方法的参数可以是一个整数, 范围是 1——10 之间, 也可以使用 Thread 类的如下三个静态常量。
- MAX PRIORITY 其值是 10。
- MIN PRIORITY 其值是 1。
- NORM PRIORITY 其值是 5。
Thread.yield()方法作用是:暂停当前正在执行的线程对象,并执行其他线程。
yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
线程同步
线程的同步是为了防止多个线程访问一个数据对象时,对数据造成的破坏。
线程安全
关于线程安全, 有一个经典的问题—银行取钱的问题。 银行取钱的基本流程基本上可以分为如下几个步骤:
- 用户输入账户、 密码, 系统判断用户的账户、 密码是否匹配。
- 用户输入取款金额。
- 系统判断账户余额是否大于取款金额 。
- 如果余额大于取款金额, 则取款成功;如果余额小于取款金额, 则取款失败。
此处忽略检查账户和密码的操作, 仅仅模拟后面三步操作。 下面先定义一个账户类, 该账户类封装了账户编号和余额两个实例变量:
public class Account {
// 封装账户编号、账户余额的两个成员变量
private String accountNo;
private double balance;
public Account(){}
// 构造器
public Account(String accountNo , double balance){
this.accountNo = accountNo;
this.balance = balance;
}
// 此处省略了accountNo和balance的setter和getter方法
……
// 下面两个方法根据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;
}
}
接下来提供一个取钱的线程类, 该线程类根据执行账户、 取钱数量进行取钱操作, 取钱的逻辑是当其余额不足时无法提取现金, 当余额足够时系统吐出钞票, 余额减少:
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()
{
// 账户余额大于取钱数目
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() + "取钱失败!余额不足!");
}
}
}
在程序的主程序中创建一个账户, 并启动两个线程从该账户中取钱:
public class DrawTest
{
public static void main(String[] args)
{
// 创建一个账户
Account acct = new Account("1234567" , 1000);
// 模拟两个线程对同一个账户取钱
new DrawThread("甲" , acct , 800).start();
new DrawThread("乙" , acct , 800).start();
}
}
从结果发现,这样的输出值明显是不合理的。原因是两个线程不加控制的访问Account对象并修改其数据所致。如果要保持结果的合理性,只需要达到一个目的,就是将对Account的访问加以限制,每次只能有一个线程在访问。这样就能保证Account对象中数据的合理性了。
同步代码块
有两种机制防止代码块受并发访问的干扰。Java语言提供一个 synchronized 关键字达到这一目的,并且 Java SE 5.0 引入了 ReentrantLock 类。
Java中每个对象都有一个内置锁。当程序运行到非静态的synchronized同步方法上时,自动获得与正在执行代码类的当前实例(this实例)有关的锁。获得一个对象的锁也称为获取锁、锁定对象、在对象上锁定或在对象上同步。当程序运行到synchronized同步方法或代码块时该对象锁才起作用。
synchronized(obj){
// 此处的代码就是同步代码块
……
}
对于上面的取钱模拟程序, 可以将account作为同步对象, 把程序修改成如下形式:
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() + "取钱失败!余额不足!");
}
}
// 同步代码块结束,该线程释放同步锁
}
}
同一时间只有一个线程能操作account对象,其它线程想要操作account对象,只能等待,直到当前线程释放对account的占用。
同步方法
Java 的多线程安全支持还提供了同步方法, 同步方法就是使用 synchronized 关键字来修饰某个方法, 则该方法称为同步方法。 对于 synchronized 修饰的实例方法( 非 static 方法) 而言, 无须显式指定锁住的对象, 同步方法锁住的对象是this。
通过使用同步方法可以非常方便地实现线程安全的类, 线程安全的类具有如下特征。
- 该类的对象可以被多个线程安全地访问。
- 每个线程调用该对象的任意方法之后都将得到正确结果。
- 每个线程调用该对象的任意方法之后, 该对象状态依然保持合理状态。
Java的一些类是不可变类,例如String,Integer,LocalDate,它们的所有成员变量都是final,它的对象状态不可改变,这些不可变类也是线程安全的。
可变对象需要额外的方法来保证其线程安全。 例如上面的 Account 就是一个可变类, 它的 accountNo 和balance 两个成员变量都可以被改变, 当两个线程同时修改 Account 对象的 balance 成员变量的值时, 程序就出现了异常。
下面将 Account 类对 balance 的访问设置成线程安全的, 那么只要把修改 balance 的方法变成同步方法即可。 程序如下所示:
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;
}
}
Account 类中己经提供了 draw()方法, 而且取消了 setBalance()方法, DrawThread 线程类只需 run()方法只要调用 Account 对象的 dmw()方法即可执行取钱操作:
public void run()
{
// 直接调用account对象的draw方法来执行取钱
// 同步方法的同步监视器是this,this代表调用draw()方法的对象。
// 也就是说:线程进入draw()方法之前,必须先对account对象的加锁。
account.draw(drawAmount);
}
main方法调用:
public class DrawTest
{
public static void main(String[] args)
{
// 创建一个账户
Account acct = new Account("1234567" , 1000);
// 模拟两个线程对同一个账户取钱
new DrawThread("甲" , acct , 800).start();
new DrawThread("乙" , acct , 800).start();
}
}
锁和同步的要点总结:
* 1)、只能同步方法,而不能同步变量和类;
* 2)、每个对象只有一个锁;当提到同步时,应该清楚在什么上同步?也就是说,在哪个对象上同步?
* 3)、不必同步类中所有的方法,类可以同时拥有同步和非同步方法。
* 4)、如果两个线程要执行一个类中的synchronized方法,并且两个线程使用相同的实例来调用方法,那么一次只能有一个线程能够执行方法,另一个需要等待,直到锁被释放。也就是说:如果一个线程在对象上获得一个锁,就没有任何其他线程可以进入(该对象的)类中的任何一个同步方法。
* 5)、如果线程拥有同步和非同步方法,则非同步方法可以被多个线程自由访问而不受锁的限制。
* 6)、线程睡眠时,它所持的任何锁都不会释放。
* 7)、线程可以获得多个锁。比如,在一个对象的同步方法里面调用另外一个对象的同步方法,则获取了两个对象的同步锁。
* 8)、同步损害并发性,应该尽可能缩小同步范围。同步不但可以同步整个方法,还可以同步方法中一部分代码块。
* 9)、在使用同步代码块时候,应该指定在哪个对象上同步,也就是说要获取哪个对象的锁。
同步锁 (Lock)
从 Java 5 开始, Java 提供了一种功能更强大的线程同步机制—通过显式定义同步锁对象来实现同步, 在这种机制下, 同步锁由 Lock 对象充当。
Lock 提供了比 synchronized 方法和 synchronized 代码块更广泛的锁定操作, Lock 允许实现更灵活的结构, 可以具有差别很大的属性, 并且支持多个相关的 Condition 对象。Lock 是控制多个线程对共享资源进行访问的工具。 通常, 锁提供了对共享资源的独占访问, 每次只能有一个线程对 Lock 对象加锁, 线程开始访问共享资源之前应先获得 Lock 对象。
某些锁可能允许对共享资源并发访问, 如 ReadWriteLock (读写锁), Lock、 ReadWriteLock 是 Java 5提供的两个根接口, 并为 Lock 提供了 ReentrantLock ( 可重入锁) 实现类, 为 ReadWriteLock 提供了ReentrantReadWriteLock 实现类。
Java 8 新增了新型的 StampedLock 类, 在大多数场景中它可以替代传统的 ReentrantReadWriteLock。ReentrantReadWriteLock 为读写操作提供了三种锁模式: Writing、 ReadingOptimistic、 Reading。在实现线程安全的控制中, 比较常用的是 ReentrantLock ( 可重入锁)。 使用该 Lock 对象可以显式
地加锁、 释放锁。
通常使用 ReentrantLock 的代码格式如下:
class X{
// 定义锁对象
private final ReentrantLock lock = new ReentrantLock();
// . . .
// 定义需要保证线程安全的方法
public void m(){
// 加 锁
lock.lock();
try{
// 需要保证线程安全的代码
// ... method body
// 使 用 finally 块来保证释放锁
}finally{
lock.unlock();
}
}
}
使用 ReentrantLock 对象来进行同步, 加锁和释放锁出现在不同的作用范围内时, 通常使用finally 块来确保在必要时释放锁。 通过使用 ReentrantLock 对象, 可以把 Account 类改为如下形式, 它依然是线程安全的。
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;
}
}
在Java中,还可使用特殊域变量(volatile)实现线程同步:
a.volatile关键字为域变量的访问提供了一种免锁机制,
b.使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,
c.因此每次使用该域就要重新计算,而不是使用寄存器中的值
d.volatile不会提供任何原子操作,它也不能用来修饰final类型的变量
例如:
class Bank {
//需要同步的变量加上volatile
private volatile int account = 100;
public int getAccount() {
return account;
}
//这里不再需要synchronized
public void save(int money) {
account += money;
}
}
</code>
API:java.concurrent.locks.Lock
API:java.util.concurrent.locks.ReentrantLock
死锁
当两个线程相互等待对方释放同步监视器时就会发生死锁, Java 虚拟机没有监测, 也没有采取措施来处理死锁情况, 所以多线程编程时应该采取措施避免死锁出现。 一旦出现死锁, 整个程序既不会发生任何异常, 也不会给出任何提示, 只是所有线程处于阻塞状态, 无法继续。
下面是一个死锁的例子:
public class DeadlockRisk {
public static void main(String[] args) {
final Hero ahri = new Hero();
ahri.name = "九尾妖狐";
final Hero annie = new Hero();
annie.name = "安妮";
Thread t1 = new Thread(){
public void run(){
//占有九尾妖狐
synchronized (ahri) {
System.out.println("t1 已占有九尾妖狐");
try {
//停顿1000毫秒,另一个线程有足够的时间占有安妮
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("t1 试图占有安妮");
System.out.println("t1 等待中 。。。。");
synchronized (annie) {
System.out.println("do something");
}
}
}
};
t1.start();
Thread t2 = new Thread(){
public void run(){
//占有安妮
synchronized (annie) {
System.out.println("t2 已占有安妮");
try {
//停顿1000毫秒,另一个线程有足够的时间占有暂用九尾妖狐
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("t2 试图占有九尾妖狐");
System.out.println("t2 等待中 。。。。");
synchronized (ahri) {
System.out.println("do something");
}
}
}
};
t2.start();
}
}
实体类Hero,定义了一个攻击方法:
public class Hero {
public String name;
public float hp;
public int damage;
public void attackHero(Hero h) {
try {
//为了表示攻击需要时间,每次攻击暂停1000毫秒
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
h.hp-=damage;
System.out.format("%s 正在攻击 %s, %s的血变成了 %.0f%n",name,h.name,h.name,h.hp);
if(h.isDead())
System.out.println(h.name +"死了!");
}
public boolean isDead() {
return 0>=hp?true:false;
}
}
线程通信
Java 提供了一些机制来保证线程协调运行。
传统的线程通信
传统的线程通信主要由java.lang.Object的三个方法来实现:
- void notify():唤醒在此对象监视器上等待的单个线程。
- void notifyAll():唤醒在此对象监视器上等待的所有线程。
- void wait():导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法。
下面的程序实现如下功能:
系统中有两个线程, 这两个线程分别代表存款者和取钱者—现在假设系统有一种特殊的要求, 系统要求存款者和取钱者不断地重复存款、 取钱的动作, 而且要求每当存款者将钱存入指定账户后, 取钱者就立即取出该笔钱。 不允许存款者连续两次存钱, 也不允许取钱者连续两次取钱。
实体类:定义了取钱、存钱方法:
public class Account
{
// 封装账户编号、账户余额的两个成员变量
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 synchronized void draw(double drawAmount)
{
try
{
// 如果flag为假,表明账户中还没有人存钱进去,取钱方法阻塞
if (!flag)
{
wait();
}
else
{
// 执行取钱
System.out.println(Thread.currentThread().getName()
+ " 取钱:" + drawAmount);
balance -= drawAmount;
System.out.println("账户余额为:" + balance);
// 将标识账户是否已有存款的旗标设为false。
flag = false;
// 唤醒其他线程
notifyAll();
}
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
}
public synchronized void deposit(double depositAmount)
{
try
{
// 如果flag为真,表明账户中已有人存钱进去,则存钱方法阻塞
if (flag) //①
{
wait();
}
else
{
// 执行存款
System.out.println(Thread.currentThread().getName()
+ " 存款:" + depositAmount);
balance += depositAmount;
System.out.println("账户余额为:" + balance);
// 将表示账户是否已有存款的旗标设为true
flag = true;
// 唤醒其他线程
notifyAll();
}
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
}
// 下面两个方法根据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;
}
}
取钱线程类:
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;
}
// 重复100次执行取钱操作
public void run()
{
for (int i = 0 ; i < 100 ; i++ )
{
account.draw(drawAmount);
}
}
}
存钱线程类:
public class DepositThread extends Thread
{
// 模拟用户账户
private Account account;
// 当前取钱线程所希望存款的钱数
private double depositAmount;
public DepositThread(String name , Account account
, double depositAmount)
{
super(name);
this.account = account;
this.depositAmount = depositAmount;
}
// 重复100次执行存款操作
public void run()
{
for (int i = 0 ; i < 100 ; i++ )
{
account.deposit(depositAmount);
}
}
}
主程序可以启动任意多个存款线程和取钱线程, 可以看到所有的取钱线程必须等存款线程存钱后才可以向下执行, 而存款线程也必须等取钱线程取钱后才可以向下执行:
public class DrawTest
{
public static void main(String[] args)
{
// 创建一个账户
Account acct = new Account("1234567" , 0);
new DrawThread("取钱者" , acct , 800).start();
new DepositThread("存款者甲" , acct , 800).start();
new DepositThread("存款者乙" , acct , 800).start();
new DepositThread("存款者丙" , acct , 800).start();
}
}
3 个存款者线程随机地向账户中存款, 只有 1 个取钱者线程执行取钱操作。 只有当取钱者取钱后, 存款者才可以存款; 同理, 只有等存款者存款后,取钱者线程才可以取钱。
程序最后被阻塞无法继续向下执行, 这是因为 3 个存款者线程共有 300 次尝试存款操作, 但 1 个取钱者线程只有 100 次尝试取钱操作, 所以程序最后被阻塞!
使用 Condition 控制线程通信
如果程序不使用 synchronized 关键字来保证同步, 而是直接使用 Lock 对象来保证同步, 则系统中不存在隐式的同步对象, 也就不能使用 wait()、 notify()、 notifyAll()方法进行线程通信了。
当使用 Lock 对象来保证同步时, Java 提供了一个 Condition 类来保持协调, 使用 Condition 可以让那些己经得到 Lock 对象却无法继续执行的线程释放 Lock 对象, Condition 对象也可以唤醒其他处于等待的线程。
Condition 将同步监视器方法( wait()、 notify() 和 notifyAll()) 分解成截然不同的对象, 以便通过将这些对象与 Lock 对象组合使用, 为每个对象提供多个等待集( wait-set )。 在这种情况下, Lock 替代了同步方法或同步代码块, Condition 替代了同步对象。
Condition 实例被绑定在一个 Lock 对象上。 要获得特定 Lock 实例的 Condition 实例, 调用 Lock 对象的 newCondition()方法即可。 Condition 类提供了如下三个方法。
- await(): 类似于隐式同步监视器上的 wait()方法, 导致当前线程等待, 直到其他线程调用该 Condition的 signal()方法或 signalAll()方法来唤醒该线程。 await()方法有更多变体, 如 long awaitNanos(longnanosTimeout)>void awaitUninterruptibly()、 awaitUntil(Date deadline)等, 可以完成更丰富的等待操作。
- signal(): 唤醒在此 Lock 对象上等待的单个线程。 如果所有线程都在该 Lock 对象上等待, 则会选择唤醒其中一个线程。 选择是任意性的。 只有当前线程放弃对该 Lock 对象的锁定后( 使用await()方法), 才可以执行被唤醒的线程。
- signalAll():唤醒在此 Lock 对象上等待的所有线程。只有当前线程放弃对该 Lock 对象的锁定后,才可以执行被唤醒的线程。
下面程序中 Account 使用 Lock 对象来控制同步, 并使用 Condition 对象来控制线程的协调运行。
import java.util.concurrent.*;
import java.util.concurrent.locks.*;
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;
}
}
Concurrent集合
Java 5 提供了一个 BlockingQueue 接口, 虽然 BlockingQueue 也是 Queue 的子接口, 但它的主要用途并不是作为容器, 而是作为线程同步的工具。 BlockingQueue 具有一个特征: 当生产者线程试图向BlockingQueue 中放入元素时, 如果该队列己满, 则该线程被阻塞; 当消费者线程试图从BlockingQueue中取出元素时, 如果该队列己空, 则该线程被阻塞。
BlockingQueue 提供如下两个支持阻塞的方法:
- put(E e): 尝试把 E 元素放入 BlockingQueue 中, 如果该队列的元素己满, 则阻塞该线程。
- take(): 尝试从 BlockingQueue 的头部取出元素, 如果该队列的元素已空, 则阻塞该线程。
BlockingQueue 与其实现类之间的类图如图所示:
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/af2cc0d86c9ca30a644e8b50850dfae3.png#pic_center)
ArrayBlockingQueue 类使用实例:
public class BlockingQueueTest
{
public static void main(String[] args)
throws Exception
{
// 定义一个长度为2的阻塞队列
BlockingQueue<String> bq = new ArrayBlockingQueue<>(2);
bq.put("Java"); // 与bq.add("Java"、bq.offer("Java")相同
bq.put("Java"); // 与bq.add("Java"、bq.offer("Java")相同
bq.put("Java"); // ① 阻塞线程。
}
}
除了BlockingQueue外,针对List、Map、Set、Deque等,java.util.concurrent包也提供了对应的并发集合类:
interface | non-thread-safe | thread-safe |
---|---|---|
List | ArrayList | CopyOnWriteArrayList |
Map | HashMap | ConcurrentHashMap |
Set | HashSet / TreeSet | CopyOnWriteArraySet |
Queue | ArrayDeque / LinkedList | ArrayBlockingQueue / LinkedBlockingQueue |
Deque | ArrayDeque / LinkedList | LinkedBlockingDeque |
API:java.util.concurrent.BlockingQueue
API:java.util.concurrent.ArrayBlockingQueue
线程池
Java语言虽然内置了多线程支持,启动一个新线程非常方便,但是,创建线程需要操作系统资源(线程资源,栈空间等),频繁创建和销毁大量线程需要消耗大量时间。
线程池
Java提供了线程池可以很好地提高性能, 尤其是当程序中需要创建大量生存期很短暂的线程时, 更应该考虑使用线程池。
与数据库连接池类似的是, 线程池在系统启动时即创建大量空闲的线程, 程序将一个 Runnable 对象或 Callable 对象传给线程池, 线程池就会启动一个空闲的线程来执行它们的 run()或 call()方法, 当 run()或 call()方法执行结束后, 该线程并不会死亡, 而是再次返回线程池中成为空闲状态, 等待执行下一个Runnable 对象的 run()或 call()方法。
Java标准库提供的线程池类ThreadPoolExecutor:
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class TestThread {
public static void main(String[] args) throws InterruptedException {
/*
第一个参数10 表示这个线程池初始化了10个线程在里面工作
第二个参数15 表示如果10个线程不够用了,就会自动增加到最多15个线程
第三个参数60 结合第四个参数TimeUnit.SECONDS,表示经过60秒,多出来的线 程还没有接到活儿,就会回收,最后保持池子里就10个
第四个参数TimeUnit.SECONDS 如上
第五个参数 new LinkedBlockingQueue() 用来放任务的集合
*/
ThreadPoolExecutor threadPool= new ThreadPoolExecutor(10, 15, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
threadPool.execute(new Runnable(){
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("任务1");
}
});
}
}
Java 8 改进的线程池
在 Java 5 以前, 开发者必须手动实现自己的线程池; 从 Java 5 开始, Java 内建支持线程池。 Java 5新增了一个 Executors 工厂类来产生线程池, 该工厂类包含如下几个静态工厂方法来创建线程池:
- ExecutorService newCachedThreadPool(): 创建一个具有缓存功能的线程池, 系统根据需要创建线程, 这些线程将会被缓存在线程池中。
- ExecutorService newFixedThreadPool(int nThreads): 创建一个可重用的、 具有固定线程数的线程池。
- ExecutorService newSingleThreadExecutor(): 创建一个只有单线程的线程池, 它相当于调用 newFixedThread Pool()方法时传入参数为 1。
- ScheduledExecutorService newScheduledThreadPool(int corePoolSize): 创建具有指定线程数的线程池, 它可以在指定延迟后执行线程任务。 corePoolSize 指池中所保存的线程数, 即使线程是空闲的也被保存在线程池内。
- ScheduledExecutorService newSingleThreadScheduledExecutor(): 创建只有一个线程的线程池, 它可以在指定延迟后执行线程任务。
- ExecutorService newWorkStealingPool(int parallelism): 创建持有足够的线程的线程池来支持给定的并行级别, 该方法还会使用多个队列来减少竞争。
- ExecutorService newWorkStealingPool(): 该方法是前一个方法的简化版本。 如果当前机器有 4 个CPU, 则目标并行级别被设置为 4, 也就是相当于为前一个方法传入 4 作为参数。
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): 创建并执行一个在给定初始延迟后首次启用的定期操作, 随后在每一次执行终止和下一次执行开始之间都存在给定的延迟。 如果任务在任一次执行时遇到异常, 就会取消后续执行; 否则, 只能通过程序来显式取消或终止该任务。
用完一个线程池后, 应该调用该线程池的 shutdownO方法, 该方法将启动线程池的关闭序列。
使用线程池来执行线程任务的步骤如下:
- 调用 Executors 类的静态工厂方法创建一个 ExecutorService 对象, 该对象代表一个线程池。
- 创建 Runnable 实现类或 Callable 实现类的实例, 作为线程执行任务。
- 调用 ExecutorService 对象的 submit()方法来提交 Runnable 实例或 Callable实例。
- 当不想提交任何任务时, 调用 ExecutorService 对象的 shutdown()方法来关闭线程池。
下面程序使用线程池来执行指定 Runnable 对象所代表的任务:
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();
}
}
API:java.util.concurrent.Executors
API:java.util.concurrent.ExecutorService
API:java.util.concurrent.ScheduledExecutorService
Java 8 增强的 ForkJoinPool
为了充分利用多 CPU、 多核 CPU 的性能优势, 计算机软件系统应该可以充分“ 挖掘” 每个 CPU的计算能力, 绝不能让某个 CPU 处于“ 空闲” 状态。 为了充分利用多 CPU、 多核 CPU 的优势, 可以考虑把一个任务拆分成多个“ 小任务”, 把多个“ 小任务” 放到多个处理器核心上并行执行; 当多个“ 小任务” 执行完成之后, 再将这些执行结果合并起来即可。
Java 7 提供了 ForkJoinPool 来支持将一个任务拆分成多个“ 小任务” 并行计算, 再把多个“ 小任务”的结果合并成总的计算结果。 ForkJoinPool 是ExecutorService 的实现类, 因此是一种特殊的线程池。
ForkJoinPool 提供了如下两个常用的构造器:
- ForkJoinPool(int parallelism): 创建一个包含 parallelism 个并行线程的 ForkJoinPool。
- ForkJoinPool(): 以 Runtime.availableProcessors()方法的返回值作为 parallelism 参数来创建 ForkJoinPooL
Java 8 进一步扩展了 ForkJoinPool 的功能, Java 8 为 ForkJoinPool 增加了通用池功能。 ForkJoinPool类通过如下两个静态方法提供通用池功能。
- ForkJoinPool commonPool(): 该方法返回一个通用池, 通用池的运行状态不会受 shutdown()或shutdownNow()方法的影响。 当然, 如果程序直接执行 System.exit(0);来终止虚拟机, 通用池以及通用池中正在执行的任务都会被自动终止。
- int getCommonPoolParallelism(): 该方法返回通用池的并行级别。
创建了 ForkJoinPool 实例之后, 就可调用 ForkJoinPool 的submit(ForkJoinTask task)或 invoke(ForkJoinTask task)方法来执行指定任务了。 其中 ForkJoinTask 代表一个可以并行、 合并的任务。ForkJoinTask 是一个抽象类, 它还有两个抽象子类:RecursiveAction 和 RecursiveTask。 其中 RecursiveTask代表有返回值的任务, 而 RecursiveAction 代表没有返回值的任务。
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/27459ae14039ee71d68acf8931628b49.png#pic_center)
下面以执行没有返回值的“ 大任务”( 简单地打印 0 300 的数值) 为例, 程序将一个“ 大任务” 拆分成多个“ 小任务”, 并将任务交给 ForkJoinPool 来执行:
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;
import java.util.concurrent.TimeUnit;
//继承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();
}
}
}
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();
}
}
上面定义的任务是一个没有返回值的打印任务, 如果大任务是有返回值的任务, 则可以让任务继承RecursiveTask, 其中泛型参数 T 就代表了该任务的返回值类型。 下面程序示例了使用 Recursive Task对一个长度为 100 的数组的元素值进行累加:
import java.util.Random;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.RecursiveTask;
//继承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(); // ①
}
}
}
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();
}
}
API:java.util.concurrent.ForkJoinPool
API:java.util.concurrent.RecursiveAction
线程相关类
ThreadLocal 类
早在 JDK 1.2 推出之时, Java 就为多线程编程提供了一个 ThreadLocal 类; 从 Java 5.0 以后, Java引入了泛型支持, Java 为该 ThreadLocal 类增加了泛型支持, 即: ThreadLocal 通过使用 ThreadLocal类可以简化多线程编程时的并发访问, 使用这个工具类可以很简捷地隔离多线程程序的竞争资源。
ThreadLocal, 是 Thread Local Variable ( 线程局部变量) 的意思, 线程局部变量 ThreadLocal) 的功用其实非常简单, 就是为每一个使用该变量的线程都提
供一个变量值的副本, 使每一个线程都可以独立地改变自己的副本, 而不会和其他线程的副本冲突。 从线程的角度看, 就好像每一个线程都完全拥有该变量一样。
ThreadLocal 类的用法非常简单, 它只提供了如下三个 public 方法。
- Tget() 返回此线程局部变量中当前线程副本中的值。
- void remove() 删除此线程局部变量中当前线程的值。
- void set(T value) 设置此线程局部变量中当前线程副本中的值。
使用ThreadLocal实例:
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);
}
}
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);
}
}
}
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 ();
}
}
账户名有三个副本, 主线程一个, 另外启动的两个线程各一个, 它们的值互不干扰, 每个线程完全拥有自己的 ThreadLocal 变量, 这就是 ThreadLocal 的用途。
ThreadLocal 并不能替代同步机制, 两者面向的问题领域不同。 同步机制是为了同步多个线程对相同资源的并发访问, 是多个线程之间进行通信的有效方式; 而 ThreadLocal 是为了隔离多个线程的数据共享, 从根本上避免多个线程之间对共享资源( 变量) 的竞争, 也就不需要对多个线程进行同步了。
通常建议: 如果多个线程之间需要共享资源, 以达到线程之间的通信功能, 就使用同步机制; 如果仅仅需要隔离多个线程之间的共享冲突, 则可以使用 ThreadLocal。
原子访问
所谓的原子性操作即不可中断的操作,比如赋值操作:
int i = 5;
原子性操作本身是线程安全的。
相反,例如自增i++这个行为,就不是原子性操作,也不是线程安全的。
i++ 这个行为,事实上是有3个原子性操作组成的。
- 步骤 1. 取 i 的值
- 步骤 2. i + 1
- 步骤 3. 把新的值赋予i
这三个步骤,每一步都是一个原子操作,但是合在一起,就不是原子操作。就不是线程安全的。
如果一个线程在步骤1 取i 的值结束后,还没有来得及进行步骤2,另一个线程也可以取 i的值了。
JDK6 以后,Java标准库新增加了一个包java.util.concurrent.atomic,里面有各种原子类,比如AtomicInteger。而AtomicInteger提供了各种自增,自减等方法,这些方法都是原子性的。 换句话说,自增方法 incrementAndGet 是线程安全的,同一个时间,只有一个线程可以调用这个方法。
AtomicInteger atomicI =new AtomicInteger();
int i = atomicI.decrementAndGet();
分别使用基本变量的非原子性的++运算符和 原子性的AtomicInteger对象的 incrementAndGet 来进行多线程测试:
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicTest {
private static int value = 0;
private static AtomicInteger atomicValue =new AtomicInteger();
public static void main(String[] args) {
int number = 100000;
Thread[] ts1 = new Thread[number];
for (int i = 0; i < number; i++) {
Thread t =new Thread(){
public void run(){
value++;
}
};
t.start();
ts1[i] = t;
}
//等待这些线程全部结束
for (Thread t : ts1) {
try {
t.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.printf("%d个线程进行value++后,value的值变成:%d%n", number,value);
Thread[] ts2 = new Thread[number];
for (int i = 0; i < number; i++) {
Thread t =new Thread(){
public void run(){
atomicValue.incrementAndGet();
}
};
t.start();
ts2[i] = t;
}
//等待这些线程全部结束
for (Thread t : ts2) {
try {
t.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.printf("%d个线程进行atomicValue.incrementAndGet();后,atomicValue的值变成:%d%n", number,atomicValue.intValue());
}
}
API:java.util.concurrent.atomic
API:java.util.concurrent.atomic.AtomicInteger
思维导图总结
Java Review系列目录 ⇐⇐Java Review(三十六、IO) Java Review(三十八、网络编程)⇒⇒ |
参考:
【1】:《疯狂Java讲义》
【2】:阮一峰的网络日志:进程与线程的一个简单解释
【3】:Java多线程编程总结
【4】:Java多线程(四)—–线程状态的转换
【6】:Java多线程(五)—–线程的同步与锁
【7】《Java核心技术 卷一》
【8】:廖雪峰的官方网站:同步方法
【9】:廖雪峰的官方网站:使用ReentrantLock
【10】:多线程系列教材 (五)- Java 演示多线程死锁
【11】:java笔记–关于线程同步(7种同步方式)
【12】:Java多线程(六)—–线程的交互
【13】:廖雪峰的官方网站:使用Concurrent集合
【14】:多线程系列教材 (六)- Java 线程之间的交互 wait和notify
【15】:廖雪峰的官方网站:使用线程池
【16】:廖雪峰的官方网站:使用ForkJoin
【17】:廖雪峰的官方网站:使用ThreadLocal
【18】:多线程系列教材 (九)- 原子访问
【19】:易百教程:Java并发AtomicLong接口
【30】:微信公众号:Java思维导图