回顾多线程基础

首先我们要明白什么是进程,什么是线程?

进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1–n个线程。(进程是资源分配的最小单位)

线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(线程是cpu调度的最小单位)

什么是多线程?

  • 1.多进程是指操作系统能同时运行多个任务(程序)。
  • 2.多线程是指在同一程序中有多个顺序流在执行.
  • 3.很多线程都是模拟出来的,真正的多线程指的是多个cpu,即多核,如服务器。
  • 4.如果是模拟出来的多线程,即在同一个cpu的情况下,cpu只能执行一个程序或一行代码,因为切换的很快,所以有了同时执行的错觉。
  • 5.在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个jVM实习在就是在操作系统中启动了一个进程。

线程的生命周期为什么?
线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。

我们之前在多线程之前学习的多线程基础基本上都是单线程执行的

普通方法调用和多线程

在这里插入图片描述

多线程核心概念

在这里插入图片描述

实现多线程的几种方式:

  • 继承Thread类:

首先,我们看JDK1.8的帮助文档对Thread类的描述:空线是一个线程在执行一个程序。java虚拟机的应用程序可以有多个同时执行的线程。 每一个线程都有一个优先权。具有更高优先级的线程在优先于较低优先级的线程中执行。每一个线程可能会或可能不会被标记为一个守护进程。在一些线程中运行的代码创建了一个新的Thread对象时,新线程的优先级被设置为等于创建线程的优先级,是守护线程的当且仅当创建线程是一个守护进程。

让我们来写一个例子:

public class TestThread extends Thread{
	
private String url;//文件地址
private String fileName;//文件名
	
	
	public TestThread(String url, String fileName) {
	this.url = url;
	this.fileName = fileName;
}


	@Override
	public void run() {
	WebDownLoader webDownLoader = new WebDownLoader();
	webDownLoader.DownloadPicture(url, fileName);
	System.out.println("下载了文件:"+fileName);	
	}
	
	public static void main(String[] args) {
		TestThread testThread = new TestThread("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1606911965336&di=b68b28cd8bd82094ddfd3f9ecfd380e4&imgtype=0&src=http%3A%2F%2Fimage.namedq.com%2Fuploads%2F20181006%2F19%2F1538823821-QrIiEnXmAv.jpg", "亚索");
		TestThread testThread2 = new TestThread("https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=2106604486,285772919&fm=26&gp=0.jpg", "光辉");
	   testThread.start();
	   testThread2.start();
	}
	
}

//创建一个下载文件的类
 class WebDownLoader{
 //创建一个下载文件的方法
public static void DownloadPicture(String url,String fileName) {
	try {
		FileUtils.copyURLToFile(new URL(url), new File(fileName));
	} catch (MalformedURLException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	} catch (IOException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	}
	
}
}

运行后:发现在项目中会多出两个下载的文件。

序启动运行main时候,java虚拟机启动一个进程,主线程main在main()调用时候被创建。随着调用MitiSay的两个对象的start方法,另外两个线程也启动了,这样,两条线程理论上就同时去下载两个文件。

注意:start()方法的调用后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable),什么时候运行是由操作系统决定的。
从程序运行的结果可以发现,多线程程序是乱序执行。因此,只有乱序执行的代码才有必要设计为多线程。
Thread.sleep()方法调用目的是不让当前线程独自霸占该进程所获取的CPU资源,以留出一定时间给其他线程执行的机会。
实际上所有的多线程代码执行顺序都是不确定的,每次执行的结果都是随机的。

  • 实现Runnable接口

首先,让我们看看JDK1.8帮助文档对Runnable接口的描述:创建一个线程是声明一个类实现Runnable接口的其他方式。该类实现run方法。那类的一个实例可以分配,作为一个参数传递Thread时创建,并开始。此外,Runnable一类是主动的而不是子类Thread提供手段。一个类实现Runnable可以运行没有子Thread实例化一个Thread实例并将自身作为目标。在大多数情况下,这Runnable接口应该是如果你只打算重写run()方法并没有其他Thread方法。这是重要的因为方法重写,继承类不应该继承除非程序员打算修改或增强类的基本行为。

