黑马程序员:多线程的介绍、应用及由安全问题引出的同步代码块介绍

---------------------- ASP.Net+Android+IOS开发.Net培训、期待与您交流! ----------------------进程:是一个正在执行中的程序。每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元
线程:进程中一个独立的控制单元,实质上是线程在控制着进程的执行,一个进程中至少有一个线程

JVM Java Virtual Machine 启动的时候会有一个进程java.exe
该进程中至少有一个线程负责java程序的执行
而且这个线程运行的代码存在于main方法中
该线程称之为主线程

扩展:其实详细分析JVM运行情况发现,JVM启动不止一个线程,至少还有一个负责垃圾回收机制的线程

创建线程的第一种方式:继承Thread类,并覆盖其run()方法
步骤:
1.定义类继承Thread
2.覆盖Thread类中的run(),目的是将自定义的代码存储在run方法中,让自定义线程运行
3.调用线程使用start(),start()有两个作用:启动线程,调用run方法
class Demo extends Thread
{
	public void run()
	{
		for (int x=0; x<60; x++)
			System.out.println("Demo Run---"+x); //由d线程执行
	}
}
class ThreadDemo
{
	public static void main(String[] args)
	{
		Demo d = new Demo(); //创建好一个线程
		d.start(); //启动线程,调用run()。若直接用d.run();则不会启动自定义线程
		for (int x=0; x<60; x++)
			System.out.println("ThreadDemo Run---"+x); //由main函数线程执行
	}
}

以上代码执行后,每一次结果都不同,原因:
多个线程都获取CPU的执行权,CPU执行到谁,谁就运行。(在某一个时刻,只能有一个程序在运行,除非是多核CPU)
即CPU在做快速切换。
从而引出多线程的一个特性:随机性(哪个线程执行多久,CPU说了算)
为什么要覆盖run方法?
Thread类用于描述线程,该类就定义了一个方法,用于存储线程要运行的代码,该存储方法就是run方法
即Thread类中的run方法,用于存储线程要运行的代码;如主线程运行的就是main函数下的代码
示例:
创建两个线程,和主线程交替运行
class Demo extends Thread
{
	private String name;
	Demo(){}
	Demo(String name)
	{
		this.name = name;
	}
	public void run()
	{
		for (int x=0; x<100; x++)
			System.out.println(name+"---"+x);
	}
}
class ThreadDemo
{
	public static void main(String[] args)
	{
		Demo d = new Demo("第1个自定义线程");
		Demo d2 = new Demo("第2个自定义线程"); //创建第二个线程
		d.start(); //线程1启动并执行run()
		d2.start(); //线程2启动并执行run()
		for (int x=0; x<100; x++)
			System.out.println("ThreadDemo Run---"+x);
	}
}

获取线程默认名称的方法:getName()
线程默认名称:Thread-编号,编号从0开始
示例:
class Demo extends Thread
{
	Demo(){}
	Demo(String name)
	{
		super(name);  //Thread中有构造函数 Thread(String name),所以这里直接拿过来用
	}
	public void run()
	{
		for (int x=0; x<100; x++)
			System.out.println(this.getName()+"-"+x);
			/*
			 * this.getName()==Thread.currentThread().getName(),
			 * 注意:this的对象为继承Thread的情况下可以使用this关键字,
			 * 否则只能使用Thread.currentThread().getName(),通过Runnable接口
			 * 实现多线程方式中就需通过这种方式
			 */
	}
}
class ThreadDemo
{
	public static void main(String[] args)
	{
		Demo d = new Demo("第1个自定义线程");
		Demo d2 = new Demo("第2个自定义线程"); //创建第二个线程
		d.start(); //线程1启动并执行run()
		d2.start(); //线程2启动并执行run()
		for (int x=0; x<100; x++)
			System.out.println("ThreadDemo Run---"+x);
	}
}

static Thread currentThread() 获取当前线程对象


创建线程的第二种方式:实现Runnable接口
步骤:
1.定义类实现Runnable接口
2.覆盖Runnable接口中的run方法
3.通过Thread类建立线程对象
4.将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数
5.调用Thread类的start方法开启线程并调用Runnable接口子类的run方法;

多线程应用:
简单的卖票程序:多窗口同时售票,共100张票
第一种方式:
class Some extends Thread
{	
	private static int tick = 100; //static关键字:各线程共享的数据。若不加static,总共就会卖出400张票
	public void run()
	{
		while (tick > 0)
		{	
			System.out.println(currentThread().getName()+"..."+tick--);
		}
	}
}
class ThreadDemo
{
	public static void main(String[] args)
	{
		Some s1 = new Some();
		Some s2 = new Some();
		Some s3 = new Some();
		Some s4 = new Some();
		s1.start();
		s2.start();
		s3.start();
		s4.start();
	}
}

