java之多线程的加锁方式,死锁和线程停止(包含interrupt方法对wait,sleep方法的作用)

一.加锁方式

1.解决需求的步骤:
需求:100张票,利用多线程进行卖票,方式有:窗口, 黄牛 ,网购。

  • 多线程访问共享资源的访问
  • 1.尝试写出需求。
  • 2.分析出现的问题。
    • 三个线程同时执行run方法,CPU执行资源随机分配。
    • 线程在执行方法过程中,随时能进入受阻塞状态。
    • 所以可以使用:假设线程停止的位置,来分析问题(找极限位置)。
  • 3.想解决方案:
    • 一个线程执行完买票操作,另一个线程才能买票。
    • 这样来保证共享数据的安全。
  • 4.找出解决方法:
    • 方式一:同步代码块(同步锁)
    • 当线程进入同步代码块,会把锁拿走,执行代码块中的代码。
    • 当代码执行完毕后,会把锁还回去。
    • 如果线程遇到同步代码块,发现没有锁,将进入等待(有锁才能进)。
  • 在找问题时,可以使用多线程的睡眠方法,放大问题。
    • 写法:
synchronized(锁){
	   	上锁的代码
 	  }
  • 锁注意:
    • 所有线程使用的都是同一把锁
    • 锁可以使用任意一个对象(同一个对象就可以)
    • 让线程让出CPU的执行资源的方法(增加让出的几率)[需要放在同步代码块外]
Thread.yield();

完成卖票需求的代码:(使用同步代码块完成)

public class Kll {
	public static void main(String[] args) {
		// 利用接口实现类 创建三个线程出来
		Tickets tickets = new Tickets();
		// 创建三个线程,并写出线程名字
		Thread t1 = new Thread(tickets, "黄牛");
		Thread t2 = new Thread(tickets, "窗口");
		Thread t3 = new Thread(tickets, "网购");
		// 开启线程
		t1.start();
		t2.start();
		t3.start();
	}

}

// 利用接口方法来保证 访问的共享资源
class Tickets implements Runnable {
	// 声明票的总数
	private int tickets = 50;
	// 声明锁对象(保证锁也是线程共享的即唯一性)
	private final Object obj = new Object();

	@Override
	public void run() {
		// 利用循环,保证票都能卖出去
		while (true) {
			synchronized (obj) {
				// 休眠(放大问题)
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}

				// 判断票
				if (tickets > 0) {
					// 可以卖
					System.out.println(Thread.currentThread().getName() + "--" + tickets);
					// 卖票
					tickets--;
				} else {
					// 卖完了
					break;
				}
			}
			// 让线程让出CPU的执行资源(增加让出的几率)[需要放在同步代码块外]
			Thread.yield();
		}
	}
}
  • 方式二:同步方法
    • 封装一个方法
    • 使用同步方法
    • 写法:在方法上 使用synchronized关键词修饰方法
    • 同步方法有没有锁?是什么?为什么?
    • 有,原理和同步代码块一样
    • 成员方法,使用的对象锁是this
    • 同步方法能不能是静态的?可以(成员变量也要是静态的)
    • 如果可以,那么静态的同步方法用的锁是this吗?
    • 不是this(因为静态方法不能用this)
    • 静态方法,使用的锁的是 类锁(本类.class)
public class Kll {
	public static void main(String[] args) {
		// 利用接口实现类 创建三个线程出来
		Ticket tickets = new Ticket();
		// 创建三个线程
		Thread t1 = new Thread(tickets, "黄牛");
		Thread t2 = new Thread(tickets, "窗口");
		Thread t3 = new Thread(tickets, "网购");
		// 开启线程
		t1.start();
		t2.start();
		t3.start();
	}

}

// 利用接口方法来保证 访问的共享资源

class Ticket implements Runnable {
	// 声明票的总数
	private static int tickets = 50;
	// 声明锁对象(保证锁也是线程共享的即唯一性)
	private final Object obj = new Object();

	@Override
	public void run() {
		// 利用循环,保证票都能卖出去
		while (true) {
			
			if (sellTickets()) {
				break;
			}
		
			// 让线程让出CPU的执行资源(增加让出的几率)[需要放在同步代码块外]
			Thread.yield();
		}
	}

