多线程、线程安全

定义

  • 一个进程可以启用多个线程。火车站购票是一个进程,飞机购票是一个进程,两个进程独立;在火车站购票大厅里面,有若干个窗口,可供旅客同时购票,购票窗口是线程,多线程并发购票。
    • 进程是一个应用程序(一个进程是一个软件),进程之间内存独立不共享
    • 线程是一个进程中的执行场景/执行单元,在java语言中,对于线程A、线程B来说,堆内存和方法区内存共享,但是栈内存独立,一个线程一个栈。
在Dos窗口输入java HelloWorld回车之后
	会先启用JVM,而JVM就是一个进程
	JVM
		启动一个主线程调用main方法
		启动一个垃圾回收线程看护、回收垃圾
	

在这里插入图片描述

  • 对于单核的CPU来说,真的可以做到真正的多线程并发吗?
    • 真正的多线程并发:线程A执行线程A的工作,线程B执行线程B的事情,互不干扰
    • 对于多核CPU来说,相当于同一时间点上有多个大脑一齐工作,可以真正的实现线程的并发执行。
    • 单核CPU相当于只有一个大脑,不能做到真正的并发执行,但是宏观上可以做到“多线程并发”的感觉。对于单核CPU来说,某一个时间结点上只能处理一件事情,但是由于CPU的处理速度极快,多个线程之间频繁切换执行,从宏观上来看:给人感觉是多个事情同时在做。

实现多线程:创建分支栈

  • java支持多线程,并且java已经实现了多线程,程序员只需要继承就可以
  • java语言中实现线程有两种方式
    • 编写一个类,直接继承java.lang.Thread,重写run方法
    • 编写一个类,实现java.lang.Runnable接口
  • 代码框架
    • 新建类实现接口/继承方法
    • 创建线程
    • 启动线程

方法一 extends Thread

  • 编写一个类,直接继承java.lang.Thread,重写run方法
    在这里插入图片描述
package Advance.thread;
/**
 * @author 衣鱼
 * 		java实现多线程的第一个方法
 *	 编写一个类,直接继承java.lang.Thread,重写run方法
 *
 *	新建类之后,重写了run方法  ——创建线程对象(new)——启动线程(start方法)
 */
public class ThreadTest01 {
	public static void main(String[] args) {
		//新建一个分支线程对象
		MyThread mytread = new MyThread();
		
		//启动线程
		mytread.start();
		
		//下面的for循环,依旧是在main方法中,即在主栈中执行
		for(int i =0;i<100;i++) {
			System.out.println("主线程执行——"+i);
		}	
	}
}

class MyThread extends Thread{
	@Override
	public void run() {
		//重写run方法,这段程序运行在分支栈中
		for(int i =0;i<100;i++) {
			System.out.println("分支线程——"+i);
		}
	}
}

方法二 implements Runnable

  • 编写一个类,实现java.lang.Runnable接口
package Advance.thread;

/**
 * @author 衣鱼
 *	实现线程的第二种方式
 *		编写一个类,实现java.lang.Runnable接口
 */
public class ThreadTest02 {
	public static void main(String[] args) {
		//创建一个可运行对象
		MyRunnable mr = new MyRunnable();
		
		//将可运行对象封装成一个线程
		//Thread(Runnable target) 分配新的线程对象
		Thread r = new Thread(mr);
		
		//启动线程
		r.start();
		
		for(int i =0;i<100;i++) {
			System.out.println("主线程执行——"+i);
		}	
	}
}

//MyRunnable 不是一个线程类,是一个可运行对象,不是一个线程
//所以不能和方法一一样利用MyRunnable创建线程
class MyRunnable implements Runnable{
	public void run() {
		// TODO Auto-generated method stub
		for(int i =0;i<100;i++) {
			System.out.println("分支线程——"+i);
		}
	}
}

方法三 Callable

  • 实现Callable接口 JDK8的新特性
  • 这个方法可以获取线程的返回值
  • 上述的两种方法都无法获取线程的返回值(run返回void类型)
    在这里插入图片描述
