JUC学习札记
I.基础理论:
1.1 线程和进程
进程:系统进行资源分配和调度的基本单位
线程:操作系统能够进行运算调度的最小单位(分配处理器时间资源的基本单元)
1.2 线程中的状态
即Thread.State
,有以下状态:
NEW(创建)、 RUNNABLE(准备就绪)、 BLOCKED(阻塞)、 WAITING(不见不散)、 TIMED_WAITING(过期不候)、 TERMINATED(终结)
1.3 sleep
和wait
方法的区别
-
sleep
是Thread
的静态方法;而wait
是Object
的方法,任何对象示例都可以调用 -
sleep
不会释放锁,因此也不许要占用锁,wait
会释放锁,但调用的前提是当前线程占有锁,代码要在synchronized
中 -
它们都可以被
interrupted
方法中断
1.4 并发和并行的区别
并发:同一时间多个线程访问同一资源
并行:多项一起执行,执行完毕后进行汇总
1.5 用户线程和守护线程
1.用户线程:主线程结束,用户线程还在运行,jvm存活
2.守护线程:没有用户线程了,都是守护线程,jvm结束
II.核心知识点:
2.1多线程简单应用
- 例1.卖票场景:
class Ticket{
//票数
private int number = 30;
//操作方法:卖票
public synchronized void sale(){
if(number >= 0){
System.out.println(Thread.currentThread()
.getName+":卖出"+(number--)+",剩下:"+number);
}
}
}
public class SaleTicket{
//创建多个线程,调用资源类的操作方法
public static void main(String[] args){
//创建Ticket对象
Ticket ticket = new Ticket();
//创建3个线程
new Thread(new Runnable(){
@Override
public void run(){
//调用卖票方法
for(int i = 0; i < 40; i++){
ticket.sale();
}
}
},"saler1").start();
new Thread(new Runnable(){
@Override
public void run(){
//调用卖票方法
for(int i = 0; i < 40; i++){
ticket.sale();
}
}
},"saler2").start();
new Thread(new Runnable(){
@Override
public void run(){
//调用卖票方法
for(int i = 0; i < 40; i++){
ticket.sale();
}
}
},"saler3").start();
}
}
使用
synchronized
关键字修饰的方法能够自动实现加锁和解锁,如需手工进行加锁解锁操作,则可以使用lock
接口(如果没有及时释放锁,可能导致出现死锁的现象)
- 例2.使用
lock
接口实现卖票场景:
class Ticket{
//票数
private int number = 30;
//创建可重入锁
private final ReentrantLock lock = new ReentrantLock();
//操作方法:卖票
public void sale(){
//上锁
lock.lock();
try{
//判断是否有票
if(number >= 0){
System.out.println(Thread.currentThread()
.getName+":卖出"+(number--)+",剩下:"+number);
}
}finally{
//解锁
lock.unlock();
}
}
}
public class saleTicket{
public static void main(String[] args){
//创建Ticket对象
Ticket ticket = new Ticket();
//创建3个线程,runnable为函数式接口,使用lambda表达式创建对象
new Thread(()->{
for(int i = 1; i<40; i++){
ticket.sale();
}
},"sale1").start();
new Thread(()->{
for(int i = 1; i<40; i++){
ticket.sale();
}
},"sale2").start();
new Thread(()->{
for(int i = 1; i<40; i++){
ticket.sale();
}
},"sale3").start();
}
}
2.2线程间通信
- 例3.使用
synchronized
实现2个线程操作同一变量:
在数值判断时使用
if
,会产生虚假唤醒的情况,因此使用if
进行判断的位置需要用while
进行替换,使其在唤醒和正常执行的情况下都必须进行数值判断,避免出现预期外的结果产生
class Share{
//初始值
private int number = 0;
//数值增加方法
public synchronized void incr() throws InterruptedException{
//判断,如果number值不等于0时等待其值改变后做加法操作
if(number != 0){
this.wait();
}
//上方判断应替换为下面的形式
while(number != 0){
this.wait();
}
//如果number值等于0,则执行自增操作
number++;
System.out.println(Thread.currentThread().getName()+"::"+number);
//通知其他线程
this.notifyAll();
}
//数字减少方法
public synchronized void decr() throws InterruptedException{
//判断,如果number值不等于1时等待其值改变后做减法操作
if(number != 1){
this.wait();
}
//上方判断应替换为下面的形式
while(number != 0){
this.wait();
}
//如果number值等于1,则执行自减操作
number--;
System.out.println(Thread.currentThread().getName()+"::"+number);
//通知其他线程
this.notifyAll();
}
}
public class testThread{
//创建多个线程调用资源类的操作方法
Share share = new Share();
//创建两个线程
new Thread(()->{
for(int i = 0; i < 0; i++){
try{
share.incr();
}catch(InterruptedException e){
e.printStackTrace();
}
}
},"AA").start();
new Thread(()->{
for(int i = 0; i < 0; i++){
try{
share.incr();
}catch(InterruptedException e){
e.printStackTrace();
}
}
},"BB").start();
}
/** 使用Lock接口实现线程间通信**/
//创建资源类
class Share{
private int number = 0;
//创建Lock
private final Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
//+1操作
public void incr() throws InterruptedException {
//上锁
lock.lock();
try {
while (number != 0){
condition.await();
}
//执行+1
number ++;
System.out.println(Thread.currentThread().getName() + "::" + number);
//通知其他线程
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//解锁
lock.unlock();
}
}
//-1操作
public void decr() throws InterruptedException {
//上锁
lock.lock();
try {
while (number != 1){
condition.await();
}
number --;
System.out.println(Thread.currentThread().getName() + "::" + number);
//通知其他线程
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//解锁
lock.unlock();
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
Share share = new Share();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
share.incr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"t1").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
share.decr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"t2").start();
}
}
- 例4.线程间定制通信:
/**
* 启动3个线程,按照如下要求:
* t1打印5次,t2打印10次,t3打印15次
* 进行10轮
*/
class SharedResource {
private int flag = 1;
//创建Lock对象
private Lock lock = new ReentrantLock();
//创建三个Condition对象
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
private Condition c3 = lock.newCondition();
public void print5(int loop) throws InterruptedException {
lock.lock();
try {
while (flag != 1){
c1.await();
}
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "::" + i + ":轮数" +loop);
}
flag = 2;
c2.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print10(int loop) throws InterruptedException {
lock.lock();
try {
while (flag != 2){
c2.await();
}
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "::" + i + ":轮数" +loop);
}
flag = 3;
c3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print15(int loop) throws InterruptedException {
lock.lock();
try {
while (flag != 3){
c3.await();
}
for (int i = 0; i < 15; i++) {
System.out.println(Thread.currentThread().getName() + "::" + i + ":轮数" +loop);
}
flag = 1;
c1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
SharedResource sharedResource = new SharedResource();
new Thread(()->{
for (int i = 1; i < 11; i++) {
try {
sharedResource.print5(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"t1").start();
new Thread(()->{
for (int i = 1; i < 11; i++) {
try {
sharedResource.print10(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"t2").start();
new Thread(()->{
for (int i = 1; i < 11; i++) {
try {
sharedResource.print15(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"t3").start();
}
}
Lock
接口和synchronized
关键字的区别:
- Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现
- synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象的发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁
- Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断
- 通过Lock可以知道有没有成功获取锁,而synchronized无法办到
- Lock可以提高多个线程进行读操作的效率
在锁竞争不激烈的情况下,使用
synchronized
关键字和Lock
接口加锁的性能是差不多的,但是在大量线程竞争资源时,则使用Lock
加锁的性能要远远优于synchronized
Synchronized
实现同步的基础:Java中的每一个对象都可以作为锁
具体表现为以下3种方式:对于普通方法,锁是当前实例对象;对于静态同步方法,锁是当前Class对象;对于同步方法块,锁是当前括号中配置的对象
ReentrantLock在创建时可以通过构造器中设置true/false来选择使用公平锁和非公平锁,使用前者使得各个县城都有机会访问资源,使用后者可能会出现线程抢占资源的情况(但是执行效率高于公平锁)
synchronized
和Lock
都是可重入锁(递归锁):
前者隐式加锁、释放锁;后者手动加锁、释放锁。即拿到外层锁后内层的锁可以无障碍进入
2.3 死锁
**死锁:**两个或两个以上的进程在执行过程中,因为争夺资源而造成的一种互相等待的现象,如果没有外力干涉,它们无法再执行下去
**造成死锁的原因:**系统资源不足、进程运行推进顺序不合适、资源分配不当
手写死锁代码:
public class DeadLockDemo {
//创建两个对象
static Object a = new Object();
static Object b = new Object();
public static void main(String[] args) {
new Thread(()->{
synchronized (a){
System.out.println(Thread.currentThread().getName()+":持有锁a,试图获取锁b");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b){
System.out.println(Thread.currentThread().getName()+":获得锁b");
}
}
},"A").start();
new Thread(()->{
synchronized (b){
System.out.println(Thread.currentThread().getName()+":持有锁b,试图获取锁a");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (a){
System.out.println(Thread.currentThread().getName()+"获得锁a");
}
}
},"B").start();
}
}
验证是否产生死锁:
- 使用
jps -l
列出Java进程定位当前进程id - 使用JVM自带的堆栈跟踪工具
jstack <进程id>
查看栈信息,如果出现Found 1 deadlock.
则可以确定出现死锁
使用Callable接口创建线程:
Callable
的实现类不能直接传入Thread
的构造方法中,需要传入FutureTask
(一个Runnable实现类)的构造器,再将FutureTask
对象传入Thread
构造器中
class MyThread implements Callable{
@Override
public Integer call() {
System.out.println(Thread.currentThread().getName()+":使用callable创建线程");
return 200;
}
}
public class CallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> futureTask = new FutureTask(()->{
System.out.println(Thread.currentThread().getName()+":使用callable创建线程");
return 1024;
});
new Thread(futureTask,"callT").start();
new Thread(new FutureTask<Integer>(new MyThread()),"callT1").start();
while (!futureTask.isDone()){
System.out.println("wait ...");
}
System.out.println(futureTask.get());
System.out.println(futureTask.get());
System.out.println(Thread.currentThread().getName()+":执行结束!");
}
}
FutureTask类的特点:
主线程依次向下执行,可以单开几个线程做其他事情,最后做汇总,汇总只须做一次
Callable
与Runnable
之间的区别:
- 是否具有返回值
- 是否抛出异常
- 需要重写的方法,前者重写
call()
后者重写run()
读写锁
常见的几种锁:
乐观锁:支持并发操作,使用版本号控制事务提交,后提交事务的线程无法完成对数据的操作
悲观锁:不支持并发操作,每个线程操作操作前后需要上锁和释放锁,效率低下
行锁:对正在被操作的行加锁,未加锁的行可以被正操作(会发生死锁)
表锁:操作某条记录时对整张表进行加锁,其他的线程无法对该表进操作
读锁(共享锁):当两个线程对一条记都有读和修改的操作时,会出现死锁
写锁(独占锁):在两个线程对两条记录都有写操作时,会出现死锁
读写锁简单应用:
class MyCache {
//创建集合
private volatile Map<String, Object> map = new HashMap<>();
//读写锁
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
//放数据
public void put(String key, Object value) {
//添加写锁
rwLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+":正在写操作" + key);
//暂停一会
TimeUnit.MICROSECONDS.sleep(300);
//放数据
map.put(key, value);
System.out.println(Thread.currentThread().getName()+":写完了"+ key);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放写锁
rwLock.writeLock().unlock();
}
}
//读数据
public Object get(String key) {
//添加读锁
rwLock.readLock().lock();
Object result = null;
try {
System.out.println(Thread.currentThread().getName()+":正在读操作" + key);
//暂停一会
TimeUnit.MICROSECONDS.sleep(300);
//取数据
result = map.get(key);
System.out.println(Thread.currentThread().getName()+":读完了"+ key);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放读锁
rwLock.readLock().unlock();
}
return result;
}
}
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache mc = new MyCache();
//创建线程放数据
for (int i = 0; i < 5; i++) {
final int num = i;
new Thread(()->{
mc.put(num+"", num+"");
},String.valueOf(i)).start();
}
//创建线程读数据
for (int i = 0; i < 5; i++) {
final int num = i;
new Thread(()->{
mc.get(num+"");
},String.valueOf(i)).start();
}
}
}
---------------------控制台输出------------------------
0:正在写操作0
0:写完了0
1:正在写操作1
1:写完了1
2:正在写操作2
2:写完了2
3:正在写操作3
3:写完了3
4:正在写操作4
4:写完了4
0:正在读操作0
1:正在读操作1
2:正在读操作2
3:正在读操作3
4:正在读操作4
0:读完了0
2:读完了2
3:读完了3
1:读完了1
4:读完了4
使用不同锁下的状态:
无锁 | 独占锁(synchronized、ReentrantLock) | 读写锁(ReentrantReadWriteLock) | |
---|---|---|---|
情况 | 多线程抢夺资源 | 每次只能由一个线程进行操作 | 两个读操作之间可以进行共享,两个写操作之间只能由其中一个线程执行操作 |
缺陷 | 操作混乱,容易产生各种问题 | 无论哪种操作都不能共享 | 读完才可以进行写操作,写操作时可以进行读操作;容易造成锁饥饿 |
jdk8中写锁降级为读锁的过程
获取写锁 ->
获取读锁 ->
释放写锁 ->
释放读锁
注:读锁不能升级为写锁
2.4阻塞队列
阻塞队列特点:
当队列是空的,从队列中获取元素的操作将会被阻塞
当队列是满的,向队列中添加元素的操作将会被阻塞
试图从空的队列中获取元素的线程将会被阻塞,直到其他的线程向空的队列中插入新的元素
试图向已满的队列中插入元素的线程将会被阻塞,直到其他的线程从队列中移除一个或多个元素或者完全清空,使得队列变得空闲起来并继续新增元素
BlockingQueue中的核心方法
方法类型 | 抛出异常 | 特殊值 | 阻塞 | 超时 |
---|---|---|---|---|
插入 | add(e) | offer(e) | put() | offer(e,time,unit) |
移除 | remove() | poll() | take() | poll(time,unit) |
检查 | element() | peek() | / | / |
抛出异常 | 当阻塞队列满时,再往队列中add插入元素会抛出IllegalStateException:Queue full;当阻塞队列空时,再往队列中remove移除元素会抛出NoSuchElementException |
---|---|
特殊值 | 插入方法,成功返回true,失败返回false;移除方法,成功返回出队列的元素,队列里没有就返回null |
一直阻塞 | 当阻塞队列满时,生产者线程继续往队列中put元素,队列会一直阻塞生产者线程直到take数据or响应中断退出;当阻塞队列空时,消费者线程试图从队列中take元素,队列会一直阻塞消费者线程直到队列可用 |
超时退出 | 当阻塞队列满时,队列会阻塞生产者线程一定时间,超过限时后生产者线程会退出 |
2.5线程池
线程池概念
线程池是一种线程使用模式,线程过多会带来调度开销,进而影响局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。
优势:线程池的工作主要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。
特点:降低资源消耗、提高响应速度、提高线程的可管理性
Java中的线程池是通过
Executor
框架实现的,该框架中用到了Executor
,Executors
,ExecutorService
、ThreadPoolExecutor
这几个类
常用线程池类型
/**一池n线程**/
ExecutorService threadPool1 = Executors.newFixedThreadPool(10);
/**一池一线程**/
ExecutorService threadPool2 = Executors.newSingleThreadExecutor();
/**可扩容线程池**/
ExecutorService threadPool3 = Executors.newCachedThreadPool();
try {
for (int i = 0; i < 50; i++) {
//执行execute方法后,线程才会创建
threadPool3.execute(()->{
System.out.println(Thread.currentThread().getName()+":正在办理业务!");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool3.shutdown();
}
上述3种类型线程池底层都是使用ThreadPoolExecutor
来实现。同时,也可以使用ThreadPoolExecutor
来自定义线程池,自定义线程池时的7个参数如下所示:
/** 1.常驻线程数量**/
int corePoolSize,
/** 2.最大线程数量**/
int maximumPoolSize,
/** 3.线程存活时间**/
long keepAliveTime,
/** 4.时间单位**/
TimeUnit unit,
/** 5.阻塞队列**/
BlockingQueue<Runnable> workQueue,
/** 6.线程工厂(用来创建线程)**/
ThreadFactory threadFactory,
/** 7.拒绝策略 **/
RejectedExecutionHandler handler
// 4种拒绝策略
-CallerRunsPolicy:将任务回退到调用者,从而降低任务流量
-AbortPolicy:默认,直接抛出RejectedExecutionException异常
-DiscardPolicy:默认丢弃无法处理的任务,不做任何处理也不抛出异常
-DiscardOldestPolicy:抛弃阻塞队列中等待最久的任务
注意:【强制】通常情况下不允许通过Executors
创建线程,而是通过ThreadPoolExecutor
的方式自定义创建线程池,为了规避资源耗尽的风险:
Executors返回线程池对象的弊端:
FixedTHreadPool
和SingleThreadPool
允许的请求队列长度为Integer.MAX_VALUE
,可能会堆积大量的请求,从而导致OOMCachedThreadPool
和ScheduledThreadPool
允许创建线程数量为Integer.MAX_VALUE
,从而导致OOM
使用ThreadPoolExecutor
自定义线程池示例:
public class CustomThreadPoolDemo {
public static void main(String[] args) {
ExecutorService threadpool = new ThreadPoolExecutor(7,
10,
2L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3),
new CustomizableThreadFactory("devon-thread"),
new ThreadPoolExecutor.DiscardPolicy());
try {
for (int i = 0; i < 20; i++) {
threadpool.execute(()->{
System.out.println(Thread.currentThread().getName()+":正在受理业务!");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadpool.shutdown();
}
}
}
--------------------------控制台输出----------------------------
devon-thread1:正在受理业务!
devon-thread3:正在受理业务!
devon-thread2:正在受理业务!
devon-thread4:正在受理业务!
devon-thread5:正在受理业务!
devon-thread6:正在受理业务!
devon-thread7:正在受理业务!
devon-thread1:正在受理业务!
devon-thread1:正在受理业务!
devon-thread8:正在受理业务!
devon-thread8:正在受理业务!
devon-thread9:正在受理业务!
devon-thread7:正在受理业务!
devon-thread2:正在受理业务!
devon-thread1:正在受理业务!
devon-thread2:正在受理业务!
devon-thread10:正在受理业务!
devon-thread7:正在受理业务!
devon-thread3:正在受理业务!
devon-thread8:正在受理业务!
Process finished with exit code 0
------ 持续更新中,敬请期待 ------