多线程
一、线程基础
线程的概念
程序:Program,是一个指令的集合。
进程:Process,(正在执行的程序)是一个静态的概念。
1、进程是程序的一次静态的执行过程,占用特定的地址空间。
2、每个进程都是独立的,由3部分组成cpu,data,code。
3、缺点是内存的浪费,cpu的负担。
线程: 是进程中一个“单一的连续控制流程”/执行路径。
1、线程又被称为轻量级进程。
2、一个进程可拥有多个并行的线程。
3、一个进程中的线程共享相同的内存单元/内存地址空间–》可以相互访问相同的变量和对象,而且它们从同一堆中分配对象–》通信、数据交换、同步操作。
4、由于线程间的通信是在同一地址空间上进行的,所以不需要额外的通信机制,这就使得通信更简便而且信息传递的速度也更快。
进程与线程
一个进程至少有一个线程。
示例代码:
public class ThreadDemo {
public static void main(String[] args) {
for (int i = 0;i < 1000000000;i++){
System.out.println("hello");
}
}
}
- Java虚拟机启动的时候会有一个进程java.exe,该进程中至少有一个线程,在负责java程序的执行。而且这个线程运行的代码存在于main方法中,该线程称之为主线程。
- 一个进程中的线程共享代码和数据空间,线程结束,进程未必结束,但进程结束,线程一定结束。
- 进程包含线程,线程是进程的一部分。
进程与线程的区别
区别 | 进程 | 线程 |
---|---|---|
根本区别 | 作为资源分配的单位 | 调度和执行的单位 |
开销 | 每个进程都是独立的代码和数据空间(进程上下文)。进程间的切换会有较大的开销 | 线程可以看成是轻量级的进程,同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换的开销小。 |
所处环境 | 在操作系统中能同时运行多个任务(程序) | 在同一应用程序中有多个顺序流同时执行 |
分配内存 | 系统在运行的时候会为每个进程分配不同的内存区域 | 除了CPU之外,不会为线程分配内存(线程所使用的资源是它所属的进程的资源),线程组共享数据 |
包含关系 | 没有线程的进程是可以被看作单个线程的,如果一个进程内拥有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的。 | 线程是进程的一部分,所以线程有的时候被称为轻权进程或者轻量级进程。 |
二、线程创建和使用
Java中实现多线程(一)
- 在Java中负责线程的这个功能的是Java.lang.Thread这个类,可以通过创建Thread的实例来创建新的线程。
- 每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,方法run()称之为线程体。
- 通过调用Thread类的start()方法来启动一个线程。
创建线程的方式—》继承Thread类
- 实现多线程的时候:
1、需要继承Thread类。
2、必须要重写run方法,指的是核心执行的程序。
3、线程在启动的时候,不要直接使用run方法,而是要通过start()来进行调用。
4、每次运行相同的代码,出来的结果可能不一样,原因在于多线程谁先抢占资源无法进行人为控制。
示例代码:
public class ThreadDemo extends Thread{
// 重写父类的run方法
@Override
public void run() {
for (int i = 0;i < 10;i++){
System.out.println(Thread.currentThread().getName() + "-------------" + i);
}
}
public static void main(String[] args) {
// 创建一个线程对象
ThreadDemo threadDemo = new ThreadDemo();
// 启动线程使用start方法
threadDemo.start();
for (int i = 0;i < 5;i++){
System.out.println(Thread.currentThread().getName() + "++++++++++++++++" + i);
}
}
}
// 输出的结果是:
// Thread-0-------------0
// Thread-0-------------1
// Thread-0-------------2
// main++++++++++++++++0
// Thread-0-------------3
// main++++++++++++++++1
// Thread-0-------------4
// Thread-0-------------5
// Thread-0-------------6
// main++++++++++++++++2
// Thread-0-------------7
// main++++++++++++++++3
// main++++++++++++++++4
// Thread-0-------------8
// Thread-0-------------9
线程的执行
Java中实现多线程(二)
- 继承Thread类方式的缺点:那就是如果我们的类已经从一个类继承,则无法再继承Thread类。
- 通过Runnable接口实现多线程。
- 优点:可以同时实现继承。实现Runnable接口方式要通用一些。
1)避免单继承。
2)方便共享资源,同一份资源,多个代理访问。
创建线程的方式二—》实现Runnable接口
- 操作步骤:
1、实现Runnable接口。
2、重写run方法。
3、创建Thread对象,将刚创建的runnable的子类实现作为thread的构造参数。
4、通过thread.start()进行启动。
示例代码:
public class RunnableDemo implements Runnable{
// 重写run方法
@Override
public void run() {
for (int i = 0;i < 10;i++){
System.out.println(Thread.currentThread().getName() + "-------------" + i);
}
}
public static void main(String[] args) {
// 创建一个线程对象
RunnableDemo runnableDemo = new RunnableDemo();
Thread thread = new Thread(runnableDemo);
// 启动线程
thread.start();
for (int i = 0;i < 5;i++){
System.out.println(Thread.currentThread().getName() + "++++++++++++++" + i);
}
}
}
// 输出的结果是:
// Thread-0-------------0
// Thread-0-------------1
// main++++++++++++++0
// Thread-0-------------2
// main++++++++++++++1
// Thread-0-------------3
// Thread-0-------------4
// Thread-0-------------5
// Thread-0-------------6
// Thread-0-------------7
// main++++++++++++++2
// Thread-0-------------8
// main++++++++++++++3
// Thread-0-------------9
// main++++++++++++++4
三、线程的代理设计模式
真实角色:潘金莲
代理角色:王婆
实现共同的接口:KindWoman
示例代码:
KindWoman接口
public interface KindWoman {
public void makeEyesWithMen();
public void playWithMen();
}
PanJinLian类
public class PanJinLian implements KindWoman {
@Override
public void makeEyesWithMen() {
System.out.println("潘金莲正在给你抛媚眼");
}
@Override
public void playWithMen() {
System.out.println("潘金莲嘿嘿嘿");
}
}
WanPo类
public class WangPo implements KindWoman {
public KindWoman kindWomen;
public WangPo(){
this.kindWomen = new PanJinLian();
}
public WangPo(KindWoman kindWomen){
this.kindWomen = kindWomen;
}
@Override
public void makeEyesWithMen() {
this.kindWomen.makeEyesWithMen();
}
@Override
public void playWithMen() {
this.kindWomen.playWithMen();
}
}
XiMenQing测试类
public class XiMenQing {
public static void main(String[] args) {
WangPo wangPo = new WangPo();
wangPo.makeEyesWithMen();
wangPo.playWithMen();
}
}
由上述示例可以推测出Threda类和Runnable的子类MyThread都实现了Runnable接口,之后将Runnable的子类MyThread放到Thread之中,测试类调用的是Thread类中的start()方法去启动多线程,实际上具体的执行者是Runnable的子类MyThread中的run()方法中的代码。
真实对象:MyThread
代理对象:Thread
实现共同的接口:Runnable
四、线程状态(生命周期)
线程的生命周期
- 新生状态:当创建好当前线程对象之后,没有启动之前(调用start方法之前)。
ThreadDemo thread = new ThreadDemo;
Runnable run = new Runnable;
- 就绪状态:准备开始执行,并没有执行,表示调用start方法之后;当对应的线程创建完成,且调用start方法之后,所有的线程会添加到一个就绪队列中,所有的线程同时去抢占cpu的资源。
- 运行状态:当当前进程获取到cpu资源之后,就绪队列中的所有线程会去抢占cpu的资源,谁先抢占到谁先执行,在执行的过程中就叫做运行状态;抢占到cpu资源,执行代码逻辑开始。
- 死亡状态:当运行中的线程正常执行完所有的代码逻辑或者因为异常情况导致程序结束叫做死亡状态。
进入的方式:
1)正常运行完成且结束。
2)人为中断执行,比如使用stop方法。
3)程序抛出未捕获的异常。 - 阻塞状态:在程序运行过程中,发生某些异常情况,导致当前线程无法再顺利执行下去,此时会进入阻塞状态,进入阻塞状态的原因消除之后,所有的阻塞队列会再次进入到就绪状态中,随机抢占cpu资源,等待执行。
进入的方式:
1)sleep方法
2)等待io资源
3)join方法(代码中执行的逻辑)
注意: 在多线程的时候,可以实现唤醒和等待的过程,但是唤醒和等待操作的对象不是thread,而是我们设置的共享对象或者共享变量。
线程操作的相关方法
方法名称 | 描述 |
---|---|
public static native Thread currentThread() | 返回目前正在执行的线程 |
public final String getName() | 返回线程的名称 |
public final int getPriority() | 返回线程的优先级 |
public final synchronized void setName(String name) | 设定线程的名称 |
public final boolean isAlive() | 判断线程是否在活动,如果是,返回true,否则返回false |
public final void join() | 调用该方法的线程强制执行,其它线程处于阻塞状态,该线程执行完毕后,其它线程再执行 |
public static void sleep(long millis) | 使用当前正在执行的线程休眠millis秒,线程处于阻塞状态 |
public static void yield() | 当前正在执行的线程暂停一次,允许其它线程执行,不阻塞,线程进入就绪状态,如果没有其它等待执行的线程,这个时候当前线程就会马上恢复执行 |
public final void stop() | 强迫线程停止执行。已经被弃用,不推荐使用 |
示例代码:
public class ThreadApiDemo implements Runnable{
public static void main(String[] args) {
//获取当前线程对象
Thread thread = Thread.currentThread();
//获取当前线程的名称
System.out.println(thread.getName());// main
//获取线程的id
System.out.println(thread.getId());// 1
//获取线程的优先级,一般系统中范围是0~10的值,如果没有经过设置的话,就是默认值5,有些系统是0~100
System.out.println(thread.getPriority());// 5
//设置线程优先级
/*
* 优先级越高一定越先执行吗?
* 不一定,只是优先级执行的概率比较大而已
* */
thread.setPriority(6);
System.out.println(thread.getPriority());// 6
ThreadApiDemo threadApiDemo = new ThreadApiDemo();
Thread t1 = new Thread(threadApiDemo);
System.out.println(t1.isAlive());// false
t1.start();
System.out.println(t1.isAlive());// true
System.out.println(t1.getPriority());// 6
for (int i = 0;i < 5;i++){
System.out.println(Thread.currentThread().getName() + "----------" + i);
}
System.out.println(t1.isAlive());// 可能是ture也可能是false
}
@Override
public void run() {
for (int i = 0;i < 5;i++){
System.out.println(Thread.currentThread().getName() + "----------" + i);
}
}
}
阻塞状态(sleep/yield/join方法)
- 有三种方法可以暂停Thread执行:
线程类:
public class MyRun implements Runnable{
@Override
public void run() {
for (int i = 0;i < 10;i++){
System.out.println(Thread.currentThread().getName() + "---" + i);
}
}
}
1、sleep:不会释放锁,Sleep时别的线程也不可以访问锁定对象。
示例代码:
public class SleepTest {
public static void main(String[] args) {
MyRun run = new MyRun();
Thread thread = new Thread(run);
thread.start();
for (int i = 0;i < 5;i++){
System.out.println(Thread.currentThread().getName() + "-----" + i);
if (i == 2){
try {
Thread.sleep(1000); // 睡1秒
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
/* 输出的结果是:
Thread-0---0
Thread-0---1
Thread-0---2
Thread-0---3
main-----0
Thread-0---4
main-----1
Thread-0---5
main-----2
Thread-0---6
Thread-0---7
Thread-0---8
Thread-0---9
main-----3
main-----4
*/
2、yield:让出CPU的使用权从运行态直接进入就绪态。让CPU重新挑选哪一个线程进入运行状态。
示例代码:
public class YieldTest {
public static void main(String[] args) {
MyRun run = new MyRun();
Thread thread = new Thread(run);
thread.start();
for (int i = 0;i < 5;i++){
if (i == 2){
Thread.yield();
System.out.println(Thread.currentThread().getName() + "-----" + i + "礼让一次");
} else {
System.out.println(Thread.currentThread().getName() + "-----" + i);
}
}
}
}
/* 输出的结果是:
Thread-0---0
Thread-0---1
main-----0
Thread-0---2
main-----1
Thread-0---3
Thread-0---4
Thread-0---5
Thread-0---6
Thread-0---7
Thread-0---8
Thread-0---9
main-----2礼让一次
main-----3
main-----4
*/
3、join:当某一个线程等待另一个线程执行结束后,才继续执行时,使调用该方法的线程在此之前执行完毕,也就是等待调用该方法的线程执行完毕后再往下继续执行。
示例代码:
public class JoinTest {
public static void main(String[] args) {
MyRun myRun = new MyRun();
Thread thread = new Thread(myRun);
thread.start();
for (int i = 0;i < 5;i++){
System.out.println(Thread.currentThread().getName() + "-------" + i);
if (i == 2){
try {
thread.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
/* 输出的结果是:
main-------0
main-------1
Thread-0---0
main-------2
Thread-0---1
Thread-0---2
Thread-0---3
Thread-0---4
Thread-0---5
Thread-0---6
Thread-0---7
Thread-0---8
Thread-0---9
main-------3
main-------4
*/
五、线程的同步与死锁
多线程的安全性问题
public class TicketThread extends Thread{
private int ticket = 5;
@Override
public void run() {
for (int i = 0;i < 100;i++){
if (ticket > 0){
System.out.println(Thread.currentThread().getName() + "正在出售第" + (ticket--) + "张票");
}
}
}
public static void main(String[] args) {
TicketThread t1 = new TicketThread();
TicketThread t2 = new TicketThread();
TicketThread t3 = new TicketThread();
TicketThread t4 = new TicketThread();
t1.start();
t2.start();
t3.start();
t4.start();
}
}
上述代码会暴露两个问题:
1、每次在启动线程对象的时候都会创建自己的对象的属性值,相当于每个线程操作自己,没有真正意义上实现共享。
解决方法:将共享对象,共享变量设置为static。
2、每次访问共享对象的时候,数据不一致了。
解决方法:使用线程同步。
线程同步
- 多线程的运行出现了安全问题
同步的前提:
- 必须有两个或两个以上的线程。
- 必须是多个线程使用同一资源。
- 必须保证同步中只能有一个线程在运行。
解决方式一(同步代码块)
public class TicketRunnable2 implements Runnable{
private int ticket = 5;
@Override
public void run() {
for (int i = 0;i < 100;i++){
try {
Thread.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (this){// 一般将当前对象作为同步对象
if (ticket > 0){
System.out.println(Thread.currentThread().getName() + "正在出售第" + (ticket--) + "张票");
}
}
}
}
public static void main(String[] args) {
TicketRunnable2 ticketRunnable = new TicketRunnable2();
Thread t1 = new Thread(ticketRunnable,"A");
Thread t2 = new Thread(ticketRunnable,"B");
Thread t3 = new Thread(ticketRunnable,"C");
Thread t4 = new Thread(ticketRunnable,"D");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
解决方式二(同步方法)
public class TicketRunnable3 implements Runnable{
private int ticket = 5;
@Override
public void run() {
for (int i = 0;i < 100;i++){
try {
Thread.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
this.sale();
}
}
/*
* 使用同步方法解决多线程数据安全的问题
* */
public synchronized void sale(){
if (ticket > 0){
System.out.println(Thread.currentThread().getName() + "正在出售第" + (ticket--) + "张票");
}
}
public static void main(String[] args) {
TicketRunnable3 ticketRunnable = new TicketRunnable3();
Thread t1 = new Thread(ticketRunnable,"A");
Thread t2 = new Thread(ticketRunnable,"B");
Thread t3 = new Thread(ticketRunnable,"C");
Thread t4 = new Thread(ticketRunnable,"D");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
小结
- 同步监视器
1、synchronized(obj){ }中的obj称为同步监视器。
2、同步代码块中同步监视器可以是任何对象,但是推荐使用共享资源最为同步监视器。
3、同步方法中无需指定同步监视器,因为同步方法的监视器是this,也就是该对象本身。 - 同步监视器的执行过程
1、第一个线程访问,锁定同步监视器,执行其中代码。
2、第二个线程访问,发现同步监视器被锁定,无法访问。
3、第一个线程访问完毕,解锁同步监视器。
4、第二个线程访问,发现同步监视器未锁,锁定并访问。
线程死锁
- 同步可以保证资源共享操作的正确性,但是过多同步也会产生死锁。
- 死锁一般情况下就表示互相等待,是程序运行时出现的一种问题。
线程的生产者和消费者
- 生产者不断生产,消费者不断取走生产者生产的产品。
生产者生产产品放到一个区域中,之后消费者从此区域中取走商品。
实际操作时会发现多线程访问的时候出现了数据安全的问题:
1、生产者没有生产商品,消费者就可以获取。
2、商品的品牌和名称对应不上。
为此我们可以使用线程的同步来解决。
货物类:
public class Goods {
private String brand;
private String name;
private boolean flag = false;
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
//消费者获取商品
public synchronized void get(){
/*
* 如果flag等于false的话,意味着生产者没有生产商品,此时消费者无法消费,需要让消费者进入到阻塞状态,等待生产者生产,当
* 有商品之后,再开始消费
* */
if (!flag){
try {
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("消费者取走了" + this.getBrand() + "------" + this.getName());
flag = false;
//唤醒生产者去进行生产
notify();
}
//生产者去生产商品
public synchronized void set(String brand,String name){
/*
* 当生产者抢占到cpu资源之后会判断当前对象是否有值,如果有的话,意味着消费者还没有消费,需要提醒消费者消费,同时
* 当前线程进入阻塞状态,等待消费者取走商品之后,再次生产,如果没有的话,不需要等待,不需要进入阻塞状态,直接生产即可
* */
if (flag){
try {
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
this.setBrand(brand);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
this.setName(name);
System.out.println("生产者生产了" + this.getBrand() + "------" + this.getName());
//如果代码执行到此处,意味着已经生产完成,需要将flag设置为true
flag = true;
//唤醒消费者去进行消费
notify();
}
}
生产者:
/*
* 生产产品,将产品放置到共享空间中
*
* */
public class Producer implements Runnable{
private Goods goods;
public Producer(Goods goods) {
this.goods = goods;
}
@Override
public void run() {
for (int i = 0;i < 10;i++){
if (i % 2 == 0){
goods.set("华为","P60Pro");
} else {
goods.set("iPhone","15ProMax");
}
}
}
}
消费者:
/*
* 取走商品,将商品从共享空间中取走
*
* */
public class Consumer implements Runnable{
private Goods goods;
public Consumer(Goods goods) {
this.goods = goods;
}
@Override
public void run() {
for (int i = 0;i < 10;i++){
goods.get();
}
}
}
测试类:
public class Test {
public static void main(String[] args) {
Goods goods = new Goods();
Producer producer = new Producer(goods);
Consumer consumer = new Consumer(goods);
Thread t1 = new Thread(producer);
Thread t2 = new Thread(consumer);
t1.start();
t2.start();
}
}
/* 输出的结果是:
生产者生产了华为------P60Pro
消费者取走了华为------P60Pro
生产者生产了iPhone------15ProMax
消费者取走了iPhone------15ProMax
生产者生产了华为------P60Pro
消费者取走了华为------P60Pro
生产者生产了iPhone------15ProMax
消费者取走了iPhone------15ProMax
生产者生产了华为------P60Pro
消费者取走了华为------P60Pro
生产者生产了iPhone------15ProMax
消费者取走了iPhone------15ProMax
生产者生产了华为------P60Pro
消费者取走了华为------P60Pro
生产者生产了iPhone------15ProMax
消费者取走了iPhone------15ProMax
生产者生产了华为------P60Pro
消费者取走了华为------P60Pro
生产者生产了iPhone------15ProMax
消费者取走了iPhone------15ProMax
*/
我们还可以使用java.util.concurrent包下的BlockingQueue类来实现。
货物类:
public class Goods {
private String brand;
private String name;
public Goods(String brand, String name) {
this.brand = brand;
this.name = name;
}
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
生产者:
import java.util.concurrent.BlockingQueue;
public class ProducerQueue implements Runnable{
private BlockingQueue<Goods> blockingQueue;
public ProducerQueue(BlockingQueue blockingQueue) {
this.blockingQueue = blockingQueue;
}
@Override
public void run() {
for (int i = 0;i < 10;i++){
Goods goods = null;
if (i % 2 == 0){
goods = new Goods("华为","P60Pro");
System.out.println("生产者开始生产商品" + goods.getBrand() + "---" + goods.getName());
} else {
goods = new Goods("iPhone","15ProMax");
System.out.println("生产者开始生产商品" + goods.getBrand() + "---" + goods.getName());
}
try {
blockingQueue.put(goods);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
消费者:
import java.util.concurrent.BlockingQueue;
public class ConsumerQueue implements Runnable{
private BlockingQueue<Goods> blockingQueue;
public ConsumerQueue(BlockingQueue blockingQueue) {
this.blockingQueue = blockingQueue;
}
@Override
public void run() {
for (int i = 0;i < 10;i++){
try {
Goods goods = blockingQueue.take();
System.out.println("消费者取走了" + goods.getBrand() + "---" + goods.getName());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
测试类:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class Test {
public static void main(String[] args) {
BlockingQueue<Goods> queue = new ArrayBlockingQueue<Goods>(5);
ProducerQueue producerQueue = new ProducerQueue(queue);
ConsumerQueue consumerQueue = new ConsumerQueue(queue);
new Thread(producerQueue).start();
new Thread(consumerQueue).start();
}
}
/* 输出的结果是:
生产者开始生产商品华为---P60Pro
生产者开始生产商品iPhone---15ProMax
生产者开始生产商品华为---P60Pro
消费者取走了华为---P60Pro
生产者开始生产商品iPhone---15ProMax
消费者取走了iPhone---15ProMax
生产者开始生产商品华为---P60Pro
消费者取走了华为---P60Pro
生产者开始生产商品iPhone---15ProMax
消费者取走了iPhone---15ProMax
生产者开始生产商品华为---P60Pro
消费者取走了华为---P60Pro
生产者开始生产商品iPhone---15ProMax
消费者取走了iPhone---15ProMax
消费者取走了华为---P60Pro
生产者开始生产商品华为---P60Pro
消费者取走了iPhone---15ProMax
生产者开始生产商品iPhone---15ProMax
消费者取走了华为---P60Pro
消费者取走了iPhone---15ProMax
*/
六、线程通信
Java提供了3个方法解决线程之间的通信问题。
方法名 | 作用 |
---|---|
final void wait() | 表示线程一直等待,直到其它线程通知 |
final void wait(long timeout) | 线程等待指定毫秒参数的时间 |
final void wait(long timeout,int nanos) | 线程等待指定毫秒、微秒的时间 |
final void notify() | 唤醒一个处于等待状态的线程 |
final void notifyAll() | 唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先执行 |
注意:以上方法都只能在同步方法或者同步代码块中使用,否则会抛出异常。