java学习笔记之死锁、线程通信

本文详细介绍了Java中的多线程概念,包括守护线程、定时器计划任务、锁的使用以及死锁的避免。同时,还探讨了线程通信中的wait、notify和notifyAll方法的应用,并举例展示了生产者消费者问题的解决。最后,讨论了线程池的重要性和使用场景,以及如何通过ExecutorService创建线程池。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

多线程

守护线程

  • 守护线程 又叫兜底线程

  • 每个程序运行当中,都会默认开启一个守护线程,用于监听我们正常的程序

  • 简单来说,就是没有任何一个线程的时候,JVM就需要退出了,这个时候守护线程也会退出,主要完成垃圾回收等功能

  • 但是 我们可以使用Thread.setDameon() 方法 把某个线程设置为守护线程

  • 但是必须在启动 start之前,否则报错

下面是如何使用:

public class Thread01daemon {

	public static void main(String[] args) {
        //创建线程
		//用的Thread 引用=new 继承Thread的类的构造方法
		//父类引用指向子类对象,多态
		Thread t1=new Processor01();
         Thread t2=new Processor01();
         //设置线程t1和t2的名字
         t1.setName("t1");
         t2.setName("t2");
         //设置t1为守护线程
         //没有其他线程在运行后,守护线程就会退出
         t1.setDaemon(true);
         //启动t1和t2线程
         t1.start();
         t2.start();
         for (int i = 0; i <10; i++) {
			System.out.println("main--->"+i);
			try {
				//线程休眠200毫秒
				Thread.sleep(200);
			} catch (Exception e) {
                   e.printStackTrace();
			}
		}
	}

}
//继承Thread类的子类
class Processor01 extends Thread{
	//覆写run方法
	public void run(){
		int i=0;
		//死循环
		while(true){
			//getName是Thread类里的静态方法
			//但由于继承的缘故我们可以直接使用
			System.out.println(getName()+"="+i++);
			try{
				//线程休眠半秒
				Thread.sleep(500);
			}catch(Exception e){
				e.printStackTrace();
			}
		}
	}
}

Timer

  • 定时器 计划任务,只要有一个任务监听 就会是一个线程

  • 1 执行任务的类 , 2 执行任务起始时间 3 执行任务间隔时间

下面是如何使用:

public class Thread02Timer {
//定时器,计划任务,只要有一个任务监听,就会是一个线程
	//1 执行任务的类 2执行任务起始时间 3执行任务间隔时间
	public static void main(String[] args) throws ParseException {
        //1 创建定时器 
		Timer t=new Timer();
		//执行任务对象
		LogTimerTask logTimerTask=new LogTimerTask();
		//指定起始时间
		SimpleDateFormat sdf=new  SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
		//1 任务对象 2起始时间 3 间隔时间(毫秒)
		//这个schedule方法自动调用run方法输出时间
		t.schedule(logTimerTask, sdf.parse("2021-01-30 20:53:00 000"), 1000*3);
         
	}

}
//任务类
class LogTimerTask extends TimerTask{

	@Override
	public void run() {
		//日期格式化对象,yyyy是年,MM是月,dd是日,HH是时,mm是分,ss是秒,SSS是毫秒
		SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
		//获取当前系统时间
		Date date=new Date();
		//用日期格式化对象格式化输出当前系统时间
		System.out.println(sdf.format(date));
	}
	
}

  • 如果访问了一个对象中加锁的成员方法,那么该对象中所有的加锁的成员方法全部锁定,都不能被其他线程访问

  • 但是和静态无关,和其他对象也无关

  • 如果访问了一个类加锁的静态方法,那么该类中所有的加锁的静态方法都被锁定,不能访问

  • 但是和成员无关

下面是如何使用:

public class Thread03synchronized {

