Java 多线程开发

本文深入探讨了Java中的程序、进程和线程概念,阐述了线程的生命周期、执行控制、同步机制以及线程池的应用。通过示例展示了线程的创建、启动、礼让、优先级设置以及线程间的通信,解释了死锁问题和生产者-消费者模型。同时,文章还提及了守护线程和线程池的使用,以提升系统性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、程序、进程与线程

程序(program)是为完成特定任务、用某种语言编写的一组指令的结合。即指一段静态的代码,静态对象。

进程(process)是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过称。——生命周期。进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。

线程(thread)
进程可以进一步细化为线程,是一个程序内部的一条执行路径。若一个进程同一时间并行执行多个线程,就是支持多线程的。线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小。
一个进程中的多个线程共享相同的内存单元/内存地址空间->它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。

单核CPU与多核CPU。单核CPU,其实就是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。例如:虽然有多车道,但是收费站只有一个工作人员在收费,只有收了费才能通过。如果是多核的话,才能更好的发挥多线程的效率。(现在的服务器都是多核的)
一个Java应用程序java.exe,其实至少有三个线程:mian()主线程,gc()垃圾回收线程,异常处理线程。

并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀,多个人做同一件事。

使用多线程的优点?
一单核CPU为例,只使用单个线程先后完成多个任务(调用多个方法),肯定比用多个线程来完成用的时间更短,为何仍需多线程呢?
多线程的优点:
1、提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
2、提高计算机系统CPU的利用率。
3、改善程序结构。将即长又复杂的进程分为多个线程,独立运行,利于理解与修改。

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

2、线程的创建与使用

在JDK1.5之前创建新执行线程的方式有两种:
(1)继承Thread类
(2)实现Runnable接口
(3)线程的启动

通过继承Thread类创建线程实例

public class TestThread1 extends Thread{

    @Override
    public void run() {
        System.out.println("执行run()");
        super.run();
    }

    public static void main(String[] args) {
        TestThread1 t1= new TestThread1();
        t1.start();//开启线程,由CPU调度线程的执行。这个执行过程是随机的,不可人为干预的。
        System.out.println("执行main()");//main方法执行体
    }
}

通过实现Runnable接口创建线程实例

//实现Runnable接口创建接口
public class TestThread3 implements Runnable{

    public void run() {
        System.out.println("执行代理线程");
    }

    public static void main(String[] args) {
        TestThread3 testThread3 = new TestThread3();
        Thread t1 = new Thread(testThread3);
        t1.start();
    }
}

线程的启动
每个线程都是通过对应的run方法进行操作的,我们把run()方法的主体称为线程体。线程是通过Thread对象的start()方法来启动,而非直接调用run()方法,调用start()方法后系统会自动帮我们执行run方法的内容。

需知:run()方法由JVM调用,什么时候调用,执行的过程控制都由操作系统的CPU调度决定。如果自己手动调用run方法,那么就只是普通方法,没有启动多线程模式。一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出以上的异常“IIIegalThreadStateException”。

3、线程的生命周期(五大状态)

线程的五大状态为:创建状态 就绪状态 阻塞状态 运行状态 死亡状态

状态流程图
多线程状态

4、线程的执行控制

4.1、线程礼让(重新排队)
使用yield()方法可以使当前线程回到就绪状态,进入待机的队列,CPU开始新一轮的调度。

public class TestYield implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "开始");
        Thread.yield();//使当前执行线程回到就绪状态,让CPU重新调度。(随机)
        System.out.println(Thread.currentThread().getName() + "停止");
    }

    public static void main(String[] args) {
        new Thread(new TestYield(),"t1").start();
        new Thread(new TestYield(),"t2").start();
    }
}

该方法会让当前线程重新进入就绪状态,让CPU重新调度,但不一定会礼让。

4.2、线程优先(强行插队)
使用join()方法可以使原来的并行执行变为串行,进入join方法后的线程被视为特殊线程,会优先执行完成。且只有此特殊线程执行完毕之后,其它线程才可获取CPU资源。

public class TestJoin implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println("run()线程:" + i);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        //t1线程
        Thread t1 = new Thread(new TestJoin(), "t1");
        t1.start();

        //main线程
        for (int i = 0; i < 3; i++) {
            System.out.println("main:" + i);
            if(i == 1){
                t1.join();
            }
        }
    }
}

