进程、线程、并发、并行:
线程的调度:
分时调度
所有的线程轮流使用 cpu 的使用权,平均分配每个线程占用 cpu 的时间
抢占式调度
优先让优先级高的线程使用 cpu,如果线程的优先级相同的,那么会随机选择一个线程执行(线程的随机性),java 使用的就是抢占式调度
新建(NEW):至今尚未启动的线程处于这种状态
运行(RUNNABLE):正在 Java 虚拟机中执行的线程处于这种状态
阻塞(BLOCKED):受阻塞并等待某个监视器锁的线程处于这种状态
无限等待(WAITING):无限期地等待另一个线程来执行某一特定操作的线程处于这种状态
睡眠(计时等待)(TIMED_WAITING):等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态
死亡(TERMINATED):已退出的线程处于这种状态
Object类中等待与唤醒线程的方法:
java.lang.Object 类:是祖宗类,里边的方法,任意的一个类都可以使用
void wait() // 让当前进程等待。可以在其他线程调用此对象的 notify() 方法或 notifyAll() 方法唤醒当前进程
void notify() // 唤醒在此对象监视器(同步锁,对象锁)上等待的单个线程。
void notifyAll() // 唤醒在此对象监视器(同步锁,对象锁)上等待的所有线程。
/*注意:
1.wait和notify方法一般使用在同步代码块中 ==> 有锁对象 ==> 对象监视器
2.一般都使用锁对象调用wait和notify方法(多个线程使用的是同一个锁对象)
Thread-0线程使用锁对象 ==> wait方法 ==> Thread-0线程进入到等待
Thread-1线程使用锁对象 ==> notify方法 ==> 唤醒在锁对象上等待的Thread-0线程
3.在同步中的线程调用wait方法,进入到等待状态,会释放锁对象
在同步中的线程调用sleep方法,进入睡眠,不会释放锁对象
*/
主要有继承 Thread 类和实现 Runnable 接口两种方式。不过实际开发中一般都是通过线程池方式来获取空闲线程。
实现 Runnable 接口(推荐)
java.lang.Thread类 implements Runnable接口
Thread类的构造方法:
Thread(Runnable target) //传递Runnable接口的实现类对象
Thread(Runnable target, String name) //传递Runnable接口的实现类对象和线程名称
实现步骤:
1.创建一个实现类,实现 Runnable 接口
2.在实现类中重写 Runnable 接口中的 run 方法(设置线程任务)
3.创建 Runnable 接口的实现类对象
4.创建 Thread 类对象,在构造方法中传递 Runnable 接口的实现类对象
5.调用 Thread 类中的 start() 方法,开启新的线程,执行 run 方法
继承 Thread 类
Thread 类本质上是实现了 Runnable 接口的一个实例;启动线程的唯一方法就是通过 Thread 类的 start() 实例方法。start()方法是一个 native 方法,它将启动一个新线程,并执行 run()方法。
实现步骤:
1.创建一个类继承 Thread 类
2.在 Thread 类的子类中,重写 Thread 类中的 run 方法(设置线程任务)
3.创建 Thread 类的子类对象
4.调用继承自 Thread 类中的 start() 方法,开启新的线程执行 run 方法
两种方式的比较
使用实现 Runnable 接口的方式创建多线程程序,可以避免单继承的局限性
使用实现 Runnable 接口的方式创建多线程程序,把设置线程任务和开启线程进行了解耦(解除了耦合性,增强扩展性)
类继承了 Thread 类,在run方法设置什么任务,创建子类对象就只能执行什么任务(耦合性强)
类实现 Runnable 接口目的:重写 run 方法设置线程任务
创建Thread类对象的目的:传递不同的Runnable接口的实现类对象(传递不同的任务),执行不同的任务
// 返回对当前正在执行的线程对象的引用
static Thread currentThread()
// 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),让线程睡眠,到时间睡醒了,继续执行
static void sleep(long millis)
// 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
void start()
/* 当我们调用start方法执行,结果是两个线程并发地运行;
当前线程(main线程:运行main方法中的代码)和另一个线程(开启的新的线程:执行其 run 方法中的代码)。
两个线程会一起抢夺cpu的执行权,谁抢到了谁执行,会出现随机性打印结果*/
// 注意;多次启动一个线程是非法的(一个线程对象只能调用一次start方法)。特别是当线程已经结束执行后,不能再重新启动。
// 返回该线程的名称 // 注意:getName方法只能在Thread类的子类中使用
String getName()
// 设置线程名称
void setName(String name)
如果锁具备可重入性,则称作为可重入锁。像 synchronized 和 ReentrantLock 都是可重入锁,可重入性实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。
举个简单的例子,当一个线程执行到某个 synchronized 方法(method1)时,在 method1 中会调用另外一个 synchronized 方法 method2 ,此时线程不必重新去申请锁,而是可以直接执行方法 method2。
class MyClass {
public synchronized void method1() {
method2();
}
public synchronized void method2() {}
}
上述代码中的两个方法 method1 和 method2 都用 synchronized 修饰了,假如某一时刻,线程 A 执行到了 method1 ,此时线程A获取了这个对象的锁,而由于 method2 也是 synchronized 方法,假如 synchronized 不具备可重入性,此时线程 A 需要重新申请锁。但是这就会造成一个问题,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待永远不会获取到的锁。
由于 synchronized 和 Lock 都具备可重入性,所以不会发生上述现象。
可中断锁:就是可以响应中断的锁。
在 Java 中,synchronized 就不是可中断锁,而 Lock 是可中断锁。
如果某一线程 A 正在执行锁中的代码,另一线程 B 正在等待获取该锁,可能由于等待时间过长,线程 B 不想等待了,想先处理其他事情,可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。
公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。
非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。
Java 中的 synchronized 就是非公平锁,它无法保证等待的线程获取锁的顺序。
ReentrantLock 和 ReentrantReadWriteLock 默认情况下是非公平锁,但是可以设置为公平锁。
在ReentrantLock中定义了2个静态内部类,一个是NotFairSync,一个是FairSync,分别用来实现非公平锁和公平锁。
可以在创建ReentrantLock对象时,通过以下方式来设置锁的公平性:
ReentrantLock lock = new ReentrantLock( true );
// 如果参数为true表示为公平锁,为fasle为非公平锁。默认情况下,如果使用无参构造器,则是非公平锁。
ReentrantLock 类的常用方法:
// 判断锁是否是公平锁
boolean isFair()
// 判断锁是否被任何线程获取了
boolean isLocked()
// 判断锁是否被当前线程获取了
boolean isHeldByCurrentThread()
// 判断是否有线程在等待该锁
boolean hasQueuedThreads()
ReentrantReadWriteLock 类中也有类似的方法,同样也可以设置为公平锁和非公平锁。不过注意:ReentrantReadWriteLock 并未实现 Lock 接口,它实现的是 ReadWriteLock 接口。
读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。
因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。
ReadWriteLock 就是读写锁,它是一个接口,ReentrantReadWriteLock 实现了这个接口。
可以通过 readLock() 获取读锁,通过 writeLock() 获取写锁。
概念:线程获取不到锁对象,从而进不去同步中执行
前提:
原因:
可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
拓展:所有的共享变量(成员变量、静态成员变量)都存储于主内存。每一个线程还存在自己的工作内存,线程的工作内存保留了被线程使用的变量的工作副本。线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问
有序性:即程序执行的顺序按照代码的先后顺序执行
原子性:即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行
volatile 关键字的作用:解决变量的可见性、有序性,但不能解决变量的原子性
解决可见性:一个成员变量被 volatile 关键字修饰,当改变该变量的值后,volatile 关键字会让该变量所有的变量副本立即失效,每个线程的工作内存想要使用该变量的值,需要在主内存中重新获取
解决有序性:变量添加了volatile关键字,编译器就不会再对该变量的相关代码进行重排了
synchronized 是 java 中的一个关键字,也就是说是 Java 语言内置的特性。
如果一个代码块被 synchronized 修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:
如果这个获取锁的线程由于要等待 IO 或者其他原因(比如调用 sleep 方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,会非常影响程序的执行效率。
使用方式:
方式1(方法内同步代码块):
synchronized(锁对象){
访问了共享数据的代码(产生了线程安全问题的代码)
}
// 注意:1.锁对象可以是任意的对象:new Object(); new Person(); "aaa"==>字符串底层是一个字符是数组,也是一个对象
// 2.必须保证所有的线程使用的都是同一个锁对象
方式2(同步方法)
权限修饰符 synchronized 返回值类型 方法名(参数列表){
访问了共享数据的代码(可能产生线程安全问题的代码)
}
// 注意:1.静态的同步方法的锁对象是本类的class文件对象
// 2.非静态同步方法的锁对象是本类创建的对象,即 this
包路径:java.util.concurrent
主要内容:
Hashtable和 ConcurrentHashMap 有什么区别:
Hashtable:采用的 synchronized ——悲观锁,效率低
Hashtable 容器使用 synchronized 来保证线程安全,但在线程竞争激烈的情况下 Hashtable 的效率非常低下。
因为其锁定的是整个哈希表,一个操作正在进行时,其他操作也同时锁定;当一个线程正在访问 Hashtable 的同步方法,其他线程也访问 Hashtable 的同步方法时,会进入阻塞状态;例如线程1使用 put 进行元素添加,线程2不但不能使用 put 方法添加元素,也不能使用 get 方法来获取元素,所以竞争越激烈效率越低。
Hashtable 是 Java 类库中从 1.0 版本提供的一个线程安全的 Map,但已过时
ConcurrentHashMap:采用 CAS + 局部(synchronized)锁定
CAS机制——乐观锁,效率更高。
局部(synchronized)锁定:只锁定 “桶” 。仅对当前元素锁定,其他 ”桶“ 里的元素不锁定。
概述:java 从 JDK1.5 开始提供了 java.util.concurrent.atomic 包(简称 Atomic 包),这个包中的原子操作类提供了一种用法简单,性能高效,线程安全地更新一个变量的方式。
属于乐观锁,只能解决一个变量的原子性,底层是 CAS 机制,反复比较,只有内存中的值和预期的值一样,才会进行修改,否则就会循环重新获取值。
常用原子类:AtomicInteger、AtomicLong、AtomicBoolean、AtomicIntegerArray
// 构造方法:
public AtomicInteger(): // 初始化一个默认值为0的原子型Integer
public AtomicInteger(int initialValue) // 初始化一个指定值的原子型Integer
// 成员方法:
int get(): // 获取值
int getAndIncrement(): // 以原子方式将当前值加1。注意,这里返回的是自增前的值。
int incrementAndGet(): // 以原子方式将当前值加1。注意,这里返回的是自增后的值。
int addAndGet(int data): // 以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。
int getAndSet(int value): // 以原子方式设置为newValue的值,并返回旧值。
int
数组:可以保证数组的原子性// 构造方法
AtomicIntegerArray(int length) // 创建指定长度的给定长度的新 AtomicIntegerArray
AtomicIntegerArray(int[] array) // 创建与给定数组具有相同长度的新 AtomicIntegerArray,并从给定数组复制其所有元素
// 成员方法:
int addAndGet(int i, int delta) // 以原子方式将给定值与索引 i 的元素相加
// i:获取指定索引处的元素
// delta:给元素增加的值
int get(int i) // 获取指定索引处元素的值
参考:Lock接口常用方法
常用的 Lock 锁:
ReentrantLock
java.util.concurrent.locks.ReentrantLock类 implements Lock接口
格式:
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
//处理任务
} catch (Exception ex){
} finally {
lock.unlock(); //释放锁
}
if (lock.tryLock()) {
try {
//处理任务
} catch (Exception ex){
} finally {
lock.unlock(); //释放锁
}
} else {
//如果不能获取锁,则直接做其他事情
}
常用方法:
// 获取锁(获取不到锁时会一直尝试获取锁,不能中断)
void lock();
// 释放锁
void unlock();
// 尝试获取锁(获取成功,返回 true,否则返回 false,不会阻塞;常用if语句判断 tryLock() 的返回结果)
boolean tryLock();
// 尝试获取锁(在指定时间内获取到锁,返回 true;在指定时间内未获取到锁,返回 false,不会阻塞,并且可以响应中断)
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 可响应中断的获取锁(获取不到锁时会一直尝试获取锁,线程可以使用 interrupt()方法进行响应中断)
void lockInterruptibly() throws InterruptedException;
用给定的计数初始化 CountDownLatch。由于调用了 countDown() 方法,所以在当前计数到达零之前,await 方法会一直受阻塞。
CountDownLatch 是通过一个计数器来实现的,每当一个线程完成了自己的任务后,可以调用countDown()方法让计数器 -1,当计数器到达 0 时,调用 CountDownLatch 的 await() 方法的线程阻塞状态解除,继续执行。
// 构造方法:
CountDownLatch(int count) //构造一个用给定计数初始化的 CountDownLatch
// 参数:
// int count:传递计数器的初始化数字
// 成员方法:
void await() // 在内部的计数器值清零之前,会一直让线程等待
void countDown() // 让内部计数器的值减1
// 注意:
// 必须保证多个线程使用的是同一个CountDownLatch对象
// 在多个线程类中定义CountDownLatch变量,使用带参构造方法给CountDownLatch变量赋值
示例:
// 线程1要执行打印:A和C,线程2要执行打印:B,但线程1在打印A后,要线程2打印B之后才能打印C,所以:线程1在打印A后,必须等待线程2打印完B之后才能继续执行打印C。
public class MyThreadAC extends Thread{
//定义成员变量CountDownLatch
private CountDownLatch countDownLatch;
//使用带参数构造方法给CountDownLatch变量赋值
public MyThreadAC(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
System.out.println("A");
//使用CountDownLatch对象中的方法await,让线程等待,等待CountDownLatch对象内部计数器的值变成0在执行
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("C");
}
}
public class MyThreadB extends Thread {
//定义成员变量CountDownLatch
private CountDownLatch countDownLatch;
//使用带参数构造方法给CountDownLatch变量赋值
public MyThreadB(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
System.out.println("B");
//使用CountDownLatch对象中的方法countDown让计数器的值-1
countDownLatch.countDown();
}
}
public class Demo01Thread {
public static void main(String[] args) throws InterruptedException {
//创建CountDownLatch对象,分别传递给每一个线程使用
CountDownLatch cdl = new CountDownLatch(1);//创建内部计数器的值为1
new MyThreadAC(cdl).start();
Thread.sleep(1000);//睡眠1秒钟,保证AC线程先执行
new MyThreadB(cdl).start();
}
}
一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点。即设置屏障,一个线程等待其他多个线程全部执行完毕,再执行
使用场景:CyclicBarrier 可以用于多线程计算数据,最后合并计算结果的场景。
需求:使用两个线程读取2个文件中的数据,当两个文件中的数据都读取完毕以后,进行数据的汇总操作。
构造方法:
// 创建一个新的 CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,并在启动 barrier 时执行给定的屏障操作,该操作由最后一个进入 barrier 的线程执行。
CyclicBarrier(int parties, Runnable barrierAction)
// 参数:
// int parties:设置的屏障的数量,设置的线程数量,设置几个线程执行完,再让其他的线程执行
// Runnable barrierAction:达到屏障之后执行的线程
常用方法:
int await() // 每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞
示例:
// 公司召集5名员工开会,等5名员工都到了,会议开始。
// 创建5个员工线程,1个开会线程,几乎同时启动,使用CyclicBarrier保证5名员工线程全部执行后,再执行开会线程。
// 员工线程
public class PersonThread extends Thread {
//定义一个成员变量CyclicBarrier
private CyclicBarrier cyclicBarrier;
//使用带参数构造方法给CyclicBarrier变量赋值
public PersonThread(CyclicBarrier cyclicBarrier) {
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
try {
int r = (int)(Math.random()*1000);//获取一些随机的数字
Thread.sleep(r);
System.out.println(Thread.currentThread().getName()+"线程花了"+r+"毫秒来到了会议的现场!");
cyclicBarrier.await();//把CyclicBarrier内部的屏障数-1
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 开会线程
public class MeetingThread extends Thread {
@Override
public void run() {
System.out.println("人齐了,开始开会了!");
}
}
public class Demo01CyclicBarrier {
public static void main(String[] args) {
//创建CyclicBarrier对象,屏障数的值5,当屏障数的值变成0,就会执行参数传递的线程对象的run方法
CyclicBarrier cb = new CyclicBarrier(5,new MeetingThread());
//创建5个人的线程,并开启线程
PersonThread p1 = new PersonThread(cb);
PersonThread p2 = new PersonThread(cb);
PersonThread p3 = new PersonThread(cb);
PersonThread p4 = new PersonThread(cb);
PersonThread p5 = new PersonThread(cb);
p1.start();
p2.start();
p3.start();
p4.start();
p5.start();
}
}
// 执行结果:
/*
Thread-4线程花了192毫秒来到了会议的现场!
Thread-3线程花了594毫秒来到了会议的现场!
Thread-2线程花了616毫秒来到了会议的现场!
Thread-5线程花了809毫秒来到了会议的现场!
Thread-1线程花了884毫秒来到了会议的现场!
人齐了,开始开会了!
*/
主要作用是控制线程的并发数量,设置同时允许几个线程执行。
构造方法:
public Semaphore(int permits)
public Semaphore(int permits, boolean fair)
// 参数:
// permits 表示许可线程的数量
// fair 表示公平性,如果这个设为 true 的话,下次执行的线程会是等待最久的线程
常用方法:
void acquire() // 表示获取许可 lock(获取锁对象)
void release() // 表示释放许可 unlock(归还锁对象)
示例:同时允许2个线程同时执行
// 教室的线程类
public class ClassRoom {
//创建Semaphore对象,参数传递2,表示可以允许2个线程获取许可执行
Semaphore semaphore = new Semaphore(2);
//定义线程进入到教室参观的方法
public void intoClassRoom() throws InterruptedException {
semaphore.acquire();//表示获取许可,允许2个线程获取许可执行
System.out.println(Thread.currentThread().getName()+"...进入到教室参观!");
Thread.sleep(2000);//让线程在教室参观2秒钟
System.out.println(Thread.currentThread().getName()+"...离开了教室!");
semaphore.release();//表示释放许可
}
}
//学生的线程类
public class StudentThread extends Thread {
//定义一个教室变量
private ClassRoom classRoom;
//使用带参数构造方法给ClassRoom变量赋值
public StudentThread(ClassRoom classRoom) {
this.classRoom = classRoom;
}
@Override
public void run() {
try {
//调用ClassRoom里边参观的方法
classRoom.intoClassRoom();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 测试类
public class Demo01Semaphore {
public static void main(String[] args) {
//创建一个教室
ClassRoom cr = new ClassRoom();
//创建5名学生对象,进入到教室参观
for (int i = 0; i < 5; i++) {
new StudentThread(cr).start();
}
}
}
// 执行结果:
/*
Thread-1...进入到教室参观!
Thread-0...进入到教室参观!
Thread-0...离开了教室!
Thread-2...进入到教室参观!
Thread-1...离开了教室!
Thread-3...进入到教室参观!
Thread-2...离开了教室!
Thread-4...进入到教室参观!
Thread-3...离开了教室!
Thread-4...离开了教室!
*/
Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger 用于进行线程间的数据交换。
两个线程通过 exchange 方法交换数据,如果第一个线程先执行 exchange() 方法,它会一直等待第二个线程也执行 exchange 方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。
注意:
保证两个交互的线程使用的是同一个 Exchanger 对象,定义成员变量,使用构造方法赋值
exchange 具有阻塞特性,等待对方线程
使用场景:
可以做数据校对工作
比如需要将纸制银行流水通过人工的方式录入成电子银行流水。为了避免错误,采用 AB 岗两人进行录入,录入到两个文件中,系统需要加载这两个文件,并对两个文件数据进行校对,看看是否录入一致
构造方法:
public Exchanger()
常用方法:
V exchange(V x) // 参数传递给对方的数据,返回值接收对方返回的数据
V exchange(V x, long timeout, TimeUnit unit)
// 参数:
// long timeout:设置等待的时长。超时抛出异常,结束等待
// TimeUnit unit:设置等待的时间单位(秒,分钟,小时,天)
示例:
public class ThreadA extends Thread {
private Exchanger<String> exchanger;
public ThreadA(Exchanger<String> exchanger) {
this.exchanger = exchanger;
}
@Override
public void run() {
System.out.println("线程A开始执行");
System.out.println("线程A给线程B100元,并从线程B得到一张火车票!");
String result = null;
try {
result = exchanger.exchange("100元");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程A得到的东西:"+result);
}
}
public class ThreadB extends Thread{
private Exchanger<String> exchanger;
public ThreadB(Exchanger<String> exchanger) {
this.exchanger = exchanger;
}
@Override
public void run() {
System.out.println("线程B开始执行");
System.out.println("线程B给线程A一张火车票,并从线程A得到100元!");
String result = null;
try {
result = exchanger.exchange("一张火车票");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程B得到的东西:"+result);
}
}
public class Demo01Exchanger {
public static void main(String[] args) {
//创建Exchanger对象
Exchanger<String> exchanger = new Exchanger<>();
//创建线程A对象,并运行
new ThreadA(exchanger).start();
new ThreadB(exchanger).start();
}
}
// 执行结果:
/*
线程A开始执行
线程A给线程B100元,并从线程B得到一张火车票!
线程B开始执行
线程B给线程A一张火车票,并从线程A得到100元!
线程B得到的东西:100元
线程A得到的东西:一张火车票
*/
常见的锁机制有:
volatile 关键字和 synchronized 关键字的区别:
synchronized 和 lock 锁的区别:
Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内置的语言实现
synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生
Lock 在发生异常时,若没有主动调用 unLock() 去释放锁,则很可能造成死锁现象,故使用 Lock 时需要在 finally 块中释放锁
Lock 可以让等待锁的线程响应中断
synchronized 不能够响应中断,等待的线程会一直等待下去
Lock 可以返回有没有成功获取锁,而 synchronized 却无法办到
Lock 可以提高多个线程进行读操作的效率
性能上,如果竞争资源不激烈,两者的性能是差不多的;当竞争资源非常激烈时(即高并发时),Lock 的性能要远远优于 synchronized,在具体使用时要根据适当情况选择。
因为 synchronized 是悲观锁,同一时间只能有一个线程获取到锁,而 Lock 提供了多样化的同步,比如有时间限制的同步,超时可以中断同步,比如使用读写锁(ReadWriteLock ),可以实现读写分离,提高多个线程进行读操作的效率。
CAS(乐观锁) 和 Synchronized(悲观锁)的区别:
悲观锁 是从悲观的角度出发:总是假设最坏的情况,每次去拿数据的时候都认为其他线程会修改,所以每次在拿数据的时候都会上锁,这样其他线程要拿这个数据就会阻塞直到它拿到锁。
共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程
Synchronized 和 ReentrantLock 都是悲观锁
CAS(乐观锁)是从乐观的角度出发:总是假设最好的情况,每次去拿数据的时候都认为其他线程不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间其他线程有没有去更新这个数据。
Threadlocal :
将数据绑定到当前线程上,同一个线程中。同一线程中经过的不同方法都可以从 Threadlocal 获取数据,并且获取的数据是同一个对象。
即 ThreadLocal 提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。通过 get 和 set 方法就可以得到当前线程对应的值。
Threadlocal 使用的方法:
ThreadLocal<T> sThreadLocal = new ThreadLocal<T>();
sThreadLocal.set(t) // 将数据绑定到当前线程
sThreadLocal.get() // 从当前线程中获取数据
示例:
// 事务管理器
@Component
public class TxManager {
@Autowired
private DataSource dataSource;
// 准备好本地存储Connection对象的ThreadLocal
private ThreadLocal<Connection> th = new ThreadLocal<Connection>();
// 获取Connection对象
public Connection getConnection() throws SQLException {
Connection connection = th.get();
if (connection == null){
connection = dataSource.getConnection();
th.set(connection);
}
return connection;
}
}
参考:
线程池:线程池就是创建大量空闲的线程并存入一个容器中;程序将一个任务传给线程池,线程池就会启动一条线程来执行这个任务,任务执行完成后,该线程又返回线程池中成为空闲线程,等待执行下一个任务。
线程池的特点:线程复用;控制最大并发数;管理线程
**线程池的作用:**避免频繁的创建线程和销毁线程,提高程序的效率
ThreadPoolExecutor(线程池)的执行顺序
一个线程池包括以下四个基本组成部分:
线程池的体系结构:
java.util.concurrent.Executor:负责线程的使用和调度的根接口
|–ExecutorService 子接口: 线程池的主要接口,提供了线程池生命周期方法
|–ThreadPoolExecutor: 线程池的实现类,提供了线程池的维护操作等相关方法
|–ScheduledThreadPoolExecutor : 继承 ThreadPoolExecutor,实现了 ScheduledExecutorService
周期性任务调度的类实现
java.util.concurrent.Executor:描述线程池的顶级接口
唯一方法:
void execute(Runnable command) // 提交一个 Runnable 任务用于执行
java.util.concurrent.ExecutorService:描述线程池的接口
常用方法:
Future<?> submit(Runnable task) // 提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future
<T> Future<T> submit(Callable<T> task) // 提交一个返回值的任务用于执行,返回一个表示任务的未决结果的 Future
/*参数:
Runnable task:传递Runnable接口的实现类对象(设置的线程任务) ==> 重写run方法,设置线程任务 ==> run方法没有返回值
Callable task:传递Callable接口的实现类对象(设置线程任务) ==> 重写call方法,设置线程任务 ==> call方法有返回值
返回值:
Future:用来接收线程任务的返回值 ==> 用来接收call方法的返回值
注:Callable 接口类似于 Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的。但是 Runnable 不会返回结果,Callable会返回一个结果。
*/
java.util.concurrent.Future:表示异步计算的结果。用来接收 call 方法的返回值
接口中的方法:
V get() // 方法可以当任务结束后返回一个结果,如果调用时,工作还没有结束,则会阻塞线程,直到任务执行完毕
V get(long timeout, TimeUnit unit) // 做多等待timeout的时间就会返回结果
boolean isDone() // 判断当前方法是否完成
boolean isCancel() // 判断当前方法是否取消
boolean cancel(boolean mayInterruptIfRunning) // 停止一个任务
/* 如果任务可以停止(通过mayInterruptIfRunning来进行判断),则可以返回true,
如果任务已经完成或者已经停止,或者这个任务无法停止,则会返回false
*/
submit 和 execute 区别:
方法名 | 返回值 | 任务接口 | 向外层调用者抛出异常 |
---|---|---|---|
execute | void | Runnable 接口 | 无法抛出异常 |
submit | Future | Callable 接口和 Runnable 接口 | 能抛出异常,通过 Future.get 捕获抛出的异常 |
execute() 方法中的是 Runnable 接口的实现,所以只能使用 try-catch 来捕获 Checked Exception,通过实现UncaughtExceptionHande 接口处理 UncheckedException, 即 和普通线程的处理方式完全一致
submit() 方法中抛出异常不管提交的是 Runnable 还是 Callable 类型的任务,如果不对返回值 Future 调用 get() 方法,都会吃掉异常
execute() 方法提交的未执行的任务可以通过 remove(Runnable) 方法删除
submit() 方法提交的任务即使还未执行也不能通过 remove(Runnable) 方法删除
ExecutorService 接口的核心成员变量:
// 核心池的大小(即线程池中的线程数目大于这个参数时,提交的任务会被放进任务缓存队列)
private volatile int corePoolSize;
// 线程池中当前的线程数
private volatile int poolSize;
// 线程池最大能容忍的线程数
private volatile int maximumPoolSize;
// 任务缓存队列,用来存放等待执行的任务
private final BlockingQueue<Runnable> workQueue;
// 线程池的主要状态锁,对线程池状态(比如线程池大小、runState等)的改变都要使用这个锁
private final ReentrantLock mainLock = new ReentrantLock();
// 用来存放工作集
private final HashSet<Worker> workers = new HashSet<Worker>();
// 线程存货时间
private volatile long keepAliveTime;
// 是否允许为核心线程设置存活时间
private volatile boolean allowCoreThreadTimeOut;
// 任务拒绝策略
private volatile RejectedExecutionHandler handler;
// 线程工厂,用来创建线程
private volatile ThreadFactory threadFactory;
// 用来记录线程池中曾经出现过的最大线程数
private int largestPoolSize;
// 用来记录已经执行完毕的任务个数
private long completedTaskCount;
java.util.concurrent.Executors:是一个创建线程池的工具类(工厂类),专门用来生产线程池,里边的方法都是静态的
常用方法:
// 创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程
static ExecutorService newFixedThreadPool(int nThreads)
/*参数:
int nThreads:创建线程池,包含线程的数量 100==>线程池中包含100个线程
*/
static ExecutorService newCachedThreadPool() // 缓存线程池,线程池的数量不固定,可以根据需求自动的更改数量
static ExecutorService newSingleThreadExecutor() // 创建单个线程池。 线程池中只有一个线程
static ScheduledExecutorService newScheduledThreadPool() // 创建固定大小的线程,可以延迟或定时的执行任务
// 注意:线程池一旦销毁,就不能在使用了,会抛出异常
void shutdown() // 关闭线程池,但不会立即终止,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务
void shutdownNow() // 立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务
使用示例
public class Test {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 200, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<Runnable>(5));
for(int i=0;i<15;i++){
MyTask myTask = new MyTask(i);
executor.execute(myTask);
System.out.println("线程池中线程数目:"+executor.getPoolSize()+",队列中等待执行的任务数目:"+
executor.getQueue().size()+",已执行玩别的任务数目:"+executor.getCompletedTaskCount());
}
executor.shutdown();
}
}
class MyTask implements Runnable {
private int taskNum;
public MyTask(int num) {
this.taskNum = num;
}
@Override
public void run() {
System.out.println("正在执行task "+taskNum);
try {
Thread.currentThread().sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("task "+taskNum+"执行完毕");
}
}
线程池对象交给 Spring 容器管理
配置类的方式配置线程池
@Configuration
public class ExecturConfig {
@Bean("threadPoolTaskExecutor") // 配置多个线程池时,设置不同的实例名称,注入时变量名为相应的实例名称
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
int i = Runtime.getRuntime().availableProcessors(); //获取到服务器的cpu内核
executor.setCorePoolSize(5); //核心池大小
executor.setMaxPoolSize(100); //最大线程数
executor.setQueueCapacity(1000); //队列程度
executor.setKeepAliveSeconds(1000); //线程空闲时间
executor.setThreadNamePrefix("tsak-asyn"); //线程前缀名称
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy()); //配置拒绝策略
executor.initialize(); //线程初始化
return executor;
}
xml 配置的方式创建
<!-- spring线程池 -->
<bean id="taskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<!-- 核心线程数 -->
<property name="corePoolSize" value="10"/>
<!-- 最大线程数 -->
<property name="maxPoolSize" value="200"/>
<!-- 队列最大长度 >=mainExecutor.maxSize -->
<property name="queueCapacity" value="10"/>
<!-- 线程池维护线程所允许的空闲时间 -->
<property name="keepAliveSeconds" value="20"/>
<!-- 线程池对拒绝任务(无线程可用)的处理策略 -->
<property name="rejectedExecutionHandler">
<bean class="java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy"/>
</property>
</bean>
使用时通过自动注入的方式获取线程池对象
// 方式1(推荐)
@AutoWired
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
// 方式2
@Resource(name="taskExecutor")
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
打印线程池的实时参数
public static void printThreadPoolStatic(ThreadPoolTaskExecutor threadPoolTaskExecutor){
log.info("-----------------------thread pool status start-----------------------");
log.info("thread core pool size:{}", threadPoolTaskExecutor.getCorePoolSize()); // 核心线程数
log.info("thread active count:{}", threadPoolTaskExecutor.getActiveCount()); // 正在工作的线程数
log.info("thread pool max size:{}", threadPoolTaskExecutor.getMaxPoolSize()); // 最大线程数
log.info("thread pool size:{}", threadPoolTaskExecutor.getPoolSize()); // 线程池中当前的线程数
log.info("thread pool wait task:{}", threadPoolTaskExecutor.getThreadPoolExecutor().getQueue().size()); // 任务队列内等待执行的任务数
log.info("-----------------------thread pool status end-----------------------");
}
// ThreadPoolExecutor类中提供的构造方法
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler);
corePoolSize :核心线程数,也可以理解为最小线程数
(1)核心线程会一直存在,即使没有任务执行
(2)当线程数小于核心线程数的时候,即使有空闲线程,也会一直创建线程直到达到核心线程数
(3)设置 allowCoreThreadTimeout=true(默认 false)时,核心线程会超时关闭
maxPoolSize(maximumPoolSize):最大线程数
(1)线程池里允许存在的最大线程数量
(2)当任务队列已满,且线程数量大于等于核心线程数时,会创建新的线程执行任务
(3)线程池里允许存在的最大线程数量。当任务队列已满,且线程数量大于等于核心线程数时,会创建新的线程执行任务
keepAliveTime :线程空闲时间
(1)当线程空闲时间达到 keepAliveTime 时,线程会退出(关闭),直到线程数等于核心线程数
(2)如果设置了 allowCoreThreadTimeout=true,则线程会退出直到线程数等于零
(BlockingQueue)workQueue:任务队列,用于传输和保存等待执行任务的阻塞队列。阻塞队列有以下几种选择:
LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为 Integer.MAX_VALUE
synchronousQueue:不保存提交的任务,而是将直接新建一个线程来执行新来的任务
ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小
PriorityBlockingQueue:一个具有优先级的无限阻塞队列
threadFactory:线程工厂,用于创建新线程。
threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)
(RejectedExecutionHandler)handler:线程饱和策略/任务拒绝处理器,当线程池和队列都满了,再加入线程会执行此策略
ThreadPoolExecutor.AbortPolicy
简单粗暴,丢弃任务并抛出RejectedExecutionException异常,这也是默认的拒绝策略。
即使是submit提交,也是使用 try-catch 捕捉 任务拒绝异常
try{
Future<T> future = threadPoolTaskExecutor.submit(() -> {任务});
T t = future.get();
} catch(TaskRejectedException e){
e.printStackTrace();
} catch(Exception e){
e.printStackTrace();
}
ThreadPoolExecutor.CallerRunsPolicy
如果线程池未关闭,则会在调用者线程中直接执行新任务,这会导致主线程提交线程性能变慢。
ThreadPoolExecutor.DiscardPolicy
丢弃任务,但是不抛出异常
ThreadPoolExecutor.DiscardOldestPolicy
抛弃最老的任务,就是从队列取出最老的任务然后放入新的任务进行执行。
unit:参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:
TimeUnit.DAYS; // 天
TimeUnit.HOURS; // 小时
TimeUnit.MINUTES; // 分钟
TimeUnit.SECONDS; // 秒
TimeUnit.MILLISECONDS; // 毫秒
TimeUnit.MICROSECONDS; // 微妙
TimeUnit.NANOSECONDS; // 纳秒
排队的三种通用策略:
直接提交
工作队列的默认选项是 SynchronousQueue,它将任务直接提交给线程而不阻塞它们。
如果不存在可用于立即运行任务的工作线程时,试图把任务加入队列将失败,因此会构造一个新的工作线程。
此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。
直接提交策略通常要求无界的 maximumPoolSizes(最大线程数)以避免拒绝新提交的任务。
无界队列(队列大小超级大,例如 Integer.MAX_VALUE)
使用无界队列(例如,使用默认值的 LinkedBlockingQueue),如果所有的工作线程都忙时,新任务将在队列中等待。
因为队列超级大,很难队列满,线程池中的线程数一般不会超过 corePoolSize(maximumPoolSize 的值也就用不到了)。
当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列。
例如,在 Web页服务器中。这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。
有界队列
当使用有界队列(如 ArrayBlockingQueue)以及有限的 maximumPoolSizes(最大线程数)时,有助于防止资源耗尽,但是性能调优可能会比较困难。
使用大型队列和小型池
可以最大限度地降低 CPU 使用率以及操作系统资源和上下文切换的开销,但是可能导致人为的降低吞吐量。
如果任务执行过程中可能频繁阻塞(例如,I/O 性能瓶颈),则耗时可能大于节约的线程调度开销。
使用小型队列通常要求较大的池大小,CPU使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。
常见线程池(实现 ExecutorService 的四种连接池,使用工厂类 Executors 的静态方法创建):
newFixedThreadPool:固定线程池
核心线程数和最大线程数固定相等,空闲存活时间为 0 毫秒(说明此参数无意义),工作队列为最大为 Integer.MAX_VALUE 大小的阻塞队列。
当执行任务时,如果线程都很忙,就会丢到工作队列等有空闲线程时再执行,队列满就执行默认的拒绝策略。
newCachedThreadPool:带缓冲线程池
核心线程数为 0,最大线程数为 Integer.MAX_VALUE,超过 0 个的空闲线程在 60 秒后销毁,SynchronousQueue(直接提交的队列)
每个新任务都会有线程来执行,如果线程池有可用线程则执行任务,没有的话就创建一个来执行,线程池中的线程数不确定,一般建议执行速度较快的线程,不然这个最大线程池边界过大容易造成内存溢出。
newSingleThreadExecutor:单线程线程池
核心线程数和最大线程数均为1,空闲线程存活 0 毫秒
每次只执行一个线程,多余的先存储到工作队列,一个一个执行,保证了线程的顺序执行。
如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。
newScheduledThreadPool:调度线程池
大小无限制的线程池,支持定时和周期性的执行线程
如何配置线程池,可以从以下几个角度来进行分析:
任务的性质:CPU 密集型任务,IO 密集型任务和混合型任务
任务性质不同的任务可以用不同规模的线程池分开处理:
CPU 密集型任务:配置尽可能少的线程数量,如配置 Ncpu+1 个线程的线程池
IO 密集型任务:由于需要等待 IO 操作,线程并不是一直在执行任务,则配置尽可能多的线程,如 2*Ncpu
混合型的任务:如果可以拆分,则将其拆分成一个 CPU 密集型任务和一个 IO 密集型任务
只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率
如果这两个任务执行时间相差太大,则没必要进行分解
注:可以通过 Runtime.getRuntime().availableProcessors()
方法获得当前设备的CPU个数
任务的优先级:高、中和低
优先级不同的任务:可以使用优先级队列 PriorityBlockingQueue 来处理
优先级队列可以让优先级高的任务先得到执行
需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。
任务的执行时间:长、中和短
执行时间不同的任务:
任务的依赖性:是否依赖其他系统资源,如数据库连接
由于线程提交 SQL 后需要等待数据库返回结果,等待的时间越长,CPU 空闲时间就越长
如果等待的时间较长,则线程数应该设置大一点,这样才能更好的利用 CPU
可以通过线程池提供的参数进行监控
线程池里有一些属性在监控线程池的时候可以使用
可以通过扩展线程池进行监控
通过继承线程池并重写线程池的 beforeExecute,afterExecute 和 terminated 方法
可以在任务执行前,执行后和线程池关闭前干一些事情。如监控任务的平均执行时间,最大执行时间和最小执行时间等。
这几个方法在线程池里是空方法。如:
protected void beforeExecute(Thread t, Runnable r) { }`
定时器:可以设置线程在某个时间执行某件事情,或者某个时间开始,每间隔指定的时间反复的做某件事情
java.util.Timer 类:
构造方法:
public Timer() // 创建一个新计时器
成员方法:
void schedule(TimerTask task, long delay) // 在指定的毫秒值之后,执行指定的任务,只会执行一次
// 参数:
// task - 所要安排的任务。定时器到时间之后要执行的任务
// delay - 执行任务前的延迟时间,单位是毫秒。 多少毫秒之后开始执行TimerTask任务
void schedule(TimerTask task, long delay, long period)
// 在指定的毫秒值之后,执行指定的任务,之后每隔固定的毫秒数重复执行定时任务
// 参数:
// period - 执行各后续任务之间的时间间隔,单位是毫秒。定时器开始执行之后,每隔多少毫秒重复执行
void schedule(TimerTask task, Date time) // 安排在指定的时间执行指定的任务,只会执行一次
// 参数:
// time - 执行任务的时间。从什么日期开始执行任务 20020-07-06 15:25:13
void schedule(TimerTask task, Date firstTime, long period) // 安排指定的任务在指定的时间开始进行重复的固定延迟执行。
void cancel() // 终止此计时器,丢弃所有当前已安排的任务。
// 注意,在此计时器调用的计时器任务的 run 方法内调用此方法,就可以绝对确保正在执行的任务是此计时器所执行的最后一个任务。
java.util.TimerTask类 implements Runnable接口:由 Timer 安排为一次执行或重复执行的任务
TimerTask 类是一个抽象类,无法直接创建
void run() // 此计时器任务要执行的操作。重写run方法,设置线程任务