package Advance.thread;

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class ThreadTest09{
	public static void main(String[] args) throws Exception{
		//创建一个“未来任务类”
		FutureTask task = new FutureTask(new Callable() {
			//内部类方式
			//call() 相当于run方法
			public Object call() throws Exception{
				System.out.println("开始");
				Thread.sleep(1000*3);
				System.out.println("结束");
				return 100;
			}
		});
		
		//创建线程对象
		Thread t = new Thread(task);
		
		//启动
		t.start();
		
		//此时程序执行是主方法,如何在主方法中得到线程t的返回值
		Object o =task.get();			//注意该处的get是main线程获取t线程的返回值
													//如果t线程执行失败,那么就get不到值,main阻塞。
		
	}
}

线程名

  • 获取线程名字:线程对象.getName()
  • 设置线程名字:线程对象.setName(“新名字”)
  • 当线程对象采用默认名字时,默认名字的规律:Thread-0 Thread-1
    在这里插入图片描述
获取当前线程
  • Thread t = Thread.currentThread();
package Advance.thread;

/**
 * @author 衣鱼
 *		获取当前线程
 *		Thread t = Thread.currentThread();
 */
public class ThreadTest03 {
	public static void main(String[] args) {
		
		//获取当前线程 :main主线程
		Thread t = Thread.currentThread();
		System.out.println(t.getName());		//main线程的名字
		
		//新建一个分支线程对象
		MyThread2 t1= new MyThread2();
		t1.setName("t1");
		
		MyThread2 t2= new MyThread2();
		t2.setName("t2");
		
		//启动线程
		t1.start();
		t2.start();
		
	}
}

class MyThread2 extends Thread{
	@Override
	public void run() {
		//重写run方法,这段程序运行在分支栈中
		Thread t = Thread.currentThread();
			/**
			 * Thread.currentThread()获取的是当前线程
			 * t1线程执行run方法,当前线程就是t1
			 * t2线程执行run方法,当前线程就是t2
			 * */
		
		for(int i =0;i<100;i++) {
			System.out.println(t.getName()+"——>"+i);
		}
	}
}

生命周期

  • 新建状态
  • 就绪状态
  • 运行状态
  • 阻塞状态
  • 死亡状态
    在这里插入图片描述

线程睡眠

  • static void sleep(long millis)

    • 静态方法 :和哪个对象调用它没有关系。
      使用static 静态变量:所有对象都有这个属性,且所有对象的这个属性值一样,建议定义为静态变量,节省内存开销。static 修饰的所有元素,都是类级别特征,与具体对象无关。静态变量在类加载的时候初始化,内存在方法区中开辟,访问的时候不需要创建对象,直接使用类名.静态变量属性名,使用引用.的方式访问,也不会报错!
    • 参数是毫秒
    • 作用:让当前线程进入休眠,进入“阻塞状态”,放弃占有CPU的时间片,让给其他线程使用。
  • 间隔特定的时间,执行特定的代码

package Advance.thread;
/**
 * @author 衣鱼
 *		间隔特定的时间,执行特定的代码
 */
public class ThreadTest04 {
	public static void main(String[] args) {
		//当前线程休眠5秒  
		//main主线程休眠5秒
		try {
			Thread.sleep(1000*5);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		//5秒之后执行输出
		System.out.println("hello world!");
		
		for(int i =0;i<10;i++) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName()+"——>"+i);
		}
	}
}

  • sleep面试题
    在这里插入图片描述

叫醒线程

在这里插入图片描述

终止线程

  • 强行终止线程 线程对象.stop();
    • 缺点:容易丢失数据,因为这种方法直接将线程杀死了,线程没有保存的数据将丢失。【类似进入电脑windows系统的任务管理器结束一个程序】
  • 合理终止线程 设置run标记
    在这里插入图片描述

