多线程(第一篇--基本介绍)

概述

  • 程序:
    是为了完成特定任务,用某种语言编写的一组指令的合集.就是一段"静态的代码"静态对象

  • 进程:
    是程序的一次执行过程,或是"正在运行的一个进程".动态过程:有他自身的产生,存在和消亡的过程."程序是静态的,进程是动态的!

  • 线程:
    进程可以进一步细化为线程,是一个程序内部的一条执行路径,若一个程序可同一时间执行多个线程就是多线程的. 例如:一个程序可以通知,执行多个命令

  • 总结:
    1. 一个程序至少有一个进程
    2. 一个进程至少有一个线程

优缺点

  • 缺点:
    因为线程之间切换需要相应时间所以,会降低程序的执行效率,系统资源的消耗
  • 优点:
    1. 提高程序的使用体验: 可同一时间做多种事情,
    2. 提高CPU使用率: 因为是多个线程同时运行那么CPU的利用率就高整体增加程序的运行速度;
    3. 改善程序结构: 可以将即长又复杂的程序拆分我多个线程,独立运行,便于理解,和修改;

常用PAI

java.long.Thread类:

  1. start();
    启动Thread类下的子线程命令;
  2. currentTread()
    静态的调用当前的线程
  3. getName():
    获取次线程的名字
  4. setName():
    设置此线程的名字
  5. yield():
    释放调用此方法线程的CPU的执行权
  6. join():
    在A线程中调用B线程的此方法,表示A线程停止执行,直到B线程运行结束才会继续运行;
  7. isAlive():
    判断此线程是否还存活
  8. sleep:
    显示的让该线程休眠一秒
  9. wait()
    暂停当前线程
  10. notify()
    恢复暂停的线程;

注意

  1. 通过对象调用 thread类new出的对象调用Start方法()后启动run方法与直接调用通过调用run方法是是不同的;
  2. 同一线程只能开起一次.否者会报非法线程状态;

守护线程

  1. 守护线程会随着主线程的死亡,而死亡.如gc线程
  2. 也可以手动设置线程为守护线程,通过 线程名.setDaemon(true)来将该线程设置为守护线程

Runnable接口创建与Thruad的区别

  1. 继承Thread:
    1. 优势在于可以扩展Thread类(添加特有方法,属性);
    2. 缺点在处理共享资源比较麻烦
  2. 实现Runnable:
    1. 无须继承Thread,更加灵活(java只能单继承,可多实现)
    2. 在处理共享资源比较简单;
多线程使用方法:
  • 继承的方式:
    1. 创建一个类继承thread类
    2. 这个子类应该重写thread类的方法run
    3. 在run方法内写需要实现的代码;
    4. 在main方法内创建继承了thread类的对象
    5. 通过该对象调用thread下的start方法
      注意:
      1. 如果不启动start,直接通过对象调用run方法的话那么就是对象调用方法,该方法还是在子线程内运行
      2. 此方法可以扩展类的方法;
  • 实现方式:
    1. 创建一个类实现Runnable类
    2. 从写run方法
    3. new出该类的实现体
    4. 将new出的实体传递给Thread(实现体,名字)的构造器;
    5. 通过Thread构造出的实体对象来调用其方法;
    "此方法更加的灵活,适合操作同一资源,和有多继承需求的线程
两种方式对比

实现类的更好:

  1. 避免了java单继承的局限性
  2. 操作同一资源,只需要通过接口new出的对象作为一个公共参数传递给Thread构造器即可
    "所有new出的对象都来自于同一个类所
  3. 避免Thread单继承的局限性!使得程序更加的灵活!

线程安全问题

    当多个线程同时共享,同一个全局变量或静态变量,做写的操作时,可能会
    发生数据冲突问题(比如:同时售卖100张票可能会出现超卖现象),也就是线程
    安全问题。但是做读操作是不会发生数据冲突问题

