欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

线程:并发安全性问题

程序员文章站 2022-05-17 18:40:23
...

线程:并发安全性问题:

一:线程概述:

1:进程:

学习线程之前,我们必须要知道什么是进程

进程是计算机中特定功能的程序再数据集上的一次运行。如,我们现在打开的word软件这就是一个进程。

 

线程:并发安全性问题

 

2:线程:

线程是进程的一个单元,一个运行中的进程,内部可能有多个线程。也就是说线程要比进程的粒度小。

如:音乐播放器,一次只能播放一首歌,这首歌播放完毕后,才可以播放下一首,这就是一个进程中有一个线程。这种叫做单线程进程。

但是往往很多的进程都是包含多个线程的,如我们使用迅雷下载视屏,可以同时下载多部电影,迅雷软件的一次运行,就是一个进程,每一部电影的下载,都是一个线程,同时下载多部电影,就是多线程进程。

我们的qq,微信,可以同时和多个人交流,这也是多线程。

我们java虚拟机运行的时候,主函数其实就是一个线程,java虚拟机是多线程的,如在堆中,有些对象不再使用了,就需要通过垃圾回收机制的线程来回收没有用的对象,垃圾回收机制的线程在主函数的线程运行的时候,垃圾回收机制的线程在后台其实也是在运行着的,只不过我们没有看到,这里又是一个线程。由此java虚拟机是多线程的。

 

 

线程:并发安全性问题

 

 

线程:并发安全性问题

 

二:线程创建:

线程的实现,在我们java中,有一个专门创建线程的类:Thread

线程:并发安全性问题

 

线程:并发安全性问题

 

创建新执行线程有两种方法。一种方法是将类声明为 Thread 的子类。该子类应重写Thread 类的 run 方法。接下来可以分配并启动该子类的实例。

创建线程的另一种方法是声明实现 Runnable 接口的类。该类然后实现 run 方法。然后可以分配该类的实例,在创建 Thread 时作为一个参数来传递并启动

 

线程:并发安全性问题

 

Run中是线程体,每一个线程执行的时候,执行的都是run中的代码。

 

线程:并发安全性问题

 

获得线程的名字

 

 

 

线程:并发安全性问题

 

启动线程:我们不能通过run方法去启动线程

 

方式一:

 

线程:并发安全性问题

/*
 * 创建线程的方式一:创建一个类,这个类继承Thread这个类,并且实现
 * Thread中的run方法
 */

public class ThreadDemo extends Thread{
	
	
	public static void main(String[] args) {
		//启动线程,创建线程的实例对象
		ThreadDemo td = new ThreadDemo();
		//由于是多线程,我可以创建多个线程的实例对象
		ThreadDemo td1 = new ThreadDemo();
/*
		 * 线程的名字除了可以获得之外,我们也可以自己设置线程的名字,我们没有设置名字的时候。java虚拟机会为每一个
		 * 线程分配一个名字
		 */
		
		td.setName("线程1");
		td1.setName("线程2");
		/*
		 * 分别启动两个线程,首先我们是要让run方法运行起来,但是我们不能直接通过实例去调run方法,
		 * 如果我直接去调用run方法的话,这不是启动线程,这里就是单纯的调用run方法,这是没有任何意义的。
		 */
		/*td.run();
		td1.run();*/
		
		/*
		 * 启动线程,这样启动线程,连个线程会同时去执行,不会等到线程0一个线程执行完,再去执行另一个线程。
		 * 这里两个线程会同时去执行
		 */
		td.start();
		td1.start();
		/*
		 * 当我执行到这里的时候,总共有三个线程在运行,主函数不能忽视
		 */
		
		
		
	}
	
	
	/*
	 * 重写Thread中run方法,run方法中就是线程的执行体
	 */
	
	@Override
	public void run() {
		//编写线程体
		for(int i = 0; i < 30;i++){
			//为了区分每一个线程,我可以获得每一个线程的名字
			System.out.println(this.getName()+"---线程---"+i);
			
		}
		
	}

}

线程:并发安全性问题 

public class ThreadDemo1 extends Thread{
	
	private String tName;
	
	
	public ThreadDemo1() {
	}


