什么是线程?
进程的概念
进程是指可执行程序并存放在计算机存储器的一个指令序列,它是一个动态执行的过程。
任务管理器中可以看出有些软件只对应一个进程,而有些软件是由多个进程组成的。
早期的操作系统都是单任务的操作系统,也就是比如QQ、音乐播放器只能有一个在运行。一个运行结束之后才能进行下一个程序的执行。比如在听歌曲,听完了才可以回复QQ好友的消息。
线程的概念
线程是比进程还要小的运行单位,一个进程包含多个线程,线程可以看做是一个子程序。
比如一个程序由很多代码组成的,这些代码可以分成很多块放到不同的线程中分别执行。
在只有一个CPU的时候如何保证多个线程同时执行的呢?一个CPU可以分成很多时间片,时间片的时间可以非常短。比如有音乐播放器、代码编辑器、QQ三个软件同时运行,音乐播放器运行1ms,然后把CPU的使用权转给代码编辑器,代码编辑器运行1ms再把CPU的使用权转给QQ。那么这些程序就轮流在很短的时间使用CPU,对于CPU来说这些软件是轮流运行的,但是由于运行的时间间隔非常短,作为我们使用者来说感觉不到变化,我们就认为这些软件是同时运行的。
通过对CPU 时间的轮转来达到同时运行这样的效果。
线程的创建
- 创建一个Thread类,或者一个Thread子类的对象
- 创建一个实现Runnable接口的类的对象
Thread类
Thread是一个线程类,位于java.lang包下
构造方法 | 说明 |
---|---|
Thread() | 创建一个线程对象 |
Thread(String name) | 创建一个具体指定名称的线程对象 |
Thread(Runnable target) | 创建一个基于Runnable接口实现类的线程对象 |
Thread(Runnable target,String name) | 创建一个基于Runnable接口实现类,并且具有指定名称的线程对象 |
Thread类的常用方法
方法 | 说明 |
---|---|
public void run() | 线程相关的代码写在该方法中,一般需要重写 |
public void start() | 启动线程的方法 |
public static void sleep(long m) | 线程休眠m毫秒的方法 |
public void join() | 优先执行调用join()方法的线程 |
Runnable接口
- 只有一个方法run();
- Runnable是Java中用以实现线程的接口
- 任何实现线程功能的类都必须实现该接口
线程创建
通过继承Thread类的方式创建线程类,重写run()方法
public class MyThread extends Thread{
@Override
public void run() {
super.run();
System.out.println(getName()+":该线程正在执行!");
}
}
public class ThreadTest {
public static void main(String[] args) {
System.out.println("主线程1");
MyThread myThread = new MyThread();
myThread.start();//启动线程
System.out.println("主线程2");
}
}
注意:
- 启动线程时不是调用run()方法,与以往调用方法不同,在线程中是调用start()方法执行线程,启动执行线程的时候还是执行的run()方法中的代码
- 一个线程只能启动一次
运行结果
当前是有两个线程正在运行,一个是main方法的线程是主线程,另一个是MyThread线程。由控制台打印内容可以看出此时"主线程2"输出在MyThread.run()方法前面了,其实执行的顺序是随机的,因为这个线程什么时候获得CPU的使用权我们是不好判断的。
public class ThreadTest {
public static void main(String[] args) {
System.out.println("主线程1");
MyThread myThread = new MyThread();
myThread.start();//启动线程
myThread.start();
System.out.println("主线程2");
}
}
一个线程不能多次启动
public class ThreadTest {
public static void main(String[] args) {
System.out.println("主线程1");
MyThread myThread = new MyThread();
myThread.start();//启动线程
myThread.start();
System.out.println("主线程2");
}
}
并没有编译错误,此时运行会抛出异常,不合逻辑的线程状态,线程不能被多次启动
新建MyThread2.java
public class MyThread2 extends Thread {
public MyThread2(String name) {
super(name);
}
@Override
public void run() {
for(int i=1;i<=10;i++){
System.out.println(getName()+"正在运行"+i);
}
}
}
ThreadTest类中创建两个线程,分别启动
public class ThreadTest {
public static void main(String[] args) {
MyThread2 myThread1 = new MyThread2("线程1");
MyThread2 myThread2 = new MyThread2("线程2");
myThread1.start();
myThread2.start();
}
}
由运行结果可以看出,每次运行顺序是随机的
通过实现Runnable接口的方式创建
为什么要实现Runnable接口?
- Java不支持多继承
- 不打算重写Thread类的其他方法
新建PrintRunnable.java
public class PrintRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"正在运行!");
}
}
public class Test {
public static void main(String[] args) {
PrintRunnable pr1 = new PrintRunnable();
Thread t1 = new Thread(pr1);
t1.start();
PrintRunnable pr2 = new PrintRunnable();
Thread t2 = new Thread(pr2);
t2.start();
}
}
public class PrintRunnable implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 10 ; i++) {
System.out.println(Thread.currentThread().getName()+"正在运行!"+i);
}
}
}
运行结果说明程序的运行具有随机性
修改PrintRunnable中i为成员变量,Test中调用同一个PrintRunnable对象
public class PrintRunnable implements Runnable {
int i = 1;
@Override
public void run() {
while (i <= 10) {
System.out.println(Thread.currentThread().getName()+"正在运行!"+i++);
}
}
}
public class Test {
public static void main(String[] args) {
PrintRunnable pr1 = new PrintRunnable();
Thread t1 = new Thread(pr1);
t1.start();
// PrintRunnable pr2 = new PrintRunnable();
Thread t2 = new Thread(pr1);
t2.start();
}
}
运行结果可以看出程序执行的过程也是随机的。Runnable run()方法中的代码可以被多个线程共享,适用于多个线程处理同一个资源的情况
线程的状态和生命周期
线程的状态
- 新建(New)
创建一个Thread或Thread子类的对象时线程就进入新建状态 - 可运行(Runnable)
线程调用start()方法就进入可运行状态,也叫就绪状态 - 正在运行(Running)
处于可运行状态的线程一旦获取了CPU的使用权就可以立刻进入正在运行状态 - 阻塞(Blocked)
当线程遇到一些干扰时将进入阻塞状态 - 终止 (Dead)
线程的生命周期
sleep方法应用
- Thread类的方法
public static void sleep(long millis)
- 作用:在指定的毫秒数内让正在执行的线程休眠(暂停执行)
- 参数为休眠的时间,单位是毫秒
创建MyThread.java
public class MyThread implements Runnable{
@Override
public void run() {
for (int i=1;i<=15;i++){
System.out.println(Thread.currentThread().getName()+"执行第"+i+"次!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class SleepDemo {
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread t = new Thread(myThread);
t.start();
}
}
运行程序,在不加sleep()之前程序很快就执行完了,在run()方法中加入Thread.sleep()方法后程序每1秒才执行一次
应用场景:
- 计时,每隔一秒显示一下计时的效果
- 定期刷新数据而不是一直刷新数据。比如,实时交通的软件,如果一直刷新用户可能会看到界面一直在闪而且数据变化也不大白白浪费了很多资源,这时候可以调用sleep方法休眠一段时间避免资源消耗。
需要说明的是,到底线程什么时候执行不仅受到sleep()的影响,调用sleep()方法休眠1000ms,当时间终止这个线程并不能马上进入正在运行的状态,而是进入可运行状态。可运行状态的线程只有获取到CPU的使用权以后才能变成运行状态去执行线程,所以实际的时间可能比1000ms要多一些。如果使用这种方式去写时钟类的程序会有误差。
public class SleepDemo {
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread t = new Thread(myThread);
t.start();
Thread t1 = new Thread(myThread);
t1.start();
}
}
运行结果发现,基本上是按照一个线程执行完再执行另外一个线程这样交替执行。执行结果也是具有随机性的,只不过通过sleep()方法去把之前的状态适当改变一下。出现这种交替执行的情况,是因为一个线程执行完一条输出语句就调用sleep()方法休眠了,在它休眠的这段时间里另外一个线程获得CPU使用权的概率就更大一些,所以就会执行Thread-1,Thread-1执行完也会调用sleep()方法休眠,这时候Thread-0的休眠时间就到了,Thread-0就会执行,这样依次类推,就产生了当前的结果。
join方法应用
- Thread类的方法
public final void join()
- 作用:等待调用该方法的线程结束后才能执行
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 1; i <=10 ; i++) {
System.out.println(getName()+"正在执行!"+i+"次!");
}
}
}
public class JoinDemo {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
try {
myThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i=1;i<=5; i++) {
System.out.println("主线程运行第"+i+"次!");
}
System.out.println("主线程运行结束!");
}
}
运行结果可以,Thread-0也就是调用了join()方法的线程先执行了10也就是先执行完,然后才是主线程的循环以及它最后的输出语句再执行。看出join()方法是抢占资源,调用这个方法的资源先执行,这个线程执行完毕其他资源才能执行。
- Thread类的方法
public final void join(long millis)
- 作用:等待该线程终止的最长时间为millis毫秒。
- 如果millis为0则意味着要一直等下去。
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 1; i <=2000 ; i++) {
System.out.println(getName()+"正在执行!"+i+"次!");
}
}
}
public class JoinDemo {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
try {
myThread.join(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i=1;i<=20; i++) {
System.out.println("主线程运行第"+i+"次!");
}
System.out.println("主线程运行结束!");
}
}
运行结果可以看出当它执行到第811次之后执行主线程,然后再执行Thread-0,调用join()方法的线程执行1ms以后就会把主动权让出来。如果join()方法不指定时间其他线程就会一直等待,调用join()方法的线程执行完毕才会执行其他线程。
线程优先级
-
Java为线程类提供了10个优先级
-
优先级可以用整数1-10表示,超过范围会抛出异常
-
主线程默认优先级为5
-
优先级常量
-MAX_PRIORITY:线程的最高优先级10
-MIN_PRIORITY:线程的最低优先级1
-NORM_PRIORITY:线程的默认优先级5
优先级相关的方法
方法 | 说明 |
---|---|
public final int getPriority() | 获取线程优先级的方法 |
public final void setPriority(int newPriority) | 设置线程优先级的方法 |
public class MyThread extends Thread {
private String name;
public MyThread(String name) {
this.name = name;
}
@Override
public void run() {
for (int i=1;i<=10;i++) {
System.out.println("线程"+name+"正在运行"+i);
}
}
}
public class PriorityDemo {
public static void main(String[] args) {
//获取主线程的优先级
int mainPriority = Thread.currentThread().getPriority();
System.out.println("主线程的优先级为:"+mainPriority);
MyThread myThread1 = new MyThread("线程1");
// myThread1.setPriority(10);
myThread1.setPriority(Thread.MAX_PRIORITY);
myThread1.start();
System.out.println("线程1的优先级为:"+myThread1.getPriority());
}
}
运行结果
这个运行过程也是具有随机性的,线程优先级的设置跟操作系统的环境以及CPU的调度方式都是有关系的,所以不能完全保证优先级高的线程一定能先执行,所以要根据每一个具体的环境考虑这个问题。
下面定义两个线程分别设置优先级
public class PriorityDemo {
public static void main(String[] args) {
MyThread myThread1 = new MyThread("线程1");
MyThread myThread2 = new MyThread("线程2");
myThread1.setPriority(Thread.MAX_PRIORITY);
myThread2.setPriority(Thread.MIN_PRIORITY);
myThread1.start();
myThread2.start();
}
}
以下运行结果可以看出线程的运行也是具有随机性的
public static void main(String[] args) {
MyThread myThread1 = new MyThread("线程1");
MyThread myThread2 = new MyThread("线程2");
myThread1.setPriority(Thread.MAX_PRIORITY);
myThread2.setPriority(Thread.MIN_PRIORITY);
myThread2.start();
myThread1.start();
}
由运行结果可以看出,虽然myThread2优先级低但是由于myThread2先启动,也有可能会出现myThread2先运行的情况。
线程同步
多线程运行问题
- 各个线程是通过竞争CPU时间而获得运行机会的
- 各线程什么时候得到CPU时间,占用多久,是不可预测的
- 一个正在运行着的线程在什么地方被暂停是不确定的
银行存取款问题
演示代码
Bank.java银行类,里面有存款和取款操作的方法
@Data
@AllArgsConstructor
public class Bank {
private String account; //账号
private int balance; //账户余额
//存款
public void saveAccount(){
//可以在不同的位置处添加sleep方法
//获取当前的账号余额
int balance = getBalance();
//修改余额,存100元
balance += 100;
//修改账户余额
setBalance(balance);
//输出存款后的账户余额
System.out.println("存款后的账户余额为:"+balance);
}
//取款
public void drawAccount(){
//可以在不同的位置处添加sleep方法
//获取当前的账号余额
int balance = getBalance();
//修改余额,存100元
balance = balance - 200;
//修改账户余额
setBalance(balance);
//输出存款后的账户余额
System.out.println("取款后的账户余额为:"+balance);
}
}
SaveAccount.java存款操作与线程相关
//存款
public class SaveAccount implements Runnable{
Bank bank;
public SaveAccount(Bank bank){
this.bank = bank;
}
@Override
public void run(){
bank.saveAccount();
}
}
DrawAccount.java取款操作与线程相关
//取款
public class DrawAccount implements Runnable{
Bank bank;
public DrawAccount(Bank bank) {
this.bank = bank;
}
@Override
public void run() {
bank.drawAccount();
}
}
Test.java测试类
public class Test {
public static void main(String[] args) {
//创建账户,给定余额为1000
Bank bank = new Bank("1001",1000);
//创建线程对象
SaveAccount sa = new SaveAccount(bank);
DrawAccount da = new DrawAccount(bank);
Thread save = new Thread(sa);
Thread draw = new Thread(da);
save.start();
draw.start();
try {
save.join();
draw.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(bank);
}
}
修改代码
try {
save.join();
draw.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
运行结果都是先存款后取款,可以看出线程的执行过程是随机的。在实际运行时银行账户会同时有很多存取款的线程,当有很多线程同时执行存取款操作时我们能保证存取款操作能正常执行吗?
在Bank.java中的存取款方法中添加sleep()方法模拟代码在执行一部分的时候暂停。现实情况下,到底执行到哪条语句线程终止了是不能确定的。
@Data
@AllArgsConstructor
public class Bank {
private String account; //账号
private int balance; //账户余额
//存款
public void saveAccount(){
//可以在不同的位置处添加sleep方法
//获取当前的账号余额
int balance = getBalance();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//修改余额,存100元
balance += 100;
//修改账户余额
setBalance(balance);
//输出存款后的账户余额
System.out.println("存款后的账户余额为:"+balance);
}
//取款
public void drawAccount(){
//可以在不同的位置处添加sleep方法
//获取当前的账号余额
int balance = getBalance();
//修改余额,存100元
balance = balance - 200;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//修改账户余额
setBalance(balance);
//输出存款后的账户余额
System.out.println("取款后的账户余额为:"+balance);
}
}
运行结果看出,取款后存款账户余额变为1100了,这个问题如果发生在银行系统中是一个非常非常严重的问题。
造成这种现象的原因,取款的线程draw执行完1000-200修改余额然后线程休眠并没有执行更新Bank的balance。然后运行存款线程save,在取得账户余额的时候还是之前的1000并没有被修改,然后继续执行取款线程draw,更新balance并打印出结果800。此时继续执行存款线程save,balance=1000+100,更新Bank的balance为1100
- 为了保证在存款或取款的时候,不允许其他线程对账户余额进行操作
- 需要将Bank对象进行锁定
- 使用关键字synchronized实现
同步
- synchronized关键字用在
— 成员方法
— 静态方法
— 语句块
public synchronized void saveAccount(){}
public static synchronized void saveAccount(){}
synchronized(obj){……}
修改Bank中的saveAccount和drawAccount
//存款
public synchronized void saveAccount(){
//可以在不同的位置处添加sleep方法
//获取当前的账号余额
int balance = getBalance();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//修改余额,存100元
balance += 100;
//修改账户余额
setBalance(balance);
//输出存款后的账户余额
System.out.println("存款后的账户余额为:"+balance);
}
//取款
public void drawAccount(){
synchronized (this){
//可以在不同的位置处添加sleep方法
//获取当前的账号余额
int balance = getBalance();
//修改余额,存100元
balance = balance - 200;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//修改账户余额
setBalance(balance);
//输出存款后的账户余额
System.out.println("取款后的账户余额为:"+balance);
}
}
再次运行发现加入同步块synchronized就解决了这个问题,保证每个方法中的代码在执行过程中是不被打断的,保证了执行完整性。
线程间通信
- 问题:账户余额不够了怎么办?
- 等待存入足够的钱后处理
演示代码
package com.zl.hello.thread.queue;
public class Queue {
private int n;
public synchronized int getN() {
System.out.println("消费:"+n);
return n;
}
public synchronized void setN(int n) {
System.out.println("生产:"+n);
this.n = n;
}
}
package com.zl.hello.thread.queue;
public class Producer implements Runnable {
Queue queue;
public Producer(Queue queue) {
this.queue = queue;
}
@Override
public void run() {
int i=0;
while (true){
queue.setN(i++);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
package com.zl.hello.thread.queue;
public class Consumer implements Runnable {
Queue queue;
public Consumer(Queue queue) {
this.queue = queue;
}
@Override
public void run() {
while (true){
queue.getN();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
package com.zl.hello.thread.queue;
public class Test {
public static void main(String[] args) {
Queue queue = new Queue();
new Thread(new Producer(queue)).start();
new Thread(new Consumer(queue)).start();
}
}
由以上运行结果可以看出不是期望的一生产一消费,生产完3没有被消费。我们可以在Queue中添加一个标记flag。当flag为false的时候表示容器中没有数据,这时候就需要我们调用set()方法生产数据,当生产数据完毕以后flag的值就变为true了。这时候消费者程序get()就可以获取数据了。
- wait()方法:中断方法的执行,使线程等待
等待其实是处于阻塞状态,如果生产者线程等待消费,消费者线程同样也在等待生产,两个线程互相等待永远都不可能再继续执行,这时候就会处于死锁的状态。所以不能让两个线程都去等待,只能是一个等待一个运行。 - notify()方法:唤醒处于等待的某一线程,使其结束等待
- notifyAll()方法:唤醒所有处于等待的线程,使它们结束等待
改写代码
package com.zl.hello.thread.queue;
public class Queue {
private int n;
boolean flag = false;
public synchronized int getN() {
if(!flag){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("消费:"+n);
flag = false; //消费完毕,容器中没有数据
notifyAll();
return n;
}
public synchronized void setN(int n) {
if (flag){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("生产:"+n);
this.n = n;
flag = true; //生产完毕,容器中已经有数据
notifyAll();
}
}