Java中的多线程(创建方式、安全问题、同步、死锁)

/*学习笔记 */

——多线程

简述

进程:正在进行中的程序(直译).

线程:就是进程中一个负责程序执行的控制单元(执行路径)
一个进程中可以有多个执行路径,称之为多线程。

一个进程中至少要有一个线程。

开启多个线程是为了同时运行多部分代码。

每一个线程都有自己运行的内容。这个内容可以称为线程要执行的任务。

多线程好处:解决了多部分同时运行的问题。

多线程的弊端:线程太多回到效率的降低。

其实应用程序的执行都是cpu在做着快速的切换完成的。这个切换是随机的。

JVM启动时就启动了多个线程,至少有两个线程可以分析出来。

1,执行main函数的线程,
该线程的任务代码都定义在main函数中。

2,负责垃圾回收的线程。

如何创建一个线程呢?

创建线程的方式

方式一:继承Thread类。

步骤:
1,定义一个类继承Thread类。
2,覆盖Thread类中的run方法。
3,直接创建Thread的子类对象创建线程。
4,调用start方法开启线程并调用线程的任务run方法执行。

创建线程的目的是为了开启一条执行路径,去运行指定的代码和其他代码实现同时运行。

而运行的指定代码就是这个执行路径的任务。

jvm创建的主线程的任务都定义在了主函数中。

而自定义的线程它的任务在哪儿呢?
Thread类用于描述线程,线程是需要任务的。所以Thread类也有对任务的描述。
这个任务就通过Thread类中的run方法来体现。也就是说,run方法就是封装自定义线程运行任务的函数。

run方法中定义就是线程要运行的任务代码。

开启线程是为了运行指定代码,所以只有继承Thread类,并复写run方法。
将运行的代码定义在run方法中即可。

注意,用strat方法来开启线程并调用run方法。
以下是一个创建并开启线程的例子:

    class Demo extends Thread
    {
    	private String name;
    	Demo(String name)
    	{
    		
    		this.name = name;
    	}
    	public void run()
    	{
    		for(int x=0; x<10; x++)
    		{
    			for(int y=-9999999; y<999999999; y++){}   //这是用来延时的
    			System.out.println(name+"....x="+x);
    		}
    	}
    }
    
    class ThreadDemo2 
    {
    	public static void main(String[] args) 
    	{
    		Demo d1 = new Demo("旺财");
    		Demo d2 = new Demo("xiaoqiang");
    		d1.start();//开启线程,调用run方法。
    		
    		d2.start();
    		
    	}
    }

从图中可以看出,两个线程在交叉进行

可以通过Thread的getName获取线程的名称 Thread-编号(从0开始)

把输出语句换成如下的语句(加上了getName 方法),并运行之:

     System.out.println(name+"....x="+x+".....name="+getName());

获得了线程的名字

还可以使用Thread.currentThread().getName()方法,获得当前线程的名称:

    //部分代码
        class Demo extends Thread
        {
        	private String name;
        	Demo(String name)
        	{
        		super(name); //直接调用父类中的构造函数
        	
        	}
        	public void run()
        	{
        		for(int x=0; x<10; x++)
        		{
        			System.out.println(name+"....x="+x+".....name="+Thread.currentThread().getName());
        		}
        	}
        }

运行结果
多线程运行图解(以上面的程序为例):
在这里插入图片描述
如图,三个线程是同时进行的。
在以上代码的基础上,在main方法中写入一条异常代码,并运行之:

    System.out.println(4/0);

在这里插入图片描述
可以看到,虽然main方法出现并报告了异常,但是其它两个线程依然在运行。说明单个线程的结束并不会影响其它线程的进行。

方式二:实现Runnable接口。

这种方式的步骤如下:
1,定义类实现Runnable接口。
2,覆盖接口中的run方法,将线程的任务代码封装到run方法中。
3,通过Thread类创建线程对象,并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递。
为什么?因为线程的任务都封装在Runnable接口子类对象的run方法中。
所以要在线程对象创建时就必须明确要运行的任务。

4,调用线程对象的start方法开启线程。
示例代码如下:

    class Demo implements Runnable //准备扩展Demo类的功能,让其中的内容可以作为线程的任务执行。
    					//通过接口的形式完成。
    {
    	public void run()  //覆盖run方法
    	{
    		show();
    	}
    	public void show()
    	{
    		for(int x=0; x<20; x++)
    		{
    			System.out.println(Thread.currentThread().getName()+"....."+x);
    		}
    	}
    }
    
    
    class  ThreadDemo
    {
    	public static void main(String[] args) 
    	{	
    		Demo d = new Demo();
    		Thread t1 = new Thread(d);//此处传入的参数是一个Runnable对象
    		Thread t2 = new Thread(d);
    		t1.start();
    		t2.start();
    	}
    }

运行结果如下:
在这里插入图片描述

实现Runnable接口的好处:
1,将线程的任务从线程的子类中分离出来,进行了单独的封装。
按照面向对象的思想将任务的封装成对象。
2,避免了java单继承的局限性。(继承了某个父类后就无法继承Thread类了)

所以,创建线程的第二种方式较为常用。

线程的状态

以下为线程状态的图解。
在这里插入图片描述
同一时间内,CPU只能赋予唯一一个线程以执行权。而其它线程将会进入临时阻塞状态。不同的资料中还会对线程的状态进行细分,而最重要的就是红框中的三种状态,它们是可以互相转化的。

多线程应用示例

