线程和进程
进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位,每个进程都有独立的代码和数据空间(进程上下文),进程切换的开销大。
线程:轻量的进程,同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换的开销小。一个线程是一个程序内部的顺序控制流。
多进程:同时运行多个任务(程序)。
多线程:在同一应用程序中有多个顺序流同时执行。
线程的概念模型
●虚拟的CPU,封装在 java. lang. Thread类中。
●CPU所执行的代码,传递给 Thread类。
●CPU所处理的数据,传递给 Thread类。
●Java的线程是通过 java. lang. Thread类来实现的。、
●每个线程都是通过某个特定 Thread对象的方法run()来完成其操作的方法run()称为线程体。
构造线程的三种方法
定义一个线程类,它继承类 Thread并重写其中的方法run();
提供一个实现接口 Runnable的类作为线程的目标对象,在初始化;
通过Callable和Future创建线程。
一个 Thread类或者 Thread子类的线程对象时,把目标对象传递给这个线程实例,由该目标对象提供线程体run()。
main线程已经执行完后,新线程才执行完,main方法调用 thread. start()方法启动新线程后并不等待其run方法返回就继续运行,线程的run方法在一边独自运行,不影响原来的main方法的运行。
Runnable接口
只有一个run()方法,Thread类实现了 Runnable接囗,便于多个线程共享资源.
□Java不支持多继承,如果已经继承了某个基类,便需要
现 Runnable接口来生成多线程
口以实现 Runnable的对象为参数建立新的线程
口sta方法启动线程就会运行run(方法
多线程的同步控制
●有时线程之间彼此不独立、需要同步
口线程间的互斥
同时运行的几个线程需要共享一个(些)数据
共享的数据,在某一时刻只允许一个线程对其进行操作
“生产者/消费者”问题
假设有一个线程负责往数据区写数据,另一个线程从同一数据
区中读数据,两个线程可以并行执行
如果数据区已满,生产者要等消费者取走一些数据后才能再写
当数据区空时,消费者要等生产者写入一些数据后再取
●线程同步
口互斥:许多线程在同一个共享数据上操作而互不干扰,同一时刻
只能有一个线程访问该共享数据。因此有些方法或程序段在同
时刻只能被一个线程执行,称之为监视区
口协作:多个线程可以有条件地同时操作共享数据。执行监视区代
码的线程在条件满足的情况下可以允许其它线程进入监视区
synchronized-一线程同步关键字,实现互斥
口用于指定需要同步的代码段或方法,也就是监视区
口可实现与一个锁的交互。例如
synchronized(对象)(代码段
口 synchronized的功能是:首先判断对象的锁是否在,如果在就获得锁
然后就可以执行紧随其后的代码段;如果对象的锁不在(已被其他
线程拿走),就进入等待状态,直到获得锁
口当被 synchronized限定的代码段执行完,就释放锁
●后台线程
口也叫守护线程,通常是为了辅助其它线程而运行的线程
口它不妨碍程序终止
口一个进程中只要还有一个前台线程在运行,这个进程就不会结束;如
果一个进程中的所有前台线程都已经结束,那么无论是否还有未结束
的后台线程,这个进程都会结束
口“垃圾回收”便是一个后台线程
口如果对某个线程对象在启动(调用stat方法)之前调用了
setDaemon(true方法,这个线程就变成了后台线程
sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,给执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。 wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。
诞生状态
口线程刚刚被创建
就绪状态
口线程的 start方法已被执行
口线程已准备好运行
运行状态
口处理机分配给了线程,线程正在运行
阻塞状态( Blocked)
口在线程发出输入/输出请求且必须等待其返回
口遇到用 synchronized标记的方法而未获得锁
口为等候一个条件变量,线程调用wat(方法
●休眠状态( Sleeping)
口执行seep方法而进入休眠
死亡状态
口线程已完成或退出
线程调度
口在单CPU的系统中,多个线程需要共享CPU,在任何时间点
上实际只能有一个线程在运行
口控制多个线程在同一个CPU上以某种顺序运行称为线程调度
Java虚拟机支持一种非常简单的、确定的调度算法,叫做固
定优先级算法。这个算法基于线程的优先级对其进行调度
考虑这些线程在运行时环境下的调度和交替执行,也
不需要进行额外的同步,或者在调用方进行任何其他
的协调操作,调用这个对象的行为都可以获得正确的
结果,那这个对象是线程安全的。
Java线程安全 互斥同步、非阻塞同步、无同步方案
互斥同步
●同步的互斥实现方式:临界区( Critical Section),互斥量
( Mutex),信号量( Semaphore)
● Synchronized关键字:经过编译后,会在同步块前后形成
monitorenter和 monitorexit两个字节码。
口(1) synchronized同步块对自己是可重入的,不会将自己锁死;
口(2)同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入
采用 synchronized,重入锁可实现:等待可中断、公平锁、锁
可以绑定多个条件
Synchronized表现为原生语法层面的互斥锁,而 RenentrantLock表
现为API层面的互斥锁
●阻塞同步:互斥同步存在的问题是进行线程阻塞和唤醒所带来的性
能问题,这种同步称为阻塞同步( Blocking Synchronization)。这是
种悲观并发策略
●非阻塞同步:不同于悲观并发策略,而是使用基于冲突检测的乐观
并发策略,就是先进行操作,如果没有其他线程征用共享数据,则
操作成功;否则就是产生了冲突,采取不断重试直到成功为止的策
种策略不需要把线程挂起,称为非阻塞同步
●使用硬件处理器指令进行不断重试策略(DK15以后
口测试并设置( Test-and-set)
口获取并增加( Fetch- and-Increment)
口交换(Swap)
口比较并交换( Compare-and-Swap,简称CAS)
口加载链接,条件存储( Load-Linked, Store-conditional简称LLSC)
例:java实现类 AtomicInteger, AtomicDouble等等。
●可重入代码:也叫纯代码。相对线程安全来说,可以保证线程安全。
可以在代码执行过程中断它,转而去执行另一段代码,而在控制权
返回后,原来的程序不会出现任何错误。
●线程本地存储:如果一段代码中所需要的数据必须与其他代码共享,
那就看看这些共享数据的代码是否能保证在同一个线程中执行,如
果能保证,就可以把共享数据的可见范围限定在同一个线程之内,
这样无需同步也能保证线程之间不出现数据争用问题。
锁优化(源自」DK6)
●自旋锁
●自适应锁
●锁消除
●锁粗化
偏向锁
●互斥同步存在的问题:挂起线程和恢复线程都需要转入内核态中完
成,这些操作给系统的并发性能带来很大的压力
●自旋锁:如果物理机器有一个以上的处理器能让两个或以上的线程
同时并行执行,那就可以让后面请求锁的那个线程“稍等一会”,
但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放
锁。为了让线程等待,我们只需要让线程执行一个忙循环(自旋)
这项技术就是自旋锁。Java中自旋次数默认10次
自适应自旋
●自适应意味着锁自旋的时间不再固定,而是由前一次在同一个锁
上的自旋时间及锁拥有者的状态来决定。如果在同一个锁对象上,
自旋等待刚刚成功获得锁,并且持有锁的线程正在运行中,那么
虚拟机就会认为这次自旋也很有可能再次成功,进而它允许自旋
等待相对更长的一段时间。
锁消除
●定义:JVM即时编译器在运行时,对一些代码上要求同步,但是
被检测到不可能存在共享数据竞争的锁进行消除
●判定依据:如果判断在一段代码中,堆上的所有数据都不会逃逸
出去从而被其他线程访问到,那就可以把它们当作栈上数据对待,
认为他们是线程私有的,同步加锁自然无需进行。
锁粗化
通常我们的代码总是将同步块的作用范围限制得尽量小,只在共享数
据的实际作用域中才进行同步,这样是为了使得同步操作的数量尽可
能变小
●另一种情况是,如果一系列的连续操作都对同一个对象反复加锁,甚至加
锁操作是出现在循环体中,那即使没有线程争用,频繁的进行互斥同步也
会导致不必要的性能损耗,此时只需要将同步块范围扩大即可。即:锁粗
化
偏向锁
●目的:消除数据无竟争情况下的同步原语,进一步提高程序运行的性
能。偏向锁就是在无竟争的情况下把整个同步都消除掉,连CAS操作
都不做
●偏向:意思是这个锁会偏向于第一个获得它的线程,如果在接下来的
执行中,该锁没有被其他线程获取,则持有偏向所得线程永远不需要
再进行同步
网友评论