一、线程和进程
(1)进程
当一个程序进入内存,即可变成一个进程。
进程包含三大特性:
- 独立性
每个进程都有自己私有独立的内存地址,没有经过进程本身允许,一个用户进程是不可以访问其它进程的地址空间的 - 动态性
程序是一个静态指令集合,而进程是一个活动的指令集合,在进程中加入时间的概念,使得进程有了生命周期。 - 并发性
在一个处理器上可以并发运行多个进程
*实际上对于一个cpu而言在同一时刻,只有一个程序在执行,cup是在多个进程间轮换着执行的,cpu轮换速度很快,看着很像并发的运行。
(2)线程
线程可以看作一个轻量级的进程,线程是进程的最小执行单元。线程也是并发独立的执行流。
(3)线程的优势
- 进程之间不能共享内存,线程之间共享内存非常容易
- 系统创建进程时要重新为他分匹配资源,但是创建线程代价要小的多。
二、线程的创建和启动
(1)继承Thread类
- 继承Thread类并重写run方法
- 创建Thread子类的实例
- 调用线程对象的start方法
- Thread.currentThread可以获取当前执行的线程
- getName可以获取当前线程的名字
setName可以设置线程的名字
注意:使用继承Thread类的方法创建线程类时,多个线程之间无法共享线程类的实例变量
(2)实现Runnable接口创建线程
- 定义Runnable的实现类,并重写该接口的run方法
- 创建实现类的实例,并此实例作为Thread的target来创建线程对象
注意:使用Runnable方式创建可以多个线程间共享线程类的实例对象
(3)使用Callable和Future创建线程
- 先定义Callable的实现类
- 将Callable实现类的实例传入FutureTask的实例中
- 将FutureTask的实例作为target创建线程类
public class MyThreadTest {
public static void main(String[] args) {
FutureTask<Integer> task=new FutureTask<Integer>(new Callable<Integer>(){
public Integer call() {
int i=0;
for(;i<100;i++) {
System.out.println(Thread.currentThread().getName()+i);
}
return i;
}
});
new Thread(task).start();
for(int i=0;i<100;i++) {
System.out.println(Thread.currentThread().getName()+i);
}
try {
System.out.println("返回值"+task.get());
}catch(Exception e) {
e.printStackTrace();
}
}
}
(4)创建三种线程方式的对比
采用Runnable和Callable接口方式的优缺点
- 只是实现了接口还可以继承其它类
- 多个线程可以共享一个target,非常适合多个相同的线程处理同一份资源的情况
- 劣势是编程稍微复杂,访问当前线程必须使用Thread.currentThread的方式
三、线程的生命周期
- 新建
- 就绪
- 运行
- 阻塞
- 死亡
(1)新建和就绪状态
当使用new关键创建一个线程时,该线程就是新建状态,此时线程对象仅仅由java虚拟机分配内存初始化成员变量,没有表现出出任何动态特性,程序也不会执行执行体里的代码。
当线程对象调用start()方法以后,线程进入就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,处于此状态的线程并没有运行,而是可以运行了,何时运行取决于JVM中的线程调度器。
注意:只能对处于新建状态的线程类进行start方法
(2)运行和阻塞状态
如果处于就绪状态的线程获得了cpu,开始执行run方法里的执行体,此时线性为运行状态
发生如下情况时,线程进入阻塞状态
- 线程调用sleep方法,主动放弃所占用的管理器资源
- 线程调用了阻塞式的io方法,在该方法返回之前,该线程阻塞
- 线程企图获得一个同步管理器,但该同步管理器被其它线程所持有
- 线程在等待某个通知(notify)
- 程序调用了线程的suspend方法将线程挂起。(该方法容易造成死锁,避免使用该方法)
当前线程阻塞之后,其它线程可以获得执行的机会,被阻塞的线程会在合适的机会进入就绪状态
针对以上的几种情况,发生如下特定情况可以进入就绪状态: - sleep的时间到了
- 阻塞的io已经有了返回结果
- 线程成功的获得同步管理器资源
- 线程正在等待通知,其它线程发送了通知
-
处于挂起状态的线程调用了resume恢复方法
(3)线程死亡
线程有三种方式结束:
- 当run方法或者call方法执行完成
- 线程抛出Exception或者Error
- 或者调用Stop方法(容易造成死锁不推荐使用)
注意:子线程不受主线程的影响,子线程和主线程拥有相同的地位
可以调用isAlive方法检测该线程是否死亡
四、控制线程
(1)join线程
join方法是让一个线程等待另一个线程完成的方法。当在某个线程执行流当中调用其它线程的join方法,调用的线程会阻塞起来,直到被join方法join进来的线程执行完毕。
join方法通常由使用线程的程序调用,可以将大问题划分成小问题,每个小问题都分配一个线程。当所有小问题执行完毕以后,再调用主线程进行下一步。
public class JoinTest {
public static void main(String[] args) {
Runnable runnable=new Runnable() {
public void run() {
for(int i=0;i<100;i++) {
System.out.println(Thread.currentThread().getName()+i);
}
}
};
for(int j=0;j<100;j++) {
System.out.println(Thread.currentThread().getName()+j);
if(j==20) {
Thread tr=new Thread(runnable);
tr.start();
try {
tr.join();
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
}
}
}
join有三个重载的方法:
join();等待被join的线程执行完成
join(long millis);如果再指定秒数内被join的线程还没执行完成则不再等待
join(long millis,int nanos);等待被join的线程millis毫秒加nanos微毫秒(很少使用)
(2)后台线程
有一种线程,它在后台运行,它的任务是为其它线程提供服务,被称为后台线程。
特征:如果所有前台线程都死亡,后台线程自动死亡
调用Thread对象的setDaemon(true)方法可以将一个线程设置成后台进程。
注意:setDaemon方法必须在start方法之前调用
public class ThreadDemo1 extends Thread{
public void run() {
for(int i=0;i<200;i++) {
System.out.println(this.getName()+i);
}
}
public static void main(String[] args) {
ThreadDemo1 demo=new ThreadDemo1();
demo.setDaemon(true);
demo.start();
for(int j=0;j<20;j++) {
System.out.println(Thread.currentThread().getName()+j);
}
}
}
Thread还提供了isDaemon()方法判断是否为后台进程
(3)线程睡眠:sleep
如果需要让线程暂停一段时间并进入阻塞状态,则可以通过调用Thread类的静态方法sleep实现
public class SleepTest {
public static void main(String[] args) {
for(int i=0;i<=100;i++) {
System.out.println(Thread.currentThread().getName()+i);
if(i==20) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
}
}
}
此外还有一个方法与sleep类似,yield方法,它可以让正在执行的线程暂停,但是不会进入阻塞状态,而是直接进入就绪状态。yield只会让线程管理器重新调度一次,很有可能线程调用yield方法暂停之后,马上又被线程调度器调度出来运行。当调用了yield方法之后只有优先级相同或者更高才有可能获得运行的机会
两者的区别:
- sleep暂停之后会给其他线程机会,不用在乎优先级,而yield只会给优先级相同或更高的机会
- sleep会让线程进入阻塞状态,而yield会让线程转到就绪状态
- sleep方法抛出了InterruptedException,而yield没有抛出异常
- sleep比yield有更好的移植性,通常不建议使用yield控制并发线程
(4)改变线程优先级
每个线程都有优先级,优先级高的线程得到的执行的机会就多。
每个线程都默认与创建它的父线程一样,在默认的情况下,main线程具有普通优先级。
Thread类提供了setPriority(int newPriority)设置和返回线程的优先级,其中传进去的参数可以是1到10之间的整数,也可以是Thread下的三个静态常量。
- MAX_PRIORITY:10
- MIN_PRIORITY:1
- NORM_PRIORITY:5
五、线程同步
当并发线程共同访问一个数据时,有可能出现问题,出现并发安全问题
(1)同步代码块
Java多线程引入了同步监视器,使用同步监视器的通用方法就是同步代码块。语法如下:
synchronized(obj){
//此处的代码就是同步代码块
}
synchronized括号里面的就是同步监视器,线程开始执行前必须获得同步监视器的锁定。Java虽然可以使用任何对象作为同步监视器,但是推荐使用有可能并发访问的共享资源作为同步监视器
(2)同步方法
与同步代码块对应,Java多线程还提供了同步方法,使用方法就是用synchronized关键字修饰方法,同步方法的同步监视器就是this
线程安全的特征有如下几个特征:
- 该类的对象可以被多个线程安全的访问
- 每个线程调用该对象任何方法都得到正确的结果
- 每个线程调用该对象任何方法之后,该对象依然保持合理的状态
注意:synchronized可以修饰代码块可以修饰方法,但是不可以修饰构造器和成员变量
可变类的线程安全是牺牲程序的运行效率的
解决方案:
不要对可变类的所有方法都进行同步,只对改变竞争资源的方法同步
可以写两个,一个安全的,一个不安全的,单线程情况下用不安全的,多线程时使用安全的
六、释放同步监视器的锁定
程序无法显式的释放同步监视器,如下情况会释放:
- 同步的方法或代码块执行完毕或者遇到return,break关键字
- 同步的方法代码块内抛出异常或者Error
- 在执行同步代码块或方法时,执行了同步监视器的wait方法,当前线程暂停,并释放同步监视器
七、同步锁
Lock是控制多个线程对共享资源的进行访问的工具。Java提供了ReentrantLock、ReetrantReadWriteLock、StampedLock。一般常用的就是ReetrantLock。
使用方法:
private final ReetrantLock lock=new ReetrantLock();
lock.lock();
try{
//执行的同步代码
}finally{
lock.unLock();
}
八、死锁
当两个线程互相等待对方释放同步监视器时就会发生死锁,典型的哲学家就餐问题。
九、线程通信
(1)传统的线程通信
通过使用Object的wait(),notify(),notifyAll()方法来控制,这三个方法只能由同步监视器来调用。当使用同步方法时,同步监视器就是this,可以直接调用。当同步代码块时,只能由Synchronized括号里面的同步监视器调用
(2)使用Condition控制线程通信
当使用Lock锁时,没有同步监视器,也就不能使用wait,notify,notifyAll方法,所以使用Condition对象来进行通信,可以通过Lock对象的newCondition方法获得
Condition提供了三个方法:
await:类似于隐式同步监视器上的wait方法,导致当前线程等待,直到其它线程调用该Condition的signal或者signalAll方法唤醒
signal:唤醒次Lock对象上等待的单个线程。如果所有线程都在改Lock上等待,则会选择唤醒其中一个线程。
signalAll:唤醒在此Lock对象上等待的所有线程。
(3)使用阻塞队列控制线程通信
BlockingQueue接口,它不是作为容器使用的,而是作为线程同步的工具。它具有一个特征:当生产者线程想往里放入元素时,如果队列已满,则该线程被阻塞,如果消费者想要取出元素时,如果队列已空则线程阻塞。
分别设有put(E e)方法和take()方法
十、线程池
java5以后提供了一个Executor工厂来生产线程池,该工厂类包含几个静态的方法来创建线程池:
- newCachedThreadpool():创建一个具有缓存功能的线程池,系统根据需求创建线程,这些线程将缓存在线程池中
- newFixedThreadPool(int nThread):创建一个可重用,固定数量线程的线程想池
- newSingleThreadExecutor():创建一个只有单线程的线程池
- newScheduledThreadPool(int nThread):创建一个固定数量线程的线程池,它可以在指定延迟时间后执行
- newSingleThreadScheduledExecutor():创建一个单线程的线程池,它可以在指定延迟以后执行
- ExecutorServicenewWorkStealing(int parallelism)创建持有足够的线程的线程池来支持给定的并行级别,该方法还会使用多个队列来减少竞争
- ExecutorServicenewWorkStealingpool()该方法是前一个方法的简化的版本,如果有四个cpu则目标并行级别设为4
十一、ThreadLocal类
ThreadLocal是线程局部变量的意思,它可以为每一个线程提供一个变量值的副本使每一个线程都可以独立改变副本,不会和其他线程冲突。
ThreadLocal提供了三个方法:
T get():返回此线程局部变量副本的值
void remove():删除此线程局部变量中当前的值
void set(T value):设置此线程局部变量中当前的副本值
网友评论