	public static synchronized boolean sellTickets() {
		if (tickets > 0) {
			System.out.println(Thread.currentThread().getName() + "--" + tickets);
			tickets--;
			return false;
		}else {
			return true;
		}
	}
}

方式三:利用锁的接口Lock

  • jdk1.5 锁的接口 Lock
  • lock();加锁的方法
  • unlock();释放锁方法
  • 保证出现异常时也能把锁关闭(释放了)finally
  • 写法:
      lock();
      try{
          加锁的代码
      }finally{
      	   释放锁
      unlock(); 
       }

案例代码:

public class Kll {
	public static void main(String[] args) {
		// 利用接口实现类 创建三个线程出来
		Tickets1 tickets = new Tickets1();
		// 创建三个线程
		Thread t1 = new Thread(tickets, "黄牛");
		Thread t2 = new Thread(tickets, "窗口");
		Thread t3 = new Thread(tickets, "网购");
		// 开启线程
		t1.start();
		t2.start();
		t3.start();
	}

}

// 利用接口方法来保证 访问的共享资源

class Tickets1 implements Runnable {
	// 声明票的总数
	private int tickets = 50;
	// 声明锁对象(保证锁也是线程共享的即唯一性)
	private final Object obj = new Object();
	// 声明lock锁 
	// 参数 true 可以尽量让线程公平进入锁
	private final ReentrantLock lock = new ReentrantLock(true);

	@Override
	public void run() {
		// 利用循环,保证票都能卖出去
		while (true) {
			// 使用lock锁
			lock.lock();
			try {
				// 判断票
				if (tickets > 0) {
					// 可以卖
					System.out.println(Thread.currentThread().getName() + "--" + tickets);
					// 卖票
					tickets--;
				} else {
					// 卖完了
					break;
				}
			} finally {
				lock.unlock();
			}
		// 让线程让出CPU的执行资源(增加让出的几率)[需要放在同步代码块外]
		Thread.yield();
		}
	}
}

需求:公司年会,进入公司有两个门(前门和后门)。进门的时候,每位人都能获取一张彩票(7位数),公司有100个员工。利用多线程模拟进门过程,统计每个入口入场的人数 ,每个人拿到的彩票的号码( 要求7位数字 7个数不能重复)。
打印格式:
编号为: 1 的员工 从后门 入场! 拿到的双色球彩票号码是: [17, 24, 29, 30, 31, 32, 07]
编号为: 2 的员工 从后门 入场! 拿到的双色球彩票号码是: [06, 11, 14, 22, 29, 32, 15]
……
从后门 入场的员工总共: 45 位员工
从前门 入场的员工总共: 55 位员工
保证总人数100即可
案例代码:

import java.util.ArrayList;

public class Kll {
	public static void main(String[] args) {
		// 创建表示前后门的线程
		Person person = new Person();
		Thread t1 = new Thread(person, "前门");
		Thread t2 = new Thread(person, "后门");
		// 开启线程
		t1.start();
		t2.start();
	}

}


class Person implements Runnable{
	// 声明总人数
	private int sum = 100;
	// 声明记录前后门人数
	private int frontNum = 0;
	private int backNum = 0;
	
	// 打印彩票的方法
	public ArrayList<Integer> lottery() {
		// 随机7个数[1,100]放入ArrayList
		ArrayList<Integer> list = new ArrayList<>();
		while (list.size() < 7) {
			int random = (int)(Math.random() * 100 + 1);
			// 判断是否重复
			if (!list.contains(random)) {
				list.add(random);
			}
		}
		return list;
	}

