提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
提高计算机系统CPU的利用率
改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改
一个Java应用程序java.exe
,其实至少两个线程:
main()
,必然存在gc()
,必然存在run()
:将此线程执行的操作声明在run()
中 (线程要完成的任务)start()
:启动当前线程,调用当前线程的run()
我们启动一个线程,必须调用start()
,不能调用run()
的方式启动线程。
如果再启动一个线程,必须重新创建一个Thread子类的对象,调用此对象的start()
.
// 继承Thread
static class MyThread extends Thread{
// 重写run
@Override
public void run() {
while (true){
System.out.println(Thread.currentThread().getName());
}
}
}
public static void main(String[] args) throws Exception {
// 创建两个线程
MyThread t1 = new MyThread();
t1.start();
MyThread t2 = new MyThread();
t2.start();
}
run()
start()
// 实现Runnable
static class MyThread implements Runnable{
// 实现run
@Override
public void run() {
while (true){
System.out.println(Thread.currentThread().getName());
}
}
}
public static void main(String[] args) throws Exception {
// 创建两个线程
Thread t1 = new Thread(new MyThread());
t1.start();
Thread t2 = new Thread(new MyThread());
t2.start();
}
开发中:优先选择实现Runnable接口的方式
原因:
联系:
public class Thread implements Runnable
相同点:
run()
,将线程要执行的逻辑声明在run()
中。start()
。call()
中FutureTask
构造器中,创建FutureTask
的对象start()
call()
方法的返回值:get()
返回值即为FutureTask
构造器参数Callable实现类重写的call()
的返回值。如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?
call()
可以有返回值的。call()
可以抛出异常,被外面的操作捕获,获取异常的信息缺点
get()
方法,会导致主线程阻塞// 实现Runnable
static class MyThread implements Callable<String> {
@Override
public String call() throws Exception {
return Thread.currentThread().getName();
}
}
public static void main(String[] args) throws Exception {
// 创建FutureTask
FutureTask<String> futureTask = new FutureTask<>(new MyThread());
// 创建Thread
Thread t1 = new Thread(futureTask);
t1.start();
// 阻塞获取返回值
System.out.println(futureTask.get());
}
java.lang.Thread
Thread()
:创建新的Thread对象Thread(String threadname)
:创建线程并指定线程名Thread(Runnable target)
:指定创建线程的目标对象Thread(Runnable target,String threadname)
:指定创建线程的目标对象,并指定线程名void start()
:启动线程并执行run()
方法
run()
:线程被调度时执行的操作,不可主动调用!
String getName()
:返回线程名称
static Thread currentThread()
:返回当前线程
static void yield()
:线程让步,线程进入就绪状态
join()
:立刻执行调用此方法线程,其他线程将被阻塞直到调用方法的线程执行完毕,可以保证线程的串行化执行
join()
方法,线程a会进入阻塞,直到线程b执行完毕static void sleep(long millis)
:当前正在活动的线程在millis时间段内放弃CPU控制,时间到后重新排队,线程进入阻塞状态
stop()
:强行结束线程生命周期,不推荐使用
boolean isAlive()
:判断线程是否还活着
int activeCount()
:获取线程组中线程的数量
getPriority()
:获取线程的优先级
setPriority(int p)
:设置线程的优先级
说明:高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。并不意味着只当高优先级的线程执行完以后,低优先级的线程才执行。
start()
前使用thread.setDaemon(ture)
方法将一个用户线程变为守护线程。Thread userThread = new Thread("User Thread") {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
};
Thread daemonThread = new Thread("Daemon Thread"){
@Override
public void run() {
while (true){
System.out.println(Thread.currentThread().getName());
}
}
};
userThread.start();
// 设置为守护线程,即使自己有死循环,也会随着所有用户线程的结束而退出
daemonThread.setDaemon(true);
daemonThread.start();
JDK中用Thread.State
类定义了线程的几种状态
要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态
start()
方法后,线程进入队列等待CPU时间片,此时已经具备了运行的条件,只是还没分配到CPU资源run()
通过同步机制,来解决线程的安全问题。
synchronized(同步监视器){
//需要被同步的代码
}
public synchronized void method(){}
如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。
同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
this
当前类.class
所谓重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程内部可以再次获取本对象上的锁,而其他的线程是不可以的。
可以重复使用的锁,锁的创建只有一次,可以重复调用lock()
和unlock()
synchronized
和ReentrantLock
都是可重入锁。
public class Demo {
public static void main(String[] args) {
Thread t = new Thread("A") {
@Override
public void run() {
// 线程第一次获取锁
synchronized (Demo.class){
for (int i = 0; i < 10000; i++) {
// 可重入,再次获取锁
synchronized (Demo.class){
System.out.println(Thread.currentThread().getName());
}
}
}
}
};
t.start();
}
}
void lock()
:获取锁,如果锁不可用,则出于线程调度的目的,当前线程将被禁用,并且在获取锁之前处于休眠状态。boolean tryLock()
:如果锁可用立即返回true,如果锁不可用立即返回false;boolean tryLock(long time, TimeUnit unit) throws InterruptedException
:如果锁可用,则此方法立即返回true。 如果该锁不可用,则当前线程将出于线程调度目的而被禁用并处于休眠状态,直到发生以下三种情况之一为止
void unlock()
:释放锁。释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。实现Lock接口,是一种可重入锁
public class Demo {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock(true);
Thread t = new Thread("A") {
@Override
public void run() {
try {
// 线程第一次获取锁
lock.lock();
for (int i = 0; i < 10000; i++) {
// 可重入,再次获取锁
lock.lock();
System.out.println(Thread.currentThread().getName());
}
}finally {
// 释放锁
lock.unlock();
}
}
};
t.start();
}
}
writeLock()
:获取写锁readLock()
:获取读锁
两个线程都是写锁:互斥,同步执行
两个线程一写一读:互斥,同步执行
两个线程都是读锁:共享,异步执行
公平锁的效率比非公平锁低
公平锁总是可以保证让所有线程中等待时间最长的线程先执行
在new ReentrantLock(true)
或new ReentrantReadWriteLock(true)
时,参数为true,创建的就是公平锁;不传参默认为false,就是非公平锁
lock()
,同时结束同步也需要手动的实现unlock()
,如果没有主动释放锁,就有可能导致出现死锁现象。trylock()
方法可以试图获取锁,获取到或获取不到时,返回不同的返回值让程序可以灵活处理。lock()
和unlock()
可以在不同的方法中执行,程序更加灵活。不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
说明:
1、调整锁的顺序,避免可能出现的死锁
2、调整锁的范围,避免在一个同步代码块中使用另一个同步代码块
3、使用可重入锁
break
、return
终止了代码块中的循环或方法的执行Error
或Exception
未处理wait()
造成线程阻塞并释放锁Thread.sleep()
或Thread.yield()
suspend()
将线程挂起(避免使用suspend()
或resume()
控制线程)wait()
:一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。notify()
:一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。notifyAll()
:一旦执行此方法,就会唤醒所有被wait的线程。
wait()
,notify()
,notifyAll()
三个方法必须使用在同步代码块或同步方法中。wait()
,notify()
,notifyAll()
三个方法的调用者必须是同步代码块或同步方法中的同步监视器。否则,会出现IllegalMonitorStateException
异常wait()
,notify()
,notifyAll()
三个方法是定义在java.lang.Object
类中。注意:wait方法等待的线程,在哪等待,被唤醒后,就从哪里开始执行,可能会出现虚假唤醒的问题,所以建议wait循环使用在while循环中
Condition condition = lock.newCondition();
await()
:当前线程等待,对应wait()signal()
:唤醒一个线程,对应notify()signalAll()
:唤醒所有线程,对应notifyAll()sleep()
, Object类中声明wait()
sleep()
可以在任何需要的场景下调用。 wait()
必须使用在同步代码块或同步方法中sleep()
不会释放锁,wait()
会释放锁。可以在指定线程内存储数据,数据存储以后,只有指定线程可以得到存储数据(可以存放线程范围内的局部变量)
在JavaWeb项目里面,我们做MyBatis的工具类的时候,为了可以满足多个线程中,各线程只关闭自己的SqlSession对象的需求,而不会产生线程安全的问题,所以,我们需要将SqlSession对象存入到ThreadLocal对象中
set()
用于向ThreadLocal对象中存值get()
用于向ThreadLocal对象中取值remove()
用于从ThreadLocal对象中删除值例如,一个int类型的变量,在高并发的情况下进行加减,通常会导致线程不安全,数值不准确的问题,例如
public class Demo {
static int count = 0;
public static void main(String[] args) {
// 创建100个线程
for (int i1 = 0; i1 < 100; i1++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
count++;
}
}
}).start();
}
// 确保其他线程都执行完成了,除了GC和主线程
while (Thread.activeCount() > 2){
Thread.yield();
}
// 结果打印出来的数据不一定是正确的10000,结果比10000少
System.out.println(count);
}
}
由于加减操作例如count++
、count = count - 1
并不是一个原子操作,在执行的时候分了三步:1、读取值;2、修改值;3、写入值。所以例如现在有两个线程A和B,原子性操作如下:
count
count
所以造成了变量count
的值经过两次加1,应该为2;但是实际上值为1
缺点是:每次进行非原子操作前,都需要加锁,操作完解锁,效率低下
public class Demo {
static int count = 0;
public static void main(String[] args) {
for (int i1 = 0; i1 < 100; i1++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
// 使用同步代码块将非原子操作同步
synchronized (Demo.class){
count++;
}
}
}
}).start();
}
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(count);
}
}
通过AtomicInteger
,可以在线程提交的时候,用自己读到的数值,与现在变量中的数值进行比对。如果一致的话,就写入变量;如果数值已经发生改变的话,那么会进行自旋,也就是重新进行运算,再进行比较
public class Demo {
static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) {
for (int i1 = 0; i1 < 100; i1++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
//读取数据并自增,等同于count++
count.getAndIncrement();
}
}
}).start();
}
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(count);
}
}
但是,自旋锁的方式,有可能会产生ABA的问题,例如现在有三个线程A、B、C,操作流程如下
+ 1
;验证变量值为0,一致;写入变量;变量值为1- 1
;验证变量为1,一致;写入变量,变量值为0+ 1
;验证变量值和自己读取的都为0,一致,写入变量;变量值为1这样看下来,虽然变量值和预期的值相同,但是中间变量其实已经修改过了,线程B比较的0已经不是自己当时读到的0了,在某些情况下,可能会出现问题
在CAS的思想上,利用版本戳的思想,从原始数据开始,给每次的数据都加上一个版本,每次对数据发生修改,版本也会进行迭代
具体的实现类,AtomicStampedReference
,这个类在实例化时,第二个参数为版本号
public class Demo {
// 第一个参数为保存的数据,第二个参数为版本戳
static AtomicStampedReference<Integer> count = new AtomicStampedReference(0,0);
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
// 比较并且设置
// 1:比较的原始值,2:设置的新参数值
// 3、比较的原始版本戳,4、设置的新版本戳
count.compareAndSet(
count.getReference()
, count.getReference() + 1
, count.getStamp()
, count.getStamp() + 1
);
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
count.compareAndSet(
count.getReference()
, count.getReference() - 1
, count.getStamp()
, count.getStamp() + 1
);
}
}
}).start();
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(count.getReference());
System.out.println(count.getStamp());
}
}
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程, 对性能影响很大。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用
好处:
相关API
ExecutorService
和Executors
java.util.concurrent
1、用户提交任务,判断核心线程是否正在处理,如果核心线程有空闲,直接使用核心线程执行
2、如果核心线程没有空闲,判断队列是否满了,如果没有满,就把任务放进队列中
3、如果队列已经满了,那么判断线程池中线程+新任务线程
是否大于最大线程数,如果大于,抛出异常,拒绝执行
4、如果小于等于最大线程数,创建新的线程,执行任务
状态 | 说明 |
---|---|
RUNNING | 允许提交并处理任务 |
SHUTDOWN | 不允许提交新的任务,但是会处理完已提交的任务 |
STOP | 不允许提交新的任务,也不会处理阻塞队列中未执行的任务,并设置正在执行的线程的中断标志位 |
TIDYING | 所有任务执行完毕,池中工作的线程数为0,等待执行terminated() 勾子方法 |
TERMINATED | terminated() 勾子方法执行完毕 |
shutdown()
方法,将线程池由 RUNNING(运行状态)转换为 SHUTDOWN状态shutdownNow()
方法,将线程池由RUNNING 或 SHUTDOWN 状态转换为 STOP 状态。SHUTDOWN 状态 和 STOP 状态 先会转变为 TIDYING 状态,最终都会变为 TERMINATED
/* 创建线程池
* 参数1:线程池创建后,核心线程数
* 参数2:最大线程数
* 参数3:核心线程外,新创建的线程空闲时,最大存活时间
* 参数4:最大存活时间的单位
* 参数5:用于存放任务的阻塞队列
* 参数6:用于创建线程的线程工厂
* 参数7:任务被拒绝后的策略
*/
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
2
, 5
, 60
, TimeUnit.SECONDS
, new ArrayBlockingQueue<>(5)
, new ThreadPoolExecutor.AbortPolicy()
);
策略 | 描述 |
---|---|
ThreadPoolExecutor.AbortPolicy() | 抛出RejectedExecutionException 异常。默认策略 |
ThreadPoolExecutor.CallerRunsPolicy() | 由向线程池提交任务的线程来执行该任务 |
ThreadPoolExecutor.DiscardPolicy() | 抛弃当前的任务 |
ThreadPoolExecutor.DiscardOldestPolicy() | 抛弃最旧的任务(最先提交而没有得到执行的任务) |
execute()
是专门用来提交Runnable接口任务的;submit()
既可以提交Runable任务也可以提交Callable任务
execute()
没有返回结果;submit()
可以使用Future接收线程的返回结果
execute()
方法中不能处理任何异常;submit()
支持处理任务中的异常,使用Funture.get()
可以使用Executors的静态方法,来创建线程池
创建方法 | 线程池 | 阻塞队列 | 使用场景 |
---|---|---|---|
newSingleThreadExecutor() | 单线程的线程池 | LinkedBlockingQueue | 适用于串行执行任务的场景,⼀个任务⼀个任务地执行 |
newFixedThreadPool(n) | 固定数目线程的线程池 | LinkedBlockingQueue | 适用于处理CPU密集型的任务,适用执行长期的任务 |
newCachedThreadPool() | 可缓存线程的线程池 | SynchronousQueue | 适用于并发执行大量短期的小任务 |
newScheduledThreadPool(n) | 定时、周期执行的线程池 | DelayedWorkQueue | 周期性执行任务的场景,需要限制线程数量的场景 |
public static void main(String[] args) {
// 创建一个固定大小的线程池:
ExecutorService es = Executors.newFixedThreadPool(4);
Future<Integer> future = es.submit(new Task("a"));
System.out.println(future);
// 关闭线程池:
es.shutdown();
}
阿里巴巴Java开发手册规定:线程池不允许使用Executors去创建,要求通过
new ThreadPoolExecutor()
的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。