让我们来写一个例子:

/**
 *  演示线程的停止:1.创建线程,然后一直让它运行,通过我们自定义的方法让线程停止
 *  2.不建议使用JDK中的自带的API方法来让线程停止,因为可能会存在某些问题(官方推荐我们使用:自定义一个标志位来让某个线程进行停止)
 * 
 *
 */
public class TestThreadStop implements  Runnable{

	private static boolean flag=true;
	
	
	@Override
	public void run() {
	while (flag) {
		System.out.println(Thread.currentThread().getName()+"-->循环");
	}	
	}
	void testflag(){
		flag=false;
		System.out.println("自定义线程停止");
	}
	
 

public static void main(String[] args) {
	TestThreadStop testThreadStop = new TestThreadStop();	
	  new Thread(testThreadStop).start();
	
	  //主线程执行for循环
	  for (int i = 0; i < 1000; i++) {
		if (i==900) {
			//让我们自定义的线程停止
			testThreadStop.testflag();
		}
		System.out.println("主线程执行--》"+i);
	}
	}
}

输出结果:
在这里插入图片描述
当自定义线程输出到达900时候,线程会根据标志位停止,线程执行完毕。主线程继续执行。

  • 实现Callable接口

首先,让我们看看JDK1.8帮助文档对Callable接口的描述:返回结果的一个任务,并可能抛出异常。用户定义一个不带参数调用 call 的Callable接口类似于Runnable,,是专为其实例的类可能被另一个线程执行。然而,Runnable,不返回结果并不能抛出异常。
Executors类包含的实用方法与其他常见的形式转换为Callable类。

让我们来写一个例子:

public class TestCallable  implements Callable<Boolean>{

	@Override
	public Boolean call() throws Exception {
		System.out.println("继承Callable接口的线程"+Thread.currentThread().getName()+"执行");
		return true;
	}
	
	public static void main(String[] args) {
		//1.创建两个继承了Callable接口的类的线程对象
	   TestCallable testCallable = new TestCallable();
	TestCallable testCallable1 = new TestCallable();
	    //2.创建线程池服务,线程池最大为2个线程
	ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
	//3.往线程池中提交线程,并获取到线程执行call方法结果
	Future<Boolean> submit = newCachedThreadPool.submit(testCallable);
	Future<Boolean> submit2 = newCachedThreadPool.submit(testCallable1);
	//4.获取接口
	try {
		Boolean boolean1 = submit.get();
		
		
	} catch (InterruptedException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	} catch (ExecutionException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	}
	try {
		Boolean boolean2 = submit2.get();
		
		
	} catch (InterruptedException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	} catch (ExecutionException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	}
	}
	

}

输出结果:
在这里插入图片描述

那我们之前学习了继承Thread类和实现Runnable接口来创建线程,这两个方式的区别是什么?

  • 如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。所以Runnbale比Thread跟具有优势
  • 可以避免java中的单继承的限制
  • 增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
  • 线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类

线程状态转换(线程的生命周期)

在这里插入图片描述
1、新建状态(New):新创建了一个线程对象。
2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(一)等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
(二)同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
(三)其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)
5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

线程的调度

  1. 线程睡眠:Thread.sleep(long millis)方法
    在这里插入图片描述

让我们来写一个例子:

public class TestThreadSleep {

	
	private static int mun=10;
	//模拟倒计时方法
	public static void tenDown() throws InterruptedException{
		while (mun>0) {
			Thread.sleep(2000);
			System.out.println(mun--);
		}	
	}
	
