黑马程序员---多线程及其安全问题

------- Java、.NetAndroid培训期待与您交流!------- 

一、概述

        进程:正在执行的程序。

        线程:是进程中用于控制程序执行的控制单元(执行路径、执行情景)进程中至少有一个线程。

        对于JVM,启动时,主要有两个线程:jvm的主线程(存在于main方法中)jvm的垃圾回收线程。

 1、如何在程序中自定义线程呢?

        Java给我们提供了对象线程这类事物的描述,该类是Thread该类中定义了,创建线程对象的方法(构造函数),提供了要被线程执行的代码存储的位置(run( )),还定义了开启线程运行的方法(start( )),同时还有一些其他的方法用于操作线程:

                static Thread currentThread();

                String getName();

                static void sleep(time)throws InterruptedException;

  1>要运行的代码都是后期定义的。

        所以创建线程的第一种方式是:继承Thread类。原因:要覆盖run方法,定义线程要运行的代码。

        步骤:

                1,继承Thread类。

                2,覆盖run方法。将线程要运行的代码定义其中。

                3,创建Thread类的子类对象,其实就是在创建线程,调用start方法。

    示例:创建两个线程,和主线程交替运行。
    思路:
    创建线程的方式一:继承Thread类,覆盖Thread中的run方法,通过创建子类对象来实现多线程。该方式在实现多线程时不能实现数据共享。比如说卖车票是多线程,如果用此方法那么将会导致卖出的票比实际票数多出:创建线程的个数 * 实际票数。
    步骤:
        1.创建Thread类的子类Test1,覆盖run方法;
        2.在main方法中创建两个Thread子类对象;
        3.分别调用start方法,开启线程,start就会自动调用Test1类中的run方法。
        4.在main方法中定义一个循环,满足在main方法中创建主线程。

class Test1 extends Thread
{
	//private String name;
	Test1(String name)
	{
		//this.name = name;	//通过构造函数传值来指定线程名。
		super(name);	//指定向父类Thread中的构造函数Thread(String name)赋值。
	}
	public void run()	//覆盖run方法。
	{
		for (int i=0; i<60; i++)
		{
			//System.out.println(name+"...test run:"+i);
			//Thread.currentThread()中Thread可省,currentThread:获取当前线程对象。
			System.out.println((Thread.currentThread()==this)+"..."+this.getName()+"...test run:"+i);
		}
	}
}

class ThreadTest1
{
	public static void main(String[] args) 
	{
		Test1 t1 = new Test1("One");	//创建一个对象就是建立一个线程。
		Test1 t2 = new Test1("Two");
		t1.start();	//开启线程,并调用该线程的run方法。并且mian方法继续执行后面的语句t2.start();。
		t2.start();
		//t.run();	//仅仅是对象调用方法,而线程创建好了并没有运行。即run方法成了主线程。
		//仅当run运行完成之后才才会执行main方法中的t.run()之后的语句。

		for(int i=0; i<60; i++)
			System.out.println("Hello world..."+i);
	}
}

  2>如果自定义的类中有多线程要运行的代码。但是该类有自己的父类。那么就不可以在继承Thread。怎么办呢?

        Java给我们提供了一个规则:Runnable接口。

        如果自定义类不继承Thread,也可以实现Runnable接口,并将多线程要运行的代码存放在Runnablerun方法中。这样多线程也可以帮助该类运行。

        这样的操作有一个好处:避免了单继承的局限性。

        创建线程的第二种方式:实现Runnable接口。

        步骤:

                1,定义类实现Runnable接口。

                2,覆盖接口的run方法,将多线程要运行的代码存入其中。

                3,创建Thread类的对象(创建线程),并将Runnable接口的子类对象作为参数传递给Thread的构造函数。

                为什么要Runnable接口的子类实例对象作为参数传递给Thread对象?因为线程要运行的代码都在Runnable子类的run方法中存储,所以要将该run方法所属的对象传递给Thread,让Thread线程去使用该对象调用其run方法。

                4,调用Thread对象的start方法,开启线程。

        实现Runnable接口的方式,避免了单继承的局限性,所以创建线程建议使用第二种方式。

  3>作为了解

        线程的状态。

                1,被创建。

                2,运行。

                3,冻结。

                4,消亡。

        其实还有一种特殊的状态:临时状态。特点:具备了执行资格,但不具备执行权。

        冻结状态的特点:放弃了执行资格。

  4>示例

        需求:简单的卖票程序,实现多个窗口同时卖票(这个地方需要满足票数资源共享,不能使用第一种方式)
        思路:
        创建线程的方式二:(常用)
                实现Runnable接口,该方法可以满足实现票数资源共享。避免单继承的局限性。
        步骤:
                1.定义类实现Runnable接口;
                2.覆盖Runnable接口中的run方法(将线程要运行的代码存放在run方法中)
                3.通过Thread类建立线程对象:Thread th = new Thread(t);
                4.将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数(Thread(Runnable target)),保证票数对象唯一,即只创建一个接口子类的对象,满足资源共享。
                5.调用Thread类的start方法开启线程并调用Runnable接口子类中的run方法。

