java基础-多线程与并发

以下为本人的学习笔记

 

1.进程与线程

1.1 什么是进程

程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。而进程是程序在处理机上的一次执行过程,它是一个动态的概念。

进程是一个具有一定独立功能的程序,一个实体,每一个进程都有它自己的地址空间

 

1.2 进程的状态

进程执行时的间断性,决定了进程可能有多种状态。事实上,运行中的进程具有以下三种基本状态。

  • 就绪状态(Ready)

  • 运行状态(Running)

  • 阻塞状态(Blocked)

cpu是有转速的,转速越快,性能越高。但CPU运行a程序时,要转到h程序,需要时间,这时候进程就会进入阻塞状态。

 

1.3 线程

线程实际上是在进程基础上调度进一步划分,一个线程启动后,里面的若干程序又可以划分成若干个线程。

线程:是进程中的一个执行路径,共享一个内存空间,程序之间可以自由切换,并发执行,一个进程最少有一个线程(单线程程序)

一个程序可以同时执行多个任务,来提高效率

例如:①同时下载多部电影

②看电影的同时吃零食

并行:就是两个任务同时运行(多个CPU)

并发:是指两个任务同时请求运行,而处理器一次只能接受一个任务,就会把两个任务安排轮流执行,由于CPU时间片运行时间较短,就会感觉是两个任务在同时执行

  • 面试题:进程和线程的区别?

 

2.线程的基本使用

线程实现的三种方式

在java中如果要想实现多线程的操作,有两种实现方法:

1)继承Thread类(只能继承一个)

2)实现Runnable接口(建议使用,可以实现多个)

3)实现Callable接口

public class ThreadDeom1{
    
    public static void main(String[] args){
        MyThread mt = new Thread();
        
        MyRunnable mr = new MyRunnable();
        Thread t2 = new Thread(mr);//将实现Runnable的当成一个任务放进线程Thread里
        
        mt.start();//启动线程,(实际是,start表准备就绪,告诉虚拟机可以启动线程)
        t2.start();//可简写,合并成:new Thread(mr).start
    }
}
​
/**
创建线程方式一:
1.继承Thread类
2.重写run()方法
3.创建线程对象,调用start()开启线程
注意:线程开启不一定立即执行,有CPU调度执行
*/
class MyThread extends Thread{
    
    public void run(){
        for(int i = 0;i<100;i++){
            System.out.println(Thread.currentThread().getName()+"-"+i);
            try{
                Thread.sleep(500);
             }catch (InterruptedException e){
                 e.printStackTrace();
            }
        }
    }
}
​
/**
创建线程方式二:
1.实现Runnable接口
2.重写run()方法
3.创建Runnable接口的实现类对象
4.创建线程对象(代理),丢入实现类对象
5.调用start()开启线程
注意:线程开启不一定立即执行,有CPU调度执行
*/
class MyRunnable implement Runnable{
     public void run(){
        for(int i = 0;i<100;i++){
            System.out.println(Thread.currentThread().getName()+"-"+i);
            try{
            /**
                线程的休眠
                在当前线程的执行中,暂停指定的毫秒数,释放CPU的时间片
                释放CPU的时间片意思:CPU给一个进程P ,3秒执行时间,线程A和线程B互抢这个3秒时间片,设A抢到了,执行了3秒,此时会中断,然后CPU执行下一个进程,CPU一直转,又转到进程P,然后A和B又开始互抢这3秒时间片,A又抢到,继上次中断的位置开始执行,执行完后,就让时间片给其他线程。
            */
                Thread.sleep(500);
            
            }catch (InterruptedException e){
                e.printStackTrace();
                
            }
        }
    }
}
/**
创建线程方式三:
1.实现Callable接口,需要返回值类型
2.重写call方法,需要抛出异常
3.创建目标对象
4.创建执行服务:ExecutorService ser = Exectors.newFixedThreadPool(1);
5.提交执行:Future<Boolean> result1 = ser.submit(t1);
6.获取结果:boolean r1 = result1.get();
7.关闭服务:ser.shutdownNow();
*/

案例

 

