多线程的一些概念
什么是并发:
并发就是指程序同时处理多个任务的能力。
并发编程的根源在于对多任务情况下对访问资源的有效控制。
程序、进程与线程
程序:是静态的概念,比如qq、微信、网易云音乐都是程序的一种。
进程:是动态的概念,是程序在运行的状态,进程说明程序在内存中的边界。
线程:线程是进程内的一个“基本任务”,每个线程都有自己的功能,是CPU分配与调度的基本单位。
并发:并发是指两个或多个事件在同一时间间隔发生。
并行:并行是指两个或者多个事件在同一时刻发生。
同步和异步
同步:同步是指一个进程在执行某个请求的时候,如果该请求需要一段时间才能返回信息,那么这个进程会一直等待下去,直到收到返回信息才继续执行下去。
临界区
临界区用来表示一种公共资源与共享数据,可以被多个线程使用。 同一时间只能有一个线程访问临界区(阻塞状态), 其他资源必须等待。
死锁、饥饿、活锁
死锁:大家对于一个公共资源进行彼此争夺,又不愿意释放自己资源的时候,那么就形成了死锁
饥饿:线程本身来说是有优先级的,一些线程一直无法充分的获取到资源,就属于饥饿状态,如果程序中发现这种情况,我们需要给予重点的关注和一些补齐
活锁:当某一个线程调度不太智能的时候,就会出现资源处于闲置状态,谁都不愿意占用的情况
上述三种状态,无论哪种状态产生都会出现系统阻塞塞车的情况,一定要有效的避免他,一个最有效的解决死锁,线程资源争抢的事情就是,为每一个线程分配一个属于自己的不被别人影响的资源,这是最理想的做法
线程安全
线程安全:
优点:可靠
缺点:执行速度慢
使用建议:需要线程共享时使用
线程不安全:
优点:速度快
缺点:可能与预期不符
使用建议:在线程内部使用,无需线程间共享
线程安全三大特性
原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何 因素打断,要么就都不执行。i = i + 1
可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
有序性:如果在本线程内观察,所有的操作都是有序的;如果在一个线程观 察另一个线程,所有的操作都是无序的
创建多线程的三种方式
Java 中创建线程三种方式
1、继承Thread类创建线程
2、实现Runnable接口创建线程
public class Match1 { public static void main(String[] args) { Runner liuxiang = new Runner(); //创建一个新的线程 liuxiang.setName("刘翔"); //设置线程名称 Runner feibing = new Runner(); feibing.setName("飞冰"); Runner op = new Runner(); op.setName("路飞"); liuxiang.start(); //启动线程 feibing.start(); op.start(); } } class Runner extends Thread{ public void run(){ Integer speed = new Random().nextInt(100); for (int i = 1; i <= 100; i++){ try { Thread.sleep(1000); //当前线程休眠1秒 } catch (InterruptedException e) { e.printStackTrace(); } //this.getName()打印当前线程的名字 System.out.println(this.getName() + "已前进" + (i * speed) + "米(" + speed + "米/秒)"); } } }
public class Match2 { public static void main(String[] args) { Runner2 liuxiang = new Runner2(); Thread thread1 = new Thread(liuxiang); thread1.setName("刘翔"); Thread feibing = new Thread(new Runner2()); feibing.setName("飞冰"); thread1.start(); feibing.start(); } } class Runner2 implements Runnable{ public void run(){ Integer speed = new Random().nextInt(100); for (int i = 1; i <= 100; i++){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "已前进" + (i * speed) + "米(" + speed + "米/秒)"); } } }
public class Match3 { public static void main(String[] args) throws ExecutionException, InterruptedException { //创建一个线程池。里面天生有3个“空”线程。Executors是调度器,对线程池进行管理 ExecutorService executorService = Executors.newFixedThreadPool(3); Runner3 liuxiang = new Runner3();//实例化Callable对象 liuxiang.setName("刘翔"); Runner3 feibing = new Runner3(); feibing.setName("飞冰"); Runner3 op = new Runner3(); op.setName("路飞"); //将这个对象扔到线程池中,线程池自动分配一个线程来运行liuxiang这个对象的call方法 //Future用于接受线程内部call方法的返回值 Future<Integer> result1 = executorService.submit(liuxiang); Future<Integer> result2 = executorService.submit(feibing); Future<Integer> result3 = executorService.submit(op); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } executorService.shutdown();//关闭线程池释放所有资源 System.out.println("刘翔累计跑了" + result1.get() + "米" ); System.out.println("飞冰累计跑了" + result2.get() + "米" ); System.out.println("路飞累计跑了" + result3.get() + "米" ); } } class Runner3 implements Callable<Integer> { private String name ; public void setName(String name){ this.name = name; } //实现Callable接口可以允许我们的线程返回值或抛出异常 @Override public Integer call() throws Exception { Integer speed = new Random().nextInt(100); Integer distince = 0; //总共奔跑的距离 for(int i = 1 ; i <= 100 ; i++){ Thread.sleep(10); distince = i * speed; System.out.println(this.name + "已前进" + distince + "米(" + speed + "米/秒)"); } return distince; } }
创建线程的三种方式对比
继承Thread | 实现Runnable | 利用线程池 | |
---|---|---|---|
优点 | 编程简单,执行效率高 | 面向接口编程,执行效率高 | 容器管理线程,允许返回值与异常 |
缺点 | 单继承 | 无法对线程组有效控制,没有返回值、异常 | 执行效率相对低,编程麻烦 |
使用场景 | 不推荐使用 | 简单的多线程程序 | 企业级应用,推荐使用 |
线程的生命周期
Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下一些状态:
五种状态
1.新建
当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态。此时它和其他Java对象一样,仅仅由JVM为其分配了内存,并初始化了实例变量的值。此时的线程对象并没有任何线程的动态特征,程序也不会执行它的线程体run()。
2.就绪
但是当线程对象调用了start()方法之后,就不一样了,线程就从新建状态转为就绪状态。JVM会为其创建方法调用栈和程序计数器,当然,处于这个状态中的线程并没有开始运行,只是表示已具备了运行的条件,随时可以被调度。至于什么时候被调度,取决于JVM里线程调度器的调度。
注意:
程序只能对新建状态的线程调用start(),并且只能调用一次,如果对非新建状态的线程,如已启动的线程或已死亡的线程调用start()都会报错IllegalThreadStateException异常。
3.运行
如果处于就绪状态的线程获得了CPU资源时,开始执行run()方法的线程体代码,则该线程处于运行状态。如果计算机只有一个CPU核心,在任何时刻只有一个线程处于运行状态,如果计算机有多个核心,将会有多个线程并行(Parallel)执行。
当然,美好的时光总是短暂的,而且CPU讲究雨露均沾。对于抢占式策略的系统而言,系统会给每个可执行的线程一个小时间段来处理任务,当该时间用完,系统会剥夺该线程所占用的资源,让其回到就绪状态等待下一次被调度。此时其他线程将获得执行机会,而在选择下一个线程时,系统会适当考虑线程的优先级。
4.阻塞
当在运行过程中的线程遇到如下情况时,会让出 CPU 并临时中止自己的执行,进入阻塞状态:
- 线程调用了sleep()方法,主动放弃所占用的CPU资源;
- 线程试图获取一个同步监视器,但该同步监视器正被其他线程持有;
- 线程执行过程中,同步监视器调用了wait(),让它等待某个通知(notify);
- 线程执行过程中,同步监视器调用了wait(time)
- 线程执行过程中,遇到了其他线程对象的加塞(join);
- 线程被调用suspend方法被挂起(已过时,因为容易发生死锁);
当前正在执行的线程被阻塞后,其他线程就有机会执行了。针对如上情况,当发生如下情况时会解除阻塞,让该线程重新进入就绪状态,等待线程调度器再次调度它:
- 线程的sleep()时间到;
- 线程成功获得了同步监视器;
- 线程等到了通知(notify);
- 线程wait的时间到了
- 加塞的线程结束了;
- 被挂起的线程又被调用了resume恢复方法(已过时,因为容易发生死锁);
5.死亡
线程会以以下三种方式之一结束,结束后的线程就处于死亡状态:
- run()方法执行完成,线程正常结束
- 线程执行过程中抛出了一个未捕获的异常(Exception)或错误(Error)
- 直接调用该线程的stop()来结束该线程(已过时,因为容易发生死锁)