一、程序、进程、多任务、线程、多线程及其对比
1、程序(Program),是含有指令和数据的文件,被存储在磁盘或其他数据存储设备中,是静态的代码。
2、进程(Process),是程序的一次执行过程,是系统运行程序的基本单位,是操作系统结构的基础。因此,进程是动态的。每个进程之间是独立的,除非利用某些通讯管道来进行通信,或是通过操作系统产生交互作用,否则基本上各进程不知道彼此的存在。
3、多任务(Multi task)是指在一个系统中可以运行多个进程,即有多个独立的运行的任务,每一个任务对应一个进程。
4、线程(Thread),是程序中一个单一的顺序控制流程,也被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。
5、多线程,就是同时执行一个以上的线程,一个线程的执行不必等待另一个线程执行完才执行,所有线程都可能发生在同一时间。
6、进程属于操作系统范畴,主要是在同一时间段内,可以同时执行一个以上的程序。而线程则是在同一程序内同时执行一个以上的程序段。
7、多任务是针对操作系统而言的,表示操作系统可以同时运行多个应用程序,而多线程是指一个进程而言的,表示一个进程内部可以同时执行多个线程
二、线程的相关API
//获取当前线程的名字
Thread.currentThread().getName()
static Thread | currentThread() 返回当前正在运行的线程对象。 |
ClassLoader | getContextClassLoader() 返回该线程的上下文 ClassLoader。 |
long | getId() 返回该线程的标识符。 |
String | getName() 返回该线程的名称。 |
int | getPriority() 返回线程的优先级。 |
void | interrupt() 中断线程。 |
boolean | isInterrupted() 测试线程是否已经中断。 |
boolean | isAlive() 测试线程是否处于活动状态。 |
boolean | isDaemon() 测试该线程是否为守护线程。 |
void | join() 等待该线程终止。 |
void | join(long millis) 等待该线程终止的时间最长为 millis 毫秒。 |
void | setDaemon(boolean on) 将该线程标记为守护线程或用户线程。 |
void | setName(String name) 改变线程名称,使之与参数 name 相同。 |
void | setPriority(int newPriority) 更改线程的优先级。 |
static void | sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。 |
void | start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。 |
void | stop() 已过时。当执行此方法时,强制结束当前线程。 |
static void | yield() 暂停当前正在执行的线程对象,并执行其他线程。 |
三、线程的调度
1、抢占式调度模型
哪个线程的优先级比较高,抢占的CPU时间片的概率就高一些/多一些,java采用的就是抢占式调度模型。
2、均分式调度模型
平均分配CPU时间片,每个线程占用的CPU时间片长度一样,一切平等。
四、线程的优先级
1、等级:
- MAX_PRIORITY:10
- MIN_PRIORITY:1
- NORM_PRIORITY:5
2、方法:
- getPriority():返回线程优先级
- setPriority(int newPriority):更改线程的优先级
/*
关于线程的优先级
*/
public class ThreadTest08 {
public static void main(String[] args) {
/*System.out.println(Thread.MAX_PRIORITY);
System.out.println(Thread.MIN_PRIORITY);
System.out.println(Thread.NORM_PRIORITY);*/
Thread.currentThread().setPriority(1);
System.out.println("main线程优先级--->"+Thread.currentThread().getPriority());
Thread t=new Thread(new MyRunnable4());
t.setPriority(10);
t.setName("支线线程t");
t.start();
for(int i=0;i<50;i++){
System.out.println(Thread.currentThread().getName()+"--->"+i);
}
}
}
class MyRunnable4 implements Runnable{
@Override
public void run() {
System.out.println("支线线程t优先级--->"+Thread.currentThread().getPriority());
for(int i=0;i<50;i++){
System.out.println(Thread.currentThread().getName()+"--->"+i);
}
}
}
注意!:高优先级的线程要抢占低优先级的线程的cpu的执行权。但是仅是从概率上来说的,高优先级的线程更有可能被执行。并不意味着只有高优先级的线程执行完以后,低优先级的线程才执行。
五、Java实现多线程方式
1、继承Thread,重写run()方法
/*
多线程并发
*/
public class ThreadTest01 {
public static void main(String[] args) {
//这是main方法,是主线程,在主栈中运行
MyThread myThread=new MyThread();
//启动线程
myThread.start();
for(int i=0;i<10;i++){
System.out.println("主线程--->"+i);
}
}
}
class MyThread extends Thread{
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println("分支线程--->"+i);
}
}
}
输出结果:
主线程—>0
主线程—>1
主线程—>2
主线程—>3
主线程—>4
分支线程—>0
分支线程—>1
分支线程—>2
分支线程—>3
分支线程—>4
分支线程—>5
分支线程—>6
主线程—>5
主线程—>6
主线程—>7
主线程—>8
主线程—>9
分支线程—>7
分支线程—>8
分支线程—>9
Process finished with exit code 0
另外,要明白启动线程的是start()方法而不是run()方法,如果用run()方法,那么他就是一个普通的方法执行了。
2、实现Runable接口
public class ThreadTest02 {
public static void main(String[] args) {
//创建一个可运行的对象
//MyRunnable r=new MyRunnable();
//将可运行的对象封装成一个线程对象
//Thread thread=new Thread(r);
Thread thread=new Thread(new MyRunnable());//合并代码
thread.start();
for(int i=0;i<5;i++){
System.out.println("主线程--->"+i);
}
}
}
//这并不是一个线程类,是一个可运行的类,还不是一个线程
class MyRunnable implements Runnable{
@Override
public void run() {
for(int i=0;i<5;i++){
System.out.println("分支线程--->"+i);
}
}
}
3、通过匿名内部类的方式实现多线程
/*
通过匿名内部类的方式实现线程
*/
public class ThreadTest03 {
public static void main(String[] args) {
//采用匿名内部类的方式创建线程对象
Thread t=new Thread(new Runnable() {
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println("t线程--->"+i);
}
}
});
//启动线程
t.start();
for(int i=0;i<10;i++){
System.out.println("main线程--->"+i);
}
}
}
4、实现Callable接口方式
与使用runnable方式相比,callable功能更强大些:
runnable重写的run方法不如callable的call方法强大,call方法可以有返回值
方法可以抛出异常
支持泛型的返回值
需要借助FutureTask类,比如获取返回结果
package com.guteng.thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @author 古藤老人
*/
public class ThreadTest02 {
/**
*创建线程的方式三:实现callable接口。---JDK 5.0新增
*是否多线程?否,就一个线程
*
* 比runable多一个FutureTask类,用来接收call方法的返回值。
* 适用于需要从线程中接收返回值的形式
*
* //callable实现新建线程的步骤:
* 1.创建一个实现callable的实现类
* 2.实现call方法,将此线程需要执行的操作声明在call()中
* 3.创建callable实现类的对象
* 4.将callable接口实现类的对象作为传递到FutureTask的构造器中,创建FutureTask的对象
* 5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start方法启动(通过FutureTask的对象调用方法get获取线程中的call的返回值)
*
* */
public static void main(String[] args) {
//new一个实现callable接口的对象
MyCallable myCallable = new MyCallable();
//通过futureTask对象的get方法来接收futureTask的值
FutureTask futureTask = new FutureTask(myCallable);
Thread t = new Thread(futureTask);
t.setName("t线程");
t.start();
try {
//get返回值即为FutureTask构造器参数callable实现类重写的call的返回值
Object sum = futureTask.get();
System.out.println(Thread.currentThread().getName() + ":" + sum);
} catch (ExecutionException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//实现callable接口的call方法
class MyCallable implements Callable {
private int sum=0;
//可以抛出异常
@Override
public Object call() throws Exception {
for(int i = 0;i<50;i++){
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName()+":"+i);
sum += i;
}
}
return sum;
}
}
5、使用线程池的方式
(1)背景:经常创建和销毁,使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
(2)思路:提前创建好多个线程,放入线程池之,使用时直接获取,使用完放回池中。可以避免频繁创建销毁,实现重复利用。类似生活中的公共交通工具。(数据库连接池)
(3)好处:提高响应速度(减少了创建新线程的时间)
降低资源消耗(重复利用线程池中线程,不需要每次都创建)
便于线程管理
(4)解释
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间后会终止
(5)JDK 5.0 起提供了线程池相关API:ExecutorService 和 Executors
- ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
- void execute(Runnable coommand):在将来某个时间执行给定任务,没有返回值,一般用来执行Runnable
- Future submit(Callable task):提交一个返回值的任务用于执行,返回一个表示任务的未决结果的 Future。
- void shutdown():关闭连接池。
Executors | 工具类,线程池的工厂类,用于创建并返回不同类型的线程池 |
Executors.newCachedThreadPool() | 创建一个可根据需要创建新线程的线程池 |
Executors.newFixedThreadPool(int nThreads) | 创建一个可重用固定线程数的线程池 |
Executors.newSingleThreadExecutor() | 创建一个只有一个线程的线程池 |
Executors.newScheduledThreadPool(int corePoolSize) | 创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。 |
(6)线程池构造批量线程代码如下:
package com.guteng.thread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author 古藤老人
*/
public class ThreadPool {
/**
* 创建线程的方式四:使用线程池(批量使用线程)
* 1.需要创建实现runnable或者callable接口方式的对象
* 2.创建Executorservice线程池
* 3.将创建好的实现了runnable接口类的对象放入executorService对象的execute方法中执行。
* 4.关闭线程池
*/
public static void main(String[] args) {
//创建固定线程个数为十个的线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
//new一个Runnable接口的对象
MyRunnable1 runnable1 = new MyRunnable1();
MyRunnable2 runnable2 = new MyRunnable2();
//执行线程,最多十个
executorService.execute(runnable1);
executorService.execute(runnable2);
//适合适用于Runnable
//executorService.submit();//适合使用于Callable
//关闭线程池
executorService.shutdown();
}
}
class MyRunnable1 implements Runnable {
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
class MyRunnable2 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 1) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
六、线程生命周期
Java线程具有五中基本状态
(1)新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();
(2)就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start(),此线程立即就会执行;
(3)运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
(4)阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
- 等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
- 同步阻塞:线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
- 其他阻塞:通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意:sleep是不会释放持有的锁)
(5)死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
七、线程相关方法
1、线程休眠
sleep()方法定义在Thread类中,sleep()的作用是让当前线程休眠,即当前线程会从“运行状态”进入到“休眠(阻塞)状态”。sleep()会指定休眠时间,线程休眠的时间会大于/等于该休眠时间;在线程重新被唤醒时,它会由“阻塞状态”变成“就绪状态”,从而等待CPU的调度执行。
public static void sleep(long millis)
静态方法:Thread.sleep(1000);
参数是毫秒
可以实现间隔一段时间,去执行一段特定的代码
/**
* 合理地终止一个进程
* @author 古藤老人
*/
public class ThreadTest07 {
public static void main(String[] args) {
MyRunnable3 r=new MyRunnable3();
Thread t=new Thread(r);
t.setName("t线程");
t.start();
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
r.run=false;
}
}
class MyRunnable3 implements Runnable{
//打一个布尔标记
boolean run=true;
@Override
public void run() {
for(int i=0;i<10;i++){
if(run){
System.out.println(Thread.currentThread().getName()+"--->"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
//终止当前线程
return;
}
}
}
}
2、线程让步
在Java线程中,yield()方法的作用是让步,它能让当前线程由“运行状态”进入到“就绪状态”,从而让其他具有相同优先级的等待线程获取执行权;但是,并不能保证在当前线程调用yield()之后,其他具有相同优先级的线程就一定能获得执行权;也有可能是当前线程有进入到“运行状态”继续运行。
/**
* 让位,当前线程暂停,回到就绪状态,让给其他线程
* @author 古藤老人
*/
public class ThreadTest09 {
public static void main(String[] args) {
Thread t=new Thread(new MyRunnable5());
t.setName("t线程");
t.start();
for(int i=1;i<10;i++){
System.out.println(Thread.currentThread().getName()+"--->"+i);
}
}
}
class MyRunnable5 implements Runnable{
@Override
public void run() {
for(int i=1;i<10;i++){
//每10个让位一次
if(i%2==0){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"--->"+i);
}
}
}
3、合并线程
join方法调用后当前线程会等待目标线程死亡后才会继续运行
/**
* @author 古藤老人
*/
public class ThreadTest11 {
public static void main(String[] args) {
System.out.println("main begin");
Thread t=new Thread(new MyRunnable6());
t.setName("支线线程");
t.start();
//合并线程
try {
t.join();//当前线程(main线程)进入堵塞,t线程执行,直到t线程结束,当前线程才可继续执行
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main over");
}
}
class MyRunnable6 implements Runnable{
@Override
public void run() {
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+"--->"+i);
}
}
}
4、对线程等待和唤醒的方法
(1)在Object.java中,定义了wait(),notify()和notifyAll()等方法。wait()的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。而notify()和notifyAll()的作用,则是唤醒当前对象上的等待线程;notify()是唤醒单个线程,而notifyAll()是唤醒所有的线程。
(2)Object中的wait()、notify()等函数和synchronized一样,会对“对象的同步锁”进行操作。wait()会使“当前线程”等待,因为线程应该释放它所持有的“同步锁”,否则其他线程获取不到该“同步锁”而无法运行。线程调用wait()之后,会释放它所持有的“同步锁”;而且,等待线程可以被notify()或notifyAll()唤醒。那么notify()是依据什么唤醒等待线程的?或者wait()和notify()之间是通过什么关联起来的?答案是:依据“对象的同步锁”。
(3)负责唤醒等待线程的那个线程(我们称为“唤醒线程”),它只有在获取“该对象的同步锁”(这里的同步锁必须和等待线程的同步锁是同一个),并且调用notify()或notifyAll()方法之后,才能唤醒等待线程。虽然,等待线程被唤醒,但是,它不能立即执行。因为唤醒线程还持有“该对象的同步锁”。必须等到唤醒线程释放“该对象的同步锁”之后,等待线程才能获取到“对象的同步锁”,然后继续运行。
(4)调用wait()、notify()和notifyAll()的任务在调用这些方法前必须获取对象的锁。
public class ThreadHomework {
public static void main(String[] args) {
Num num=new Num(0);
Thread t1=new Thread(new Producer1(num));
Thread t2=new Thread(new Producer2(num));
t1.setName("t1");
t2.setName("t2");
t1.start();
t2.start();
}
}
class Num{
int i;
public Num(int i) {
this.i = i;
}
}
class Producer1 implements Runnable{
Num num;
public Producer1(Num num) {
this.num = num;
}
@Override
public void run() {
while(true){
synchronized(num){
//num.i是奇数
if(num.i%2!=0){
try {
num.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num.i++;
System.out.println(Thread.currentThread().getName()+"--->"+num.i);
num.notifyAll();
}
}
}
}
class Producer2 implements Runnable{
private Num num;
public Producer2(Num num) {
this.num = num;
}
@Override
public void run() {
while(true){
synchronized(num){
//num.i是偶数
if(num.i%2==0){
try {
num.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num.i++;
System.out.println(Thread.currentThread().getName()+"--->"+num.i);
num.notifyAll();
}
}
}
}