3.线程的休眠

public static void sleep(long millis) throws InterruptedException

使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行),释放CPU的时间片,具体取决于系统定时器和调度程序的精度和准确性。 线程不会丢失任何显示器的所有权。

  • 参数

    millis - 以毫秒为单位的睡眠时间长度

  • 异常

    IllegalArgumentException - 如果 millis值为负数

    InterruptedException - 如果任何线程中断当前线程。 当抛出此异常时,当前线程的中断状态将被清除。 由线程本身中断不会报错。

  • 线程的休眠,目的是让出CPU执行的时间片,让其他工作的线程可以执行,但不会释放对象锁

  • sleep可以模拟网络延迟,倒计时等

  • sleep时间达到后线程进入就绪状态

public static void sleep(long millis,int nanos) throws InterruptedException//毫秒,纳秒
public static Thread currentThread()//返回对当前正在执行的线程对象的引用,即获取当前线程

 

4.join与中断线程

public final void join() throws InterruptedException

等待这个线程死亡。

调用此方法的行为方式与调用join(0) 完全相同

  • 异常

    InterruptedException - 如果任何线程中断当前线程。 当抛出此异常时,当前线程的中断状态将被清除。

   
public void interrupt() 中断这个线程。 除非当前线程中断自身,这是始终允许的
public static boolean interrupted() 测试当前线程是否中断。 该方法可以清除线程的中断状态 。 换句话说,如果这个方法被连续调用两次,那么第二个调用将返回false(除非当前线程再次中断,在第一个调用已经清除其中断状态之后,在第二个调用之前已经检查过)。 忽略线程中断,因为线程在中断时不存在将被该方法返回false所反映。

join方法的主要作用就是同步,它可以使得线程之间的并行执行变为串行执行

或者说 join合并线程,待此线程执行完毕后,回去再执行其他线程,其他线程阻塞,可以想象成插队

public class ThreadDeom2{
    public static void main(String[] args){
        MyRunnable mr2 = new MyRunnable2();
        Thread t1 = new Thread(mr2);
        MyRunnable mr3 = new MyRunnable3();
​
        Thread t2 = new Thread(mr3);
        t1.start();
        t2.start();
        for(int i = 0;i<50;i++){
            System.out.println(Thread.currentThread().getName()+"-"+i);
            try{
            
                 Thread.sleep(300);
            
             }catch (InterruptedException e){
                e.printStackTrace();
                }
            if(i==20){
             /**  try{          
                    t1.join();//让t线程执行完毕
                }catch (InterruptedException e){
                     e.printStackTrace();
                    }*/
                
              //  t1.interrupt();//中断线程(不会真的中断线程),只是作了一个中断标记
                
               mr3.flag =false; 
             }
         }
    }
}
​
class MyRunable2 implements Runnable{
    public void run(){
        for(int i = 0;i<50;i++){
            /**
            中断线程方式一:
            1.使用interrupt()来中断线程,设置一个中断状态(标记)
            2.run方法里测试中断状态Thread.interrupted()
            3.有sleep等抛出InterruptedException异常的,要重新打上中断标记
            
            中断线程方式二(更加推荐使用):
            1.自定义标记的方式,设置一个布尔值为ture,作为while的条件,要中断时就将该布尔值设为false
            */
            if(Thread.interrupted()){//测试中断状态,此方法会把中断状态清除
                break;//不会真的中断,因为sleep会抛出InterruptedException异常并把中断标记清除,所以要重新打上中断标记
            }
            System.out.println(Thread.currentThread().getName()+"-"+i);
            try{
            
                 Thread.sleep(300);
            
             }catch (InterruptedException e){
                e.printStackTrace();
                Thread.currentThread().interrupt();//重新打上中断标记
                }
         }
    }
}
​
class MyRunable3 implements Runnable{
    
    public boolean flag = true;
    
    public MyRunable3(){
        flag = true;
    }
    
    public void run(){
        int i = 0;
        while(flag){
             System.out.println(Thread.currentThread().getName()+"---"+i);
            try{
            
                 Thread.sleep(300);
            
             }catch (InterruptedException e){
                e.printStackTrace();
                }
        }
    }
}

 