第二种方式:
class Some implements Runnable
{	
	/*
	 *这里的成员变量tick不用加static修饰符是因为再ThreadDemo类中,各个线程同时使用的是一个对象s
	 *而上例中,每个线程使用的是不同的Some对象s1-s4
	 */
	private int tick = 100; 
	public void run()
	{
		while (tick > 0)
		{	
			System.out.println(Thread.currentThread().getName()+"..."+tick--);
		}
	}
}
class ThreadDemo
{
	public static void main(String[] args)
	{
		Some s = new Some();
		Thread t1 = new Thread(s);
		Thread t2 = new Thread(s);
		Thread t3 = new Thread(s);
		Thread t4 = new Thread(s);
		t1.start();
		t2.start();
		t3.start();
		t4.start();


	}
}

实现方式和继承方式有什么区别?  面试常见
实现方式的好处:避免了单继承的局限性,在定义线程时,建议使用实现方式。
因为如果使用继承方式,那么继承Thread的就不能继承其他类了

两种方式存放位置区别:
继承Thread:线程代码存放在Thread子类的run方法中

实现Runnable:线程代码存放在接口的子类的run方法中


多线程的安全问题--特别重要,比如需要注意CPU切换导致参数判断问题
示例:
class Some implements Runnable
{	
	private int tick = 100; 
	public void run()
	{
		while (tick > 0)
		{	//当tick=1时,而0线程进来之后被CPU切换出去,1线程就过了tick>0的判断也会进来,后面的线程也可能会
			try{Thread.sleep(10);}catch(Exception e){}  //用sleep导致的冻结状态演示CPU切换导致线程进入阻塞状态,从而引发安全问题
			System.out.println(Thread.currentThread().getName()+"..."+tick--); //问题:tick打印出0,-1,-2等不符合正常情况的值
		}
	}
}
class ThreadDemo
{
	public static void main(String[] args)
	{
		Some s = new Some();
		Thread t1 = new Thread(s);
		Thread t2 = new Thread(s);
		Thread t3 = new Thread(s);
		Thread t4 = new Thread(s);
		t1.start();
		t2.start();
		t3.start();
		t4.start();


	}
}


关于sleep():
public static void sleep(long millis) throws InterruptedException
在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。该线程不丢失任何监视器的所属权。

以上问题原因:
当多条语句在操作同一个线程共享数据时,若一个线程对多条语句只执行了一部分,还没有完全执行,另一个线程也参与进来执行时,就会导致共享数据的错误

解决办法:
对多条操作共享数据的语句,只能让一个线程都执行完,在该线程执行过程中,其他线程不可以参与执行

Java对于多线程的安全问题提供了专业的解决方式:同步代码块
同步的前提:非常重要
1.必须要有两个或者两个以上的线程 (比如懒汉式单例设计模式)
2.必须是多个线程使用同一个锁
3.必须保证同步中只能有一个线程在运行

好处:解决了多线程的安全问题
弊端:多个线程需要判断锁,较为消耗资源
同步代码块格式:
synchronized(对象)
{
需要被同步的代码(操作共享数据的代码,但要注意循环代码)
}
注:线程进入synchronized之后,synchronized判断对象(对象如同锁,可以引用Object对象也可以自定义新的对象)的标志位是否是开,若开,则放线程进入,进入后,synchronized把该对象的标志位改为关,然后进入的线程再执行内部(synchronized内部)代码,执行完之后线程出去了,synchronized才会把标志位改为开

示例:
class Some implements Runnable
{	
	private int tick = 1000; 
	Object obj = new Object;
	public void run()
	{
		/*
		若像下面这样写,也是不合理的,因为同步锁住了一个循环,只有一个进程进去把循环结束了才能让另外的进程进去(循环结束代表票卖完,这样其他线程就一点用处都没有了,失去了多线程的意义(成了单线程))
		synchronized(obj) //也可以新建一个其他对象
		{
			while (tick > 0)
			{	
				try{Thread.sleep(10);}catch(Exception e){}
				System.out.println(Thread.currentThread().getName()+"..."+tick--);
			}
		}
		*/
		
		while (tick>0)
		{	
			synchronized(obj)
			{
				if (tick>0)
				{
					try{Thread.sleep(10);}catch(Exception e){}  
					System.out.println(Thread.currentThread().getName()+"..."+tick--); 
				}
			}
		}
	}
}
class ThreadDemo
{
	public static void main(String[] args)
	{
		Some s = new Some();
		Thread t1 = new Thread(s);
		Thread t2 = new Thread(s);
		Thread t3 = new Thread(s);
		Thread t4 = new Thread(s);
		t1.start();
		t2.start();
		t3.start();
		t4.start();


	}
}