	public static void main(String[] args) {
        //创建Myclass01类的对象my 
		Myclass01 my=new Myclass01();
         //创建线程对象的方法
         //Thread 对象名=new Thread有参的构造方法,构造方法参数传入一个实现了Runnable接口的类的对象
		//创建线程t1、t2、t3
         Thread t1=new Thread(new Processor03(my));
         Thread t2=new Thread(new Processor03(my));
         Thread t3=new Thread(new Processor03(my));
         t1.setName("t1");
         t2.setName("t2");
         t3.setName("t3");
         //t1线程启动
         t1.start();
         //睡眠半秒,保证t1先执行,先进入加锁的m1方法,并睡眠5秒
         try {
			Thread.sleep(500);
		} catch (Exception e) {
               e.printStackTrace();
		}
      // 因为t1进入m1后就加锁,并睡眠,
         //所以在t1线程睡眠时t2线程无法调用加锁的m2方法,
         //因为访问同一个对象的加锁成员方法,该对象中所有的加锁成员方法都不能被访问
         //m3方法没有加锁,所以可以正常执行
 		t2.start();
 		t3.start();
	}

}
//实现Runnable接口的类
class Processor03 implements Runnable{
	//类中有一个成员变量
	//Myclass01类型的my对象
	Myclass01 my;
	//类里有参的构造方法,传入Myclass01类的对象
	public Processor03(Myclass01 my){
		super();
		//创建对象的my是传入的my
		this.my=my;
	}
	//实现Runnable类,覆写run方法
	public void run(){
		//如果当前启动线程的名字是t1
		//那么t1的my调用m1方法
		if("t1".equals(Thread.currentThread().getName())){
			my.m1();
		}else if("t2".equals(Thread.currentThread().getName())){
			my.m2();
		}else if("t3".equals(Thread.currentThread().getName())){
			my.m3();
		}

	}
}
class Myclass01 {
	//加锁的成员方法
	public synchronized void m1() {
		System.out.println("加锁成员方法m1,即将进入睡眠");
		//线程睡眠5秒
		try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	public synchronized void m2() {
		System.out.println("加锁成员方法m2");
	}

	public void m3() {
		System.out.println("未加锁成员方法m3");
	}
}

死锁

  • 死锁 : 就是大家在执行过程中,都遇到对方进入加锁方法中,导致大家都访问不了

  • 原理 :

  • 1 某个线程的执行完成,需要先后,嵌套,锁定执行两个对象,并且先执行对象1

  • 2 另一个线程的执行完成,需要先后,嵌套,锁定执行两个对象,并且先执行对象2

  • 3 在第一个线程中,要去访问对象2的时候,发现被锁定 了,只能等待

  • 在第二个线程中,要去访问对象1的时候,发现被锁定了,只能等待

    synchronized(xxx){}代码块锁,可以锁类,也可以锁对象
    如果锁对象,当访问该代码块锁的时候,该对象中所有的代码块锁和加锁的成员方法都被锁定
    同理访问对象中加锁的成员方法的时候,代码块锁也会被锁定

    如果是锁类,当访问该代码块的时候,该类中所有的代码块锁和加锁的静态方法都被锁定
    同理访问类中加锁的静态方法的时候,代码块锁也会被锁定

    死锁实现思路 :

  •  	1 两个线程
    
  •  	2 两个对象
    
  •  	3 两个线程中 保存的两个对象是相同的
    
  •  	4 线程1 先访问对象1 再嵌套访问对象2
    
  •  	5 线程2 先访问对象2 再嵌套访问对象1
    

    两个线程相当于两辆车,两个对象相当于两条路,路中间有个栅栏,栅栏前面可以拐弯,
    两辆车都想往对方道路拐,但是由于加锁,相当于有红灯,不让拐

下面是代码实现:

public static void main(String[] args) {
        //创建两个Object类对象
		Object o1=new Object();
        Object o2=new Object();
        //创建两个线程
        //创建线程的方法用的是
        //Thread 线程名=new Thread类有参的构造方法
        //传入一个实现
        Thread t1=new T1(o1,o2);
        Thread t2=new T2(o1,o2);
        t1.start();
        t2.start();
	}

}
//继承Thread类,覆写run方法
class T1 extends Thread{
	Object o1;
	Object o2;
	public T1(Object o1,Object o2){
		super();
		this.o1=o1;
		this.o2=o2;
			
	}
	public void run(){
		synchronized(o1){
			try {
				//加入睡眠,保证一定会死锁,因为要确保执行到这里时t2把o2锁定了
				Thread.sleep(10);
			} catch (Exception e) {
			    e.printStackTrace();
			}
			synchronized(o2){
				System.out.println("T1执行完成");
			}
		}
	}
}
class T2 extends Thread{
	Object o1;
	Object o2;
	public T2(Object o1,Object o2){
		super();
		this.o1=o1;
		this.o2=o2;
	}
	public void run(){
	    synchronized(o2){
	    	try{
	    		//需要加入睡眠,保证一定会死锁,因为执行到这里,需要t1先把o1给锁定
	    		Thread.sleep(10);
	    	}catch(Exception e){
	    		e.printStackTrace();
	    	}
	    	synchronized (o1) {
				System.out.println("T2执行完成");
			}
	    }
	}
}

