1、线程的基本概念
程序:是一段可执行的静态代码
进程:程序的一次动态加载过程(将程序加载到内存时,此时程序就转换为了进程)
线程: 进程可进一步细化为线程,是一个程序内部的一条执行路径,线程不能独立存在,必须依附于某个进程
一个Java应用程序,至少有三个线程:
main线程,垃圾回收线程,异常处理线程
多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈
2、使用多线程的优点
优点在于充分利用了CPU的空闲时间片,用尽可能少的时间来对用户的要求做出响应,使得进程的整体运行效率得到较大提高,同时增强了应用程序的灵活性。
如果是单线程,那同时只能处理一个用户请求,多线程则可以同时处理多个用户的请求
3、上下文切换
上下文切换是指:一个工作的线程被另外一个线程暂停,另外一个线程占用处理器开始执行任务
CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程是同时执行的。
4、多线程的创建方式
1.继承Thread类
实现步骤:
- 创建一个继承Thread类的子类
- 重写Thread类的
run()
将线程需要执行的操作声明在run()
中 - 创建Thread类子类的对象
- 通过此对象调用
start()
start()作用:启动当前线程 调用当前线程的run
2.实现Runnable接口
实现步骤:
- 创建一个实现了Runnable接口的类
- 实现类去实现Runnable中的抽象方法:
run()
- 创建实现类的对象
- 将此对象作为参数传递到Thread类的构造器,创建Thread对象
//Thread类的构造器需要接收一个Runnable对象
public Thread(Runnable target)
通过Thread类的对象调用start()
以上两种方式的对比
- 实现接口没有单继承的局限性
- Thread类需要使用static来修饰共享数据,而实现Runnable接口就是将实现类当作参数传递给Thread构造器,run中数据天然就是共享数据
3.实现Callable接口
与实现Runnable接口相比,实现Callable接口功能更强大 我们需要重写call方法,call方法可以抛出异常
实现步骤:
- 创建一个实现Callable的实现类。
- 实现call()方法,将此线程需要执行的操作声明在call()中。
- 创建Callable接口实现类的对象。
- 将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象。
- 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()方法。
4.线程池
优点:线程池,其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作
使用线程池中线程对象的步骤:
- 创建线程池对象
- 创建Runnable接口子类对象
- 提交Runnable接口子类对象
- 关闭线程池
线程池方法:
- Executors:线程池创建工厂类
- public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象
- ExecutorService:线程池类
- Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行
public class ThreadPool implements Runnable { @Override public void run() { System.out.println("获取一个线程"); } } public static void main(String[] args) { //创建线程池对象 ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象 //创建Runnable实例对象 ThreadPool t = new ThreadPool(); //从线程池中获取线程对象,然后调用ThreadPool中的run() service.submit(t); //再获取个线程对象,调用run() service.submit(t); //注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。将使用完的线程又归还到了线程池中 //关闭线程池 service.shutdown(); }
5、线程的生命周期
- 新建 :new Thread对象此时线程就是新建状态
- 就绪 :新建的线程执行start方法就是就绪状态
- 运行 :就绪的线程获取cpu执行权就是运行状态
- 阻塞 :运行的线程遇到sleep wait 等待同步锁等情况变为阻塞状态
- 死亡 :运行的线程执行完run 或者遇到stop 异常就变成死亡状态
6、线程同步
首先引入锁的概念:当某个方法或者代码块使用锁时,那么在同一时刻至多仅有有一个线程在执行该段代码
1.同步代码块
1.1实现Runnable接口
synchronized(锁){//任何一个类的对象 都可以充当锁
//需要被同步的代码 操作共享数据的方法
}
private int ticket=100;
@Override
public void run() {
while (true) {
synchronized (this) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "票号为" + ticket);
ticket--;
} else {
break;
}
}
}
}
多个线程必须要共用一把锁
由于是使用同一个runnable对象实例化出来的Thread对象,此时用this当前对象充当锁
1.2继承Thread类
继承与实现接口的方法基本一致,只需要确保锁是同一个锁即可
锁设置为static或者用当前类来充当锁
2.同步方法
操作共享数据的方法可以设置为同步方法
public synchronized void show(){
//操作共享数据的代码
}
非静态的同步方法 同步监视器是this
静态的同步方法 同步监视器是当前类本身
3.新增方式:Lock锁
lock中的方法
locke()方法: 加锁
unlock()方法: 释放锁
private ReentrantLock lock=new ReentrantLock();
//上锁
lock.lock();
//释放锁
lock.unlock();
synchronized和lock不同之处:
synchronized机制在执行完同步代码之后,自动释放锁
lock需要手动启动锁,手动关闭锁
7、死锁问题
所谓死锁是指多个进程因竞争资源而相互等待,若无外力作用,这些进程都无法向前推进
死锁产生的四个必要条件
- 互斥条件:资源一次只允许一个进程访问
- 不剥夺条件:已被占用的资源只能由属主释放,不允许被其它进程剥夺
- 请求和保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
- 循环等待条件:存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源
如何解决死锁问题:
破坏死锁产生的四个必要条件中的一个或多个
- 破坏互斥:允许多个进程同时访问资源
- 破坏不剥夺:必须释放以保持的资源
- 破坏请求和保持:一次性分配所有资源
- 破坏循环等待:定义资源类型的线性顺序来预防
8、线程通信
- wait():一旦执行此方法,当前线程进入阻塞状态,并释放锁
- notify():一旦执行此方法,就会唤醒一个wait的线程, wait方法释放锁,notify方法不释放锁
- notifyAll(): 会唤醒所有被wait的线程
- wait(),notify(),notifyAll()只能在同步代码块或者同步方法中执行
以上三个方法都是Object中的方法
sleep和wait方法的不同之处:
(1)方法声明的位置不同,sleep是Thread类中的方法,wait是Object类中的方法
(2)wait只能使用在同步代码块或者同步方法中
(3)wait会释放锁,而sleep不会释放锁
9、思考与提问
1.Thread类中的start()和run()方法有什么区别?
start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程。
2.产生死锁的条件
1.互斥条件:一个资源每次只能被一个进程使用。 2.请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。 3.不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。 4.循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
3.你有哪些多线程开发良好的实践?
- 给线程命名
- 最小化同步范围
- 优先使用volatile
- 尽可能使用更高层次的并发工具而非wait和notify()来实现线程通信,如BlockingQueue,Semeaphore
- 优先使用并发容器而非同步容器.
- 考虑使用线程池
4.创建高铁卖票,总票数为100张, 如何把票同步且安全的卖出去?
Java 解决方案:同步机制
同步机制总结:对于并发工作,你需要某种方式来防止两个任务访问相同的资源(其实就是共享资源竞争)。防止这种冲突的方法就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须锁定这项资源,使其他仼务在其被解锁之前,就无法访问它了,而在其被解锁之时,另一个任务就可以锁定并使用它了。
5.前端的如何实现多线程?
Web worker(多线程编程)。JavaScript程序运行在主线程之外的另外一个线程中。将一些任务分配给后者运行。在主线程运行的同时,Worker(子)线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。应用场景:例如处理ajax返回的大批量数据,读取用户上传文件,计算MD5,更改canvas的位图的过滤,分析视频和声频文件等。