Java线程的创建方式
- 继承Thread类
- 实现Runnable接口
- 通过ExecutorService和Callable<Class>实现有返回值的线程
- 基于线程池
继承Thread类
Thread类通过实现了Runnable接口并设置了一些操作线程的方法,可以通过继承Thread类来创建一个线程。具体实现为创建一个类并继承了Thread类,然后实例化该线程对象并调用start方法。start方法是一个native方法,通过调用操作系统的接口创建一个线程,并最终执行run方法启动一个线程。run方法内的代码是线程类的具体实现逻辑。
// step 1: 通过继承Thread类创建NewThread线程
public class NewThread extends Thread {
public void run() {
System.out.println("this is a new thread");
}
}
// step 2:实例化线程对象
NewThread newThread = new NewThread();
// step 3:调用start方法
newThread.start();
实现Runnable接口
基于Java的单继承机制,如果子类已经继承了一个父类,就不能直接继承Thread类了,此时可以通过实现Runnable接口创建线程。具体的实现过程为:通过实现Runnable接口创建ChildrenClassThread线程,实例化为childrenClassThread对象。实例化Thread类并传入childrenClassThread对象,通过线程的start启动。
// step 1:继承Runnable类
public class ChildrenClassThread extends superClass implements Runnable {
public void run() {
System.out.println("this is a new thread");
}
}
// step 2:实例化ChildrenClassThread
ChildrenClassThread childrenClassThread = new ChildrenClassThread();
// step 3:将该对象传递给一个Thread对象
Thread thread = new Thread(childrenClassThread);
// step 4:启动线程
thread.start();
通过ExecutorService和Callable<Class>实现有返回值的线程
在需要开启多个线程执行一个任务,然后收集线程执行返回的结果并将最终结果汇总起来,这时候就要用到Callable接口。创建一个类实现Callable接口,实现call方法,call方法内为具体的计算逻辑并返回结果。具体调用过程为:创建一个线程池,接收返回结果的Future List以及Callable实例,使用线程池提交任务并将线程执行之后的返回结果保存在Future对象中。在线程执行结束后便利Future List中的Furure对象,在该对象调用get方法就可以获取Callable线程任务返回的数据并汇总结果。
step 1:通过实现Callable接口创建MyCallable线程
public class MyCallable implements Callable<String> {
private String name;
public MyCallable(String name) {
this.name = name;
}
@Override
public String call() throws Exception {
return name;
}
}
// step 2:创建线程池
ExecutorService pool = Executors.newFixedThreadPool(5);
// step 3:创建多个有返回值的任务列表
List<Future> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
// step 4:创建Callable线程实例
Callable c = new MyCallable(i + "");
// step 5:提交线程,获取Future对象并将其保存在Future List中
Future future = pool.submit(c);
list.add(future);
}
// step 6:关闭线程池,等待线程执行结束
pool.shutdown();
// step 7:遍历所有的线程的运行结果
for (Future future : list) {
System.out.println(future.get().toString());
}
基于线程池
线程是非常宝贵的资源,每次在需要时去创建并在运行结束后销毁是非常浪费资源的。我们可以通过缓存策略并使用线程池来创建线程池来创建线程。
ExecutorService pool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
pool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "is running");
}
})
}
线程池的工作原理
线程池的主要作用是线程复用,线程资源管理,控制操作系统的最大并发数,以保证系统高效(通过线程资源复用实现)且安全(通过控制最大线程并发数实现)地运行。
线程池的核心组件和核心类
Java线程池主要由以下4个核心组件组成
- 线程池管理器:用于创建并管理线程
- 工作线程:线程池中执行具体任务的线程
- 任务接口:用于定义工作线程的调度和执行策略,只有实现了该接口的线程才能被线程池调度
- 任务队列:存放待处理的任务,新的任务会不断加入到队列中,完成任务的队列将会从队列中移除
Java线程池的工作流程
线程池刚被创建时,只是向系统申请一个用于执行线程队列和管理线程池的线程资源。在调用execute方法添加任务时:
- 如果正在运行的线程数量小于corePoolSize(用户定义的核心线程数),线程池会立即创建线程并执行该任务
- 如果正在运行的线程数量大于等于corePoolSize时,该任务会放入阻塞队列中
- 如果阻塞队列已满并且正在运行的线程数小于maximumPoolSize,线程池会创建一个非核心线程立即执行该任务
- 如果阻塞队列已满并且正在运行的线程数大于等于maximumPoolSize,线程池会拒绝该线程任务并抛出RejectExecutorException异常
- 在线程任务执行完成后,该任务将从线程池队列中移除,线程池将从队列中取出下一个任务继续执行
- 在线程处于空闲状态的时间超过keepAliveTime时,且正在执行的任务超过corePoolSize,该线程会被认定为空闲线程并停止。
线程池的拒绝策略
若线程池的核心线程数被用完且阻塞队列已满,则此时线程池的线程资源已经耗尽,线程池将没有足够的线程资源执行新的任务。为了操作系统的安全,线程池将通过拒绝策略来处理新添加的进程。JDK内置的拒绝策略有:
- AbortPolicy:直接抛出异常,阻止线程正常运行
- CallerRunsPolicy:如果被丢弃的线程任务未被关闭,则执行该线程任务
- DiscardOldestPolicy:移除线程队列中最早的一个线程任务,并尝试提交当前任务
- DiscardPolicy:丢弃当前的线程任务而不做任何处理
- 自定义拒绝策略:可以自己扩展RejectedExecutionHandler接口自定义策略
// AbprtPolicy
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectExecutionException("Task" + r.toString() + "rejected from " + e.toString());
}
// CallerRunsPolicy
public void rejectedExcetion(Runnable r, ThreadPoolExecutor e) {
if(!e.isShutdown()) {
r.run();
}
}
// DiscardOldestPolicy
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if(!e.isShutdown()) {
e.getQueue().poll(); // poll the oldest task
e.execute(r); // try to execute current task
}
}
// DiscardPolicy
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
// not do anything
}
// 自定义策略:DiscardOldestNPolicy
public class DiscardOldestNPolicy implements RejectedExecutionHandler {
private int discardNumber = 5;
private List<Runnable> discardList = new ArrayList<>();
public DiscardOldestNPolicy(int discardNumber) {
this.discardNumber = discardNumber;
}
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if(e.getQueue().size() > this.discardNumber) {
e,getQueue.drainTo(discardList, discardNumber);
discardList.clear();
if(!e.shutdown()) {
e.execute(r);
}
}
}
}
5种常用的线程池
ExecutorService接口有多个实现类可用于创建不同的线程池
| 名称 | 说明 |
|---|---|
| newCachedThreadPool | 可缓存的线程池,在创建新线程时如果有可重用的线程,则重用它,对于执行时间很短的任务而言,能很大程度重用线程进而提高系统的性能 |
| newFixedThreadPool | 固定大小的线程池 |
| newScheduledThreadPool | 可做任务调度的线程池,可设置在给定的延迟时间后执行或者定期执行某个任务 |
| newSingleThreadExecutor | 单个线程的线程池,保证永远有且只有一个可用的线程 |
| newWorkStealingPool | 足够大小的线程池来达到快速运算的目的,JDK1.8新增 |
ScheduledExecutorService scheduledExecutionPool = Executors.newScheduledThreadPool(5);
// 1:创建一个延迟5秒执行的线程
scheduledExecutionPool.schedule(new Runnable() {
@Override
public void run() {
System.out.println("delay 5 seconds execute");
}
}, 5, TimeUnit.SECONDS);
// 2:创建一个延迟1秒且每3秒执行一次的线程
scheduledExecutionPool.scheduleAtFixedRate(new Runable() {
@Override
public void run() {
// do something
}
}, 1, 3, TimeUnit.SECONDS);
线程的生命周期
新建
调用new方法新建一个线程,这时线程处于新建状态
就绪
调用start方法启动一个线程,这时线程处于就绪状态
运行
处于就绪状态的线程等待线程获取CPU资源,获取到CPU资源后线程会进入运行状态
正在运行的线程在调用了yeild方法或失去处理器资源时,会再次进入就绪状态
阻塞
正在执行的线程在执行了sleep方法,I/O阻塞,等待同步锁,等待通知,调用suspend方法等操作后,会调起进入阻塞状态
阻塞状态的线程由于出现sleep时间已到,I/O方法返回,获得同步锁,收到通知,调用resume方法等,会再次进入就绪状态
死亡
处于运行状态的线程,在调用run方法或call方法正常执行完成,调用stop方法停止线程或者程序执行错误导致异常退出时,会进入死亡状态
线程的基本方法
线程相关的基本方法有:wait,notify,notifyALl,sleep,join,yield,interrupt等
线程等待:wait方法
调用wait方法的线程会进入WAITING状态,只有等到其他线程的通知或被中断后才会返回。在调用wait方法会释放对象的锁,因此wait方法一般被用于同步方法或同步代码块中
线程睡眠:sleep方法
调用sleep方法的线程会进入休眠,与wait不同的是sleep不会释放当前占有的锁
线程让步:yield方法
调用yield方法会使当前线程让出CPU资源和其他线程重新一起竞争CPU时间片
线程中断:interrupt方法
interrupt方法用于向线程发行一个终止通知信号,会影响该线程内部的一个中断标识位,这个线程本身并不会因为调用了interrupt方法而改变状态。
-
调用interrupt方法并不会中断一个正在运行的线程,仅仅是改变了内部维护的中断标识位而已
-
若因为调用sleep方法而使线程处于TIMED-WATING状态,则此时调用interrupt方法会抛出InterruptedException,使当前线程提前结束TIMED-WAITING状态
-
许多声明抛出InterruptedException的方法如Thread.sleep(long mills),在抛出异常前都会清除中断标识位,所以在抛出异常后调用isInterrupted方法将会返回false
-
中断状态是线程固有的一个标识位,可以通过此标识位安全终止线程
线程加入:join方法
join方法用于等待其他线程终止,如果在当前线程中调用一个线程的join方法,则当前线程转为阻塞状态,等到另一个线程结束,当前线程再由阻塞状态转为就绪状态,等待CPU的使用权
System.out.println("ChildThread start...");
ChildThread childThread = new ChildThread();
childThread.join();
System.out.println("ChildThread end, main Thread start...");
线程唤醒:notify方法
我们通常调用其中一个对象的wait方法在对象的监视器上等待,直到当前线程放弃此对象上的锁定,才能继续执行被唤醒的线程,被唤醒的线程将以常规方法与该对象上主动同步的其他线程竞争
sleep方法和wait方法的区别
- sleep方法属于Thread类,wait方法属于Object类
- 在调用sleep方法线程不会释放对象锁,wait会释放对象锁,需要notify唤醒
start方法与run方法的区别
- start方法用于启动线程,此线程处于就绪状态,没有运行
- run方法也叫做方法体,包含了要执行的线程的逻辑,在调用run方法,线程就进入运行状态
终止线程的4种方法
-
正常运行结束
-
使用退出标志退出线程:如设置一个同步变量来控制循环
-
使用interrupt方法终止
3.1 当线程处于阻塞状态时,调用线程的interrupt方法时,会抛出InterruptException,通过代码俘获该异常,然后通过break跳出状态检查循环
3.2 线程未处于阻塞状态,使用isInterrupted方法判断线程的中断标志来退出循环
-
使用stop方法终止线程:不安全
使用Thread.stop方法时,该线程的子线程会抛出ThreadDeatherror错误,并且释放子线程持有的所有锁,可能导致被保护的数据出现不一致的情况,其他线程使用这些数据可能会导致程序运行错误
Java中的锁
Java中的锁主要用于保障多并发线程下数据的一致性
在使用对象或者方法之前需要对其进行加锁,如果发现锁正在被其他线程使用,则该线程需要等待占用锁的进程执行完毕后释放锁,这样当前的线程才有可能获取锁对其进行操作,这样保障了在同一时刻只有一个线程持有该对象的锁并修改对象,从而保证数据的安全
锁从乐观和悲观的角度可以分为乐观锁和悲观锁,从获取资源公平性角度可分为公平锁和非公平锁,从是否共享的角度可分为共享锁和独占锁,从锁的状态可以分为偏向锁,轻量级锁和重量级锁,同时JVM还巧妙地设计了自旋锁以更快地使用CPU资源
乐观锁
乐观锁采用乐观的思想处理数据,在每次读取数据时都认为别人不会更改数据,所以不会上锁,但在更新时会判断在此期间别人是否修改了数据,具体过程为:比较当前版本号和上一次的版本号,如果相同则更新,如果不一致则重复进行读,比较,写操作
Java中的乐观锁大部分是使用CAS操作实现的,CAS是一种原子更新操作,在对数据操作之前会比较当前值和传入值是否一样,如果一样则更新,否则不执行更新操作,直接返回失败状态
悲观锁
悲观锁采用悲观思想处理数据,认为在每次读取数据别人都会修改数据,所以在对数据进行读写都需要对其进行加锁,这样别人想读写这个数据时就会阻塞,等待直到拿到锁
Java中的悲观锁大部分基于AQS架构实现。AQS定义了一套多线程访问共享资源的同步框架,许多类的实现都依赖于它,例如:Synchronized,ReetrantLock,Semaphore,CountDownLatch等。该框架下的锁会先尝试以CAS乐观锁去获取锁,如果获取不到则转为悲观锁
自旋锁
自旋锁认为:如果持有锁的线程会在很短的时间释放锁,那么等待的锁的线程就不需要做内核状态和用户状态的切换进入阻塞,等待状态,只需要等一等(也叫做自旋),在等待持有锁的线程释放锁后可以立即获取锁,这样就避免了线程在状态的切换上所花费的时间
线程在自旋时会占用CPU,在线程长时间自旋获取不到锁时,将会产生CPU的浪费,甚至有时候导致永远无法获得锁而导致CPU的永久浪费,所以需要设置一个自旋的最大等待时间,超过这个时间,线程会退出自旋模式并释放持有的锁
- 优点:可以减少CPU上下文的切换,对于占用锁的时间或竞争不激烈的代码块来说性能大幅度提升
- 缺点:在持有锁的线程占有锁时间过长或锁的竞争过于激烈时,会导致线程的长时间自旋,将引起CPU的浪费,在存在复杂锁依赖的情况下不适合采用自旋锁
时间阀值
JDK1.5为固定DE时间,JDK1.6引入了适应性自旋锁,自旋时间不再是固定值,而是根据上一次在用一个锁的自旋时间及锁的拥有者的状态来决定的,可基本认为一个线程上下文切换的时间就是一个最件时间
synchronized
synchronized关键字为Java对象,方法,代码块提供线程安全的操作。synchronized属于独占的悲观锁,同时还属于可重入锁。在使用synchronized修饰对象时,同一时刻只有一个线程能操作这个对象;在使用该关键字修饰方法和代码块时,同一时刻只有一个线程能执行该方法和代码块,其他线程只能等待当前线程执行完毕释放锁资源后才能访问该对象或执行方法和代码块。
Java中的每个对象都有一个monitor对象,加锁就是在竞争monitor对象。对代码块加锁就是在前后加上monitorenter和monitorexit,对方法是否加锁是通过一个标志位来判断的。
synchronized的作用范围
-
对于成员变量和非静态方法,synchronized锁的是this实例
-
作用于静态方法时,synchronized锁住的是class实例
-
作用于代码块时锁定的是代码块中所有的对象
synchronized的实现原理
在synchronized内部包括:
-
ContentionList:锁竞争队列,所有请求锁的线程都放在竞争队列中
-
EntryList:竞争候选列表,在ContentionList中有资格成为候选者来竞争锁资源的线程移动到EntryList
-
WaitSet:等待集合,调用wait方法后被阻塞的线程进入WaitSet中
-
OnDeck:竞争候选者,在同一时刻最多只有一个线程在竞争锁资源,该线程的状态称为OnDeck
-
Owner:竞争到锁资源的线程被称为Owner状态线程
-
!Owner:在Owner状态释放锁之后,线程的状态会变成!Owner
synchronized在收到新的锁请求首先进行自旋,如果通过自旋也没有获得锁,则将被放入ContentionList
为了防止ContentionList尾部元素被大量的并发CAS访问而影响到性能,Owner线程在释放锁资源的同时会将一部分ContentionList的元素放入EntryList中,再将EntryList的某个线程(一般是最先进入的那个线程)放入OnDeck,Owner线程并不会直接把锁传递为OnDeck,而是赋予OnDeck竞争锁的权利,让OnDeck线程重新竞争锁,这样的操作称为“竞争切换”,牺牲了公平,但是提升了性能
获取到锁资源的OnDeck线程会被转换为Owner线程,而未获取到的线程会停留在EntryList中
Owner线程在被wait方法阻塞后会进入WaitSet中,直到某个时刻被notify方法唤醒重新进入EntryList中
ContentionList,EntryList和WaitSet中的线程都是阻塞状态,该阻塞是由操作系统完成的
Owner线程在执行完毕后会释放锁资源并转换为!Owner状态
为什么synchronized是非公平锁
-
在synchronized中,在线程进入ContentionList之前,会尝试通过自旋获取锁,如果获取不到就进入ContentionList,这对原来就在队列中的线程不公平
-
在自旋获取到锁的线程可以抢占OnDeck线程的锁资源
在JDK1.6中,synchronized引入了适应自旋,锁消除,锁偏向,轻量级锁和重量级锁来提高锁的效率。锁可以从偏向锁升级到轻量级锁,再升级到重量级锁,这个过程叫做锁膨胀。在JDK1.6中默认开启了偏向锁和轻量级锁
ReentrantLock
ReentrantLock继承了Lock接口并实现了在接口中定义的方法,它是一个可重入的独占锁。ReentrantLock通过自定义同步队列(AQS)来实现锁的获取和释放
独占锁是在同一时刻一个锁只能被一个线程占有,其他线程只能在队列中等待。可重入锁指的是支持同一线程对同一对象重复加锁的操作
ReetrantLock可支持公平锁和非公平锁,公平与否是针对线程竞争获取锁的机制
ReentrantLock不仅提供了sychronized对锁的操作,还提供了诸如可响应中断锁,可轮询锁请求,定时锁等避免多线程死锁的方法
ReentrantLock的用法
ReentrantLock有显示的操作过程,何时加锁和释放锁都在程序员的控制下。具体的使用是定义一个ReentrantLock,在需要加锁的地方使用lock方法,等资源使用完毕后使用unlock方法释放锁
public class ReentrantLockDemo implements Runable {
public static ReentrantLock lock = new ReentrantLock();
public static int i = 0;
@Override
public void run() {
for(int j = 0; j < 10; j++) {
lock.lock();
// lock.lock(); // 可重入锁
try {
i++;
} finally {
lock.lock();
// lock.lock();
}
}
}
public static void main(String[] args) throws InterruptedException {
ReentrantLockDemo reentrantLockDemo = new ReentrantLockDemo();
Thread thread = new Thread(reentrantLock);
thread.start();
thread.join();
}
}
加锁和释放锁的次数要相
如果加锁的次数多于释放锁的次数,则该线程会一直持有这个锁,其他线程永远无法获取
如果加锁的次数少于释放锁的次数,则会抛出java.lang.IllegalMonitorStateException
ReentrantLock如何避免死锁:响应中断,可轮询锁,定时锁
-
响应中断:在synchronized中如果有一个线程尝试获取一把锁,其结果要么是获取锁继续执行,要么保持等待;ReentrantLock中提供了可响应中断锁,在等待锁的过程中,线程可以根据自己的需要取消对锁的请求
-
可轮询锁:通过
boolean tryLock()获取锁,如果有可用锁,则获取该锁并返回true,否则立即返回false -
定时锁:通过
boolean tryLock(long time, TimeUnit unit) throws InterruptedException获取定时锁。如果在给定的时间获取了可用锁,且线程被被中断,则获取该锁并返回true。如果在给定的时间获取不到可用锁,则禁用当前状态,并且在发生以下三种情况之前,该线程一直保持休眠状态3.1 当前线程获取到可用锁并返回true
3.2 当前线程在进入此方法之前设置了中断标志,或者在获取锁时被中断,则将抛出InterruptedException,并清除当前线程的已中断状态
3.3 获取锁的时间超过了给定的时间,则将放回false,如果给定的时间小于等于0, 则将完全不等待
公平锁和非公平锁
公平锁指的是锁的分配和竞争机制是公平的,采取先到先得原则。非公平锁遵循随机,就近原则分配锁的机制
ReentranrLock通过在构造函数中传递不同的参数定义不同类型的锁,默认实现的是非公平锁,因此该锁的执行效率明显高于公平锁
tryLock,lock和lockInterruptibly的区别
-
tryLock方法若有可用锁,则会立即获取锁并返回true,否则会立即返回false,还可以增加时间限制,在给定的时间没有获取可用锁,则返回false
-
lock方法若有可用锁,则会立即获取锁并返回true,否则进入等待状态
-
在锁中断时lockInterruptibly会抛出异常,lock不会
synchronized和ReentrantLock的比较
共同点:
-
都用于控制多线程对共享对象的方法
-
都是可重入锁
-
都保证了可见性和互斥性
不同点:
-
ReentrantLock显式获取和释放锁;synchronized隐式获取和释放锁。为了避免程序出现异常而无法释放锁的情况,在使用ReentrantLock必须在finally控制块中释放锁
-
ReentrantLock提供了可响应中断等操作,更灵活
-
ReentrantLock是API级别的,synchronized是JVM级别的
-
ReenTrantLock可定义公平锁
-
ReenTrantLock可通过Condition绑定多个条件
-
二者的实现底层不一样,sychronized是同步阻塞,采用的是悲观并发策略;ReentrantLock是同步非阻塞,采用的是乐观并发策略
-
Lock是一个接口,synchronized是一个关键字
-
可以通过lock知道有没有成功获取锁
-
Lock可以通过分别定义读写锁提高多个线程读操作的效率
Semaphore
Semaphore是一种基于计数的信号量,在定义信号量对象时可以设置一个阀值,基于该阀值,多个线程竞争该许可信号,线程竞争到许可信号后开始执行具体的业务逻辑,在执行完毕后释放该信号量。当竞争信号量的线程超过该阀值后,新加入的申请许可信号量的线程将被阻塞,直到有其他许可信号量被释放
Semaphore semp = new Semaphore(5);
try {
semp.acquire();
try {
// 具体业务逻辑
} catch(Exception e) {
semp.release();
}
} catch(Exception e) {
}
AtomicInteger
AtomicInteger为提供原子操作的Integer的类,常见的原子操作类还有AtomicBoolean等,性能是synchronized和ReentrantLock的好几倍
读写锁
读写锁分为读锁和写锁,多个读锁不互斥,读锁和写锁互斥
重量级锁和轻量级锁
重量级锁是基于操作系统实现的互斥量而实现的锁,会导致进程在用户态和内核态之间切换,相对开销较大
轻量级锁是相对于重量级锁而言的,轻量级锁的核心设计是在没有多线程竞争的前提下,减少重量级锁的使用以提高系统的使用效率。轻量级锁适合于线程交替执行代码块的情况,如果同一时刻有多个线程竞争同一个锁,则会导致轻量级锁膨胀为重量级锁
偏向锁
偏向锁的目的是在同一个线程多次获取某个锁的情况下尽量减少轻量级锁的执行路径,因为轻量级锁的获取和释放需要多次CAS的原子操作,而偏向锁的切换只需要执行一次CAS操作
随着锁竞争越来越激烈,锁可能从偏向锁升级到轻量级锁,再到重量级锁,但在Java中锁只单向升级,不会降级
分段锁
分段不是一种实际的锁,而是一种思想,用于将数据分段并在每个分段上分别单独加锁,把锁进一步细粒度化,以提高并发效率。ConcurrrentHashMap在内部就是使用分段锁实现的
如何进行锁的优化
-
减少锁持有的时间
-
减少锁粒度
-
锁分离
-
锁粗化:将关联性强的锁操作集中起来处理,以提高系统整体的效率
-
锁消除:消除误用锁操作导致的性能下降的情况