	public ThreadDemo1(String tName) {
		super(tName);
	}


    @Override
	public void run() {
		for(int i = 0; i < 30;i++){
			System.out.println(this.getName()+"---线程---"+i);
			
		}
		
	}

}



public class Test {
	
	
    public static void main(String[] args) {
    	
   
    	ThreadDemo1 td = new ThreadDemo1("线程一");
    	ThreadDemo1 td1 = new ThreadDemo1("线程二");
    	
    	td.start();
    	td1.start();
    	
    }
}

方式二:

创建一个类实现一个接口:Runnable

 

线程:并发安全性问题

 

 

Runnable 接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为 run 的无参数方法。

 

我们可以通过Thread类的静态方法来获得线程的名字:

线程:并发安全性问题

 

方式二:我们使用这种构造器来创建线程:

线程:并发安全性问题

 

方式二:我们也可以通过这种方式来创建线程,这里可以指定线程的名字:

一般情况下面我们不会去指定线程的名字,没有什么意义。

线程:并发安全性问题

 

import com.lb.thread.ThreadDemo2;

public class Test {

	@org.junit.Test
	public void test() {
		
		//创建Runnable接口的实现类的对象
		ThreadDemo2 td = new ThreadDemo2();
		//通过Runnable接口的实现类的对象创建线程
		//Thread t = new Thread(td);
		Thread t = new Thread(td,"线程一");
		
		ThreadDemo2 td1 = new ThreadDemo2();
		//Thread t1 = new Thread(td1);
		Thread t1 = new Thread(td1,"线程二");
		
		//启动上面两个线程:
		t.start();
		t1.start();
	}

}

/*
 * 创建线程的方式二,实现Runnable接口并且实现接口中的run方法。
 */


public class ThreadDemo2 implements Runnable{

	@Override
	public void run() {
		for(int i = 0; i < 30;i++){
			/*
			 * System.out.println(this.getName()+"---线程---"+i);
			 * 现在我是不能通过getName来获得线程的名字了,因为
			 * getName是Thread中的方法
			 * 我们可以通过Thread类中的静态方法currentThread可以获得当前正在执行的线程对象的引用
			 * 通过对象引用使用getName方法,我们就可以获得线程的名字
			 */
			
			System.out.println(Thread.currentThread().getName()+"--线程--"+i);
			
			
		}
	}

线程:并发安全性问题 

 

为什么线程的实现要有两种方式:一种方式是继承一个Thread的类,一种是实现接口Runnabel。由于java是单继承的,如果我当前的类继承了一个其他的类,那么就不能继承Thread类了,但是可以通过实现接口Runnabel来创建线程。

 

 

三:线程的执行原理和生命周期:

1:线程的执行原理:

线程:并发安全性问题

 

线程:并发安全性问题

 

 

 

 

但是,其实上面两个线程的执行,并非完全并发

 

 

 

CPU

 

 
   

 

                          

 

 

 

 

 

 

进程:

 

 

 
   

 

 

线程三

线程二

线程一

  

 

 

 

 

 

 

 

现在,我上面一个进程中有三个线程,这三个线程同时执行,但是线程的执行必须要有CPU才可以执行,但是现在CPU的资源只有一份。也就是说这几个线程不能同时拥有这一份CPU,

所以上面三个线程在执行的时候,可能是线程一抢到了CPU的资源,那么线程二,线程三九不会得到执行,只有线程一得到执行。

虽然我们看似这三个线程是同时执行的,但是实际上,他们是来回的抢占CPU的资源,也就是说,一般是交替执行的,但是这种交替没有规律。这种CPU资源的抢占是很快的,我们会以为其是同时执行的,但是一份CPU的资源只能被一个线程所使用

我们从程序的角度来看,上面三个线程是同时执行,是并发的。

 

线程的并发执行通过多个线程不断的切换CPU的资源,这个速度非常快,我们感知不到,我们能感知到的是三个线程在并发的执行。

 

2:线程的生命周期:

线程有下面几种状态:

状态一:新建状态:ThreadDemo1 td = new ThreadDemo1();

 

状态二:准备就绪状态:线程启动,具备执行资格。具备执行资格,还不能真正的去执行,因为,我多个线程启动之后,哪个线程先抢占到了CPU的资源,哪个线程才可以得到执行。

 

状态三:运行状态:具备执行的资格和执行的权力

 

状态四:阻塞状态,挂起状态:没有执行的资格以及执行权力,但是这个状态的线程是一个启动了的线程。线程休眠就进入阻塞状态,在运行状态的时候,可以让线程休眠(sleep)一段时间,或者线程之间互相协作的时候,可以让某个线程做等待(wait)。阻塞状态的线程是可以被唤醒的。

 

状态五:销毁状态:线程对象变成了垃圾,需要释放资源

 

 