join()方法真正的让当前线程优先执行。

4.3、线程的优先级设置(修改线程获取CPU资源的概率)
Java提供线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该哪个线程来
执行。线程优先级有1-10
Thread.MIN_PRIORITY=1;
Thread.NORM_PRIORITY=5;
Thread.MAX_PRIORITY=10;
本质上是应该是优先级越大,优先权利越重。但实际上最终线程执行的优先级还是由CPU来决定的,只是优先级越高优先执行的概率更高而已。

public class TestPriority {    
    public static void main(String[] args) {        
        Runnable r = ()->{            
            for (int i = 0; i < 2; i++) {               		 
            	System.out.println(Thread.currentThread().getName() + ":" + i);            
            }        
        };        
        Thread t1 = new Thread(r,"t1");        
        Thread t2 = new Thread(r,"t2");        
        t1.setPriority(Thread.MAX_PRIORITY);
        //t1线程设置最高优先级        
        t2.setPriority(Thread.MIN_PRIORITY);//t2线程设置最低优先级        
        t1.start();        
        t2.start();    
    }
}

给线程显式的标注了优先级,但实际上执行的顺序还是得看CPU心情。

5、线程同步

下面是线程同步相关内容,点击后面的 # 号即可查看详细内容。
线程在并发的时候可能存在安全隐患:
(1)线程不安全案例(一)买票 #
(2)线程不安全案例(二)银行取钱 #
(3)线程不安全案例(三)集合 #

我们可以给访问数据的方法加锁,来保证资源的同步,以此来解决多线程并发下可能发生的问题。
(1)线程同步 sychronized方法 #
(2)线程同步 sychronized块 #
(3)CopyOnWriteArrayList,JUC线程安全的集合 #
(4)强大的可灵活添加、释放的锁Lock #

同步锁下可能出现的问题
死锁 #

线程不安全案例(一) 买票

public class UnsafeTicket {
    public static void main(String[] args) {
        BuyTicket buyTicket = new BuyTicket();
        new Thread(buyTicket,"p1").start();
        new Thread(buyTicket,"p2").start();
        new Thread(buyTicket,"p3").start();
    }
}
class BuyTicket implements Runnable{
    private int ticketNum = 2;//票的数量
    private boolean flag = true;//是否还有票
    @Override
    public void run() {
        //买票
        if (flag){
            buy();
        }
    }

    private void buy(){
        if(ticketNum  < 1){
            flag = false;
            return;
        }
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "买到最后第" + ticketNum-- + "张票");
    }
}

线程不安全案例(二)银行取钱

package sychronize;//不安全的取钱//两个人去银行取钱,账户
public class UnsafeBank {    
    public static void main(String[] args) {        
        Account account = new Account(100,"卡名");    
        new Thread(new Drawing(account,100)).start();      
        new Thread(new Drawing(account,100)).start();      
        new Thread(new Drawing(account,100)).start();    
    }
}
//账户
class Account{    
    int money;//余额    
    String name;//卡名    
    public Account(int money, String name) {        
        this.money = money;        
        this.name = name;    
    }
}
//银行:模拟取款
package sychronize;//不安全的取钱//两个人去银行取钱,账户
public class UnsafeBank {    
    public static void main(String[] args) {        
        Account account = new Account(100,"卡名");    
        new Thread(new Drawing(account,100)).start();      
        new Thread(new Drawing(account,100)).start();      
        new Thread(new Drawing(account,100)).start();    
    }
}
//账户
class Account{    
    int money;//余额    
    String name;//卡名    
    public Account(int money, String name) {        
        this.money = money;        
        this.name = name;    
    }
}
//银行:模拟取款
class Drawing extends Thread{    
    Account account;//账户    
    //取款金额    
    int drawingMoney;   
    public Drawing(Account account,int drawingMoney){       
        this.account = account;        
        this.drawingMoney = drawingMoney;    
    }    
    //取钱    
    @Override    public void run() {        
        //判断有没有钱        
       if(account.money - drawingMoney < 0){           								 
	       System.out.println("余额不足!!!您余额还有" + account.money);           		
	       return;        
        }        
        //sleep可以放大问题的发生性       
        try {            
            Thread.sleep(100);        
        } catch (InterruptedException e) { 
            e.printStackTrace();       
        }        
        account.money = account.money - drawingMoney;        
        System.out.println("取款成功。剩余余额" + account.money);    
    }
}

