文章目录
- 一、进程与线程
- 二、并发和并行
- 三、线程的创建方式
- 四、线程的操作
- 五、多线程的优势与存在的风险
- 六、线程的原子性、可见性、有序性
- 七、轻量级同步机制:volatile关键字
- 八、ThreadLocal
- 九、Lock显示锁
- 十、读写锁ReentrantReadWriteLock
- 十一、线程管理
- 十二、ForkJoinPool线程池
- 十三:保障线程安全的设计技术
一、进程与线程
1. 进程
- 狭义定义:进程是正在运行程序的实例。
- 广义定义:进程是一个具有独立功能的程序关于某种数据集合的一次运行活动,它是操作系统动态执行的基本单位。
- 进程是用来加载指令、管理内存、管理IO的。
- 当一个程序被运行时,从磁盘加载这个程序的代码到内存,这时就开启了一个进程。
- 进程可以看作一个程序的实例。可以运行多个进程(如:记事本),有的程序只能启动一个实例进程(如:网易云音乐)。
2. 线程
- 一个进程内可以有一到多个线程。
- 一个线程就是一个指令流,将指令流的一条条指令以一定顺序交给cpu执行。
- java中,线程作为最小的调度单位,进程作为资源分配的最小单位。
3. 二者对比
- 线程存在于进程中,是进程的一个子集。
- 进程拥有共享的资源,如内存空间,供其内存的线程共享。
- 进程间通信较为复杂:
- 同一台计算机的进程通信称为IPC(Inter-process communication)。
- 不同计算机之间的进程通信,需要通过网络,并遵循相同的协议,如HTTP。
- 线程通信相对简单,因为它共享进程内的内存,多线程可以共享同一个共享变量。
- 线程更轻量,线程上下文切换成本一般比进程上下文切换低。
二、并发和并行
1. 并发
- 单核cpu下,线程实际还是串行执行的。操作系统有一个组件叫做任务调度器,将CPU的时间片(windows下时间片最小约为15毫秒)分给不同的线程使用,只是由于线程间的切换非常快,我们感觉是在同时运行。
- 总结:微观串行,宏观并行。
- 线程轮流使用cpu的做法称为并发(concurrent)。
- 并发是同一时间应对多件事情的能力。
2. 并行
- 多核cpu下,每个核都可以调度运行线程,并行(parallel)的。
- 并行是同一时间动手做多件事情的能力。
3. 异步调用
- 从方法调用的角度:
- 需要等待结果返回,才能继续运行就是同步。(同步在多线程调用中还有另外一层意思,就是让多个线程步调一致)
- 不需要等待结果返回,就能继续运行就是异步。
- 多线程可以让方法执行变成异步的。
- 比如在项目中,视频文件的格式转化操作比较耗时,可以新开一个线程处理视频转化,避免主线程阻塞。tomcat中异步servlet也是类似的,让用户线程处理耗时较长的操作,避免阻塞tomcat的工作线程。
三、线程的创建方式
第一种:继承Thread类
class MyThread extends Thread{
@Override
public void run() {
// 打印偶数
for(int i = 0; i <= 100; i++) {
if(i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
public class T1{
public static void main(String[] args) {
// 创建一个线程
MyThread m = new MyThread();
// 设置线程的名字
m.setName("m1");
// 启动线程
m.start();
// 下面的是主线程执行的
// 打印奇数
for(int i = 0; i < 100; i++) {
if(i % 2 != 0){
System.out.println(Thread.currentThread().getName() + ":" + i + "main");
}
}
}
}
使用匿名内部类·`
public class ThreadDemo {
public static void main(String[] args) {
// 使用匿名内部类
new Thread() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
}.start();
new Thread() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
}.start();
}
}
-
start()方法:
- 启动当前线程。
- 调用当前线程的run()方法。
-
同一个线程不能start()两次。
-
实现3个窗口卖票:有线程安全问题。
/**
* 实现三个窗口卖票
*/
class Window extends Thread {
private static int ticket = 100;
@Override
public void run() {
while(true) {
if(ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":卖票,票号为" + ticket);
ticket--;
}else{
break;
}
}
}
}
public class WindowTest {
public static void main(String[] args) {
Window w1 = new Window();
Window w2 = new Window();
Window w3 = new Window();
w1.setName("窗口1");
w2.setName("窗口2");
w3.setName("窗口3");
w1.start();
w2.start();
w3.start();
}
}
第二种:实现Runnable接口
class MyThread1 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
public class RunnableTest {
public static void main(String[] args) {
MyThread1 myThread = new MyThread1();
Thread thread1 = new Thread(myThread);
Thread thread2 = new Thread(myThread);
thread1.setName("t1");
thread2.setName("t2");
thread1.start();
thread2.start();
}
}
实现多窗口卖票
/**
* 使用Runnable接口:实现多窗口卖票
*/
class Window1 implements Runnable {
private int ticket = 100;
@Override
public void run() {
while(true) {
if(ticket > 0) {
System.out.println(Thread.currentThread().getName() + "卖出当前票" + ticket);
ticket--;
}else {
break;
}
}
}
}
public class WindowRunnable {
public static void main(String[] args) {
Window1 w1 = new Window1();
// 新建多个窗口
Thread t1 = new Thread(w1,"t1");
Thread t2 = new Thread(w1,"t2");
Thread t3 = new Thread(w1,"t3");
t1.start();
t2.start();
t3.start();
}
}
第三种:实现Callable接口(jdk1.5)
- 实现Callable接口需要重写call()方法。
- call()方法与run()方法相比,可以有返回值。
- 方法可以抛出异常。
- 支持泛型的返回值。
- 需要借助FutureTask类,比如获取返回结果。
- Future接口:
- 可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
- FutureTask是Future接口的唯一实现类。
- FutureTask同时实现了Runnable、Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。
/**
* 实现Callable接口创建线程
*/
class NewThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
// 计算100以内数的和,并返回结果
for (int i = 1; i <= 100; i++) {
sum += i;
}
return sum;
}
}
public class CallableTest {
public static void main(String[] args) {
// 新建一个线程
NewThread newThread = new NewThread();
// 需要借助FutureTask类,接收Callable线程的返回值
FutureTask<Integer> task = new FutureTask<>(newThread);
// 启动线程还是需要Thread类
// FutureTask类实现了Runnable接口,作为Thread类的参数传入
new Thread(task).start();
try {
// 接受call方法执行的返回值
Object sum = task.get();
System.out.println(sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
第四种:使用线程池(jdk1.5)
- 背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
- 思路:提前创建好多个线程,放到线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。
- 好处:
- 提高响应速度(减少创建新线程的时间)。
- 降低资源消耗(重复利用线程池中的线程,不需要每次创建)。
- 便于线程管理。
corePoolSize
:核心池的大小。maxinumPoolSize
:最大线程数keepAliveTime
:线程没有任务时最多保持多长时间后会终止。
/**
* 使用线程池
*/
class Run implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
}
class Call implements Callable{
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 0; i < 100; i++) {
if(i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + " " + i);
sum += i;
}
}
return sum;
}
}
public class ThreadPool {
public static void main(String[] args) {
// 创建一个指定数量线程的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
// 适用于Runnable接口,没有返回值
service.execute(new Run());
try {
// 创建一个实现Callable接口的类
Call call = new Call();
// 适用于Callable接口,接收返回值
Future submit = service.submit(call);
Object sum = submit.get();
System.out.println(sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
// 线程池不用了,可以关闭线程池
//service.shutdown();
}
}
- 设置线程池的一些参数
// 设置线程池的一些参数
// ExecutorService是一个接口,提供的参数较少
// 需要转化为它的实现类
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
service1.setCorePoolSize(15);
service1.setKeepAliveTime();
service1.setMaximumPoolSize();
四、线程的操作
1. Thread类的常用方法:
start()
:启动当前线程,调用当前线程的run()方法。run()
:创建线程需要执行的操作。currentThread()
:静态方法,返回当前执行的线程。getName()
:获取当前线程的名字。setName()
:设置当前线程的名字。yield()
:释放当前线程cpu的执行权。有可能重新抢回执行权。join()
:线程a调用线程b的join()方法,此时线程a就进入阻塞状态,等待线程b执行完,线程a才结束阻塞状态。
stop()
:强制线程生命周期结束,不推荐使用。(过期:使用stop()方法释放锁会给数据造成不一致的结果)sleep(millitime):
让线程阻塞指定的millitime毫秒数。isAlive()
:判断当前线程是否存活。
class Hello extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 == 0) {
try {
// 让线程阻塞
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " " + i);
}
// 如果取模为0,就释放当前线程对cpu的执行权。有可能抢回执行权。
if(i % 20 == 0) {
yield();
}
}
}
}
public class ThreadMethodTest {
public static void main(String[] args) {
Hello hello = new Hello();
hello.setName("h1");
hello.start();
// 给主线程命名
Thread.currentThread().setName("main线程");
for (int i = 0; i < 100; i++) {
if(i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
if(i == 20) {
try {
// 调用hello线程的join()方法,主线程就进入等待,等待线程hello运行结束
hello.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 判断hello线程是否存活
System.out.println(hello.isAlive());
}
}
suspend()
:暂停线程。过期,不推荐使用。 (使用suspend()方法暂停线程使用不当,极易造成公共的同步对象的独占,使得其他线程无法访问公共同步资源)
/**
* 测试suspend方法:过期原因
* 暂停线程独占对象锁的问题
*/
class SynchronizedObj{
synchronized public void printString() {
System.out.println("printString---begin");
if(Thread.currentThread().getName() == "a") {
System.out.println("a线程永远suspend(),暂停了");
// 暂停后就会永久的持当前方法的一把锁,不能释放,其他方法无法进入
Thread.currentThread().suspend();
}
System.out.println("printString---end");
}
}
public class SuspendTest {
public static void main(String[] args) {
try {
SynchronizedObj obj = new SynchronizedObj();
// 创建一个a线程
Thread a = new Thread() {
@Override
public void run() {
// 调用printString方法
obj.printString();
}
};
// 设置a线程名字
a.setName("a");
a.start();
// 让主线程睡眠1秒
Thread.sleep(1000);
// 创建一个b线程
Thread b = new Thread() {
@Override
public void run() {
System.out.println("b线程启动了,但是进入不了printString()方法!");
System.out.println("因为printString()方法被a线程锁定,并且永久suspend()暂停了");
// 调用printString方法,下面的不会执行
obj.printString();
}
};
// 设置b线程名字
b.setName("b");
b.start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
resume()
:使暂停的线程从新就绪。过期,不推荐使用。
2. 线程的优先级设置
1. 线程的调度
- 时间片,抢占式:高优先级的线程抢占CPU。
- java的调度方法:同优先级线程组成先进先出队列,使用时间片策略。对高优先级,使用优先调度的抢占式策略。线程的优先级具有继承性。
- 线程的优先级等级:
- MAX_PRIORITY:10
- MIN_PRIORITY:1
- NORM_PRIORITY:5(默认)
getPriority()
:返回线程优先级。setpriority(int newPriority)
:设置线程的优先级。- 说明:高优先级的线程会抢占低优先级的线程的cpu执行权,但是不意味着高优先级线程执行完了,低优先级线程才执行。
// 设置线程的优先级
hello.setPriority(Thread.MAX_PRIORITY);
// 获取线程的优先级:默认为5
System.out.println(Thread.currentThread().getPriority());
3. 线程的生命周期
- Thread的内部类State定义了线程的几种状态:
- 新建
- 就绪
- 运行
- 阻塞
- 死亡
4. 线程的同步
一、synchronized同步代码块
synchronized(对象) {}
同步代码块- 在继承Thread的线程中,慎重使用this当同步锁。可能会创建多个线程对象,this对象就不唯一。
- 在实现Runnable接口的线程中,可以考虑使用this充当同步锁。
二、synchronized同步方法
public synchronized void method() {}
实现Runnable接口时使用同步方法,同步方法的同步锁就是this。public static synchronized method() {}
继承Thread类使用同步方法,需要加static关键字,不然每个线程会创建一个对象的同步锁,不能达到同步的目的。加上static关键字,此时的同步锁是:类名.class。
三、Lock锁同步(jdk1.5)
- 通过显示定义同步锁对象来进行同步。同步锁使用Lock对象充当。
Lock
接口是控制多线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。- 常用
ReentrantLock
类,实现了Lock接口。它拥有与synchronized相同的并发性和内存语义,它可以显示的加锁lock()
,释放锁unlock()
。
/**
* 使用Lock锁解决线程安全问题
*/
class Window4 implements Runnable {
private int ticket = 100;
// 采用Lock方式进行同步
// 新建一个锁对象,Lock是一个接口,ReentrantLock是Lock的实现类
ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
// 使用lock()方法进行同步
lock.lock();
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": 当前票号" + ticket);
ticket--;
} else {
break;
}
} finally {
// 此处使用try{}finally{}只是为了在finally中进行解锁,不然程序不会停止
// 使用unlock()方法进行解锁
lock.unlock();
}
}
}
}
public class LockTest {
public static void main(String[] args) {
Window4 window4 = new Window4();
// 新建三个线程
Thread t1 = new Thread(window4, "窗口1");
Thread t2 = new Thread(window4, "窗口2");
Thread t3 = new Thread(window4, "窗口3");
t1.start();
t2.start();
t3.start();
}
}
四、synchronized与Lock锁的对比
- Lock是显示锁(手动开启锁,别忘记关锁),synchronized是隐式锁,出了作用域自动释放。
- Lock只有代码块锁,synchronized有代码块锁和方法锁。
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性。(提供更多的子类)
- 建议优先使用顺序:Lock锁,synchronized同步代码块,同步方法。
5. 懒汉式改成线程安全
/**
* 将单例中的懒汉式改成线程安全的
*/
class Bank{
private static Bank bank = null;
private Bank() {
}
public static Bank getBank() {
// 加同步锁,效率较差,每个线程都要进去判断一次
// synchronized(Bank.class) {
// if(bank == null)
// bank = new Bank();
//
// return bank;
// }
// 效率较高,后面来的线程就直接判断不为null
if(bank == null) {
synchronized(Bank.class) {
if(bank == null) {
bank = new Bank();
}
}
}
return bank;
}
}
public class BankTest {
}
6. 线程的死锁
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
- 出现死锁不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态。
- 代码演示:
/**
* 线程死锁问题
* 1. 主线程执行A对象的foo方法,拿到了对象A的锁,然后进入睡眠,再执行b.last()方法,由于last方法是
* synchronized线程安全的,所以需要拿到对象B的锁。
* 2. 分线程执行B对象的bar方法,拿到了对象B的锁,然后进入睡眠,再执行a.last()方法,由于last方法是
* synchronized线程安全的,所以需要拿到对象A的锁。
* 3. 两个线程都互相握住锁,等待对方先释放锁,于是形成了死锁。
*/
class A {
public synchronized void foo(B b) { // 同步锁是A对象
System.out.println(Thread.currentThread().getName() + " A对象的foo方法执行");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
b.last();
}
public synchronized void last() {
System.out.println("A对象的last方法执行!");
}
}
class B {
public synchronized void bar(A a) { // 同步锁是B对象
System.out.println(Thread.currentThread().getName() + " B类的bar方法执行");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
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("main线程");
// 调用a对象的方法
a.foo(b);
System.out.println("main线程结束");
}
@Override
public void run() {
// 设置分线程名字
Thread.currentThread().setName("分线程");
// 调用b对象的方法
b.bar(a);
System.out.println("分线程结束");
}
public static void main(String[] args) {
DeadLock lock = new DeadLock();
// 启动分线程
new Thread(lock).start();
// 执行主线程的方法
lock.init();
}
}
7. 线程的通信
7.1 wait()、notify()
-
wait()
方法:当前线程进入阻塞状态,并释放同步监视器。只能在同步代码块中被由锁对象调用。 -
notify()
方法:执行此方法会唤醒一个wait的线程。如果有多个线程被wait,就唤醒优先级高的那个。也必须在同步代码块中由锁对象调用。 -
notifyAll()
方法:会唤醒所有被wait的方法。 -
这三个方法都是Object的方法。
-
这三个方法必须使用在同步代码块或同步方法中,Lock都不行。(Lock有其他线程通信的方法)
-
这三个方法的调用者必须是synchronized的同步锁对象,不然会报错IllegalMonitorStateException。
-
测试代码
/**
* 测试wait()和notify()方法
*/
public class WaitNotifyTest {
public static void main(String[] args) {
// 定义一个锁对象
String str = "hello";
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (str) {
System.out.println("线程1执行___");
try {
// 阻塞
str.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1执行完成___");
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (str) {
System.out.println("线程2唤醒线程1之前-----");
// 唤醒该锁对象的线程
str.notify();
System.out.println("唤醒成功-----");
}
}
});
t1.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
- 运行结果:
7.2 notify()不会立即释放同步锁
- wait()会释放锁对象,等待notify()对阻塞的线程进行唤醒。
- 但是调用notify()之后,线程不会立即释放锁对象,要等待synchronized中的代码执行完毕后,才释放锁对象。
- 代码演示:
/**
* 测试调用notify方法后,线程不会立即释放同步锁
*/
public class NotifyTest {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
// 新建一个线程
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// 使用list作为锁对象
synchronized(list) {
System.out.println("线程1开始执行!");
// 判断list中的元素是否为5个
if(list.size() != 5) {
try {
System.out.println("线程1开始睡眠------");
// 不等于5就阻塞
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程1被唤醒,执行完毕!");
}
}
});
// 再创建一个线程
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized(list) {
System.out.println("线程2开始执行======");
// 先集合list中添加元素
for (int i = 0; i < 10; i++) {
list.add("data--" + i);
System.out.println("线程2添加了第" + (i+1) + "个元素!");
if(list.size() == 5) {
// 唤醒线程1
list.notify();
System.out.println("线程2发起唤醒通知");
}
}
System.out.println("线程2执行结束======");
}
}
});
t1.start();
// 为了确保线程1先拿到list锁,主线程睡眠500毫秒
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
- 运行结果:
- 结论:调用notify()唤醒其他线程后,该线程不会立即释放锁对象,需要等待synchronized同步代码块执行完毕后,才释放锁对象。
7.3 interrupt()会中断线程的wait()等待
interrupt()
方法作用是中断线程。- interrupt()只会改变中断状态,不会中断一个正在运行的线程。
- 如果正常运行的线程,interrupt中断后,isInterrupted置为true,如果在sleep()或wait()、join()时被中断会抛出异常,但是isInterrupted还是false。
- 一旦线程处于中断状态,就会抛出一个中断异常
InterruptedException
。 - 如果线程被
Object.wait()
、Thread.join()
、Thread.sleep()
这三个方法所阻塞,此时调用该线程的interrupt()方法,该线程就会提早终结被阻塞状态,抛出一个InterruptedException异常。 - 如果线程没有被阻塞,调用interrupt()方法将不起作用。直到执行到wait()、join()、sleep()时,会马上抛出InterruptedException异常。
- 代码测试:
/**
* 测试interrupt()方法,中断wait的阻塞状态
*/
public class InterruptTest {
// 定义一个对象锁
private static final Object obj = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized(obj) {
try {
System.out.println("线程1阻塞之前代码----");
obj.wait();
System.out.println("线程1唤醒之后代码----");
} catch (InterruptedException e) {
System.out.println("线程1被中断了======");
}
}
}
});
t1.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 调用线程的中断方法
t1.interrupt();
}
}
- 运行结果:
7.4 wait(long)
wait(long)
线程进入阻塞,在指定的时间内没有被唤醒,将自动唤醒。
7.5 解决notify通知过早
- 如果线程2唤醒线程1时,先调用了notify()方法,线程1才进入wait阻塞状态,那么就可能导致线程1一直处于wait阻塞状态。
- 代码演示:定义一个标志isFirst,判断是否是第一个运行的线程,如果线程1先运行,那么就wait阻塞,如果线程2先运行,线程1就不阻塞。
/**
* 如果线程还没有进入wait阻塞状态,另一个线程就调用了notify(),唤醒线程,那么就可能一直处于阻塞状态
* 可以定义一个标识值:如果线程1先进入阻塞状态,线程2就唤醒,如果线程2先执行,那么线程1就不进入阻塞状态
*/
public class NotifyTest2 {
// 定义一个对象锁
private static final Object obj = new Object();
// 定义一个标志值:判断是不是第一个执行的线程
private static boolean isFirst = true;
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized(obj) {
// 判断是不是第一个启动的线程,是就进入阻塞状态,不是就不阻塞
while (isFirst) {
try {
System.out.println("线程1开始等待");
obj.wait();
System.out.println("线程1等待结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized(obj) {
System.out.println("线程2唤醒线程1之前");
obj.notify();
System.out.println("唤醒线程1之后");
// 如果线程2先启动,线程1就不用启动了,不然线程1会处于一直等待状态
isFirst = false;
}
}
});
// 调用线程的start()方法,线程进入就绪态,等待CPU进行执行,但是不一定先进入就绪态就先运行
// 一般情况下t1线程执行在t2之前
// t1.start();
// t2.start();
// 一般情况下t2在t1之前启动
t2.start();
t1.start();
}
}
-
运行结果:
线程2先运行
线程1先运行
-
补充:线程调用start()方法后,进入就绪状态,获得CPU执行权就进入运行状态。但是不一定先进入就绪状态,就一定先运行。
7.6 通过管道流实现线程间的通信
- 在java.io包中的
PipStream
管道流用于在线程之间传递数据。一个线程发送数据到输出管道,另一个线程从输入管道读取数据。 - 相关类:
PipedInputStream
、PipedOutputStream
、PipedReader
、PipedWriter
。 - 代码演示:
/**
* 通过管道流,实现线程之间的通信
*/
public class PipedStreamTest {
public static void main(String[] args) throws IOException {
// 定义管道字节流
PipedInputStream inputStream = new PipedInputStream();
PipedOutputStream outputStream = new PipedOutputStream();
// 在管道输入输出流之间建立连接
inputStream.connect(outputStream);
// 创建线程写入数据
new Thread(new Runnable() {
@Override
public void run() {
// 写数据
writeData(outputStream);
}
}).start();
// 创建线程读出数据
new Thread(new Runnable() {
@Override
public void run() {
readData(inputStream);
}
}).start();
}
// 写入管道中的方法
private static void writeData(OutputStream out) {
try {
// 写入数据
out.write("hello".getBytes());
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 从管道中读取数据
private static void readData(InputStream input) {
try {
byte[] buffer = new byte[1024];
int len;
while((len = input.read(buffer)) != -1) {
System.out.println(new String(buffer, 0, len));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
input.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
8. 面试题:sleep()和wait()的异同?
1. 相同点:
- sleep()和wait()都能让线程进入阻塞状态。
2. 不同点:
- sleep()指定阻塞的时间,时间到后线程自动进入就绪状态。
- wait()阻塞后,不能自己进入就绪状态,需要其他线程调用notify()或notifyAll()方法,才能结束阻塞,进入就绪状态。
- sleep()在synchronized中阻塞时,不会释放同步监视器。
- wait()在阻塞时,会释放同步监视器,其他线程可以进入synchronized同步中。
- sleep()方法在Thread类中。可以在任何需要的场景中调用。
- wait()方法在Object类中,wait()必须使用在synchronized(同步方法或同步代码块)中。
9. 经典例题:生产者与消费者问题
/**
* 生产者与消费者问题
* 生产者将产品交给店员,而消费者从店员处取走产品,店员最多只能持有20个产品,如果生产者还有
* 生产,店员需要叫停,如果店中有空位放新的产品在通知生产者进行生产,如果店中没有产品了,
* 店员会告诉消费者等一下,等店中有产品了在通知消费者来取走产品。
*/
// 创建一个店员类
class Clerk {
private int produceCount = 0;
// 生产产品
public synchronized void produceProduct() {
if(produceCount < 20) {
produceCount++;
System.out.println("生产者" + Thread.currentThread().getName() + "开始生产第" + produceCount);
// 生产了就需要唤醒消费线程进行消费
notify();
}else{
// 如果产品超过20个,就等待一下,等消费了在生产
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 消费产品
public synchronized void customProduct() {
if(produceCount > 0 ) {
System.out.println("消费者" + Thread.currentThread().getName() + "开始消费第" + produceCount);
produceCount--;
// 消费了就需要唤醒生产线程进行生产
notify();
}else{
// 如果产品小于0个,就等待生产了在消费
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 创建一个生产者类
class Producer extends Thread{
private Clerk clerk;
public Producer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(getName() + ":开始生产......");
while(true) {
// 慢点生产
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 生产产品
clerk.produceProduct();
}
}
}
// 创建一个消费者类
class Customer extends Thread{
private Clerk clerk;
public Customer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(getName() + ":开始消费......");
while(true) {
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 消费产品
clerk.customProduct();
}
}
}
public class ProductTest {
public static void main(String[] args) {
// 新建一个店员
Clerk clerk = new Clerk();
// 新建一个生产者
Producer p1 = new Producer(clerk);
p1.setName("p1");
// 新建一个消费者
Customer c1 = new Customer(clerk);
c1.setName("c1");
p1.start();
c1.start();
}
}
10. 守护线程
- java中的线程分为用户线程和守护线程。
- 守护线程是为其他线程提供服务的,如垃圾回收器(GC)就是一个典型的守护线程。
- 守护线程不能单独运行,当JVM中没有其他线程,只有守护线程时,守护线程会自动销毁,JVM会退出。
setDaemon()
设置当前线程为守护线程。- 测试代码:
/**
* 新建一个线程
*/
public class DaemonThread extends Thread{
@Override
public void run() {
// 循环打印
while(true) {
System.out.println("DaemonThread ......");
}
}
}
public class Test {
public static void main(String[] args) {
DaemonThread thread = new DaemonThread();
// 设置该线程为守护线程
thread.setDaemon(true);
thread.start();
for(int i = 0; i < 10; i++) {
System.out.println("main : " + i);
}
}
// main线程结束以后,守护线程thread自动销毁
}
- 输出结果:可以看出在main线程执行完后,设置为守护线程的线程也停止了。(但是没有立即停止,是因为关闭守护进程需要时间,这段时间CPU又执行了线程)
五、多线程的优势与存在的风险
1. 多线程编程的优势
- 提高系统的吞吐率(Throughout),多线程编程可以使一个进程有多个并发(Concurrent)的操作。
- 提高响应性(Responsiveness),Web服务器会采用一些专门的线程负责用户的请求处理,缩短了用户的等待时间。
- 充分利用多核(Multicore)处理器资源,通过多线程可以充分的利用CPPU资源。
2. 多线程编程存在的问题和风险
- 线程安全(Thread safe)问题。多线程共享数据时,如果没有采取正确的并发访问控制措施,就可能会产生数据一致性问题。
- 线程活性(Thread liveness)问题。由于程序自身的缺陷或者由资源稀缺性导致线程一直处于非runnable状态,这就是线程活性问题。常见的活性故障有以下几种:
死锁(Deadlock)类似鹬蚌相争
锁死(Lockout)类似于睡美人故事中,王子挂了
活锁(Livelock)类似小猫要自己尾巴
饥饿(Starvation)类似于健壮的雏鸟总是从母鸟的嘴中抢到吃的,弱小的小鸟容易饿死
- 上下文切换(Context Switch)。处理器从执行一个线程切换到执行另一个线程,需要消耗资源。
- 可靠性。一个线程的执行可能导致JVM意外终止,其他的线程也无法执行。
六、线程的原子性、可见性、有序性
1. 原子性
- 原子(Atomic)就是不可分割。原子的不可分割有两层含义:
一、访问(读、写)某个共享变量的操作从其他线程来看,该操作要么已经执行完毕,
要么尚未发生,即其他线程看不到当前先的中间结果。
二、访问同一组共享变量的原子操作是不能交错的。
- java主要两种方式实现原子性:一种是使用锁,另一种是利用处理器的CAS(Compare and Swap)指令。
- 锁:具有排他性,保证共享变量在某一时刻只能被一个线程访问。
- CAS指令:直接在硬件(处理器和内存)层次上实现,看作是硬件锁。
- 代码演示原子性:
public class ThreadSafe {
public static void main(String[] args) {
MyInt myInt = new MyInt();
// 启动两个线程不断获取num
new Thread() {
@Override
public void run() {
while(true) {
System.out.println(currentThread().getName() + " -> " + myInt.getNum());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
// 启动两个线程不断获取num
new Thread() {
@Override
public void run() {
while(true) {
System.out.println(currentThread().getName() + " -> " + myInt.getNum());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
// 定义一个静态内部类
static class MyInt {
int num;
public int getNum() {
return num++;
}
}
}
- 执行结果:有线程安全问题
- 使用
AtomicInteger
类保证原子性。
// 定义一个静态内部类
static class MyInt {
// int num;
// 保证原子性的Integer类
AtomicInteger num = new AtomicInteger();
public int getNum() {
// 自增后返回
// return num++;
return num.getAndIncrement();
}
}
2. 可见性
- 在多线程环境中,一个线程对某个共享变量进行更新之后,后续其他的线程可能无法立即读取到这个更行结果。则这个线程对共享变量的更新对其他线程是不可见的。
- 不可见,可能造成线程安全问题,可能读取到旧数据(脏数据)。(可见性 visibility)
- 代码测试:线程中共享变量的可见性
/**
* 测试线程的可见性
*/
public class ThreadVisiable {
public static void main(String[] args) throws InterruptedException {
MyTask myTask = new MyTask();
new Thread(myTask).start();
Thread.sleep(1000);
// 修改线程中toCancel的值
myTask.cancel();
}
static class MyTask implements Runnable {
private boolean toCancel = false;
@Override
public void run() {
while(!toCancel) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 执行任务
doSomething();
}
if(toCancel) {
System.out.println("取消任务成功");
}else {
System.out.println("任务正常结束");
}
}
public void doSomething() {
System.out.println("执行任务。。。。。");
}
public void cancel() {
toCancel = true;
System.out.println("取消任务!");
}
}
}
- 执行结果:
- 可能会出现以下情况:
- 在main线程中调用task.cancel方法,把task对象的toCancel变量修改为true,可能子线程看不到main线程所做的修改。导致子线程中的toCancel变量一直为false。
- 导致子线程看不到main线程对toCancel变量更新的原因:
- 原因一:JIT即时编译器可能对run方法中的while循环进行优化为:
- 原因二:可能和计算机的存储系统有关,假设有两个CPU内核分别运行main线程和子线程,运行子线程的CPU无法立即读取运行main线程中的数据。
if(!toCancel) {
while(true) {
doSomething();
}
}
3. 有序性
- 有序性(Ordering)是指在什么情况下,一个处理器上运行的一个线程所执行的内存访问操作在另一个处理器上运行的其他线程来看是乱序(Out of Order)的。
- 乱序是指内存访问操作的顺序看起来发生了变化。
- 在多核处理器的环境下,编写的顺序结构,这种操作执行的顺序可能是没有保障的:
- 编译器可能会改变两个操作的先后顺序。
- 处理器也可能不会按照目标代码的顺序执行。
3.1 重排序
- 这种一个处理器上执行的多个操作,在其他处理器来看它的顺序与目标代码指定的顺序可能不一样,这种现象称为重排序。
- 重排序是对内存访问有序操作的一种优化,可以在不影响单线程程序正确的情况下提升程序的性能。但是,可能对多线程程序的正确性产生影响,可能导致线程安全问题。
- 重排序与可见性问题类似,不是必然出现的。
3.2 与内存操作顺序有关的几个概念
- 源代码顺序:源码中指定的内存访问顺序。
- 程序顺序:处理器上运行的目标代码所指定的内存访问顺序。
- 执行顺序:内存访问操作在处理器上的实际执行顺序。
- 感知顺序:给定处理器所感知到的该处理器及其他处理器的内存访问操作的顺序。
3.3 重排序分类
- 重排序分为指令重排序与存储子系统重排序。
- 指令重排序主要是由JIT即时编译器引起的,指程序顺序与执行顺序不一样。
- 存储子系统重排序是由高速缓存、写缓冲器引起的,指感知顺序与执行顺序不一样。
3.4 指令重排序
- 在源码顺序与程序顺序不一致,或者程序顺序与执行顺序不一致的情况下,我们就说发生了指令重排序。
- 指令重排序是一种动作,确实对指令的顺序做了调整,重排序的对象是指令。
- javac编译器(将java文件转化为字节码文件)一般不会执行指令重排序,而JIT编译器可能执行指令重排序。
- 处理器也可能执行指令重排,使得执行顺序与程序顺序不一致。
- 指令重排不会对单线程程序产生影响,可能导致多线程程序出现非预期的结果。
3.5 存储子系统重排序
-
存储子系统是指写缓冲器与高速缓存。
-
高速缓存(Cache)是CPU中为了匹配与主内存处理速度不匹配而设计的一个高速缓存。
-
写缓冲器(Store Buffer,Writer Buffer)用来提高高速缓存操作的效率。
-
即使处理器严格按照程序顺序执行两个内存访问操作,在存储子系统的作用下,其他处理器对这两个操作的感知顺序与程序执行顺序不一致,即这两个操作的顺序看起来好像发生了变化,这种现象称为存储子系统重排序(内存重排序)。
-
存储子系统重排序并没有真正的对指令执行顺序进行调整,而是造成一种指令执行顺序被调整的假象。
-
存储子系统重排序的对象是内存操作的结果。
-
从处理器角度看,读内存就是从指定的RAM地址中加载数据到寄存器,称为Load操作;写内存就是把数据存储到指定的地址表达的RAM存储单元中,称为Store操作。
-
内存重排序可能有以下四种情况:
- LoadLoad重排序:一个处理器先后执行两个读操作L1和L2,其他处理器对两个内存操作的感知顺序可能是L2 -> L1。
- StoreStore重排序:一个处理器先后执行两个写操作W1和W2,其他处理器对两个内存操作的感知顺序可能是W2 -> W1。
- LoadStore重排序:一个处理器先执行读内存操作L1,在执行写内存操作W1,其他处理器对两个内存操作的感知顺序可能是W1 -> L1。
- StoreLoad重排序:一个处理器先执行一个写操作W1,在执行一个读内存操作L1,其他处理器对两个内存操作的感知顺序可能是L1 -> W1。
-
内存重排序与具体的处理器微架构有关,不同的架构的处理器所允许的内存重排序不同。
4. 貌似串行语义
- JIT编译器、处理器、存储子系统是按照一定规则对指令、内存操作的结果进行重排序,给单线程程序造成一种假象------指令是按照源码的顺序执行的,这种现象被称为貌似串行语义。 并不能保证多线程环境程序的正确性。
- 如果两个操作(指令)访问同一个变量,且其中一个操作为写操作,那么这两个操作之间就存在数据依赖关系(Data Dependency)。(如x = 1; y = x + 1)
- 为了保证貌似串行语义,有数据依赖关系的语句不会被重排序,只有不存在数据依赖关系的语句才会被重排序。
- 存在控制依赖关系的语句允许重排。
- 一条语句(指令)的执行结果会决定另一条语句能否被执行,这两条语句存在控制依赖关系(Control Dependency)。(如在if语句中允许重排,可能存在处理器先执行if代码块,再判断if条件是否成立)
七、轻量级同步机制:volatile关键字
1. volatile关键字的作用
volatile
关键字的作用:使变量在多个线程之间可见,能够保证每个线程获取到变量的最新值,从而避免脏读的发生。- java内存模型告诉我们,各个线程会将共享变量从主内存拷贝到工作内存,然后执行引擎会基于工作内存中的数据进行操作处理。
- 线程在工作内存进行操作后,何时会写到主内存中?这个时候对普通变量是没有规定的。针对volatile关键字修饰的变量,java虚拟机有特殊的约定,线程对volatile变量的修改会立即被其他线程所感知,即不会出现数据脏读的现象,从而保证数据的“可见性”。
- 代码演示:
/**
* 子线程读取不到main线程中,改变打印标志的后的值
*/
public class VolatileTest02 {
public static void main(String[] args) {
PrintString printString = new PrintString();
// 创建一个线程执行
new Thread() {
@Override
public void run() {
// 调用打印方法
printString.printStringMethod();
}
}.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("在main线程中修改打印标志");
// 改变变量的值
printString.setContinuePrint(false);
}
// 定义一个类打印字符串
static class PrintString {
private boolean continuePrint = true;
public void setContinuePrint(boolean continuePrint) {
this.continuePrint = continuePrint;
}
public void printStringMethod() {
System.out.println("打印开始........");
while(continuePrint) {
}
System.out.println("打印结束.......");
}
}
}
- 执行结果:陷入死循环,没有退出while循环。
- 分析原因:main线程修改printString对象的打印标志后,子线程读取不到。
- 解决方法:使用
volatile
关键字修饰printString对象的打印标志。volatile关键字的作用可以强制线程从公共内存中读取变量的值,而不是从工作内存中读取。(对多个线程可见)
private volatile boolean continuePrint = true;
2. volatile和synchronized比较
- volatile关键字是线程同步轻量级实现。性能比synchronized要好。(随着JDK新版本的发布,synchronized的执行效率也有很大的提升)
- volatile只能修饰变量,而synchronized可以修饰方法和代码块。
- 多线程访问volatile变量不会发生阻塞,而synchronized可能会阻塞。
- volatile能保证数据的可见性,但是不能保证原子性;而synchronized既可以保证可见性,也能保证原子性。
- volatile解决的是变量在多个线程之间的可见性;synchronized解决的是多个线程访问公共资源的同步性。
3. volatile不具有原子性
- 代码演示:
/**
* volatile不具有原子性
*/
public class VolatileTest03 {
public static void main(String[] args) {
// 创建10个线程
for (int i = 0; i < 10; i++) {
new MyThread().start();
}
}
static class MyThread extends Thread {
// volatile不具有原子性
private static volatile int count;
public static void addCount() {
for (int i = 0; i < 1000; i++) {
// count++不具有原子性
count++;
}
System.out.println(currentThread().getName() + " : " + count);
}
@Override
public void run() {
addCount();
}
}
}
- 运行结果:
- 结论:volatile关键字不具有原子性,count++操作也不具有原子性。
- 修改为synchronized方法,就能进行同步:
public synchronized static void addCount() {
for (int i = 0; i < 1000; i++) {
// count++不具有原子性
count++;
}
System.out.println(currentThread().getName() + " : " + count);
}
- 说明synchronized,既可以保证可见性,可能保证原子性。
4. 使用原子类代替++操作
- ++操作不是原子操作。
- 除了使用synchronized实现同步外,还可以使用AtomicInteger/AtomicLong原子类代替++实现原子操作。
- 代码实现:
// 使用原子类,保证原子性
private static AtomicInteger count = new AtomicInteger();
public static void addCount() {
for (int i = 0; i < 1000; i++) {
// 使用原子类自增
count.getAndIncrement();
}
System.out.println(currentThread().getName() + " : " + count.get());
}
5. CAS
- 原子类能够实现原子的自增,底层就是使用CAS。
- CAS(Compare And Swap)是由硬件实现的。
- CAS可以将read(读)-modify(改)-write(写)这类操作转化为原子操作。
- i++自增操作,包括三个子操作:
- 从主内存读取 i 的值,到线程的工作内存
- 对 i 的值加1
- 再把加1后的值写回主内存
- CAS原理:在把数据更新到主内存时(写),再读取一次主内存的值,如果现在变量的值与期望的值(操作起始时读取的值)一致,就进行更新。
- 如果更新之前读取到的值与期望不一样,就撤销本次自增操作。
6. 自定义CAS实现线程安全的计数器
/**
* 实现一个CAS的线程安全的自增操作
*/
public class CASTest {
public static void main(String[] args) {
CASCounter counter = new CASCounter();
// 创建100个线程
for (int i = 0; i < 100; i++) {
new Thread() {
@Override
public void run() {
System.out.println(counter.incrementAndGet());
}
}.start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("最终值:" + counter.getValue());
}
}
// CAS的计数器
class CASCounter {
// 使用volatile,使线程可见
volatile private long value;
public long getValue() {
return value;
}
// 定义CAS方法
public boolean compareAndSwap(long expectedValue, long newValue) {
synchronized(this) {
// 如果当前value的值与当前期望的值一样,就把当前value替换为newValue
if(value == expectedValue) {
value = newValue;
return true;
}
return false;
}
}
// 定义自增的方法
public long incrementAndGet() {
long oldValue;
long newValue;
// 循环判断是否能够自增
do {
oldValue = value;
newValue = oldValue + 1;
}while (!compareAndSwap(oldValue, newValue));
return newValue;
}
}
7. CAS中的ABA问题
- CAS实现原子操作背后有一个假设:共享变量的当前值与当前线程提供的期望值相同,就认为这个变量没有被其他线程修改过。
- 实际上这种假设并不一定总是成立。
- 如有共享变量count = 0。
- A线程对count修改为10。
- B线程修改为20。
- C线程又修改为0。
- 当前线程看到count变量的值现在是0,是否认为count变量的值没有被其他线程更新呢?这种结果能否接受?
- 这就是CAS中的ABA问题,即共享变量经历了A值 改为 B值 再改为A值的过程。
- 如何规避ABA问题:可以为共享变量引入一个修订号(时间戳),每次修改共享变量时,相应的修订号就会+1。通过修订号可以准确的判断变量是否被其他线程修改过。(原子类中的AtomicStampedReference类就是基于这种思想)
8. 原子变量类
- JDK5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronized同步锁的一种乐观锁。
- synchronized关键字保证同步是一种独占锁,也就是悲观锁。
- 原子变量类基于CAS实现,当对共享变量进行read-modify-write更新操作时,通过原子变量类可以保障原子性和可见性。
- 对变量进行更新不是一个简单的赋值,而是变量的新值依赖变量的旧值,如自增操作i++。
- 原子变量类内部就是借助一个volatile变量,并且保证了该变量更新的原子性。有时也把原子变量类看作增强的volatile变量。
8.1 AtomicLong
- 使用AtomicLong定义一个计数器,记录网站总的请求数
/**
* 使用原子类定义一个计数器
* 该计数器,在整个程序中都能使用,并且所有地方都使用这一个计数器,所以定义为单例的
* 该计数器,用于统计一个服务器接受的请求总数,成功总数,失败总数
*/
public class Indicator {
// 私有化构造器
private Indicator() {}
// 饿汉式
private static final Indicator INSTANCE = new Indicator();
public static Indicator getInstance() {
return INSTANCE;
}
// 使用原子类定义服务器接收的请求总数、成功总数、失败总数
// 请求总数
private final AtomicLong requestCount = new AtomicLong(0);
// 成功总数
private final AtomicLong successCount = new AtomicLong(0);
// 失败总数
private final AtomicLong failCount = new AtomicLong(0);
// 定义几个请求,请求一次总数就加一
public void requestReceive() {
requestCount.incrementAndGet();
}
public void requestSuccess() {
successCount.incrementAndGet();
}
public void requestFail() {
failCount.incrementAndGet();
}
// 获取总数
public long getRequestCount() {
return requestCount.get();
}
public long getSuccessRequest() {
return successCount.get();
}
public long getFailRequest() {
return failCount.get();
}
}
/**
* 模拟服务器的请求测试
*/
public class IndicatorTest {
public static void main(String[] args) {
// 通过线程模拟用户请求,在实际应用中可以在ServletFilter中调用Indicator计数器
// 获得一个计数器
Indicator indicator = Indicator.getInstance();
// 创建100个线程进行模拟
for (int i = 0; i < 100; i++) {
new Thread() {
@Override
public void run() {
// 每个线程就是一个请求,总请求数加1
indicator.requestReceive();
// 生成一个随机数,模拟请求成功或失败
int num = new Random().nextInt(10000);
if(num % 2 == 0) {
// 偶数请求成功
indicator.requestSuccess();
}else{
// 奇数请求失败
indicator.requestFail();
}
}
}.start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 获取总数
System.out.println(indicator.getRequestCount());
System.out.println(indicator.getSuccessRequest());
System.out.println(indicator.getFailRequest());
}
}
8.2 AtomicIntegerArray
- 原子类数组,在多线程情况下,能够保证原子性。
/**
* 测试原子数组类,AtomicIntegerArray
*/
public class AtomicIntegerArrayTest {
// 定义一个原子数组类,指定数组的大小
static AtomicIntegerArray array = new AtomicIntegerArray(10);
public static void main(String[] args) {
// 定义一个线程数组,10个线程
Thread[] threads = new Thread[10];
for (int i = 0; i < threads.length; i++) {
threads[i] = new MyThread();
// 开启线程
threads[i].start();
try {
// 使10个线程调用完成
threads[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 打印数组的值
System.out.println(array);
}
// 定义一个线程类
static class MyThread extends Thread {
@Override
public void run() {
// 对数组中的每个元素,自增到1000
for (int i = 0; i < 10000; i++) {
// getAndIncrement(0) 对几个元素进行自增
array.getAndIncrement(i % array.length());
}
}
}
}
8.3 AtomicIntegerFiledUpdate
- AtomicIntegerFiledUpdate原子整数字段更新类:用于对象的整数字段进行更新。
- 字段必须使用volatile修饰,使线程可见。只能是实例变量,不能是静态变量,也不能是final修饰的,不能为private不然不能修改。
- 代码演示:
/**
* 新建一个用户类
* 使用AtomicIntegerFieldUpdater更新User类的int字段
* 该字段必须是volatile修饰的
*/
public class User {
int id;
// 需要使用AtomicIntegerFieldUpdater更新的字段必须使用volatile修饰
volatile int age;
public User(int id, int age) {
this.id = id;
this.age = age;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", age=" + age +
'}';
}
}
/**
* 使用AtomicIntegerFieldUpdater对User对象的字段进行更新
*/
public class AtomicIntegerFiledUpdateTest {
public static void main(String[] args) {
// 创建一个User对象
User user = new User(1, 18);
// 创建10个线程对user的age进行更新
for (int i = 0; i < 10; i++) {
new SubThread(user).start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(user);
}
}
// 新建一个线程类
class SubThread extends Thread {
private User user;
// 创建一个更新字段的原子更新类,传入需要更新的对象和字段
private AtomicIntegerFieldUpdater<User> updater = AtomicIntegerFieldUpdater.newUpdater(User.class, "age");
public SubThread(User user) {
this.user = user;
}
@Override
public void run() {
// 对字段进行更新:此处+1
updater.incrementAndGet(user);
}
}
8.4 AtomicReference
- AtomicReference可以原子的读写一个对象。
- 代码演示:读写一个字符串对象
/**
* AtomicReference原子的读写一个对象
*/
public class AtomicReferenceTest {
public static void main(String[] args) {
// 读写一个字符串对象
AtomicReference<String> reference = new AtomicReference<>("abc");
// 开启100个线程修改
for (int i = 0; i < 100; i++) {
new Thread() {
@Override
public void run() {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 如果字符串是abc,就修改为ref
if(reference.compareAndSet("abc", "ref")) {
System.out.println("字符串修改为ref");
}
}
}.start();
}
// 开启100个线程修改
for (int i = 0; i < 100; i++) {
new Thread() {
@Override
public void run() {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 如果字符串是ref,就修改为abc
if(reference.compareAndSet("ref", "abc")) {
System.out.println("字符串修改为abc");
}
}
}.start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 打印字符串现在的值
System.out.println(reference.get());
}
}
- 运行结果:
8.4.1 演示AtomicReference可能出现CAS中的ABA问题
- CAS中的ABA问题:一个线程将值A -> 修改为B -> 再修改为A,另一个线程进行比较时期望的值为A,所以认为是线程安全的。但是其他线程已经对值进行了修改。
- 代码测试:
/**
* AtomicReference可能会出现CAS中的ABA问题
*/
public class AtomicReferenceTestABA {
private static AtomicReference<String> reference = new AtomicReference<>("abc");
public static void main(String[] args) throws InterruptedException {
// 创建一个线程对字符串对象进行修改
Thread t1 = new Thread() {
@Override
public void run() {
// 对字符串对象进行修改
reference.compareAndSet("abc", "ref");
System.out.println(currentThread().getName() + " -- " + reference.get());
// 再修改回abc
reference.compareAndSet("ref", "abc");
}
};
// 再创建一个线程对值进行修改
Thread t2 = new Thread() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 对字符串对象进行修改
reference.compareAndSet("abc", "ggg");
}
};
t1.start();
t2.start();
// 等待t1,t2执行完
t1.join();
t2.join();
// 在线程2中,abc就是它期待的值,所以修改成功,但是其他线程已经对字符串进行修改
// 只是修改回来了,所以产生了CAS中的ABA问题
System.out.println(reference.get());
}
}
- 运行结果:
- 如果解决:使用
AtomicStampedReference
类和AtomicMarkableReference
类。
8.4.2 AtomicStampReference
- 使用AtomicStampReference加入一个版本号,解决AtomicReference可能出现的的ABA问题。
- 代码演示:
/**
* 使用AtomicStampReference解决AtomicReference可能出现的ABA问题
*/
public class AtomicStampReferenceTest {
// 初始化版本号为0
private static AtomicStampedReference<String> stampedReference = new AtomicStampedReference<>("abc", 0);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// 获取版本号
int stamp = stampedReference.getStamp();
// 更新版本号 + 1
stampedReference.compareAndSet("abc", "ref", stamp, stamp+1);
System.out.println(stampedReference.getReference());
// 获取新的版本号
int stamp1 = stampedReference.getStamp();
// 再修改回abc
stampedReference.compareAndSet("ref", "abc", stamp1, stamp1+1);
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
// 获取版本号
int stamp = stampedReference.getStamp();
// 再睡眠之前拿到版本号,睡醒之后拿到的版本号可能就不一致了,导致更新失败
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean set = stampedReference.compareAndSet("abc", "ggg", stamp, stamp + 1);
// 打印是否更新成功
System.out.println(set);
}
});
t1.start();
t2.start();
t1.join();
t2.join();
Thread.sleep(1000);
System.out.println(stampedReference.getReference());
}
}
- 运行结果:
- 结果分析:线程2更新失败,因为在线程2睡眠之前拿到版本号,睡醒之后版本号发生了变化,所以更新失败。从而避免了CAS中的ABA问题。
八、ThreadLocal
1. ThreadLocal基本使用
- 在多线程中,除了控制访问资源外(synchronized),还可以增加资源来保证线程安全。
ThreadLocal
会为每个线程绑定自己的值。- 代码演示:ThreadLocal的基本使用
/**
* ThreadLocal类的基本使用
*/
public class ThreadLocalTest {
// 定义一个ThreadLocal类
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
static class SubThread extends Thread {
@Override
public void run() {
// 为当前线程设置一个值
threadLocal.set(currentThread().getName() + ": " + System.currentTimeMillis() + "时间");
// 或取当前线程设置的值
System.out.println(threadLocal.get());
}
}
public static void main(String[] args) {
SubThread t1 = new SubThread();
SubThread t2 = new SubThread();
t1.start();
t2.start();
}
}
- 运行结果:
2. ThreadLocal解决线程安全问题
- 将字符串转化为时间,使用100个线程进行转化。
- 代码演示:
/**
* 使用线程将字符串转化为时间
* 如果线程共用一个format转化类进行转化,可能出现线程安全问题
* 解决方法:
* 1. 加synchronized锁
* 2. 为每个线程设置一个format转化类
*/
public class ThreadLocalTest02 {
// 定义一个字符串转化为时间类
private static SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:MM:ss");
// 定义一个线程类
static class SubThread implements Runnable {
private int i;
public SubThread(int i) {
this.i = i;
}
@Override
public void run() {
try {
String date = "2012-12-23 12:45:" + i % 60;
// 将字符串转化为时间
Date parse = format.parse(date);
System.out.println(Thread.currentThread().getName() + ": " + parse);
} catch (ParseException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
// 创建100个线程,将字符串转化为时间
new Thread(new SubThread(i)).start();
}
}
}
- 运行结果:有的线程转化成功,有的线程没有转化成功,抛出异常。
- 问题分析:多个线程使用同一个SimpleDateFormat类,可能导致线程安全问题。
- 解决方法:1. 线程转化的时候,使用synchronized。2. 使用ThreadLocal类为每个线程设置一个SimpleDateFormat类。
- 解决代码:
// 创建一个ThreadLocal类
private static ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<>();
@Override
public void run() {
try {
String date = "2012-12-23 12:45:" + i % 60;
// 判断该线程中是否有转化类了
if(threadLocal.get() == null) {
// 没有就新建一个设置进去
threadLocal.set(new SimpleDateFormat("yyyy-MM-dd hh:MM:ss"));
}
// 将字符串转化为时间
Date parse = threadLocal.get().parse(date);
System.out.println(Thread.currentThread().getName() + ": " + parse);
} catch (ParseException e) {
e.printStackTrace();
}
}
3. 为ThreadLocal设置一个初始值
- 新建一个类继承
ThreadLocal
类,并重写它的initialValue()
方法。 - 代码演示:
/**
* 为ThreadLocal设置一个初始值
*/
public class ThreadLocalInitialValue {
// 新建一个类继承ThreadLocal,并重写它的initialValue()方法
static class SubThreadLocal extends ThreadLocal<Date> {
@Override
protected Date initialValue() {
// 返回的值,就是为ThreadLocal设置的初始值,时间为1小时前
return new Date(System.currentTimeMillis() - 60 * 60 * 1000);
}
}
// 新建一个带初始值的ThreadLocal
private static SubThreadLocal subThreadLocal = new SubThreadLocal();
// 新建一个线程类
static class SubThread extends Thread {
@Override
public void run() {
// 如果没有初始值,会get()到null
if(subThreadLocal.get() == null) {
System.out.println("**********");
subThreadLocal.set(new Date());
}
System.out.println(currentThread().getName() + ": " + subThreadLocal.get());
}
}
public static void main(String[] args) {
// 新建两个线程类
SubThread s1 = new SubThread();
SubThread s2 = new SubThread();
s1.start();
s2.start();
}
}
- 运行结果:
九、Lock显示锁
- JDK5新增了
Lock
锁接口,常用实现类ReentrantLock
,称为可重入锁。它的功能比synchronized更强大。
1. 锁的可重入性
- 锁的可重入性是指:当一个线程获得一个锁对象后,能够再次获得该锁对象。
- 代码演示:
/**
* 测试锁的可重入性
*/
public class ReentrantTest {
private synchronized void sm1() {
System.out.println("sm1方法");
// sm1同步方法,获得当前锁对象是this,然后调用sm2同步方法,sm2的锁对象也是this
// 当前线程已经持有了this锁对象,可以再次获得该锁对象,这就是锁的可重入性
// 假设锁不可重入的话,可能会造成死锁
sm2();
}
private synchronized void sm2() {
System.out.println("sm2方法");
sm3();
}
private synchronized void sm3() {
System.out.println("sm3方法");
}
public static void main(String[] args) {
ReentrantTest test = new ReentrantTest();
new Thread(new Runnable() {
@Override
public void run() {
test.sm1();
}
}).start();
}
}
- 代码分析:sm1是同步方法,sm2也是同步方法,默认的锁对象都是this。在sm1中调用sm2方法,当前线程已经持有了this锁对象,可以再次获得该锁对象,这就是锁的可重入性。如果锁不可重入,那么可能造成死锁。
2. 锁的基本使用
- 获得锁
lock()
,释放锁unlock()
。 - 一般在try中获得锁lock(),在finally中释放锁unlock()。
- 代码演示:
/**
* 锁的基本使用
*/
public class ReentrantLockTest {
// 新建一个锁对象
private static Lock lock = new ReentrantLock();
private static int num;
public static void sm() {
// 先获得锁
lock.lock();
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 100; i++) {
num++;
}
System.out.println(num);
// 释放锁
lock.unlock();
}
public static void main(String[] args) {
Runnable r = new Runnable() {
@Override
public void run() {
sm();
}
};
// 新建3个线程
new Thread(r).start();
new Thread(r).start();
new Thread(r).start();
}
}
3. lockInterruptibly()
- 普通获得Lock锁调用lock()方法。
lockInterruptibly()
方法也是获得Lock锁,但是如果当前线程被中断了就抛出异常,无法获得Lock锁。- 代码演示:
/**
* 测试lockInterruptibly()方法
*/
public class LockInterruptiblyTest {
private static Lock lock = new ReentrantLock();
static class Service {
public void service() {
try {
// 获得一个lock锁
lock.lockInterruptibly();
System.out.println(Thread.currentThread().getName() + "begin lock ......");
// 执行一段耗时操作
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName() + "end lock ......");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "释放锁");
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
Service service = new Service();
Runnable r = new Runnable() {
@Override
public void run() {
service.service();
}
};
// 新建两个线程
Thread t1 = new Thread(r, "线程1");
Thread t2 = new Thread(r, "线程2");
t1.start();
t2.start();
Thread.sleep(2000);
// 等待两秒之后,中断线程2
t2.interrupt();
}
}
- 运行结果:线程1正常执行完毕,线程2没有获得Lock锁。
- 结果分析:Lock锁是使用
lockInterrupribly()
方法获得锁,线程2被中断,就不会获得锁,抛出异常。
4. 使用lockInterruptibly()解决死锁问题
- 获得锁时,使用
lockInterruptibly()
方法。 - 当两个线程出现死锁,调用一个线程的中断方法interrupt()方法中断其中一个线程,另一个线程就解锁。
- 代码演示:
/**
* 使用lockInterruptibly()解决死锁问题
*/
public class DeadLock {
// 定义两个锁
private static ReentrantLock lock1 = new ReentrantLock();
private static ReentrantLock lock2 = new ReentrantLock();
static class DeadLockTest implements Runnable {
private int num;
public DeadLockTest(int num) {
this.num = num;
}
@Override
public void run() {
try {
if(num % 2 == 1) {
// 如果为奇数,先获得锁1,再获得锁2
lock1.lockInterruptibly();
System.out.println(Thread.currentThread().getName() + "获得锁1");
Thread.sleep(1000);
lock2.lockInterruptibly();
System.out.println(Thread.currentThread().getName() + "同时获得获得锁1和锁2");
} else {
// 如果为偶数,先获得锁2,再获得锁1
lock2.lockInterruptibly();
System.out.println(Thread.currentThread().getName() + "获得锁1");
Thread.sleep(1000);
lock1.lockInterruptibly();
System.out.println(Thread.currentThread().getName() + "同时获得获得锁1和锁2");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if(lock1.isHeldByCurrentThread())
// isHeldByCurrentThread()如果当前线程持有锁1才释放
// 该方法只有实现类有,Lock接口没有
lock1.unlock();
if(lock2.isHeldByCurrentThread())
lock2.unlock();
System.out.println("释放锁");
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new DeadLockTest(11));
Thread t2 = new Thread(new DeadLockTest(22));
t1.start();
t2.start();
// 出现死锁
// 中断其中一个线程,就可避免死锁
Thread.sleep(3000);
t2.interrupt();
}
}
- 运行结果:
- 结果分析:线程1拿到锁1,线程2拿到锁2。两个线程出现了死锁。锁是使用
lockInterruptibly()
获得的。调用线程2的interrupt()方法,线程2中断,线程1同时拿到锁1和锁2,线程1执行成功。
5. tryLock方法
- Lock接口中的
tryLock(long time, TimeUnit unit)
方法:给定等待时长内锁没有被其他线程持有,并且当前线程没有被中断,则获得该锁,返回true。如果给定时间内没有没有获得锁就返回false。 tryLock()
不带参数的,尝试获得锁,被其他线程占用就放弃,返回false,获得锁就返回true。- 代码测试:等待时间之内没有获得锁,就放弃。
public class TryLockTest {
private static ReentrantLock lock = new ReentrantLock();
static class ThreadTest implements Runnable {
@Override
public void run() {
try {
// tryLock()返回boolean值,获得锁返回true
if(lock.tryLock(3, TimeUnit.SECONDS)) {
// 如果3秒内拿到锁执行操作
System.out.println(Thread.currentThread().getName() + "获得锁,执行一段操作");
Thread.sleep(4000);
} else {
// 如果3秒内没有拿到锁,就返回false
System.out.println(Thread.currentThread().getName() + "获取锁失败");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 如果当前线程获得了锁就关闭锁
if(lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
public static void main(String[] args) {
// 创建两个线程
ThreadTest r = new ThreadTest();
new Thread(r).start();
new Thread(r).start();
}
}
-
运行结果:
-
分析:第一个线程拿到锁,执行一个4秒的操作。第二个线程再执行
lock.tryLock(3, TimeUnit.SECONDS)
这段代码,3秒之内就没有获得锁,获取锁失败。
6. tryLock()避免死锁
- tryLock()方法尝试获得锁,获取不到就返回false,可以使用一个while循环一直调用tryLock()方法一直尝试获得锁,就能避免死锁。
- 代码演示:两个线程分别持有锁1和锁2,同时想要获得对方的锁。一直while循环tryLock尝试获得锁。
/**
* 使用tryLock()避免死锁
*/
public class TryLockTestDeadLock {
// 定义两个锁
private static ReentrantLock lock1 = new ReentrantLock();
private static ReentrantLock lock2 = new ReentrantLock();
static class IntLock implements Runnable {
private int num;
public IntLock(int num) {
this.num = num;
}
@Override
public void run() {
// num为奇数就先获得锁1
if(num % 2 != 0) {
// 使用一个while循环一直尝试获得锁
while(true) {
try {
if (lock1.tryLock()) {
System.out.println(Thread.currentThread().getName() + "获得锁1,还要获得锁2");
Thread.sleep(100);
try {
// 尝试获得锁2
if(lock2.tryLock()) {
System.out.println(Thread.currentThread().getName() + "同时获得锁1和锁2");
// 退出run方法,线程结束
return;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if(lock2.isHeldByCurrentThread())
lock2.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if(lock1.isHeldByCurrentThread())
lock1.unlock();
}
}
} else {
// num为偶数,就先获得锁2
// 一直循环获得锁
while(true) {
try {
if(lock2.tryLock()) {
System.out.println(Thread.currentThread().getName() + "获得锁2,还要获得锁1");
Thread.sleep(100);
try {
if(lock1.tryLock()) {
System.out.println(Thread.currentThread().getName() + "同时获得锁1和锁2");
// 退出run方法,线程结束
return;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if(lock1.isHeldByCurrentThread())
lock1.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if(lock2.isHeldByCurrentThread())
lock2.unlock();
}
}
}
}
}
public static void main(String[] args) {
// 新建两个线程
IntLock r1 = new IntLock(11);
IntLock r2 = new IntLock(22);
new Thread(r1).start();
new Thread(r2).start();
}
}
- 运行结果:经过一段时间的等待,两个线程结束。
7. Lock锁的线程通信newCondition()
- 关键字
synchronized
与wait()
/nofity()
一起使用可以实现等待/通知模式。 Lock
接口中的newCondition()
返回一个Condition
对象。Condition类也可以实现等待通知机制。- Condition类中的
await()
方法会使当前线程等待(类似于wait()方法),同时会释放锁。 signal()
方法能够指定唤醒某个等待的线程。(类似于notify()方法,但是notify()方法是随机唤醒某个阻塞的线程,优先唤醒线程优先级高的)- 注意:await()和signal()方法也需要线程持有相关的Lock锁。调用await()后,线程会释放这个锁。再signal()调用后会从当前Condition对象的等待队列中,唤醒一个线程,唤醒的线程尝试获得锁,获得锁成功就继续执行。
- 代码演示:
/**
* 测试Condition的等待通知机制
*/
public class ConditionTest {
private static Lock lock = new ReentrantLock();
// 获得Condition对象
private static Condition condition = lock.newCondition();
static class SubThread extends Thread {
@Override
public void run() {
try {
// 获得锁
lock.lock();
System.out.println("await()方法执行之前");
// 线程阻塞
condition.await();
System.out.println("await()方法执行之后");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println("释放锁");
}
}
}
public static void main(String[] args) {
SubThread thread = new SubThread();
// 线程启动进入阻塞状态
thread.start();
try {
// 两秒后主线程唤醒子线程
Thread.sleep(2000);
// 先拿到锁,才能调用signal()方法唤醒线程
lock.lock();
// 唤醒子线程
condition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
- 代码演示:多个Candition对象,指定唤醒那个线程。
/**
* 多个Condition对象指定唤醒某个线程
*/
public class ConditionTest02 {
private static Lock lock = new ReentrantLock();
// 获得两个Condition对象
static Condition condition1 = lock.newCondition();
static Condition condition2 = lock.newCondition();
static class Service {
// 使用Condition1进行阻塞
public void methodAwait1() {
try {
// 获得锁
lock.lock();
System.out.println("开始阻塞 -- " + Thread.currentThread().getName());
condition1.await();
System.out.println("阻塞结束 -- " + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁
lock.unlock();
}
}
// 使用Condition1进行阻塞
public void methodAwait2() {
try {
// 获得锁
lock.lock();
System.out.println("开始阻塞 -- " + Thread.currentThread().getName());
condition2.await();
System.out.println("阻塞结束 -- " + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁
lock.unlock();
}
}
// 使用Condition1唤醒线程
public void signal1() {
try {
// 获得锁
lock.lock();
condition1.signal();
System.out.println("唤醒成功 -- " + Thread.currentThread().getName());
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
// 使用Condition2唤醒线程
public void signal2() {
try {
// 获得锁
lock.lock();
condition2.signal();
System.out.println("唤醒成功 -- " + Thread.currentThread().getName());
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
Service service = new Service();
// 定义两个线程
new Thread(new Runnable() {
@Override
public void run() {
// 阻塞该线程
service.methodAwait1();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
// 阻塞该线程
service.methodAwait2();
}
}).start();
Thread.sleep(3000);
// 释放线程1
service.signal1();
}
}
- 运行结果:使用Condition对象可以指定唤醒那个线程,只唤醒了线程1。
8. Condition通信实现两个线程交替打印
- 代码演示:
/**
* 两个线程实现交替打印
*/
public class ConditionTest03 {
static class Service {
private static Lock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();
// 定义一个交替打印的标志
private static boolean flag = true;
public void printA() {
try {
// 获得锁
lock.lock();
while(flag) {
// 等待
condition.await();
}
System.out.println(Thread.currentThread().getName() + "A");
flag = true;
// 唤醒另一个线程进行等待
condition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁
lock.unlock();
}
}
public void printB() {
try {
// 获得锁
lock.lock();
while(!flag) {
// 等待
condition.await();
}
System.out.println(Thread.currentThread().getName() + "--B");
flag = false;
// 唤醒另一个线程进行等待
condition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁
lock.unlock();
}
}
}
public static void main(String[] args) {
Service service = new Service();
// 创建线程1,打印A
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
service.printA();
}
}
}).start();
// 创建线程2,打印B
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
service.printB();
}
}
}).start();
}
}
9. Condition通信实现生产者和消费者问题
- 代码测试:
/**
* 使用Condition实现生产者和消费者问题
*/
public class ProducerAndConsumer {
// 产品类
static class Product {
// 产品数量
private int num;
// 定义一个锁
private Lock lock = new ReentrantLock();
// 获得Condition对象
private Condition condition = lock.newCondition();
// 定义生产产品的方法
public void production() {
try {
// 获得锁
lock.lock();
// 最多生产20个产品
if(num < 20) {
System.out.println(Thread.currentThread().getName() + "开始生产产品" + ++num);
// 唤醒消费者进行消费
condition.signal();
} else {
// 生产满了就等待
condition.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
// 定义消费产品的方法
public void consume() {
// 获得锁
lock.lock();
try {
// 如果还有产品就消费
if(num > 0) {
System.out.println(Thread.currentThread().getName() + "消费了产品" + num--);
// 唤醒生产者进行生产
condition.signal();
} else {
// 如果没有产品了就等待
condition.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
// 创建一个生产者
static class Producter extends Thread{
private Product product;
public Producter(Product product) {
this.product = product;
}
@Override
public void run() {
while (true) {
// 生产产品
product.production();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 创建一个消费者
static class Consumer extends Thread {
private Product product;
public Consumer(Product product) {
this.product = product;
}
@Override
public void run() {
while(true) {
// 消费产品
product.consume();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
// 新建一个产品
Product product = new Product();
// 新建两个生产者
new Producter(product).start();
new Producter(product).start();
// 新建一个消费者
new Consumer(product).start();
}
}
10. 公平锁和非公平锁
- 大多数情况下,锁的申请都是非公平的。线程1和线程2同时请求锁A,系统会从阻塞队列中,随机选择一个线程,可能线程1一直重复获得锁。不能保证其公平性。
- 公平锁会按照时间顺序,保证先到先得,公平锁这一特点不会出现线程饥饿现象。
synchronized
内部锁是非公平的。ReentrantLock(boolean fair)
有参构造器设置为true
,就可以创建一个公平锁。- 代码演示:
/**
* 测试公平锁
*/
public class EquitableTest {
// 默认构造函数时非公平锁:使用非公平锁可能导致一个线程一直重复获得锁
// static Lock lock = new ReentrantLock();
// 公平锁
static Lock lock = new ReentrantLock(true);
static class SubThread extends Thread {
@Override
public void run() {
while(true) {
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + "获得锁");
} finally {
lock.unlock();
}
}
}
}
public static void main(String[] args) {
// 创建5个线程
for (int i = 0; i < 5; i++) {
new SubThread().start();
}
}
}
-
运行结果:5个线程轮流获得锁
-
分析:实现公平锁必须要求系统维护一个有序队列,所以公平锁的实现成本较高,性能也比较低。不是特别需要,一般不使用公平锁。
11. ReentrantLock类中的常用方法
getHoldCount()
:返回当前线程调用lock()方法的次数。getQueueLength()
:返回等待锁的线程数。getWaitQueueLength(Condition condition)
:返回Condition条件上等待线程的数量。hasQueuedThread(Thread thread)
:判断指定线程是否在等待获得锁。hasQueuedThreads()
:判断是否还有线程在等待获得锁。hasWaiters(Condition condition)
:判断线程是否正在等待指定的Condition条件。isFair()
:判断当前锁是否是公平锁。isHeldByCurrentThread()
:判断锁是否被当前线程所持有。- `isLocked():判断当前锁是否被线程持有。
十、读写锁ReentrantReadWriteLock
1. 读写锁简介
- synchronized内部锁和ReentrantLock锁都是独占锁(排他锁),同一个时间内只允许一个线程执行同步代码块,可以保证线程安全,但是执行效率低。
ReentrantReadWriteLock
读写锁是一种改进的排他锁,也称为共享锁。允许多个线程同时读写共享数据,但是一次只允许一个线程对共享数据进行更新。- 通过读锁和写锁来完成读写操作。
- 线程在读取共享变量之前,必须先持有读锁,读锁可以被多个线程持有。
- 线程在修改数据之前,必须先持有写锁,写锁是排他的。
- 如果一个线程持有写锁,其他线程将无法获得相应的锁(包括读锁和写锁)。
- 读锁只是在读线程之间共享,任何一个线程持有读锁时,其他线程都无法获得写锁。保证线程在读取数据期间没有其他线程对数据进行更新,使得读线程能够读取数据的最新值。
获得条件 | 排他性 | 作用 | |
---|---|---|---|
读锁 | 写锁未被任何线程获得 | 对读线程是共享的,对写线程是排他的 | 允许多个读线程持有读锁读取共享数据。保证在读取共享数据的同时,没有其他线程对数据进行修改 |
写锁 | 该写锁没有被任何线程锁持有,并且相应的读锁也未被其他线程持有 | 对读线程和写线程都是排他的 | 保证写线程以独占的方式,修改共享数据 |
- 读写锁允许读读共享,读写互斥,写写互斥。
2. ReentrantReadWriteLock
- 接口
ReadWriteLock
,两个方法readLock()
返回读锁,writeLock()
返回写锁。 - 读写锁实现类
ReentrantReadWriteLock
。 - 注意:readLock()与writeLock()返回的是同一个锁对象。这一个锁对象可以是读写两个角色。
3. 读读共享
- 读锁可以同时被多个线程获得。
- 代码演示:创建5个线程持有读锁
/**
* 测试ReentrantReadWriteLock读写锁的读读共享
*/
public class ReadLock {
// 获得一个读写锁
static ReadWriteLock rwLock = new ReentrantReadWriteLock();
static class Service {
// 读取数据的方法
public void readDate() {
try {
// 获得读锁
rwLock.readLock().lock();
System.out.println(Thread.currentThread().getName() + "获得读锁--" + System.currentTimeMillis());
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放读锁
rwLock.readLock().unlock();
}
}
}
public static void main(String[] args) {
Service service = new Service();
// 创建5个线程尝试获取读锁
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
service.readDate();
}
}).start();
}
}
}
- 运行结果:5个线程在同一时间获得读锁
4. 写写互斥
- 代码演示:写锁只能被一个线程持有
/**
* 写写互斥
*/
public class WriteLock {
// 定义一个读写锁
private static ReadWriteLock rwLock = new ReentrantReadWriteLock();
static class Service {
// 定义一个写数据的方法
public void writeData() {
try {
// 获得写锁
rwLock.writeLock().lock();
System.out.println(Thread.currentThread().getName() + "获得写锁,时间--" + System.currentTimeMillis());
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "释放锁,时间" + System.currentTimeMillis());
rwLock.writeLock().unlock();
}
}
}
public static void main(String[] args) {
Service service = new Service();
// 创建5个线程写数据
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
service.writeData();
}
}).start();
}
}
}
- 运行结果:
4. 读写互斥
- 代码演示:
/**
* 读写互斥
*/
public class ReadWriteLockTest {
static class Service {
// 定义一个读写锁
private ReadWriteLock rwLock = new ReentrantReadWriteLock();
// 获得读锁
private Lock readLock = rwLock.readLock();
// 获得写锁
private Lock writeLock = rwLock.writeLock();
// 定义一个读方法
public void read() {
try {
readLock.lock();
System.out.println(Thread.currentThread().getName() + "获得读锁,时间:" + System.currentTimeMillis());
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "释放读锁,时间:" + System.currentTimeMillis());
readLock.unlock();
}
}
// 定义一个读方法
public void write() {
try {
writeLock.lock();
System.out.println(Thread.currentThread().getName() + "获得写锁,时间:" + System.currentTimeMillis());
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "释放写锁,时间:" + System.currentTimeMillis());
writeLock.unlock();
}
}
}
public static void main(String[] args) {
Service service = new Service();
// 定义一个线程读数据
new Thread(new Runnable() {
@Override
public void run() {
service.read();
}
}).start();
// 定义一个线程写数据
new Thread(new Runnable() {
@Override
public void run() {
service.write();
}
}).start();
}
}
十一、线程管理
1. 线程组
- 类似于计算机中文件夹管理文件,线程组可以管理线程。
- 在线程组中可以定义一组相似(相关的)线程,也可以定义子线程。
- Thread类有几个构造方法允许创建线程的时候指定线程组,没有指定就属于父线程所在的线程组。
- JVM创建main线程时会为他指定一个线程组。
- 每个java线程都有一个线程组与之关联。
- 调用线程的
getThreadGroup()
方法返回该线程的线程组。 - 线程组开始是出于安全的考虑设计的,区分不同的Applet,然而线程组ThreadGroup并未实现这一目标。现在,已经很少使用线程组,一般会将一组相关的线程存放到一个数组或一个集合中。多数情况下,可以忽略线程组。
- 代码测试:创建线程组
/**
* 创建线程组
*/
public class ThreadGroupTest01 {
public static void main(String[] args) {
// 获取当前main线程所在的线程组
ThreadGroup mainGroup = Thread.currentThread().getThreadGroup();
System.out.println("main线程所在线程组:" + mainGroup);
// 定义一个线程组:不指定父线程组,默认是当前线程组的子线程组
ThreadGroup group1 = new ThreadGroup("group1");
System.out.println(group1);
// 创建一个线程:默认属于父线程的线程组main
Thread t1 = new Thread();
System.out.println(t1);
// 创建一个线程,指定所属线程组
Thread t2 = new Thread(group1, "t2");
System.out.println(t2);
}
}
- 运行结果:
2. 捕获线程的运行异常
- static
setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh)
:设置一个全局的线程由于运行时异常突然终止而调用的默认处理程序。 setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)
:设置当前线程出现运行时异常的默认处理程序。- 代码测试:
/**
* 线程出现运行时异常的处理程序
*/
public class ThreadExceptionTest {
public static void main(String[] args) {
// 设置一个全局的线程异常处理器
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
// 打印那个线程出现什么异常
System.out.println(t.getName() + "出现异常:" + e.getMessage());
}
});
// 新建一个线程
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int i = 10 / 0;
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
String str = null;
str.length();
}
}).start();
}
}
- 运行结果:
3. 注入Hook钩子线程
- 现在很多软件包括MySql、Zookeeper、Kafka等都存在Hook线程的校验机制。Hook线程的目的是校验进程是否已启动,防止重复启动程序。
- Hook线程也称为钩子线程,当JVM退出的时候会执行Hook线程。
- 在程序启动的时候会创建一个.lock文件,用.lock文件校验程序是否已经启动,当程序退出(JVM退出)时删除该.lock文件,经常在Hook线程中删除该.lock文件。
- Hook线程除了可以防止重启进程外,还可以做资源释放,尽量避免在Hook线程中进行复杂的操作。
- 代码演示:程序启动的时候创建tmp.lock文件,程序退出时,在Hook线程中删除tmp.lock文件。在程序启动时判断tmp.lock文件是否存在,如果存在就说明程序已经启动,不要重复启动程序。
/**
* Hook函数测试
*/
public class HookTest {
public static void main(String[] args) {
// 使用Runtime类添加一个Hook钩子函数
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
System.out.println("JVM退出,启动Hook线程,在Hook线程中删除.lock文件");
// 获取.lock文件的路径转化为File对象,删除文件
getFile().toFile().delete();
}
});
// 判断.lock文件是否存在
if(getFile().toFile().exists()) {
// 如果该文件已存在,说明程序已经启动
throw new RuntimeException("程序已经启动---");
} else {
// 如果该文件不存在,就创建一个.lock文件
try {
getFile().toFile().createNewFile();
System.out.println("程序启动===,创建了tmp.lock文件");
} catch (IOException e) {
e.printStackTrace();
}
}
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(1000);
System.out.println("程序运行中。。。");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 获得.lock文件的路径
public static Path getFile() {
// 返回一个当前目录下的.lock文件路径
return Paths.get("", "tmp.lock");
}
}
4. 线程池
4.1 什么时线程池
- 当一个线程的run()方法结束以后,线程对象就会被GC释放。
- 线程的开销主要包括:
- 创建和启动线程的开销。
- 线程销毁的开销。
- 线程调度的开销。
- 线程的数量受限CPU处理器数量。
- 线程池内部可以预先创建一定数量的线程。线程池将工作任务缓存在工作队列中,线程池的工作队列不断从队列中取出任务执行。
4.2 线程池的基本使用
- 使用
Executors
线程池工具类创建线程池,使用executor()
方法或submit()
方法将任务添加到线程池中,任务就存储到线程池的阻塞队列中,线程池中的线程就从阻塞队列中取任务执行。 shutdown()
方法:表示线程池不再接收任务,已经接收的任务会执行完毕。- 代码演示:
/**
* 线程池的基本使用
*/
public class ThreadPoolTest01 {
public static void main(String[] args) {
// 使用Executors线程池工具类,创建一个线程池,包含5个线程
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
// 创建18个任务交给线程池执行
for (int i = 0; i < 18; i++) {
fixedThreadPool.execute(new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getId() + "号线程执行任务");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
}
- 运行分析:5个线程同时执行任务,执行完成后再继续执行。
4.3 ScheduleExecutorService调度线程任务
ScheduleExecutorService
线程池,可以对任务进行调度。shedule(Runnable任务,time等待时间,TimeUnit时间单位)
:等待多长时间后执行任务。sheduleAtFixedRate(Runnable任务,time等待时间,time间隔时间,TimeUnit时间单位)
:间隔多长时间执行一次任务,如果线程的执行时间超过间隔时间,再线程执行完后,立马执行下一次任务。scheduleWithFixedDelay(Runnable任务,time等待时间,time间隔时间,TimeUnit时间单位)
:间隔多长时间执行一次任务,如果线程的执行时间超过间隔时间,仍然需要等待间隔时间之后再执行下一次任务。- 代码演示:
/**
* 具有调度任务功能的线程池
*/
public class ScheduleExecutorServiceTest {
public static void main(String[] args) {
// 创建一个具有调度功能的线程池
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
// schedule(Runnable任务, 等待时间, 时间单位)
scheduledExecutorService.schedule(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getId() + "执行任务" + System.currentTimeMillis());
}
}, 2, TimeUnit.SECONDS);
// 以固定频率执行任务 scheduleAtFixedRate(Runnable任务, 执行时间, 间隔时间执行任务, 时间单位)
// 如果线程执行时间超过了线程间隔的时间,再线程执行完成后马上执行下一次任务
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getId() + "调度执行任务" + System.currentTimeMillis());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, 3, 2, TimeUnit.SECONDS);
// 以固定的频率执行任务,如果线程执行时间超过间隔的时间,还是需要等待间隔时间之后再执行下一次任务
scheduledExecutorService.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getId() + "调度执行任务" + System.currentTimeMillis());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, 3, 2, TimeUnit.SECONDS);
}
}
4.4 核心线程池的底层实现原理
-
Executors线程池工具类中创建线程池的常用方法:
- ExecutorService
newCachedThreadPool()
:创建一个线程池,重用以前的线程。如果没有可用的线程,将创建一个新的线程并将其添加到该池中。 未使用六十秒的线程将被终止并从缓存中删除。 因此,长时间保持闲置的池将不会消耗任何资源。适合执行大量耗时短提交频繁的任务。 - ExecutorService
newFixedThreadPool(int nThreads)
:创建一个特定数量线程的线程池,该线程池重用固定数量的从共享无界队列中运行的线程。 - ExecutorService
newSingleThreadExecutor()
:创建一个使用从无界队列运行的单个工作线程的执行程序。
- ExecutorService
-
这些创建线程池的方法,都是调用的
ThreadPoolExecutor()
线程池实现类的构造方法。 -
ThreadPoolExecutor使用一个int高3位,来表示线程状态,低29位表示线程的数量。(为什么不使用两个整数,一个表示线程状态,一个表示线程数量?只使用一次CAS就可以更新两个状态。)
-
构造方法源码如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
参数 | 含义 |
---|---|
corePoolSize | 线程池中核心线程的数量 |
maximumPoolSize | 线程池中最大线程数量 |
keepAliveTime | 当线程池中的线程数量超过corePoolSize时,多余线程存活时长 |
unit | keepAliveTime的时长单位 |
workQueue | 任务队列,把任务提交到该任务队列中等待执行 |
threaFactory | 线程工厂用于创建线程 |
headler | 拒绝策略,当任务太多来不及处理时,如何拒绝 |
workQueue
工作队列是指提交未执行的任务队列,它是BolckingQueue
接口的对象,仅用于存储Runnable任务,根据队列分类,ThreadPoolExecutor构造函数可以使用以下几种阻塞队列:
阻塞队列 | 作用 |
---|---|
SynchronousQueue | 直接提交队列,该对象没有容量,提交给线程池的任务不会被真正的保存,一个任务由一个线程处理,如果没有空闲线程就创建新的线程,如果线程数量达到了maximumPoolSize,就执行拒绝策略 |
ArrayBlockingQueue | 有界任务队列,再创建ArrayBlockingQueue对象时,可以指定一个容量。当有任务需要执行时,线程池中的线程数小于corePoolSize核心线程数就创建新的线程;大于corePoolSize核心线程数就加入等待队列;如果队列满了就无法加入;如果线程池中的线程数量小于maximumPoolSize就会创建新的线程执行;如果线程数大于maximumPoolSize就执行拒绝策略 |
LinkedBlockingQueue | 无界任务队列,除非系统资源耗尽,否则不存在任务入队失败的情况;当有新的任务时,线程池中的线程数量小于corePoolSize就创建新的线程;大于corePoolSize就把任务加入队列 |
priorityBlockingQueue | 优先级队列,是一个特殊的无界队列;优先级队列不是按照先进先出,根据任务优先级执行 |
拒绝策略
:当线程数已经用完,等待队列也满了,无法为新的任务提供服务,就通过拒绝策略处理问题。- ThreadPoolExecutor类中四个静态内部类,内置了四种拒绝策略:
类 | 拒绝策略 |
---|---|
AbortPolicy | 抛出异常(Executors线程池工具类的默认拒绝策略) |
CallerRunsPolicy | 只要线程池没有关闭,会再调用者线程中运行当前被丢弃的任务 |
DiscardOldestPolicy | 将任务队列中最老的任务丢弃,尝试再次提交新的线程 |
DiscardPolicy | 直接丢弃这个无法处理的任务 |
- 如果内置的拒绝策略无法满足实际需要,可以扩展RejectedExecutorHandler接口。
ThreadFactory
线程工厂:线程池中的线程从那里来的?由线程工厂创建。- ThreadFactory是一个接口,只有一个方法用来创建线程。
- 代码演示:自定义线程工厂
/**
* 自定义线程工厂
*/
public class ThreadFactoryTest {
public static void main(String[] args) throws InterruptedException {
Runnable r = new Runnable() {
@Override
public void run() {
// 生成一个随机数
int num = new Random().nextInt(5);
System.out.println(Thread.currentThread().getId() + "生成了" + num);
try {
Thread.sleep(num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
// 创建一个线程池
ExecutorService executorService = new ThreadPoolExecutor(5, 5, 0, TimeUnit.SECONDS, new SynchronousQueue<>(), new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
// 自定义线程工厂,创建线程
Thread t = new Thread(r);
// 设置t为守护线程,当main线程退出以后,就退出
t.setDaemon(true);
System.out.println("通过自定义线程工厂创建线程");
return t;
}
});
// 提交5个任务,超过5个会抛出异常
for (int i = 0; i < 5; i++) {
executorService.submit(r);
}
Thread.sleep(5000);
}
}
4.5 监控线程池
- ThreadPoolExecutor类中有许多方法,能够监控线程池的状态。
方法 | 监控 |
---|---|
getCorePoolSize() | 当前核心线程的数量 |
getMaxmumPoolSize() | 最大线程数量 |
getPoolSize() | 当前线程池的数量 |
getActiveCount() | 活动线程的数量 |
getTaskCount() | 收到任务数量 |
getCompletedTaskCount() | 完成任务数量 |
getQueue().size() | 队列中等待任务的数量 |
- 测试代码:
/**
* 使用ThreadPoolExecutor类中的方法监控线程池
*/
public class SpyThreadPool {
public static void main(String[] args) {
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getId() + "线程开始执行" + System.currentTimeMillis());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
// 创建一个线程池
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(2, 5, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardPolicy());
for (int i = 0; i < 30; i++) {
try {
// 提交30个任务
poolExecutor.submit(r);
System.out.println("当前线程核心线程数数量:" + poolExecutor.getCorePoolSize() + ",最大线程数量:" + poolExecutor.getMaximumPoolSize()
+ ",当前线程池大小:" + poolExecutor.getPoolSize() + ",活动线程数量:" + poolExecutor.getActiveCount()
+ ",收到任务数量:" + poolExecutor.getTaskCount() + ",完成任务的数量:" + poolExecutor.getCompletedTaskCount()
+ "等待任务数:" + poolExecutor.getQueue().size());
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
4.6 扩展线程池
- 有时需要对线程进行扩展,如在监控每个任务的开始和结束时间,或者自定义一些其他增强方法。
- 线程池ThreadPoolExecutor提供了两个方法:
- protected void
afterExecutor(Runnable r, Throwable t)
- protected void
beforeExecutor(Thread t, Runnable r)
- protected void
- 在ThreadPoolExecutor源码中,该类定义一个内部类
Worker
,线程池中工作线程就是Worker实例。执行任务时,在执行任务之前会调用beforeExecutor(Thread t, Runnable r)方法,在执行任务之后会调用afterExecutor(Runnable r, Throwable t)方法。 - 代码演示:
/**
* 扩展线程池
*/
public class ThreadPoolExecutor02 {
// 新建一个任务类
static class MyTask implements Runnable{
String name;
public MyTask(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "线程正在执行:" + name + "任务");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
// 新建一个线程池,对线程池进行扩展: 1.编写一个类继承ThreadPoolExecutor; 2.使用匿名内部类
// 匿名内部类:重写beforeExecutor方法
ExecutorService executorService = new ThreadPoolExecutor(5, 5 , 0, TimeUnit.SECONDS, new LinkedBlockingDeque<>()) {
// 重写beforeExecutor方法
@Override
protected void beforeExecute(Thread t, Runnable r) {
System.out.println(Thread.currentThread().getName() + "准备执行任务");
}
// 重写afterExecutor方法
@Override
protected void afterExecute(Runnable r, Throwable t) {
System.out.println(Thread.currentThread().getName() + "执行任务完毕");
}
// 重写terminated方法:线程池退出时,调用此方法
@Override
protected void terminated() {
System.out.println("线程池退出了");
}
};
// 创建5个任务
for (int i = 0; i < 5; i++) {
executorService.execute(new MyTask("任务" + i));
}
// shutdown()方法表示线程池不再接收任务
executorService.shutdown();
}
}
4.7 优化线程池的大小
- 线程池的大小对系统性能是有一定影响的,过大和过小都无法发挥最优系统性能。
- 阿里明确规定不能使用JDK自定义的Executors线程池工具类提供的线程池,可能造成的一些问题:
newCachedThreadPool
:每个任务都会创建一个线程进行执行,并发量过大可能会造成CPU的资源一直消耗,导致CPU使用率过高造成卡死。newFixedThreadPool
:可能导致内存飙升,造成OOM。核心线程数就是总线程数,线程达到数量就将任务放到工作队列中,使用的是无界的工作队列,任务堆积导致内存飙升。newSingleThreadExecutor
:一个线程执行任务太少。
- 估算线程池中线程多少的公式:CPU的数量 * 目标CPU的使用率 * (1 + 等待时间与计算时间之比)
4.8 线程池的死锁
- 如果线程池中只有一个线程,执行的任务A需要等待任务B的执行结果,然而任务B需要任务A执行结束后执行,两个任务一直等待就造成了死锁。
- 适合给线程池提交相互独立的任务,而不是彼此依赖的任务,对于彼此依赖的任务,提交给不同的线程池。
4.9 线程池中的异常捕获
- 使用
submit()
方法提交任务,线程池可能会吃掉产生的异常。 - 代码演示:
/**
* 使用submit()提交任务,线程池可能会吃掉异常
*/
public class ExeceptionTest {
// 定义一个除法运算
static class DivTask implements Runnable{
private int x;
private int y;
public DivTask(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ":" + x + " / " + y + " = " + x/y);
}
}
public static void main(String[] args) {
// 新建一个线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(0,Integer.MAX_VALUE, 0, TimeUnit.SECONDS, new SynchronousQueue<>());
for (int i = 0; i < 5; i++) {
// 会有一个10 / 0
executor.submit(new DivTask(10, i));
}
}
}
-
运行结果:在除法运算中有5个线程进行,有一个10 /0 ,但是只有4个线程输出,数学异常被线程池吃掉了。
-
解决方法:
- 使用
execute()
方法提交任务。 - 自定义线程池继承ThreadPoolExecutor类,重写submit()方法。
- 使用
-
使用自定义线程池继承ThreadPoolExecutor类,
/**
* 使用submit()提交任务,线程池可能会吃掉异常
*/
public class ExceptionTest {
// 自定义线程池,重写submit方法
static class MyThreadPoolExecutor extends ThreadPoolExecutor {
public MyThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
// 定义一个抛出异常的方法
private Runnable swap(Runnable task) {
return new Runnable() {
@Override
public void run() {
try {
task.run();
} catch (Exception ex) {
ex.printStackTrace();
throw ex;
}
}
};
}
// 重写submit方法
@Override
public Future<?> submit(Runnable task) {
return super.submit(swap(task));
}
}
// 定义一个除法运算
static class DivTask implements Runnable{
private int x;
private int y;
public DivTask(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ":" + x + " / " + y + " = " + x/y);
}
}
public static void main(String[] args) {
// 新建一个线程池
// ThreadPoolExecutor executor = new ThreadPoolExecutor(0,Integer.MAX_VALUE, 0, TimeUnit.SECONDS, new SynchronousQueue<>());
// 新建自定义线程池
ThreadPoolExecutor executor = new MyThreadPoolExecutor(0,Integer.MAX_VALUE, 0, TimeUnit.SECONDS, new SynchronousQueue<>());
for (int i = 0; i < 5; i++) {
// 会有一个10 / 0
executor.submit(new DivTask(10, i));
}
}
}
- 运行结果:
十二、ForkJoinPool线程池
- 采用“分而治之”的思想。
- 将一个大任务使用
fork()
拆分为多个小任务,在使用join()
合并成一个大的结果。 - 系统对线程池进行了优化,提交的任务数量不一定与线程是一一对应的关系,在多数情况下,一个线程需要处理多个任务。
- 线程A执行完任务后,如果线程B的等待队列中还有任务,线程A会从等待队列的最后取出任务,帮助线程B执行。
- ForkJoinPool线程池最常用的方法是
<T> ForkJoinTask<T> submit(ForkJoinTask<T> task)
,向线程池中提交一个ForkJoinTask
任务。 - ForkJoinTask任务支持fork()分解与join()等待。有两个重要的子类
RecursiveAction
(没有返回值)和RecursiveTask
(带有返回值)。 - 代码演示:计算一个很大数列之和,分解成多个小任务进行计算。
/**
* 使用ForkJoinPool线程池计算一个大的数列的和
*/
public class ForkJoinPoolTest {
// 定义一个任务继承RecursiveTask
static class CountTask extends RecursiveTask<Long> {
// 定义一个阈值,如果超过10000个数就需要分解成多个任务进行计算
private static final int THRESHOLD = 10000;
// 定义一个分解任务的数量,超过阈值就分解成100个任务进行计算
private static final int TASKNUM = 100;
// 定义计算数列的开始值和结束值
private long start;
private long end;
public CountTask(long start, long end) {
this.start = start;
this.end = end;
}
// 重写它的计算方法,计算数列的结果
@Override
protected Long compute() {
// 定义一个数保存计算结果
long sum = 0;
// 判断需要计算的数是否大于阈值
if(end - start < THRESHOLD) {
// 小于阈值就直接进行计算
for (long i = start; i <= end; i++) {
sum += i;
}
} else {
// 如果超过阈值,就需要分解任务进行计算
// 分成100个任务,每个需要计算的数
long step = (end - start) / TASKNUM;
// 创建一个存储任务的集合
List<CountTask> taskList = new ArrayList<>();
// 每个任务数列开始的位置
long pos = start;
// 循环向集合中添加子任务
for (int i = 0; i < TASKNUM; i++) {
// 结束的位置
long lastOne = pos + step;
// 如果最后的任务
if(lastOne > end) {
lastOne = end;
}
// 新建一个子任务
CountTask subTask = new CountTask(pos, lastOne);
// 将子任务加入到集合中
taskList.add(subTask);
// 提交子任务
subTask.fork();
// 调整下一个任务开始的位置
pos += step + 1;
}
// 取出所有的子任务进行计算
for (CountTask task : taskList) {
// join会一直等待子任务执行完毕返回计算结果
sum += task.join();
}
}
return sum;
}
}
public static void main(String[] args) {
// 创建一个ForkJoinPool线程池
ForkJoinPool forkJoinPool = new ForkJoinPool();
// 创建一个大的任务
CountTask task = new CountTask(0,20000);
// 将任务提交给线程池
ForkJoinTask<Long> result = forkJoinPool.submit(task);
try {
// get()方法获取计算结果
System.out.println("计算结果为:" + result.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
// 验证一下
long res = 0;
for (long i = 0; i <= 20000; i++) {
res += i;
}
System.out.println(res);
}
}
十三:保障线程安全的设计技术
1. java运行时存储空间
- java运行时空间分为栈区、堆区、方法区(非堆区)。
- 栈空间(Stack Space)是为线程执行准备的一段固定大小的存储空间,每个线程都有独立的栈空间。在线程栈中每个方法都会分配一个栈帧,栈帧用于存储方法的局部变量,返回值等数据。所以局部变量具有固有的线程安全性。
- 堆空间(Heap Space)用于存储对象,是在JVM启动时分配一段可以动态扩容的内存空间。创建对象时,在堆空间中给对象分配存储空间,实例变量存储在堆空间中。堆空间是多个线程可以共享的空间,多个线程操作实例变量,可能存在线程安全问题。
- 方法区(非堆空间Non-Heap Space)用于存储常量、类的元数据等,非堆空间也是在JVM启动的时候分配的一段可以动态扩容的存储空间。类的元数据包括静态变量、类的方法以及方法的元数据。因此访问非堆空间中的静态变量也可能存在线程安全问题。
2. 无状态对象
- 对象就是数据和数据操作的封装。
- 无状态对象就是不包含任何实例变量也不包含任何静态变量的对象。
- 线程安全问题就是多个线程共享数据,没有了共享数据就不存在线程安全问题。