                                             运行状态可以变为就绪状态(等待CPU)

 

新建线程:      

运行状态

准备就绪:

 

               
     
 
           

 

                             start();方法                                 一个线程

                                              抢占到CPU

                               

休眠时间到(sleep)

                                                             1:run方法执行完毕

                                                                                                       2:线程调用stop();方

                                                                                                       法

 

阻塞状态,挂起状态

                                    Sleep();

 

Wait();

 

销毁状态

 

 
   

 

 

 

 

 

 

 

 

static void

sleep(long millis, int nanos)
在指定的毫秒数加指定的纳秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。

 

notify():线程被唤醒,线程之间相互合作的时候,其他的线程将此线程唤醒,使其进入到就绪状态,如果抢占到CPU的资源,此线程就会得到执行。

 

运行状态-----》就绪状态:如我有两个相称在轮番的抢占CPU,CPU就会在两个线程之间来回切换,当CPU切换到线程一的时候,线程二就是就绪状态,切换到线程二的时候,线程一就由运行状态变为就绪状态,等待抢占CPU的资源。

 

 

 

 

 

 

 

四:线程的并发安全性问题:

1:并发:

在现实的项目中,由其是互联网项目,存在着大量的并发的案例:

举例:

现在有100张火车票,有四个窗口正在同时售卖火车票,四个窗口相当于四个线程,四个线程同时在运行,但是资源只有100张火车票。所以此时我们需要来防止并发问题,有时候在卖火车票的时候,卖到了最后一张,可能会有多个窗口在同时售卖这最后一张火车票,导致最后一张票被多个人买到的情况。就会导致多个人面对同一个座位,这样显然是不行的。

 

再有网购的时候,秒杀抢购的时候,一台手机,十个人抢购,一定有人买到了,还有其他人买不到,如果我没有控制好并发,可能会导致多个人买到这一台手机,显然这是不可以的。

 

所以,对于并发我们必须要控制好。

 

 

现在有100张火车票,4个窗口在售卖火车票。四个窗口卖出的火车票总和一定不能大于100张。

 

分析:四个窗口,四个线程同时在运行,100张火车票是四个线程的共享资源。

 

 

窗口2正在卖第100张票
窗口4正在卖第98张票
窗口1正在卖第100张票
窗口3正在卖第99张票
窗口1正在卖第97张票
窗口2正在卖第95张票
窗口3正在卖第96张票
窗口4正在卖第97张票
窗口2正在卖第94张票
窗口1正在卖第92张票
窗口3正在卖第93张票
窗口4正在卖第94张票
窗口4正在卖第90张票
窗口1正在卖第88张票
窗口2正在卖第89张票
窗口3正在卖第91张票
窗口2正在卖第87张票
窗口4正在卖第86张票
窗口3正在卖第87张票
窗口1正在卖第87张票
这里,有多个窗口卖同一张票,最后我卖出去的总票数肯定是大于100张的。

上面的结果,我已经处理了,但是还是会出现并发问题,

 

 

 

 

100张火车票

 

 

 

 

 

 

 

 

 

 

 

线程一                                                线程二

 

现在,两个线程同时启动,两个线程处于准备就绪的状态,俩个线程会去抢CPU的资源,一个线程会抢到。抢到资源后,线程一会休眠10毫秒,CPU的资源此时是会让出来的,线程二就会使用CPU的资源,线程二也会休眠。之后两个线程休眠时间到了之后,会唤醒。此时就可能会同时去执行售票的操作,对票的总数进行减票数的操作。

这里虽然有线程的先后进入的顺序,但是这种速度是很快的,我们根本察觉不到,当多个线程使用同一个资源的时候,就可能会出现经过休眠之后,同时对同一数据进行操作,即使我没有使用休眠,还是可能会出现这种情况。

public class SaleTicket extends Thread{
	