线程安全:

  1. 如何解决多线程之间线程安全问题?
    使用多线程之间同步synchronized或使用锁(lock)。
  2. 为什么使用线程同步或使用锁能解决线程安全问题呢?
    将可能会发生数据冲突问题(线程不安全问题),只能让当前一个线程进行执行。代码执行完成后释放锁,让后才能让其他线程进行执行。这样的话就可以解决线程不安全问题。
  3. 什么是多线程之间同步?
    当多个线程共享同一个资源,不会受到其他线程的干扰。
  4. 什么是多线程同步?
    当多个线程共享同一个资源,不会受到其他线程的干扰。

同步

内置的锁
Java提供了一种内置的锁机制来支持原子性
每一个Java对象都可以用作一个实现同步的锁,称为内置锁,线程进入同步代码块之前自动获取到锁,代码块执行完成正常退出或代码块中抛出异常退出时会释放掉锁
内置锁为互斥锁,即线程A获取到锁后,线程B阻塞直到线程A释放锁,线程B才能获取到同一个锁

  • 锁的使用
    内置锁使用synchronized关键字实现,synchronized关键字有两种用法:
    1. 修饰需要进行同步的方法(所有访问状态变量的方法都必须进行同步),此时充当锁的对象为调用同步方法的对象
    2. 同步代码块和直接使用synchronized修饰需要同步的方法是一样的,但是锁的粒度可以更细,并且充当锁的对象不一定是this,也可以是其它对象,所以使用起来更加灵活
      ** 同步代码块:**
 Synchronized():
     同步代码块:
          Synchronized(锁对象){
             这里的代码一次只能允许一个线程对象代码块执行;     
          }
1. 锁对象必须唯一,可以是任意类型对象; 
2. 可以是this(当前对象但是他必须是唯一的)
3. 可以是当前类的class对象(.class)(最常用)!
    **总结:**
4. 同步代码块会让一个线程结束之后再让另一个线程运行
5. 等同于单线程,除非需要否者不要使用同步代码块

同步方法:

//在方法上修饰synchronized 称为同步方法
public synchronized void sale() {
		if (trainCount > 0) {
			System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - trainCount + 1) + "张票");
			trainCount--;
		}
	}
	/*同步方法是否有锁对象?
		1. 有
		2. 普通方法默认this
		3. 静态方法对象默认是 当前类.class; 	
	证明方式: 
		一个线程使用同步代码块(this明锁),另一个线程使用同步函数。
		如果两个线程抢票不能实现同步,那么会出现数据错误。*/ 

死锁问题:

  1. 同步中嵌套同步,导致锁无法释放,会产生死锁问题
  2. 产生原因:
    • T1 : 先获取到A锁,再获取B锁
    • T2 : 先获取到B锁,在获取A锁
    • 这种情情况下就会产生一个死循环,T1与T2各自拿着对方下一步所需要资源,无法释放当前的锁程序无法继续运行.
      注意: 同步中嵌套同步,导致锁无法释放,会产生死锁问题

volatile与synchronized的区别:
什么是Volatile:可见性也就是说一旦某个线程修改了该被volatile修饰的变量,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,可以立即获取修改之后的值。

  1. synchronized: 保证内存的可见性和操作的原子性
  2. volatile: 只能保证内存的可可见性不需要加锁
  3. 被volatile:修饰的变量不会被重排序多线程如果某些变量需要按照固定的顺序不希望被重排序的时候必须要使用volatile关键字进行修饰
  4. volatile: 是变量修饰符只能用于变量

多线程生命周期

在这里插入图片描述

ThreadLocal

ThreadLocal提高一个线程的局部变量,访问某个线程拥有自己局部变量。
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
ThreadLocal类接口只有4个方法:

  • void set(Object value)设置当前线程的线程局部变量的值。
  • public Object get()该方法返回当前线程所对应的线程局部变量。
  • public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
  • protected Object initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。

