—— 目录 ——
1. 线程的创建
- 继承 Thread 类,重写 run() 方法,使用
xx.start()
启动
注意:不能使用xx.run()
去启动线程,这只是调用了 run 方法,并没有成功启动一个线程 - 实现 Runnable 接口,实现 run() 方法,需要
new Thread(xx).start()
启动 - 实现 Callable 接口,实现 call() 方法,有返回值,类型右泛型决定
// 创建执行服务 ExecutorService executorService = Executors.newFixedThreadPool(<线程数>); // 提交执行 Future<String> future = executorService.submit(<Callable 的实现类实例>); // 获取结果 String result = future.get(); // 关闭服务 executorService.shutdownNow();
注意: 线程调用 start() 方法后并不是就开始执行了,而是等待获取 CPU 时间片。所以线程 start() 的顺序并不等于线程执行的顺序,这也体现了线程执行的随机性,谁先得到 CPU 时间片谁就先执行
2. 线程的生命周期与监测
- new 新建状态:当线程被创建出来时
- runnable 就绪状态:当线程被 start() 时进入就绪状态
- 当获取到 CPU 时间片时执行 run() 方法
- 如果时间片不够用则重新进入 runnable 状态,再次等待时间片
- 如果够用了执行完毕,就进入 terminated 结束状态
- 如果中途被 sleep(time) 或者 wait(time) 了则进入 time waiting 时间等待状态
- 如果中途被 wait() 了则进入 waiting 等待状态
- blocked 等待锁状态:当线程抢不到锁时,被锁在外面,等待锁被释放继续进入,如果进入了则变成 runnable 就绪状态
- waiting 等待状态:当线程调用 wait() 或 join时进入等待状态,直到被 notify() 或 notifyAll() 才会重新进入 runnable 状态
- time waiting 时间等待状态:当线程被 sleep(time) 或者 wait(time) 后进入,直到睡眠时间结束后重新进入 runnable 状态
- terminated 结束状态:当线程的 run() 方法执行完毕后进入结束状态
其中 locked、waitting 和 time waiting 统称为阻塞状态
不建议使用 stop() 方法让线程停止,可以使用标志位让线程主动停止,如:
@Override
public void run() {
while(flag) {
// ...
}
}
public void stop(){
flag = false;
}
3. 常用方法
1) 线程状态的监测:getState();
2) 判断一个线程是否存活:isAlive();
3) 程序中活跃的线程数:activeCount();
4) 得到当前线程:currentThread();
5) 设置一个线程为守护线程:setDaemon();(直到程序结束时才结束)
4. sleep 和 wait 的区别
- sleep 在 Thread 类中
wait 在 Object 类中 - sleep 只是先停下来,并不会释放锁
wait 会将锁让出来给别的线程 - sleep 可以在任何代码块中使用
wait 只能在同步方法或同步代码块中使用为什么 wait 只能放在同步方法或者同步代码块当中? 因为 wait 之后需要由别的线程来 notify 唤醒 如果不用同步锁上的话,很可能是 wait 还没执行,notify 就先执行了 这样在 wait 的那个线程将不能像预期那样被唤醒 而如果加上了锁,即使该线程还没执行到 wait 那么由于用的是同一把锁,notify 的那个线程还不能执行 就只能等到该线程执行了 wait ,才能执行到 notify ,这样就能确保不出现上面的问题
5. yield 和 join
yield 为礼让,指一个正在执行的线程停止执行,重新出来与其他线程竞争
join 则是强行执行,在其他线程还没执行完毕时,join 可强行获得执行权
6. 线程的优先级
获取优先级
thread.getPriority();
设置优先级
thread.setPriority();
最小优先级:MIN_PRIORITY = 1
最大优先级:MAX_PRIORITY = 10
不在这个范围的优先级都会报错
优先级必须在线程启动之前设置,启动后再设置没有效果
默认的优先级都是 NORM_PRIORITY = 5
所以主线程的优先级没法修改,一直是默认优先级
注:优先级大的不一定就先执行,只是先执行的机会变大了
7. 线程同步和锁
① synchronized
同步块(可以锁任何对象)
synchronized (object) {
// ...
}
同步方法(默认锁的是当前对象)
public synchronized void method() {
// ...
}
相当于
public void method() {
synchronized (this) {
// ...
}
}
加锁的对象是需要增删改的
② Lock
定义 lock 锁
ReentrantLock lock = new ReentrantLock();
锁必须紧跟 try 代码块,且 unlock 要放到finally第一行,如:
public void method() {
lock.lock();
try {
// ...
} finally {
lock.unlock();
}
}
synchronized 与 Lock 的对比:
synchronized 是隐式锁,出了作用域会自动释放
Lock 是显示锁,需要手动开启和关闭锁,而且它只有代码锁,没有方法锁
③ 轻量级同步机制:volatile
使得变量在多个线程之间可见,即一旦一个被 volatile 修饰的值改变了,其他线程立即就知道了,常用于标志位的改变通知其他线程。
例如可以用于一个线程在某个标识下循环做某件事,而另一个线程通过改变该标识让其停止循环
④ 锁的分类
-
可重入锁
synchronized 和 Lock 都是可重入锁
指的是,如果一个线程持有一个锁的时候还能够继续成功申请该锁,称该锁是可重入的, 否则就称该锁为不可重入的 -
死锁
- 资源是互斥的
- 持有资源而不释放
- 资源不可抢占
- 循环等待 / 相互等待
处理方法:当需要获得多个锁时,所有线程获得锁的顺序保持一致即可
-
乐观锁
乐观锁指的是当多个线程同时修改一个数据时,该数据会被带上一个版本号,这时所有线程都可以同时修改这个数据,产生不同版本的数据,最后再区决定那个版本的数据被保留 -
悲观锁
悲观锁指的是当一个数据在被修改时,其他线程想要修改这个数据都会被阻塞,等待锁的释放
8. 线程池
线程池存放了一定数量的线程,外部只需要将 Runnable 任务放入线程池的阻塞队列中,然后各个线程会从阻塞队列中取出任务并执行
线程池的创建
ThreadPoolExecutor 的构造方法:
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,
long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory, RejectedExecutionHandler handler)
各个参数含义:
- corePoolSize, 指定线程池中核心线程的数量
- maxinumPoolSize,指定线程池中最大线程数量
- keepAliveTime,当线程池线程的数量超过 corePoolSize 时,多余的空闲线程的存活时长,即空闲-线程在多长时长内销毁
- unit, 是 keepAliveTime 时长单位
- workQueue,任务队列,把任务提交到该任务队列中等待执行
- threadFactory,线程工厂,用于创建线程
- handler 拒绝策略,当任务太多来不及处理时,如何拒绝
队列分类:
- 直接提交队列 SynchronousQueue,该队列没有容量,有新任务时则交给线程执行,线程不够就创建新的,达到最大线程数 maxinumPoolSize 则执行拒绝策略
- 有界任务队列 ArrayBlockingQueue,该队列需指定一个容量,当有任务要执行时,若线程数小于核心线程数 corePoolSize 则创建新线程。如果队列已满,在线程数小于最大线程数 maxinumPoolSize 时创建新的线程来执行,已满的话执行拒绝策略
- 无界任务队列 LinkedBlockingQueue,队列没有容量,除非系统资源耗尽,否则不会存在仍无入队失败的情况。有新任务时,在线程数小于核心线程数 corePoolSize 时,创建新的线程来执行任务,大于时则加入等待队列。
- 优先任务队列 PriorityBlockingQueue,是一个特殊的无界队列,前面两个时按照先进先出原则,而这个时按照优先级先后执行的。
9. CyclicBarrier 用法
当等待的线程达到一定数量时,才让所有线程一起执行
常用于多线程的分组计算
用法:在一个线程的某一个位置加上 cyclicBarrier.await();
则线程将会在执行到这里时被阻塞
直到被阻塞的线程达到预设数量时,全部线程的阻塞将一起被释放,如:
public static void main(String[] args) throws InterruptedException {
CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
for (int i = 0; i < 6; i++) {
new Thread(()->{
try {
System.out.println("被阻塞之前");
cyclicBarrier.await();
System.out.println("阻塞释放后执行");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
Thread.sleep(500);
}
}
10. CountDownLatch 类
该类是一个同步辅助类,用在其他线程需要执行的操作之前
在需要的线程中,某一个位置加上 await() 方法使线程阻塞,让线程一直等待
在它初始化时会设定一个默认值 n,每次调用 countDown() 方法时都会使 n 值 -1
当 n 值减到 0 时,await() 将放开阻塞,达到在等待的线程开始执行往后的任务的效果
该功能可以使用在抢票系统中
通常由很多用户过来抢票,但抢票时间还没到
这是利用这个类,将用户阻塞在抢票之前
接着使用 countDown() 模拟抢票倒计时
等到抢票时间到了,await() 阻塞将放开,达到所有用户共同抢票的效果。
点滴零星,万籁无声(IceClean)