JAVA之线程问题总结

线程概述

  操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

线程特点

  1. 效率高
  2. 多个线程之间互不干扰

线程调度

  1. 分时调度
    所有的线程轮流使用CPU的使用权,平均分配每个使用CPU的时间
  2. 抢占式调度
    将所有的线程设置一个优先级。优先让优先级高的线程使用CPU的使用权,如果优先级相等,则随机调度一个线程进行使用CPU。JAVA便是使用的抢占式调度。

线程创建方法

方法一:继承Thread类

步骤:

  1. 定义一个继承了Thread类的子类
  2. 重写run方法。里面书写需要线程完成的内容
  3. 创建继承了Thread类的子类实例。即创建了线程对象
  4. 该对象调用start()方法。开启线程
实例
package thread;
public class TicketThread extends Thread{
	String name;
	public TicketThread(String name) {
		// TODO Auto-generated constructor stub
		this.name = name;
	}
	@Override
	public void run() {
		// TODO Auto-generated method stub
		for(int i = 0 ; i < 20 ; i ++) {
			System.out.println(this.name + ":" + i);
		}
	}

}

package thread;

public class Main {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		TicketThread t1 = new TicketThread("张三");
		TicketThread t2 = new TicketThread("李四");
		t1.start();
		t2.start();
	}

}

运行结果:
thread结果

方法二:实现Runnable接口

步骤

  1. 定义一个Runnable接口的实现类
  2. 重写run方法。里面书写需要线程完成的内容.
  3. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。如有多个线程创建多个Thread对象
  4. 调用指定Thread对象的start()方法来启动线程。

实例

package thread;

public class TicketRunnable implements Runnable{
	
	String name;
	
	public TicketRunnable(String name) {
		// TODO Auto-generated constructor stub
		this.name = name;
	}
	
	@Override
	public void run() {
		// TODO Auto-generated method stub
		for(int i = 0 ; i < 20 ; i ++) {
			System.out.println(this.name + ":" + i);
		}
	}

}

package thread;

public class Main {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		TicketRunnable t1 = new TicketRunnable("张三");
		TicketRunnable t2 = new TicketRunnable("李四");
		Thread th1 = new Thread(t1);
		Thread th2 = new Thread(t2);
		th1.start();
		th2.start();
	}

}

运行结果:
runnable结果

两种方法区别

  1. 在java程序中,由于是单继承多实现。因此使用实现runnable更具有优势。
  2. 如果使用继承thread就不能再继承其他类了。但如果实现runnable,则还可以继承其他类
  3. 使用实现runnable可以使耦合性降低。
  4. 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。

多线程开启内存图解

多线程内存图解

线程安全问题

  由于每一次开启线程,都会创建一个栈空间,如果我们的栈空间访问的数据含有共享数据,那么必会出现,安全问题。比如:当A线程刚好进入到了run方法,即将执行里面的代码(i–)的时候,此时的i是共享数据,突然B线程也进入到了run方法,这个时候由于是使用同一堆内存,i 还是没有改变的,他们都会对这个 i 进行时候。再者:电影院售票的时候,一共有三个窗口,他们都在卖下午3点半的误杀电影,这个时候有两个去买票,此时显示了票的剩余还有1张,两个窗口都把票出售给了用户,这样的话到时候就会出现一共座位两个人的情况,这是一种不安全的情况。具体步骤如下图所示:
线程安全问题

解决线程安全问题方法

方法一:线程同步代码块

使用格式:synchronized (锁对象) { 存放因为共享数据会引起安全问题的代码块 }

方法二:同步方法

使用步骤:

  1. 方法一
    1. 创建一个普通方法,形如:权限修饰符 synchronized 返回值 方法名(参数列表){ 存放因为共享数据会引起安全问题的代码块 }
    2. 在重写的run方法中,调用该方法
  2. 方法二
    1. 创建一个普通方法,在普通方法体中,使用:synchronized (锁对象) { 存放因为共享数据会引起安全问题的代码块 }
    2. 在重写的run方法中,调用该方法

方法三:静态同步方法

使用格式:

  1. 方法一
    1. 创建一个静态方法,形如:权限修饰符 static synchronized 返回值 方法名(参数列表){ 存放因为共享数据会引起安全问题的代码块 }
    2. 在重写的run方法中,调用该方法
  2. 方法二
    1. 创建一个静态方法,在静态方法体中,使用:synchronized (锁对象) { 存放因为共享数据会引起安全问题的代码块 }
    2. 在重写的run方法中,调用该方法

注意事项:
  由于静态方法在实例化对象生成前都已经存在,因此锁对象不能像普通方法一样取this实例化的一个对象,而是取class文件对象

样例:

	private static void x() {
		synchronized (TicketRunnable.class) {
			while (true) {
				if (ticket > 0) {
					System.out.println(name + ":" + ticket--);
				}
			}
		}		
	}

方法四:lock锁

使用步骤:

  1. 在声明成员变量位置创建一 一个ReentrantLock对象
  2. 在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁
  3. 在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁

