Java基础——线程(一)

线程

  • 进程:正在执行中的程序。每一个进程执行,都有一个执行的顺序,该顺序就是一个执行路径,或者叫一个控制单元。
  • 线程:就是进程中的一个独立的控制单元,线程在控制着进程的执行。

简单一点来说进程和线程的关系,打开任务管理器可以看到很多正在执行的程序,每一个正在执行的程序就是进程,
而比如说迅雷下载数据的时候,会开辟很多条请求去找服务端请求数据(一条请求下载120%,另一条请求下载2140%…这样可以提高效率),而这些开辟的请求就是线程。

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

例如:

public class ThreadDemo {
	public static void main(String[] args) 
	{
		for (int x=0;x<800 ;x++ )
		{
			System.out.println("Hello World");
		}
	}
}
  • 在该程序运行的时候,Java虚拟机会启动,这个时候就会多一个进程java.exe.
  • 该进程中至少有一个线程负责Java程序的运行,而且这个线程运行的代码存在于main方法中,该线程称之为主线程。
  • 其实要是深追究的话,该程序运行的时候,不止一个线程,还有负责垃圾回收机制的线程,

多线程存在的意义:

  1. 可以使在运行时有多个程序同时运行的效果。
  2. 多条线程运行同一个程序,提高了程序运行的效率。

创建线程

  1. 如何自定义一个线程呢?
    步骤:
    1. 定义类继承Thread
    2. 复写Thread中的run方法(将自定义的代码存储在run方法中,让线程运行)。
    3. 调用线程中的start方法该方法有两个作用:启动线程和调用run方法。

示例:运行一下代码,

//创建线程
class Demo extends Thread
{
	public void run()
	{
		for (int x=0;x<70 ;x++ )
		{
			System.out.println("demo   run---"+x);
		}
	}
}
public class ThreadDemo2 {
	public static void main(String[] args) 
	{
		Demo d = new Demo();
		d.start();


		for (int x=0;x<70 ;x++ )
		{
			System.out.println("Hello  World......."+x);
		}
	}
}

会出现图中的结果:
自定义线程运行结果

由运行结果可以看到,程序执行过程中,自定义线程和主线程共同抢夺CPU资源,运行流程图如下:

简单的多线程流程图

  • 发现运行的结果每次都不一样,这是因为多个线程都在获取CPU的执行权,CPU执行到谁,谁就运行
  • 明确一点,在某一时刻,只能有一个程序在运行(多核除外),CPU在做着快速的切换,以达到看上去是同时运行的效果。
  • 我们可以形象的把多线程的运行行为理解为线程在互相抢夺CPU的执行权。
  • 这就是多线程的一个特性:随机性。谁抢到谁执行,至于执行多长时间,CPU说了算。

为什么要覆盖run方法呢?

  • Thread类用于描述线程。
  • 该类定义了一个功能,用于存储线程要运行的代码,该存储功能就是run方法。
  • 也就是说Thread类中的run方法,用于存储线程要运行的代码。

简短的一个小练习:

//创建两个线程,和主线程交替执行


//那么我先定义一个类继承Thread
class Test extends Thread
{
	private String name;//为了区分线程1和线程2,给线程一个自己特有的标识
	Test(String name)
	{
		this.name=name;
	}
	public void run()//1,固有格式:先覆盖run方法,在run方法中写上线程要执行的代码
	{
		for (int x=0;x<60 ;x++ )
		{
			System.out.println(name+"test   run----"+x);
		}
		
	}
}

public class ThreadTest {
	public static void main(String[] args) 
	{
		Test t1 = new Test("one");
		Test t2 = new Test("two");//2、建立线程
		t1.start();//3、启动线程
		t2.start();
		for (int x =0;x<60 ;x++ )//4、主函数执行的代码
		{
			System.out.println("main  run---------"+x);
		}
	}
}

运行结果部分截图:

线程小练习运行结果

  • 线程都是有自己默认的名称的:就是Thread_编号,该编号从0开始。
  • 那么既然有自己默认的名称,这个名称也是可以自己进行修改的。设置名称的两种方法:
    • this.setName();
    • super(name);(因为Thread类中有构造函数直接可以自定义线程名称)
  • 获取当前线程对象:
    • this
    • static Thread currentThread();(这个用的比较多一点)两个其实返回的是同一个对象。其实用法就是Thread.currentThread();

通过一段小程序来练习一下线程名称的获取:

//创建两个线程,和主线程交替执行
//然后显示出线程的默认名称,

class Test extends Thread
{
	
	public void run()
	{
		for (int x=0;x<60 ;x++ )
		{
			System.out.println(this.getName()/*获取名称*/+"===run----"+x);
		}
		
	}
}

public class ThreadDemo3 {
	public static void main(String[] args) 
	{
		Test t1 = new Test();
		Test t2 = new Test();
		t1.start();
		t2.start();
		for (int x =0;x<60 ;x++ )
		{
			System.out.println("main  run---------"+x);
		}
	}
}

运行结果为:
获取线程默认名称运行结果
发现在默认情况下,每个线程都有自己的名称,名称有自己的编号,编号从0开始。

接下来自定义线程的名称:

//创建两个线程,和主线程交替执行
//自定义线程的名称

class Test extends Thread
{
	Test(String name)
	{
		super(name);
	}
	public void run()
	{
		for (int x=0;x<60 ;x++ )
		{
			System.out.println(Thread.currentThread().getName()+"===run----"+x);
						      //获取当前线程对象。
		}
		
	}
}

public class ThreadDemo4 {
	public static void main(String[] args) 
	{
		Test t1 = new Test("one");
		Test t2 = new Test("two");
		t1.start();
		t2.start();
		for (int x =0;x<60 ;x++ )
		{
			System.out.println("main  run---------"+x);
		}
	}
}

运行结果为:
自定义线程名称运行结果

线程的五种状态:

  • 被创建:已经被创建,但是没有启用的线程。
  • 运行:已经被创建,而且正在执行的线程(有执行资格,有执行权)
  • 临时状态:有执行资格但是没有执行权(CPU在某一个时间只能执行一个线程,而这个时候在等候的其他可执行线程就处在临时状态上)
  • 冻结:当线程遇到sleep(time),wait指令的时候,会进入冻结状态,(没有执行资格,也没有执行权,但是该线程没有挂掉)什么时候醒呢?等到sleep时间到,或者wait遇上notify(唤醒),该线程就会变为临时状态,有了执行资格,等待CPU的执行权。
  • 消亡:当run方法执行完,或者遇到stop方法,表明该线程挂掉了。

通过模拟售票系统来理解线程的应用:

//火车站售票的例子:

class Ticket extends Thread
{
	//因为售票系统需要开启多线程,但是票数是固定不变的,所以需要这些线程共享一个数据,所以就把票数定义成静态的
	private static int count =100;
	public void run()
	{
		while (true)
		{
			if (count>0)
			{
				System.out.println(Thread.currentThread().getName()+"---"+count--);
			}
		}
	}
	
}
public class TicketDemo {
	public static void main(String[] args) 
	{
		//这里假设开启四个窗口售票:
		Ticket t1 = new Ticket();
		Ticket t2 = new Ticket();
		Ticket t3 = new Ticket();
		Ticket t4 = new Ticket();
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}

运行结果:
售票系统模拟运行结果

由以上代码中,我们可以看出,在定义票的总数的时候,为了让多个线程共享同一个数据,将数据定义成了静态的,这样做的缺点是生命周期太长了,一直到类没有数据才会被清空。所以引出了第二种创建线程的方法。

创建线程的第二种方法:

  • 1,定义类实现Runnable接口
  • 2,覆盖Runnable接口中的run方法
    • 将线程要运行的代码存放在该run方法中
  • 3,通过Thread类建立线程对象。
  • 4,将Runnable接口的子类对象作为实际参数传给Thread类的构造函数。
    • 为什么要将Runnable接口的子类对象传递给Thread的构造函数呢? 因为自定义的run方法所属的对象是Runnable接口的子类对象,,所以要让线程去指定对象的run方法,就必须明确该run方法所属对象。
  • 5,调用Thread类的start方法开启线程,并调用Runnable接口子类中的run方法。

实例2:

//火车站售票的例子:
//创建线程的第二种方式:
class Ticket implements Runnable
{
	//这里无需定义成静态的
	private int count =100;
	//同样的要覆盖run方法
	public void run()
	{
		while (true)
		{
			if (count>0)
			{
				System.out.println(Thread.currentThread().getName()+"---"+count--);
			}
		}
	}
	
}
public class TicketDemo {
	public static void main(String[] args) 
	{
		//创建一个Runnable子类对象。
		//再创建一个Thread对象,将Runnable子类对象作为实际参数传给Thread类
		//开启线程:
		Ticket c = new Ticket();
		Thread t1 = new Thread(c);
		Thread t2 = new Thread(c);
		Thread t3 = new Thread(c);
		Thread t4 = new Thread(c);
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}

运行结果:
第二种创建对象方式运行结果

实现方式和继承方式有什么区别呢?

  • 实现的好处:避免了单继承的局限性。
  • 在定义线程时,建议使用实现方式。
  • 继承Thread:线程代码存放Thread子类run方法中。
  • 实现Runnable:线程代码存在接口的子类run方法中。

通过分析发现,多线程运行出现了安全问题。

多线程安全问题出现的原因图解

  • 问题的原因是:当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分们还没有执行完,另一个线程参与进来执行,导致了共享数据的错误。
  • 解决办法:对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
  • Java对于多线程的安全问题提供了专业的解决方式。就是同步代码块。synchronized(对象) { 需要被同步的代码 }

这里我们用sleep来模拟CPU切换出去的效果,再次执行以上的代码:

//火车站售票的例子:
//创建线程的第二种方式:
class Ticket implements Runnable
{
	//这里无需定义成静态的
	private int count =100;
	//同样的要覆盖run方法
	public void run()
	{
		while (true)
		{
			if (count>0)
			{
				try
				{
					Thread.sleep(10);//线程刚判断完进来就睡着了,以此来模拟CPU切换的效果。
				}
				catch (InterruptedException e)//中断异常之后再进行详细的解说
				{
					//这里为了简单起见就不进行处理了。
				}
				
				System.out.println(Thread.currentThread().getName()+"---"+count--);
			}
		}
	}
	
}
public class TicketDemo {
	public static void main(String[] args) 
	{
		//创建一个Runnable子类对象。
		//再创建一个Thread对象,将Runnable子类对象作为实际参数传给Thread类
		//开启线程:
		Ticket c = new Ticket();
		Thread t1 = new Thread(c);
		Thread t2 = new Thread(c);
		Thread t3 = new Thread(c);
		Thread t4 = new Thread(c);
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}

运行结果是:
线程安全问题演示

运行结果显示,这里还真的出现了安全问题。要怎么解决呢?
上边提到了,用Java中的同步代码块进行解决。
修改方案如下:

//火车站售票的例子:
//解决多线程安全问题,用同步代码块:
class Ticket implements Runnable
{
	private int count =100;
	Object obj = new Object();
	public void run()
	{
		while (true)
		{
			//同步代码块是要在外边加一个锁,只要一个线程进来了,在他没有执行完同步代码块中的内容之前,
			//其他线程都进不来,
			//这个例子就好比火车上的厕所,前一个人出不来,后一个人就别想要进去。
			//用同步代码块需要一个对象参数,在这里哪一类型的对象都可以,
			//需要注意的是,同步代码块扩住的范围是操作共有数据的(count)部分都要扩起来
			synchronized(obj)
				{
					if (count>0)
					{
						try
						{
							Thread.sleep(10);
						}
						catch (InterruptedException e)
						{
							//这里为了简单起见就不进行处理了。
						}
				
						System.out.println(Thread.currentThread().getName()+"---"+count--);
					}
				}
			}
		}
	}
	
public class TicketDemo {
	public static void main(String[] args) 
	{
		//创建一个Runnable子类对象。
		//再创建一个Thread对象,将Runnable子类对象作为实际参数传给Thread类
		//开启线程:
		Ticket c = new Ticket();
		Thread t1 = new Thread(c);
		Thread t2 = new Thread(c);
		Thread t3 = new Thread(c);
		Thread t4 = new Thread(c);
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}

运行结果:
同步代码块运行结果

同步代码块:

synchronized(对象)
{
	//需要被同步的代码
}

对象如同锁。只有锁的线程才可以在同步中执行。
没有持有锁的线程及时获取CPU执行权,也进不去,因为没有获取锁。

经典的实例说明:火车上的卫生间。

同步的前提:

  • 必须要有两个或两个以上的线程。
  • 必须是多个线程使用同一个锁。

必须保证同步中只有一个线程在运行。

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

弊端:多个线程都需要判断锁,较为消耗资源,

练习:银行存款的例子:

//银行储户存钱的例子:两个用户同时过来存钱,两人都存300,每次存100 共存3次
//希望用多线程;


/*
思路:银行只有一个,同时里边会有一个和,表示银行里现在一共有多少钱了。
		银行还有存钱方法,add
	储户可以 有多个,这里可以开启多线程,储户调用add方法进行存钱,
*/
/*
步骤:1 先描述类,运行程序
      2 看哪里有多线程产生的安全问题,进行处理
			如何找问题:
			1,明确哪些代码是多线程运行代码
			2,明确共享数据
			3,明确多线程运行代码中哪些语句是操作共享数据的。

*/
class Bank
{
	private int sum;
	public void add(int n)//add方法中有两句代码,分析问题可能发生的情况。
	{
		sum = sum+n;
		try{Thread.sleep(10);}catch(Exception e){}
		//假如线程在这里发生了CPU切换(用sleep方法模拟切换出去的动作),线程0进来sum值变成100-->跳出
		//线程2现在进来,因为是共享数据,将sum 的值变为200-->跳出
		//这个时候线程0回来了,打印sum值得时候一看是200,就打印出来了,
		//这就出现了两次200的情况。


		System.out.println(Thread.currentThread().getName()+"*****"+sum);
	}
}
class Chus implements Runnable
{
	Bank b = new Bank();
	public void run()//这里是多线程运行的代码,每一个线程都会有一次循环,而且只有一句执行语句,没有安全问题
	{
		for (int x=0;x<3 ; x++)
		{
			b.add(100);//线程运行代码中调用到了add方法,
		}
		
	}
	
}
public class BankDemo {
	public static void main(String[] args) 
	{
		Chus c = new Chus();
		Thread t1 = new Thread(c);
		Thread t2 = new Thread(c);
		t1.start();
		t2.start();
	}
}


因为该程序有安全问题,所以运行结果如下:
银行存款事例不安全结果

所以需要同步代码块进行处理:

//处理过之后的代码
class Bank
{
	private int sum;
	Object obj = new Object();
	public void add(int n)
	{
		synchronized(obj)
		{
			sum = sum+n;
			try{Thread.sleep(10);}catch(Exception e){}
			System.out.println(Thread.currentThread().getName()+"*****"+sum);
		}
		
	}
}
class Chus implements Runnable
{
	Bank b = new Bank();
	public void run()
	{
		for (int x=0;x<3 ; x++)
		{
			b.add(100);
		}
		
	}
	
}
public class BankDemo {
	public static void main(String[] args) 
	{
		Chus c = new Chus();
		Thread t1 = new Thread(c);
		Thread t2 = new Thread(c);
		t1.start();
		t2.start();
	}
}

运行结果发现:
处理完之后的运行结果

发现同步代码块和函数都有一个功能就是封装代码,所以我们可以将同步代码块改写为同步函数的形式,这样就不用创建obj对象了。修改后的代码如下:

class Bank
{
	private int sum;
	//Object obj = new Object();
	public synchronized void add(int n)
	{
		//synchronized(obj)
		{
			sum = sum+n;
			try{Thread.sleep(10);}catch(Exception e){}
			System.out.println(Thread.currentThread().getName()+"*****"+sum);
		}
		
	}
}

那么既然学习了用同步函数的方法,也可以讲之前火车票的例子用同步函数的方法进行优化代码:

//火车站售票的例子:
//解决多线程安全问题,用同步代码块:
//通过上一个案例的学习,我们可以用到同步函数:

class Ticket implements Runnable
{
	private int count =1000;
	public void run()
	{
		while (true)
		{
			show();
		}
		
	}
	public synchronized void show()
	{
		if (count>0)
				{
					try
					{
						Thread.sleep(10);
					}
					catch (InterruptedException e)
					{
						
					}
				
					System.out.println(Thread.currentThread().getName()+"---"+count--);
				}
	}
}
public class TicketDemo {
	public static void main(String[] args) 
	{
		
		Ticket c = new Ticket();
		Thread t1 = new Thread(c);
		Thread t2 = new Thread(c);
		Thread t3 = new Thread(c);
		Thread t4 = new Thread(c);
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}

那么问题也就来了,同步代码块变成同步函数之后,那个用来当锁的对象没有了,那么同步函数用到的是什么锁呢?

函数需要被调用,那么函数都有一个所属对象引用,就是this
所以同步函数使用的锁是this,

说到这里回顾一下单例设计模式中的懒汉式,因为懒汉式中也用到了同步

//单例设计模式

/*
饿汉式

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)
		{
			syschronized(Single.class)
			{
				if(s==null)
				{
					s=new Single();
				}
			}
		}
		return s;
	}
}

注意:

  • 懒汉式和饿汉式有什么区别:懒汉式的特点在于实例的延迟加载;
  • 懒汉式延迟加载有没有问题?有,多线程执行的时候会有安全问题。
  • 怎么解决?用同步的方式。
  • 加同步有哪些方法?用同步代码块和同步函数的方法,但是稍微有一些低效,可以使用双重判断的方法来稍微的提高效率。
  • 加同步的时候使用的锁是哪一个?该类所属的字节码文件。(不是this,因为静态中不可能有this)

死锁

什么是死锁?
就像是两个人都只有一只筷子,但是谁都不给谁自己的筷子,这样两个人就吃不上饭了,这就是发生了死锁。
死锁的产生原因?
同步发生了嵌套,就是锁里边还有锁。
死锁事例代码:

//死锁演示:就是锁里边嵌套锁



class Test implements Runnable
{
	private boolean flag;
	Test(boolean flag)//给要创建的线程对象先初始化一个布尔型的值
	{
		this.flag = flag;
	}
	public void run()
	{
		if (flag)//如果为真,先进a锁,再进b锁
		{
			while(true)
			{
				synchronized(MyLock.a)
				{
					System.out.println("locka");
					synchronized(MyLock.b)
					{
						System.out.println("lockb");
					}
				}
			}
		}
		else//如果为假,先进b锁,再进a锁
		{
			while(true)
			{
				synchronized(MyLock.b)
				{
					System.out.println("lockb");
					synchronized(MyLock.a)
					{
						System.out.println("locka");
					}
				}	
			}
		}
	}
}

class MyLock
{
	static Object a = new Object();
	static Object b = new Object();
}

//如果线程0进了a锁,CPU切换,同时
//线程1进了b锁,CPU切换,这时线程0重新夺回执行权的时候,想要b锁,但是b锁被线程1占用着,
//所以两边都不放资源,程序无法进行下去,形成死锁。

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

运行结果:
死锁实例

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值