——几个人同时卖同一种票,票的总量是固定的:

   class Ticket implements Runnable  //不使用继承的做法,因为只需要一个对象、多个线程
    {
    	private  int num = 100;
    
   
    	public void run()
    	{
    		while(true)
    		{
    		
    				if(num>0)
    				{    					
    					System.out.println(Thread.currentThread().getName()+".....sale...."+num--);
    				}    			
    		}
    	}
    }
    
    
    class  TicketDemo
    {
    	public static void main(String[] args) 
    	{
    
    		Ticket t = new Ticket();//创建一个线程任务对象。
    
    		Thread t1 = new Thread(t);
    		Thread t2 = new Thread(t);
    		Thread t3 = new Thread(t);
    		Thread t4 = new Thread(t);
    
    		t1.start();
    		t2.start();
    		t3.start();
    		t4.start();
    		
    	}
    }

运行结果:
在这里插入图片描述

多线程安全问题

多线程安全问题产生的原因:

1,多个线程在操作共享的数据。
2,操作共享数据的线程代码有多条。

当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算。
就会导致线程安全问题的产生。

解决思路:
就是将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码的时候,其他线程是不可以参与运算的。
必须要当前线程把这些代码都执行完毕后,其他线程才可以参与运算。

在java中,用同步代码块就可以解决这个问题。

同步

Java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查), 将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用, 从而保证了该变量的唯一性和准确性。

同步的好处:解决了线程的安全问题。

同步的弊端:相对降低了效率,因为同步外的线程的都会判断同步锁。

同步的前提:同步中必须有多个线程并使用同一个

两种使用同步的方法:
1,同步代码块

    synchronized(对象) //括号内的就是同步锁
    {
    	需要被同步的代码 ;
    }

2,同步函数:在函数加synchronized关键字。如:

  public synchronized void show() 

同步应用示例

依然使用的卖票的例子。但是加入了同步代码块和同步函数。

     class Ticket implements Runnable
        {
        	private  int num = 100;
        	boolean flag = true;
        	public void run()
        	{
    
    		if(flag)
    			while(true)
    			{
    				synchronized(this)  //同步代码块
    				{
    					if(num>0)
    					{
    						try{Thread.sleep(10);}catch (InterruptedException e){}						
    						System.out.println(Thread.currentThread().getName()+".....obj...."+num--);
    					}
    				}
    			}
    		else
    			while(true)
    				this.show();
    	}
    
    	public synchronized void show() //同步函数
    	{
    		if(num>0)
    		{
    			try{Thread.sleep(10);}catch (InterruptedException e){}
    			
    			System.out.println(Thread.currentThread().getName()+".....function...."+num--);
    		}
    	}
    }
    
    class SynFunctionLockDemo 
    {
    	public static void main(String[] args) 
    	{
    		Ticket t = new Ticket();
    		Thread t1 = new Thread(t);
    		Thread t2 = new Thread(t);
    
    		t1.start();
    		try{Thread.sleep(10);}catch(InterruptedException e){} //给第二个线程运行的机会
    		t.flag = false;
    		t2.start();
    	}
    }

在这里插入图片描述
同步函数的使用的锁是this;

同步函数和同步代码块的区别:
同步函数的锁是固定的this。

同步代码块的锁是任意的对象。

建议使用同步代码块。

静态的同步函数使用的锁是——该函数所属字节码文件对象
可以用 getClass方法获取,也可以用当前 类名.class 表示。

死锁示例

死锁:常见情景之一就是同步的嵌套。

    class Test implements Runnable
    {
    	private boolean flag;
    	Test(boolean flag)
    	{
    		this.flag = flag;
    	}
    
    	public void run()
    	{
    		
    		if(flag)
    		{
    			while(true)
    				synchronized(MyLock.locka)
    				{
    					System.out.println(Thread.currentThread().getName()+"..if   locka....");
    					synchronized(MyLock.lockb)				{
    						
    						System.out.println(Thread.currentThread().getName()+"..if   lockb....");
    					}
    				}
    		}
    		else
    		{
    			while(true)			
    				synchronized(MyLock.lockb)
    				{
    					System.out.println(Thread.currentThread().getName()+"..else  lockb....");
    					synchronized(MyLock.locka)
    					{
    						System.out.println(Thread.currentThread().getName()+"..else   locka....");
    					}
    				}
    		}
    
    	}
    
    }
    
    class MyLock
    {
    	public static final Object locka = new Object();
    	public static final Object lockb = new Object(); //两个锁
    }

    class DeadLockTest 
    {
    	public static void main(String[] args) 
    	{
    		Test a = new Test(true);
    		Test b = new Test(false);
    
    		Thread t1 = new Thread(a);
    		Thread t2 = new Thread(b);
    		t1.start();
    		t2.start();
    	}
    }

运行结果如下,由于两个进程都拿到了同步锁并且不释放,造成循环无法进行下去的现象。
在这里插入图片描述

多线程下的单例模式

    //饿汉式
    class Single
    {
    	private static final Single s = new Single();
    	private Single(){}
    	public static Single getInstance()
    	{
    		return s; //在多线程状态下不能保证只有一个对象,出现问题
    	}
    }
    
    
    
    //懒汉式
    
    加入同步为了解决多线程安全问题。
    
    加入双重判断是为了解决效率问题。
    
    
    
    
    class Single
    {
    	private static Single s = null;
    
    	private Single(){}
    
    	public static Single getInstance()
    	{
    		if(s==null)
    		{
    			synchronized(Single.class)		
    			{
    				if(s==null)
    					s = new Single();
    			}
    		}
    		return s;
    	}
    }
    
    class  SingleDemo
    {
    	public static void main(String[] args) 
    	{
    		System.out.println("Hello World!");
    	}
    }

猜你喜欢

转载自blog.csdn.net/z714405489/article/details/82793350