	//打印当前时间方法
	private static void datedayin(){
		while (true) {
			Date date = new Date(System.currentTimeMillis());//获取当前时间
			try {
				Thread.sleep(1000);//睡眠一秒
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			
			System.out.println("当前时间是:"+new SimpleDateFormat("HH:mm:ss").format(date));
			 date = new Date(System.currentTimeMillis());//更新当前时间
			
		}
	}
	
	
	
	
	public static void main(String[] args) {
		
		//模拟倒计时
		/*try {
			tenDown();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}*/
		
		//打印当前时间
		datedayin();
		
	
		
		
		
	}
}

输出结果:
在这里插入图片描述

注意:wait和sleep的区别:Obj.wait(),与Obj.notify()必须要与synchronized(Obj)一起使用,也就是wait,与notify是针对已经获取了Obj锁进行操作,从语法角度来说就是Obj.wait(),Obj.notify必须在synchronized(Obj){…}语句块内。从功能上来说wait就是说线程在获取对象锁后,主动释放对象锁,同时本线程休眠。直到有其它线程调用对象的notify()唤醒该线程,才能继续获取对象锁,并继续执行。相应的notify()就是对对象锁的唤醒操作。但有一点需要注意的是notify()调用后,并不是马上就释放对象锁的,而是在相应的synchronized(){}语句块执行结束,自动释放锁后,JVM会在wait()对象锁的线程中随机选取一线程,赋予其对象锁,唤醒线程,继续执行。这样就提供了在线程间同步、唤醒的操作。Thread.sleep()与Object.wait()二者都可以暂停当前线程,释放CPU控制权,主要的区别在于Object.wait()在释放CPU同时,释放了对象锁的控制。

2.线程礼让:Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。
在这里插入图片描述
3.线程强制执行:join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。

让我们写一个例子:

/**
 * 
 * 演示java线程中的join方法:使某个线程进行插队,直到这个线程执行完后,其他线程才能执行
 *
 */
public class TestJoin implements Runnable{

	@Override
	public void run() {
	for (int i = 0; i < 200; i++) {
		System.out.println("大哥线程来执行了:"+Thread.currentThread().getName()+"-->"+i);
	}
		
	}
	
	
	public static void main(String[] args) {
		
//创建自定义线程
		TestJoin testJoin = new TestJoin();
		Thread thread = new Thread(testJoin);
		thread.start();
		
		//主线程执行for循环,到200的时候 join方法让自定义线程进行插队,直到自定义线程执行完后,主线程才继续执行
		//(如果电脑是多核的,有多个cpu,可能在200之前,两个线程会交替执行)
		
		for (int i = 0; i < 500; i++) {
			if (i==200) {
			try {
				thread.join(); //进行插队
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			}
			System.out.println("执行main方法:-->"+i);
			
		}
	}


}

输出结果:
在这里插入图片描述
当主线程达到200的时候,自定义线程使用join方法进行插队,直到自定义线程执行完毕为止,主线程才继续执行。

3.调整线程优先级:Java线程有优先级,优先级高的线程会获得较多的运行机会。
在这里插入图片描述

注意:每个线程都有默认的优先级。主线程的默认优先级Thread.NORM_PRIORITY。
线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。
JVM提供了10个线程优先级,但与常见的操作系统都不能很好的映射。如果希望程序能移植到各个操作系统中,应该仅仅使用Thread类有以下三个静态常量作为优先级,这样能保证同样的优先级采用了同样的调度方式。

让我们来写一个例子:

/**
 * 
 * 多线程的优先等级:1-10
 * 1 最低
 * 5  默认
 * 10  最高
 */
public class 多线程的优先级 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
//获取多线程的优先级等级
		//1.最高优先级
		System.out.println(Thread.MAX_PRIORITY);//10
		//2.默认优先级
		System.out.println(Thread.NORM_PRIORITY);//5
		//3.最低优先级
		System.out.println(Thread.MIN_PRIORITY);//1
		
