栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

咱就是说,一篇带你搞懂进程&线程---基于Java

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

咱就是说,一篇带你搞懂进程&线程---基于Java

文章目录

基本概念:程序、进程、线程

单核CPU和多核CPU的理解并行与并发使用多线程的优点何时需要多线程 线程的创建和使用

多线程的创建方式一Thread类的有关方法线程的调度多线程的创建方式二两种创建方式的对比线程的分类 线程的生命周期线程的同步Lock锁方式解决线程安全问题线程的通信生产者/消费者问题JDK5.0新增线程创建方式

新增方式一:实现Callable接口新增方式二:使用线程池

基本概念:程序、进程、线程

程序是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。进程是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程(生命周期)。
比如:运行中的QQ、运行中的MP3播放器都是一个进程;
程序是静态的,进程是动态的;
进程作为资源分配的基本单位,系统再运行时会为每个进程分配不同的内存区域。线程:进程可进一步细化为线程,是一个程序内部的一条执行路径。
若一个进程同一时间并行执行多个线程,就是支持多线程的;
线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器,线程切换的开销小;
一个进程中的多个线程共享相同的内存单元/内存地址空间->它们从同一堆中分配对象,可以访问相同的变量和对象,这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能会带来一些安全隐患。 单核CPU和多核CPU的理解

单核CPU其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。如果是多核的话,才能更好的发挥多线程的效率。一个Java应用程序java.exe,至少有三个线程:main()主线程、gc()垃圾回收线程,异常处理线程。异常发生的时候会影响到主线程。 并行与并发

并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事情。并发:一个CPU(采用时间片)“同时”执行多个任务。比如:秒杀系统、多个人做同一件事。 使用多线程的优点

背景: 以单核CPU为例,只使用单个线程先后完成多个任务(调用多个方法),肯定比用多个线程来完成用的时间更短,那为何仍需多线程呢?
多线程程序的优点:

    提高应用程序的响应,对图形化界面更有意义,可增强用户体验。提高计算机系统CPU的使用率。改善程序结构,将既长又复杂的进程分为多个线程,独立运行,利于理解和修改。
何时需要多线程

程序需要同时执行两个或多个任务。程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。需要一些后台运行的程序时。


线程的创建和使用

我们经常写的下面这种代码不是多线程的例子!!!

public static void main(String[] args) {
        fun1();
    }
    public static void fun1(){
        System.out.println("fun1方法执行!");
        fun2();
    }
    public static void fun2(){
        System.out.println("fun2方法执行!");
    }
多线程的创建方式一

Java语言的JVM允许程序运行多个线程,接着我们来看一下多线程的例子:

package com.hpf.test;

//举例:遍历100以内的所有质数
 //1.创建一个继承于Thread类的子类
class  MyThread extends Thread{
    //2.重写Thread类的run()方法
    @Override
    public void run() {
        for(int i=2;i<100;++i){
            boolean flag = true;
            for(int j=2;j<=Math.sqrt(i);++j){
                if(i%j==0){
                    flag = false;
                    break;
                }
            }
            if(flag)
                System.out.print(i+" ");
        }
    }
}
public class ThreadTest {
    //这是主线程
    public static void main(String[] args) {
        //3.创建Thread类的子类的对象
        MyThread thread1 = new MyThread();
        //4.通过此对象调用start()方法
        
        thread1.start();
        
        System.out.println("hello world");
    }
}

练习: 创建两个分线程,其中一个线程遍历10以内的偶数,另一个线程遍历100以内的奇数。

package com.hpf.test;

