导学
多线程编程是Java语言最为重要的特性之一,利用多线程技术可以提升单位时间内的程序处理性能,也是现代程序开发中高并发的主要涉及形式。 使用多线程只有一个目的,那就是更好的利用cpu的资源。
并行和并发
- 并行:多个CPU实例或是多台机器同时执行一段处理逻辑,是真正的同时。
并发:通过CUP调度算法,让用户看上去同时去执行,实际上从CPU操作层面并不是真正的同时。并发往往需要公共的资源,对公共资源的处理和线程之间的协调是并发的难点。
进程和线程
进程是指可执行程序并存放在计算机存储器的一个指令序列,他是一个程序的动态执行的过程,经历了从代码加载,执行到执行完成的一个完整过程。
- 线程是指进程中的一个执行流程,一个进程中可以运行多个线程,一个线程是一个子进程。
案例: 日常使用电脑,一边写代码一遍听歌,有时候还会去打开微信回复一下朋友的信息 这样一个场景,我们使用了三个软件,音乐播放器,代码编辑器和微信。这三个软件同时工作,就是我们的进程在起作用。
早期的操作系统,是单任务操作系统,也就是只有一个程序运行结束后,才能进行下一个程序的执行。而现在的操作系统则多任务操作系统,可以同时运行多个程序。
线程相等于子程序,代码可以分成多个代码块,放在多个线程中去执行 案例: 我们使用Eclipse编写代码的时候,Eclipse会在我们代码错误的地方给出红色浪线。每次启动Eclipse对于操作系统而言就是启动了一个系统的进程,而在这个进程上又有其他的子程序(线程)——代码检查。 如果关闭Eclipse,代码检查的线程肯定也会关闭,但是如果代码检查的线程消失了,却不一定会让Eclipse的进程消失
时间片机制
多任务操作系统同时运行多个进程(程序),是因为CPU具备分时机制,所以每个进程都能循环获得自己的CPU时间片,由于CPU执行速度非常快,使得所有程序好像是在同时运行一样。
实际上,通俗的理解就是CPU每隔一段时间(这个时间非常短且不一定固定),会将当前进程切换到其他进程,使这些进程轮番穿插运行。
因为这个时间非常短,所以我们会认为这些程序是同时进行的。
线程的创建
线程的创建有三种方式
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
可以通过实现Callable接口,重写call()方法
常用的创建线程的方法就是继承Thread类和实现Runable接口,第三种方法自学完善。
继承Thread类
Thread是一个线程类,位于java.lang包下
Thread类常用构造方法
构造方法 | 说明 |
---|---|
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()方法的线程 |
使用Thread类创建线程
public class MyThread extends Thread {
@Override
public void run() {
System.out.println(getName() + " say:Hello,World");
}
}
//该段程序中共有两个线程,一个是主线程,还有一个则是我们自己创建的myThread线程
//main方法是一个主线程,程序的启动都是由main方法来启动的,如有兴趣可以了解下用户线程和守护线程
public static void main(String[] args) {
System.out.println("主线程1");
MyThread myThread = new MyThread();
//使用start方法启动线程,执行的是run方法中的代码
myThread.start();
//线程是不能被多次启动的,否则会报不合逻辑的线程错误
//只能启动一次
//myThread.start();
System.out.println("主线程2");
}
输出结果:最后的结果其实是不可预知的,因为我们不知道什么时候线程才会获得CPU的使用权
主线程1
主线程2
Thread-0 say:Hello,World
public class MyThread extends Thread {
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for(int i = 0; i < 10; i++) {
System.out.println(getName() + "线程正在循环第" + i + "次");
}
}
}
public class ThreadTest {
public static void main(String[] args) {
MyThread mtf = new MyThread("我的线程1");
mtf.start();
MyThread mts = new MyThread("我的线程2");
mts.start();
}
}
输出结果:
随机的结果
实现Runnable接口
- 只有一个方法run()
- Runnable是Java中用以实现线程的接口
- 任何实现线程功能的类都必须实现该接口
为什么要使用Runnable接口
- Java不支持多继承
- 不打算重写Thread类的其他方法
使用Runnable创建线程
public class MyRunnable implements Runnable{
@Override
public void run() {
//打印该线程的当前线程名称
System.out.println(Thread.currentThread().getName()+"say:Hello,World");
}
}
public class MyRunnableUse {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
MyRunnable myRunnable1 = new MyRunnable();
Thread thread1 = new Thread(myRunnable1);
thread1.start();
}
}
运行结果:
Thread-1say:Hello,World
Thread-0say:Hello,World
其实也是随机的
public class MyRunnable implements Runnable{
@Override
public void run() {
int i = 1;
while(i <= 10) {
//打印该线程的当前线程名称
System.out.println(Thread.currentThread().getName()+"say:Hello,World" + i);
i++;
}
}
}
public class MyRunnableUse {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
MyRunnable myRunnable1 = new MyRunnable();
Thread thread1 = new Thread(myRunnable1);
thread1.start();
}
}
Runnable中的代码可以被多个线程共享,适用于多个线程共享一个资源的场景
线程的生命周期
线程存在五个状态:
- 创建状态(new)
- 就绪状态(runnable)
- 运行状态(running)
- 阻塞状态(blocked)
- 终止状态(dead)
需要注意的是,stop()方法现已不建议使用了
状态详解
- 创建状态
在程序中使用构造方法创建一个线程对象后,新的线程对象便处于新建状态。此时,它已经有了相应的内存空间和其他资源,但还是处于不可运行状态。
比如可以Thread myThread = new Thread();
- 就绪状态
新建线程对象后,调用该线程的start()
方法就可以启动线程,当线程启动的时候,线程就进入就绪状态。此时线程进入线程队列排队,等待CPU调度服务。 - 运行状态
当就绪状态的线程被调用并获得处理器资源时,线程就进入了运行状态。此时将自动调用该线程对象的run()
方法。 - 阻塞状态
一个正在运行的线程在某些特殊情况下,将让出CPU并暂时停止自己的运行,进入阻塞状态。阻塞时,线程不能进入排队队列,只有当引起阻塞的原因被消除后,线程才会转入就状态 - 终止状态
当线程体中的run()
方法运行结束后,线程即处于终止状态,处于终止状态的线程不具有继续运行的能力线程休眠-sleep()方法的使用
休眠会使线程进入休眠状态sleep()
方法是Thread
类的静态方法,该方法无返回值。
作用:在指定的毫秒数内让正在执行线程休眠(暂停执行)
参数:参数为休眠的时间,单位是毫秒
使用场景:public class MyRunnable implements Runnable{
@Override
public void run() {
//for(int i=0;i<=30;i++)
int i = 1;
while(i <= 30) {
//打印该线程的当前线程名称
System.out.println(Thread.currentThread().getName()+"say:Hello,World" + i);
//休眠1000毫秒后,线程并不是立即进入可运行状态,而是要等待CPU的调度,所以如果使用sleep写一个时钟类的应用可能会有一些误差
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
i++;
}
}
}
public class MyRunnableUse {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
Thread thread1 = new Thread(myRunnable);
thread1.start();
}
}
- 比如我们可以使用
sleep()
方法计时30秒钟,然后一秒一秒的输出倒计时 - 在交通类的应用中可以使用
sleep()
方法定期刷新数据,获取最新的交通信息使用sleep()方法在休眠相应时间后,转为可运行状态,在获取cpu使用权后进入运行状态;这个方法可能会发生InterruptedException异常,需要强制性的使用try-catch捕获。
线程强制执行-join()方法
在多线程并发执行中每一个线程对象都会交替还行,如果此时某个线程对象中的内容非常重要,需要优先执行完成,则可以设置为强制执行,待其执行完毕后其他线程再继续执行。public final void join()
方法优先执行,抢占资源,该线程执行完后其他线程才能执行,该方法不允许重写。public final void join(long millis)
可带参数,等待该进程的最长时间(即该线程占用资源的时间),时间完成后该线程不再拥有优先权,正常执行
public class MyThread extends Thread {
@Override
public void run() {
System.out.println(getName() + "线程正在执行");
}
}
public class ThreadTest {
public static void main(String[] args) {
MyThread mtf = new MyThread();
mtf.start();
try {
mtf.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程运行结束");
}
}
public class MyThread extends Thread {
@Override
public void run() {
for(int i = 0; i < 10; i++) {
System.out.println(getName() + "线程正在执行" + i + "次");
}
}
}
public class ThreadTest {
public static void main(String[] args) {
MyThread mtf = new MyThread();
mtf.start();
try {
mtf.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i = 0; i < 20; i++) {
System.out.println("主线程正在运行第" + i + "次");
}
System.out.println("主线程运行结束");
}
}
使用join()方法也可能会发生InterruptedException异常,需要强制性的使用try-catch捕获。
线程礼让-yield()方法
线程礼让是指当满足某些条件的时候可以将当前的CPU调度让给其他线程执行。如果某些不是很重要的线程抢占到资源但又不急于执行时,就可以将当前的CPU资源礼让出去,交由其他线程先执行。
yield方法定义如下:public static void yield()
作用:表示当前线程对象提示调度器自己愿意让出CPU资源,但是调度器可以自由忽略。
public class MyThread extends Thread {
@Override
public void run() {
for(int i = 0; i < 10; i++) {
if(i == 3) {
yield();
}
System.out.println(getName() + "线程正在执行" + i + "次");
}
}
}
public class ThreadTest {
public static void main(String[] args) {
MyThread mtf = new MyThread();
mtf.start();
for(int i = 0; i < 20; i++) {
System.out.println("主线程正在运行第" + i + "次");
}
System.out.println("主线程运行结束");
}
}
sleep()和yield()的区别
- 都能是调用的线程放弃CPU,把运行机会给其他线程
- sleep()会给其他线程的运行机会,且不考虑优先级,但是yield()只会给同优先级或更高优先级的线程运行的机会(不一定能运行)
调用sleep()后,线程进入阻塞状态,而调用yield()后进入就绪状态(随时等待JVM的再次调用)
线程优先级
所有创造的线程都是子线程,所有的子线程在启动时都会保持同样的优先权限。但是如果某些重要的线程希望可以优先抢占到CPU资源并且先执行,就可以通过修改线程的优先级实现。
哪个线程的优先级越高,哪个线程就有可能会先执行。
线程优先级:Java为线程类提供了10个优先级
- 优先级可以用整数1-10表示,超出范围会抛出异常
- 主线程默认优先级为5
- 可以使用优先级常量表示优先级 | 方法或常量 | 类型 | 描述 | | :—- | :—-: | :—- | | public static final in MAX_PRIORITY | 常量 | 最高优先级,数值为10 | | public static final in NORM_PRIORITY | 常量 | 中等优先级,数值为5 | | public static final in MIN_PRIORITY | 常量 | 最低优先级,数值为1 | | public final void setPriority(int newPriority) | 普通 | 设置线程优先级 | | public final int getPriority() | 普通 | 取得线程优先级 |
public class MyThread extends Thread {
private String name;
public MyThread(String name) {
this.name = name;
}
@Override
public void run() {
for(int i = 0; i < 10; i++) {
System.out.println("线程:" + name + "正在执行第" + i + "次");
}
}
}
public class ThreadTest {
public static void main(String[] args) {
//获得主线程的优先级
//int mainPriority = Thread.currentThread().getPriority();
//System.out.println("主线程优先级为:" + mainPriority);
MyThread mtf = new MyThread("子线程1");
MyThread mts = new MyThread("子线程2");
//mtf.setPriority(10);
//使用常量设置线程优先级
mtf.setPriority(Thread.MAX_PRIORITY);
mts.setPriority(Thread.MIN_PRIORITY);
mtf.start();
mts.start();
//System.out.println("子线程1的优先级为:" + mtf.getPriority());
}
}
优先级的设置与操作系统的环境和CPU的工作方式都是有很大的关系的。
线程同步
多线程运行问题
在之前的学习中,我们发现在线程的运行过程中,线程什么时候运行是不确定的。
多线程运行问题总结:
- 各个线程是通过竞争cpu时间来获取运行机会的;
- 各线程什么时候得到cpu时间占用多久,是不可预测的;
- 一个正在运行着的线程在什么时候被暂停是不确定的。
多线程的这些问题,在具体的开发中也会带来不可预测的结果。
场景:公司有一个公共的银行账号,在这个账号中经常进行存取款操作。由于存取款操作可能同时进行,就有可能发生一定的风险。使用以下代码模拟这样一个过程
public class Bank {
private String account;//账户
private int balance;//余额
public String getAccount() {
return account;
}
public void setAccount(String account) {
this.account = account;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
public Bank(String account, int balance) {
super();
this.account = account;
this.balance = balance;
}
public Bank() {
super();
}
@Override
public String toString() {
return "Bank [账户=" + account + ", 余额=" + balance + "]";
}
/**
* 存款方法
*/
public void saveAccount() {
//可以在不同的地方添加sleep()方法
//获取当前账户余额
int balance = getBalance();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//修改存款
balance += 100;
//修改账户余额
setBalance(balance);
//输出修改后的账户余额
System.out.println("输出存款后的账户余额" + balance);
}
/**
* 取款方法
*/
public void drawAccount() {
//可以在不同的地方添加sleep()方法
//获取当前的账户余额
int balance = getBalance();
//修改余额
balance = balance - 200;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//修改账户余额
setBalance(balance);
//输出
System.out.println("取款后的账户余额" + balance);
}
}
/**
* 存款
* @author LiXinRong
*
*/
public class SaveAccount implements Runnable {
Bank bank;
public SaveAccount(Bank bank) {
this.bank = bank;
}
@Override
public void run() {
bank.saveAccount();
}
}
/**
* 取款
* @author LiXinRong
*
*/
public class DrawAccount implements Runnable{
Bank bank;
public DrawAccount(Bank bank) {
this.bank = bank;
}
@Override
public void run() {
bank.drawAccount();
}
}
public class SaveAndDrawDemo {
public static void main(String[] args) {
//创建账户,给定账户余额
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 {
//这里设置join方法,是为了先执行存取款线程,最后执行代码末尾的打印银行语句
save.join();
draw.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(bank);
}
}
当没有对Bank类中的进行数据及时更新时,就有可能会造成运行的错误。尤其是我们今后在设计有关钱的代码时,需要慎之又慎。
线程同步(线程互斥)处理
造成并发资源访问不同步的主要原因在于没有将若干个程序逻辑单元进行整体的锁定,即当判断数据和修改数据时,只允许一个线程进行处理,而其他线程需要等待当前线程执行完毕后才可以继续执行,这样就使得同一个时间段内,只允许一个线程执行操作,从而实现同步的处理。
就相当于,我们给CPU加了一把锁
使用synchronized关键字,可以实现同步处理。
同步的关键是要为代码加上“锁”,对于锁的操作程序有两种:同步代码块,同步方法(成员方法、静态方法);
同步方法与同步代码块
/**
* 存款方法
*/
public synchronized void saveAccount() {
//可以在不同的地方添加sleep()方法
//获取当前账户余额
int balance = getBalance();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//修改存款
balance += 100;
//修改账户余额
setBalance(balance);
//输出修改后的账户余额
System.out.println("输出存款后的账户余额" + balance);
}
/**
* 取款方法
*/
public void drawAccount() {
synchronized(this) {
//可以在不同的地方添加sleep()方法
//获取当前的账户余额
int balance = getBalance();
//修改余额
balance = balance - 200;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//修改账户余额
setBalance(balance);
//输出
System.out.println("取款后的账户余额" + balance);
}
}
当使用同步代码块的时候,当多个线程并发执行时,只允许一个线程执行此部分内容,从而实现同步处理操作。
同步代码块可以直接定义在某个方法中,使得方法的部分操作进行同步处理,但是如果某个方法中的全部操作都需要进行同步处理,则可以采用同步方法的形式进行定义。
同步会造成处理性能下降 同步操作的本质在于同一个时间段内只允许有一个线程运行,所以在此线程对象未执行完的过程中其他线程对象将处于等待状态,这样就会造成程序处理性能下降。但是同步也带来一些优点:数据的线程访问安全。
线程死锁
死锁是在多线程开发中较为常见的一种不确定出现的问题,其所带来的影响计时导致程序出现“假死”状态。
同步保证了一个线程要等待另外一个线程执行完毕才会继续执行,虽然在一个程序中,使用同步可以办证资源共享操作的正确性,但是过多的同步也会产生问题。
例如:
现在张三想要李四的画,李四想要张三的书,那么张三对李四说:“把你的画给我,我就给你书”。李四也对张三说:“把你的书给我,我就给你画”。此时,张三在等李四的回答,李四在等张三的回答。最终的结果,就是张三得不到李四的画,李四得不到张三的书。
所谓死锁,是指两个线程都在等待对方先完成,造成程序的停滞状态,一般程序的死锁都是在程序运行时出现的。
在开发过程中回避线程的死锁问题,是设计的难点。
线程通信
在我们之前的例子中,我们存在一个问题。当我们的账户余额不够了怎么办?我们就需要等待存入足够的钱后才能去处理。接下来,我们采用一个更直观更经典的一个例子说明
经典案例-生产者与消费者
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;
}
}
public class Producer implements Runnable{
//共享queue类
Queue queue;
public Producer(Queue queue) {
this.queue = queue;
}
@Override
public void run() {
int i = 0;
while(true) {
i++;
//对queue类中的n进行赋值
queue.setN(i);
//模拟实际生产中线程被打断
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
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();
}
}
}
}
public class PCTest {
public static void main(String[] args) {
Queue queue = new Queue();
Producer pd = new Producer(queue);
Consumer cs = new Consumer(queue);
Thread threadP = new Thread(pd);
Thread threadC = new Thread(cs);
threadP.start();
threadC.start();
//new Thread(new Producer(queue)).start();
}
}
本案例实现了一个基础的线程交互模型,但是通过执行结果,可以发现程序中存在两个问题。
- 数据错位
如果没有采用同步的话,生产者线程刚刚生产了数据,但是消费者线程取出的数据却不是刚刚生产出来的数据 - 重复操作
生产者生产了若干的数据,消费者才开始取出数据;或者是消费者去完一个数据后又接着取出数据。Object线程等待与唤醒
重复操作问题的解决,需要引入线程的等待与唤醒机制,而这一机制我们可以通过Object类完成。Object类中定义有线程的等待与唤醒支持。我们主要要掌握Object类中的
wait()
,notify()
,notifyAll()
三个方法。
方法 | 说明 |
---|---|
public final void wait() throws InterruputedException | 线程的等待 |
public final void wait(long timeout) throws InterruputedException | 设置线程等待毫秒数 |
public final void wait(long timeout,int nanos) throws InterruputedException | 设置线程等待毫秒数和纳秒数 |
public final void notify() | 唤醒某一个等待线程,使其结束等待 |
public final void notifyAll() | 唤醒全部等待线程,使它们结束等待 |
接下来,修改一下我们的程序吧
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;//消费完毕,容器中没有数据
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;//生产完毕,容器中已经有数据
}
}
在我们修改完这样的程序后,我们会发现我们的程序有可能会进入一个死锁状态。因为我们在代码中没有明确的唤醒处于等待状态的线程。
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();
}
}