一、锁对象 Lock
1. 创建锁对象 锁对象同一时间只允许一个线程进入
Lock lock = new ReentrantLock(true);//可重入锁
2. 加锁,解锁
lock.lock();//加锁
lock.unlock();//解锁
3. 尝试加锁
lock.tryLock() //尝试加锁 加锁成功 true 失败返回false
//如果锁已经被其他线程持有,则当前线程不会等待,而是立即返回。这种方法适用于需要快速检查锁是否可用的情况,避免线程长时间等待。
4. 加锁之后一定要解锁,否则会造成死锁。
死锁(Deadlock)指的是在多线程编程中,两个或多个线程互相持有对方所需的资源,并且由于无法获得所需的资源而陷入无限等待的状态,导致程序无法继续执行下去。
发生死锁的必要条件有四个,分别是:
互斥条件(Mutual Exclusion):至少有一个资源每次只能被一个线程占用。
请求与保持条件(Hold and Wait):线程在持有资源的同时还可以请求其他资源。
不剥夺条件(No Preemption):线程不能被强制剥夺已经获得的资源,只能在自愿释放资源后才能由其他线程获取。
循环等待条件(Circular Wait):存在一种等待循环,即线程1等待线程2持有的资源,线程2等待线程3持有的资源,...,线程N等待线程1持有的资源。
当以上四个条件同时满足时,就可能发生死锁。一旦发生死锁,线程无法继续执行下去,程序会陷入无法解开的僵局,需要通过外部干预来解决。
为了避免死锁的发生,可以采取以下措施:
破坏互斥条件:例如,对于某些资源可以采用共享的方式,允许多个线程同时访问。
破坏请求与保持条件:一次性申请所有需要的资源,而不是一个一个地申请。
破坏不剥夺条件:允许线程在持有资源的同时,根据需要剥夺其他线程的资源。
破坏循环等待条件:通过定义资源的顺序,要求线程按照顺序申请资源,避免循环等待的发生。
以上措施可以在设计和实现多线程程序时考虑,来减少死锁的概率和影响。此外,通过合理的资源管理和使用,以及良好的编码规范,也可以减少死锁的发生。
二、读写锁
1. 读写锁
//ReentrantReadWriteLock不是一个锁,是一个锁容器,里面有一个读锁(共享锁)和一个写锁(独占锁);原本有读锁时可以再加一个读锁,但写锁只能在没有任何线程占用时才能添加。
public static ReentrantReadWriteLock rrwl = new ReentrantReadWriteLock();
//读锁:允许多个读线程同时访问共享资源,但写线程在获得写锁时会阻塞其他读线程和写线程,直到写操作完成。这提高了读取操作的并发性,同时保证了写入操作的独占性。读锁常用于读多写少的场景,例如缓存系统、数据库查询等。
//写锁:当一个线程获得写锁时,其他线程(无论是读线程还是写线程)都无法访问共享资源,直到写锁被释放。这确保了数据在写入时不会被其他线程读取或修改,从而避免了数据的不一致性。写锁常用于写多读少的场景,例如数据更新、写入操作等。
//获取读锁或写锁对象
Lock lock = rrwl.readLock();//读锁
Lock lock = rrwl.writeLock();//写锁
2. 线程的生命周期中,等待是主动的,阻塞是被动的
三、 synchronized:
1. 作用:
保持多线程下操作的互斥性
保证共享变量的修改及时可见性
有效解决重排序问题
2. 用法:
1.修饰普通方法,锁是当前类的实例对象
2.修饰静态方法,锁是当前类的Class对象
3.修饰代码块,锁是synchronized括号里配置的对象 synchronized (OBJ){ }
3. synchroniezd同步实际上是通过管理对象的monitor(监视器)实现的;
4. 可重入锁是指但当一个线程再次请求自己持有对象锁的临界资源时,请求将会成功。Java中synchronized是可重入的。
5. Java中默认为非公平锁,但是可以设置为公平锁
public static ReentrantLock rl=new ReentrantLock(true);//设置为公平锁
6. 公平锁:保证线程获取锁的顺序与线程请求锁的顺序一致,即按照先来先服务的原则。
非公平锁:线程获取锁的顺序与线程请求锁的顺序无关,允许插队操作。
四、等待唤醒机制:
notify notifyAll方法 wait方法
1. notify/notifyAll和wait方法必须在synchronized代码块或者synchronized方法调用。
被唤醒的线程重新进入就绪状态而不是运行状态。
2. 锁对象
所以对象都可以是锁对象,锁对象的方法在Object类中定义,但只有对象是锁对象的时候才能调用锁对象的方法,否则会报状态异常。
public static final Object OBJ = new Object();//新建对象
synchronized (obj) {//obj为锁对象
obj.wait(); //让执行到该行代码的线程进入等待状态 (等待池)
obj.notify();//唤醒一条被该锁对象wait的线程
obj.notifyAll(); //唤醒全部被该锁对象wait的线程
}
3. sleep与wait方法的区别:
1. wait 是Object中定义的方法,可以有锁对象调用,让执行到该代码的线程进入到等待状态
2. sleep是Thread类中定义的静态方法 ,可以让执行到该行的线程进入等待状态
区别: 1.sleep需要传入一个毫秒数 ,到达时间后会自动唤醒
wait不能自动唤醒,必须调用notify/notifyAll方法唤醒
2.sleep方法保持锁状态进入等待状态,wait方法会解除锁状态,其他线程可以进入运行
与sleep方法不同的是wait方法调用完成后,线程将被暂停,但wait方法将会释放当前持有的监视器锁(monitor),直到有线程调用notify/notifyAll方法后方能继续执行,而sleep方法只让线程休眠并不释放锁。同时notify/notifyAll方法调用后,并不会马上释放监视器锁,而是在相应的synchronized(){}/synchronized方法执行结束后才自动释放锁。
五、线程池 Executors
完成线程的创建、管理和销毁工作
线程池是一种管理和维护线程的机制,它可以在应用程序中预先创建一组可重复使用的线程,并在需要时分配这些线程来执行任务。
线程池的主要作用是优化线程的创建和销毁开销,提高线程的重用率和执行效率。通过线程池,可以将任务提交给线程池,线程池会自动管理线程的生命周期,使得线程可以被重复利用,避免频繁地创建和销毁线程的开销。
创建线程池对象
ThreadPoolExecutor tpe=new ThreadPoolExecutor(5,10,10,TimeUnit.SECONDS,qu,Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
线程池ThreadPoolExecutor的构造方法有7个参数依次是:
int corePoolSize 1.核心线程数 int maximumPoolSize 2.最大线程数 long keepAliveTime 3.持续存活时间 TimeUnit unit 4.时间单位 BlockingQueue<Runnable> workQueue 5.任务队列 ThreadFactory threadFactory 6.线程工厂(用于创建线程) RejectedExecutionHandler handler 7.拒绝执行的处理器(回绝策略)
实现Runnable接口或者Callable接口都可以作为线程任务执行
execute 方法 执行 :
将Runnable的线程任务交给线程池执行
Runnable run =EasyExecutors::method;
//交给线程池执行
tpe.execute(run);
submit 方法 提交:
将实现Runnable接口或Callable接口的线程任务提交给线程池执行
Callable<String> call = EasyExecutors::methodCall;
Future<String> f = tpe.submit(call);//提交
//tpe.submit(run);
shutdown 方法 关闭:
//关闭线程池
tpe.shutdown();//结束,销毁
Callable接口
Callable接口定义了一个单一的方法call(),该方法在执行任务时可以返回结果。与Runnable的run()方法不同,call()方法可以返回一个泛型类型的结果,也可以抛出一个Exception。在执行Callable任务时,可以通过Future对象来接收任务的返回结果。
Future类
Future类是用来表示异步计算结果的,它提供了一系列的方法来获取、取消和监控异步任务的执行状态,使得多线程编程更加方便和灵活。
get 方法 获取 :会等待线程执行完毕
cancel 方法 取消 :
对于正在执行中的任务,cancel方法的mayInterruptIfRunning参数可设为true,这样可通过中断执行线程来中断任务。通过其他途径来中断Executor内的线程是不允许的,因为你不知道该线程正在执行哪个任务;而Future对象是Executor创建的,它会与Executor线程池互相配合来实现其任务中断策略。
线程池的4种回绝策略
1. AbortPolicy (默认)放弃该任务并会抛出一个异常RejectedExecutionException
2. CallerRunsPolicy 调用者执行,让传递任务的线程执行此任务
3. DiscardOldestPolicy 放弃队列中最早提交的任务,不会抛出异常
4. DiscardPolicy 直接放弃新的任务,不会抛出异常
线程池的工作原理:
任务放置在工作队列中
1. 先检查池中是否有空闲的线程,如果有就让该线程执行任务
2. 如果没有空闲的线程,判断池中的线程数量有没有达到核心线程数:
没有达到核心线程数,创建新的线程执行任务,直到填满核心线程数;
如果已经达到,优先在队列中存储,直到队列填满;
3. 工作队列填满后,再添加新的任务,看是否达到最大线程数:
如果没有,创建新的线程执行任务,直到填满最大线程数;
已经填满最大线程数,队列也已经填满,没有空闲线程,就执行回绝策略
4. 线程池中的线程达到(超过)核心线程数,超出的数量在空闲时会根据存活时间进行销毁:
直到数量达到核心线程数,如果线程的数量少于核心线程数,不会消亡
Java中内置的线程池:
-
//Java中内置的线程池
-
//可以根据工作任务来创建线程,如果没有空闲的线程就创建新的线程 线程存活时间60s
-
Executors.newCachedThreadPool();
-
//设定最大线程数量的线程池
-
Executors.newFixedThreadPool(10);
-
//提供定时运行的处理方案
-
Executors.newScheduledThreadPool(10);
-
//创建一个具有单个线程的线程池,保障任务队列完全按照顺序执行
-
Executors.newSingleThreadExecutor();
在高并发高任务情况下,核心线程数怎么设置合适?
取决于以下几个因素:
-
系统资源:核心线程数应根据系统的处理能力来确定。如果系统的CPU、内存等资源充足,可以适当增加核心线程数来提高并发处理能力。
-
任务类型:不同类型的任务对线程的需求不同。如果任务是CPU密集型,即需要大量的计算资源,那么线程数可以设置得较少,以避免线程过多而导致资源竞争。如果任务是IO密集型,即需要等待IO操作的完成,那么线程数可以设置得较多,以充分利用CPU资源。
-
响应时间:线程池的核心目标是提供快速响应的服务。核心线程数的设置应能够确保任务能够在合理的时间内得到处理,而不是等待线程的创建和启动。
-
可用线程数:线程池的总线程数应根据系统的可用线程数来设置。可用线程数是指除了线程池的核心线程数之外,系统还能够分配给线程池的最大线程数。一般情况下,可用线程数可以设置为核心线程数的两倍或更多。
综合考虑以上因素,合适的核心线程数设置可以先根据系统资源和任务类型进行初步估算,然后进行性能测试和调优,根据实际情况进行调整。
枚举类
枚举类是一种特殊的类,它定义了一组常量作为其实例,并提供了一些功能扩展。
在Java中,我们可以使用关键字"enum"来定义枚举类。枚举类的实例通常用于表示一组相关的常量,例如颜色、星期几、季节等。
需要注意的是,枚举类的实例是唯一的,可以通过调用枚举常量的名称进行比较。例如,使用day == Day.MONDAY来比较两个枚举常量是否相等。
用关键字 enum 来创建枚举类,枚举类首行必须枚举所有实例
枚举类默认继承Enum,但不能写继承自Enum;
是可比较的:根据实例声明的顺序
枚举类是不可序列化的,也不能克隆
-
public enum EasyColor {
-
RED,YELLOW,GREEN,BLUE,PINK;
-
public void printColor(){
-
System.out.println(this.name());
-
System.out.println(this.ordinal());
-
}
-
}
-
class Test{
-
public static void main(String[] args) {
-
EasyColor.GREEN.printColor();
-
}
- }
六、枚举类
public enum EasyColor {
//枚举类 默认继承Enum
//首行 必须枚举所有的实例
RED, YELLOW, GREEN, BLUE, PINK;
public void printColor() {
System.out.println(this.name());
System.out.println(this.ordinal());//枚举常量序数
}
}
class Test {
public static void main(String[] args) {
EasyColor.GREEN.printColor();//GREEN 2
}
}