线程调度

  • 常见的线程调度模型
    • 抢占式调度模型:线程的优先级高,占用CPU时间片的概率高。【java采用】
    • 均分式调度模型:平均分配CPU时间片,每个线程占用时间片长度一样
  • java中提供线程调度的方法
实例方法:void setPriority(int newPriority)最低优先级为1;最高优先级为10;默认优先级为5;优先级高,抢占的CPU时间片相对多
静态方法:Static void yield() 让位方法暂停当前正在执行的线程对象,执行其他线程;yield方法不是阻塞方法,方法执行时会让当前线程从“运行状态”回到“就绪状态” 【回到就绪状态的线程,有可能再次抢到CPU】
实例方法:void join() 合并线程使当前线程进入阻塞状态,执行其他线程,当其他线程执行完毕,退出CPU后,当前线程才能继续执行【例:t.jion(); 意思是 t 线程合并到当前线程,当前线程进入阻塞状态,执行 t 线程】

在这里插入图片描述在这里插入图片描述

⭐线程安全⭐线程同步⭐

  • 在多线程并发的环境下,保证数据的安全
    • 在开发过程中,项目都是运行在服务器当中,服务器已经将线程的定义、创建、启动实现完毕。不需要额外编写
    • 在此时,程序员编写的程序放置在多线程环境下运行,对于程序员来说,更需要关注这些数据在多线程并发环境中是否安全。
  • 多线程并发存在安全问题的条件,同时满足以上三个条件之后,存在线程安全问题
    • 多线程并发
    • 共享数据
    • 共享数据有修改行为
  • 如何解决线程安全问题
    • 多线程并发环境下,共享数据被修改,存在线程安全问题。解决方法:线程同步机制
    • 线程同步机制 :线程排队执行,(不能并发)。线程同步就会牺牲一部分效率,保证数据安全为首位。
异步编程模型线程t1和线程t2各自执行各自的,谁也不需要等谁。——多线程并发。
同步编程模型线程t1和线程t2;线程t1执行时,必须等线程t2执行结束(反之亦然);两个线程之间发生了等待关系。效率较低——线程排队

synchronized

  • 线程同步机制语法
			synchronized(共享对象){
					//线程同步代码块
 			}
 		synchronized后面()小括号传的”数据“必须是线程共享的数据,才能达到多线程排队
 		假设有t1、t2、t3、t4、t5,五个线程,程序只需要t1、t3、t4三个线程进行同步处理,t4、t5不需要排队。
 		那么,在()中就需要标明确线程排队的对象。
  • synchronized代码执行原理

    • 在java语言中,每个对象都对应一把锁。如果没有synchronized关键词修饰,就不需要找锁,直接执行
    • 假设t1、t2线程并发,开始执行synchronized同步代码块的时候,肯定是一先一后执行的。
    • synchrnized的共享数据不能是包装类类型,i++相当于i = new Integer(i+1),i++之后的对象已经不是之前的i了。
    • 假设t1先执行,遇到synchronized,这个时候t1会找()里面的共享对象的对象锁,找到之后,并占用这把锁,直到同步代码块代码结束,这把锁才会释放。
    • 假设t1已经占用这把锁了,但是尚未执行完代码块释放锁,此时t2也遇到synchronized关键字,它也会去寻找后面的共享对象的这把锁,找到之后发现该锁被t1占有,t2只能在同步代码块外等待t1 的结束。直到t1把同步代码块执行结束之后,t1会归还这把锁,这时t2才会拿到该锁,并占用该锁,进入同步代码块执行程序。
    • 注:()内的共享对象需要谨慎选择,共享对象是线程(需要排队执行的线程)所共享的
  • synchronized于五个状态的关系在这里插入图片描述在这里插入图片描述

死锁

  • synchronized在开发中尽量不要嵌套使用,容易造成死锁!在这里插入图片描述在这里插入图片描述
package Advance.thread;

/**
 * @author 衣鱼
 *		死锁
 */