通过synchronized解决问题示例:
需求:
银行有一个金库
有四个储户分别存2000元,每次存100,存20次
目的:该程序是否有安全问题,如果有,如何解决?
class Bank
{
	private int sum;
	Object obj = new Object();
	public void add(int n)
	{
		/*
		sum = sum + n;
		//问题可能出现的地方  当t1进来sum=100时,CPU切出去了,没有执行下面的打印语句,t2进来之后,sum=200,这时候t1恢复并执行答应语句时,本该打印sum=100的,却打印sum=200了
		try{Thread.sleep(10);}catch(Exception e){}
		System.out.println("sum="+sum);
		*/
		//解决办法如下
		synchronized(obj)
		{
			sum = sum + n;
			try{Thread.sleep(0);}catch(Exception e){} //为了测试问题时候能解决而故意加入的可能引起问题代码
			System.out.println(Thread.currentThread().getName()+"---"+"sum="+sum);
		}
	}
}
class Customer implements Runnable
{
	private Bank b = new Bank();
	public void run()
	{
		for(int x=0; x<20; x++)  //储户存20次的动作
		{
			b.add(100); //每次存100元
		}
	}
}
class BankDemo
{
	public static void main(String[] args)
	{
		Customer cus = new Customer();
		Thread t1 = new Thread(cus);
		Thread t2 = new Thread(cus);
		Thread t3 = new Thread(cus);
		Thread t4 = new Thread(cus);
		t1.start(); //第一个储户存钱
		t2.start(); //第二个储户存钱
		t3.start(); //第三个储户存钱
		t4.start(); //第四个储户存钱
	}
}

多线程如何找问题:
1.明确哪些代码是多线程运行代码 (上题中run()和add())
2.明确共享数据 (上题中共享数据 b,sum)
3.明确多线程运行代码中哪些语句是操作共享数据的(问题容易出现在多条语句之间)


上题的另一种解决方式:(同步函数的应用,也要注意循环体)
class Bank
{
	private int sum;
	Object obj = new Object();
	//解决办法如下
	public synchronized void add(int n) //通过synchronized修饰函数,让函数有同步的功能
	{
		
		sum = sum + n;
		try{Thread.sleep(10);}catch(Exception e){} //为了测试问题时候能解决而故意加入的可能引起问题代码
		System.out.println(Thread.currentThread().getName()+"---"+"sum="+sum);
		/*
		synchronized(obj) //同步代码块或者同步函数,选其一即可,但为了资源合理利用,需考虑用哪个更有效率
		{
			sum = sum + n;
			try{Thread.sleep(0);}catch(Exception e){} 
			System.out.println(Thread.currentThread().getName()+"---"+"sum="+sum);
		}
		*/
	}
}
class Customer implements Runnable
{
	private Bank b = new Bank();
	public void run()
	{
		for(int x=0; x<20; x++)  //储户存20次的动作
		{
			b.add(100); //每次存100元
		}
	}
}
class BankDemo
{
	public static void main(String[] args)
	{
		Customer cus = new Customer();
		Thread t1 = new Thread(cus);
		Thread t2 = new Thread(cus);
		Thread t3 = new Thread(cus);
		Thread t4 = new Thread(cus);
		t1.start(); //第一个储户存钱
		t2.start(); //第二个储户存钱
		t3.start(); //第三个储户存钱
		t4.start(); //第四个储户存钱
	}
}

同步函数、同步代码块用的都是什么锁?
同步代码块:用的是参数中的对象,该对象就是它的锁
synchronized(对象)
{
代码;
}
同步函数:同步函数需要被对象调用(除静态方法),函数都有一个所属对象的引用,就是this,所以非静态同步函数使用的锁是this