5.守护线程与yield

线程分为用户线程和守护线程

虚拟机必须确保用户线程执行完毕

虚拟机不用等待守护线程执行完毕(用户线程执行完毕后,JVM虚拟机自动退出,不用等待守护线程执行完毕)

如,后台记录操作日志,监控内存,垃圾回收等待....

method 说明
public final void setDaemon(boolean on) 将此线程标记为daemon线程或用户线程。 当运行的唯一线程都是守护进程线程时,Java虚拟机将退出。
public final boolean isDaemon() 测试这个线程是否是守护线程。
public static void yield() 暂停当前正在执行的线程对象,并执行其他线程(了解)

yield作用是暂停当前正在执行的线程对象(放弃当前CPU资源),并执行其他线程。

yield是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会

补充:当源码由native修饰,表示为本地方法,由c或c++实现

        for(int i = 0;i<50;i++){
            System.out.println(Thread.currentThread().getName()+"-"+i);
            try{
            
                 Thread.sleep(300);
            
             }catch (InterruptedException e){
                e.printStackTrace();
                }
            if(i==5){
                Thread.yield();//让出本次CPU执行时间片,就让一次,下一次还抢CPU时间片
            }
         }

 

6.其他方法与优先级

Method or Fieids 说明
long getId() 返回此线程的标识符。
String getName() 返回此线程的名称。
int getPriority() 返回此线程的优先级。
boolean isAlive() 测试这个线程是否处于活动状态,start以后就是活动状态。
void setName(String name) 将此线程的名称更改为等于参数 name
void``setPriority(int newPriority) 更改此线程的优先级。
static int MAX_PRIORITY 线程可以拥有的最大优先级。
static int ``MIN_PRIORITY 线程可以拥有的最小优先级。
static int NORM_PRIORITY 分配给线程的默认优先级。

 

7.线程同步

7.1 多线程共享数据

在多线程的操作中,多个线程有可能同时处理同一个资源,这就是多线程中的共享数据

7.2线程同步

解决数据共享问题,必须使用同步,所谓同步就是指多个线程在同一个时间段内只有一个线程执行指定代码,其他线程要等待此线程完成之后才可以继续执行。

在多线程编程时,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何时刻,最多有一个线程访问,以保证数据的完整性

线程同步是确保多线程共享数据的安全性,同时也会牺牲性能,同步过多还可能产生死锁,因此务必按需求使用同步

线程进行同步,有以下两种方法:

1)同步代码块

synchronized(要同步的对象){

要同步的操作;

}

2)同步方法

public synchronized void method(){

要同步的操作;

}

3)Lock(ReentrantLock)

/**
1.多线程共享数据时,会发生线程不安全的情况(多个线程操作同一个数据)
2.多线程共享数据必须使用同步
*/
public class ThreadDeom4{
    public static void main(String[] args){
        MyRunnable5 = mr5 = new MyRunnable();//一个任务
        Thread t1 = new Thread(mr5);//线程1
        Thread t2 = new Thread(mr5);//线程2
        
        t1.start();
        t2.start();
​
    }
}
​
class MyRunnable5 implements Runnable{
    
    private int ticket;//售票
    Object obj = new Object;
    public void run(){
    
        for(int i = 0;i<300;i++){
            if(ticket>0){
                //同步方法一:
                synchronized(obj){
                     ticket--;
                try{
                    Thread.sleep(1000);
                 }catch (InterruptedException e){
                     e.printStackTrace();
                }
                System.out.println("您购买的票已剩余"+ticket--+"张");
                }
               //method();
            }
        }
    }
    
    //同步方法二,同步的对象是当前对象(this)
     private synchronized void method(){
         if(ticket>0){
                     ticket--;
                try{
                    Thread.sleep(1000);
                 }catch (InterruptedException e){
                     e.printStackTrace();
                }
                System.out.println("您购买的票已剩余"+ticket--+"张");
                }
     }
    
