线程锁对象
锁对象Lock
创建锁对象
可重入锁
公平锁
方法
lock.lock(); | 加锁 |
lock.trylock(); | 尝试加锁,加锁成功返回true,失败返回false |
lock.unlock(); | 解锁 |
读写锁
ReentrantReadWriteLock是锁的实现类,内部维护了两个锁,一个读锁,一个写锁
获取读锁
获取写锁
写锁是独占的,读锁是共享的
在使用synchronized代码块时在括号内需要锁对象
代码块内锁对象可以使用方法
以OBJ作为代码块的锁对象
OBJ.notify() | 唤醒一条被该对象wait的线程 |
OBJ.notifyAll() | 唤醒全部被该锁对象wait的线程 |
OBJ.wait() | 让执行到该行代码的线程进入等待状态(等待池) |
线程池
池即重用
线程池完成线程的创建和管理,销毁工作
定义
qu定义
ThreadPoolExecutor的构造方法共有七个参数
int corePoolSize | 核心进程数 |
int maximumPoolSize | 最大进程数 |
long keepAliveTime | 存活时间 |
TimeUnit unit | 时间单位 |
BlockingQueue<Runnable> workQueue | 工作队列 |
ThreadFactory threadFactory | 线程工厂 |
RejectedExecutionHandler handler | 回拒策略 |
线程任务可以有两种方式设置
Runnable
Callable
其中使用Runnable方法也可以使用submit方法设置
submit方法可以返回Feature<V>类型获得线程返回的信息
f.get()方法可以在等待线程执行完毕后获取线程返回的信息
线程池的四种回绝策略
1.AbortPolicy() 放弃该任务并会抛出一个异常RejectedExecutionException
2.CallerRunsPolicy() 调用者执行,让传递任务的线程执行任务
3.DiscardOldestPolicy() 放弃队列当中时间最长的任务,不会抛出异常
4.DiscardPolicy() 直接放弃新的任务,不会抛出异常
线程池的四种内置线程池
- Executors.newCachedThreadPool();
java中内置的线程池对象
可以根据工作任务来创建线程,如果没有空闲的线程就创建新的线程,线程存活时间60s
- Executors.newFixedThreadPool(10);
设定最大线程数量的线程池
- Executors.newScheduledThreadPool(10);
提供定时运行的处理方案
- Executors.newSingleThreadExecutor();
创建一个具有单个线程的线程池,保障任务队列完全按照顺序执行
线程池的工作原理
任务放置在工作队列中
- 池中是否有空闲的线程,如果有便让该线程执行任务
- 如果池中没有空闲的线程,判断线程数量是否达到核心线程数
- 如果没有达到,创建新的线程执行任务,直到填满核心线程数。
如果已经达到,优先在队列存储,直到队列填满
- 当工作队列填满后,判断池中的线程数量有没有达到最大线程数
没有达到最大线程数,创建新的线程执行任务
直到填满最大线程数
- 已经填满最大线程数,队列也已经填满,没有空闲的线程,就执行回绝策略
注意:线程池中的线程达到(超过)核心线程数,超出的数量会根据存活时间,进行销毁。直到数量达到核心线程数。
如果线程的数量少于核心线程数,不会消亡
单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供了一个全局访问点来访问该实例。
注意:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
单例模式的实现方式
1、懒汉式,线程不安全
这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。
这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作。
2、懒汉式,线程安全
这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。
优点:第一次调用才初始化,避免内存浪费。
缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。
getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)。
3、饿汉式
这种方式比较常用,但容易产生垃圾对象。
优点:没有加锁,执行效率会提高。
缺点:类加载时就初始化,浪费内存。
它基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。
4、双检锁/双重校验锁(DCL,即 double-checked locking)
这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
getInstance() 的性能对应用程序很关键。
5、枚举
这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
不能通过 reflection attack 来调用私有构造方法。
死锁
死锁是一种非常严重的bug,是说多个线程同时被阻塞,线程中的一个或者多个又或者全部都在等待某个资源被释放,造成线程无限期的阻塞,导致程序不能正常终止
死锁产生的四个必要条件
- 互斥使用:当资源被一个线程使用或者占用时,别的线程不能使用该资源
- 不可抢占:获取资源的一方,不能从正在使用资源的一方抢占掠夺资源,资源只能被使用者主动释放
- 请求和保持:资源请求者在请求别的资源时,同时保持对已有资源的占有
- 循环等待:即p1占有p2的资源,p2占有p3的资源,p3占有p1的资源,这样形成了一个等待环路
如何避免死锁
死锁的产生必须满足互斥使用,不可抢占,请求和保持,循环等待这四个条件,但是只要破坏其中任意一个条件即可破坏死锁,其中最容易破坏的就是循环等待这个条件,那么如何破坏循环等待这个条件呢?
多个线程约定好一定的顺序,按照这个顺序加锁释放锁