验证为何同步函数的锁是this?
两个线程,一个使用同步函数,一个使用同步代码块
sum出现重复值,则说明程序存在问题
class Bank
{
	private int sum;
	private boolean flag = true;
	public void setFlag(boolean flag)
	{
		this.flag = flag;
	}
	Object obj = new Object();
	public void add(int n) 
	{
		if(flag)
		{
			synchronized(obj) //obj对象的时候,输出结果sum会出现重复值。把obj改成this就不会出现重复值 
			{
				sum = sum + n;
				try{Thread.sleep(10);}catch(Exception e){} //若不用synchronized,容易出现问题的地方
				System.out.println(Thread.currentThread().getName()+"---"+"sum="+sum);
			}
		}
		else
			lock(n);
	}
	public synchronized void lock(int n)  //同步函数
	{
		sum = sum + n;
 		try{Thread.sleep(10);}catch(Exception e){} //通过此语句构造问题的存在
		System.out.println(Thread.currentThread().getName()+"---lock---"+"sum="+sum);
	}
}
class Customer implements Runnable
{
	private Bank b = new Bank();
	public void flaseFlag()
	{
		b.setFlag(false);
	}
	public void run()
	{
		for(int x=0; x<10; x++) 
		{
			b.add(100); 
		}
	}
}
class BankDemo
{
	public static void main(String[] args)
	{
		Customer cus = new Customer();
		Thread t1 = new Thread(cus);
		Thread t2 = new Thread(cus);
		t1.start(); //第一个储户存钱
		try{Thread.sleep(10);}catch(Exception e){} //为了避免主线程早于t1执行,若早于t1,则会出现全走同步函数路线,导致实验无法证明结果(原因,t1还没判断flag的时候,主线程却已经执行过cus.flaseFlag()导致flag变成false,等t1再判断的时候flag因是false而选择lock()路线)
		cus.flaseFlag();
		t2.start(); //第二个储户存钱
	}
}

如果同步函数被静态修饰,使用的锁是该函数所在类的字节码文件对象 该同步函数所在类类名.class 该对象类型Class
注:静态方法中使用同步代码块,代码块中的对象也应该是该类所属的字节码文件对象,即方法所在类类名.class
原因:
1.静态方法中不可以定义this
2.静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象
示例:
class Bank
{
	private static int sum;
	private boolean flag = true;
	public void setFlag(boolean flag)
	{
		this.flag = flag;
	}
	Object obj = new Object();
	public void add(int n) 
	{
		if(flag)
		{
			synchronized(Bank.class) //该同步函数所在类类名.class 该对象类型Class 
			{
				sum = sum + n;
				try{Thread.sleep(10);}catch(Exception e){} 
				System.out.println(Thread.currentThread().getName()+"---"+"sum="+sum);
			}
		}
		else
			lock(n);
	}
	public static synchronized void lock(int n) //静态同步函数
	{
		sum = sum + n;
 		try{Thread.sleep(10);}catch(Exception e){} 
		System.out.println(Thread.currentThread().getName()+"---lock---"+"sum="+sum);
	}
}
class Customer implements Runnable
{
	private Bank b = new Bank();
	public void flaseFlag()
	{
		b.setFlag(false);
	}
	public void run()
	{
		for(int x=0; x<10; x++) 
		{
			b.add(100); 
		}
	}
}
class BankDemo
{
	public static void main(String[] args)
	{
		Customer cus = new Customer();
		Thread t1 = new Thread(cus);
		Thread t2 = new Thread(cus);
		t1.start(); //第一个储户存钱
		try{Thread.sleep(10);}catch(Exception e){}
		cus.flaseFlag();
		t2.start(); //第二个储户存钱
	}
}

当两个线程在类中单独存在时,同步锁就需要更加注意它的前提了
1.两个线程或以上
2.同一锁(对象)


示例:
class Res
{
	String name;
	String sex;
}
class Input implements Runnable
{
	private Res r;
	Input(Res r)
	{
		this.r = r;
	}
	public void run()
	{
		int x = 0;
		while(true)
		{
			synchronized(r) //在Input类中其实只有t1线程,要使用synchronized,必须两个或两个以上线程
			{
				if(x==0)
				{
						r.name = "mike";
						r.sex = "man";
				}
				else
				{
						r.name = "小红";
						r.sex = "女";
				}
			}
				x = (x+1)%2; //为了if和else都能执行到
		}
	}
}
class Output implements Runnable
{
	private Res r;
	Output(Res r)
	{
		this.r = r;
	}
	public void run()
	{
		while(true)
			synchronized(r) //t2线程操作的也是Res资源,和t1操作的是同一资源,所以这里也要同步,这样就构成了两个线程。里面的锁r是内存中单独存在的对象new Res(),都已在Input和Output构造函数中导入
			{
				System.out.println(r.name+"...."+r.sex);
			}
	}
}


class InputOutputDemo
{
	public static void main(String[] args)
	{
		Res r = new Res();
		Input in = new Input(r);
		Output out = new Output(r);
		Thread t1 = new Thread(in);
		Thread t2 = new Thread(out);
		t1.start();
		t2.start();
	}
}

---------------------- ASP.Net+Android+IOS开发.Net培训、期待与您交流! ----------------------详细请查看: http://edu.csdn.net
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值