【无标题】校招面试必须会的线程同步(细解)

多线程概述

一、多线程的概念

多线程是指程序中包含多个执行单元,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行

二、使用多线程的情况

  1. 程序需要同时执行两个或多个任务
  2. 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。比如用户输入,当用户输入部分占据CPU时,如果一直没有输入,不可能让这一部分一直占据CPU,这个时候会让别的线程上CPU运行
  3. 需要一些后台运行的程序时

二、多线程的优缺点(重在缺点)

优点:

  1. 提高程序的响应

  2. 提高CPU的利用率

  3. 改善程序结构,将复杂任务分为对个线程,独立运行

缺点:

  1. 线程也是程序,所以线程需要占用内存,线程越多占用内存也越多

​ 第一点容易改善,我们不断提高电脑的性能就可以很好的改善占用内存的问题

  1. 多线程需要协调和管理,所以需要CPU时间跟踪线程

  2. 线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题(最主要的问题)

​ 比如我们春节期间买电影票,班里30个人买同一电影院的同一场次,电影票作为一个共享的资源,张三想买7排8号,恰巧李四也想买7排8号。张三和李四他俩购票的时候相当于创建了两个线程(多线程),如果不加以控制,他俩竞争共享资源,张三刚买完票,票数计数器还没来得及减一就进入阻塞状态,而李四看到的这张票还在,他也买了。这个时候就会出现事故。这是多线程存在的最大问题。

三、并行和并发

理解并行和并发要注意到两个词:同一时刻和一个时间段

并行:并行是在同一时刻发生多件事情。例如:多个运动员听到枪响那一时刻同时起跑、多个CPU同一时刻开始执行多个线程

并发:并发是在一个时间段内,多个事情发生,这几个事件在时间上很紧凑,但还是有先后顺序的。例如:一个单核CPU一次只能执行一个线程,但是我们一边放音乐,一边用QQ聊天看起来是同时发生的,但其实是不断交换着进行的。由于这些线程上下处理机的时间非常短暂,在我们看来他们是同时发生的,这就是并发。

线程同步

一、多线程同步

*** 多个线程同时读写同一份共享资源时,可能会引起冲突(上面买电影票的例子)。所以引入线程“同步”机制,即各线程间要有先来后到;

同步就是排队+锁:

  1. 几个线程之间要排队,一个个对共享资源进行操作,而不是同时进行操作

  2. 为了保证数据在方法中被访问时的正确性,在访问时加入锁机制

二、模拟买票为例讲解线程同步

有两个窗口卖票,当前的总票数是10张,分别用继承Thread和实现Runnable两种方式实现:

一、继承Thread方式

创建包:package com.ffyc.javathread.demo6;

创建类:TicketThread类

创建类:Test类

public class TicketThread extends Thread{
    int num = 10; //设定有10张票
    static Object obj = new Object();
    @Override
    public void run(){
        while(true){
            
            synchronized(obj) {  //进入同步代码块就会枷锁-->将对象头中的锁状态改为枷锁
                if (num > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "买到了票:" + num);

                    num--;
                } else {
                    break;
                }
            }  //同步代码块执行完后会自动释放锁
        }
    }

}
/*
synchronized(同步对象)  同步对象可以是任意类的对象,但是只能是唯一的,只有一份的
这种情况不能用this,因为我们创建两个线程对象,哪一个线程访问,this就表示哪一个线程
对象中有一个区域叫对象头,对象头中有一个锁状态的记录
*/

上面的代码让我详细讲解,看不懂,不可能!

public class TicketThread extends Thread{
    int num = 10;//num表示车票 作为类的成员属性==整体的变量
    
    @Override
    public void run(){  
        /*线程创建看上一篇文章,无论是继承Thread,还是实现Runnable接口,都要重写run()方法,run()方法的方         法体写的是线程要执行的任务*/
        
    }
    
}    

先重写出run()方法的轮廓就可以,具体实现完后看

***我们要在main方法中创建出题目要求的两个窗口(线程)

创建包:package com.ffyc.javathread.demo6;

创建类:Test类

package com.ffyc.javathread.demo6;

public class Test{
    public static void main(String [] args){
        
        TicketThread th1 = new TicketThread();  
        /*创建了一个线程对象,也就是创建了一个线程*/
        th1.setName("窗口1"); //Thread中给线程命名的方发:setName()
        th1.start();
        /* 启动线程,也就是-->进入就绪状态,等待CPU的调度*/
        
        //创建第二个线程
        TicketThread th2 = new TicketThread();
        th2.setName("窗口2");
        th2.start();
        
        
    }
}

