1. 线程与进程
进程
进程是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间。
正运行中的应用程序叫进程,每个进程运行时,都有自已的地址空间(内存空间)
线程
线程是轻量级的进程,是进程中一个负责程序执行的控制单元,线程没有独立的地址空间(内存空间),线程是由进程创建的(寄生在进程中),一个进程可以拥有多个线程,至少一个线程。
线程是进程的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行,一个进程最少有一个线程。
线程实际上是进程的进一步划分,一个进程启动后可以产生多个线程去完成功能。
2. 并行与并发、同步与异步
并行与并发
并行:指两个或多个事物在同一时刻发生(同时发生)。
并发:指两个或多个事件在同一个时间段内发生。
同步与异步
同步:排队执行,效率低但是安全。
异步:同时执行,效率高但是数据不安全。
3. 线程的调度以及生命周期
1. 分时调度
- 所有线程轮流使用CPU来处理任务,将CPU的使用时间平均分配给每个线程
2. 抢占式调度
优先让优先级高的线程使用CPU来处理任务,若优先级相同会随机选择一个(线程随机性),Java使用的为抢占式调度。
CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核心而言,某个时刻,只能执行一个线程,而CPU在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。
3. 线程的优先级
计算机只有一个CPU,各个线程轮流获得CPU的使用权,才能执行任务;优先级比较高的线程会获得更多的使用CPU的机会,反之亦然;优先级有10个等级:1~10,一般情况下线程默认优先级是5,也可以通过setPriority和getPriority方法来设置或返回优先级。
注意:要想设置优先级,必须在线程启动前设置好它的优先级别。
getPriority() :返回线程优先级
setPriority(int newPriority) :改变线程的优先级,线程创建时继承父线程的优先级
4. 线程的生命周期
线程从创建(new)到死亡(dead)称为一个线程的生命周期。
线程在生命周期中有五种状态。
新建(New):新创建了一个线程对象。
就绪(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于“可运行线程池”中,变得可运行,只等待获取CPU的使用权。即在就绪状态的进程除CPU之外,其它的运行所需资源都已全部获得。
运行(Running):就绪状态的线程获取了CPU,执行任务。
阻塞(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
阻塞状态分为三种:
等待阻塞:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒,
同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中。
其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
- 死亡(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
4. 多线程的优缺点
多线程能解决什么问题:解决资源浪费和分段一起执行;开启多个线程是为了同时运行多部分代码,每个线程都有自已的运行的内容,这个内容可以称线程要执行的任务(放在run()方法中)
多线程的优点:
多线程最大的好处在于可以同时并发执行多个任务。
多线程可以最大限度地减低CPU的闲置时间,从而提高CPU的利用率。
多线程的缺点:
线程也是程序,所以线程需要占用内存,线程越多占用内存也越多。
多线程需要协调和管理,所以需要CPU时间跟踪线程。
线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题。
线程太多会导致控制太复杂,最终可能造成很多Bug。
5. 线程的创建与启动方式
- 线程分为用户线程与守护线程;用户线程是当一个进程不包含任何存活的用户线程时,进行结束;守护线程是用于守护用户线程的线程,当最后一个用户线程结束时,所有守护线程自动死亡。将一个线程设置为守护线程需在线程启动前调用setDeamon方法,并传递布尔型变量true即可将线程设置为守护线程
1. 继承Thread类创建线程
- 定义子类继承Thread类。
- 子类中重写Thread类中的run方法。
- 创建Thread子类对象,即创建了线程对象。
- 调用线程对象start方法:启动线程,调用run方法
//先创建一个类,继承Thread类创建线程,重写Thread类中的run方法 //在run方法中写创建新线程要完成的任务 public class 线程练习1 { public static void main(String[] args) { /* * Demo d1 = new Demo();// 继承thread实例 * d1.start();// 开启线程,执行 其实是进入到进程队列 */ Demo d1 = new Demo(); d1.setName("c11111");//使用setName方法给线程起名 d1.start(); Demo d2 = new Demo(); d2.setName("c22222"); d2.start(); Demo d3 = new Demo(); d3.setName("c33333"); d3.start(); } } class Demo extends Thread { /* * int a = 0; * * public void run() { // 在run方法中写创建新线程要完成的任务 * for (int i = 0; a < 10000; i++) System.out.println(a++); * } */ /* * static int a=0; * int count=1; * public void run() { // 在run方法中写创建新线程要完成的任务 * for(int i = 0; a < 10000; i++) { * int a1=a+1; * a=a1; * System.out.println(this.getName()+"\t线程"+a+"\t争抢过"+count+++"次"); * } *} * 发生资源争抢,不断写入陈旧数据,要避免这种情况,引入资源锁,一个资源在同一时间只能由一个线程调用 } } */ // 有资源锁 static int a = 0; int count = 1; Object o = new Object(); public void run() { // 在run方法中写创建新线程要完成的任务 for (int i = 0; a < 10000; i++) { synchronized (o) { int a1 = a + 1; a = a1; System.out.println(this.getName() + "\t线程" + a + "\t争抢过" + count++ + "次"); } } } }
2. 实现Runnable接口创建线程
- 定义子类,实现Runnable接口。
- 子类中重写Runnable接口中的run方法。
- 通过Thread类含参构造器创建线程对象。
- 将Runnable接口的子类对象作为实际参数传递给Thread类的构造方法中。
- 调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法。
//先创建一个类,实现Runnable类创建线程,重写Runnable类中的run方法 //在run方法中写创建新线程要完成的任务 public class 线程练习2 { public static void main(String[] args) { Demo2 d1 = new Demo2(); Demo2 d2 = new Demo2(); Thread t1 = new Thread(d1, "1线程"); Thread t2 = new Thread(d2, "2线程"); t1.start(); t2.start(); } } class Demo2 implements Runnable { @Override public void run() { // TODO Auto-generated method stub for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName()+"输出的:"+i); } } }
3. 使用Callable和Future创建线程
- 定义子类,实现Callable接口。
- 子类中重写Callable接口中的call方法。
- 创建FutureTask类对象包装Callable线程任务
- 通过Thread类含参构造器创建线程对象。
- 将FutureTask的对象作为实际参数传递给Thread类的构造方法中。
- 调用Thread类的start方法:开启线程,调用Callable子类接口的call方法。
public class demo3 { public static void main(String[] args) { Demo2 d1 = new Demo2();// 创建myCallable对象 FutureTask<Integer> ft1 = new FutureTask<>(d1);// 使用FutureTask来包装myCallable对象 new Thread(ft1,"1线程").start();// FutureTask对象作为Thread对象的target创建新的线程 for(int i = 0;i < 100;i++)System.out.println(Thread.currentThread().getName()+"线程 "+i); try { System.out.println("1线程的返回值:"+ft1.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } //跟前两种方法比较,这个方法有返回值,要注意返回值类型 static class Demo2 implements Callable { @Override public Object call() throws Exception { int i = 0; for(;i<100;i++) { System.out.println(Thread.currentThread().getName()+" "+i); } return i; } } }
4. 使用线程池例如用Executor框架创建线程
系统启动一个新线程的成本是比较高的,因为它涉及与os交互。这种情况下,系统启动时即创建大量空闲的线程,就可以很好地提高性能,尤其是当程序需要创建大量生存期很短暂的线程时。
除此之外,使用线程池可以有效地控制系统中并发线程的数量。避免因并发创建的线程过多,导致系统性能下降,JVM崩溃。
Java 5以前,需要手动创建自己的线程池;Java 5开始,新增了Executors工厂类产生线程池。
使用线程池执行线程任务的步骤如下:
1.调用Executors 类的静态方法newFixedThreadPool(int nThreads),创建一个可重用的、具有固定线程数的线程池ExecutorService对象
2.创建Runnable实例,作为线程执行任务
3.调用ExecutorService对象的submit()提交Runnable实例
4.调用ExecutorService对象的shutDown()方法关闭线程池。
1. 优点
降低资源消耗
提高响应速度
提高线程的可管理性
2. 几种线程池
- 缓冲线程池
/** * 缓存线程池. * (长度无限制) * 执行流程: * 1\. 判断线程池是否存在空闲线程 * 2\. 存在则使用 * 3\. 不存在,则创建线程 并放入线程池, 然后使用 */ ExecutorService service = Executors.newCachedThreadPool(); //向线程池中 加入 新的任务 service.execute(new Runnable() { @Override public void run() { System.out.println("线程的名称:"+Thread.currentThread().getName()); } }); service.execute(new Runnable() { @Override public void run() { System.out.println("线程的名称:"+Thread.currentThread().getName()); } }); service.execute(new Runnable() { @Override public void run() { System.out.println("线程的名称:"+Thread.currentThread().getName()); } });
- 定长线程池
/** * 定长线程池. * (长度是指定的数值) * 执行流程: * 1\. 判断线程池是否存在空闲线程 * 2\. 存在则使用 * 3\. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用 * 4\. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程 */ ExecutorService service = Executors.newFixedThreadPool(2); service.execute(new Runnable() { @Override public void run() { System.out.println("线程的名称:"+Thread.currentThread().getName()); } }); service.execute(new Runnable() { @Override public void run() { System.out.println("线程的名称:"+Thread.currentThread().getName()); } });
- 单线程线程池
效果与定长线程池 创建时传入数值1 效果一致. /** * 单线程线程池. * 执行流程: * 1\. 判断线程池 的那个线程 是否空闲 * 2\. 空闲则使用 * 4\. 不空闲,则等待 池中的单个线程空闲后 使用 */ ExecutorService service = Executors.newSingleThreadExecutor(); service.execute(new Runnable() { @Override public void run() { System.out.println("线程的名称:"+Thread.currentThread().getName()); } }); service.execute(new Runnable() { @Override public void run() { System.out.println("线程的名称:"+Thread.currentThread().getName()); } });
- 周期性任务定长线程池
public static void main(String[] args) { /** * 周期任务 定长线程池. * 执行流程: * 1\. 判断线程池是否存在空闲线程 * 2\. 存在则使用 * 3\. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用 * 4\. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程 * * 周期性任务执行时: * 定时执行, 当某个时机触发时, 自动执行某任务 . */ ScheduledExecutorService service = Executors.newScheduledThreadPool(2); /** * 定时执行 * 参数1\. runnable类型的任务 * 参数2\. 时长数字 * 参数3\. 时长数字的单位 */ /*service.schedule(new Runnable() { @Override public void run() { System.out.println("俩人相视一笑~ 嘿嘿嘿"); } },5,TimeUnit.SECONDS); */ /** * 周期执行 * 参数1\. runnable类型的任务 * 参数2\. 时长数字(延迟执行的时长) * 参数3\. 周期时长(每次执行的间隔时间) * 参数4\. 时长数字的单位 */ service.scheduleAtFixedRate(new Runnable() { @Override public void run() { System.out.println("俩人相视一笑~ 嘿嘿嘿"); } },5,2,TimeUnit.SECONDS); }
5. 线程的停止
使用stop方法强制让线程停止,会造成数据丢失或其他的逻辑错误,可以使用isInterrupted方法是否标记为关闭,如果标记为关闭,当前线程就调节为停止状态或开始做停止操作返回值Boolean型,这个方法不会停止线程,它是给线程一个停止信号,然后需要配合判断语句来进行线程的停止。
也可以通过使用interrupted方法给线程一个中断标记,然后在相应的catch代码块中释放相应的资源最后return出去将这个线程结束。
5. 创建线程的三种方式(使用线程池除外)的对比
1. 采用实现Runnable、Callable接口的方式创建多线程
优点
- 线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
- 在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
缺点
编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。
2. 使用继承Thread类的方式创建多线程
优点
编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
缺点
线程类已经继承了Thread类,所以不能再继承其他父类。
3. Runnable和Callable的区别
- Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。
- Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
- call方法可以抛出异常,run方法不可以。
- 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
- Callalble接口支持返回执行结果,需要调用FutureTask.get()得到,此方***阻塞主进程的继续往下执行,如果不调用不会阻塞。
4. 实现接口与继承类创建线程有哪些优势
- 通过创建任务,然后给线程分配任务的方式实现多线程,更适合多个线程同时执行任务的情况。
- 可以避免单继承所带来的局限性。
- 任务与线程是分离的,提高了程序的健壮性。
- 后期学习的线程池技术,接受Runnable类型的任务,不接受Thread类型的线程。
6. 多线程会出现安全问题
多个线程在对相同对象运行任务时,因为CPU的使用权是线程之间争抢的,所以会出现数据的脏读。例如卖票系统,多个线程对票数对象进行出售操作,如果不加控制会出现线程1正在卖票,正好他的CPU使用权时间到了,接着线程2将票都卖完了线程1才抢回CPU的使用权,此时线程1会进行上次未完成的任务继续完成,也就是说会将已经卖出去的票再卖一次。
怎样处理安全问题
加锁处理:对资源加上锁,使多个线程排队访问资源
- 隐式锁synchronized
- 同步代码块:
synchronized(锁对象){}
- 同步代码块:
锁对象必须是多个线程看一个锁对象,若每个线程看自己的锁也会出现安全隐患,因为每个线程看的都不是一个锁,所以这个锁不会让其他线程看到,就会出现安全问题。因此需要上锁的对象必须是所有线程看一个锁对象,这样才不会出现安全问题
- 同步方法: 将synchronized关键字修饰在方法上,这样每次调用这个方法就会排队进行。
需要注意的是同步方法的锁对象是当前对象,如果这个方法时静态方法,那么这个锁对象是类型.class,若同步代码块和同步方法使用锁对象的是相同的对象,那么只能等待这两个锁着相同对象的代码块和方法都执行完毕时其他线程才能对其进行操作
- 显式锁Lock
使用Lock的子类ReentrantLock创建显式锁,显式锁需要自己锁(通过调用lock方法),自己开锁(通过调用unlock方法)
- 公平锁与不公平锁
公平锁:先来先进行(使用显式锁Lock,并传入参数true)
不公平锁:谁抢到谁进行
- 锁的释放
- 当前线程的同步方法、同步代码块执行结束。
- 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。
- 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。
- 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。
- 死锁
当两个线程被阻塞,每个线程在等待另一个线程时就发生死锁,也就是说不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
解决方法:让线程持有独立的资源,尽量不采用嵌套的synchronized语句,要设计出良好的算法来避免死锁的发生
7. 线程的通信
- 睡眠sleep
对象.sleep(时间单位毫秒);睡眠sleep是Thread类的静态方法,后面必须指定时间,到点就醒,如果有资源锁的情况下,它会占用资源锁一起睡眠,到点醒时,完成锁内任务后回到争夺资源的队列。
- 休眠(等待)wait
对象.wait(时间单位毫秒);休眠wait是object类的final方法,后面可以指定时间,也可以不指定时间,但是必须用notify唤醒,要不然这个线程会一直休眠,但是wait不会占用资源锁,别的线程会抢夺资源锁进行别的操作。
- 唤醒notify、notifyAll
对象.notify();唤醒notify是object类的final方法,用来唤醒正在排队等待同步资源的线程中优先级最高者结束等待,让其继续工作。
对象.notifyAll();notifyAll方法是object类的final方法,用来唤醒正在排队等待资源的所有线程结束等待,让其继续工作。
注意:Java.lang.Object提供的这三个方法只有在synchronized方法或synchronized代码块中才能使用,否则会报java.lang.IllegalMonitorStateException异常