    //同步方法三,Lock锁
    ReentrantLock lock = new ReentrantLock():
     private synchronized void method(){
         lock.lock();//上锁
         try{
         if(ticket>0){
                     ticket--;
                try{
                    Thread.sleep(1000);
                 }catch (InterruptedException e){
                     e.printStackTrace();
                }
                System.out.println("您购买的票已剩余"+ticket--+"张");
                }
         }finally{//加try,catch确保一定能释放锁,避免死锁
         lock.unlock();//释放锁
         }
     }
}

解释sleep不会释放对象锁:

sleep在synchronized同步代码块里,CPU分配3秒,任务执行1秒,sleep休眠1秒,剩下一秒sleep会释放CPU时间片给其他线程,但是不会释放对象锁,也就是synchronized锁还在,尽管有释放CPU时间片,但是其他线程进不去这个同步锁里面,所以剩下1秒其他线程只能干等。

7.3同步准则

当编写synchronized块时,有几个简单的准则可以遵循,这些准则在避免死锁和性能危险的风险方面大有帮助:

  1. 使代码块保持简洁。把不随线程变化的预处理和后处理移出synchronized块

  2. 不要阻塞。如InputStream.read()。

  3. 在持有锁的时候,不要对其他对象调用其同步方法

 

8.死锁

过多的同步有可能出现死锁,死锁的操作一般是在程序运行的时候才有可能出现

多线程中要进行资源的共享,就需要同步,但同步过多,就可能造成死锁,要避免出现死锁

线程死锁:在一个同步方法中调用了另一个对象的同步方法,可能产生死锁。

 

9.生产者和消费者问题

应用场景:生产者和消费者问题

  • 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费

  • 如果仓库中没有产品,则生产者将产品放入仓库,否则停止产品并等待,直到仓库中的产品被消费者取走为止

  • 如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止

分析:

这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者直接相互依赖,互为条件

  • 对于生产者,没有生产产品之前,要通知消费者等待,而生产了产品之后,又需要马上通知消费者消费

  • 对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费

  • 在生产者消费者问题中,仅有synchronized是不够的

  • synchronized可阻止并发更新同一个共享资源,实现了同步

  • synchronized不能用来实现不同线程之间的消息传递(通信)

java提供了几个方法解决线程之间的通信问题

method 说明
wait() 表示线程一直等待,直到其他线程通知,与sleep不同,会释放锁
wait(long timeout) 指定等待的毫秒数
notify() 唤醒一个处于等待状态的线程
notifyAll() 唤醒同一个对象上所有调用wait()方法是线程,优先级别高的线程优先调度

注意:均是Object类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常IIIegalMOnitorStateException

解决方式

并发协作模型“生产者/消费者模式”---->管程法

  • 生产者:负责生产数据的模块(可能是方法,对象,线程,进程);

  • 消费者:负责处理数据的模块(可能是方法,对象,线程,进程);

  • 缓冲区:消费者不能直接使用生产者的数据,他们之间有个“缓冲区”

生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据

//测试:生产者消费者模型”---->利用缓冲区解决:管程法
//生产者,消费者,产品,缓冲区
public class TestPC{
    public static void main(String[] args){
        SynContainer container = new SynContainer();
        
        new Product(container).start();
        new Consumer(container).start();
    }
}
​
//生产者
class Productor extends Thread{
    SynContainer container;
    
    public Productor(SynContainer container){
        this.container = container;  
    }
}
​
//消费者
class Consumer extends Thread{
    SynContainer container;
    
    public Consumer(SynContainer container){
        this.container = container;
    }
    
    //消费
    public void run(){
        for(int i = 0;i<10;i++){
            System.out.println("消费了--->"+container.pop().id+"-"+"只鸡");
        }
    }
}
​
//产品
class Chicken{
    int id;//产品编号
    
    public Chicken(int id){
        this.id= id;
    }
    
}
​
//缓冲区
class SynContainer{
    
    //需要一个容器大小
    Chicken[] chickens = new Chicken[10];
    //容器计数器
    int count =0;
    