		Thread t1=new projuct();//获取线程
		Thread t2=new projuct();
		t1.setName("T1");//修改线程名
		t2.setName("T2");
		//查看线程优先级,自定义的线程没有设置前都是默认的优先级 5
		System.out.println(t1.getPriority());//5
		System.out.println(t2.getPriority());//5
		//设置优先级
		t1.setPriority(4);
		t2.setPriority(10);
		//执行线程
		t1.start();
		t2.start();
		
	}

}
//定义一个线程
class projuct extends Thread{
	public void run(){
		for(int i=0;i<30;i++){
			System.out.println(Thread.currentThread().getName()+"-->"+i);
			//获取当前的线程和名称
		}
	}
}

输出结果为:
在这里插入图片描述

 会发现在抢夺CPU调度的过程中,T2线程因为优先级的原因抢夺的CPU时间片会多一些。

守护线程

在这里插入图片描述
让我们来写一个例子:

public class 学习守护线程 {
/**
 * 守护线程
 * 其他所有线程结束,则守护线程退出
 * 守护线程一般都是无限执行的
 * 
 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
Thread t1=new  guard();
t1.setName("t1");
t1.setDaemon(true);//设置t1为守护线程
//知道主线程结束 它才会结束
t1.start();

for(int i=0;i<10;i++){
	System.out.println(Thread.currentThread().getName()+"-->"+i);
	try {
		Thread.sleep(1000);
	} catch (InterruptedException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	}
}
	}

}
class guard extends Thread{
	public void run(){
		int i=0;
		while(true){
			i++;
			System.out.println(Thread.currentThread().getName()+"-->"+i);
			try {
				Thread.sleep(500);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
}

输出结果:
在这里插入图片描述

线程的同步

 首先,我们需要理解一个概念:什么是线程的同步?线程的同步就是多个线程同时操作同一个资源。

首先,我们可以看一个多个线程同时操作一个共有资源的例子,会发现出现很多bug和漏洞:

public class TestTicket implements Runnable{

	//总共有10张票
  private int Ticket=10;
	
	@Override
	public void run() {
		
		while (true) {
			if (Ticket<=0) {
				break;//当票被抢完的时候,就退出循环
			}	
			
				try {
					Thread.sleep(100);//每当一个线程抢完一次票的时候,就让线程暂停0.2秒
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				//Thread.currentThread():返回当前线程
				System.out.println(Thread.currentThread().getName()+"抢到了第"+Ticket--+"票");//每当一个线程抢到一张票的时候,票数资源就--	
			
		}
	}
	public static void main(String[] args) {
		//创建线程
		TestTicket testTicket = new TestTicket();
		
		new Thread(testTicket,"小明").start();
		new Thread(testTicket,"老师").start();
		new Thread(testTicket,"黄牛党").start();
	}	

}


结束输出:
在这里插入图片描述
这肯定是不允许的,事实上是不允许几个同时抢到同一张票的,这时候出现了数据混乱。

这时候我们就可以使用synchronized关键字

我们在上面的例子中加入synchronized锁:

public class TestTicket implements Runnable{

	//总共有10张票
  private int Ticket=10;
	
	@Override
	public void run() {
		synchronized (this) {
		while (true) {
			if (Ticket<=0) {
				break;//当票被抢完的时候,就退出循环
			}	
			
				try {
					Thread.sleep(100);//每当一个线程抢完一次票的时候,就让线程暂停0.2秒
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				//Thread.currentThread():返回当前线程
				System.out.println(Thread.currentThread().getName()+"抢到了第"+Ticket--+"票");//每当一个线程抢到一张票的时候,票数资源就--	
			
		}
		}
	}
	public static void main(String[] args) {
		//创建线程
		TestTicket testTicket = new TestTicket();
		
		new Thread(testTicket,"小明").start();
		new Thread(testTicket,"老师").start();
		new Thread(testTicket,"黄牛党").start();
	}	

}

synchronized关键字有什么用?

1)是某个对象实例内,synchronized aMethod(){}可以防止多个线程同时访问这个对象的synchronized方法(如果一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法)。这时,不同的对象实例的synchronized方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法;
2)是某个类的范围,synchronized static aStaticMethod{}防止多个线程同时访问这个类中的synchronized static 方法。它可以对类的所有对象实例起作用。
3) 除了方法前用synchronized关键字,synchronized关键字还可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。用法是: synchronized(this){/区块/},它的作用域是当前对象;
4) synchronized关键字是不能继承的,也就是说,基类的方法synchronized f(){} 在继承类中并不自动是synchronized f(){},而是变成了f(){}。继承类需要你显式的指定它的某个方法为synchronized方法;

什么是死锁?

在这里插入图片描述
简单来说:就是两个线程各有一把锁,但是又想要对面手中的锁,但是又不肯把自己的锁给对方,这时候程序就无法继续向下执行。

/**
 * 演示死锁的概念:当我们去获取到一个对象的锁之后,在这个锁还没有执行完毕的时候去获取到另一个对象的锁(关键是另一个对象的锁还在别的线程的手中)
 * 这就会造成死锁,程序无法向下执行。
 * 或者说:当我们去获取到一个对象的锁之后,在这个锁还没有执行完毕的时候去获取到另一个对象的锁(关键是另一个对象的锁还在别的线程的手中,并且别的线程这时候也想
 * 获取到我们手中对象的锁,这样就会僵持不下,程序无法向下执行,直接卡死)
 * 
 *
 */
/**
 * 演示:1.创建两个小孩对象,小孩1获取到玩具车对象的锁,这时候想去获取到小孩2手中玩具飞机对象的锁
 *     2.小孩2获取到玩具飞机对象的锁,这时候象去获取到小孩1手中玩具车对象的锁
 *     3.这就造成了死锁状态,程序无法向下执行了
 */

//创建玩具车类
class Car{
	
}
//创建玩具飞机类
class Aircraft{
	
}

public class TestDieLock extends Thread {

static	Car car=new Car();
static Aircraft aircraft=new Aircraft();
	

private String child;//小孩名
	


