创建线程的方式三:实现Callable接口。(JDK 5.0新增'了解即可')
如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?
- call()可以有返回值的。
- call()可以抛出异常,被外面的操作捕获,获取异常的信息
- Callable是支持泛型的
//1、实现Callable接口
class NumThread implements Callable{//2、重写call方法
@Override
public Object call() throws Exception {
int sum=0;
for (int i = 0; i < 100; i++) {
if (i%2==0){
System.out.println(i);
sum+=i;
}
}
return sum;
}
}
public class ThreadNew {
public static void main(String[] args) {
//3、创建实现Callable接口的对象
NumThread numThread=new NumThread();
//4、将创建实现Callable接口的对象作为参数传到FutureTask类,并new一个对象
FutureTask futureTask=new FutureTask(numThread);
//5、FutureTask的对象作为thread类的参数
new Thread(futureTask).start();
try {
//6、获取方法返回值
Object o = futureTask.get();
System.out.println(o);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
创建线程的方式四:使用线程池
- 优点:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理
常用方法:*.setCorePoolSize(int number)
:核心池的大小*.setMaximumPoolSize()
:最大池大小*.setKeepAliveTime()
:线程没有任务时最多保持多长时间后会终止
- 面试题:创建多线程有几种方式?
答: 一共有三种方式创建多线程:
- 通过实现Runnable 接口;
- 通过继承Thread 类本身;
- 通过Callable 和Future 创建线程;
- 使用线程池创建多线程。
几种方式的优缺点:
- 采用继承Thread类方式:
(1)优点:编写简单,如果需要访问当前线程,无需使用Thread.currentThread()方法,直接使用this,即可获得当前线程
(2)缺点:因为线程类已经继承了Thread类,所以不能再继承其他的父类 - 采用实现Runnable接口方式:
(1)优点:线程类只是实现了Runable接口,还可以继承其他的类。在这种方式下,可以多个线程共享同一个目标对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU代码和数据分开,形成清晰的模型,较好的体现了面向对象的思想。
(2)缺点:编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法 - Runnable和Callable的区别:
(1)Callable规定的方法是call(),Runnable规定的方法是run()
(2)Callable的任务执行后可返回值,而Runnable的任务是不能返回值的
(3)call方法可以抛出异常,run方法不可以,因为run方法本身没有抛出异常,所以自定义的线程类在重写run的时候也无法抛出异常
(4)运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务的执行情况,可取消任务的执行,还可获取执行结果 - 使用线程池:
优点:
(1)降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
(2)提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
(3)提高线程的可管理性。线程是稀缺资源,使用线程池可以进行统一的分配、调优和监控。
缺点:
(1)启动较慢
(2)复杂性较高
死锁:
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃己需要的同步资源,就形成了线程的死锁.
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
解决办法:
- 使用专门的算法、原则。
- 尽量减少同步资源的定义
- 尽量避免嵌套同步
解决线程安全问题的几种方法:(解决多个线程同时操作同一个共有属性的问题)
概念:
- 悲观锁---总有有刁民想害朕,当前线程认为自己在使用数据的时候,一定有别的线程来修改数据,因此在获取数据的时候先加锁,确保数据不会被线程修改。(像单例模式的lazy)
- 乐观锁就是线程认为在自己使用数据时,不会有别的线程来修改数据,就不会加锁,只是在更新数据的时候去判断之前有没有别的线程来更新了数据。(像单例模式的crazy)
方法一:使用synchronized关键字,悲观锁,使用它的时需要一个唯一的,每个线程都可以访问的监听对象 通常就是当前类的对象,此关键字不会造成死锁,使用synchronized可以拿来修饰类,静态方法,普通方法和代码块。
- 必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全
- 一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方法共用同一把锁(this),同步代码块(指定需谨慎)
同步代码块: synchronized (对象){
// 需要被同步的代码;
- synchronized还可以放在方法声明中,表示整个方法为同步方法。
public synchronized void show (String name){ ......
}
示例代码:
public synchronized void products(){ if (productCount<20){
productCount++;
notify();//通信方法,用来唤醒其他线程
}else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
} } }
方法二:使用lock锁
class Window implements Runnable{private int ticket = 100;
private ReentrantLock lock = new ReentrantLock(); //1.实例化ReentrantLock
public void run() {
while(true){
try{
lock.lock();//2.调用锁定方法lock()
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
ticket--;
}else{
break;
}
}finally {
//3.调用解锁方法:unlock()
lock.unlock();
}} }}
- 面试题:synchronized 与 Lock的异同?
相同:二者都可以解决线程安全问题
不同:synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器
Lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock()) - 优先使用顺序:
Lock 同步代码块(已经进入了方法体,分配了相应资源) 同步方法(在方法体之外)