接下来我们来实现run()方法:

@Override
    public void run(){  
        while(true){
            if(num>0){
                //买票方法体
                System.out.println(Thread.currentThread().getName()+"买到票:"+mun);
                num--;
            }else{
                break;//当mun票数小于等于0就说明没票了,break结束
            }
                
        }
        
    }

现在,整合起来就是:

package com.ffyc.javathread.demo6;
public class TicketThread extends Thread{
    int num = 10;//num表示车票 作为类的成员属性==整体的变量
    
    @Override
    public void run(){  
        /*线程创建看上一篇文章,无论是继承Thread,还是实现Runnable接口,都要重写run()方法,run()方法的方         法体写的是线程要执行的任务*/
         while(true){
            if(num>0){
                //买票方法体
                System.out.println(Thread.currentThread().getName()+"买到票:"+mun);
                //sleep()方法需要try/catch异常
                //sleep()方法是线程休眠参数100指的是休眠100ms,即让当前正在执行的线程休眠(暂停执行)
                try{
                Thread.sleep(100);
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
                
                num--;
            }else{
                break;//当mun票数小于等于0就说明没票了,break结束
            }
                
        }
        
    }
    
}    


package com.ffyc.javathread.demo6;
public class Test{
    public static void main(String [] args){
        
        TicketThread th1 = new TicketThread();  
        /*创建了一个线程对象,也就是创建了一个线程*/
        th1.setName("窗口1"); //Thread中给线程命名的方发:setName()
        th1.start();
        /* 启动线程,也就是-->进入就绪状态,等待CPU的调度*/
        
        //创建第二个线程
        TicketThread th2 = new TicketThread();
        th2.setName("窗口2");
        th2.start();
        
        
    }
}

上面这个卖票程序会出错,不是编译期的错误,是程序运行时会出现上面举的买电影票例子的情况:窗口1卖出去了7排8号的票,窗口2也卖出去了这张票。

【无标题】校招面试必须会的线程同步(细解)

同时买到了第10张票,同时买到了第8张票…总共10张票却卖出去了12张。

多线程存在的这个问题我们可以通过排队+加锁来处理,即线程同步机制。

加锁的两种方式(这里先说给代码块加锁,给方法加锁见下一篇)
  1. 用synchronized(同步对象)关键字加锁

用synchronized(同步对象)可以给方法加锁,也可以给代码块加锁:

(1)给代码块加锁:

package com.ffyc.javathread.demo6;
public class TicketThread extends Thread{
    int num = 10;//num表示车票 作为类的成员属性==整体的变量
    
    Object obj = new Object();
    
    @Override
    public void run(){  
        /*线程创建看上一篇文章,无论是继承Thread,还是实现Runnable接口,都要重写run()方法,run()方法的方         法体写的是线程要执行的任务*/
         while(true){
            
             
             synchronized(obj){
            	if(num>0){
                	//买票方法体
                	System.out.println(Thread.currentThread().getName()+"买到票:"+mun);
                	//sleep()方法需要try/catch异常
                //sleep()方法是线程休眠参数100指的是休眠100ms,即让当前正在执行的线程休眠(暂停执行)
                	try{
                		Thread.sleep(100);
                	}catch(InterruptedException e){
                    	e.printStackTrace();
                	}
                
                	num--;
            		}else{
                		break;//当mun票数小于等于0就说明没票了,break结束
            		}
            }
                
        }
        
    }
    
}    

​ synchronized(同步对象) 中的同步对象 可以是任意类的对象,但是只能是唯一的,只有一份的。

想明白synchronized关键字怎么加锁,需要了解一点对象的底层:

​ 其实,对象中有一个区域叫对象头,对象头中有一个锁状态的记录,我们在TicketThread类中,创建了一个Object对象,当两个线程中的一个进入锁之后obj对象的锁状态会标记锁里面有线程,此时别的线程就无法进来,直到synchronized(obj){}执行完,释放标志位 。一次只允许一个线程进入,是安全的。

​ 这种情况不能用this,因为我们创建了两个线程对象(th1和th2),在访问锁里面的代码时:哪一个线程访问,this就表示 哪一个线程的对象,两个线程,就有两个this对象,不唯一,导致都可以进入锁。

以下是运行结果:

【无标题】校招面试必须会的线程同步(细解)

这篇文章就是理解线程同步,后面部分请见下一篇~

上一篇:Selenium操作cookie绕过验证码登陆实战


下一篇:LifeSmart云起局域网直接控制向往背景音乐