注意事项:

  1. 一般线程开启我们都会对线程进行异常处理代码块
  2. 如果产生了异常处理块,我们可以将unlock释放锁的步骤,放入finally代码块中,以免因为程序跑飞,死机造成错误

样例:

	@Override
	public void run() {
		// TODO Auto-generated method stub

		while (true) {
			
			if (ticket > 0) {
				l.lock();
				try {
					Thread.sleep(1000);
					System.out.println(name + ":" + ticket--);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				} finally {
					l.unlock();
				}
			}

		}
		

	}

实现同步原理

同步原理

线程阻塞与唤醒

  在我们现实生活中,经常会出现一共物品处于缺货状态,我们就应该进行补货。或者有一个更重要的事情来临的时候,我们应该停下目前手中的事情,优先去完成另外一件事。这里的缺货或者停下手中的事情就相当于阻塞状态,当补货或者做完了另外一件事就相当于唤醒状态。
  最著名的例子就是:生产者与消费者问题。简称PV原语。

等待唤醒的三个方法:

  1. wait:线程不再活动,不再参与调度,进入wait set中,因此不会浪费CPU资源,也不会去竞争锁了,这时
    的线程状态即是WAITING。它还要等着别的线程执行一个特别的动作,也即是"通知( notify)”在这个对象上等待的线程从wait set中释放出来,重新进入到调度队列( ready queue )中
  2. notify:则选取所通知对象的wait set中的一一个线程释放;例如,餐馆有空位置后,等候就餐最久的顾客最先入座。
  3. notifyAll:则释放所通知对象的wait set上的全部线程。

样例:
生产者(包子铺)

package pvOperate;

public class Producer implements Runnable{
	private BaoZi bz;
	
	public Producer() {
		// TODO Auto-generated constructor stub
	}
	
	public Producer(BaoZi bz) {
		super();
		this.bz = bz;
	}



	@Override
	public void run() {
		// TODO Auto-generated method stub
		int count = 1;
		while(true) {
			synchronized (bz) {
				if(bz.isFlag() == true) {
					try {
						bz.wait();
					} catch (Exception e) {
						// TODO: handle exception
						System.out.println(e);
					}
				}
		
				if(count % 2 == 0) {
					bz.setPi("薄皮");
					bz.setXian("牛肉馅");
				}else {
					bz.setPi("厚皮");
					bz.setXian("白菜馅");
				}
				count++;
				
				System.out.println("我们作为生产者,正在加急生产。请耐心等待");
				try {
					Thread.sleep(3000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				
				System.out.println("包子已经做好了哟!今天的包子是:" + bz.getPi() + bz.getXian());
				bz.setFlag(true);
				bz.notify();
			}
		}
	}
	
}

消费者


package pvOperate;

public class Consumer implements Runnable{
	private BaoZi bz;
	
	public Consumer() {
		// TODO Auto-generated constructor stub
	}
	
	public Consumer(BaoZi bz) {
		super();
		this.bz = bz;
	}
	
	@Override
	public void run() {
		// TODO Auto-generated method stub
		while(true) {
			synchronized (bz) {
				if(bz.isFlag() == false) {
					try {
						bz.wait();
					} catch (Exception e) {
						// TODO: handle exception
						System.out.println(e);
					}
				}
				
				System.out.println("包子终于好了! "+ Thread.currentThread() +" 要准备开吃了~今天是:"+bz.getPi() + bz.getXian());
				
				bz.setFlag(false);
				System.out.println("包子已经吃完了。请包子铺生产者赶快生产~");
				System.out.println("====================================");
				bz.notify();
				
			}
		}
	}
	
	
	
}

包子类

package pvOperate;

public class BaoZi {
	
	private String pi;
	private String xian;
	private boolean flag ;

	public boolean isFlag() {
		return flag;
	}

	public void setFlag(boolean flag) {
		this.flag = flag;
	}

	public String getPi() {
		return pi;
	}

	public void setPi(String pi) {
		this.pi = pi;
	}

	public String getXian() {
		return xian;
	}

	public void setXian(String xian) {
		this.xian = xian;
	}
}

线程池

  其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

线程池好处

  1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。

使用步骤

  1. 使用线程池的工厂类Executors里边提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池
  2. 创建一个类,实现Runnable接口,重写run方法, 设置线程任务
  3. 调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法
  4. 调用ExecutorService中的方法shutdown销毁线程池(不建议执行)

样例

package thread;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		
		// 1.使用线程池的工厂类Executors里边提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池。参数是 开启线程个数。后面的线程处于等待状态。前面程序执行完毕,释放资源即可执行
		ExecutorService es = Executors.newFixedThreadPool(2);
		
		// 2.调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法
		es.submit(new TicketRunnable("张三"));
		es.submit(new TicketRunnable("李四"));
		es.submit(new TicketRunnable("王五"));
		
		// 3.销毁线程池
		es.shutdown();

	}

}


public class TicketRunnable implements Runnable {

	
	String name;

	public TicketRunnable(String name) {
		// TODO Auto-generated constructor stub
		this.name = name;
	}

	@Override
	public void run() {
		// TODO Auto-generated method stub
		

		System.out.println(name + ": 你好 ! ");


	}

}



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值