public class DeadLock {
	public static void main(String[] args) {
		Object o1 = new Object();
		Object o2 =new Object();
		
		//t1 t2 共享o1、o2
		Thread t1 = new MyThread1(o1, o2);
		Thread t2 = new MyThread10(o1, o2);
		
		t1.start();
		t2.start();
	}
}

class MyThread1 extends Thread{
	Object o1;
	Object o2;
	public MyThread1(Object o1, Object o2) {
		this.o1 = o1;
		this.o2 = o2;
	}
	public void run() {
		synchronized(o1) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			synchronized(o2) {}
		}
	}	
}

class MyThread10 extends Thread{
	Object o1;
	Object o2;
	public MyThread10(Object o1, Object o2) {
		this.o1 = o1;
		this.o2 = o2;
	}
	public void run() {
		synchronized(o2) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			synchronized(o1) {}
		}
	}
}


银行取款

  • 设置一个用户类、一个线程类、一个测试类
  • 当同一用户使用多线程取款时,如何保证存取的安全。
    在这里插入图片描述
  • 针对存取款的同步机制处理
    在这里插入图片描述在这里插入图片描述在这里插入图片描述
package Advance.thread.ThreadSafeSynchronized;
public class Account {
	//账户
	private String actno;
	//余额
	private double banlance;
	//测试synchronized
	Object obj = new Object();
	//余额方法
	public void withdraw(double money) {

		synchronized(this)		//此时的共享对象是act账户,Account类的构造方法传入了该参数
											//线程1、线程2共享的act-1用户,它们两个同步
											//如果此时有线程3针对的act-2用户,那么采用”this“关键字时线程三不需要等候线程1、2
											//线程3可以直接取得act-2对象对应的锁
											//即:只有针对同一账户的存取行为需要同步,不同账户异步
	
		//synchronized("abc");	//传入的参数为字符串,创建在字符串常量池里面,如此哪怕线程1、线程3针对的不是一个用户
												//即不是一个共享对象,此时只有一个字符串,也就是只有一把锁,所以线程1和线程3仍会同步......
												//即:一个人取款;整个银行等候
		
		//synchronized(obj)		//传入静态变量obj参数,也可以达到效果
		
		//Object partObj = new Object();
		//synchronized(partObj)	//传入该局部变量参数,执行错误,因为传入是partObj不是共享的,每一次都是新对象
		{	//取款之前余额
			double before = this.getBanlance();
			//取款之后余额
			double after = before - money;
			//更新余额
			this.setBanlance(after);
		}
	}
	
	public Account() {
		super();
	}
	public Account(String actno, double banlance) {
		super();
		this.actno = actno;
		this.banlance = banlance;
	}
	public String getActno() {
		return actno;
	}
	public void setActno(String actno) {
		this.actno = actno;
	}
	public double getBanlance() {
		return banlance;
	}
	public void setBanlance(double banlance) {
		this.banlance = banlance;
	}
	
}

变量的线程安全

  • java中的三大变量
    • 静态变量:方法区 ,可能存在线程安全问题
    • 实例变量:堆内存 , 可能存在线程安全问题
    • 局部变量:栈内存 ,局部变量不共享。不会存在线程安全问题,局部变量在栈中,一个线程一个栈
    • 常量:不可修改,不会有线程安全问题。
  • 因为使用了线程安全,所以会降低执行效率,当使用局部变量的时候,建议使用非线程安全的类型——提高效率
    • StringBulid 非线程安全
    • ArrayList 非线程安全
    • HashMap 非线程安全
    • Vector 线程安全
    • Hashtable 线程安全
      在这里插入图片描述

开发中解决线程安全问题

  • synchronized在开发中不是首先项,因为执行效率降低,用户体验感差。在不得已的情况下使用线程同步机制
  • 解决线程安全问题
    • 方案一:尽量使用局部变量代替“实例变量和静态变量”
    • 方案二:如果必须是实例变量,可以考虑创建多个对象,只有实例变量的内存不再共享。一个线程一个对象,对象不共享,就没有数据安全问题
    • 方案三:不能使用局部变量、对象不能创建多个,则使用synchronized。