    //生产者放入产品
    public synchronized void push(Chicken chicken){
        //如果容器满了,就需要调度消费者消费
        if(count==chickens.length){
            //通知消费者消费,生产等待
            try{
                this.wait();
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
        
        //如果没有满,我们就需要丢入产品
        chickens[count]=chicken;
        count++;
        
        //可以通知消费者消费了
        this.notifyAll();
        
    }
    
    //消费者消费产品
    public synchronized Chicken pop(){
        //判断能否消费
        if(count==0){
            //等待生产者生产,消费者等待
            try{
                this.wait();
            }catch(InterrupedException e){
                e.printStackTrace();
            }
        }
        
        //如果可以消费
        count--;
        Chicken chicken = chickens[count];
        
        //吃完了,通知生产者生产
        this.notifyAll();
        return chicken;
    }
}

面试题;sleep与wait的区别?

  • sleep:让线程进入休眠状态,让出CPU的时间片,不释放对象锁

  • wait:让线程进入等待状态,让出CPU的时间片,并释放对象锁,等待其他线程通过notify方法来唤醒(同步方法里使用)

 

10.线程生命周期图:

 

11、线程池

线程池是预先创建线程的一种技术。线程池在还没有任务到来之前,创建一定数量的线程,放入空闲队列中,然后对这些资源进行赋予。减少频繁的创建和销毁对象。

JDK1.5版本以上提供了现成的线程池

java里面线程池的顶级接口是Executor,是一个执行线程的工具

线程池接口是ExecutorService

使用线程池的好处:

线程可以重复利用,减少创建和销毁线程所带来的的系统资源的开销,提升性能(节省线程创建的时间开销,使程序响应更快)

 

java.util.concurrent包:并发编程中很常用的使用工具类(concurrent并发(工具类))

Executor接口(译:执行器) :

执行已提交的Runnable任务的对象

ExecutorService接口:

Executor提供了管理终止的方法,以及可为跟踪一个或多个异步任务执行状况而生成Future的方法

Executors类:

此包中所定义的Executor、ExecutorService等的工厂和使用方法

在Executors类里面提供了一些静态工厂,生成一些常用的线程池

newSingleThreadExecutor:

  创建一个单线程的线程池。这个线程只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

newFixedThreadPool: (fixed 译:固定的)

  创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。

线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程

newCachedThreadPool(少用到):

  创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

newScheduledThreadPool:

  创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求

public class ThreadDeom5{
    public static void main(String[] args){
        //创建线程池(4种)
        //1.创建一个单线程的线程池,每个线程轮流走完才轮到下一个走
    //  ExecutorService es = Executors.newSingleThreadExecutor();
        //2.创建固定大小的线程池,多个线程同时执行
    //  ExecutorService es = Executors.newFixedThreadPool(2);
        //3.创建一个可缓存的线程池
    //  ExecutorService es = Executors.newCacheThreadPool();
        //4.创建一个大小无限的线程池
    //  ExecutorService es = Executors.newScaheduleThreadPool(3)//要给初始容量
    /** es.execute(new MyRunnable6());//一个execute()就为一个线程,MyRunnable6是一个任务
        es.execute(new MyRunnable6());*/
        es.schedule(new MYRunnable6(),3000,TimeUnit.MiLLISECONDS);//可以调度:延迟3秒
        es.shutdown();
    }
}
​
class MyRunnable6 implements Runnable{
    public void run(){
        for(int i = 0;i<10;i++){
            System.out.println(Thread.currentThread().getName()+"-"+i);
        
            try{
                Thread.sleep(300);
             }catch (InterruptedException e){
                 e.printStackTrace();
            }
        }
    }
}

 

参考资料:

JDK1.8帮助文档

 

■免责申明
⒈ 本站是纯粹个人学习网站,与朋友交流共赏,不存在任何商业目的。
⒉ 本站利用了部分网络资源,版权归原作者及网站所有,如果您对本站所载文章及作品版权的归属存有异议,请立即通知我们,我们将在第一时间予以删除,同时向你表示歉意!

 

 

posted @ 2022-10-12 22:27  逝去の年华  阅读(174)  评论(0编辑  收藏  举报