本章内容
- 线程同步
- 线程状态
同步方法
- 使用synchronized修饰的方法就叫做同步方法。保证A线程执行该方法的时候,其他线程只能在方法外面等待。
格式
public synchronized void method(){
// 可能会产生线程安全的代码
}
备注:同步锁是谁?
对于非static方法,同步锁就是this
对于static方法,我们使用当前方法所在类的字节码对象(类名.class)当同步锁
Lock锁
'java.util.concurrent.Locks.Lock’机制提供了比synchronized代码块和synchronized同步方法更加广泛的所操作
同步代码块/同步方法具有的功能,Lock都有,除此之外更强大,更能体现面向对象的特征
Lock锁也称为同步锁,定义了加锁与解锁的动作:
- public void lock() 加同步锁
- public void unlock() 释放同步锁
线程状态
线程状态的描述:
当线程被创建被启动之后,他不是一启动就进入到运行状态,也不是一直处于执行状态。在线程生命周期中,有六种状态
在java的API帮助文档中,java.lang.Thread.State描述了线程的六种状态
线程状态 | 导致状态发生条件 |
---|---|
NEW(新建) | 线程刚被创建,还没有启动start |
RUNNABLE(可运行状态) | 线程可以在java虚拟机中运行的状态,可以是正在运行自自己的代码,也可能没有,取决于操作系统处理器 |
BLOCKED(锁阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他线程所持有,则该线程进入blocked状态,当该线程持有锁时,该线程就进入Runnable状态 |
WAITING(无限等待) | 一个线程在等待另一个线程执行一个动作,该线程就进入到了waiting状态,进入waiting状态后,不能自动唤醒的,必须等待另一个线程调用notify或者是notifyAll方法才能够唤醒 |
TIMED-WAITING(计时等待) | 同waiting状态,有几个方法有超时参数,调用他们将进入TIMED-WAITING状态,这一状态将一直保持到超时期满,或者是收到唤醒通知,带有超时参数的常用方法有Thread.sleep(); Object.wait(); |
TERMINATED(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡 |
Timed Waiting(计时等待)
一个正在限时等待另一个线程执行一个(唤醒)动作的线程处于这一状态。
当我们调用sleep方法之后,当前正在执行的线程就进入到了计时等待状态
练习:实现一个计数器。计数到100,每个数字之间暂停一秒。每隔10个数字输出一个字符串
public class Mythread extends Thread{
@Override
public void rum(){
for(int i=1;i<=100;i++){
if(i%10==0){
System.out.println("----------"+i);
}
System.out.println(i);
try{
Thread.sleep(1000);
}catch(Exception e){
e.printStackTrace();
}
}
}
//准备一各main函数
public static void main(String []args){
new Mythread().start();
}
}
备注:
1.进入到Timed Waiting 状态的一种常见操作是调用sleep 方法,单独的线程也可以调用,不一定非要有协作关系
2.为了让其他线程有机会执行到,一般建议将Thread.sleep 方法调用放到run方法内,这样才能保证该线程执行过程中会睡眠
3 sleep 方法与锁无关,线程睡眠到期会自动苏醒并返回到Runnable状态。sleep方法里面的参数指定的时间是线程不会运行的最短时间,因此sleep方法不能保证该线程睡眠到期后就会立刻开始执行
Blocked锁阻塞
一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态
比如:线程A和线程B代码中使用了同一把锁,如果线程A获得了锁对象,线程A就进入到了Runnable状态,线程B就进入到锁阻塞状态
Waiting 无限等待状态
一个正在无限等待另一个线程执行一个特别的唤醒动作的线程处于这一状态
一个调用了某个对象的Object.wait方法的线程,会等待另一个线程调用此对象的Object.notify方法或者Object.notifyAll方法
其实Waiting状态并不是一个线程的操作,他体现的是多个线程之间的通信,可以理解为多个线程之间的协作关系,多个线程会争取锁,同时相互之间有存在协作关系
等待唤醒机制
线程间通信
概念:多个线程在处理同一个资源的时候,但是处理的动作(线程的任务)却又不相同
比如说:线程A用来生产娃哈哈,线程B用来消费娃哈哈饮料,娃哈哈可以理解为同一资源,线程A与线程B处理的动作不一样,一个是生产,一个是消费,那么线程A与线程B之间就存在线程通信问题。
为什么要处理线程之间的通信
多个线程并发在执行,在默认情况下,CPU是随机切换线程的,当我们需要多个线程共同来完成一个任务时,并且我们希望他们有规律的执行, 那么多个线程之间都需要一些协调通信,以此来帮助我们达到多线程共同操作同一份数据
如何保证线程间通信有效的利用资源?
多个线程之间在处理同一个资源的时候,任务并不相同。需要线程通信来帮助我们解决线程之间对同一个变量的使用或者操作,就是多个线程在操作同一份数据时,为了避免对同一共享变量的争夺,也就是我们需要通过一定的手段使各个线程有效的利用资源。而这个手段就是等待唤醒机制
什么是等待唤醒机制呢?
这是多个线程之间的一种协作机制,就是一个线程进行了规定操作后,就进入到了等待状态(wait方法),等待其他线程执行完他们的指定代码后,再将其唤醒(notify方法)。在有多个线程进行等待时,如果需要,可以使用notifyAll方法来唤醒所有的等待线程
wait/notify之间就是线程间的一种协作机制
等待唤醒中的方法
等待唤醒机制就是用来解决线程间通信问题的,可以使用到的方法有以下三个:
- wait() 线程不在活动,不再参与调度,进入到wait set中,因此不会浪费CPU资源,也不会去竞争锁,这是的线程状态就是WAITING状态,还要等着别的线程执行一个特别的动作(唤醒通知notify)在这个对象上等待的线程从wait set中释放出来。重新进入到调度队列中
- notify() 选取所通知对象的wait set中的一个线程释放
- notifyAll()释放所通知对象的wait set中的全部线程
备注:
哪怕只通知了一个等待线程,被通知的线程也不能立即恢复执行,因为他当初中断的地方是在同步块内,而此刻他已经不持有锁了,所以他需要再次尝试获取锁(很可能面临着其他线程的竞争),成功后才能在当初调用wait方法之后的地方恢复执行。
总结:
如果能获取到锁,线程就从WAITING状态转变成RUNNABLE状态,否者从wait set中,又进入set中,线程就从WAITING状态转变成BLOCKED状态.
调用wait和notify的注意细节:
- wait方法与notify方法必须由同一个锁对象调用,因为,对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法
- wait方法和notify方法时属于Object类的方法。因为锁对象可以是任意对象。而任意对象的所属类都是继承了Object类。
- wait方法与notify方法都必须在同步代码块或者是同步方法中使用。因为必须通过锁对象调用这两个非来实现线程通信
线程池
概念
- 线程池:其实就是一个可以容纳多个线程的容器,其中的线程可以反复使用,省去了频繁的创建线程对象的操作,无需反复创建而消耗过多的系统资源
由于线程池中有许多操作都是与优化系统资源有关。
线程池的工作原理
当程序第一次启动的时候,创建多个线程,保存到一个集合当中,当我们使用线程的时候,就可以从集合当中去取出一个线程来使用。
Thread t=list.remove(0):返回的是被移除的元素,(线程只能被一个任务使用)
Thread t=list.removeFirst()
当我们使用完的时候,再把线程放回去。
list.add(t);
list.addLast();
合理利用线程池的好处
- 降低资源消耗。减少了线程的创建与销毁的次数,每个工作线程都可以被重复利用,可以执行多个任务
- 提高了响应速度,当任务到达时,任务可以不需要等到线程的创建就能立即创建
- 提高线程的可管理性。可以根据系统的承受能力调整线程池中工作线程的数目,防止因为消耗过多的内存导服务器宕机(每个线程大约需要1M内存,线程越多,消耗内存就越大,死机的风险就更高)
线程池的使用
java里面线程池的顶级接口是java.util.concurrent.Executor,但是严格意义上讲Executor并不是一个线程池,只是一个执行线程的工具,真正的线程池接口是java.util.concurrent.ExecutorSrevice
因此在java.util.concurrent.Executors线程工厂类提供了一些静态工厂方法,生产一些常用的线程池,官方也建议使用Executors来创建线程池对象。
- public static ExecutorSrevice newFixed(int nThreads) 返回的就是线程池对象、(创建的线程池是有界的线程池,就是池中的线程个数可以指定最大容量)。
获取到了一个线程池ExecutorSrevice对象,在该类中定义了一个使用线程池对象的方法如下:
- public Future<?> submit(Runnable task): 获取线程池中的某一个线程对象,并执行
Feture接口:用来记录线程任务执行完毕以后产生的结果的。线程的创建和使用
使用线程池中线程对象的步骤
- 创建线程池对象
- 创建Runnable接口子类对象。(task)
- 提交Runnable接口子类对象。(take task)
- 关闭线程池(一般不关)