文章目录
- 概述
- 创建线程
- `Thread`类中的常用方法
- ①`public static Thread currentThread()`
- ②`public final String getName()`
- ③`public final void setName(String name)`
- ④`public static void yield()`
- ⑤`public final void join()`
- ⑥`stop()`
- ⑦`public static void sleep(long millis)`
- ⑧`public final boolean isAlive()`
- ⑨`public final void setPriority(int newPriority)`
- ⑩`public final int getPriority()`
- 线程的生命周期
- 线程同步
- 线程通信
概述
Java中的main(){}
是Java程序的主线程,所有的分线程都在main(){}
中开始,在main(){}
中结束。
有4
种创建线程的方式:①继承Thread
类;②实现Runnable
接口;③实现Callable<>
泛型接口;④线程池。
创建线程
继承Thread
类
MyThread.java
public class MyThread extends Thread {
@Override
public void run() {
//some statement
}
}
Main.java
public class Main {
public static void main(String[] args) {
MyThread thread_1 = new MyThread();
MyThread thread_2 = new MyThread();
thread_1.start();
thread_2.start();
}
}
注意:
①线程逻辑在run(){}
中设计,因此Thread
子类必须重写run(){}
。
②创建线程的流程为:创建Thread
子类的实例⇨调用实例的start()
。
③运行当中的线程不能再次调用start()
,否则start()
会抛IllegalThreadStateException
。例:
MyThread thread = new MyThread();
thread.start();
thread.start(); //throws IllegalThreadStateException
④start()
的作用:1.启动线程;2.调用run()
。
实现Runnable
接口
Run.java
public class Run implements Runnable {
@Override
public void run() {
for(int i=0;i<100;i++){
if(i%2==0){
System.out.println(i+" 是偶数");
}
}
}
}
Main.java
public class Main {
public static void main(Sting[] args){
Thread thread = new Thread(new Run());
thread.start();
}
}
注意:
①此时调用的Thread
类的构造器为:
public Thread(Runnable target) {...}
②线程逻辑在Runnable
接口实现类的run(){}
中设计。
③使用Thread
对象的start()
启动线程并调用run()
。
④继承Thread
类与实现Runnable
接口这两种方式的区别在于类与接口的区别。定义类时只能继承一个父类,而继承Thread
类的方式中,Thread
类会占据唯一父类;定义类时可以实现多个接口,因此实现Runnable
接口比继承Thread
类更好。
实现Callable<>
泛型接口
代码示例
Call.java
public class Call implements Callable<String> {
@Override
public String call() throws Exception {
for(int i=0;i<100;i++){
if(i%2==0){
System.out.println(i+" 是偶数");
}
}
return "线程结束";
}
}
Main.java
public class Main {
public static void main(String[] args) {
Call call = new Call();
FutureTask<String> task = new FutureTask<>(call);
Thread thread = new Thread(task);
thread.start();
try{
String message = task.get();
System.out.println(message);
}
catch(Exception e){
System.out.println(e);
}
}
}
Callable<>
泛型接口
Callable<>
接口可带1
个泛型参数。Callable<>
的定义为:
public interface Callable<V> {
V call() throws Exception;
}
call()
的返回类型即为Callable<>
被实现时传入的泛型参数类型。
使用Callable<>
接口方式需要借助FutureTask<>
泛型类。使用FutureTask<>
对象的get()
可获得call()
的返回值,get()
返回值类型与FutureTask<>
传入的泛型参数类型一致。
Thread
类中的常用方法
①public static Thread currentThread()
1.作用:获取当前运行的线程。
2.返回:对应线程的Thread
对象。
3.注意:currentThread()
是Thread
类中的静态方法,必须在线程的逻辑执行体中调用才能获取对应的线程对象。对于主线程,Thread.currentThread()
必须在main(){}
中调用;对于继承Thread
类和实现Runnable
接口创建线程的方式,Thread.currentThread()
必须在run(){}
中调用;对于实现Callable<>
泛型接口创建线程的方式,Thread.currentThread()
必须在call(){}
中调用。
4.示例:
public class Main {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName());
}
}
public class Thr extends Thread {
@Override
public void run() {
System.out.println(currentThread().getName());
}
}
public class Run implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
public class Call implements Callable<String> {
@Override
public String call() {
return Thread.currentThread().getName()
}
}
②public final String getName()
1.作用:获取线程名。
2.返回:String
,线程名。
3.注意:若未设置线程名,系统默认按照线程的创建顺序分配线程名为:Thread-0
,Thread-1
,Thread-2
…主线程的默认线程名为main
。
③public final void setName(String name)
1.作用:设置线程名。
2.示例:
Call.java
public class Call implements Callable<String> {
@Override
public String call() {
for(int i=0;i<100;i++) {
System.out.println(Thread.currentThread().getName()+" output "+i);
}
return null;
}
}
Main.java
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new FutureTask<String>(new Call()));
thread.setName("CallThread");
thread.start();
}
}
④public static void yield()
1.作用:线程主动释放当前CPU执行权,下一时刻该线程可能重新获得CPU执行权。
2.注意:yield()
是Thread
类中的静态方法,必须在线程的逻辑执行体中调用,例如main(){}
,run(){}
和call(){}
。
3.示例:
public class Call implements Callable<String> {
@Override
public void String call() {
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+" output "+i);
if(i%20==0){
Thread.yield();
}
}
}
}
⑤public final void join()
1.作用:调用线程等待目标线程死亡,之后继续执行。例如在线程a
中调用b.join()
,线程a
将暂停等待,直到线程b
死亡之后线程a
才继续执行。
2.注意:join()
会抛InterruptedException
。join()
不会使线程得到同步监视器或失去同步监视器,不会使线程阻塞,而是使线程处于等待状态(WAITING/TIMED_WAITING
)。
3.扩展:join()
的带参版本1:public final void join(long millis)
。调用线程等待目标线程死亡的最长时间为millis
毫秒。直到到达最长等待时间或者目标线程提前死亡之后,调用线程才继续执行。若millis=0
,作用与join()
相同。join(long millis)
会抛InterruptedException
。
join()
的带参版本2:public final void join(long millis,int nanos)
。在join(long millis)
的基础上增加毫秒和纳秒精度,nanos
的范围为0-999999
。若millis
和nanos
均为0,作用与join()
相同。join(long millis,int nanos)
会抛InterruptedException
。
4.示例:
Call.java
public class Call implements Callable<String> {
@Override
public String call() {
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+" output "+i);
}
return "this is a test";
}
}
Main.java
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new FutureTask<String>(new Call()));
thread.setName("CallThread");
Thread.currentThread().setName("MainThread");
thread.start();
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+" output "+i);
if(i==10){
try{
thread.join(0,100);
} catch(Exception e) {
System.out.println("some error occur");
}
}
}
}
}
⑥stop()
强制结束一个线程。此方法已过时,不推荐使用。
⑦public static void sleep(long millis)
1.作用:使当前线程暂时停止执行指定毫秒数。暂停执行期间线程处于等待状态(WAITING/TIMED_WAITING
)。
2.注意:sleep()
是Thread
类中的静态方法,必须在线程的逻辑执行体中调用,例如main(){}
,run(){}
和call(){}
。sleep(long millis)
会抛InterruptedException
。
3.扩展:sleep()
的另外一个版本:public static void sleep(long millis,int nanos)
。在sleep(long millis)
的基础上增加毫秒和纳秒精度,nanos
的范围为0-999999
。sleep(long millis,int nanos)
会抛InterruptedException
。
4.示例:
public class Main {
public static void main(String[] args) {
Thread.currentThread().setName("MainThread");
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+" output "+i);
if(i%10==0){
try{
Thread.sleep(500);
} catch(Exception e) {
System.out.println("some error occur");
}
}
}
}
}
⑧public final boolean isAlive()
判断线程是否仍然存活。
⑨public final void setPriority(int newPriority)
1.作用:设置线程的调度优先级。
2.注意:优先级更高的线程不一定先执行,只是拥有更高的CPU调度概率。
3.扩展:线程的优先级范围为1-10
。Thread
类中定义了三个常量标识具有代表性的优先级:
//最低优先级为1
public static final int MIN_PRIORITY = 1;
//中间优先级为5(默认优先级)
public static final int NORM_PRIORITY = 5;
//最高优先级为10
public static final int MAX_PRIORITY = 10;
⑩public final int getPriority()
1.作用:获取线程优先级。
2.示例:
Call.java
public class Call implements Callable<String> {
@Override
public String call() {
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+" output "+i);
}
return "this is a test";
}
}
Main.java
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new FutureTask<String>(new Call()));
thread.setName("CallThread");
Thread.currentThread().setName("MainThread");
thread.setPriority(Thread.MAX_PRIORITY);
Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
thread.start();
for(int i=0;i<100;i++){
if(i==0){
System.out.println("Priority of "
+thread.getName()
+" is "+thread.getPriority());
System.out.println("Priority of "
+Thread.currentThread().getName()
+" is "+Thread.currentThread().getPriority());
}
System.out.println(Thread.currentThread().getName()+" output "+i);
}
}
}
线程的生命周期
一个线程的生命周期大致为:create
⇨run
⇨(pause)
⇨terminated
从线程Thread
对象的创建到调用start()
之间为线程的创建;调用start()
后线程进入运行状态;线程运行期间(可能)处于阻塞或者等待状态而暂停运行;线程执行完毕或者由于异常终止后线程生命终止。
Thread
类中定义了线程的状态标识:
public enum State {
/*就绪(线程创建到调用start()之间)*/
NEW,
/*运行(调用start()后)*/
RUNNABLE,
/*阻塞(线程未获得而等待获得同步锁)*/
BLOCKED,
/*等待(线程已获得同步锁,但需要等待其它线程进行某些操作)*/
WAITING,
/*限时等待(线程已获得同步锁,但需要限时等待其它线程进行某些操作)*/
TIMED_WAITING,
/*终止(线程执行完毕或由于异常终止)*/
TERMINATED;
}
可以使用Thread
对象的getState()
(定义为public State getState(){}
)获取当前的线程状态。
线程阻塞与线程等待的区别:
线程阻塞是由于线程未获得同步锁而不能操作共享资源,只有其它线程中对于共享资源的操作结束并且获得同步锁之后,线程才能操作共享资源,在此期间线程处于阻塞状态。
线程等待是由于线程运行时需要等待其它线程的某些操作完成,或者需要减缓线程的运行速度而进行的主动等待,此时线程已经获得同步锁。
线程同步
线程安全
多个线程操作共享数据时可能会出现线程安全问题。例如线程a
需要根据共享标志变量flag
的true
/false
进行不同的操作,线程a
判断完毕后开始执行相应的操作,此时线程b
改变flag
的值,而线程a
依然根据flag
被线程b
更改之前的值进行操作,最终将导致线程a
操作与判断不一致的问题。实例:
DrawMoney.java
public class DrawMoney implements Callable<String> {
//提款人
private String user;
//余额
private static int balance = 5000;
//本次提款数额
private int howMuch;
public DrawMoney(String user,int howMuch) {
this.user = user;
this.howMuch = howMuch;
}
public String call() {
if((balance-this.howMuch) >= 0) {
balance -= this.howMuch;
System.out.println(this.user+"本次提款:"+this.howMuch+" 元");
}
else {
System.out.println("抱歉,您的账户余额不足!");
}
return "this is a test.";
}
public static int getBalance() {
return balance;
}
}
Main.java
public class Main {
public static void main(String[] args) {
//妻子将提款4000元
Thread wife = new Thread(new FutureTask<String>(new DrawMoney("妻子",4000)));
//丈夫将提款2000元
Thread husb = new Thread(new FutureTask<String>(new DrawMoney("丈夫",2000)));
//丈夫和妻子同时开始提款
wife.start();
husb.start();
//等待提款操作完成
while (wife.isAlive()||husb.isAlive());
System.out.println("余额:"+DrawMoney.getBalance()+" 元");
}
}
妻子提款和丈夫提款是两个独立运行的线程,可能会出现一种极端情况,例如:妻子提款完成后余额管理系统未及时更新余额,同时丈夫开始提款。由于余额未及时更新,丈夫提款时余额管理系统仍然判断余额足够,当丈夫提款完成后,余额已为负值。
多个线程操作共享数据时,尽管极端情况发生的概率很小,但在很多时候,线程安全问题会带来极大的隐患。因此多个线程操作共享数据时必须考虑线程安全问题。
同步代码块
/**
*monitor:同步监视器(任何对象)
*/
synchronized(monitor) {
//需要被同步的代码块
}
由**(synchronized
关键字)+(同步监视器)+(需要被同步的代码块)构成的代码块称为同步代码块**。
同步监视器的使用:同步监视器可以是任何类的对象(包括任何类的Class
对象)。操作共享资源的同步代码块必须使用同一个同步监视器。
对于使用相同同步监视器的同步代码块,任何时刻都只有一个同步代码块拥有同步监视器,其它线程中未获得同步监视器的同步代码块将导致线程处于阻塞状态。同步代码块执行完毕后自动释放同步监视器,其它同步代码块开始竞争同步监视器。
对于提款的改进:
DrawMoney.java
public class DrawMoney implements Callable<String> {
//提款人
private String user;
//余额
private static int balance = 5000;
//本次提款数额
private int howMuch;
//使用当前类的Class对象作为同步监视器
private static final Class<?> monitor = DrawMoney.class;
public DrawMoney(String user,int howMuch) {
this.user = user;
this.howMuch = howMuch;
}
public String call() {
synchronized(monitor) {
if((balance-this.howMuch) >= 0) {
balance -= this.howMuch;
System.out.println(this.user+"本次提款:"+this.howMuch+" 元");
}
else {
System.out.println("抱歉,您的账户余额不足!");
}
}
return "this is a test.";
}
public static int getBalance() {
return balance;
}
}
同步方法
使用synchronized
修饰的方法为同步方法。将需要同步的操作逻辑置于同步方法中,作用与同步代码块相同。同步方法的同步监视器为this
,没有显式声明的同步监视器。
同步方法的局限在于:使用不同对象创建线程时同步监视器对应不同的对象,从而导致同步监视器不一致。解决方法:将同步方法声明为静态方法,此时同步监视器为当前类的Class
对象。
private static synchronized void synMethod() {}
对于提款的改进:
DrawMoney.java
public class DrawMoney implements Callable<String> {
//提款人
private String user;
//余额
private static int balance = 5000;
//本次提款数额
private int howMuch;
public DrawMoney(String user,int howMuch) {
this.user = user;
this.howMuch = howMuch;
}
//将同步方法声明为 静态方法
private static synchronized void draw(String user,int howMuch) {
if((balance-howMuch) >= 0 ) {
balance -= howMuch;
System.out.println(user+"本次提款:"+howMuch+" 元");
}
else {
System.out.println("抱歉,您的账户余额不足!");
}
}
public String call() {
//在线程逻辑执行体中调用同步方法
draw(this.user,this.howMuch);
return "this is a test.";
}
public static int getBalance() {
return balance;
}
}
死锁
线程不释放同步监视器的情况称为死锁。线程中出现死循环或者线程之间互相等待对方释放同步监视器的情况都会导致死锁。死锁不会导致程序运行出错或者抛异常,而是导致线程永久处于阻塞状态。实例:
Main.java
public class Main {
private static final String str_1 = "give me the str_2";
private static final String str_2 = "give me the str_1";
public static void main(String[] args) {
//线程1
new Thread(new FutureTask<String>(new Callable<String>() {
@Override
public String call() {
synchronized(str_1) {
System.out.println(str_1);
synchronized(str_2) {
System.out.println(str_2);
}
}
return Thread.currentThread().getName();
}
})).start();
//线程2
new Thread(new FutureTask<String>(new Callable<String>() {
@Override
public String call() {
synchronized(str_2) {
System.out.println(str_2);
synchronized(str_1) {
System.out.println(str_1);
}
}
return Thread.currentThread().getName();
}
})).start();
}
}
实例中出现了同步代码块的嵌套:线程1中的外层监视器为str_1
,内层为str_2
;线程2的外层监视器为str_2
,内层为str_1
。两个线程的内层监视器分别对应对方的外层监视器,因此线程1开始运行后必须获得str_2
才能运行完毕,线程2开始运行后必须获得str_1
才能运行完毕。
若线程1和线程2运行时均未获得各自的内层监视器,两个线程将一直等待对方运行完毕后释放外层监视器而处于僵持局面,线程将永久阻塞。
实际开发中应当慎重选择同步监视器,避免出现死锁。
线程通信
线程影响其它线程运行状态的操作称为线程通信。实现线程通信的方法:
①wait()
:
使当前线程阻塞并释放同步监视器,直到被其它线程唤醒并获得同步监视器才继续执行。线程被唤醒之前不能参与同步监视器的竞争。
wait()
必须在同步代码块(或同步方法)中由对应的同步监视器调用。
wait()
会抛InterruptedException
。
②wait(long timeoutMillis)
:
使当前线程阻塞指定毫秒数并释放同步监视器,直到被其它线程唤醒并获得同步监视器或到达设定时间才继续执行。线程被唤醒之前不能参与同步监视器的竞争。
wait(long timeoutMillis)
必须在同步代码块(或同步方法)中由对应的同步监视器调用。
wait(long timeoutMillis)
会抛InterruptedException
。
timeoutMillis
设置为0
时作用与wait()
相同。
③wait(long timeoutMillis,int nanos)
:
在wait(long timeoutMillis)
基础上增加纳秒精度。timeoutMillis
与nanos
均为0
时作用与wait()
相同。
④notify()
:
唤醒一个在此同步监视器(notify()
的调用者)上等待(被wait()
阻塞)的线程,使其参与同步监视器的竞争。如果有多条线程等待,notify()
将随机唤醒其中一个线程。
被唤醒的线程将继续阻塞,直到竞争获得同步监视器才继续执行。
notify()
必须在同步代码块(或同步方法)中由对应的同步监视器调用。
⑤notifyAll()
:
唤醒所有在此同步监视器等待的线程,使它们参与同步监视器的竞争。
被唤醒的线程将继续阻塞,直到竞争获得同步监视器才继续执行。
notifyAll()
必须在同步代码块(或同步方法)中由对应的同步监视器调用。
注意:
sleep()
与wait()
的异同:sleep()
和wait()
都能使线程阻塞或限时阻塞。sleep()
使线程阻塞时,线程仍然持有同步监视器;wait()
使线程阻塞时,线程释放同步监视器,并且线程被唤醒之前不能参与同步监视器的竞争。sleep()
可以在线程逻辑执行体中的任何位置调用;wait()
只能在同步代码块或同步方法中调用。
wait()
,wait(long timeoutMillis)
,wait(long timeoutMillis,int nanos)
,notify()
和notifyAll()
是定义在Object
类中的方法,它们只能在同步代码块或同步方法中由对应的同步监视器调用。
实例:
Main.java
public class Main {
//设备1
private static final Object equipment_1 = new Object();
//设备2
private static final Object equipment_2 = new Object();
//当前设备
private static Object currentEquipment = equipment_2;
//零件1加工进度
private static int progress_1 = 0;
//零件2加工进度
private static int progress_2 = 0;
public static void main(String[] args) {
//零件1加工线程
new Thread(new FutureTask<String>(new Callable<String>() {
@Override
public String call() {
for(int i = 1;i <= 100;i++) {
synchronized(equipment_1) {
progress_1++;
System.out.println("零件1加工进度:"+progress_1+"%");
if(progress_2 == 50) {
try{
System.out.println("零件1暂停加工");
equipment_1.wait();
System.out.println("零件1继续加工");
} catch(Exception e) {
System.out.println("some error occur.");
}
}
}
}
System.out.println("零件1加工完成");
return "this is a test.";
}
})).start();
//零件2加工线程
new Thread(new FutureTask<String>(new Callable<String>() {
@Override
public String call() {
for(int i = 1;i <= 100;i++) {
synchronized(currentEquipment) {
progress_2++;
System.out.println("零件2加工进度:"+progress_2+"%");
if(progress_2 == 50) {
System.out.println("零件2占用设备1");
currentEquipment = equipment_1;
}
else if(progress_2 == 80) {
currentEquipment.notify();
currentEquipment = equipment_2;
System.out.println("零件2退出设备1");
}
}
}
System.out.println("零件2加工完成");
return "this is a test.";
}
})).start();
}
}
代码实现:零件1和零件2分别由设备1和设备2加工,对应各自的加工线程。通过加工进度(0-100%
)表示零件的加工过程。零件2进度的51%-80%
需要占用设备1加工。零件1线程同步监视器为equipment_1
,零件2线程同步监视器为可更换的currentEquipment
,起始为equipment_2
。
通过换锁实现零件2对设备1的申请和占用:零件2加工到50%
时,零件1线程必须准确判断零件2的加工进度,否则零件2将继续在设备2中加工,出现错误工序。具体的做法是使零件2线程加工到50%
时暂停等待零件1线程的响应。
代码中暂停零件2线程的做法:零件2加工到50%
时,更换线程同步监视器为equipment_1
,由于此时零件1线程仍然持有equipment_1
,零件2线程进行下一步加工时将被阻塞暂停。在零件1线程响应之前,零件2的加工进度停留在50%
,从而使零件1线程能够及时准确地判断零件2的加工进度。
零件1线程判断零件2加工进度为50%
后,equipment_1
调用wait()
使零件1线程阻塞并释放equipment_1
。之后零件2线程将获得equipment_1
并开始运行。零件2线程进度为80%
时equipment_1
调用notify()
唤醒零件1线程并再次更换线程同步监视器为equipment_2
。之后零件2线程将在equipment_2
中继续执行直到加工完成,零件1线程获得equipment_1
后继续运行直到加工完成。