线程不安全案例(三)集合

//线程不安全的集合
public class UnsafeList {    
    static List<String> list = new ArrayList<>();    
    public static void main(String[] args) throws InterruptedException {        
	    for (int i = 0; i < 10000; i++) {            
	        new Thread(()->{                
	            UnsafeList.list.add(Thread.currentThread().getName());         
	        }).start();        
	    }        
	    Thread.sleep(3000);        
	    System.out.println(list.size());        
	}
}

线程同步 synchronized方法,同一个对象下访问方法同步
synchronized方法控制对"对象"的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的锁才能执行,否则线程会阻塞。方法一旦执行,就独占该锁,直到该方法结束才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。同步锁还存在一个缺陷,若将一个大的方法申明为synchronized将会影响效率,故我们只需要锁住需要修改的资源。一些只读的的资源可以不加锁。

//给方法上加锁,锁的是this就是同一个对象的方法。    
private synchronized void buy(){        
    if(ticketNum  < 1){            
        flag = false;            
        return;        
    }        
    try {            
        Thread.sleep(100);//添加延时,可以更明显的看到线程错误        
    } catch (InterruptedException e) {            
        e.printStackTrace();        
    }      
    System.out.println(Thread.currentThread().getName() + "买到最后第" + ticketNum-- + "张票");   
}

在方法上加synchronized关键字,可以使同一个对象下访问该方法同步。需要注意的是,当一个线程获得同步方法时,实际是锁住了整个当前对象。这时如果有其它线程想要访问该对象下其它同步方法也不允许的,需要等待占锁线程释放对象才可以使用。

线程同步 sychronized 同步块,在不同对象下访问方法同步

//不安全的取钱
//两个人去银行取钱,账户
public class UnsafeBank {    
    public static void main(String[] args) {        
        Account account = new Account(1000,"卡名");       
        new Thread(new Drawing(account,100)).start();   
        new Thread(new Drawing(account,100)).start();   
        new Thread(new Drawing(account,100)).start();    
    }
}
//账户
class Account{    
    int money;//余额   
    String name;//卡名   
    public Account(int money, String name) {       
        this.money = money;        
        this.name = name;   
    }
}
//银行:模拟取款
class Drawing extends Thread{    
    //账户    
    Account account;
   
    //取款金额    
    int drawingMoney;    
    public Drawing(Account account,int drawingMoney){ 
        this.account = account;     
        this.drawingMoney = drawingMoney;  
    }   
    //取钱   
    @Override    
    public void run() {     
        //同步块,可以锁任何对象     
        synchronized (account){         
            //判断有没有钱         
            if(account.money - drawingMoney < 0){               
                System.out.println("余额不足!!!您余额还有" + account.money);                return;          
            }           
            //sleep可以更加直观的看到线程错误         
            try {             
                Thread.sleep(100);      
            } catch (InterruptedException e) {
                e.printStackTrace();        
            }           
            account.money = account.money - drawingMoney;  
            System.out.println("取款成功。剩余余额" + account.money);       
         }    
      }
}

使用同步块可以锁任何对象,但是注意一定是要锁要修改的对象。

CopyOnWriteArrayList,JUC线程安全的集合

import java.util.concurrent.CopyOnWriteArrayList;
public class TestJUC {    
    public static void main(String[] args) {    
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>();       
        for (int i = 0; i < 10000; i++) {          
            new Thread(()->{               
                list.add(Thread.currentThread().getName());  
            }).start();     
        }       
        try {        
            Thread.sleep(3000);   
        } catch (InterruptedException e) { 
            e.printStackTrace();       
        }       
        System.out.println(list.size());
    }
}

更加强大的线程同步机制 Lock锁
Lock可以显式的定义同步锁对象来实现同步。ReentrantLock可以显式的加锁、释放锁。

package lock;
import java.util.concurrent.locks.ReentrantLock;
//测试Lock锁
public class TestLock {   
    public static void main(String[] args) {   
        TestLock2 t2 = new TestLock2();   
        for (int i = 0; i < 20; i++) {     
            new Thread(t2).start();     
        }   
    }
}
class TestLock2 implements Runnable{    
    int ticketNums = 10;    //定义lock锁 
    private final ReentrantLock lock = new ReentrantLock();   
    @Override   
    public void run() {      
        lock.lock();//加锁    
        if(ticketNums > 0){        
            try {               
                Thread.sleep(100);     
            } catch (InterruptedException e) {     
                e.printStackTrace();      
            }          
            System.out.println(ticketNums--);      
        }        
        lock.unlock();//解锁 
    }
}