线程通信
wait() 与 notify() 和 notifyAll()

a)wait():令当前线程挂起并放弃CPU、同步资源并等待,使别的线程可访问并修改共享资源,而当 前线程排队等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有 权后才能继续执行。

b)notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待

c)notifyAll ():唤醒正在排队等待资源的所有线程结束等待.

注意:这三个方法只有在synchronized方法或synchronized代码块中才能使用,否则会报
java.lang.IllegalMonitorStateException异常
因为这三个方法必须有锁对象调用,而任意对象都可以作为synchronized的同步锁, 因此这三个方法只能在Object类中声

  • Object 中的方法

  • wait() : 该线程进入等待状态,功能还在,只是没有运行,当被唤醒之后进入就绪状态,当执行的时候接着当前等待的状态继续执行

  • notify() : 唤醒该对象上等待中的某一个线程(优先级)

  • notifyAll() : 唤醒该对象上所有等待的线程

  • 必须用在成员方法中,因为是对象相关,并且该成员方法必须加锁(synchronized)

  • wait : 无参 或者是传入 0 ,说明不会自动醒,只能被notifyAll/notify唤醒

  • 也可以传入long类型的值,单位是毫秒数,过了指定时间之后自动醒

  • 注意 sleep是Thread类中的静态方法,睡眠指定时间,不会交出锁(其他线程依旧不能交出该方法)

  • 而 wait是Object中的成员方法,也就是每个对象都有的方法,挂起,会交出锁(其他线程就可以访问该方法了)

下面是代码实现:

public class Thread05Wait {

	public static void main(String[] args) {
        //创建Num类的对象
		Num num=new Num(1);
		//创建线程t1和t2
		//用Thread 线程引用=new 继承Thread子类的构造方法创建线程
        Thread t1=new PrintOdd(num);
        //用Thread 线程引用=new Thread有参的构造方法,传入参数为实现Runnable接口的类的对象
        Thread t2=new Thread(new PrintEven(num));
        //设置t1和t2线程的名字
        t1.setName("t1");
        t2.setName("t2");
        //t1和t2线程启动
        t1.start();
        t2.start();
	}

}
//打印奇数
class PrintOdd extends Thread{
	//Num类型的引用
	Num num;
	//打印奇数
	//构造方法,传入一个Num类型的引用
	public PrintOdd(Num num){
		super();
		//构造方法初始化类的成员属性
		this.num=num;
	}
	public void run(){
		while(true){
			//用num对象去调用Num类的成员方法
			num.printOdd();
		}
	}
}
//打印偶数
class PrintEven implements Runnable{
	Num num;
	//打印偶数
	public PrintEven(Num num){
		super();
		//将调用这个方法的对象的num成员属性赋值为num
		this.num=num;
	}
	@Override
	public void run() {
         while(true){
        	 //启动线程就会通过num对象调用Num类的打印偶数的成员方法
        	 num.printEven();
         }		
	}
}
//业务类
class Num{
	int count;
	public Num(int count){
		super();
		this.count=count;
	}
	//加锁的成员方法,如果在访问一个加锁的成员方法时,那么该类中所有其他加锁的成员方法都不能用
	public synchronized void printOdd(){
		System.out.println(Thread.currentThread().getName()+":"+count);
		count++;
		//自己唤醒其他线程
		this.notifyAll();
		try {
			//当前线程睡眠半秒,使效果直观
			Thread.sleep(500);
			//自己进入挂起等待
			this.wait();
		} catch (Exception e) {
            e.printStackTrace();
		}
	}
	public synchronized void printEven(){
		System.out.println(Thread.currentThread().getName()+":"+count);
		count++;
		
		// 唤醒其他线程
		this.notifyAll();
		
		// 自己进入挂起等待
		try {
			// 加入睡眠,效果更直观
			Thread.sleep(500);
			this.wait();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	
	}
}

生产者和消费者问题
下面是代码实现:

public class Thread06producerconsumer {