ThreadLoca实现原理
ThreadLoca通过map集合Map.put(“当前线程”,值);

java内存模型

  • 概述
      由于计算机的存储设备与处理器的运算能力之间有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中没这样处理器就无需等待缓慢的内存读写了。
      基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是引入了一个新的问题:缓存一致性(Cache Coherence)。在多处理器系统中,每个处理器都有自己的高速缓存,而他们又共享同一主存,如下图所示:多个处理器运算任务都涉及同一块主存,需要一种协议可以保障数据的一致性,这类协议有MSI、MESI、MOSI及Dragon Protocol等。Java虚拟机内存模型中定义的内存访问操作与硬件的缓存访问操作是具有可比性的,后续将介绍Java内存模型。
      除此之外,为了使得处理器内部的运算单元能竟可能被充分利用,处理器可能会对输入代码进行乱起执行(Out-Of-Order Execution)优化,处理器会在计算之后将对乱序执行的代码进行结果重组,保证结果准确性。与处理器的乱序执行优化类似,Java虚拟机的即时编译器中也有类似的指令重排序(Instruction Recorder)优化。
    在这里插入图片描述
    Java内存模型

定义Java内存模型并不是一件容易的事情,这个模型必须定义得足够严谨,才能让Java的并发操作不会产生歧义;但是,也必须得足够宽松,使得虚拟机的实现能有足够的自由空间去利用硬件的各种特性(寄存器、高速缓存等)来获取更好的执行速度。经过长时间的验证和修补,在JDK1.5发布后,Java内存模型就已经成熟和完善起来了。

主内存与工作内存

Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样底层细节。此处的变量与Java编程时所说的变量不一样,指包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,后者是线程私有的,不会被共享。

Java内存模型中规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存(可以与前面将的处理器的高速缓存类比),线程的工作内存中保存了该线程使用到的变量到主内存副本拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要在主内存来完成,线程、主内存和工作内存的交互关系如下图所示,和上图很类似。

这里的主内存、工作内存与Java内存区域的Java堆、栈、方法区不是同一层次内存划分。

内存间交互操作

关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来完成:

  1. lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
  2. unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  3. read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
  4. load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  5. use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
  6. assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  7. store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
  8. write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。

如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行read和load操作,如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。Java内存模型只要求上述操作必须按顺序执行 ,而没有保证必须是连续执行也就是read和load之间,store和write之间是可以插入其他指令的,如对主内存中的变量a、b进行访问时,可能的顺序是read a,read b,load b, load a。Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则:

  • 不允许read和load、store和write操作之一单独出现
  • 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
  • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
  • 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
  • 一个变量在同一时刻只允许一条线程对其进行lock操作,lock和unlock必须成对出现
    -> 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
  • 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
  • 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。

重排序:

在执行程序时为了提高性能,编译器和处理器经常会对指令进行重排序。重排序分成三种类型:

编译器优化的重排序。编译器在不改变单线程程序语义放入前提下,可以重新安排语句的执行顺序。
指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
内存系统的重排序。由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
从Java源代码到最终实际执行的指令序列,会经过下面三种重排序:

为了保证内存的可见性,Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。Java内存模型把内存屏障分为LoadLoad、LoadStore、StoreLoad和StoreStore四种:
在这里插入图片描述
→→Java内存模型原文←←

wait、notify的区别

1.因为涉及到对象锁,他们必须都放在synchronized中来使用. Wait、Notify一定要在synchronized里面进行使用。
2.Wait必须暂定当前正在执行的线程,并释放资源锁,让其他线程可以有机会运行
3. notify/notifyall: 唤醒因锁池中的线程,使之运行

注意:一定要在线程同步中使用,并且是同一个锁的资源

wait与sleep区别

  1. 对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。
  2. sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。
  3. 在调用sleep()方法的过程中,线程不会释放对象锁。
    而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备
  4. 获取对象锁进入运行状态。
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值