使用lock锁可以显示(手动)的加锁与释放锁,注意一定要记得释放锁。使用lock锁,JVM调度线程花费的时间更少,性能更优。故想要线程同步优先考虑的顺序应该是:Lock > 同步代码块(锁部分代码)> 同步方法(锁整个方法)

死锁
多个线程互相抱着对方需要的资源,然后形成僵持的情况称为死锁。

public class DeadLock {    
    public static void main(String[] args) {        
        Makeup m1 = new Makeup(0,"灰姑娘");        
        Makeup m2 = new Makeup(1,"白雪公主");        
        m1.start();        
        m2.start();   
    }
}
//口红
class Lipstick{}
//镜子
class Mirror{
    
}
class Makeup extends Thread{    
    //需要的资源只有一份,用static来保证只有一份    
    static Lipstick lipstick = new Lipstick();   
    static Mirror mirror = new Mirror();    
    int choice;//选择    
    String girlName;//使用化妆品的人    
    Makeup(int choice,String girlName){        
        this.choice = choice;        
        this.girlName = girlName;    
    }    
    @Override    
    public void run() {        
        try {            
            makeup();        
        } catch (InterruptedException e) {           
            e.printStackTrace();        
        }    
    }    
    //化妆,相互持有对方的锁,就是需要拿到对方的资源    
    private void makeup() throws InterruptedException{        
        if(choice == 0){            
            synchronized (lipstick){//获得口红的锁      
                System.out.println(this.girlName + "获得口红的锁");                
                Thread.sleep(1000);                
                synchronized (mirror){
                    //一秒钟后向获得镜子                    
                    System.out.println(this.girlName + "获得镜子的锁");                
                }           
            }        
        }else {            
            synchronized (mirror){//获得镜子的锁                
                System.out.println(this.girlName + "获得镜子的锁");   
                Thread.sleep(2000);               
                synchronized (lipstick){
                    //一秒钟后向获得镜子                   
                    System.out.println(this.girlName + "获得口红的锁");              
                }            
            }       
        }   
    }
}

出现死锁的原因是对方都抱有对方所需要的资源(锁住了),并且都不肯放手(释放锁)。解决这个问题的方法就是一方先释放对方需要的资源。

6、线程之间的通信 生产者与消费者案例

线程之间的通信:wait()与notify()和notifyAll()方法

  • wait():令当前线程挂起并放弃CPU、同步资源并等待,使别的线程可以访问并修改共享资源,而当前线程排队等候其他线程调用notify()或notigyAll()方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行。
  • notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待。
  • notifyAllI():唤醒正在排队等待资源的所有线程结束等待。

注意:这三个方法只有在sychronized方法或sychronized代码块中才能使用,否则会报java.lang.IIIegalMonitorStateException异常。因为这三个方法必须有锁对象调用,而任意对象都可以作为synchronized的同步锁,因此这三个方法只能在Object类中声明。

线程通信:生产者与消费者案例
程序流程:生产者生产产品——通知消费者拿走——消费者拿走产品——通知生产者生产
(1)信号灯法,标志位解决

package deal;//测试生产者消费者问题2:信号灯法,标志位解决
public class TestDeal2 {    
    public static void main(String[] args) {        
        Food f = new Food();     
        new Canteen(f).start();       
        new Customer(f).start();    
    }
}
//生产者-->施粥者
class Canteen extends Thread{  
    Food food;   
    public Canteen(Food food) {   
        this.food = food;  
    }   
    @Override   
    public void run() {        
        for (int i = 0; i < 4; i++) {       
            food.serving("面包");     
        }   
    }
}
//消费者-->食客
class Customer extends Thread{   
    Food food;    
    public Customer(Food food) {     
        this.food = food;    
    }    
    @Override    
    public void run() {     
        for (int i = 0; i < 4; i++) {    
            food.eat();  
        }   
    }
}
//产品(粥)
class Food{    
    //拍摄电视,观众等待   
    //观众观看,演员休息   
    String food;//食物  
    boolean flag = true;    //上菜  
    public synchronized void serving(String food){    
        if(!flag){          
            try {              
                System.out.println("服务员等待...");      
                this.wait();           
            } catch (InterruptedException e) {     
                e.printStackTrace();   
            }        
        }      
        //通知食客食用     
        System.out.println("服务员上了一碗" + food);  
        this.food = food;     
        this.flag = !this.flag;        
        this.notifyAll();//通知唤醒   
    }    
    //食客食用    
    public synchronized void eat(){        
        if(flag){           
            try {            
                System.out.println("顾客等待..."); 
                this.wait();            
            } catch (InterruptedException e) {  
                e.printStackTrace();    
            }      
        }       
        System.out.println("食客吃完了" + food);        //通知餐厅上菜     
        this.flag = !this.flag;        
        this.notifyAll();   
    }
}

