一个线程是一个程序内部的顺序控制流
线程和进程
1.每个进程都有独立的代码和数据空间(进程上下文),进程切换的开销大。
2.线程:轻量的进程,同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(pc)
线程切换的开销小。
多进程:在操作系统中,能同时运行多个任务(程序)
多线程:在同一应用程序中,有多个顺序流同时执行
线程的概念模型
1.虚拟的cpu,封装在java.lang.Thread类中
2.cpu所执行的代码,传递给Thread类
3.cpu所处理的数据,传递给Thread类
线程体
1.java的线程是通过java.lang.Thread类来实现的
2.每个线程都是通过特定Thread对象的方法run()来完成其操作的,
方法run()称为线程体
构造线程的两种方法
1.定义一个线程类,他继承类Thread并重写其中的方法run();
2.提供一个实现接口Runnable的类作为线程的目标对象,在初始化一个Thread类
或者Thread子类的线程对象时,把目标对象传递给这个线程实例,由该目标对象提供线程体run()
Thread类
1.直接继承了object类,并实现了Runnable接口。位于java.lang包中
2.封装了线程对象需要的属性和方法
继承Thread类--创建多线程的方法之一
1.从Thread类派生一个子类,并创建子类的对象
2.子类应该重写Thread类的run方法,写入需要在线程中执行的语句段
3.调用start方法来启动新线程,自动进入run方法
修改:延长主线程
如果启动新线程后希望主线程多持续一会再结束,可在start语句后加速让当前线程休眠的语句
thread.start();
try{ Thread.sleep(time);}
catch(Exception e){};
线程休眠的原因就是让其他线程得到执行机会
通过Runnable接口构造线程
Runnable接口
1.只有一个run()方法
2.Thread类实现了Runnable接口
3.便于多个线程共享资源
4.Java不支持多继承,如果已经继承了某个基类,便需要实现Runnable接口来生成多线程
5.以实现Runnable的对象为参数建立新的线程
6.start方法启动线程就会运行run()方法
两种线程构造方式的比较
1.使用Runnable接口
可以将cpu,代码和数据分开,形成清晰的模型;还可以从其他类继承
2.直接继承Thread类
编写简单,直接继承,重写run方法,不能再从其他类继承
线程内部的数据共享
1.用同一个实现了Runnable接口的对象作为参数创建多个线程
2.多个线程共享同一个对象中的相同数据
用一个Runnable类型对象创建的3个新线程,这三个线程就共享了这个对象的私有成员
独立且同时运行的线程有时需要共享一些数据并且考虑到彼此的状态和动作
多线程的同步控制
1.有时线程之间彼此不独立,需要同步
1)线程间的互斥
同时运行的几个线程需要共享一些数据
共享的数据,在某一时刻只允许一个线程对其进行操作
2)“生产者/消费者”问题
假设由一个线程负责往数据区写数据,另一个线程从同一数据区中读数据,两个线程可以并行执行
如果数据区已满,生产者等消费者取走一些数据后才能再写
当数据区空时,消费者要等生产者写入一些数据后再取
2.线程同步
1)互斥:许多线程在同一个数据上操作而互不干扰,在同一时刻只能有一个线程
访问该共享数据。因此有些方法或程序段在同一时刻只能被一个线程执行,称之为监视区
2)协作:多个线程可以有条件地同时操作共享数据。执行监视区代码的线程在条件满足的情况
下可以允许其他线程进入监视区
synchronized--线程同步关键字,实现互斥
1.用于指定需要同步的代码段或方法,也就是监视区
2.可实现与一个锁的交互。例如
synchronized(对象){代码段}
3.synchronized的功能是:首先判断对象的锁是否在,如果在就获得锁,然后就可以执行紧随其后的代码段;
如果对象的锁不在(已被其他线程拿走),就进入等待状态,直到获得锁
4.当被synchronized限定的代码段执行完,就释放锁
java使用监视器机制
1.每个对象只有一个“锁”,利用多线程对“锁”的争夺实现线程间的互斥
2.当线程A获得了一个对象的锁后,线程B必须等待线程A完成规定的操作,并释放出锁后,
才能获得该对象的锁,并执行线程B中的操作
同步与锁的要点:
1.只能同步方法,而不能同步变量;
2.每个对象只有一个锁;当提到同步时,应该清楚在什么上同步?也就是说,在那个对象上同步?
3.类可以同时拥有同步和非同步方法,非同步方法可以被多个线程自由访问而不受锁的限制。
4.如果两个线程使用相同的实例来调用的synchronized方法,那么一次只能有一个线程执行方法,另一个需要等待锁
5.线程休眠时,它所持的任何锁都不会释放
6.线程可以获得多个锁。比如,在一个对象的同步方法里面调用另外一个对象的同步方法,则获取了两个对象的同步锁
7.同步损害并发性,应该尽可能缩小同步范围。
8.在使用同步代码块时候,应该指定在那个对象上同步,也就是说要获取那个对象的锁
线程的等待--wait()方法
1.为了更有效的协调不同线程的工作,需要在线程间建立沟通渠道,通过线程间的“对话”
来解决线程间的同步问题
2.java.lang.Object类的一些方法为线程间的通讯提供了有效手段-wait(0
线程的唤醒--notify()和notifyAll()方法
1.notify()随机唤醒一个等待的线程,本线程继续执行
1)线程被唤醒以后,还要等发出唤醒消息者释放监视器,这期间关键数据人可能被改变
2)被唤醒的线程开始执行时,一定要判断当前状态是否适合自己运行
2.notifyAll()唤醒所有等待的线程,本线程继续执行
public synchronized void put(){
if (available) //如果还有存票待售,则存票线程等待
try {
wait();
}catch (Exception e){}
System.out.println("生产票数"+(++number));
available = true;
notify(); //存票后唤醒售票线程开始售票
}
public synchronized void sell(){
if (!available) //如果没有存票,则售票线程等待
try {
wait();
}catch (Exception e){}
if (available == true&&i<=number)
System.out.println("卖出票数"+(++i));
notify();//售票后唤醒存票线程开始存票
if (number==size)
number=size+1;//在售完最后一张票后,设置一个结束标志,number>size表示售票结束
}
}
后台线程
1.也叫守护线程,通常是为了辅助其他线程而运行的线程
2.他不妨碍程序终止
3.一个进程中只要还有一个前台线程在运行,这个进程就不会结束;如果
一个进程中的所有前台线程都已经结束,那么无论是否还有未结束的后台线程,这个进程都会结束
4.“垃圾回收”便是一个后台进程
5.如果对某个线程对象在启动(调用start方法)之前调用了setDaemon(true)方法,这个线程就变成了后台线程
线程的生命周期
1.线程从产生到消亡的过程
2.一个线程在任何时刻都处于某种线程状态(thread state)
线程的几种基本状态
阻塞(进入对象lock池)<-- notify
synchronized未获得锁 --> wait() 阻塞(进入对象wait池)
新线程-->就绪状态-->运行状态 --> run完成()死亡状态
线程调度 --> sleep() 阻塞(休眠)
<-- 时间到
1.诞生状态:线程刚刚被创建
2.就绪状态:1)线程的start方法已被执行
2)线程已准备好运行
3.运行状态:处理机分配给了线程,线程正在运行
4.阻塞状态(Blocked):
1)在线程发出输入/输出请求且必须等待其返回
2)遇到用synchronized标记的方法而未获得锁
3)为等候一个条件变量,线程调用wait()方法
5.休眠状态(Sleeping):执行sleep方法而进入休眠
6.死亡状态:线程已完成或退出
线程有 6 种状态:
NEW:初始化状态、RUNNABLE:可运行/运行状态、BLOCKED:阻塞状态、WAITING:无时限等待状态、TIMED_WAITING:有时限等待状态和 TERMINATED:终止状态。
而线程池的状态有以下 5 种:
RUNNING:运行状态,线程池创建好之后就会进入此状态,如果不手动调用关闭方法,那么线程池在整个程序运行期间都是此状态。
SHUTDOWN:关闭状态,不再接受新任务提交,但是会将已保存在任务队列中的任务处理完。
STOP:停止状态,不再接受新任务提交,并且会中断当前正在执行的任务、放弃任务队列中已有的任务。
TIDYING:整理状态,所有的任务都执行完毕后(也包括任务队列中的任务执行完),当前线程池中的活动线程数降为 0 时的状态。到此状态之后,会调用线程池的 terminated() 方法。
TERMINATED:销毁状态,当执行完线程池的 terminated() 方法之后就会变为此状态。
死锁
1.线程在运行过程中,其中某个步骤往往需要满足一些条件才能继续下去,如果这个条件不能满足
线程将在这个步骤上出现阻塞
2.线程A可能会陷于对线程B的等待,而线程B同样陷于对线程C的等待,以此类推,
整个等待链最后又可能回到线程A。如此一来便陷入一个彼此等待的轮回中,任何线程都
动弹不得,此即所谓死锁(deadlock)
3.对于死锁问题,关键不在于出现问题后调试,而是在于预防
结束线程的生命
1.通常,可通过控制run方法中循环条件的方式来结束一个线程
2.用stop方法可以结束线程的生命
但如果一个线程正在操作共享数据段,操作过程没有完成就用stop结束的话,将会导致数据的
不完整,因此并不提倡用此方式
线程调度
1.在单cpu的系统中,多个线程需要共享cpu,在任何时间点上实际只能有一个线程在运行
2.控制多个线程在同一个cpu上以某种顺序运行称为线程调度
3.java虚拟机支持一种非常简单的,确定的调度算法,叫做固定优先级算法。
这个算法基于线程的优先级对其进行调度
线程的优先级
1.每个java线程都有一个优先级,其范围都在1和10之间。默认情况下,每个线程的优先级都设置为5
2.在线程A运行过程中创建的新的线程对象B,初始状态具有和线程A相同的优先级
3.如果A是个后台线程,则B也是个后台线程
4.可在线程创建之后的任何时候,通过setPriority(int priority)方法改变其原来的优先级
基于线程优先级的线程调度
1.具有较高优先级的线程比优先级低的线程优先执行
2.对具有相同优先级的线程,Java处理是随机的
3.底层操作系统支持的优先级可能要少于10个,这样会造成一些混乱。最后的控制可以通过明智地使用yield()函数来完成
4.我们只能基于效率的考虑来使用线程优先级,而不能依靠线程优先级来保证算法的正确性
假设某线程正在运行,则只有出现以下情况之一,才会使其暂停运行
1.一个具有更高优先级的线程变为就绪状态(ready)
2.由于输入/输出(或其他一些原因),调用sleep,wait,yield方法使其发生阻塞
3.对于支持时间分片的系统,时间片的时间期满
线程安全
实现java的线程安全
1.不可变
2.绝对线程安全
3.相对线程安全
4.线程兼容和对立
不可变
1.final修饰:public final a=100
2.java.lang.String s =“sttring”;
3.枚举类型:public enum Color {RED,GREEN,BLANK,YELLOW}
4.java.lang.Number的子类如Long,Double
5.Biglnteger,BigDecimal(数值类型的高精度实现)
绝对线程安全
1.满足Brian Goetz在---中的定义的线程为绝对线程安全的
2.java API中标注自己是线程安全的类绝大部分不是绝对线程安全的
如:java.util.Vector
相对线程安全
1.通常意义上的线程安全,需要保证这个对象单独操作是线程安全的,调用的时候
不需要做额外的保证措施,但是对于一些特定顺序的连续调用,就需要在
调用的时候同步手段保证调用的正确性
2.如:Vector,HashTable
线程兼容和线程对立
1.线程兼容:对象本身不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象
在并发环境中可以安全使用
2.线程对立:无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码
3.java中线程对立的例子:Thread类的suspend()和resume()方法可能导致死锁。
所以JDK已对其声明废弃
线程安全的实现方式
互斥同步
非阻塞同步
无同步方案
互斥同步
1.同步的互斥实现方式:临界区,互斥量,信号量
2.synchronized关键字:经过编译后,会在同步块前后形成
monitorenter和monitorexit两个字节码
1)synchronized同步块对自己是可重入的,不会将自己锁死;
2)同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入
3.重入锁ReentrantLock(java.util.concurrent)
4.相比采用synchronized,重入锁可实现:等待可中断,公平锁,锁可以绑定多个条件
5.synchronized表现为原生语法层面的互斥锁,而RenentrantLock表现为API层面的互斥锁
阻塞同步:互斥同步存在的问题是进行线程阻塞和唤醒所带来的性能问题,这种同步称为阻塞同步。
这是一种悲观并发策略
非阻塞同步:不同于悲观并发策略,而是使用基于冲突检测的乐观并发策略
这种策略不需要把线程挂起
1.使用硬件处理器指令进行不断重试策略(jdk1.5以后)
1)测试并设置(Test-and-Set)
2)获取并增加(Fetch-and-Increment)
3)交换(Swap)
4)比较并交换(Compare-and-Swap,简称CAS)
5)加载链接,条件存储(Load-Linked,Store-conditional,简称LL,SC)
例:java实现类AtomicInteger,AtomicDouble等等
无同步方案
1.可重入代码:也叫纯代码。相对线程安全来说,可以保证线程安全
2.线程本地存储:
锁优化(jdk6)
自旋锁
自适应锁
锁消除:jvm即时编译器在运行时,对一些代码上要求同步,
但是被检测到不可能存在共享数据竞争的锁进行消除
锁粗化
偏向锁