一.加锁方式
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);
}
}
}