public class Test {
    public static void main(String[] args) {
        new MyThread1().start();
        new MyThread2().start();
    }
}
class MyThread1 extends Thread{
    @Override
    public void run() {
        for(int i=0;i<=10;++i){
            if(i%2==0)
                System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}
class MyThread2 extends Thread{
    @Override
    public void run() {
        for(int i=0;i<=10;++i){
            if(i%2!=0)
                System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}

或者采取更为简便的方式实现上面的多线程环境:

package com.hpf.test;

public class Test {
    public static void main(String[] args) {
        //创建Thread类的匿名子类
        new Thread(){
            @Override
            public void run() {
                for(int i=0;i<=10;++i){
                    if(i%2==0)
                        System.out.println(Thread.currentThread().getName()+":"+i);
                }
            }
        }.start();
        new Thread(){
            @Override
            public void run() {
                for(int i=0;i<=10;++i){
                    if(i%2!=0)
                        System.out.println(Thread.currentThread().getName()+":"+i);
                }
            }
        }.start();
    }
}

运行结果如图所示:多线程环境当中,每次运行的结果都是不一样的。

Thread类的有关方法

Thread类的特性:

每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体;通过该Thread对象的start()方法来启动这个线程,而非直接调用run()方法。

直接上代码:

package com.hpf.test;


public class Test {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("主线程"+Thread.currentThread().getName()+"正在执行...");
        HelloThread helloThread = new HelloThread();
        helloThread.start();
        for(int i=1;i<=10;++i){
            System.out.println("主线程"+i);
            if(i==5)
                helloThread.join();
        }
        System.out.println("主线程"+Thread.currentThread().getName()+"执行完毕!");

    }
}

class HelloThread extends Thread{
    @Override
    public void run() {
        String name = currentThread().getName();
        System.out.println("分线程" + name+"正在执行...");
        for(int i=0;i<=10;++i){
            if(i%2==0){
                System.out.println("分线程"+i);
            }
        }
        System.out.println(name+"分线程业务执行完毕!");
        try {
            currentThread().sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

执行结果如图所示:

线程的调度

调度策略
1.时间片

2.抢占式:高优先级的线程抢占CPU。

Java的调度方法
1.同优先级线程组成先进先出队列(先到先服务),使用时间片策略;
2.对高优先级,使用有限调度的抢占式策略。

线程的优先级等级:
MAX_PRIORITY:10
MIN_PRIORITY:1
NORM_PRIORITY:5默认的优先级

涉及的方法:
getPriority():返回线程优先值
setPriority(int newPriority):改变线程的优先级
说明:
线程在创建时默认继承父线程的优先级;
低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用。

package com.hpf.test;


public class Test {
    public static void main(String[] args) throws InterruptedException {
        System.out.println(Thread.currentThread().getName()+"主线程,优先级为:"+Thread.currentThread().getPriority()+"正在执行...");
        HelloThread helloThread = new HelloThread();
        helloThread.setPriority(Thread.MAX_PRIORITY);//启动线程之前设置该线程的优先级
        helloThread.start();//启动分线程
        //接着写主线程自己的业务
        for(int i=0;i<=10;++i){
            if(i%2==0)
                System.out.println("获取偶数"+i);
        }
        System.out.println(Thread.currentThread().getName()+"主线程执行完毕!");
    }
}
class HelloThread extends Thread{
    @Override
    public void run() {
        System.out.println(currentThread().getName()+"分线程,优先级为:"+currentThread().getPriority()+"正在执行...");
        for(int i=0;i<=10;++i){
            if(i%2!=0)
                System.out.println("获取奇数"+i);
        }
        System.out.println(currentThread().getName()+"分线程执行完毕!");
    }
}

执行结果如图所示:

说明: 高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。并不意味着只有当高优先级的线程执行完以后,低优先级的线程才执行。

package com.hpf.test;


public class WindowTest {
    public static void main(String[] args){
        TicketThread t1 = new TicketThread();
        t1.setName("窗口一");
        TicketThread t2 = new TicketThread();
        t2.setName("窗口二");
        TicketThread t3 = new TicketThread();
        t3.setName("窗口三");
        t1.start();
        t2.start();
        t3.start();
    }
}

class TicketThread extends Thread{
    private static int ticket = 100;
    @Override
    public void run() {
        while(true){
            if(ticket>0){
                System.out.println(currentThread().getName() + ticket);
                --ticket;
            }else
                break;
        }
    }
}

执行结果如图所示:

说明: 可以看到,票号为10的票卖了三次,存在线程安全问题,待解决。以此引出下面的内容

多线程的创建方式二
package com.hpf.test;


public class Test {
    public static void main(String[] args) throws InterruptedException {
        //3.创建实现类的对象
        HelloThread helloThread = new HelloThread();
        //4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
        Thread t1 = new Thread(helloThread);
        //5.通过Thread类的对象调用start()方法
        t1.start();
    }
}
//1.创建一个实现了Runnable接口的类
class HelloThread implements Runnable{
    //2.实现类去实现Runnable中的抽象方法
    @Override
    public void run() {
        for(int i=0;i<10;++i){
            if(i%2!=0)
                System.out.println(i);
        }
    }
}

我们根据创建线程的方式二再来实现上面卖票的例子:

package com.hpf.test;


public class WindowTest {
    public static void main(String[] args){
        TicketThread ticket = new TicketThread();
        Thread t1 = new Thread(ticket);t1.setName("窗口一");
        Thread t2 = new Thread(ticket);t2.setName("窗口二");
        Thread t3 = new Thread(ticket);t3.setName("窗口三");
        t1.start();
        t2.start();
        t3.start();
    }
}
class TicketThread implements Runnable{
    private int ticket = 10;//注意,这里的变量没有添加static修饰
    @Override
    public void run() {
        while(true){
            if(ticket>0){
                System.out.println(Thread.currentThread().getName() + ticket);
                --ticket;
            }else
                break;
        }
    }
}

执行结果如图所示:

说明: 仍然存在线程安全问题,待解决。敬请跟随本文深入下去…

两种创建方式的对比

开发中:优先选择实现Runnable接口的方式!
原因:
1.实现的方式没有类的单继承性的局限性;
2.实现的方式更适合来处理多个线程有共享数据的情况。
相同点:
两种方式都需要重写run(),将线程要执行的逻辑声明在run()中。

线程的分类

Java中线程分为两类:一种是守护线程,一种是用户线程

它们在几乎每个方面都是相同的,唯一的区别是判断JVM何时离开。守护线程是用来服务用户线程的,通过在start()方法前调用thread.setDaemon(true)可以把一个用户线程变成一个守护线程。Java垃圾回收就是一个典型的守护线程。若JVM中都是守护线程,当前JVM将退出。 线程的生命周期

JDK中用Thread.State类定义了线程的几种状态–>要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:

新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
就绪: 处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
运行: 当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线程的操作和功能
阻塞: 在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
死亡: 线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束

线程的同步

问题的提出:

多个线程执行的不确定性会引起执行结果的不确定;多个线程对账本的共享,会造成操作的不完整性,会破坏数据。

本章节就可以开始解决上面卖票的例子当中出现的线程安全问题了。
1.问题:卖票过程中,出现了重票、错票情况—>出现了线程的安全问题;
2.问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其它线程参与进来,也操作了车票;
3.如何解决:当一个线程A在操作车票的时候,其它线程不能参与进来,直到线程A操作完车票时,其它进程才可以开始操作车票。在这种情形下,即使线程A出现了阻塞,也不能被改变。
4.在Java中,我们通过同步机制,来解决线程的安全问题:

方式1:同步代码块
synchronized(同步监视器){
//需要被同步的代码
}
说明:操作共享数据的代码,即为需要被同步的代码。
共享数据:多个线程共同操作的变量,比如本例子中的车票ticket。
同步监视器,俗称。任何一个类的对象,都可以充当为锁。但是有个大前提,多个线程必须共用(公用)同一把锁。

package com.hpf.test;


public class WindowTest {
    public static void main(String[] args){
        TicketThread ticket = new TicketThread();
        Thread t1 = new Thread(ticket);t1.setName("窗口一");
        Thread t2 = new Thread(ticket);t2.setName("窗口二");
        Thread t3 = new Thread(ticket);t3.setName("窗口三");
        t1.start();
        t2.start();
        t3.start();
    }
}
class TicketThread implements Runnable{
    private int ticket = 10;
    private Object obj = new Object();
    @Override
    public void run() {
        while(true){
            synchronized (obj){
                if(ticket>0){
                    System.out.println(Thread.currentThread().getName() + ticket);
                    --ticket;
                }else
                    break;
            }

        }
    }
}

执行结果如图所示:

总结:同步的方式,解决了线程的安全问题。从另一方面来讲,操作同步代码时,只能有一个线程参与,其它线程等待。相当于是一个单线程的过程,效率比较低。

方式2:同步方法

如果操作共享数据的代码完整的声明在一个方法中,我们可以将此方法设置为同步的。

public class ThreadTest {

	public static void main(String[] args){
		Window window = new Window();
		Thread t1 = new Thread(window);
		t1.setName("窗口一");
		Thread t2 = new Thread(window);
		t2.setName("窗口二");
		t1.start();
		t2.start();
	}
}
class Window implements Runnable{
	private int ticket = 10;
	@Override
	public void run() {
			while(true) {
				show();
			}
		}
	private synchronized void show() {//声明为同步方法
			if(ticket>0) {
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName()+ticket);
				--ticket;
			}
	}
}

总结:
1.同步方法仍然涉及到同步监视器,只是不需要我们显示的声明。
2.非静态的同步方法,同步监视器是this;
静态的同步方法,同步监视器是当前类本身。

Lock锁方式解决线程安全问题

从JDK5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
java.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
ReentrantLock类实现了Lock,它拥有与synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
实例代码如下:

import java.util.concurrent.locks.ReentrantLock;

public class ThreadTest {

	public static void main(String[] args){
		Window window = new Window();
		Thread t1 = new Thread(window);
		t1.setName("窗口一");
		Thread t2 = new Thread(window);
		t2.setName("窗口二");
		t1.start();
		t2.start();
	}
}

class Window implements Runnable{
	private int ticket = 10;
	//1.实例化ReentrantLock
	private ReentrantLock lock = new ReentrantLock();
	@Override
	public void run() {
			while(true) {
				try {
					//2.调用lock()
					lock.lock();
					if(ticket>0) {
						try {
							Thread.sleep(100);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
						System.out.println(Thread.currentThread().getName()+ticket);
						ticket--;
					}else {
						break;
					}
				} finally {
					lock.unlock();
				}
			}
		}
}

面试题:synchronized与Lock的异同?
相同:二者都可以解决线程安全问题;
不同:
synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器,Lock需要手动的启动同步,同时结束同步也需要手动的实现。
Lock只有代码块锁,synchronized有代码块锁和方法锁两种。

线程的通信

举例:两个线程交替打印窗口的票号。

public class ThreadTest {
	public static void main(String[] args){
		Window window = new Window();
		Thread t1 = new Thread(window);
		t1.setName("窗口一");
		Thread t2 = new Thread(window);
		t2.setName("窗口二");
		t1.start();
		t2.start();
	}
}

class Window implements Runnable{
	private int ticket = 10;
	@Override
	public void run() {
			while(true) {
				synchronized (this) {
					notify();//唤醒进程
					if(ticket>0) {
						System.out.println(Thread.currentThread().getName()+ticket);
						ticket--;
						try {
							wait();//阻塞当前进程
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					}else {
						break;
					}
				}
			}
		}
}

执行结果如下:

线程通信涉及到的三个方法:

wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。notify():一旦执行此方法,就会唤醒wait的一个线程,如果有多个线程在wait,就唤醒优先级高的那个线程。notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。

说明: 以上的三个方法必须使用在同步代码块或者同步方法中!这些方法的调用者必须是同步代码块或者同步方法中的同步监视器!
面试题:sleep()和wait()的异同?
1.相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
2.不同点:
1)两个方法声明的位置不同:Thread类中声明sleep(),Object类中声明wait();
2)调用的要求不同:sleep()可以在任何需要的场景下调用,wait()必须使用在同步代码块或同步方法中;
3)是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。

生产者/消费者问题

例:生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20)。如果生产者试图生产更多的产品,店员会叫生产者停一下;如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下;如果店中有产品了再通知消费者来取走产品。
这里可能出现两个问题:
生产者比消费者快时,消费者会漏掉一些数据没有取到;
消费者比生产者快时,消费者会取相同的数据。

class Clerk{
	private int productCount = 0;
	//生产产品
	public synchronized void produceProduct() {
		if(productCount<20) {
			productCount++;
			System.out.println(Thread.currentThread().getName()+":开始生产第"+productCount+"号产品");
			notify();
		}else {
			//阻塞当前进程,让消费者进程开始执行
			try {
				wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
	
	//消费产品
	public synchronized void consumeProduct() {
		if(productCount>0) {
			System.out.println(Thread.currentThread().getName()+":开始消费第"+productCount+"号产品");
			productCount--;
			notify();
		}else {
			//阻塞当前进程,让生产者进程开始执行
			try {
				wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}
class Productor extends Thread{
	private Clerk clerk;
	public Productor(Clerk clerk) {
		this.clerk = clerk;
	}
	@Override
	public void run() {//这里写业务逻辑代码
		System.out.println(this.getName()+":开始生产产品...");
		while(true) {
			try {
				Thread.sleep(500);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			clerk.produceProduct();
		}
	}
}
class Customer extends Thread{
	private Clerk clerk;
	public Customer(Clerk clerk) {
		this.clerk = clerk;
	}
	@Override
	public void run() {//这里写业务逻辑代码
		System.out.println(this.getName()+":开始消费产品...");
		while(true) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			clerk.consumeProduct();
		}
	}
}
public class ProductTest {
	public static void main(String[] args) {
		Clerk clerk = new Clerk();
		Productor p1 = new Productor(clerk);
		p1.setName("生产者1");
		Customer c1 = new Customer(clerk);
		c1.setName("消费者1");
		p1.start();
		c1.start();
	}
}
JDK5.0新增线程创建方式 新增方式一:实现Callable接口

与Runnable相比,Callable功能更强大一些!

相比run()方法,可以有返回值;

方法可以抛出异常;

支持泛型的返回值;

需要借助FutureTask类,比如获取返回结果。

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

class NumThread implements Callable{
	@Override
	public Object call() throws Exception {
		int sum = 0;
		for(int i=0;i<10;++i) {
			if(i%2==0)
				sum += i;
		}
		return sum;
	}
}

public class ThreadTest {
	public static void main(String[] args){
		NumThread numThread = new NumThread();
		FutureTask futureTask = new FutureTask(numThread);
		new Thread(futureTask).start();
		try {
			//get()返回值即为FutureTask构造器参数Callable实现类重写的call()
			Object sum = futureTask.get();
			System.out.println(sum);
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (ExecutionException e) {
			e.printStackTrace();
		}
	}
}

如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式更强大?
1.call()可以有返回值;
2.call()可以抛出异常,可以被外面的操作捕获,获取异常的信息;
3.Callable接口是支持泛型的。

新增方式二:使用线程池

背景: 经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
思路: 提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。
好处:

提高响应速度(减少了创建新线程的时间)

降低资源消耗(重复利用线程池中的线程,不需要每次都创建)

便于线程管理
corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAliveTime:线程没有任务时最多保持多长时间后会终止

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class NumThread implements Runnable{
	@Override
	public void run() {
		for(int i=0;i<10;++i) {
			if(i%2==0)
				System.out.println(i);
		}
	}
}

public class ThreadTest {
	public static void main(String[] args){
		ExecutorService service = Executors.newFixedThreadPool(10);
		service.execute(new NumThread());//适用于Runnable
		//service.submit();//适用于Callable
		service.shutdown();//关闭线程池
	}
}

给看完到最后的你一个大大的赞!

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/736470.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号