使用同步锁锁住对象的使用,wait()让其它线程进入等待状态,notifyAll()唤醒等待的线程,加上一个flag标志位做判断。如此就实现了线程通信。–线程通信的方法wait() 线程一直等待,直到其他线程通知,与sleep不同,会释放锁。wait(long timeout) 指定等待的毫秒数notify() 唤醒一个处于等待状态的线程notifyAll() 唤醒同一个对象上所有调用wait()方法的线程,优先级别越高的线程优先调度

(2)管程法

package com.kuang;

public class TestProduce {
    public static void main(String[] args) {
        Systack sy = new Systack();
        Shengchan sc = new Shengchan(sy);
        XiaoFei xf = new XiaoFei(sy);
        new Thread(xf).start();
        new Thread(sc).start();
    }
}

class ManTou {
    int id;

    ManTou(int x) {
        id = x;
    }

    public ManTou() {
        // TODO Auto-generated constructor stub
    }
}

class Systack {
    int index = 0;
    ManTou[] mt = new ManTou[10];

    public synchronized void push(ManTou m) {
        while (index == mt.length) {
            try {
                this.wait();//释放锁
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.notify();//
        mt[index] = m;
        index++;
    }

    public synchronized ManTou pop() {
        while (index == 0) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.notify();
        index--;
        return mt[index];
    }
}

class XiaoFei implements Runnable {
    Systack sc = null;

    public void run() {
        for (int i = 0; i < 20; i++) {
            ManTou m = sc.pop();
            System.out.println("吃馒头");
        }
    }

    public XiaoFei(Systack sc) {
        super();
        this.sc = sc;
    }

}

class Shengchan implements Runnable {
    Systack sc = null;


    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println("造馒头");
            ManTou mt = new ManTou(i);
            sc.push(mt);
        }
    }

    public Shengchan(Systack sc) {
        super();
        this.sc = sc;
    }
}

7、线程池

系统中经常创建和销毁使用量特别大的资源,如在并发情况下的线程,对性能影响很大。可以使用线程池提前创建好的线程,使用时直接获取,用完放回池中。即可避免频繁创建销毁,实现重复利用。

import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;//测试线程池
public class TestPool {    
    public static void main(String[] args) {        
	    //1.创建服务,创建线程池       
	    ExecutorService service = Executors.newFixedThreadPool(3);      
	    //执行      
	    service.execute(new MyThread());       
	    service.execute(new MyThread());    
	    service.execute(new MyThread());     
	    service.execute(new MyThread());        
	    //关闭连接    
	    service.shutdown();    
    }
}
class MyThread implements Runnable{   
    @Override   
    public void run() {       
        System.out.println(Thread.currentThread().getName());     
    }
}

使用线程池可以减少系统性能的损耗。

8、守护线程

线程分为用户线程与守护线程。虚拟机必须确保用户线程执行完毕,不用等待守护线程执行完毕。守护线程有:后台记录操作日志,监控内存,垃圾回收等…守护线程用于守护用户线程,且会在用户线程执行完毕后结束。

//守护线程
public class TestDaemon {    
    public static void main(String[] args) {        
        Thread daemonThread = new Thread(() -> {            
            while (true) {                
                System.out.println("守护线程执行...");           
            }        
        });        
        Thread userThread = new Thread(() -> {            
            for (int i = 0; i < 10; i++) {                
                System.out.println("用户线程执行:" + i);            
            }        
        });        
        daemonThread.setDaemon(true);        
        daemonThread.start();        
        userThread.start();    
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值