class Test2 implements Runnable	//定义类实现Runnable
{
	private int ticket = 100;	//声明变量并初始化票数
	//Object o = new Object();
	public void run()  //覆盖run方法
	{
		while (true)  // ticket>0
		{
			synchronized(this)	 //同步代码块
			{
				if (ticket>0)
				{
					//睡眠10ms,这样能将安全问题扩大化,检验是否出现负票和重票。
					try{Thread.sleep(10);}catch(Exception e){}
					System.out.println(Thread.currentThread().getName()+"_"+"卖票:"+ticket--);
				}
			}
		}
		//System.out.println("No tickets."); //该语句在ticket<0时会被执行四次,每个对象执行一次。
	}
}

class  ThreadTest2
{
	public static void main(String[] args) 
	{
		Test2 t = new Test2();   //创建接口子类对象,保证对象唯一。

		Thread t1 = new Thread(t);   //创建线程对象,并传值。
		Thread t2 = new Thread(t);
		Thread t3 = new Thread(t);
		Thread t4 = new Thread(t);
		t1.start();		//开启线程,并执行Runnable接口子类中的run方法。
		t2.start();
		t3.start();
		t4.start();
	}
}

二、synchronized(同步)

 1、同步安全问题:

        多线程具备随机性。因为是由cpu不断的快速切换造成的,所以有可能会产生多线程的安全问题。

        问题的产生的原因,几个关键点:

                1,多线程代码中有操作共享数据。

                2,多条语句操作该共享数据。

        当具备两个关键点时,有一个线程对多条操作共享数据的代码执行的一部分,还没有执行完,另一个线程开始参与执行,就会发生数据错误。

 2、解决方法:

        当一个线程在执行多条操作共享数据代码时,其他线程即使获取了执行权,也不可以参与操作。

        Java就对这种解决方式提供了专业的代码:同步synchronized

        同步的原理:就是将部分操作功能数据的代码进行加锁。

        经典示例:火车上的卫生间。

        同步的表现形式:

                1,同步代码块。

                2,同步函数。

        两者有什么不同:

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

                同步函数使用的锁是this

        注意:对于static的同步函数,使用的锁不是this,是:类名.class,是该类的字节码文件对象。

        涉及到了单例设计模式的懒汉式。

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

        弊端:较为消耗资源。同步嵌套后,容易死锁。

        要记住:同步使用的前提:

                1,必须是两个或者两个以上的线程。

                2,必须是多个线程使用同一个锁。这时才可以称为这些线程被同步了。

 3、死锁:

        死锁出现的前提:同步中嵌套同步,至少是有两个嵌套锁

        原因:嵌套锁中内外锁的持有的对象不同,且两个不同嵌套锁内外锁持有对象刚好相反,出现锁的互相调用。   

        死锁代码一定要会写,但开发时一定注意避免。

class Ticket implements Runnable
{
	private int tick = 1000;
	Object o = new Object();
	boolean flag = true;
	public void run()
	{
		if (flag)
			while (true)
				synchronized (o)//嵌套锁,外锁同步代码块对象为Object类型;
				{
					//--->
					show();//内锁同步函数对象为this;
				}
		else
			while (true)
				show();
	}
	public synchronized void show()//this
	{
		synchronized (o)//内锁,同步代码块对象是Object类型;
		{
			if (tick>0)
			{
				System.out.println(Thread.currentThread().getName()+"...show:"+tick--);
			}
		}
	}
}

class  DeadLock
{
	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(5);}catch(Exception e){}
		t.flag = false;
		t2.start();
	}
}

        总结:多线程在后期程序编码中会经常遇到,理解其特点及其两种创建线程的方式,run方法和start方法的区别:调用前者,主线程会直接执行run方法,知道执行完后才运行main方法中后面的语句,其实就相当于一个单线程;而调用后者,主线程执行的是start方法,再由start方法调用自定义的run方法,而主线程会继续执行main方法中后面的语句,与run方法无关,通过这种机制能够实现多线程。但是在多线程中,当出现多个线程共享同一个数据时,那么会出现安全问题,这就是我们使用synchronized来避免问题的原因。当出现多重同步嵌套时,有可能会导致死锁,这是在开发中不希望看到的。所以理解死锁产生的原理,怎么解决死锁是必须掌握的,也是面试的一个考点。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值