目录
一、什么是线程
1.1进程
1.1.1进程的定义
了解线程就必须先知道进程,下面是进程的几种定义:
- 进程是程序的一次执行过程
- 进程是一个程序及其数据在处理机上顺序执行时所发生的活动
- 进程是具有独立功能的程序在一个数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位
1.1.2 进程的组成
进程是一个独立的运行单位,也是操作系统进行资源分配和调度的基本单位。
进程是由进程控制块(PCB)、程序段、数据段组成的。
1.1.3 进程的体现
在Window系统中可以直接打开任务管理器查看当前运行的进程
而我们运行的Java通常可以看成一个进程
1.2.线程
进程是有自己独立的资源的,如内存,堆栈。在对于单核的Cpu来说,所有的进程都是并发执行的,即在宏观上看Cpu运行着不同的进程,而在微观具体到某一时间片里,cpu只能运行一个进程。这样就会有着不断的进程切换的过程。在进程的切换过程中,由于每个进程的资源是不同的,需要保存进程的运行现场,这会消耗cpu的资源。
引入进程,就是为了更好地使程序并发执行,提高资源利用率和系统吞吐量,增加并发度。
引入线程,是为了减少程序在并发执行时所付出的时空开销,提高操作系统的并发性能。
1.2.1线程的基本概念
线程最简单的理解就是“轻量级进程”,它是一个基本的Cpu执行单位,也是程序执行流的最小单元,由线程ID、程序计数器、寄存器集合和堆栈组成。
注意:
- 线程是不拥有系统资源的,只拥有一点在运行中必不可少的资源。
- 进程才能拥有独立的系统的资源,而进程内的线程将会共享所在进程的资源,这也是为什么线程切换起来会比进程切换快的原因。
二、java中多线程的实现方式
2.1. 继承Thread类的方式
2.1.1 步骤
- 定义一个类,继承Thread类
- 在自定义类中重写run方法,用于定义新线程要运行的内容
- 创建自定义类型的对象
- 调用线程的启动方法:start 方法
2.1.2 实现代码
public class Mythread extends Thread{
@Override
public void run() {
// TODO Auto-generated method stub
for(int i = 0; i < 100; i++) {
System.out.println(getName() + ": " + i); //getName()为Thread实例化方法获取线程名字
}
}
}
public static void main(String[] args) {
// 继承方式
Mythread th1 = new Mythread("th1");
Mythread th2 = new Mythread("th2");
th1.start();
th2.start();
for(int i = 0; i < 100; i++) {
//Thread.currentThread(),Thread的静态方法,返回当前线程对象
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
2.2 实现Runnable接口方式
2.2.1 步骤
- 定义一个任务类,实现Runnable接口
- 重写任务类中的run方法,用于定义任务的内容
- 创建任务类对象,表示任务
- 创建一个thread类型的对象,用于执行任务类对象
- 调用线程对象的start方法,开启新线程
2.2.2 实现代码
public static void main(String[] args) {
//实现方式
Thread th1 = new Thread(new MyRunnable());
th1.start();
for(int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
public class MyRunnable implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
for(int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
2.3两种方式的比较
从代码复杂程度来看,很明显,继承Thread 的方式比较简单,而实现Runnable 接口的方式比较复杂
2.3.1 继承Thread方式的实现原理
继承方式:调用start方法,调用start0方法,start0是本地方法(native),由虚拟机实现,是C语言实现的方法,所以在java中看不到代码。本地方法start0返回来调用java中的run方法,run方法已经在子类中重写过了,所以最终运行的是子类重写了的run方法
2.3.2实现Runnable接口方式原理
构造方法中,将Runnable的实现类对象传入构造方法中,经过一路init方法的传递,最终,用于给Thread类型中的某个成员变量(target)赋值;调用对象的start方法,最终也是返回来调用Thread类中的run方法,判断当前的成员变量target是否为null,如果不为null,就调用target的run方法,而这个run方法我们已经重写过了,最终运行的是我们重写过的run方法。
2.3.3从java程序设计角度
由于java中只支持单继承、不支持多继承,但可以实现多个接口
- 继承方式:某个类继承了Thread类,那么就无法继承其他业务中需要的类型,就限制了我们的设计。所以扩展性较差。将线程对象和任务内容绑定在了一起,耦合性较强、灵活性较差
- 实现方式:某个类通过实现Runnable的方式完成了多线程的设计,仍然可以继承当前业务中的其他类型,扩展性较强。将线程对象和任务对象分离,耦合性就降低,灵活性增强:同一个任务可以被多个线程对象执行,某个线程对象也可以执行其他的任务对象。并且将来还可以将任务类对象,提交到线程池中运行;任务类对象可以被不同线程运行,方便进行线程之间的数据交互。
2.4 Thread类常用的方法
2.4.1实例方法
- String getName() - 获取线程名称,默认为Thread-x,x为从0开始的序号
- void setName(String name) - 设置线程名称, 可在启动前设置,也可以在启动后设置
- void start() - 线程开始执行
- long getId() - 获取线程的标识符
- int getPriority() - 返回此线程的优先级
- void setPriority(int newPriority) - 设置线程优先级,优先级数字越大,优先级越高,默认为5,最大为10,最小为1,Thread内有MAX_PRIORITY、NORM_PRIORITY、MIN_PRIORITY 三个优先级常量对应上述数字
- void setDaemon(boolean on) - 设置此线程为守护线程(即后台线程,如垃圾回收线程,若非守护线程都死亡,程序将即将退出)
- void join() - 等待这个线程实例死亡,即执行结束
- void join(long millis) - 等待这个线程实例millis毫秒
Mythread th1 = new Mythread();
for(int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
if(i == 4) {
th1.start();
th1.join();
}
}
/**执行结果
main: 0
main: 1
main: 2
main: 3
main: 4
Thread-0: 0
Thread-0: 1
Thread-0: 2
Thread-0: 3
Thread-0: 4
Thread-0: 5
Thread-0: 6
Thread-0: 7
Thread-0: 8
Thread-0: 9
main: 5
main: 6
main: 7
main: 8
main: 9
*/
2.4.2 静态方法
- static void sleep(long millis) - 暂停当前线程
- static Thread currentThread() - 返回当前线程对象
三、多线程的安全问题
线程的执行顺序是随机的,在某个线程执行没有完成的时候,cpu就可能被其他线程抢走,结果导致当前代码中的一些数据发生错误。
public class Myprint {
public void printHello() {
System.out.print("H");
System.out.print("e");
System.out.print("l");
System.out.print("l");
System.out.print("o");
System.out.print(" ");
}
public void printWorld() {
System.out.print("W");
System.out.print("o");
System.out.print("r");
System.out.print("l");
System.out.print("d");
System.out.print("\n");
}
}
public static void main(String[] args) throws InterruptedException {
Myprint p = new Myprint();
new Thread() {
@Override
public void run() {
// TODO Auto-generated method stub
while(true) {
p.printHello();
}
}
}.start();
new Thread() {
@Override
public void run() {
// TODO Auto-generated method stub
while(true) {
p.printWorld();
}
}
}.start();
}
如上述例子中希望的是能完整得打印出Hello、World,而运行结果却不尽人意
3.1 同步代码块
使用一种格式,达到让某段代码执行的时候,cpu不要切换到影响当前代码的代码上去这种格式,可以确保cpu在执行A线程的时候,不会切换到影响A线程执行的其他线程上去
3.1.1 同步代码块的格式
需要用到synchronized 关键字
synchronized (锁对象) {
//需要同步的代码
}
如对Myprint类进行修改:
public class Myprint {
private Object obj = new Object();
public void printHello() {
synchronized (obj) {
System.out.print("H");
System.out.print("e");
System.out.print("l");
System.out.print("l");
System.out.print("o");
System.out.print(" ");
}
}
public void printWorld() {
synchronized (obj) {
System.out.print("W");
System.out.print("o");
System.out.print("r");
System.out.print("l");
System.out.print("d");
System.out.print("\n");
}
}
}
当cpu想去执行同步代码块的时候,需要先获取到锁对象,获取之后就可以运行代码块中的内容;当cpu正在执行当前代码块的内容时,cpu可以切换到其他代码,但是不能切换到具有相同锁对象的代码上。
当cpu执行完当前代码块中的代码之后,就会释放锁对象,cpu就可以运行其他具有当前锁对象的同步代码块了
这样就能完整得打印出Hello World
3.2 同步方法
如上述的Myprint内的方法可修改为
public class Myprint {
public synchronized void printHello() {
System.out.print("H");
System.out.print("e");
System.out.print("l");
System.out.print("l");
System.out.print("o");
System.out.print(" ");
}
public synchronized void printWorld() {
System.out.print("W");
System.out.print("o");
System.out.print("r");
System.out.print("l");
System.out.print("d");
System.out.print("\n");
}
}
用synchronized修饰方法效果会如上同步代码块一样,而此时锁的对象默认为 this
3.3 同步锁
也可用Lock类对同步代码进行加锁、释放锁,以此来保证线程执行的原子性
import java.util.concurrent.locks.ReentrantLock;
public class Myprint {
private ReentrantLock lock = new ReentrantLock();
public void printHello() {
lock.lock();
System.out.print("H");
System.out.print("e");
System.out.print("l");
System.out.print("l");
System.out.print("o");
System.out.print(" ");
lock.unlock();
}
public synchronized void printWorld() {
lock.lock();
System.out.print("W");
System.out.print("o");
System.out.print("r");
System.out.print("l");
System.out.print("d");
System.out.print("\n");
lock.unlock();
}
}
四、线程池
在没有引入线程池前,我们使用一条线程时,先将线程对象创建出来,启动线程,在运行过程中,可能完成任务,也可能会在中途被任务内容中断掉,任务还没有完成;或是正常完成,线程对象结束就变成垃圾对象被垃圾回收器回收。如果在整个程序中有很多的小任务,任务所需要执行的时间又很少,就会有大量的时间浪费在线程对象的创建和死亡中。
而引入线程池后,系统会在创建大量空闲的线程,程序将一个 Runnable对象传给线程池,线程池就会启动一个空闲的线程来执行它们的run()方法,当run()方法执行结束后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个Runnable对象的run()。
4.1 使用Executors工具类获取线程池对象
public static void main(String[] args) throws InterruptedException {
ExecutorService es = Executors.newFixedThreadPool(2); //创建拥有2个线程的线程池
MyRunnable th1 = new MyRunnable("th1");
MyRunnable th2 = new MyRunnable("th2");
es.submit(th1);
es.submit(th2);
es.shutdown(); //结束线程池
es.shutdownNow(); //结束线程池,已经开始运行的保证完成,提交却没有运行的不再运行
}