	/*
	 * 现在,我需要对100张火车票进行属性的定义,如果我将其定义在当前类中,
	 * private int tickets = 100;这是一个对象属性,现在我需要创建四个线程,
	 * 因为着我每创建出来的一个线程,也就是一个窗口,都有100张票,但是现在应该是
	 * 4个窗口总共100张票。所以这个票的数量我们应该将其定义为类的属性,也就是定义的属性要使用static。
	 * 这样就可以使用四个线程共享100张火车票
	 */
	
	private static int tickets = 100;
	
	private String name;
	
	
	
	
	public SaleTicket(String name) {
		super(name);
	}




	@Override
	public void run() {
		
		while(true){
			if(tickets > 0){
				try {
					Thread.sleep(10);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(this.getName()+"正在卖第"+tickets--+"张票");
			}else{
				System.out.println("票已售馨");
				break;
			}
			
		}
	}

}

public class Test {
	
	
    public static void main(String[] args) {
    	
    	/*
		 * 创建四个线程,也就是4个售票的窗口
		 */
		SaleTicket st1 = new SaleTicket("窗口1");
		SaleTicket st2 = new SaleTicket("窗口2");
		SaleTicket st3 = new SaleTicket("窗口3");
		SaleTicket st4 = new SaleTicket("窗口4");
		
		/*
		 * 开启四个线程
		 */
		
		st1.start();
		st2.start();
		st3.start();
		st4.start();
    	
    }
}

2:synchronized解决并发问题:

 

线程:并发安全性问题

 

 

 

 

 

 

 

对于并发问题的解决,我们采用同步:通过同步(就是要加锁,共享资源只能一个线程访问使用)锁来解决。

 

 

洗手间:

 

       
   
     
 

 

 

 

 

 

 

现在多个人要上洗手间,假设一次只能一个人上洗手间。洗手间就是共享额资源,上洗手间的每一个人都是一个线程。这个资源,每一次只能有一个线程进行操作。

现在有一个人进入了洗手间,这个人就要把洗手间的门锁上,那么其他人就肯定进不去,只有当这个人上完洗手间后,打开锁,出来之后,第二个人再上,第二个人也会将门锁上。

综上,共享资源,一个线程在使用的时候,需要使用锁,将共享资源锁上。这样就不会出现一次有多个线程使用同一个共享资源的情况了。

 

我们上面的售票,一张票只能有一个窗口售卖,就要求我要在资源上加锁。

 

线程:并发安全性问题

 

使用同步锁的格式:

同步锁的使用格式不止一种。

synchronized(锁对象){

  

     操作共享资源的代码

}

 

锁对象:相当于门上的锁,这个锁对象是多个线程共享的,谁拿到这把锁,就可以访问共享数据。不能出现一个线程一把锁的情况。也可以理解为锁的钥匙。

synchronized(同步代码)使用的位置:

1:代码被多个线程访问

2:代码中有共享的数据

3:共享数据被多条语句操作

 

线程:并发安全性问题

 

 

窗口2正在卖第100张票
窗口2正在卖第99张票
窗口2正在卖第98张票
窗口2正在卖第97张票
窗口2正在卖第96张票
窗口2正在卖第95张票
窗口2正在卖第94张票
窗口2正在卖第93张票
窗口2正在卖第92张票
窗口2正在卖第91张票
窗口2正在卖第90张票
窗口2正在卖第89张票
窗口2正在卖第88张票
窗口2正在卖第87张票
窗口2正在卖第86张票
窗口2正在卖第85张票
窗口2正在卖第84张票
窗口2正在卖第83张票
窗口2正在卖第82张票
窗口2正在卖第81张票
窗口2正在卖第80张票
窗口2正在卖第79张票
窗口2正在卖第78张票
窗口2正在卖第77张票
窗口2正在卖第76张票
窗口2正在卖第75张票
窗口2正在卖第74张票
窗口2正在卖第73张票
窗口2正在卖第72张票
窗口2正在卖第71张票
窗口2正在卖第70张票
窗口2正在卖第69张票
窗口2正在卖第68张票
窗口2正在卖第67张票
窗口2正在卖第66张票
窗口2正在卖第65张票
窗口2正在卖第64张票
窗口2正在卖第63张票
窗口2正在卖第62张票
窗口2正在卖第61张票
窗口2正在卖第60张票
窗口2正在卖第59张票
窗口2正在卖第58张票
窗口2正在卖第57张票
窗口2正在卖第56张票
窗口2正在卖第55张票
窗口2正在卖第54张票
窗口2正在卖第53张票
窗口2正在卖第52张票
窗口2正在卖第51张票
窗口2正在卖第50张票
窗口2正在卖第49张票
窗口2正在卖第48张票
窗口2正在卖第47张票
窗口2正在卖第46张票
窗口2正在卖第45张票
窗口2正在卖第44张票
窗口2正在卖第43张票
窗口2正在卖第42张票
窗口2正在卖第41张票
窗口2正在卖第40张票
窗口2正在卖第39张票
窗口2正在卖第38张票
窗口2正在卖第37张票
窗口2正在卖第36张票
窗口2正在卖第35张票
窗口2正在卖第34张票
窗口2正在卖第33张票
窗口2正在卖第32张票
窗口2正在卖第31张票
窗口2正在卖第30张票
窗口2正在卖第29张票
窗口2正在卖第28张票
窗口2正在卖第27张票
窗口2正在卖第26张票
窗口2正在卖第25张票
窗口2正在卖第24张票
窗口2正在卖第23张票
窗口2正在卖第22张票
窗口2正在卖第21张票
窗口2正在卖第20张票
窗口2正在卖第19张票
窗口2正在卖第18张票
窗口2正在卖第17张票
窗口2正在卖第16张票
窗口2正在卖第15张票
窗口2正在卖第14张票
窗口2正在卖第13张票
窗口2正在卖第12张票
窗口2正在卖第11张票
窗口2正在卖第10张票
窗口2正在卖第9张票
窗口2正在卖第8张票
窗口2正在卖第7张票
窗口2正在卖第6张票
窗口2正在卖第5张票
窗口2正在卖第4张票
窗口2正在卖第3张票
窗口2正在卖第2张票
窗口2正在卖第1张票
票已售馨
票已售馨
票已售馨
票已售馨

public class SaleTicket extends Thread{

   