	public TestDieLock(String child) {

	this.child = child;
}

	public void run() {
		try {
			wanju();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

 public void wanju() throws InterruptedException {
	 if (child.equals("小孩1")) {
		synchronized (car) {
			System.out.println("小孩1获取到了玩具车的对象锁");
			Thread.sleep(1000);
			synchronized (aircraft) {
				System.out.println("小孩1还想获取玩具飞机的对象锁");
			}
		}
	}else {
	synchronized (aircraft) {
		System.out.println("小孩2获取到了玩具飞机的对象锁");
		Thread.sleep(1000);
		synchronized (car) {
			System.out.println("小孩2还想获取玩具车的对象锁");
		}
	}
	}
	
}
 
 public static void main(String[] args) {
	TestDieLock testDieLock = new TestDieLock("小孩1");
	TestDieLock testDieLock2 = new TestDieLock("小孩2");
	testDieLock.start();
	testDieLock.start();
}
}



Lock锁

在这里插入图片描述
让我们来写一个例子:

/**
 * 在JDK5.0的时候推出了一个可重入锁:Lock,这个锁可以作用和对象锁相同,但是这个锁需要自己去加锁和解锁,通常我们使用try catch来让我们的方法或者代码块
 * 进行锁机制
 *
 */
public class TestLock implements Runnable{
int num=10;

//定义可重入锁对象
private static  ReentrantLock reentrantlock=new ReentrantLock();

	@Override
	public void run() {
		while (true) {
	try {
		//调用可重入锁对象加锁方法,给下面代码块加锁
		reentrantlock.lock();
		if (num>10) {
			System.out.println(num--);
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}	
		}else {
		break;
		}
		
	}finally {
		//使用可重入锁,别忘记在finally中解锁
		reentrantlock.unlock();
	}
		}
	}
	
	public static void main(String[] args) {
		TestLock testLock = new TestLock();
		new Thread(testLock).start();
		new Thread(testLock).start();
		new Thread(testLock).start();
	}
	

}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

杨某人的快乐学习日记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值