守护线程

  • java语言中的线程分为两大类
    • 用户线程 : 主线程main方法是一个用户线程
    • 守护线程(后台线程) :代表性的线程—垃圾回收器线程(守护线程)
  • 守护线程的缺点
    • 守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束
  • 守护线程的使用
    • 每天定时数据备份,将定时器设置为守护线程,每到约定的时间就备份一次。所有的用户线程如果结束,守护线程自动退出,没有必要进行数据备份。
    • 假设设置一个线程,要求该线程随着main线程的结束而结束,将该线程在开始之前设置为守护线程。t.setDaemon(true);

定时器

  • 间隔特定的时间,执行特定的程序
    • 采用sleep方法 设置睡眠时间
    • java类库中已经写好一个定时器,java.until.Timer
    • 现实开发中,较多使用Spring框架的SpringTask框架,因为这个框架简单配置,就可以实现定时器功能。
      在这里插入图片描述

生产者 消费者 模式

  • 生产线程负责生产,消费线程负责消费。生产和消费要达到均衡。这样的特殊情况需要使用到wait、notify方法
  • wait和notify方法不是线程对象的方法,是java中任何java对象都有的方法,因为这两个方法都是Object类中自带的。
  • wait和notify方法建立在线程同步的基础之上,因为多线程要同时操作一个仓库,有线程安全问题。
  • Object o = new Object();
  • o.wait();
  • o.notifyAll(); 唤醒o对象上处于等待的所有线程

在这里插入图片描述

  • 注意:下图中,仓库是共享的,那么wait和notify方法是被仓库调用的!仓库的wait方法。可以让在仓库上活动的线程进入等待状态…
    在这里插入图片描述
  • 模拟需求:仓库采用List集合。List集合假设只能存储一个元素,一个元素就代表仓库存满,如果List集合元素个数是0 就代表仓库为空,保证List集合中永远最多存储一个元素
    在这里插入图片描述
package Advance.thread;

import java.util.ArrayList;
import java.util.List;

/**
 * @author 衣鱼
 *	使用wait方法和notify方法实现“生产者消费者模式”
 *		
 *	模拟需求
 *		仓库采用List集合
 *		List集合假设只能存储一个元素
 *		一个元素就代表仓库存满
 *		如果List集合元素个数是0 就代表仓库为空
 *		保证List集合中永远最多存储一个元素
 */
public class ThreadTest10 {
	public static void main(String[] args) {
		List list = new ArrayList();
		//创建线程
		Thread t1 = new Thread(new Producer(list));
		Thread t2 = new Thread(new Consumer(list));
		
		t1.setName("生产者线程");
		t2.setName("消费者线程");
		
		t1.start();
		t2.start();
	}
}

class Producer implements Runnable{
	private List list;
	public Producer(List list) {
		this.list = list;
	}
	public void run() {
		//一直生产
		while(true) {
			synchronized(list) {										//——加锁
				if(list.size()>0) {		//证明仓库满了,有产品
					try {
						list.wait();											//——放锁
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
				//仓库没有产品  生产产品
				Object o = new Object();
				list.add(o);
				System.out.println(Thread.currentThread().getName()+"——>"+o);
				//唤醒消费者消费
				list.notify();
			}	
		}
	}
}

class Consumer implements Runnable{

	private List list;
	public Consumer(List list) {
		this.list=list;
	}
	public void run() {
		//一直消费
		while(true) {
			synchronized(list) {
					if(list.size()==0) {		//仓库消费干净 没有产品
						try {
							list.wait();
						} catch (InterruptedException e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
					}
					//产品还可以被购买
					Object obj =list.remove(0);		//根据下标删除
					System.out.println(Thread.currentThread().getName()+"——>"+obj);
					//唤醒生成者生产
					list.notify();
			}
		}
	}
}


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值