基本概念
程序 - 主要指存放在硬盘/磁盘上的可执行文件。
进程 - 主要指运行在内存中的程序。
目前主流的操作系统都支持多进程,为了同时执行多个任务,进程是重量级的,新建进程对系统的资源消耗比较大,因此进程的数量还是有限的。
线程就是指进程内部的程序流,是轻量级的,新建线程会共享所在进程的资源,因此对系统的资源消耗比较小,以后开发中采用多线程技术是主流的方式。
目前主流的操作系统都是采用时间片轮转法来保证多个任务的并发执行效果,所谓的并发就是指宏观并行微观串行。
线程的创建(重中之重)
(1)使用Thread类创建线程
java.lang.Thread类用于描述线程,Java虚拟机允许应用程序并发地运行多个执行线程
创建并启动线程的方式有两种:
a.自定义类继承Thread类并重写run()方法,然后创建对象调用start()方法。
b.自定义类实现Runnable接口并重写run方法,创建Thread类对象,参数传递Runnable接口的引用,再调run方法。
继承Thread类的方法:
public class TestThreadStart extends Thread{
@Override
public void run(){
System.out.println("调用run方法");
}
public static void main(String[] args) {
TestThreadStart tts = new TestThreadStart(); //使用子类的引用指向子类的对象
tts.start(); //调用start()方法,自动调用run()方法
System.out.println("调用start方法");
}
}
实现Runnable接口的方法:
当使用Runnable引用作为参数构造对象时(尽量理解):
a.new Runnable接口的实现类对象传递给Thread类的构造方法。
b.Thread类的构造方法将实现类对象传递给init()方法。
c.init方法的内部将实现类对象传递给成员变量target。
d.在run()方法中判断成员变量target是否为空,若不为空则调用run()方法。
由于target引用指向实现类的对象,因此在运行阶段调用实现类中的run()方法。
public class TestRunnableRun implements Runnable{
@Override
public void run(){
System.out.println("你到底会不会调用我呢?");
}
public static void main(String[] args) {
//声明一个Thread类的引用记录本类的对象,参数传递Runnable接口的引用
Thread t1 = new Thread(new TestRunnableRun());
//调用run()方法,最终调用上述类中的run()方法
t1.run();
System.out.println("程序正常结束了!");
}
}
匿名内部类的方法:
public class TestNoNameStart {
public static void main(String[] args) {
//采用继承Thread类的方式加上匿名内部类来创建并启动一个线程
Thread t1 = new Thread(){
@Override
public void run(){
System.out.println("继承Thread+匿名内部类");
}
};
t1.start();
//采用实现接口的方式加上匿名内部类来创建并启动一个线程
Runnable r = new Runnable(){
@Override
public void run(){
System.out.println("实现接口+匿名内部类");
}
};
Thread t2 = new Thread(r);
t2.start();
}
}
备注:
a.继承的方式代码相对简单,但Java语言支持单继承,若继承Thread类则无法继承其他父类.
b.实现接口的方式代码相对复杂,但不影响该类继承其他父类,并且支持多实现,
从项目的可维护性上来说,推荐使用该方式。
(2)Thread类的方法
Thread() - 使用无参的方式构造对象。
Thread(Runnable target) - 根据参数指定的接口引用来构造对象。实参可传实现类的对象,或使用匿名内部类。
Thread(String name) - 根据参数指定的名称来构造对象。
Thread(Runnable target, String name) - 参数指定接口引用和名称。
void run() - 若使用Runnable接口作为参数构造的线程对象,则最终调用接口中的run()方法;
否则该方法啥也不做直接返回。
void start() - 启动线程并自动调用run()方法。
(3)原理分析
执行main()方法的线程叫做主线程,执行run()方法的线程叫做新线程/子线程。
对于start()方法之前的代码来说,只会被主线程执行一次,当start()方法调用成功后,线程的个数瞬间由1个变成了2个,其中新创建出来的线程去执行run()方法,原来执行main()方法的主线程继续向下执行,两个线程各自独立运行。
当run()方法结束时,则子线程结束;当main()方法结束时,则主线程结束。
主线程和子线程之间没有明确的执行先后次序,由系统的调度算法来决定。
线程的名称和编号(熟悉)
String getName() - 用于获取线程的名称。
void setName(String name) - 用于设置线程的名称为参数指定的数值。
long getId() - 用于获取线程的编号。
static Thread currentThread() - 用于返回当前正在执行的线程对象的引用。
线程的主要状态(熟悉)
新建状态 - 当使用new关键字创建完线程对象之后的状态。此时线程并没有开始执行。
就绪状态 - 调用start()方法之后进入的状态。此时线程依然没有开始执行。
运行状态 - 当线程调度器调度该线程之后进入的状态。此时线程【正式开始】执行。
若时间片执行结束但任务没有做完就回到就绪状态。
消亡状态 - 当时间片执行结束并且任务已经完成时进入的状态。此时【线程终止】。
阻塞状态 - 当线程执行的过程中发生了阻塞事件进入的状态,如:sleep()方法。
当阻塞状态解除后进入就绪状态而不是运行状态。
线程类的常用方法
static void yield() - 用于让出当前线程执行权,也就是暂停执行当前线程,转而执行其他线程(了解)。
static void sleep(long millis) - 用于让当前线程休眠参数指定的毫秒数。
static void sleep(long millis, int nanos) -用于让当前线程休眠指定毫秒+纳秒
1秒=1000毫秒 1毫秒=1000微秒 1微秒 = 1000纳秒
void interrupt() - 中断线程,通常用于打断线程的休眠操作(了解)。
void setPriority(int newPriority) - 用于更改线程的优先级为参数指定的数值。
int getPriority() - 用于获取线程的优先级并返回。
【优先级高的线程并不一定先执行,只是获取到时间片的机会更多一些】
void join() - 用于主线程等待该线程终止。
void join(long millis) - 等待该线程终止的时间最长为参数指定的毫秒。
void join(long millis, int nanos) - 等待该线程终止的最长时间为参数指定的毫秒 + 参数指定的纳秒。
void setDaemon(boolean on) - 用于将该线程设置为守护线程/用户线程。
- 守护线程用于守护其他线程,当其他线程都结束时,守护线程随之结束。
要求重点掌握的方法:sleep()方法 和 join()方法。
代码实现
Join等待线程终止
public class TestThreadJoin extends Thread {
@Override
public void run(){
//让子线程模拟倒计时的效果
for(int i = 20; i > 0; i--){
System.out.println("倒计时开始,还有" + i + "秒!");
try {
Thread.sleep(1000); //让当前线程休眠
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public static void main(String[] args) {
TestThreadJoin ttj = new TestThreadJoin();
ttj.start();
//让主线程等待子线程的执行,等子线程结束后主线程再结束
System.out.println("主线程开始等待...");
try {
//ttj.join(); //谁调用join()方法主线程就等待谁结束
ttj.join(3000); //主线程等待ttj执行3秒钟,之后就不等待了
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//System.out.println("终于等到你还好没放弃,主线程结束!");
System.out.println("实在等不起,GoodBye!");
}
}
主线程开始等待...
倒计时开始,还有20秒!
倒计时开始,还有19秒!
倒计时开始,还有18秒!
实在等不起,GoodBye!
倒计时开始,还有17秒!...
Priority线程优先级
public class TestThreadPriority extends Thread {
@Override
public void run(){
for(int i = 0;i<20;i++){
System.out.println("子线程:i = "+i);
}
}
public static void main(String[] args) {
TestThreadPriority ttp = new TestThreadPriority();
System.out.println("子线程初始优先级为:"+ttp.getPriority());
ttp.setPriority(MAX_PRIORITY);
System.out.println("子线程修改后的优先级为:"+ttp.getPriority());
ttp.start();
//返回当前正在执行的线程对象的引用。
Thread main = Thread.currentThread();
System.out.println("主线程初始优先级为:"+main.getPriority());
main.setPriority(MIN_PRIORITY);
System.out.println("主线程修改后的优先级为:"+main.getPriority());
for(int j = 0;j<20;j++){
System.out.println("---主线程:"+j);
}
}
}
Daemon守护线程
public class TestThreadDaemon extends Thread{
@Override
public void run(){
for(int i = 0; i < 50; i++){
System.out.println("子线程中:i = " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
TestThreadDaemon ttd = new TestThreadDaemon();
ttd.setDaemon(true); //将子线程设置为守护线程
ttd.start();
try {
Thread.sleep(3000); //主线程等待5秒后开始执行
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("主程序结束"); //输出后主线程执行完成,子线程也随之结束
}
}
子线程中:i = 0
子线程中:i = 1
子线程中:i = 2
主程序结束
线程的同步机制(重点)
StringBuffer类属于线程安全的类,支持线程的同步机制,执行效率低。
StringBuilder类属于非线程安全的类,不支持线程的同步机制,执行效率高。
当多个线程同时访问同一种共享资源时可能会造成数据的不一致性,此时就需要进行线程之间的协调,而线程之间的协调和通信就叫做线程的同步机制。即一个方法使用这个关键字后,当有多个对象调用该方法时,先调用的占用该方法,只有该对象调用完成后,下一对象才可调用该方法。
解决方案
由程序案例可知:当多个线程同时访问账户余额时,会导致最终账户余额不正确。
导致的原因:其中一个线程还没有完成操作时,第二个线程就开始访问账户余额。
解决方案:将多个线程之间并行的操作改为串行的操作。
经验:上述方案会导致程序的执行效率变低,因此能不用则不用。
实现方式
在Java语言中使用synchronized关键字来实现同步锁机制,从而保证操作的原子性。
a.使用同步语句块来实现同步锁机制,语法格式如下:
synchronized(锁对象的引用){ //常用this
编写所有需要锁定的语句块;
}
b.使用synchronized关键字修饰方法,表示锁定整个方法的所有方法体。等价于:
synchronized(this){
编写所有需要锁定的语句块;
}
死锁的发生
线程一执行的代码:
public void run(){
synchronized(a){ 持有对象锁a,等待对象锁b
synchronized(b){
... ...
}
}
}
线程二执行的代码:
public void run(){
synchronized(b){ 持有对象锁b,等待对象锁a
synchronized(a){
... ...
}
}
}
以后的开发中不要进行同步语句块的嵌套使用。
Object类的常用方法
void wait() - 用于让当前线程进入等待状态,直到其他线程调用notify()/notifyAll()方法
void wait(long timeout) - 用于让当前线程进入等待状态,直到调用上述方法或者参数指定的毫秒到了。
void wait(long timeout, int nanos)- 用于让当前线程进入等待状态,直到参数指定的毫秒+纳秒到了或调用方法
void notify() - 用于唤醒等待的单个线程(随机)。
void notifyAll() - 用于唤醒等待的所有线程。