18.线程安全

线程安全

成因:多个线程同时操作同一个数据 并且其中有数据的修改更新 大概率会出现数据异常问题

(数据在内存中修改更新的速度 慢于CPU在各个线程间切换的速度)

结果:出现重复数据 出现并不存在的数据

解决方案一:

​ 添加同步代码块 将线程更新数据的代码保护起来 没有执行完毕前 其他线程无法进入

​ 效果:某一个线程未执行完毕同步代码块内部代码之前 其他线程一直处于就绪状态 且获取不到CPU

​ 实现逻辑:

​ 1.某个线程获取CPU时间片段后 进入同步代码块 同时 会获得一把对象锁(标记) 带着锁进入代码块

​ 2.期间 其他线程获取CPU时间片段后 也预备进入代码块 发现锁不存在 无法进入

​ 3.线程执行完必代码块 退出时 会将锁归还 同时进入就绪状态 与其他就绪状态的线程一起 再次开始争抢CPU时间片段 直到有一个线程获取锁对象进入代码块

案例三:两人同时买100张票 显示第几个人买到第几张票

		Runnable r = new Runnable() {
			Object o = new Object();//所有线程共享的一把对象锁
			int count = 100;		//所有线程共享的票

			@Override
			public void run() {
				// TODO Auto-generated method stub
				String name = Thread.currentThread().getName();

				while (true) {				//通过循环控制线程结束与否
					try {
						Thread.sleep(200);	//休眠增加线程的交叉
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					synchronized (o) {		//同步代码块 保护数据的更新
						if(count>0) {		//预备买票前 判断是否还有余票
							System.out.println(name + "获取第" + (101 - count) + "张票");
							count--;
						}else {
							break;			//退出大循环 结束当前线程任务
						}
					}						//退出同步代码块 归还锁对象 进入就绪状态
				}
			}
		};
		new Thread(r, "张三").start();
		new Thread(r, "李四").start();

练习:

​ 四个线程 int j =0 两个对变量j进行+1操作 两个对j进行-1操作 展示每个线程操作后变量j的值

对象锁

​ 所有线程【共用一把】对象锁 一般可以使用this 但因为所代表的的本类对象内的资源过多 可能过大 因此可声明Object o = new Object作为对象锁

解决方案二

​ 同步方法 public synchronized 返回值 方法名(参数列表){将要同步的代码块放入其中} run中调用该方法

public class Nums implements Runnable {

	int j =0;
	Object o = new Object();
	@Override
	public void run() {
		// TODO Auto-generated method stub
		show();//是一个同步方法 只要有线程在内部执行 其他线程就无法进入
	}
	
	public synchronized void show() {
		String name = Thread.currentThread().getName();
		if(name.equals("1")||name.equals("2")) {
			j++;
		}else {
			j--;
		}
		System.out.println(name+":"+j);
	}
}
解决方案三

方法中的同步代码块

	//普通方法中				//run方法调用这个普通方法 线程遇到同步代码块 就会判断是否有锁
	public void show() {
		String name = Thread.currentThread().getName();
		//放入同步代码块
		synchronized (o) {
			if(name.equals("1")||name.equals("2")) {
				j++;
			}else {
				j--;
			}
			System.out.println(name+":"+j);
		}
	}
解决方案四

​ 同步对象锁 Lock

​ Lock l = new ReentrantLock();//创建Lock锁对象 提供lock() unlock()

public class Nums implements Runnable {
	int j =0;
	Lock l = new ReentrantLock();//创建Lock锁对象
	@Override
	public void run() {
		// TODO Auto-generated method stub
		String name = Thread.currentThread().getName();
		
		l.lock();//上锁
		if(name.equals("1")||name.equals("2")) {
			j++;
		}else {
			j--;
		}
		System.out.println(name+":"+j);
		l.unlock();//解锁
	}
}
死锁

​ 两个线程分别拿到了对方继续执行所必须的对象锁 导致两个线程都无法继续执行(也不会主动释放自己的锁)

​ 死锁条件:

​ 1.互斥原则:一个资源(锁对象)只能被一个线程对象锁使用

​ 2.请求与保持:一个线程因请求资源而阻塞时 另一个线程保持资源不释放

​ 3.不剥夺原则:线程已经获取的资源 再未使用完毕前 不可被强制剥夺

​ 4.循环等待:若干线程之间 形成一种首尾相接 互相等待资源的关系

	Object o1 = new Object();
	Object o2 = new Object();//两个锁对象
	@Override
	public void run() {
		// TODO Auto-generated method stub

		synchronized (o1) {
			System.out.println(Thread.currentThread().getName()+"进入模块一");
			synchronized (o2) {
				System.out.println(Thread.currentThread().getName()+"获取锁2");
			}
		}
		
		synchronized (o2) {
			System.out.println(Thread.currentThread().getName()+"进入模块二");
			synchronized (o1) {
				System.out.println(Thread.currentThread().getName()+"获取锁1");
			}
		}
		
	}
生命周期

​ 1.新生状态:通过new Thread() 此时已经有对应的栈空间和资源 但是还没有开始执行

​ 2.就绪状态:通过.start() 启动线程 一切准备就绪 只需要获取CPU时间片段 即可执行

​ 3.运行状态:获取到了CPU时间片段 开始执行线程所绑定的任务

​ 4.阻塞状态:正在执行的线程 遇到特殊情况被挂起 让出CPU使用权限 一旦阻塞结束 重新回到就绪状态

​ 5.死亡状态:通过利用结束循环或者判断 条件为假 来结束线程任务

线程常用方法

​ 1.休眠方法 .sleep(long 毫秒)

​ 注意:

​ 1.主动让出CPU 设定时间后 重新进入就绪状态

​ 2.抱着锁睡觉 如果休眠代码处于同步代码块内部 则只是让出CPU 不会释放所对象

​ 3.需要抛出一个InterruptedException编译期异常

​ 2.线程打断 当前线程对象.interrupt()

​ 注意:不会终止线程任务的执行 可以提前结束休眠状态 继续向下执行

​ 3.线程礼让 .yield() 当前线程让出CPU 增加其他线程执行的几率 但是不绝对

​ 4.强制执行 .join() 强制执行完毕加入的线程任务后 再继续执行原始线程任务

		//获取主线程对象
		Thread t1 = Thread.currentThread();
		new Thread(new Runnable() {
			@Override
			public void run() {
				// TODO Auto-generated method stub
				for(int i = 'a'; i<='z';i++) {
					System.out.println((char)i);
					if((char)i=='h') {
						//让出CPU
						//Thread.yield();
						try {
							t1.join();
						} catch (InterruptedException e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
					}
				}
			}
		}).start();
		
		Thread.sleep(3000);
		System.out.println("主线程优先");

​ 5.线程通信 等待唤醒

​ 案例:

​ 消费者 顾客线程 要购买包子 等待包子制作 生产者 老板线程 制作包子 并唤醒顾客来吃包子

​ 锁对象.wait() //当前线程执行等待 让出CPU 释放锁对象

​ 锁对象.notify() //唤醒当前锁对象标记的等待线程

​ 锁对象.notifyAll() //唤醒当前锁对象标记的所有等待线程

		Object o = new Object();//消费者和生产者 使用同一个锁对象
		
		//消费者任务
		Runnable r1 = new Runnable() {
			@Override
			public void run() {
				// TODO Auto-generated method stub
				synchronized (o) {
					System.out.println("老板 来笼包子");
					try {
						o.wait();//通过锁对象 标记处于等待的线程
						//当前线程 让出CPU 让出锁对象
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					//唤醒后 从等待位置向下执行
					System.out.println("包子真好吃");
				}
			}
		};
		//消费者任务
		Runnable r2 = new Runnable() {
			@Override
			public void run() {
				// TODO Auto-generated method stub
				synchronized (o) {
					System.out.println("老板 要三笼包子");
					try {
						o.wait();//通过锁对象 标记处于等待的线程
						//当前线程 让出CPU 让出锁对象
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					//唤醒后 从等待位置向下执行
					System.out.println("包子打包 走咯");
				}
			}
		};
		//三个消费者开启线程 
		new Thread(r1).start();
		new Thread(r2).start();
		new Thread(r1).start();
		
		Thread.sleep(1000);//控制老板线程最后开启
		//生产者任务设计并绑定线程 开启
		new Thread(new Runnable() {
			@Override
			public void run() {
				// TODO Auto-generated method stub
				synchronized (o) {
					System.out.println("5秒后 包子出笼");
					try {
						Thread.sleep(2000);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					//o.notify();//唤醒一个线程
					o.notifyAll();//唤醒当前锁对象标记的所有等待线程
					System.out.println("包子好了 过来吃");
				}//生产者释放锁对象 消费者才能获取锁对象
			}
		}).start();

​ 注意:

​ 1.至少两个线程 实现线程通信

​ 2.两个线程需要使用【同一个】对象锁

​ 3.唤醒后从等待位置 向下执行

​ 4.多个等待线程 唤醒顺序与等待顺序相反

​ 5.等待过程中 会让出CPU 释放锁对象

练习:

​ 员工A到了 开始喝茶等待 员工B到了 开始聊天等待 老板到了 开始上菜 开餐 A开动了 B开动了

休眠与等待

​ 1.所属的类不同 休眠方法 属于 Thread类 等待方法 属于Object类

​ 2.休眠尽量不在同步代码块中 等待必须存在锁对象

​ 3.休眠会让出CPU 不释放锁对象 等待会让出CPU 释放锁对象

​ 4.休眠时间一到自动进入就绪状态 等待线程只有被唤醒后进入就绪状态

同步屏障

​ 达到要求的线程数量前 所有线程会在标记节点等待 直到数量达到 被唤醒 开启下一步任务

​ 案例:五个人到达后 才能开始吃饭

​ CyclicBarrier对象.await() 当前线程等待 并且将等待线程的数量更新

​ CyclicBarrier对象.getNumberWaiting() 获取对象中已有线程数量

		//设定同步屏障 可以设定最少线程数量 达标后执行的任务(可要可不要 一定在达标后第一时间执行)
		CyclicBarrier cb = new CyclicBarrier(5, new Runnable() {
			@Override
			public void run() {
				// TODO Auto-generated method stub
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				System.out.println("人到齐了 合张影");
			}
		});

		//等待线程的任务
		Runnable r = new Runnable() {
			Random random = new Random();//可以作为对象锁
			@Override
			public void run() {
				// TODO Auto-generated method stub
				String name = Thread.currentThread().getName();
				try {
					Thread.sleep(random.nextInt(2000));
				} catch (InterruptedException e1) {
					// TODO Auto-generated catch block
					e1.printStackTrace();
				}
				synchronized (random) {
					System.out.print(name+"到达现场");
					System.out.println("还差"+(4-cb.getNumberWaiting())+"人");
				}
				try {
					cb.await();//当前线程开始等待 此时将当前线程数量更新
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				} catch (BrokenBarrierException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
                //唤醒后执行的代码
				System.out.println(name+"开始吃饭");
			}
		};

		//开启五个线程 绑定唯一的任务
		for(int i =0;i<5;i++) {
			new Thread(r,(i+1)+"号").start();
		}
信号量

​ 设定同时访问当前任务的最大线程数量 超出的线程不能绑定任务

​ 控制流量 可以类似参考 线程数量固定的线程池

​ 案例:两个窗口办理业务 显示进入和离开

		//设定该任务最多线程数量
		Semaphore s = new Semaphore(2);
		
		Runnable r = new Runnable() {
			Random random = new Random();
			@Override
			public void run() {
				// TODO Auto-generated method stub
				try {
					s.acquire();//判断当前任务中是否已经有足够的线程
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				
				String name = Thread.currentThread().getName();
				System.out.println(name+"进入了窗口");
				try {
					Thread.sleep(random.nextInt(2000));
				} catch (InterruptedException e1) {
					// TODO Auto-generated catch block
					e1.printStackTrace();
				}
				System.out.println(name+"离开了窗口");
				
				s.release();//更新对象中线程的数量
			}
		};
		
		for(int i=0;i<10;i++) {
            //创建了十个线程对象
			new Thread(r,(i+1)+"号").start();
		}
	//如果采用线程池解决这个道题 直接设定线程池中 只有两个线程
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值