Java 多线程编程
目录
一、进程与线程
进程
应用程序的执行实例。
有独立的内存空间和系统资源
线程
CPU调度和分派的基本单位
进程中执行运算的最小单位,可完成一个独立的顺序控制流程
简而言之:
一个程序至少有一个进程,一个进程至少有一个线程。
线程的划分尺度小于进程,使得多进程程序的并发性高。
另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。
在Java中,每次程序运行至少启动2个线程:一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM实际上就是在操作系统中启动了一个进程。
二、多线程
(1)、什么是多线程
- 如果在一个进程中同时运行了多个线程,用来完成不同的工作,则称之为“多线程”
- 多个线程交替占用CPU资源,而非真正的并行执行
(2)、多线程好处
- 充分利用CPU的资源
- 简化编程模型
- 带来良好的用户体验
三、主线程
Thread类
- Java提供了java.lang.Thread类支持多线程编程
主线程
- main()方法即为主线程入口
- 产生其他子线程的线程
- 必须最后完成执行,因为它执行各种关闭动作
运行结果:
四、线程的创建和启动
在Java中创建线程的两种方式:
继承java.lang.Thread类
实现java.lang.Runnable接口
使用线程的步骤:
(1)、继承Thread类创建线程
- 创建一个新的类,继承 Thread 类
- 必须重写 run() 方法,该方法是新线程的入口点,编写线程执行体
- 创建线程对象,调用start()方法启动线程,而不是run()方法
package com.zb.one;
/**
* @author XiaChuanKe
* @Description TODO
* @Date 2020/3/16
* @Version V1.0
*/
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 1; i < 11; i++) {
System.out.println(
Thread.currentThread().getName() + ":" + i);
}
}
}
package com.zb.one;
/**
* @author XiaChuanKe
* @Description TODO
* @Date 2020/3/16
* @Version V1.0
*/
public class Test {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); //启动线程
}
}
运行结果:
多个线程交替执行,不是真正的“并行”
线程每次执行时长由分配的CPU时间片长度决定
package com.zb.one;
/**
* @author XiaChuanKe
* @Description TODO
* @Date 2020/3/16
* @Version V1.0
*/
public class Test {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.start();
t2.start();
}
}
运行结果:
(2)、实现Runnable接口创建线程
- 定义MyRunnable类实现Runnable接口
- 实现run()方法,编写线程执行体
- 创建线程对象,调用start()方法启动线程
package com.zb.runnable;
/**
* @author XiaChuanKe
* @Description TODO
* @Date 2020/3/16
* @Version V1.0
*/
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 1; i < 11; i++) {
System.out.println(
Thread.currentThread().getName() + ":" + i);
}
}
}
package com.zb.runnable;
/**
* @author XiaChuanKe
* @Description TODO
* @Date 2020/3/16
* @Version V1.0
*/
public class Test {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread myThread = new Thread(myRunnable, "张三");
Thread myThread1 = new Thread(myRunnable, "李四");
myThread.start();
myThread1.start();
}
}
运行结果:
(3)、直接调用run()和start()区别
(4)、比较两种创建线程的方式
继承Thread类
- 编写简单,可直接操作线程
- 适用于单继承
实现Runnable接口
- 避免单继承局限性
- 便于共享资源
五、线程的生命周期
- 新建状态:
使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
- 就绪状态:
当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
- 运行状态:
如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
- 阻塞状态:
如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
-
等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
-
同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
-
其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
-
- 死亡状态:
一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
六、线程调度
线程调度指按照特定机制为多个线程分配CPU的使用权
方 法 | 说 明 |
setPriority(int newPriority) getPriority() | 更改线程的优先级 获取线程的优先级 |
static void sleep(long millis) | 在指定的毫秒数内让当前正在执行的线程休眠 |
void join() | 等待该线程终止 |
static void yield() | 暂停当前正在执行的线程对象,并执行其他线程 |
void interrupt() | 中断线程 |
boolean isAlive() | 测试线程是否处于活动状态 |
1、线程优先级
线程优先级由1 (Thread.MIN_PRIORITY )~ 10 (Thread.MAX_PRIORITY )表示,1最低,默认优先级为5
优先级高的线程获得CPU资源的概率较大
默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。
package com.zb.runnable;
/**
* @author XiaChuanKe
* @Description TODO
* @Date 2020/3/16
* @Version V1.0
*/
public class Test {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread myThread = new Thread(myRunnable, "张三");
Thread myThread1 = new Thread(myRunnable, "李四");
Thread myThread2 = new Thread(myRunnable, "王五");
myThread.setPriority(Thread.MAX_PRIORITY);
myThread1.setPriority(Thread.MIN_PRIORITY);
myThread2.setPriority(3);
myThread.start();
myThread1.start();
myThread2.start();
}
}
2、线程休眠
让线程暂时睡眠指定时长,线程进入阻塞状态
睡眠时间过后线程会再进入可运行状态
public static void sleep(long millis)
millis为休眠时长,以毫秒为单位
调用sleep()方法需处理InterruptedException异常
package com.zb.thread;
import com.zb.one.MyThread;
/**
* @author XiaChuanKe
* @Description TODO
* @Date 2020/3/16
* @Version V1.0
*/
public class Test {
public static void main(String[] args) {
MyThread1 t1 = new MyThread1();
MyThread1 t2 = new MyThread1();
t1.start();
t2.start();
}
}
class MyThread1 extends Thread {
@Override
public void run() {
for (int i = 1; i < 11; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
3、线程的强制运行
使当前线程暂停执行,等待其他线程结束后再继续执行本线程
public final void join()
public final void join(long mills)
public final void join(long mills,int nanos)
millis:以毫秒为单位的等待时长
nanos:要等待的附加纳秒时长
需处理InterruptedException异常
package com.zb.thread;
import com.zb.one.MyThread;
/**
* @author XiaChuanKe
* @Description TODO
* @Date 2020/3/16
* @Version V1.0
*/
public class Test {
public static void main(String[] args) {
MyThread4 mt = new MyThread4();
try {
for (int i = 0; i < 10; i++) {
if (i == 4) {
mt.start();
mt.join();
}
System.out.println("主线程开始执行--->" + (i + 1));
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class MyThread4 extends Thread {
@Override
public void run() {
try {
for (int i = 0; i < 5; i++) {
System.out.println("强行插入的线程:" + (i + 1));
Thread.sleep(500);
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
运行结果:
4、线程的礼让
暂停当前线程,允许其他具有相同优先级的线程获得运行机会
该线程处于就绪状态,不转为阻塞状态
public static void yield()
package com.zb.thread;
import com.zb.one.MyThread;
/**
* @author XiaChuanKe
* @Description TODO
* @Date 2020/3/16
* @Version V1.0
*/
public class Test {
public static void main(String[] args) {
MyThread5 my5 = new MyThread5();
Thread t1 = new Thread(my5, "线程A");
Thread t2 = new Thread(my5, "线程B");
t1.start();
t2.start();
}
}
class MyThread5 implements Runnable {
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().
getName() + "正在运行:" + i);
if (i == 3) {
System.out.print("线程礼让:");
Thread.yield();
}
}
}
}
运行结果:
操作练习
使用多线程模拟多人徒步爬山
package com.zb.test;
import com.zb.demo.ClimbThread;
public class TestClimb {
public static void main(String[] args) {
// TODO Auto-generated method stub
ClimbThread yong=new ClimbThread(1, 20, "年轻人");
ClimbThread old = new ClimbThread(3, 5, "老年人");
yong.start();
old.start();
}
}
class ClimbThread extends Thread {
private long time;
private int num;
public ClimbThread(long time, int num, String name) {
// TODO Auto-generated constructor stub
super.setName(name);
this.time = time;
this.num = num;
}
@Override
public void run() {
try {
for (int i = 0; i < num; i++) {
System.out.println(super.getName() + "爬了100米");
Thread.sleep(time * 1000);
}
} catch (Exception e) {
// TODO: handle exception
}
System.out.println(super.getName() + "爬完了" + num * 100 + "米");
}
}
模拟叫号看病
package com.test;
public class Test01 {
public static void main(String[] args) {
MyThread01 myThread1 = new MyThread01();
myThread1.setPriority(Thread.MAX_PRIORITY);
myThread1.start();
for (int i = 0; i < 50; i++) {
System.out.println("普通号" + (i + 1) + "号病人在看病");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (i == 9) {
try {
myThread1.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
class MyThread01 extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("特需号:" + (i + 1) + "号病人在看病");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
七、多线程共享数据引发的问题
(1)、同步方法
使用synchronized修饰的方法控制对类成员变量的访问
访问修饰符 synchronized 返回类型 方法名(参数列表){……}
或者
synchronized 访问修饰符 返回类型 方法名(参数列表){……}
synchronized就是为当前的线程声明一个锁
package com.zb.two;
/**
* @author XiaChuanKe
* @Description TODO
* @Date 2020/3/17
* @Version V1.0
*/
public class Test {
public static void main(String[] args) {
// TODO Auto-generated method stub
DataSource dataSource = new DataSource();
Thread zhangsan = new Thread(dataSource, "张三");
Thread huangniu = new Thread(dataSource, "黄牛");
zhangsan.start();
huangniu.start();
}
}
class DataSource implements Runnable {
int num = 0;
int count = 10;
boolean val = true;
@Override
public void run() {
while (val) {
buy();
}
}
public synchronized void buy() {
if (count <= 0) {
val = false;
return;
}
num++;
count--;
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "抢到了第" + num + "张票,还剩" + count + "张");
}
}
运行结果:
(2)、同步代码块
使用synchronized关键字修饰的代码块
synchronized(syncObject){
//需要同步的代码
}
syncObject为需同步的对象,通常为this
效果与同步方法相同
public void run() {
while (true) {
synchronized (this) { //同步代码块
// 省略修改数据的代码......
// 省略显示信息的代码......
}
}
}
多个并发线程访问同一资源的同步代码块时
- 同一时刻只能有一个线程进入synchronized(this)同步代码块
- 当一个线程访问一个synchronized(this)同步代码块时,其他synchronized(this)同步代码块同样被锁定
- 当一个线程访问一个synchronized(this)同步代码块时,其他线程可以访问该资源的非synchronized(this)同步代码
package com.zb.test;
import com.zb.demo.DataSource;
public class TestDataSource {
public static void main(String[] args) {
// TODO Auto-generated method stub
DataSource dataSource = new DataSource();
Thread zhangsan = new Thread(dataSource, "张三");
Thread huangniu = new Thread(dataSource, "黄牛");
zhangsan.start();
huangniu.start();
}
}
class DataSource implements Runnable {
int num = 0;
int count = 10;
boolean val = true;
@Override
public void run() {
System.out.println("代码块");
while (val) {
synchronized (this) {
if (count <= 0) {
break;
}
num++;
count--;
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "抢到了第" + num + "张票,还剩" + count + "张");
}
}
}
}
八、线程安全的类型
ensureCapacityInternal(),它的作用就是判断如果将当前的新元素加到列表后面,列表的elementData数组的大小是否满足,如果size + 1的这个需求长度大于了elementData这个数组的长度,那么就要对这个数组进行扩容。
这样也就出现了第一个导致线程不安全的隐患,在多个线程进行add操作时可能会导致elementData数组越界。具体逻辑如下:
- 列表大小为9,即size=9
- 线程A开始进入add方法,这时它获取到size的值为9,调用ensureCapacityInternal方法进行容量判断。
- 线程B此时也进入add方法,它获取到size的值也为9,也开始调用ensureCapacityInternal方法。
- 线程A发现需求大小为10,而elementData的大小就为10,可以容纳。于是它不再扩容,返回。
- 线程B也发现需求大小为10,也可以容纳,返回。
- 线程A开始进行设置值操作, elementData[size++] = e 操作。此时size变为10。
- 线程B也开始进行设置值操作,它尝试设置elementData[10] = e,而elementData没有进行过扩容,它的下标最大为9。于是此时会报出一个数组越界的异常ArrayIndexOutOfBoundsException.
另外第二步 elementData[size++] = e 设置值的操作同样会导致线程不安全。从这儿可以看出,这步操作也不是一个原子操作,它由如下两步操作构成:
elementData[size] = e;
size = size + 1;
在单线程执行这两条代码时没有任何问题,但是当多线程环境下执行时,可能就会发生一个线程的值覆盖另一个线程添加的值,具体逻辑如下:
- 列表大小为0,即size=0
- 线程A开始添加一个元素,值为A。此时它执行第一条操作,将A放在了elementData下标为0的位置上。
- 接着线程B刚好也要开始添加一个值为B的元素,且走到了第一步操作。此时线程B获取到size的值依然为0,于是它将B也放在了elementData下标为0的位置上。
- 线程A开始将size的值增加为1
- 线程B开始将size的值增加为2
- 这样线程AB执行完毕后,理想中情况为size为2,elementData下标0的位置为A,下标1的位置为B。而实际情况变成了size为2,elementData下标为0的位置变成了B,下标1的位置上什么都没有。并且后续除非使用set方法修改此位置的值,否则将一直为null,因为size为2,添加元素时会从下标为2的位置上开始。
九、常见类型对比
十、线程的通信
通信常用方法:
通信方法 | 描述 |
---|---|
wait() | 一旦执行此方法,当前线程就进入阻塞状态,并释放锁 |
notify | 一旦执行此方法,就会唤醒被wait的一个线程,如果有多个线程,就唤醒优先级高的线程 |
notifyAll | 一旦执行此方法,就会唤醒所有被wait()的线程 |
使用前提:这三个方法均只能使用在同步代码块或者同步方法中。
sleep和wait的异同:
相同点:一旦执行方法以后,都会使得当前的进程进入阻塞状态
不同点:
1.两个方法声明的位置不同,Thread类中声明sleep,Object类中声明wait。
2.调用的要求不同,sleep可以在任何需要的场景下调用,wait必须使用在同步代码块或者同步方法中
3.关于是否释放锁,如果两个方法都使用在同步代码块或同步方法中,sleep不会释放,wait会释放
生产者和消费者
什么是等待/通知机制?
使用wait/notify方法实现线程间通信,要注意以下两点:
(1) wait和notify必须配合synchronized关键字使用
(2)wait方法释放锁,notify方法不释放锁
- 方法wait()的作用就是使当前执行代码的线程进行等待,wait()方法是Object类的方法,该方法用来将当前线程置入“预执行队列”中,并且在wait()所在的代码行处停止执行,直到接到通知或被中断终止。在调用wait()之前,必须要获得该对象的对象级别锁,即只能在同步方法或同步块中调用wait()方法。在执行wait()方法后,当前线程释放锁。在wait返回前,线程与其他线程竞争重新获得锁。如果调用wait()时没有持有适当的锁,则抛出IllegalMonitorStateException,它是RuntimeException的一个子类,因此,不需要try-catch语句进行捕捉异常。
- 方法notify()也要在同步方法或同步块中调用,即在调用前,线程也必须获得该对象的对象级别锁。如果调用notify()时没有持有适当的锁,也会抛出IllegalMonitorStateException。该方法用来通知那些可能等待该对象的对象锁的其他线程,如果有多个线程等待,则由线程规划器随机挑选出其中一个呈wait状态的线程,对其发出通知notify,并使它等待获取该对象的对象锁。需要说明的是,在执行notify()方法后,当前线程不会马上释放该对象锁,呈wait状态的线程也并不能马上获取该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出synchronized代码块后,当前线程才会释放锁,而呈wait状态所在的线程才能获取该对象锁。当第一个获得了该对象锁的wait线程执行运行完毕以后,他会释放掉该对象锁,此时如果该对象没有再次使用notify语句,则即便该对象已经空闲,其他wait状态等待的线程由于没有得到该对象的通知,还会继续阻塞在wait状态,直到这个对象发出一个notify或notifyAll。
package com.zb.three;
/**
* @author XiaChuanKe
* @Description TODO
* @Date 2020/3/17
* @Version V1.0
*/
public class ConsumerAndProducerTest {
public static void main(String[] args) throws InterruptedException {
SyncStack lanzi = new SyncStack();
Producer producer = new Producer(lanzi);
Comsuer comsuer = new Comsuer(lanzi);
producer.start();
Thread.sleep(8000);
comsuer.start();
}
}
class WoTo {
int id;
@Override
public String toString() {
// TODO Auto-generated method stub
return "第" + id + "个窝头";
}
}
/**
* 容器类型
* @author Administrator
*
*/
class SyncStack {
//容器的大小
WoTo[] woTos = new WoTo[6];
//当前存储的标记
int index = 0;
/*生产者调用的存储方法*/
public synchronized void push(WoTo woto) throws Exception {
//当前篮子为满的状态
if (index == woTos.length) {
System.out.println("厨师休息会,没有人吃!");
wait();//当前线程等待
}
//将做好的窝头存储到容器总
woTos[index] = woto;
//标记递增
index++;
//通知正在等待的消费者开始工作
notify();
}
/*消费者方法*/
public synchronized WoTo pop() throws Exception {
//当前容器为空的时候
if (index == 0) {
System.out.println("消费者休息会,没有做出窝头!");
wait();//当前线程停止工作,设置成等待状态
}
index--;//标记递减
//获取容器中最顶层的产品
WoTo woTo = woTos[index];
//容器中有空的位置, 通知生产者继续工作
notify();
return woTo;
}
}
/*生产者*/
class Producer extends Thread {
//需要的容器对象
private SyncStack stack;
public Producer(SyncStack stack) {
this.stack = stack;
}
@Override
public void run() {
try {
for (int i = 0; i < 20; i++) {
//创建产品
WoTo woTo = new WoTo();
woTo.id = i + 1;
//将产品存储到容器中
stack.push(woTo);
System.out.println("厨师做好了" + woTo);
Thread.sleep(1000);
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
/*消费者对象*/
class Comsuer extends Thread {
//容器对象
private SyncStack stack;
public Comsuer(SyncStack stack) {
this.stack = stack;
}
@Override
public void run() {
try {
for (int i = 0; i < 20; i++) {
//返回产品信息
WoTo woto = stack.pop();
System.out.println("消费者吃掉" + woto);
Thread.sleep(3000);
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
运行结果: