多线程 - 2
使用内部类创建线程
- 通常我们可以通过匿名内部类的方式创建线程,使用该方式可以简化编写代码的复杂度,当一个线程仅需要一个实例时我们通常使用这种方式来创建。
1
2
3
4
5
6
7创建方式:
Thread t = new Thread(){
public void run() {
//线程体
}
};
t.start();//启动线程
synchronized关键字
- synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
- 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
- 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
- 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
- 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。
- 实现原理:
JVM 是通过进入、退出对象监视器( Monitor )来实现对方法、同步块的同步的。
具体实现是在编译之后在同步方法调用前加入一个 monitor.enter 指令,在退出方法和异常处插入 monitor.exit 的指令。其本质就是对一个对象监视器( Monitor )进行获取,而这个获取过程具有排他性从而达到了同一时刻只能一个线程访问的目的。而对于没有获取到锁的线程将会阻塞到方法入口处,直到获取锁的线程 monitor.exit 之后才能尝试继续获取锁。
锁机制
- Java提供了一种内置的锁机制来支持原子性:
- 同步代码块(synchronized),同步代码快包含两部分:一个作为锁的对象的引用,一个作为由这个。
- 若方法所有代码都需要同步也可以给方法直接加锁。
- 每个Java对象都可以用做一个实现同步的锁,线程进入同步代码快之前会自动获得锁,并且在退出同步代码时自动释放锁,而且无论是通过正常途径还是通过抛异常退出都一样,获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。
- 选择合适锁对象:
- 使用synchroinzed需要对一个对象上锁以保证线程同步。
- 多个需要同步的线程在访问该同步块时,看到的应该是同一个所对象引用。否则达不到同步效果。
- 通常我们会使用this来作为锁对象。
- 选择合适锁范围:
- 在使用同步块时,应当尽量在允许的情况下减少同步范围,以提高并发的执行效率。
静态方法锁
- 当我们对一个静态方法加锁,那么该方法锁的对象是类对象。每个类都有唯一的一个类对象。获取类对象的方式: 类名.class
- 静态方法与非静态方法同时声明了synchronized,他们之间是非互斥关系的。原因在于,静态方法锁的是类对象而非静态方法锁的是当前方法所属对象。
使用ExecutorService实现线程池
- ExecutorService是java提供的用于管理线程池的类。
- 线程池有两个主要作用:
- 控制线程数量
- 重用线程
- 当一个程序中创建大量线程,并在任务结束后销毁,会给系统带来过度消耗资源,以及过度切换线程的危险,从而可能导致系统崩溃。
- 概念:
- 首先创建一些线程,他们的集合称为线程池,当服务器接收到一个客户请求后,就从线程池中取出一个空闲的线程为之服务,服务完成后不关闭该线程,而是将该线程还回到线程池中。
- 在线程池的编程模式下,任务是提交给整个线程池,而不是直接交给某个线程,线程池在拿到任务后,他就在内部找有无空闲的线程,再把任务交给内部某个空闲的线程。一个线程同时只能执行一个任务,但是可以同时向一个线程池提交多个任务。
- 线程池的几种实现策略:
- Executors.newCachedThreadPool():
- 创建一个可以根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。
- Executors.newFixedThreadPoll(int nThreads):
- 创建一个可重用固定线程集合的线程池,以共享的无界队列方式来运行这些线程。
- Executors.newScheduledThreadPool(int corePoolSize):
- 创建一个线程池,它可安排在给定延迟后运行命令或者定期的执行。
- Executors.newSingleThreadExecutor():
- 创建一个使用单个worker线程的Executor,以无界队列方式来运行该线程。
- Executors.newCachedThreadPool():
BlockingQueue
- BlockingQueue从字面意思就知道它是阻塞队列,它在以下两种情况会造成阻塞:
- 当队列满了的时候进行入队操作。
- 当队列空了的时候进行出队操作。
也就是说,当一个线程对已经满了的队列进行入队操作时,会被阻塞,除非另外一个线程进行了出队操作。或者当一个线程对一个空的队列进行出队操作的时候,会被阻塞,除非另外一个线程进行了入队的操作。
- 阻塞队列是线程安全的,主要用于生产者/消费者的场景。比如一个线程在队尾进行put操作,另外一个线程在队头进行take操作。需要注意的是BlockingQueue不能插入Null值,否则会报NullPointerException异常。
方法\处理方式 | 抛出异常 | 返回特殊值 | 一直阻塞 | 超时退出 |
---|---|---|---|---|
插入方法 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除方法 | remove() | poll() | take() | poll(time,unit) |
检查方法 | element() | peek() | 不可用 | 不可用 |