    /*

     * 现在,我需要对100张火车票进行属性的定义,如果我将其定义在当前类中,

     * private int tickets = 100;这是一个对象属性,现在我需要创建四个线程,

     * 因为着我每创建出来的一个线程,也就是一个窗口,都有100张票,但是现在应该是

     * 4个窗口总共100张票。所以这个票的数量我们应该将其定义为类的属性,也就是定义的属性要使用static

     * 这样就可以使用四个线程共享100张火车票

     */

   

    private static int tickets = 100;

   

    private String name;

   

    //创建共享锁对象,现在是static的,多个线程之间肯定是共享这个对象的。

    private static Object obj = new Object();

   

   

    public SaleTicket(String name) {

        super(name);

    }

 

 

 

 

    @Override

    public void run() {

       

        while(true){

            /*

             * 括号中的锁对象:一定要是一个共享的对象。一个线程获得锁对象之后

             * 就可以访问以及使用共享资源了,其他的线程就不可以使用以及访问了

             * 当前线程执行完毕后,下一个线程才可以访问以及使用共享资源

             */

           

            //同步代码块

            synchronized (obj) {

                if(tickets > 0){

                   try {

                       Thread.sleep(10);

                   } catch (InterruptedException e) {

                       e.printStackTrace();

                   }

                   System.out.println(this.getName()+"正在卖第"+tickets--+"张票");

                }else{

                   System.out.println("票已售馨");

                   break;

                }

               

            }

           

           

        }

    }

 

}