1. 进程(Process):
进程是系统中独立存在的实体,拥有自己独立的资源,拥有自己私有的地址空间。进程的实质,就是程序在多道程序系统中的一次执行过程,它是动态产生,动态消亡的,具有自己的生命周期和各种不同的状态。进程具有并发性,它可以同其他进程一起并发执行,按各自独立的、不可预知的速度向前推进。
描述进程的有一句话非常经典——进程是系统进行资源分配和调度的一个独立单位。
(注意,并发性(concurrency)和并行性(parallel)是不同的。并行指的是同一时刻,多个指令在多台处理器上同时运行。并发指的是同一时刻只能有一条指令执行,但多个进程指令被被快速轮换执行,看起来就好像多个指令同时执行一样。)
进程由 程序 、 数据 和 进程控制块 三部分组成。
2. 线程:
有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元,是CPU调度和分派的基本单位,可完成一个独立的顺序控制流程。
线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。
什么是多线程:如果在一个进程中同时运行了多个线程,用来完成不同的工作,则称之为“多线程”。多个线程交替占用CPU资源,而非真正的并行执行。
多线程的好处:
- 充分利用CPU的资源;
- 简化编程模型;
- 带来良好的用户体验
3. 在Java中创建线程的方式
一、继承java.lang.Thread类:编写简单,可直接操作线程,适用于单继承
- 定义MyThread类继承Thread类;
- 重写run()方法,编写线程执行体;
- 创建线程对象,调用start()方法启动线程
public class TestThread extends Thread {
@Override
public void run() { //run()方法中编写线程执行的代码
for(int i=0;i<10;i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
public static void main(String[] args) {
TestThread thread = new TestThread();
//thread.start():启动线程调用了父类的start方法
thread.start();
}
}
执行结果如下:
二、实现java.lang.Runnable接口(推荐):避免单继承局限性,便于共享资源
- 定义MyRunnable类实现Runnable接口;
- 实现run()方法,编写线程执行体;
- 创建线程对象,调用start()方法启动线程
public class TestThread implements Runnable {
@Override
public void run() { //run()方法中编写线程执行的代码
for(int i=0;i<10;i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
public static void main(String[] args) {
TestThread myRunnable = new TestThread();
Thread myThread = new Thread(myRunnable);//创建线程对象
myThread.start(); //启动线程
}
}
执行结果同上。
三、实现Callable接口
Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
4. 线程的状态:
线程共包括以下5种状态:
- 新建状态(New) : 线程对象被创建后,就进入了新建状态。此时它和其他Java对象一样,仅仅由Java虚拟机分配了内存,并初始化其成员变量值。
- 就绪状态(Runnable): 也被称为“可执行状态”。线程对象被调用了该对象的start()方法,该线程处于就绪状态。Java虚拟机会为其创建方法调用栈和程序计数器。处于就绪状态的线程,随时可能被CPU调度执行,取决于JVM中线程调度器的调度。
- 运行状态(Running) : 线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
- 阻塞状态(Blocked) : 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
> (01) 等待阻塞 -- 通过调用线程的wait()方法,让线程等待某工作的完成。
> (02) 同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
> (03) 其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
- 死亡状态(Dead) : 线程执行完了、因异常退出了run()方法或者直接调用该线程的stop()方法(容易导致死锁,现在已经不推荐使用),该线程结束生命周期。
线程调度 : 指按照特定机制为多个线程分配CPU的使用权
5. wait()、notify()、nofityAll()方法
在Object.java中,定义了wait(), notify()和notifyAll()等方法。
- wait():作用是让“当前线程”等待(会释放锁),而“当前线程”是指正在cpu上运行的线程!
- notify()和notifyAll()的作用,则是唤醒当前对象上的等待线程;notify()是唤醒单个线程,而notifyAll()是唤醒所有的线程。
Object类中关于等待/唤醒的API详细信息如下:
notify() : 唤醒在此对象监视器上等待的单个线程,使其进入“就绪状态”。 notifyAll(): 唤醒在此对象监视器上等待的所有线程,使其进入“就绪状态”。
wait() :让当前线程处于“等待(阻塞)状态”。
wait(long timeout) : 让当前线程处于“等待(阻塞)状态”。
wait(long timeout, int nanos) : 让当前线程处于“等待(阻塞)状态”。
6. yield()、sleep()、join()和interrupt()方法
- yield() :是Thread类的静态方法。它能让当前线程暂停,但不会阻塞该线程,而是由“运行状态”进入到“就绪状态”,从而让 其它具有相同优先级的等待线程获取执行权;但是,并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权;也有可能是 当前线程又进入到“运行状态”继续运行!
值得注意的是,yield()方法不会释放锁。 - sleep()是Thread类的静态方法。该方法声明抛出了InterrupedException异常。所以使用时,要么捕捉,要么声明抛出。sleep() 的作用是让当前线程休眠,即当前线程会从“运行状态”进入到“休眠(阻塞)状态”。sleep()会指定休眠时间,线程休眠的时间会大于/等于该休眠时间;在线程重新被唤醒时,它会由“阻塞状态”变成“就绪状态”,从而等待cpu的调度执行。常用来暂停程序的运行。
同时注意,sleep()方法不会释放锁。
sleep()有2种重载方式:
//让当前正在执行的线程暂停millis毫秒,并进入阻塞状态,
//该方法受到系统计时器和线程调度器的精度和准度的影响。
static void sleep(long millis) :
//让当前正在执行的线程暂停millis毫秒加nanos微秒,并进入阻塞状态,
//该方法受到系统计时器和线程调度器的精度和准度的影响。
static void sleep(long millis , int nanos) :
- join() 是Thread的一个实例方法。表示,当某个程序执行流中调用其他线程的join方法时,调用线程将被阻塞,直到被join的线程执行完毕.即当前线程内,用某个线程对象调用join()后,会使当前线程等待,直到该线程对象的线程运行完毕,原线程才会继续运行。
join()有3种重载的形式:
join(): 等待被join的线程执行完成;
join(long millis): 等待被join的线程的时间最长为millis毫秒,若在millis毫秒内,被join的线程还未执行结束,则不等待。
join(long millis , int nanos): 等待被join的线程的时间最长为millis毫秒加nanos微秒,若在此时间内,被join的线程还未执行结束,则不等待。
- interrupt(): interrupt()是Thread类的一个实例方法,用于中断本线程。这个方法被调用时,会立即将线程的中断标志设置为“true”。所以当中断处于“阻塞状态”的线程时,由于处于阻塞状态,中断标记会被设置为“false”,抛出一个 InterruptedException。所以我们在线程的循环外捕获这个异常,就可以退出线程了。
7. Synchronized关键字和Volatile关键字
Synchronized关键字 : 在java中,每一个对象有且仅有一个同步锁。这也意味着,同步锁是依赖于对象而存在。当当前线程调用某对象的synchronized方法时,就获取了该对象的同步锁。例如,synchronized(obj),当前线程就获取了“obj这个对象”的同步锁。
不同线程对同步锁的访问是互斥的。
实例锁 : 锁在某一个实例对象上。如果该类是单例,那么该锁也具有全局锁的概念。实例锁对应的就是synchronized关键字。
全局锁 : 该锁针对的是类,无论实例多少个对象,那么线程都共享该锁。全局锁对应的就是static synchronized(或者是锁在该类的class或者classloader对象上)。
就是说,一个非静态方法上的synchronized关键字,代表该方法依赖其所属对象。一个静态方法上synchronized关键字,代表该方法依赖这个类本身。
Volatile关键字 :
volatile关键字的作用主要有两个:
(1)多线程主要围绕可见性和原子性两个特性而展开,使用volatile关键字修饰的变量,保证了其在多线程之间的可见性,即每次读取到volatile变量,一定是最新的数据
(2)代码底层执行不像我们看到的高级语言—-Java程序这么简单,它的执行是Java代码–>字节码–>根据字节码执行对应的C/C++代码–>C/C++代码被编译成汇编语言–>和硬件电路交互,现实中,为了获取更好的性能JVM可能会对指令进行重排序,多线程下可能会出现一些意想不到的问题。使用volatile则会对禁止语义重排序,当然这也一定程度上降低了代码执行效率
从实践角度而言,volatile的一个重要作用就是和CAS结合,保证了原子性,详细的可以参见java.util.concurrent.atomic包下的类,比如AtomicInteger。
8. 线程的优先级和守护线程:
1、线程优先级
java中的线程优先级的范围是1~10,1最低,默认的优先级是5。优先级高的线程获得CPU资源的概率较大。Thread提供了setPriority(int newPriority)和getPriority()方法来设置和返回线程优先级。
Thread类有3个静态常量:
——MAX_PRIORITY = 10
——MIN_PRIORITY = 1
——NORM_PRIORITY = 5
2、守护线程
java 中有两种线程:用户线程和守护线程。可以通过isDaemon()方法来区别它们:如果返回false,则说明该线程是“用户线程”;否则就是“守护线程”。
用户线程一般用户执行用户级任务,而守护线程也就是“后台线程”,一般用来执行后台任务。需要注意的是:Java虚拟机在“用户线程”都结束后会后退出。
守护线程又称“后台线程”、“精灵线程”,它有一个特征——如果所有前台线程都死亡,后台线程自动死亡。
通过setDaemon(true)来设置一个线程。
9. 线程的休眠和礼让
线程的休眠:
- 让线程暂时睡眠:指定时长,线程进入阻塞状态;
- 睡眠时间过后线程会再进入可运行状态
public class TestThread {
public static void bySec(long s) {
for (int i = 0; i < s; i++) {
System.out.println(i + 1 + "秒");
try {
Thread.sleep(1000);//线程休眠1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
System.out.println("*****主线程开始休眠*****");
TestThread.bySec(5); //让主线程休眠5秒
System.out.println("*****主线程休眠结束*****");
}
}
测试结果如下:
线程的礼让:暂停当前线程,允许其他具有相同优先级的线程获得运行机会,该线程处于就绪状态,不转为阻塞状态。
【只是提供一种可能,但是不能保证一定会实现礼让】
public class TestThread implements Runnable {
public void run(){
for(int i=0;i<5;i++){
System.out.println(Thread.currentThread().getName()+"正在运行:"+i);
if(i==3){
System.out.print("线程礼让:");
Thread.yield();//当i=3时,当前线程礼让
}
}
}
public static void main(String[] args) {
TestThread testThread = new TestThread();
Thread t1 = new Thread(testThread,"线程A");
Thread t2 = new Thread(testThread,"线程2");
t1.start();
t2.start();
}
}
执行结果如下图:(结果不唯一)
10. AQS的介绍
AQS全称为Abstract Queued Sychronizer,翻译过来应该是 抽象队列同步器。
如果说java.util.concurrent的基础是CAS的话,那么AQS就是整个Java并发包的核心了,ReentrantLock、CountDownLatch、Semaphore等等都用到了它。AQS实际上以双向队列的形式连接所有的Entry,比方说ReentrantLock,所有等待的线程都被放在一个Entry中并连成双向队列,前面一个线程使用ReentrantLock好了,则双向队列实际上的第一个Entry开始运行。
AQS定义了对双向队列所有的操作,而只开放了tryLock和tryRelease方法给开发者使用,开发者可以根据自己的实现重写tryLock和tryRelease方法,以实现自己的并发功能。
11. 线程的同步机制
前提:如果我们创建的多个线程,存在着共享数据,那么就有可能出现线程的安全问题:当其中一个线程操作共享数据时,还未操作完成,另外的线程就参与进来,导致对共享数据的操作出现问题。
解决方式:要求一个线程操作共享数据时,只有当其完成操作完成共享数据,其它线程才有机会执行共享数据。