	public static void main(String[] args) {
        //创建业务类的对象s 
		SynStack s=new SynStack();
         //创建消费者线程,传入消费者类的对象
         Thread t1=new Thread(new Consumer(s));
         //创建生产者线程,传入生产者类的对象
         Thread t2=new Thread(new Producer(s));
         //启动线程
         t1.start();
         t2.start();
	}

}
//实现Runnable接口的消费线程类
class Consumer implements Runnable{
	//SynStack类型的引用s
	SynStack s;
	//构造方法初始化成员变量s
	public Consumer(SynStack s){
		super();
		this.s=s;
	}
	@Override
	public void run() {
          for (int i = 0; i < 20; i++) {
			//将产品弹栈,进行消费
        	  s.pop();
			//线程睡眠半秒
			try {
				Thread.sleep(500);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
	
}
//生产线程类
class Producer implements Runnable{
	SynStack s;
	
	public Producer(SynStack s) {
		super();
		this.s = s;
	}

	@Override
	public void run() {
		char ch;
		//生成产品
		for (int i = 0; i < 20; i++) {
			ch = (char)('a'+i);
			//将产品压栈
			s.push(ch);
		}
	}
}
//业务类
class SynStack{
	// 保存以生产的个数.同时也是下一个元素的下标
	int cnt = 0;
	char[] data = new char[6];
	//加锁保证生产产品的同时不能消费产品
	//访问一个类的加锁的成员方法时,其他加锁的成员方法也会被锁定
	public synchronized void push(char ch){
		// 生产个数如果和容器大小一致,说明生产满了,就不再生产
		//挂起生产者线程
		if (cnt == data.length) {
			try {
				wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		
		// 先唤醒消费者 提醒消费
		notifyAll();
		// 把元素保存到数组中
		data[cnt] = ch;
		// 个数+1
		cnt++;
		System.out.println("生产了 : "+ch+" , 容器中剩余 "+cnt+" 个字符");
	}
	
	public synchronized char pop() {
		// 生产个数如果是0 就不进行消费
		//挂起消费者线程
		if (cnt == 0) {
			try {
				wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}

		// 先唤醒生产者提醒生产
		notifyAll();
		// 先 -- 再取出,因为最后一个元素下标为个数-1 , 而 cnt是个数,所以要先--
		cnt--;
		char ch = data[cnt];
		System.out.println("消费了 : " + ch + " , 容器中剩余 " + cnt + " 个字符");
		return ch;
	}
}

线程池
线程池的作用 :

  • 线程池作用就是限制系统中执行线程的数量

  • 根据系统的环境情况,可以自动或者手动来设置线程数量,以达到运行的最佳效果

  • 少了浪费系统资源,多了造成系统拥挤效率不高

  • 用线程池控制线程数量,其他线程排队等候

  • 一个任务 执行完成,再从队列中取最前面的任务开始执行

  • 如果队列中没有等待进程,线程池的这个资源处于等待状态

  • 当一个新任务需要运行时,如果此时线程池中还有等待中的工作线程时,可以直接开始运行

  • 否则需要进入等待队列

  • 为什么要使用线程池

  • 1 减少了创建 和销毁线程的次数,因为每个工作线程都可以被重复使用,可执行多个任务

  • 2 可以根据系统的承受能力,调整线程池中的线程数量,防止因为消耗过多的内存,导致服务器死机

  • (每个线程需要大概1MB内存,线程开的越多,消耗内存越大,最后导致死机)

下面是代码实现:

public class Newcachedthreadpooltest {

	public static void main(String[] args) {
		// 创建一个可缓存线程池,如果线程池长度超过处理需要,可以灵活回收空闲线程
		// 若没有可以回收的,则新建线程,线程池规模不存在限制,数量不固定

		// 创建池子
		ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
		//线程池启动10个线程
		for (int i = 0; i < 10; i++) {
			// 执行任务,传入了Runnable的匿名内部类,也可以是实现类对象
			cachedThreadPool.execute(new Runnable() {
				@Override
				public void run() {
						try {
							//线程睡眠1秒
							Thread.sleep(1000);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
						//输出当前线程的名字
						System.out.println(Thread.currentThread().getName());
				}
			});
		}
		System.out.println("----------");
		// 关闭线程池
		cachedThreadPool.shutdown();
	}

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值