	@Override
	public void run() {
		
		// 循环,保证所有人都进来
		while (true) {
			// -----
			synchronized (this) {
				// 接收集合
				ArrayList<Integer> list = lottery();
				// 获取当前线程的名字
				String name = Thread.currentThread().getName();
				// 循环停止条件
				// 判断人数
				if (sum <= 0) {
					break;
				} else {
					// 判断从哪儿进来的
					if (name.equals("前门")) {
						frontNum++;
						System.out.println("编号为:" + (101 - sum) + " 的员工 从" + name +  " 入场! " + "拿到的双色球彩票号码是: " + list);
						sum--;
					}else if (name.equals("后门")) {
						backNum++;
						System.out.println("编号为:" + (101 - sum) + " 的员工 从" + name +  " 入场! " + "拿到的双色球彩票号码是: " + list);
						sum--;
					}
				}
				if (sum <= 0) {
					System.out.println("从前门 入场的员工总共: "+ frontNum +" 位员工");
					System.out.println("从后门 入场的员工总共: "+ backNum +" 位员工");
				}
			}
			// 让出资源
			Thread.yield();
		}
	}
}
为什么不把最后的if(sum == 0)判断和循环结束的条件放在一起?
因为两个线程都在synchronized (this)同步代码块外,循环内等着结束。
1.如果结束条件和最后的if判断放在一起,那么,只有,一个线程会结束,此时,sum刚好等于0,另外一个线程进入执行完if(sum == 0)之前的语句之后,sum=-1,就永远结束不了程序,进入死循环。同理,若把判断条件sum=0改为sum<=0,那么最另外一个线程执行完if(sum<=0)之前的语句,已经使人数多了1个,比需求多了一个人。
2.若是把最后的判断条件和最开始的结束条件合二为一放在最开始,那么两个线程都会走一遍这个结束语句,就会把最终的(从后门 入场的员工总共: 45 位员工 ; 从前门 入场的员工总共: 55 位员工)结果各打印两遍

二.死锁

前提:

  • 1.至少两个线程。
  • 2.锁的嵌套(同步代码块的嵌套)。
    • 线程1和线程2同时访问有嵌套的同步代码块,且有两个锁A和B:
    • 线程1拿到了A 向进入下一个代码块需要B锁。
    • 线程2拿到了B 向进入下一个代码块需要A锁。
    • 这时谁也进不去 线程进入相互等待的状态 导致程序卡主。
      一定几率出现死锁,,出现时的图如下:在这里插入图片描述

简单死锁出现的代码:

public class Kll {
	public static void main(String[] args) {
		DieLock dieLock = new DieLock();
		Thread t1 = new Thread(dieLock);
		Thread t2 = new Thread(dieLock);
		t1.start();
		t2.start();
		
	}
}
// 声明锁
class LockA {
	// 私有化构造方法
	private LockA() {
	}
	// 创建锁对象(声明一个常量)
	public static final LockA A = new LockA();
}
class LockB {
	// 私有化构造方法
	private LockB() {
	}
	// 创建锁对象(声明一个常量)
	public static final LockB B = new LockB();
}
// 线程
class DieLock implements Runnable{
	// 利用标记控制先A-->B或先B-->
	boolean isTrue = false;
	@Override
	public void run() {
		// 利用死循环 增加死锁几率
		while (true) {
			// 不断让两个线程先进A锁再进B锁
			// 下一次从B锁进A锁
			if (!isTrue) {
				synchronized (LockA.A) {
					System.out.println(Thread.currentThread().getName() + " if中A锁");
					synchronized (LockB.B) {
						System.out.println(Thread.currentThread().getName() + " if中B锁");
					}
				}
			}else {
				synchronized (LockB.B) {
					System.out.println(Thread.currentThread().getName() + " else中B锁");
					synchronized (LockA.A) {
						System.out.println(Thread.currentThread().getName() + " else中A锁");
					}
				}
			}
			// 改变一下标记
			isTrue = !isTrue;
		}
	}
}

三.线程停止

如何让线程停止

  • 调用stop()方法 已过时,不推荐
  • interrupt() 不能中断线程
  • 其作用:
    • 1.改变中断状态(就是个布尔值,初值false–>true)
    • 2.当你这个线程中使用了sleep,wait或join方法时,会抛出异常InterruptedException
      • 中断状态将被清除 这时interrupted()的值还是false
      • 中断状态被清除指的是从休眠状态转为运行状态或者受阻塞状态
  • 如何停止线程?
    • 正确方式:使用标记停止线程

方式一:在线程内没有sleep,wait,或join方法出现时,用interrupt()方法改变线程状态中断。
代码如下:

public class Kll {
	public static void main(String[] args) throws InterruptedException {
		INter1 iNter = new INter1();
		Thread t1 = new Thread(iNter);
		t1.start();
		// 休眠几秒 给线程运行时间
		t1.sleep(1000);
		// 中断线程
		t1.interrupt();
		// 利用标记停止线程
		//iNter.isTrue = true;
		System.out.println("线程中断");
		// 让主线程运行一会儿
		Thread.sleep(1000);
		System.out.println("主线程结束");
	}
}

class INter1 implements Runnable{
	@Override
	public void run() {
		// 打印中断状态
		System.out.println(Thread.currentThread().isInterrupted());
		// 为真时循环
		// 默认的中断状态是false
		while (!Thread.currentThread().isInterrupted()) {
			System.out.println(Thread.currentThread().getName() + "run方法");
		}
	}
}

方法二:用标记法

  • 一.线程内有sleep方法时(同时测试interrupt方法对sleep方法的作用)
    代码如下:
public class Kll {
		public static void main(String[] args) throws InterruptedException {
			INter1 iNter = new INter1();
			Thread t1 = new Thread(iNter);
			t1.start();
			// 休眠几秒 给线程运行时间
			t1.sleep(1000);
			// 中断线程
			// 利用标记停止线程
			iNter.isTrue = true;
			System.out.println("线程中断");
			// 让主线程运行一会儿
			Thread.sleep(1000);
			System.out.println("主线程结束");
			
		}
	}

	class INter1 implements Runnable{
		// 声明标记 控制线程的停止
		public boolean isTrue = false;

		@Override
		public void run() {
			// 打印中断状态
			System.out.println(Thread.currentThread().isInterrupted());
			while (!isTrue) {
				// 线程休眠
				try {
					// 中断异常 InterruptedException
					// 中断状态被清除指的是
					// 从休眠状态-->运行状态(受阻塞状态)
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName() + " run方法");
			}
		}
	}
  • 二.测试interrupt方法对wait作用
  • 注意:wait方法需要锁对象调用
    代码如下:
public class Kll {
	public static void main(String[] args) throws InterruptedException {
		WaitRunnable runnable = new WaitRunnable();
		Thread t1 = new Thread(runnable);
		t1.start();
		
		//Thread.sleep(1000);
		for (int i = 0; i < 10; i++) {
			if (i == 5) {
				// interrupt 方法
				//t1.interrupt();
				// 让线程停止
				runnable.isTrue = true;
			}
		}
		Thread.sleep(1000);
		System.out.println("主线程停止");
	}

}

class WaitRunnable implements Runnable{
	// 生声明标记停止线程
	public boolean isTrue = false;

	@Override
	public void run() {
		System.out.println(Thread.interrupted());
		synchronized (this) {
			while (!isTrue) {
	
				// 使用线程等待
				try {
					// wait方法需要锁对象调用
					// InterruptedException抛出一个异常
					// 将线程从等待状态转换为-->运行状态(或受阻塞状态)  清除了原有状态
					this.wait();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName());
			}
		}
		
	}
	
}

注意:wait方法让线程进入中断状态被清除(指的是从休眠状态转为运行状态或者受阻塞状态)。

  • 在主线程内使用sleep方法会线程的这种中断状态被清除的效果更明显。

主线程立即接收到子线程修改状态的方法:
从子线程中修改状态,查看主线程中是否能够立即接收到

  • 不能,解决方法是:
    • 当从子线程中修改状态时
    • 主线程不能立即接收到这个状态的改变
    • 使用关键词 volatile 来标识你改变的状态的变量
    • 效果:可以让主线程立即接收到改变的值
public class Kll {
	public static void main(String[] args) {
		ChangeState state = new ChangeState();
		Thread t1 = new Thread(state);
		t1.start();
		
		// 利用线程标记 卡住主线程
		while (!state.isTrue) {
		}
		System.out.println(Thread.currentThread().getName() + "main结束" + state.isTrue);
	}
}

class ChangeState implements Runnable{
	// 标记线程状态
	public volatile boolean isTrue = false;
	// 记录循环次数
	public int num = 0;

	@Override
	public void run() {
		while (!isTrue) {
			num++;
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			if (num == 5) {
				// 修改状态
				isTrue = true;
			}
			System.out.println(Thread.currentThread().getName() + " " + num);
		}
	}
}

猜你喜欢

转载自blog.csdn.net/KongLingLei_08225/article/details/82778340